diff --git a/packages/cli/__tests__/commands/mcp.test.ts b/packages/cli/__tests__/commands/mcp.test.ts new file mode 100644 index 0000000..220302b --- /dev/null +++ b/packages/cli/__tests__/commands/mcp.test.ts @@ -0,0 +1,139 @@ +import yargs from 'yargs'; +import { buildTestJwt } from '@spotify-confidence/testing/auth'; +import { prepareAuthTokens } from '@spotify-confidence/testing/auth'; +import { mcpCommand } from '@commands/mcp.js'; +import { captureOutput } from '../helpers/capture.js'; +import { createMockIntegration } from '../helpers/mock-integration.js'; + +const mockIntegration = createMockIntegration({ + detectMcpStatuses: vi.fn().mockResolvedValue({ + 'confidence-flags': 'connected', + 'confidence-docs': 'connected', + }), +}); + +const mockAuthenticate = vi.fn().mockResolvedValue({ + accessToken: buildTestJwt({ email: 'test@example.com' }), + region: 'EU', +}); + +vi.mock('@spotify-confidence/core', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getIntegration: () => mockIntegration, + getIntegrations: () => [mockIntegration], + authenticate: (...args: unknown[]) => mockAuthenticate(...args), + }; +}); + +vi.mock('@inquirer/select', () => ({ + default: vi.fn().mockResolvedValue('claude'), +})); + +beforeEach(() => { + vi.clearAllMocks(); +}); + +function run(args: string[]) { + return yargs(args) + .option('json', { type: 'boolean', default: false }) + .option('output', { type: 'string' }) + .command(mcpCommand) + .parse(); +} + +describe('mcp list', () => { + it('outputs available servers as JSON', async () => { + using output = captureOutput(); + + await run(['mcp', 'list', '--json']); + + const parsed = JSON.parse(output.stdout); + expect(parsed.data).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'confidence-flags' }), + expect.objectContaining({ name: 'confidence-docs' }), + ]), + ); + }); + + it('outputs available servers as a table', async () => { + using output = captureOutput(); + + await run(['mcp', 'list', '--output', 'table']); + + expect(output.stdout).toContain('confidence-flags'); + expect(output.stdout).toContain('confidence-docs'); + expect(output.stdout).toContain('Server'); + }); +}); + +describe('mcp status', () => { + it('outputs server statuses as JSON', async () => { + using _auth = prepareAuthTokens('valid'); + using output = captureOutput(); + + await run(['mcp', 'status', '--json']); + + const parsed = JSON.parse(output.stdout); + expect(parsed.data).toEqual( + expect.arrayContaining([ + expect.objectContaining({ server: 'confidence-flags', status: 'Connected' }), + expect.objectContaining({ server: 'confidence-docs', status: 'Connected' }), + ]), + ); + }); +}); + +describe('mcp install', () => { + it('connects all MCP servers', async () => { + using _auth = prepareAuthTokens('valid'); + using _output = captureOutput(); + + await run(['mcp', 'install']); + + expect(mockIntegration.connectMcpServer).toHaveBeenCalledTimes(2); + expect(mockIntegration.connectMcpServer).toHaveBeenCalledWith( + expect.objectContaining({ serverName: 'confidence-flags' }), + ); + expect(mockIntegration.connectMcpServer).toHaveBeenCalledWith( + expect.objectContaining({ serverName: 'confidence-docs' }), + ); + }); + + it('reports failure to stderr when a connection fails', async () => { + using _auth = prepareAuthTokens('valid'); + using output = captureOutput(); + vi.mocked(mockIntegration.connectMcpServer).mockRejectedValueOnce( + new Error('connection refused'), + ); + + await run(['mcp', 'install']); + + expect(output.stderr).toContain('connection refused'); + }); +}); + +describe('mcp uninstall', () => { + it('disconnects all MCP servers', async () => { + using _auth = prepareAuthTokens('valid'); + using _output = captureOutput(); + + await run(['mcp', 'uninstall']); + + expect(mockIntegration.disconnectMcpServer).toHaveBeenCalledTimes(2); + }); +}); + +describe('mcp auth', () => { + it('re-authenticates and reconnects all servers', async () => { + using _auth = prepareAuthTokens('valid'); + using _output = captureOutput(); + + await run(['mcp', 'auth']); + + expect(mockAuthenticate).toHaveBeenCalled(); + expect(mockIntegration.connectMcpServer).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/cli/__tests__/commands/sdk.test.ts b/packages/cli/__tests__/commands/sdk.test.ts new file mode 100644 index 0000000..5e06e3f --- /dev/null +++ b/packages/cli/__tests__/commands/sdk.test.ts @@ -0,0 +1,202 @@ +import yargs from 'yargs'; +import { sdkCommand } from '@commands/sdk.js'; +import { captureOutput } from '../helpers/capture.js'; + +const mockCallMcpTool = vi.fn().mockResolvedValue('npm install @spotify-confidence/sdk'); +const mockExecFile = vi.fn().mockResolvedValue({ stdout: '', stderr: '' }); +const mockDetectFramework = vi.fn(); +const mockDetectPackageManager = vi.fn().mockReturnValue('npm'); + +vi.mock('@spotify-confidence/core', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + callMcpTool: (...args: unknown[]) => mockCallMcpTool(...args), + execFile: (...args: unknown[]) => mockExecFile(...args), + detectFramework: (...args: unknown[]) => mockDetectFramework(...args), + detectPackageManager: (...args: unknown[]) => mockDetectPackageManager(...args), + }; +}); + +const REACT_FRAMEWORK = { + id: 'react', + name: 'React', + sdkPackage: '@spotify-confidence/sdk', + docsUrl: 'https://confidence.spotify.com/docs/sdks/client/javascript', +}; + +const PYTHON_FRAMEWORK = { + id: 'python', + name: 'Python', + sdkPackage: 'confidence-openfeature-provider', + docsUrl: 'https://confidence.spotify.com/docs/sdks/server/python', +}; + +const KOTLIN_FRAMEWORK = { + id: 'kotlin', + name: 'Android (Kotlin)', + sdkPackage: 'com.spotify.confidence:openfeature-provider-android', + docsUrl: 'https://confidence.spotify.com/docs/sdks/client/android', +}; + +beforeEach(() => { + vi.clearAllMocks(); +}); + +function run(args: string[]) { + return yargs(args) + .option('json', { type: 'boolean', default: false }) + .option('output', { type: 'string' }) + .option('project', { type: 'string' }) + .command(sdkCommand) + .parse(); +} + +describe('sdk list', () => { + it('outputs available SDKs as JSON', async () => { + using output = captureOutput(); + + await run(['sdk', 'list', '--json']); + + const parsed = JSON.parse(output.stdout); + expect(parsed.data).toEqual( + expect.arrayContaining([ + expect.objectContaining({ package: '@spotify-confidence/sdk' }), + expect.objectContaining({ + name: 'Python', + package: 'confidence-openfeature-provider', + }), + expect.objectContaining({ + package: 'github.com/spotify/confidence-resolver/openfeature-provider/go', + }), + ]), + ); + }); + + it('deduplicates SDKs sharing the same package', async () => { + using output = captureOutput(); + + await run(['sdk', 'list', '--json']); + + const parsed = JSON.parse(output.stdout); + const packages = parsed.data.map((s: { package: string }) => s.package); + expect(packages).toEqual([...new Set(packages)]); + }); + + it('outputs available SDKs as a table', async () => { + using output = captureOutput(); + + await run(['sdk', 'list', '--output', 'table']); + + expect(output.stdout).toContain('SDK'); + expect(output.stdout).toContain('Package'); + }); +}); + +describe('sdk install', () => { + it('detects framework and installs SDK via docs lookup', async () => { + using _output = captureOutput(); + mockDetectFramework.mockResolvedValue(REACT_FRAMEWORK); + mockCallMcpTool.mockResolvedValue('npm install @spotify-confidence/sdk'); + + await run(['sdk', 'install']); + + expect(mockDetectFramework).toHaveBeenCalled(); + expect(mockCallMcpTool).toHaveBeenCalledWith( + 'confidence-docs', + 'searchDocumentation', + { query: 'React SDK install command' }, + expect.objectContaining({}), + ); + expect(mockExecFile).toHaveBeenCalledWith( + 'npm', + ['install', '@spotify-confidence/sdk'], + expect.objectContaining({ cwd: expect.any(String) }), + ); + }); + + it('uses detected package manager regardless of docs response', async () => { + using _output = captureOutput(); + mockDetectPackageManager.mockReturnValue('pnpm'); + mockDetectFramework.mockResolvedValue(REACT_FRAMEWORK); + mockCallMcpTool.mockResolvedValue('npm install @spotify-confidence/sdk'); + + await run(['sdk', 'install']); + + expect(mockExecFile).toHaveBeenCalledWith( + 'pnpm', + ['install', '@spotify-confidence/sdk'], + expect.objectContaining({ cwd: expect.any(String) }), + ); + }); + + it('falls back to framework sdkPackage when docs are unavailable', async () => { + using _output = captureOutput(); + mockDetectPackageManager.mockReturnValue('pip'); + mockDetectFramework.mockResolvedValue(PYTHON_FRAMEWORK); + mockCallMcpTool.mockRejectedValue(new Error('network error')); + + await run(['sdk', 'install']); + + expect(mockExecFile).toHaveBeenCalledWith( + 'pip', + ['install', 'confidence-openfeature-provider'], + expect.objectContaining({ cwd: expect.any(String) }), + ); + }); + + it('accepts --sdk to skip detection', async () => { + using _output = captureOutput(); + mockDetectPackageManager.mockReturnValue('go'); + mockCallMcpTool.mockResolvedValue( + 'go get github.com/spotify/confidence-resolver/openfeature-provider/go', + ); + + await run(['sdk', 'install', '--sdk', 'go']); + + expect(mockDetectFramework).not.toHaveBeenCalled(); + expect(mockExecFile).toHaveBeenCalledWith( + 'go', + ['get', 'github.com/spotify/confidence-resolver/openfeature-provider/go'], + expect.objectContaining({ cwd: expect.any(String) }), + ); + }); + + it('prints manual instructions for gradle projects', async () => { + using output = captureOutput(); + mockDetectPackageManager.mockReturnValue('gradle'); + mockDetectFramework.mockResolvedValue(KOTLIN_FRAMEWORK); + + await run(['sdk', 'install']); + + expect(output.stdout).toContain('build.gradle'); + expect(output.stdout).toContain(KOTLIN_FRAMEWORK.sdkPackage); + expect(mockExecFile).not.toHaveBeenCalled(); + }); + + it('reports error for unknown SDK id', async () => { + using output = captureOutput(); + + await run(['sdk', 'install', '--sdk', 'cobol']); + + expect(output.stderr).toContain('Unknown SDK: cobol'); + }); + + it('reports error when framework cannot be detected', async () => { + using output = captureOutput(); + mockDetectFramework.mockResolvedValue(null); + + await run(['sdk', 'install']); + + expect(output.stderr).toContain('Could not detect project framework'); + }); + + it('reports error when no project is found', async () => { + using output = captureOutput(); + mockDetectPackageManager.mockReturnValue(null); + + await run(['sdk', 'install']); + + expect(output.stderr).toContain('No project found'); + }); +}); diff --git a/packages/cli/__tests__/features/mcp/resolve-ide.test.ts b/packages/cli/__tests__/features/mcp/resolve-ide.test.ts new file mode 100644 index 0000000..33d23d0 --- /dev/null +++ b/packages/cli/__tests__/features/mcp/resolve-ide.test.ts @@ -0,0 +1,52 @@ +import { setConfigValue } from '@spotify-confidence/core'; +import { prepareAuthTokens } from '@spotify-confidence/testing/auth'; + +vi.mock('@inquirer/select', () => ({ + default: vi.fn(), +})); + +import select from '@inquirer/select'; + +describe('resolveIde', () => { + async function loadResolveIde() { + const mod = await import('@features/mcp/resolve-ide.js'); + return mod.resolveIde; + } + + it('returns the saved IDE from config without prompting', async () => { + using _config = prepareAuthTokens('none'); + setConfigValue('ide', 'cursor'); + const sut = await loadResolveIde(); + + const result = await sut(); + + expect(result).toBe('cursor'); + expect(select).not.toHaveBeenCalled(); + }); + + it('prompts when no IDE is saved and returns the selection', async () => { + using _config = prepareAuthTokens('none'); + vi.mocked(select).mockResolvedValueOnce('codex'); + const sut = await loadResolveIde(); + + const result = await sut(); + + expect(result).toBe('codex'); + expect(select).toHaveBeenCalledWith( + expect.objectContaining({ + message: expect.stringContaining('AI coding agent'), + }), + ); + }); + + it('persists the selected IDE to config', async () => { + using _config = prepareAuthTokens('none'); + vi.mocked(select).mockResolvedValueOnce('claude'); + const sut = await loadResolveIde(); + + await sut(); + + const { getConfigValue } = await import('@spotify-confidence/core'); + expect(getConfigValue('ide')).toBe('claude'); + }); +}); diff --git a/packages/cli/__tests__/helpers/capture.ts b/packages/cli/__tests__/helpers/capture.ts new file mode 100644 index 0000000..9a032bc --- /dev/null +++ b/packages/cli/__tests__/helpers/capture.ts @@ -0,0 +1,30 @@ +export function captureOutput() { + let stdout = ''; + let stderr = ''; + + const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => { + stdout += String(chunk); + return true; + }); + + const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => { + stderr += String(chunk); + return true; + }); + + const prevExitCode = process.exitCode; + + return { + get stdout() { + return stdout; + }, + get stderr() { + return stderr; + }, + [Symbol.dispose]() { + stdoutSpy.mockRestore(); + stderrSpy.mockRestore(); + process.exitCode = prevExitCode; + }, + }; +} diff --git a/packages/cli/__tests__/helpers/mock-integration.ts b/packages/cli/__tests__/helpers/mock-integration.ts new file mode 100644 index 0000000..cd83e1b --- /dev/null +++ b/packages/cli/__tests__/helpers/mock-integration.ts @@ -0,0 +1,19 @@ +import type { IdeIntegration } from '@spotify-confidence/core'; + +export function createMockIntegration(overrides?: Partial): IdeIntegration { + return { + id: 'claude', + name: 'Claude Code', + launchChat: vi.fn(), + runOnboarding: vi.fn().mockReturnValue(null), + prepare: vi.fn().mockResolvedValue(undefined), + skillsDir: vi.fn().mockReturnValue('/project/.claude/skills'), + detectPlugin: vi.fn().mockResolvedValue(null), + installPlugin: vi.fn().mockResolvedValue(undefined), + updatePlugin: vi.fn().mockResolvedValue(undefined), + detectMcpStatuses: vi.fn().mockResolvedValue({}), + connectMcpServer: vi.fn().mockResolvedValue(undefined), + disconnectMcpServer: vi.fn().mockResolvedValue(undefined), + ...overrides, + }; +} diff --git a/packages/cli/__tests__/output/print.test.ts b/packages/cli/__tests__/output/print.test.ts new file mode 100644 index 0000000..55f25d4 --- /dev/null +++ b/packages/cli/__tests__/output/print.test.ts @@ -0,0 +1,120 @@ +import { message, error, fail, extractFlags, print } from '@output/print.js'; + +describe('message', () => { + it('writes text to stdout with a trailing newline', () => { + const spy = vi.spyOn(process.stdout, 'write').mockReturnValue(true); + + message('hello world'); + + expect(spy).toHaveBeenCalledWith('hello world\n'); + }); +}); + +describe('error', () => { + it('writes text to stderr with a trailing newline', () => { + const spy = vi.spyOn(process.stderr, 'write').mockReturnValue(true); + + error('something went wrong'); + + expect(spy).toHaveBeenCalledWith('something went wrong\n'); + }); +}); + +describe('fail', () => { + afterEach(() => { + process.exitCode = 0; + }); + + it('writes to stderr and sets exitCode to 1', () => { + vi.spyOn(process.stderr, 'write').mockReturnValue(true); + + fail('fatal error'); + + expect(process.exitCode).toBe(1); + }); +}); + +describe('extractFlags', () => { + it('extracts json and output from argv', () => { + const sut = extractFlags({ json: true, output: 'table', other: 'ignored' }); + + expect(sut).toEqual({ json: true, output: 'table' }); + }); + + it('returns undefined for missing flags', () => { + expect(extractFlags({})).toEqual({ json: undefined, output: undefined }); + }); +}); + +describe('print', () => { + let spy: ReturnType; + + beforeEach(() => { + spy = vi.spyOn(process.stdout, 'write').mockReturnValue(true); + }); + + it('renders an array as a table', () => { + print({ + data: [ + { name: 'confidence-flags', url: 'https://example.com/flags' }, + { name: 'confidence-docs', url: 'https://example.com/docs' }, + ], + columns: [ + { key: 'name', header: 'Server', width: 20 }, + { key: 'url', header: 'URL' }, + ], + flags: { output: 'table' }, + }); + + const output = (spy.mock.calls[0] as string[])[0]; + expect(output).toContain('Server'); + expect(output).toContain('confidence-flags'); + expect(output).toContain('confidence-docs'); + }); + + it('renders an array as JSON', () => { + const data = [{ name: 'test-server', url: 'https://example.com' }]; + + print({ + data, + columns: [ + { key: 'name', header: 'Server' }, + { key: 'url', header: 'URL' }, + ], + flags: { json: true }, + }); + + const output = (spy.mock.calls[0] as string[])[0]; + const parsed = JSON.parse(output); + expect(parsed.data).toEqual(data); + }); + + it('renders a key-value object as a table', () => { + print({ + data: { email: 'user@example.com', region: 'EU' }, + columns: [ + { key: 'key', header: 'Field', width: 10 }, + { key: 'value', header: 'Value' }, + ], + flags: { output: 'table' }, + }); + + const output = (spy.mock.calls[0] as string[])[0]; + expect(output).toContain('email'); + expect(output).toContain('user@example.com'); + }); + + it('prints "No results." for an empty object', () => { + print({ + data: {}, + columns: [ + { key: 'key', header: 'Key' }, + { key: 'value', header: 'Value' }, + ], + flags: { output: 'table' }, + }); + + const output = (spy.mock.calls[0] as string[])[0]; + expect(output).toContain('No results.'); + }); +}); diff --git a/packages/cli/bin/cli.ts b/packages/cli/bin/cli.ts index a3048a5..9126014 100644 --- a/packages/cli/bin/cli.ts +++ b/packages/cli/bin/cli.ts @@ -11,6 +11,8 @@ import { flagsCommand, recordingsCommand, quickstartCommand, + mcpCommand, + sdkCommand, } from '../src/commands/index.js'; const APP_NAME = 'confidence'; @@ -68,6 +70,8 @@ const cli = yargs(hideBin(process.argv)) .command(flagsCommand) .command(recordingsCommand) .command(quickstartCommand) + .command(mcpCommand) + .command(sdkCommand) .demandCommand(1, 'Run "confidence --help" to see available commands.') .strict() .help() diff --git a/packages/cli/package.json b/packages/cli/package.json index 9816f22..19fd3b9 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -22,7 +22,9 @@ "qa": "pnpm typecheck && pnpm lint && pnpm test" }, "dependencies": { + "@inquirer/select": "^5.2.5", "@spotify-confidence/quickstart": "workspace:*", + "ora": "^9.4.1", "yargs": "^18.2.0" }, "devDependencies": { diff --git a/packages/cli/src/commands/config.ts b/packages/cli/src/commands/config.ts index fab5874..308aacc 100644 --- a/packages/cli/src/commands/config.ts +++ b/packages/cli/src/commands/config.ts @@ -1,5 +1,4 @@ import type { Argv } from 'yargs'; -import type { GlobalFlags } from './types.js'; import { readConfig, getConfigValue, @@ -7,9 +6,7 @@ import { resetConfig, validKeys, } from '@features/config/index.js'; -import { resolveFormat } from '@output/detect.js'; -import { formatJson } from '@output/json.js'; -import { formatTable } from '@output/table.js'; +import { print, message, fail, extractFlags } from '@output/print.js'; export const configCommand = { command: 'config ', @@ -26,10 +23,9 @@ export const configCommand = { (argv) => { try { setConfigValue(argv.key as string, argv.value as string); - console.log(`Set ${argv.key} = ${argv.value}`); + message(`Set ${argv.key} = ${argv.value}`); } catch (err) { - console.error((err as Error).message); - process.exitCode = 1; + fail((err as Error).message); } }, ) @@ -41,13 +37,12 @@ export const configCommand = { try { const value = getConfigValue(argv.key as string); if (value === undefined) { - console.log(`${argv.key} is not set`); + message(`${argv.key} is not set`); } else { - console.log(value); + message(value); } } catch (err) { - console.error((err as Error).message); - process.exitCode = 1; + fail((err as Error).message); } }, ) @@ -57,30 +52,14 @@ export const configCommand = { () => {}, (argv) => { const config = readConfig(); - const format = resolveFormat({ - json: argv.json as boolean | undefined, - output: argv.output as GlobalFlags['output'], + print({ + data: config, + columns: [ + { key: 'key', header: 'Key', width: 14 }, + { key: 'value', header: 'Value' }, + ], + flags: extractFlags(argv), }); - - if (format === 'json') { - console.log(formatJson(config)); - } else { - const entries = Object.entries(config); - if (entries.length === 0) { - console.log('No configuration set.'); - return; - } - const rows = entries.map(([key, value]) => ({ - key, - value: String(value), - })); - console.log( - formatTable(rows, [ - { key: 'key', header: 'Key', width: 14 }, - { key: 'value', header: 'Value' }, - ]), - ); - } }, ) .command( @@ -89,7 +68,7 @@ export const configCommand = { () => {}, () => { resetConfig(); - console.log('Configuration reset.'); + message('Configuration reset.'); }, ) .demandCommand( diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index f111af3..72b7e0d 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -6,4 +6,6 @@ export { eventsCommand } from './events.js'; export { flagsCommand } from './flags.js'; export { recordingsCommand } from './recordings.js'; export { quickstartCommand } from './quickstart.js'; +export { mcpCommand } from './mcp.js'; +export { sdkCommand } from './sdk.js'; export type { GlobalFlags } from './types.js'; diff --git a/packages/cli/src/commands/login.ts b/packages/cli/src/commands/login.ts index 9a63720..bb868b4 100644 --- a/packages/cli/src/commands/login.ts +++ b/packages/cli/src/commands/login.ts @@ -1,4 +1,5 @@ import { authenticate } from '@spotify-confidence/core'; +import { message, fail } from '@output/print.js'; export const loginCommand = { command: 'login', @@ -7,15 +8,14 @@ export const loginCommand = { const profile = argv.profile as string | undefined; try { - console.log('Opening browser for authentication...'); + message('Opening browser for authentication...'); const result = await authenticate('login', undefined, profile, (url) => { - console.log(`If the browser did not open, visit:\n${url}`); + message(`If the browser did not open, visit:\n${url}`); }); - console.log(`Authenticated as ${result.workspace ?? 'unknown'} (${result.region})`); + message(`Authenticated as ${result.workspace ?? 'unknown'} (${result.region})`); } catch (err) { const msg = err instanceof Error ? err.message : 'Authentication failed'; - console.error(`Login failed: ${msg}`); - process.exitCode = 1; + fail(`Login failed: ${msg}`); } }, }; diff --git a/packages/cli/src/commands/logout.ts b/packages/cli/src/commands/logout.ts index 3450f33..c04ee4c 100644 --- a/packages/cli/src/commands/logout.ts +++ b/packages/cli/src/commands/logout.ts @@ -1,4 +1,5 @@ import { clearTokens } from '@spotify-confidence/core'; +import { message } from '@output/print.js'; export const logoutCommand = { command: 'logout', @@ -6,6 +7,6 @@ export const logoutCommand = { handler(argv: Record) { const profile = argv.profile as string | undefined; clearTokens(profile); - console.log('Logged out.'); + message('Logged out.'); }, }; diff --git a/packages/cli/src/commands/mcp.ts b/packages/cli/src/commands/mcp.ts new file mode 100644 index 0000000..cd09310 --- /dev/null +++ b/packages/cli/src/commands/mcp.ts @@ -0,0 +1,109 @@ +import type { Argv } from 'yargs'; +import { print, fail, extractFlags } from '@output/print.js'; +import { getAvailableMcpServers } from '@spotify-confidence/core'; +import { + resolveIde, + installMcpServers, + uninstallMcpServers, + getMcpStatuses, + refreshMcpAuth, +} from '@features/mcp/index.js'; +import { resolveProjectDir } from '../utils/resolve-project-dir.js'; + +const STATUS_LABELS: Record = { + connected: 'Connected', + installed: 'Installed (unreachable)', + 'auth-expired': 'Auth expired', + 'not-installed': 'Not installed', +}; + +export const mcpCommand = { + command: 'mcp ', + describe: 'Manage Confidence MCP server connections', + builder(yargs: Argv) { + return yargs + .command( + 'install', + 'Install Confidence MCP servers for your AI coding agent', + () => {}, + async (argv) => { + try { + const ideId = await resolveIde(); + await installMcpServers(ideId, resolveProjectDir(argv)); + } catch (err) { + fail((err as Error).message); + } + }, + ) + .command( + 'uninstall', + 'Remove Confidence MCP servers from your AI coding agent', + () => {}, + async (argv) => { + try { + const ideId = await resolveIde(); + await uninstallMcpServers(ideId, resolveProjectDir(argv)); + } catch (err) { + fail((err as Error).message); + } + }, + ) + .command( + 'status', + 'Show MCP server connection status', + () => {}, + async (argv) => { + try { + const ideId = await resolveIde(); + const statuses = await getMcpStatuses(ideId, resolveProjectDir(argv)); + const rows = Object.entries(statuses).map(([server, status]) => ({ + server, + status: STATUS_LABELS[status] ?? status, + })); + print({ + data: rows, + columns: [ + { key: 'server', header: 'Server', width: 20 }, + { key: 'status', header: 'Status' }, + ], + flags: extractFlags(argv), + }); + } catch (err) { + fail((err as Error).message); + } + }, + ) + .command( + 'list', + 'List available Confidence MCP servers', + () => {}, + (argv) => { + const servers = getAvailableMcpServers(); + print({ + data: servers.map((s) => ({ name: s.name, url: s.url })), + columns: [ + { key: 'name', header: 'Server', width: 20 }, + { key: 'url', header: 'URL' }, + ], + flags: extractFlags(argv), + }); + }, + ) + .command( + 'auth', + 'Re-authenticate MCP servers with a fresh token', + () => {}, + async (argv) => { + try { + const ideId = await resolveIde(); + await refreshMcpAuth(ideId, resolveProjectDir(argv)); + } catch (err) { + fail((err as Error).message); + } + }, + ) + .demandCommand(1, 'Available actions: install, uninstall, status, list, auth') + .strict(); + }, + handler() {}, +}; diff --git a/packages/cli/src/commands/sdk.ts b/packages/cli/src/commands/sdk.ts new file mode 100644 index 0000000..e7ff29f --- /dev/null +++ b/packages/cli/src/commands/sdk.ts @@ -0,0 +1,64 @@ +import type { Argv } from 'yargs'; +import { print, extractFlags, fail } from '@output/print.js'; +import { listSdks, installSdk } from '@features/sdk/index.js'; +import { QUICKSTART_BUILDER, launchQuickstart } from '@features/quickstart/index.js'; +import { loadPersistedToken, type FrameworkId } from '@spotify-confidence/core'; +import { resolveProjectDir } from '../utils/resolve-project-dir.js'; + +export const sdkCommand = { + command: 'sdk ', + describe: 'Manage Confidence SDKs', + builder(yargs: Argv) { + return yargs + .command( + 'list', + 'List available Confidence SDKs', + () => {}, + (argv) => { + const sdks = listSdks(); + print({ + data: sdks, + columns: [ + { key: 'name', header: 'SDK', width: 20 }, + { key: 'package', header: 'Package', width: 45 }, + { key: 'docs', header: 'Docs' }, + ], + flags: extractFlags(argv as Record), + }); + }, + ) + .command( + 'install', + 'Install the Confidence SDK for your project', + { + dir: { + type: 'string' as const, + describe: 'Project directory', + normalize: true, + }, + sdk: { + type: 'string' as const, + describe: 'SDK to install (auto-detected if omitted)', + }, + }, + async (argv) => { + const args = argv as Record; + try { + await installSdk({ + projectDir: resolveProjectDir(args), + accessToken: loadPersistedToken() ?? undefined, + sdkId: args.sdk as FrameworkId | undefined, + }); + } catch (err) { + fail((err as Error).message); + } + }, + ) + .command('setup', 'Run interactive SDK setup wizard', QUICKSTART_BUILDER, async (argv) => { + await launchQuickstart(argv as Record, { features: ['none'] }); + }) + .demandCommand(1, 'Available actions: list, install, setup') + .strict(); + }, + handler() {}, +}; diff --git a/packages/cli/src/commands/whoami.ts b/packages/cli/src/commands/whoami.ts index f3d3117..93ef0be 100644 --- a/packages/cli/src/commands/whoami.ts +++ b/packages/cli/src/commands/whoami.ts @@ -1,27 +1,21 @@ import { loadPersistedToken, validateToken, decodeJwtPayload } from '@spotify-confidence/core'; -import { resolveFormat } from '@output/detect.js'; -import { formatJson } from '@output/json.js'; -import { formatTable } from '@output/table.js'; +import { print, fail, extractFlags } from '@output/print.js'; export const whoamiCommand = { command: 'whoami', describe: 'Show current user, org, region, and token expiry', handler(argv: Record) { const profile = argv.profile as string | undefined; - const json = argv.json as boolean | undefined; - const output = argv.output as 'json' | 'table' | 'plain' | undefined; const token = loadPersistedToken(profile); if (!token) { - console.error('Not logged in. Run "confidence login" first.'); - process.exitCode = 1; + fail('Not logged in. Run "confidence login" first.'); return; } const validation = validateToken(token); if (!validation.valid) { - console.error('Token is expired. Run "confidence login" to re-authenticate.'); - process.exitCode = 1; + fail('Token is expired. Run "confidence login" to re-authenticate.'); return; } @@ -34,30 +28,20 @@ export const whoamiCommand = { const exp = payload.exp as number | undefined; const expiresAt = exp ? new Date(exp * 1000).toISOString() : 'unknown'; - const info = { - email: email ?? 'unknown', - organization: org ?? 'unknown', - region: validation.region ?? 'unknown', - workspace: validation.workspace ?? 'unknown', - expires: expiresAt, - profile: profile ?? 'default', - }; - - const format = resolveFormat({ json, output }); - - if (format === 'json') { - console.log(formatJson(info)); - } else { - const rows = Object.entries(info).map(([key, value]) => ({ - key, - value: String(value), - })); - console.log( - formatTable(rows, [ - { key: 'key', header: 'Field', width: 14 }, - { key: 'value', header: 'Value' }, - ]), - ); - } + print({ + data: { + email: email ?? 'unknown', + organization: org ?? 'unknown', + region: validation.region ?? 'unknown', + workspace: validation.workspace ?? 'unknown', + expires: expiresAt, + profile: profile ?? 'default', + }, + columns: [ + { key: 'key', header: 'Field', width: 14 }, + { key: 'value', header: 'Value' }, + ], + flags: extractFlags(argv), + }); }, }; diff --git a/packages/cli/src/features/mcp/actions.ts b/packages/cli/src/features/mcp/actions.ts new file mode 100644 index 0000000..3128b4f --- /dev/null +++ b/packages/cli/src/features/mcp/actions.ts @@ -0,0 +1,124 @@ +import ora from 'ora'; +import type { IdeId } from '@spotify-confidence/shared-kernel'; +import { message, error } from '@output/print.js'; +import { + getIntegration, + getAvailableMcpServers, + MCP_SERVERS, + loadPersistedToken, + authenticate, + validateToken, + type McpServerName, + type McpServerStatus, +} from '@spotify-confidence/core'; + +export async function installMcpServers(ideId: IdeId, projectDir: string): Promise { + const integration = getIntegration(ideId); + const servers = getAvailableMcpServers(); + + const token = await resolveAuthToken(); + if (!token) return; + + const spinner = ora(`Installing MCP servers for ${integration.name}...`).start(); + + for (const server of servers) { + const serverDef = MCP_SERVERS[server.name]; + spinner.text = `Connecting ${server.name}...`; + + try { + await integration.connectMcpServer({ + serverName: server.name, + serverUrl: server.url, + serverType: serverDef.type, + serverHeaders: serverDef.headers, + projectDir, + accessToken: token, + }); + } catch (err) { + spinner.fail(`Failed to connect ${server.name}: ${(err as Error).message}`); + return; + } + } + + spinner.succeed(`MCP servers installed for ${integration.name}`); +} + +export async function uninstallMcpServers(ideId: IdeId, projectDir: string): Promise { + const integration = getIntegration(ideId); + const servers = getAvailableMcpServers(); + const spinner = ora(`Removing MCP servers from ${integration.name}...`).start(); + + for (const server of servers) { + spinner.text = `Removing ${server.name}...`; + try { + await integration.disconnectMcpServer({ + serverName: server.name, + projectDir, + }); + } catch { + // best-effort — server may not have been installed + } + } + + spinner.succeed(`MCP servers removed from ${integration.name}`); +} + +export async function getMcpStatuses( + ideId: IdeId, + projectDir: string, +): Promise> { + const integration = getIntegration(ideId); + return integration.detectMcpStatuses(projectDir); +} + +export async function refreshMcpAuth(ideId: IdeId, projectDir: string): Promise { + const integration = getIntegration(ideId); + const servers = getAvailableMcpServers(); + + const token = await resolveAuthToken({ forceNew: true }); + if (!token) return; + + const spinner = ora('Updating MCP server credentials...').start(); + + for (const server of servers) { + const serverDef = MCP_SERVERS[server.name]; + spinner.text = `Updating ${server.name}...`; + + try { + await integration.connectMcpServer({ + serverName: server.name, + serverUrl: server.url, + serverType: serverDef.type, + serverHeaders: serverDef.headers, + projectDir, + accessToken: token, + }); + } catch (err) { + spinner.fail(`Failed to update ${server.name}: ${(err as Error).message}`); + return; + } + } + + spinner.succeed('MCP server credentials updated'); +} + +async function resolveAuthToken(opts?: { forceNew?: boolean }): Promise { + if (!opts?.forceNew) { + const existing = loadPersistedToken(); + if (existing) { + const { valid } = validateToken(existing); + if (valid) return existing; + } + } + + try { + message('Authentication required. Opening browser...'); + const result = await authenticate('login', undefined, undefined, (url) => { + message(`If the browser did not open, visit:\n${url}`); + }); + return result.accessToken; + } catch (err) { + error(`Authentication failed: ${(err as Error).message}`); + return null; + } +} diff --git a/packages/cli/src/features/mcp/index.ts b/packages/cli/src/features/mcp/index.ts new file mode 100644 index 0000000..0289770 --- /dev/null +++ b/packages/cli/src/features/mcp/index.ts @@ -0,0 +1,7 @@ +export { resolveIde } from './resolve-ide.js'; +export { + installMcpServers, + uninstallMcpServers, + getMcpStatuses, + refreshMcpAuth, +} from './actions.js'; diff --git a/packages/cli/src/features/mcp/resolve-ide.ts b/packages/cli/src/features/mcp/resolve-ide.ts new file mode 100644 index 0000000..414c70b --- /dev/null +++ b/packages/cli/src/features/mcp/resolve-ide.ts @@ -0,0 +1,18 @@ +import select from '@inquirer/select'; +import type { IdeId } from '@spotify-confidence/shared-kernel'; +import { getConfigValue, setConfigValue, getIntegrations } from '@spotify-confidence/core'; + +export async function resolveIde(): Promise { + const saved = getConfigValue('ide') as IdeId | undefined; + if (saved) return saved; + + const integrations = getIntegrations(); + const chosen = await select({ + message: 'Which AI coding agent are you using?', + choices: integrations.map((i) => ({ name: i.name, value: i.id })), + }); + + setConfigValue('ide', chosen); + + return chosen; +} diff --git a/packages/cli/src/features/quickstart/launch.ts b/packages/cli/src/features/quickstart/launch.ts index badead4..b4f90b0 100644 --- a/packages/cli/src/features/quickstart/launch.ts +++ b/packages/cli/src/features/quickstart/launch.ts @@ -1,9 +1,3 @@ -const FEATURE_TO_GOAL: Record = { - flags: 'feature-flags', - events: 'event-tracking', - recordings: 'session-recordings', -}; - export const QUICKSTART_BUILDER = { dir: { type: 'string' as const, @@ -18,8 +12,8 @@ export const QUICKSTART_BUILDER = { features: { type: 'string' as const, array: true, - choices: ['flags', 'events', 'recordings'] as const, - describe: 'Pre-select onboarding features', + choices: ['none', 'flags', 'events', 'recordings'] as const, + describe: 'Pre-select onboarding features (use "none" for SDK-only setup)', }, } as const; @@ -32,12 +26,11 @@ export async function launchQuickstart( const dir = argv.dir as string | undefined; const noTelemetry = argv.telemetry === false; const features = (argv.features as string[] | undefined) ?? defaults?.features; - const goals = features?.map((f) => FEATURE_TO_GOAL[f]).filter(Boolean); if (noTelemetry) { process.env.CONFIDENCE_TELEMETRY = 'false'; } - const { startTui } = await import('@spotify-confidence/quickstart'); - await startTui({ dryRun, debug, dir, goals }); + const { startTui, resolveGoals } = await import('@spotify-confidence/quickstart'); + await startTui({ dryRun, debug, dir, goals: resolveGoals(features) }); } diff --git a/packages/cli/src/features/sdk/index.ts b/packages/cli/src/features/sdk/index.ts new file mode 100644 index 0000000..005c418 --- /dev/null +++ b/packages/cli/src/features/sdk/index.ts @@ -0,0 +1,2 @@ +export { listSdks } from './list.js'; +export { installSdk } from './install.js'; diff --git a/packages/cli/src/features/sdk/install-strategy.ts b/packages/cli/src/features/sdk/install-strategy.ts new file mode 100644 index 0000000..97f1303 --- /dev/null +++ b/packages/cli/src/features/sdk/install-strategy.ts @@ -0,0 +1,33 @@ +import type { PackageManager } from '@spotify-confidence/core'; + +type CliInstallStrategy = { + kind: 'cli'; + buildCommand: (sdkPackage: string) => string; +}; + +type ManualInstallStrategy = { + kind: 'manual'; + manifestFile: string; +}; + +export type InstallStrategy = CliInstallStrategy | ManualInstallStrategy; + +const STRATEGIES: Record = { + npm: { kind: 'cli', buildCommand: (pkg) => `npm install ${pkg}` }, + pnpm: { kind: 'cli', buildCommand: (pkg) => `pnpm install ${pkg}` }, + yarn: { kind: 'cli', buildCommand: (pkg) => `yarn add ${pkg}` }, + bun: { kind: 'cli', buildCommand: (pkg) => `bun add ${pkg}` }, + pip: { kind: 'cli', buildCommand: (pkg) => `pip install ${pkg}` }, + go: { kind: 'cli', buildCommand: (pkg) => `go get ${pkg}` }, + swift: { + kind: 'cli', + buildCommand: (pkg) => `swift package add https://github.com/spotify/${pkg}`, + }, + + maven: { kind: 'manual', manifestFile: 'pom.xml' }, + gradle: { kind: 'manual', manifestFile: 'build.gradle' }, +}; + +export function getInstallStrategy(pm: PackageManager): InstallStrategy { + return STRATEGIES[pm]; +} diff --git a/packages/cli/src/features/sdk/install.ts b/packages/cli/src/features/sdk/install.ts new file mode 100644 index 0000000..81eab60 --- /dev/null +++ b/packages/cli/src/features/sdk/install.ts @@ -0,0 +1,63 @@ +import ora from 'ora'; +import { message, fail } from '@output/print.js'; +import { detectPackageManager } from '@spotify-confidence/core'; +import type { FrameworkId } from '@spotify-confidence/core'; +import { getInstallStrategy } from './install-strategy.js'; +import { resolveFramework } from './resolve-framework.js'; +import { resolvePackage } from './resolve-package.js'; +import { runCommand } from './run-command.js'; + +type InstallSdkOpts = { + projectDir: string; + accessToken?: string; + sdkId?: FrameworkId; +}; + +export async function installSdk({ + projectDir, + accessToken, + sdkId, +}: InstallSdkOpts): Promise { + const pm = detectPackageManager(projectDir); + if (!pm) { + fail(`No project found in ${projectDir}. Run from a project directory or use --dir.`); + return; + } + + const framework = await resolveFramework(projectDir, sdkId); + if (!framework) { + fail( + sdkId + ? `Unknown SDK: ${sdkId}. Run "confidence sdk list" to see available SDKs.` + : 'Could not detect project framework. Specify an SDK with --sdk or run from a project directory.', + ); + return; + } + + const installStrategy = getInstallStrategy(pm); + + if (installStrategy.kind === 'manual') { + message(`Add ${framework.sdkPackage} to your ${installStrategy.manifestFile}.`); + message(`See: ${framework.docsUrl}`); + return; + } + + const spinner = ora('Looking up install instructions...').start(); + const sdkPackage = await resolvePackage(framework.name, framework.sdkPackage, accessToken); + + spinner.text = `Installing ${sdkPackage}...`; + const result = await runCommand(installStrategy.buildCommand(sdkPackage), projectDir); + + if (result.ok) { + spinner.succeed(`Installed ${sdkPackage}`); + return; + } + + if ('aborted' in result) { + spinner.stop(); + return; + } + + spinner.fail(); + fail(`Failed to install ${sdkPackage}: ${result.error}`); +} diff --git a/packages/cli/src/features/sdk/list.ts b/packages/cli/src/features/sdk/list.ts new file mode 100644 index 0000000..2aeac45 --- /dev/null +++ b/packages/cli/src/features/sdk/list.ts @@ -0,0 +1,16 @@ +import { getFrameworks } from '@spotify-confidence/core'; + +export type SdkListEntry = { + name: string; + package: string; + docs: string; +}; + +export function listSdks(): SdkListEntry[] { + const seen = new Set(); + return getFrameworks().flatMap((fw) => { + if (seen.has(fw.sdkPackage)) return []; + seen.add(fw.sdkPackage); + return [{ name: fw.name, package: fw.sdkPackage, docs: fw.docsUrl }]; + }); +} diff --git a/packages/cli/src/features/sdk/parse-docs.ts b/packages/cli/src/features/sdk/parse-docs.ts new file mode 100644 index 0000000..8ad830a --- /dev/null +++ b/packages/cli/src/features/sdk/parse-docs.ts @@ -0,0 +1,25 @@ +const INSTALL_PREFIXES = [ + 'npm install', + 'pnpm install', + 'pnpm add', + 'yarn add', + 'bun add', + 'pip install', + 'go get', + 'swift package add', +]; + +export function parsePackageName(docsResponse: string): string | null { + for (const line of docsResponse.split('\n')) { + const trimmed = line.trim(); + + for (const prefix of INSTALL_PREFIXES) { + if (trimmed.startsWith(`${prefix} `)) { + const pkg = trimmed.slice(prefix.length).trim(); + if (pkg) return pkg; + } + } + } + + return null; +} diff --git a/packages/cli/src/features/sdk/resolve-framework.ts b/packages/cli/src/features/sdk/resolve-framework.ts new file mode 100644 index 0000000..7b1a52e --- /dev/null +++ b/packages/cli/src/features/sdk/resolve-framework.ts @@ -0,0 +1,16 @@ +import { message } from '@output/print.js'; +import { getFrameworks, detectFramework } from '@spotify-confidence/core'; +import type { FrameworkConfig, FrameworkId } from '@spotify-confidence/core'; + +export async function resolveFramework( + projectDir: string, + sdkId?: FrameworkId, +): Promise { + if (sdkId) { + return getFrameworks().find((fw) => fw.id === sdkId) ?? null; + } + + const detected = await detectFramework(projectDir); + if (detected) message(`Detected ${detected.name} project`); + return detected; +} diff --git a/packages/cli/src/features/sdk/resolve-package.ts b/packages/cli/src/features/sdk/resolve-package.ts new file mode 100644 index 0000000..9afad7d --- /dev/null +++ b/packages/cli/src/features/sdk/resolve-package.ts @@ -0,0 +1,20 @@ +import { callMcpTool } from '@spotify-confidence/core'; +import { parsePackageName } from './parse-docs.js'; + +export async function resolvePackage( + frameworkName: string, + fallback: string, + accessToken?: string, +): Promise { + try { + const response = await callMcpTool( + 'confidence-docs', + 'searchDocumentation', + { query: `${frameworkName} SDK install command` }, + { accessToken }, + ); + return parsePackageName(response) ?? fallback; + } catch { + return fallback; + } +} diff --git a/packages/cli/src/features/sdk/run-command.ts b/packages/cli/src/features/sdk/run-command.ts new file mode 100644 index 0000000..204a0e7 --- /dev/null +++ b/packages/cli/src/features/sdk/run-command.ts @@ -0,0 +1,24 @@ +import { execFile } from '@spotify-confidence/core'; + +type RunResult = { ok: true } | { ok: false; error: string } | { ok: false; aborted: true }; + +export async function runCommand(command: string, projectDir: string): Promise { + const [cmd, ...args] = command.split(/\s+/); + + const ac = new AbortController(); + const onSignal = () => ac.abort(); + process.once('SIGINT', onSignal); + process.once('SIGTERM', onSignal); + + try { + await execFile(cmd, args, { cwd: projectDir, signal: ac.signal }); + return { ok: true }; + } catch (err) { + return ac.signal.aborted + ? { ok: false, aborted: true } + : { ok: false, error: (err as Error).message }; + } finally { + process.off('SIGINT', onSignal); + process.off('SIGTERM', onSignal); + } +} diff --git a/packages/cli/src/output/index.ts b/packages/cli/src/output/index.ts index 6983be3..5264659 100644 --- a/packages/cli/src/output/index.ts +++ b/packages/cli/src/output/index.ts @@ -1,3 +1,4 @@ export { resolveFormat, type OutputFormat } from './detect.js'; export { formatJson } from './json.js'; export { formatTable } from './table.js'; +export { print, message, error, fail, extractFlags } from './print.js'; diff --git a/packages/cli/src/output/print.ts b/packages/cli/src/output/print.ts new file mode 100644 index 0000000..8312797 --- /dev/null +++ b/packages/cli/src/output/print.ts @@ -0,0 +1,74 @@ +import type { OutputFormat } from './detect.js'; +import { resolveFormat } from './detect.js'; +import { formatJson } from './json.js'; +import { formatTable } from './table.js'; + +type Column = { + key: keyof T & string; + header: string; + width?: number; +}; + +type FormatFlags = { + json?: boolean; + output?: OutputFormat; +}; + +type PrintTableOpts> = { + data: T[]; + columns: Column[]; + flags: FormatFlags; +}; + +type PrintKeyValueOpts = { + data: Record; + columns: [Column<{ key: string; value: string }>, Column<{ key: string; value: string }>]; + flags: FormatFlags; +}; + +export function message(text: string): void { + process.stdout.write(text + '\n'); +} + +export function error(text: string): void { + process.stderr.write(text + '\n'); +} + +export function fail(text: string): void { + error(text); + process.exitCode = 1; +} + +export function print>(opts: PrintTableOpts): void; +export function print(opts: PrintKeyValueOpts): void; +export function print>( + opts: PrintTableOpts | PrintKeyValueOpts, +): void { + const format = resolveFormat(opts.flags); + + if (format === 'json') { + message(formatJson(opts.data)); + return; + } + + if (Array.isArray(opts.data)) { + message(formatTable(opts.data, opts.columns as Column[])); + return; + } + + const entries = Object.entries(opts.data); + if (entries.length === 0) { + message('No results.'); + return; + } + + const rows = entries.map(([key, value]) => ({ key, value: String(value ?? '') })); + message(formatTable(rows, opts.columns as Column<{ key: string; value: string }>[])); +} + +export function extractFlags(argv: Record): FormatFlags { + return { + json: argv.json as boolean | undefined, + output: argv.output as OutputFormat | undefined, + }; +} diff --git a/packages/cli/src/utils/resolve-project-dir.ts b/packages/cli/src/utils/resolve-project-dir.ts new file mode 100644 index 0000000..d513dfd --- /dev/null +++ b/packages/cli/src/utils/resolve-project-dir.ts @@ -0,0 +1,3 @@ +export function resolveProjectDir(argv: Record): string { + return (argv.dir as string | undefined) ?? (argv.project as string | undefined) ?? process.cwd(); +} diff --git a/packages/cli/types/quickstart.d.ts b/packages/cli/types/quickstart.d.ts index 4f46488..1b8d480 100644 --- a/packages/cli/types/quickstart.d.ts +++ b/packages/cli/types/quickstart.d.ts @@ -7,4 +7,5 @@ declare module '@spotify-confidence/quickstart' { }; function startTui(opts?: StoreOptions): Promise; + function resolveGoals(features?: string[]): string[] | undefined; } diff --git a/packages/core/__tests__/integrations/mcp-disconnect.test.ts b/packages/core/__tests__/integrations/mcp-disconnect.test.ts new file mode 100644 index 0000000..3e59182 --- /dev/null +++ b/packages/core/__tests__/integrations/mcp-disconnect.test.ts @@ -0,0 +1,124 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { createProjectDir } from '@spotify-confidence/testing/scaffold'; +import { + writeClaudeSettings, + writeCursorMcpConfig, + writeCursorCliConfig, +} from '@spotify-confidence/testing/scaffold'; + +const execFile = vi.fn().mockResolvedValue({ stdout: '' }); + +vi.mock('../../src/exec/exec.js', () => ({ + execFile: (cmd: string, args: string[], opts?: Record) => + execFile(cmd, args, opts) as Promise<{ stdout: string }>, +})); + +beforeEach(() => { + execFile.mockReset().mockResolvedValue({ stdout: '' }); +}); + +describe('claude disconnectMcpServer', () => { + async function loadDisconnect() { + const mod = await import('@integrations/claude/mcp.js'); + return mod.disconnectMcpServer; + } + + it('calls claude mcp remove with the server name', async () => { + const sut = await loadDisconnect(); + + await sut({ serverName: 'confidence-flags', projectDir: '/project' }); + + expect(execFile).toHaveBeenCalledWith( + 'claude', + ['mcp', 'remove', '--scope', 'project', 'confidence-flags'], + { cwd: '/project' }, + ); + }); + + it('removes the tool permission from settings.local.json', async () => { + using project = createProjectDir('empty'); + writeClaudeSettings(project.path, { + permissions: { allow: ['mcp__confidence-flags__*', 'mcp__other__*'] }, + enabledMcpjsonServers: ['confidence-flags', 'other-server'], + }); + const sut = await loadDisconnect(); + + await sut({ serverName: 'confidence-flags', projectDir: project.path }); + + const settings = JSON.parse( + readFileSync(join(project.path, '.claude', 'settings.local.json'), 'utf-8'), + ); + expect(settings.permissions.allow).toEqual(['mcp__other__*']); + expect(settings.enabledMcpjsonServers).toEqual(['other-server']); + }); + + it('does not fail when settings.local.json does not exist', async () => { + using project = createProjectDir('empty'); + const sut = await loadDisconnect(); + + await expect( + sut({ serverName: 'confidence-flags', projectDir: project.path }), + ).resolves.toBeUndefined(); + }); +}); + +describe('cursor disconnectMcpServer', () => { + async function loadDisconnect() { + const mod = await import('@integrations/cursor/mcp.js'); + return mod.disconnectMcpServer; + } + + it('removes the server entry from project and global MCP configs', async () => { + using project = createProjectDir('empty'); + writeCursorMcpConfig(project.path, { + mcpServers: { + 'confidence-flags': { type: 'http', url: 'https://example.com' }, + 'other-server': { type: 'http', url: 'https://other.com' }, + }, + }); + writeCursorCliConfig(project.path, { + permissions: { allow: ['Mcp(confidence-flags:*)', 'Mcp(other:*)'], deny: [] }, + }); + + const sut = await loadDisconnect(); + vi.stubEnv('HOME', project.path); + + await sut({ serverName: 'confidence-flags', projectDir: project.path }); + + const updatedMcp = JSON.parse(readFileSync(join(project.path, '.cursor', 'mcp.json'), 'utf-8')); + expect(updatedMcp.mcpServers).not.toHaveProperty('confidence-flags'); + expect(updatedMcp.mcpServers).toHaveProperty('other-server'); + + const updatedCli = JSON.parse(readFileSync(join(project.path, '.cursor', 'cli.json'), 'utf-8')); + expect(updatedCli.permissions.allow).toEqual(['Mcp(other:*)']); + }); + + it('does not fail when config files do not exist', async () => { + using project = createProjectDir('empty'); + const sut = await loadDisconnect(); + + await expect( + sut({ serverName: 'confidence-flags', projectDir: project.path }), + ).resolves.toBeUndefined(); + }); +}); + +describe('codex disconnectMcpServer', () => { + async function loadDisconnect() { + const mod = await import('@integrations/codex/mcp.js'); + return mod.disconnectMcpServer; + } + + it('calls codex mcp remove with the server name', async () => { + const sut = await loadDisconnect(); + + await sut({ serverName: 'confidence-flags', projectDir: '/project' }); + + expect(execFile).toHaveBeenCalledWith( + 'codex', + ['mcp', 'remove', 'confidence-flags'], + undefined, + ); + }); +}); diff --git a/packages/core/__tests__/integrations/plugins.test.ts b/packages/core/__tests__/integrations/plugins.test.ts index 64072e9..56b24fb 100644 --- a/packages/core/__tests__/integrations/plugins.test.ts +++ b/packages/core/__tests__/integrations/plugins.test.ts @@ -18,6 +18,7 @@ const mockIntegration: IdeIntegration = { updatePlugin: vi.fn().mockResolvedValue(undefined), detectMcpStatuses: vi.fn().mockResolvedValue({}), connectMcpServer: vi.fn().mockResolvedValue(undefined), + disconnectMcpServer: vi.fn().mockResolvedValue(undefined), }; vi.mock('../../src/integrations/registry.js', () => ({ diff --git a/packages/core/package.json b/packages/core/package.json index d8d5b25..c54b314 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -7,6 +7,7 @@ ".": "./src/index.ts" }, "dependencies": { + "@modelcontextprotocol/sdk": "^1.30.1", "@spotify-confidence/shared-kernel": "workspace:*" }, "scripts": { diff --git a/packages/core/src/frameworks/go/index.ts b/packages/core/src/frameworks/go/index.ts index 063087d..fb54802 100644 --- a/packages/core/src/frameworks/go/index.ts +++ b/packages/core/src/frameworks/go/index.ts @@ -5,8 +5,8 @@ import { join } from 'node:path'; export const goFramework: FrameworkConfig = { id: 'go', name: 'Go', - docsUrl: 'https://confidence.spotify.com/docs/sdk/go', - sdkPackage: 'github.com/spotify/confidence-sdk-go', + docsUrl: 'https://confidence.spotify.com/docs/sdks/server/go', + sdkPackage: 'github.com/spotify/confidence-resolver/openfeature-provider/go', detect: async (dir) => { return existsSync(join(dir, 'go.mod')); }, diff --git a/packages/core/src/frameworks/index.ts b/packages/core/src/frameworks/index.ts index a09514f..57fa699 100644 --- a/packages/core/src/frameworks/index.ts +++ b/packages/core/src/frameworks/index.ts @@ -8,7 +8,7 @@ import { javaFramework } from './java/index.js'; import { goFramework } from './go/index.js'; import { pythonFramework } from './python/index.js'; -export type { FrameworkConfig } from './types.js'; +export type { FrameworkConfig, FrameworkId } from './types.js'; const FRAMEWORKS: FrameworkConfig[] = [ nextjsFramework, diff --git a/packages/core/src/frameworks/java/index.ts b/packages/core/src/frameworks/java/index.ts index bbb64ee..ffe5445 100644 --- a/packages/core/src/frameworks/java/index.ts +++ b/packages/core/src/frameworks/java/index.ts @@ -5,8 +5,8 @@ import { join } from 'node:path'; export const javaFramework: FrameworkConfig = { id: 'java', name: 'Java', - docsUrl: 'https://confidence.spotify.com/docs/sdk/java', - sdkPackage: 'com.spotify.confidence:openfeature-provider', + docsUrl: 'https://confidence.spotify.com/docs/sdks/server/java', + sdkPackage: 'com.spotify.confidence:openfeature-provider-local', detect: async (dir) => { if (existsSync(join(dir, 'pom.xml'))) return true; const hasGradle = diff --git a/packages/core/src/frameworks/kotlin/index.ts b/packages/core/src/frameworks/kotlin/index.ts index 486f24a..a03787e 100644 --- a/packages/core/src/frameworks/kotlin/index.ts +++ b/packages/core/src/frameworks/kotlin/index.ts @@ -5,7 +5,7 @@ import { join } from 'node:path'; export const kotlinFramework: FrameworkConfig = { id: 'kotlin', name: 'Android (Kotlin)', - docsUrl: 'https://confidence.spotify.com/docs/sdk/android', + docsUrl: 'https://confidence.spotify.com/docs/sdks/client/android', sdkPackage: 'com.spotify.confidence:openfeature-provider-android', detect: async (dir) => { const hasGradle = diff --git a/packages/core/src/frameworks/nextjs/index.ts b/packages/core/src/frameworks/nextjs/index.ts index 1adb813..10ef49a 100644 --- a/packages/core/src/frameworks/nextjs/index.ts +++ b/packages/core/src/frameworks/nextjs/index.ts @@ -5,8 +5,8 @@ import { join } from 'node:path'; export const nextjsFramework: FrameworkConfig = { id: 'nextjs', name: 'Next.js', - docsUrl: 'https://confidence.spotify.com/docs/sdk/nextjs', - sdkPackage: '@spotify-confidence/sdk', + docsUrl: 'https://confidence.spotify.com/docs/sdks/server/nextjs', + sdkPackage: '@spotify-confidence/openfeature-server-provider-local', detect: async (dir) => { const pkgPath = join(dir, 'package.json'); if (!existsSync(pkgPath)) return false; diff --git a/packages/core/src/frameworks/node/index.ts b/packages/core/src/frameworks/node/index.ts index 6b86aff..ba1418b 100644 --- a/packages/core/src/frameworks/node/index.ts +++ b/packages/core/src/frameworks/node/index.ts @@ -5,8 +5,8 @@ import { join } from 'node:path'; export const nodeFramework: FrameworkConfig = { id: 'node', name: 'Node.js', - docsUrl: 'https://confidence.spotify.com/docs/sdk/node', - sdkPackage: '@spotify-confidence/server-sdk', + docsUrl: 'https://confidence.spotify.com/docs/sdks/server/javascript', + sdkPackage: '@spotify-confidence/openfeature-server-provider-local', detect: async (dir) => { const pkgPath = join(dir, 'package.json'); return existsSync(pkgPath); diff --git a/packages/core/src/frameworks/python/index.ts b/packages/core/src/frameworks/python/index.ts index aaa4990..d2643ef 100644 --- a/packages/core/src/frameworks/python/index.ts +++ b/packages/core/src/frameworks/python/index.ts @@ -5,8 +5,8 @@ import { join } from 'node:path'; export const pythonFramework: FrameworkConfig = { id: 'python', name: 'Python', - docsUrl: 'https://confidence.spotify.com/docs/sdk/python', - sdkPackage: 'spotify-confidence-sdk', + docsUrl: 'https://confidence.spotify.com/docs/sdks/server/python', + sdkPackage: 'confidence-openfeature-provider', detect: async (dir) => { return ( existsSync(join(dir, 'requirements.txt')) || diff --git a/packages/core/src/frameworks/react/index.ts b/packages/core/src/frameworks/react/index.ts index 88c3bd4..fd113b4 100644 --- a/packages/core/src/frameworks/react/index.ts +++ b/packages/core/src/frameworks/react/index.ts @@ -5,7 +5,7 @@ import { join } from 'node:path'; export const reactFramework: FrameworkConfig = { id: 'react', name: 'React', - docsUrl: 'https://confidence.spotify.com/docs/sdk/react', + docsUrl: 'https://confidence.spotify.com/docs/sdks/client/javascript', sdkPackage: '@spotify-confidence/sdk', detect: async (dir) => { const pkgPath = join(dir, 'package.json'); diff --git a/packages/core/src/frameworks/swift/index.ts b/packages/core/src/frameworks/swift/index.ts index 53a699f..9b96917 100644 --- a/packages/core/src/frameworks/swift/index.ts +++ b/packages/core/src/frameworks/swift/index.ts @@ -6,7 +6,7 @@ import { join } from 'node:path'; export const swiftFramework: FrameworkConfig = { id: 'swift', name: 'Swift', - docsUrl: 'https://confidence.spotify.com/docs/sdk/swift', + docsUrl: 'https://confidence.spotify.com/docs/sdks/client/ios', sdkPackage: 'confidence-sdk-swift', detect: async (dir) => { if (existsSync(join(dir, 'Package.swift'))) return true; diff --git a/packages/core/src/frameworks/types.ts b/packages/core/src/frameworks/types.ts index 51a06e3..46c10b4 100644 --- a/packages/core/src/frameworks/types.ts +++ b/packages/core/src/frameworks/types.ts @@ -1,5 +1,8 @@ +export type FrameworkId = + 'nextjs' | 'react' | 'swift' | 'kotlin' | 'java' | 'go' | 'python' | 'node'; + export type FrameworkConfig = { - id: string; + id: FrameworkId; name: string; docsUrl: string; sdkPackage: string; diff --git a/packages/core/src/integrations/claude/index.ts b/packages/core/src/integrations/claude/index.ts index a5ae41e..5ca741a 100644 --- a/packages/core/src/integrations/claude/index.ts +++ b/packages/core/src/integrations/claude/index.ts @@ -2,7 +2,7 @@ import type { IdeIntegration } from '../types.js'; import { launchChat } from './chat.js'; import { detectPlugin, installPlugin, updatePlugin } from './plugins.js'; import { skillsDir } from './paths.js'; -import { detectMcpStatuses, connectMcpServer } from './mcp.js'; +import { detectMcpStatuses, connectMcpServer, disconnectMcpServer } from './mcp.js'; import { runOnboarding } from './onboarding.js'; import { prepare } from './prepare.js'; @@ -19,4 +19,5 @@ export const claudeIntegration: IdeIntegration = { updatePlugin, detectMcpStatuses, connectMcpServer, + disconnectMcpServer, }; diff --git a/packages/core/src/integrations/claude/mcp.ts b/packages/core/src/integrations/claude/mcp.ts index 7312774..cb7ad8a 100644 --- a/packages/core/src/integrations/claude/mcp.ts +++ b/packages/core/src/integrations/claude/mcp.ts @@ -1,7 +1,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { join } from 'node:path'; import { execFile } from '../../exec/exec.js'; -import type { McpConnectOpts } from '../types.js'; +import type { McpConnectOpts, McpDisconnectOpts } from '../types.js'; import { type McpServerName, type McpServerStatus, @@ -26,7 +26,7 @@ export async function connectMcpServer(opts: McpConnectOpts): Promise { cwd: opts.projectDir, }); } catch { - // Not registered yet — that's fine + // Server may not be registered yet; the subsequent `mcp add` is idempotent } const headers: Record = { ...opts.serverHeaders }; @@ -53,6 +53,37 @@ export async function connectMcpServer(opts: McpConnectOpts): Promise { allowMcpToolsInSettings(opts.serverName, opts.projectDir); } +export async function disconnectMcpServer(opts: McpDisconnectOpts): Promise { + await execFile('claude', ['mcp', 'remove', '--scope', 'project', opts.serverName], { + cwd: opts.projectDir, + }); + + removeMcpToolsFromSettings(opts.serverName, opts.projectDir); +} + +function removeMcpToolsFromSettings(serverName: string, projectDir: string): void { + const settingsPath = join(projectDir, '.claude', 'settings.local.json'); + if (!existsSync(settingsPath)) return; + + let settings: ClaudeSettings; + try { + settings = JSON.parse(readFileSync(settingsPath, 'utf-8')) as ClaudeSettings; + } catch { + return; + } + + const toolPattern = `mcp__${serverName}__*`; + if (settings.permissions?.allow) { + settings.permissions.allow = settings.permissions.allow.filter((p) => p !== toolPattern); + } + + if (settings.enabledMcpjsonServers) { + settings.enabledMcpjsonServers = settings.enabledMcpjsonServers.filter((s) => s !== serverName); + } + + writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8'); +} + type ClaudeSettings = { permissions?: { allow?: string[]; @@ -75,7 +106,7 @@ function allowMcpToolsInSettings(serverName: string, projectDir: string): void { try { settings = JSON.parse(readFileSync(settingsPath, 'utf-8')) as ClaudeSettings; } catch { - // overwrite + // Corrupt JSON — fall through to overwrite with valid settings } } diff --git a/packages/core/src/integrations/codex/index.ts b/packages/core/src/integrations/codex/index.ts index 2aac0a1..4dc64de 100644 --- a/packages/core/src/integrations/codex/index.ts +++ b/packages/core/src/integrations/codex/index.ts @@ -2,7 +2,7 @@ import type { IdeIntegration } from '../types.js'; import { launchChat } from './chat.js'; import { detectPlugin, installPlugin, updatePlugin } from './plugins.js'; import { skillsDir } from './paths.js'; -import { detectMcpStatuses, connectMcpServer } from './mcp.js'; +import { detectMcpStatuses, connectMcpServer, disconnectMcpServer } from './mcp.js'; import { runOnboarding } from './onboarding.js'; import { prepare } from './prepare.js'; @@ -19,4 +19,5 @@ export const codexIntegration: IdeIntegration = { updatePlugin, detectMcpStatuses, connectMcpServer, + disconnectMcpServer, }; diff --git a/packages/core/src/integrations/codex/mcp.ts b/packages/core/src/integrations/codex/mcp.ts index 99b249a..ac40236 100644 --- a/packages/core/src/integrations/codex/mcp.ts +++ b/packages/core/src/integrations/codex/mcp.ts @@ -1,6 +1,6 @@ import { readFileSync, writeFileSync } from 'node:fs'; import { execFile } from '../../exec/exec.js'; -import type { McpConnectOpts } from '../types.js'; +import type { McpConnectOpts, McpDisconnectOpts } from '../types.js'; import { MCP_SERVERS, type McpServerName, @@ -22,7 +22,7 @@ export async function connectMcpServer(opts: McpConnectOpts): Promise { try { await execFile('codex', ['mcp', 'remove', opts.serverName]); } catch { - // Not registered yet — that's fine + // Server may not be registered yet; the subsequent `mcp add` is idempotent } await execFile('codex', ['mcp', 'add', opts.serverName, '--url', opts.serverUrl]); @@ -35,6 +35,10 @@ export async function connectMcpServer(opts: McpConnectOpts): Promise { patchHttpHeaders(opts.serverName, headers); } +export async function disconnectMcpServer(opts: McpDisconnectOpts): Promise { + await execFile('codex', ['mcp', 'remove', opts.serverName]); +} + function getRegisteredMcpNames(projectDir: string): McpServerName[] { const names = Object.keys(MCP_SERVERS) as McpServerName[]; const paths = [globalConfigPath(), projectConfigPath(projectDir)]; @@ -45,6 +49,7 @@ function getRegisteredMcpNames(projectDir: string): McpServerName[] { const content = readFileSync(configPath, 'utf-8'); return content.includes(`[mcp_servers.${name}]`) || content.includes(`"${name}"`); } catch { + // Config file doesn't exist — server is not registered in this scope return false; } }), @@ -63,6 +68,7 @@ function getStoredAuthToken(serverName: McpServerName): string | null { const match = section.match(/"Authorization"\s*=\s*"Bearer\s+([^"]+)"/); return match?.[1] ?? null; } catch { + // Config file missing or unreadable — treat as no stored token return null; } } @@ -105,6 +111,6 @@ export function patchHttpHeaders( writeFileSync(configPath, content, 'utf-8'); } catch { - // Best-effort — MCP still works without the header + // Config may not exist yet if `codex mcp add` failed; MCP still works without custom headers } } diff --git a/packages/core/src/integrations/cursor/index.ts b/packages/core/src/integrations/cursor/index.ts index 23e002d..342caca 100644 --- a/packages/core/src/integrations/cursor/index.ts +++ b/packages/core/src/integrations/cursor/index.ts @@ -1,6 +1,6 @@ import type { IdeIntegration } from '../types.js'; import { launchChat } from './chat.js'; -import { detectMcpStatuses, connectMcpServer } from './mcp.js'; +import { detectMcpStatuses, connectMcpServer, disconnectMcpServer } from './mcp.js'; import { runOnboarding } from './onboarding.js'; import { detectPlugin, installPlugin, updatePlugin } from './plugins.js'; import { skillsDir } from './paths.js'; @@ -19,4 +19,5 @@ export const cursorIntegration: IdeIntegration = { updatePlugin, detectMcpStatuses, connectMcpServer, + disconnectMcpServer, }; diff --git a/packages/core/src/integrations/cursor/mcp.ts b/packages/core/src/integrations/cursor/mcp.ts index 8d0f69c..1b54154 100644 --- a/packages/core/src/integrations/cursor/mcp.ts +++ b/packages/core/src/integrations/cursor/mcp.ts @@ -1,7 +1,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { join } from 'node:path'; import { execFile } from '../../exec/exec.js'; -import type { McpConnectOpts } from '../types.js'; +import type { McpConnectOpts, McpDisconnectOpts } from '../types.js'; import { type McpServerName, type McpServerStatus, @@ -35,7 +35,41 @@ export async function connectMcpServer(opts: McpConnectOpts): Promise { try { await execFile('cursor', ['agent', 'mcp', 'enable', opts.serverName]); } catch { - // cursor agent CLI may not be available + // `cursor` CLI is not always installed; config files were already written above + } +} + +export async function disconnectMcpServer(opts: McpDisconnectOpts): Promise { + removeMcpEntry(mcpConfigPath(opts.projectDir), opts.serverName); + removeMcpEntry(globalConfigPath(), opts.serverName); + removeCliPermission(cliConfigPath(opts.projectDir), opts.serverName); +} + +function removeMcpEntry(configPath: string, serverName: string): void { + if (!existsSync(configPath)) return; + try { + const config = JSON.parse(readFileSync(configPath, 'utf-8')) as Record; + const mcpServers = (config.mcpServers ?? {}) as Record; + delete mcpServers[serverName]; + config.mcpServers = mcpServers; + writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8'); + } catch { + // Corrupt or unreadable config — server is effectively unregistered already + } +} + +function removeCliPermission(configPath: string, serverName: string): void { + if (!existsSync(configPath)) return; + try { + const config = JSON.parse(readFileSync(configPath, 'utf-8')) as Record; + const permissions = (config.permissions ?? {}) as Record; + const allow = (permissions.allow ?? []) as string[]; + const rule = `Mcp(${serverName}:*)`; + permissions.allow = allow.filter((r) => r !== rule); + config.permissions = permissions; + writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8'); + } catch { + // Corrupt or unreadable config — stale permission rules are harmless } } @@ -45,7 +79,7 @@ function writeMcpEntry(configPath: string, serverName: string, entry: unknown): try { config = JSON.parse(readFileSync(configPath, 'utf-8')) as Record; } catch { - // overwrite if corrupt + // Corrupt JSON — fall through to overwrite with valid config } } else { mkdirSync(join(configPath, '..'), { recursive: true }); @@ -64,7 +98,7 @@ function writeCliPermission(configPath: string, serverName: string): void { try { config = JSON.parse(readFileSync(configPath, 'utf-8')) as Record; } catch { - // overwrite if corrupt + // Corrupt JSON — fall through to overwrite with valid config } } else { mkdirSync(join(configPath, '..'), { recursive: true }); diff --git a/packages/core/src/integrations/index.ts b/packages/core/src/integrations/index.ts index e12a4fb..7750f12 100644 --- a/packages/core/src/integrations/index.ts +++ b/packages/core/src/integrations/index.ts @@ -1,6 +1,7 @@ export type { IdeIntegration, McpConnectOpts, + McpDisconnectOpts, OnboardingOpts, OnboardingCallbacks, InstalledPlugin, @@ -21,5 +22,6 @@ export { loadMcpPreference, persistMcpPreference, clearMcpPreference, + callMcpTool, } from './mcp/index.js'; export { detectInstalledPlugins, prepareIde, installPlugin, updatePlugin } from './skills/index.js'; diff --git a/packages/core/src/integrations/mcp/client.ts b/packages/core/src/integrations/mcp/client.ts new file mode 100644 index 0000000..a35c0ed --- /dev/null +++ b/packages/core/src/integrations/mcp/client.ts @@ -0,0 +1,68 @@ +import { Client } from '@modelcontextprotocol/sdk/client'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; +import type { CallToolResult, TextContent } from '@modelcontextprotocol/sdk/types'; +import { MCP_SERVERS, type McpServerName } from './servers.js'; + +type McpClientOpts = { accessToken?: string }; +type McpConnection = AsyncDisposable & { client: Client }; + +async function connectMcpClient( + serverName: McpServerName, + opts?: McpClientOpts, +): Promise { + const server = MCP_SERVERS[serverName]; + const headers: Record = { ...server.headers }; + + if (opts?.accessToken) { + headers['Authorization'] = `Bearer ${opts.accessToken}`; + } + + const transport = new StreamableHTTPClientTransport(new URL(server.url), { + requestInit: { headers }, + }); + + const client = new Client({ + name: 'confidence-cli', + version: '1.0.0', + }); + + await client.connect(transport); + + return { + client, + [Symbol.asyncDispose]: () => client.close(), + }; +} + +function isTextContent(block: { type: string }): block is TextContent { + return block.type === 'text'; +} + +function extractText(result: CallToolResult): string { + return result.content + .filter(isTextContent) + .map((c) => c.text) + .join('\n'); +} + +export async function callMcpTool( + serverName: McpServerName, + toolName: string, + args: Record, + opts?: McpClientOpts, +): Promise { + await using conn = await connectMcpClient(serverName, opts); + + const result = (await conn.client.callTool({ + name: toolName, + arguments: args, + })) as CallToolResult; + + const text = extractText(result); + + if (result.isError) { + throw new Error(text || 'MCP tool call failed'); + } + + return text; +} diff --git a/packages/core/src/integrations/mcp/index.ts b/packages/core/src/integrations/mcp/index.ts index bb53462..7d83237 100644 --- a/packages/core/src/integrations/mcp/index.ts +++ b/packages/core/src/integrations/mcp/index.ts @@ -8,3 +8,4 @@ export { verifyMcpServer, } from './servers.js'; export { loadMcpPreference, persistMcpPreference, clearMcpPreference } from './preference.js'; +export { callMcpTool } from './client.js'; diff --git a/packages/core/src/integrations/types.ts b/packages/core/src/integrations/types.ts index 84ee861..1649a7d 100644 --- a/packages/core/src/integrations/types.ts +++ b/packages/core/src/integrations/types.ts @@ -16,6 +16,11 @@ export type McpConnectOpts = { accessToken?: string; }; +export type McpDisconnectOpts = { + serverName: string; + projectDir: string; +}; + export type OnboardingCallbacks = { onStatus: (text: string) => void; onStdout: (line: string) => void; @@ -49,6 +54,7 @@ export type IdeIntegration = { detectMcpStatuses: (projectDir: string) => Promise>; connectMcpServer: (opts: McpConnectOpts) => Promise; + disconnectMcpServer: (opts: McpDisconnectOpts) => Promise; runOnboarding: (opts: OnboardingOpts, callbacks: OnboardingCallbacks) => ChildProcess | null; diff --git a/packages/core/src/session/session.ts b/packages/core/src/session/session.ts index 1ede32f..b1d41a6 100644 --- a/packages/core/src/session/session.ts +++ b/packages/core/src/session/session.ts @@ -180,7 +180,7 @@ export function createSession(opts?: { isEmptyProject: false, detectedProviders: [], onboardingGoals: opts?.goals ?? [], - goalsPreset: (opts?.goals?.length ?? 0) > 0, + goalsPreset: opts?.goals !== undefined, onboardingStatus: '', reportFile: null, codeChanges: [], diff --git a/packages/core/src/system/index.ts b/packages/core/src/system/index.ts index 680c1ba..60f3c24 100644 --- a/packages/core/src/system/index.ts +++ b/packages/core/src/system/index.ts @@ -1,3 +1,4 @@ export { env, isCI, isDebug } from './env.js'; export { isDirectoryEmpty } from './fs.js'; export { checkNode, checkGit, runAllChecks } from './system-check.js'; +export { detectPackageManager, type PackageManager } from './package-manager.js'; diff --git a/packages/core/src/system/package-manager.ts b/packages/core/src/system/package-manager.ts new file mode 100644 index 0000000..5e32c7d --- /dev/null +++ b/packages/core/src/system/package-manager.ts @@ -0,0 +1,55 @@ +import { existsSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +export type PackageManager = + 'npm' | 'pnpm' | 'yarn' | 'bun' | 'pip' | 'go' | 'swift' | 'gradle' | 'maven'; + +const JS_PACKAGE_MANAGERS: PackageManager[] = ['pnpm', 'yarn', 'npm', 'bun'] as const; + +const LOCKFILE_TO_PM: [string, PackageManager][] = [ + ['pnpm-lock.yaml', 'pnpm'], + ['yarn.lock', 'yarn'], + ['bun.lockb', 'bun'], + ['bun.lock', 'bun'], + ['package-lock.json', 'npm'], + ['go.mod', 'go'], + ['Package.swift', 'swift'], + ['pom.xml', 'maven'], + ['build.gradle', 'gradle'], + ['build.gradle.kts', 'gradle'], + ['requirements.txt', 'pip'], + ['pyproject.toml', 'pip'], + ['Pipfile', 'pip'], + + // Polyglot projects (e.g. Go + JS tooling) + // should match the primary ecosystem first, + // hence `package.json` at the end: + ['package.json', 'npm'], +]; + +export function detectPackageManager(projectDir: string): PackageManager | null { + const fromManifest = readPackageManagerField(projectDir); + if (fromManifest) return fromManifest; + + for (const [file, pm] of LOCKFILE_TO_PM) { + if (existsSync(join(projectDir, file))) return pm; + } + + return null; +} + +function readPackageManagerField(projectDir: string): PackageManager | null { + const pkgPath = join(projectDir, 'package.json'); + if (!existsSync(pkgPath)) return null; + + try { + const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); + const field = pkg.packageManager as string | undefined; + if (!field) return null; + + const name = field.split('@')[0] as PackageManager; + return JS_PACKAGE_MANAGERS.includes(name) ? name : null; + } catch { + return null; + } +} diff --git a/packages/quickstart/__tests__/e2e/sdk-only.e2e.ts b/packages/quickstart/__tests__/e2e/sdk-only.e2e.ts new file mode 100644 index 0000000..f16303b --- /dev/null +++ b/packages/quickstart/__tests__/e2e/sdk-only.e2e.ts @@ -0,0 +1,60 @@ +import { + createSession, + navigatePastWelcome, + navigatePastAuth, + selectIdeAndOnboard, + readInvocation, +} from '@spotify-confidence/testing/e2e'; + +describe('--features none (SDK-only setup)', () => { + it('skips goal selection screen', async () => { + using session = createSession({ extraArgs: ['--features', 'none'] }); + + await navigatePastWelcome(session); + await navigatePastAuth(session); + + await session.waitForText('Which CLI agent would you like to use?'); + }); + + it('omits "Select features" from the task list', async () => { + using session = createSession({ extraArgs: ['--features', 'none'] }); + + await navigatePastWelcome(session); + await navigatePastAuth(session); + await session.waitForText('Which CLI agent would you like to use?'); + + expect(session.screen).not.toContain('Select features'); + }); + + it('produces an SDK-only onboarding prompt', async () => { + using session = createSession({ extraArgs: ['--features', 'none'] }); + + await navigatePastWelcome(session); + await navigatePastAuth(session); + await session.waitForText('Which CLI agent would you like to use?'); + await selectIdeAndOnboard(session, 0); + + const invocation = readInvocation(session.cwd); + + expect(invocation.prompt).toContain('Install the appropriate Confidence SDK'); + expect(invocation.prompt).toContain('searchDocumentation'); + expect(invocation.prompt).toContain( + 'do not configure providers, create flags, or add instrumentation', + ); + }); + + it('does not include feature flag, recording, or event sections in the prompt', async () => { + using session = createSession({ extraArgs: ['--features', 'none'] }); + + await navigatePastWelcome(session); + await navigatePastAuth(session); + await session.waitForText('Which CLI agent would you like to use?'); + await selectIdeAndOnboard(session, 0); + + const invocation = readInvocation(session.cwd); + + expect(invocation.prompt).not.toContain('analyze-project'); + expect(invocation.prompt).not.toContain('Session Recording'); + expect(invocation.prompt).not.toContain('instrument-events'); + }); +}); diff --git a/packages/quickstart/__tests__/features/onboarding/build-prompt.test.ts b/packages/quickstart/__tests__/features/onboarding/build-prompt.test.ts index d0f85e1..88dd68a 100644 --- a/packages/quickstart/__tests__/features/onboarding/build-prompt.test.ts +++ b/packages/quickstart/__tests__/features/onboarding/build-prompt.test.ts @@ -1,3 +1,4 @@ +import type { OnboardingGoal } from '@spotify-confidence/shared-kernel'; import { buildOnboardingPrompt } from '@features/onboarding/index.js'; describe('buildOnboardingPrompt', () => { @@ -132,6 +133,38 @@ describe('buildOnboardingPrompt', () => { }); }); + describe('with no goals (SDK-only setup)', () => { + const sdkOnlyOpts = { ...baseOpts, goals: [] as OnboardingGoal[] }; + + it('produces an SDK-only install prompt', () => { + const sut = buildOnboardingPrompt(sdkOnlyOpts); + + expect(sut).toContain('Install the appropriate Confidence SDK'); + expect(sut).toContain('react'); + }); + + it('does not include feature flag, recording, or event tracking sections', () => { + const sut = buildOnboardingPrompt(sdkOnlyOpts); + + expect(sut).not.toContain('integrateFeatureFlags'); + expect(sut).not.toContain('Session Recording'); + expect(sut).not.toContain('Event Tracking'); + }); + + it('instructs the agent to only install the SDK', () => { + const sut = buildOnboardingPrompt(sdkOnlyOpts); + + expect(sut).toContain('do not configure providers, create flags, or add instrumentation'); + }); + + it('references the docs tool for install instructions', () => { + const sut = buildOnboardingPrompt(sdkOnlyOpts); + + expect(sut).toContain('searchDocumentation'); + expect(sut).toContain('SDK install'); + }); + }); + describe('session recordings skill reference', () => { const recordingOpts = { ...baseOpts, diff --git a/packages/quickstart/bin/cli.ts b/packages/quickstart/bin/cli.ts index 1c743ac..bc3eb46 100644 --- a/packages/quickstart/bin/cli.ts +++ b/packages/quickstart/bin/cli.ts @@ -31,8 +31,8 @@ const cli = yargs(hideBin(process.argv)) .option('features', { type: 'string', array: true, - choices: ['flags', 'events', 'recordings'] as const, - describe: 'Pre-select onboarding features (skips feature selection screen)', + choices: ['none', 'flags', 'events', 'recordings'] as const, + describe: 'Pre-select onboarding features (use "none" for SDK-only setup)', }) .command(defaultCommand.name, defaultCommand.description, () => {}, defaultCommand.handler) .command('start', defaultCommand.description, () => {}, defaultCommand.handler) diff --git a/packages/quickstart/src/commands/default.ts b/packages/quickstart/src/commands/default.ts index 33fbfb8..e00f904 100644 --- a/packages/quickstart/src/commands/default.ts +++ b/packages/quickstart/src/commands/default.ts @@ -1,12 +1,6 @@ -import type { OnboardingGoal } from '@spotify-confidence/shared-kernel'; import type { Command } from './types.js'; import { startTui } from '@ui/start-tui.js'; - -const FEATURE_TO_GOAL: Record = { - flags: 'feature-flags', - events: 'event-tracking', - recordings: 'session-recordings', -}; +import { resolveGoals } from './resolve-goals.js'; export const defaultCommand: Command = { name: '$0', @@ -17,8 +11,7 @@ export const defaultCommand: Command = { const debug = Boolean(args.debug); const dir = args.dir as string | undefined; const noTelemetry = args.telemetry === false; - const features = args.features as string[] | undefined; - const goals = features?.map((f) => FEATURE_TO_GOAL[f]).filter(Boolean); + const goals = resolveGoals(args.features as string[] | undefined); if (noTelemetry) { process.env.CONFIDENCE_TELEMETRY = 'false'; diff --git a/packages/quickstart/src/commands/help.ts b/packages/quickstart/src/commands/help.ts index ccec845..a15789d 100644 --- a/packages/quickstart/src/commands/help.ts +++ b/packages/quickstart/src/commands/help.ts @@ -19,7 +19,7 @@ export const helpCommand: Command = { 'Options:', ' --dir Project directory to run the wizard in', ' --features Pre-select onboarding features (skips feature selection screen)', - ' Choices: flags, events, recordings', + ' Choices: none, flags, events, recordings', ' --dry-run Run without making real API calls', ' --debug Enable debug output and preserve terminal history', ' --no-telemetry Disable anonymous usage telemetry', diff --git a/packages/quickstart/src/commands/index.ts b/packages/quickstart/src/commands/index.ts index cdf5faa..97d8b2d 100644 --- a/packages/quickstart/src/commands/index.ts +++ b/packages/quickstart/src/commands/index.ts @@ -1,3 +1,4 @@ export { defaultCommand } from './default.js'; export { helpCommand } from './help.js'; +export { resolveGoals } from './resolve-goals.js'; export type { Command } from './types.js'; diff --git a/packages/quickstart/src/commands/resolve-goals.ts b/packages/quickstart/src/commands/resolve-goals.ts new file mode 100644 index 0000000..1c28400 --- /dev/null +++ b/packages/quickstart/src/commands/resolve-goals.ts @@ -0,0 +1,13 @@ +import type { OnboardingGoal } from '@spotify-confidence/shared-kernel'; + +const FEATURE_TO_GOAL: Record = { + flags: 'feature-flags', + events: 'event-tracking', + recordings: 'session-recordings', +}; + +export function resolveGoals(features?: string[]): OnboardingGoal[] | undefined { + if (!features) return undefined; + if (features.includes('none')) return []; + return features.map((f) => FEATURE_TO_GOAL[f]).filter(Boolean); +} diff --git a/packages/quickstart/src/features/onboarding/build-prompt.ts b/packages/quickstart/src/features/onboarding/build-prompt.ts index d673cb4..3820c38 100644 --- a/packages/quickstart/src/features/onboarding/build-prompt.ts +++ b/packages/quickstart/src/features/onboarding/build-prompt.ts @@ -12,6 +12,7 @@ import { integrateRecording } from './sections/recording.js'; import { instrumentEvents } from './sections/event-tracking.js'; import { generateReport } from './sections/report.js'; import { summary, rules } from './sections/summary.js'; +import { sdkOnly } from './sections/sdk-only.js'; type PromptOptions = { framework: string; @@ -32,6 +33,10 @@ export function buildOnboardingPrompt({ hasProviders = false, pluginInstallMethod = null, }: PromptOptions): string { + if (goals.length === 0) { + return sdkOnly(framework, projectDir, ide); + } + const steps = new StepCounter(isEmptyProject ? 2 : 1); const tools = buildToolVars(ide); diff --git a/packages/quickstart/src/features/onboarding/sections/sdk-only.ts b/packages/quickstart/src/features/onboarding/sections/sdk-only.ts new file mode 100644 index 0000000..7c30bd0 --- /dev/null +++ b/packages/quickstart/src/features/onboarding/sections/sdk-only.ts @@ -0,0 +1,8 @@ +import type { IdeId } from '@spotify-confidence/shared-kernel'; +import { buildToolVars } from '../tool-vars.js'; +import { loadStep } from '../steps/load.js'; + +export function sdkOnly(framework: string, projectDir: string, ide: IdeId): string { + const tools = buildToolVars(ide); + return loadStep('sdk-only.md', { FRAMEWORK: framework, PROJECT_DIR: projectDir, ...tools }); +} diff --git a/packages/quickstart/src/features/onboarding/steps/sdk-only.md b/packages/quickstart/src/features/onboarding/steps/sdk-only.md new file mode 100644 index 0000000..2a4bc8a --- /dev/null +++ b/packages/quickstart/src/features/onboarding/steps/sdk-only.md @@ -0,0 +1,30 @@ +Install the appropriate Confidence SDK into the {{FRAMEWORK}} project at {{PROJECT_DIR}}. +Follow these steps in order, printing a short status line before each one. + +## 1. Preflight + +Print "STATUS: Looking up SDK for {{FRAMEWORK}}..." + +Call {{DOCS_searchDocumentation}} with query "{{FRAMEWORK}} SDK install" to look up the recommended SDK package and installation instructions. If the call fails, continue — step 2 will fall back to the framework's default package. + +## 2. Install SDK + +Print "STATUS: Installing SDK..." + +Detect the project's package manager from its lockfile (pnpm-lock.yaml → pnpm, yarn.lock → yarn, package-lock.json → npm, go.sum → go, Pipfile.lock / requirements.txt → pip, Package.swift → swift). Install the SDK package recommended by the docs lookup; if unavailable, fall back to the framework's default SDK package. + +Do NOT set up feature flags, event tracking, or session recordings — only install the SDK dependency. + +## 3. Verify + +Print "STATUS: Verifying installation..." + +Read the project's dependency manifest (package.json, go.mod, requirements.txt, etc.) and confirm the SDK package appears. Print a one-line summary of what was installed. + +## Rules + +- Prefix every progress update with "STATUS: " — these are shown in the UI. +- Only install the SDK — do not configure providers, create flags, or add instrumentation. +- Use the project's existing package manager; never switch to a different one. +- Only create or modify files inside the project directory. +- If a step fails, print the error and continue with remaining steps. diff --git a/packages/quickstart/src/index.ts b/packages/quickstart/src/index.ts index 44852c4..0fb29cc 100644 --- a/packages/quickstart/src/index.ts +++ b/packages/quickstart/src/index.ts @@ -1,2 +1,3 @@ export { startTui } from '@ui/start-tui.js'; +export { resolveGoals } from '@commands/resolve-goals.js'; export type { StoreOptions } from '@ui/store.js'; diff --git a/packages/quickstart/src/ui/screens/select-goal/useGoalSelection.ts b/packages/quickstart/src/ui/screens/select-goal/useGoalSelection.ts index 096e343..1c52354 100644 --- a/packages/quickstart/src/ui/screens/select-goal/useGoalSelection.ts +++ b/packages/quickstart/src/ui/screens/select-goal/useGoalSelection.ts @@ -22,8 +22,7 @@ export function useGoalSelection(): GoalSelection { const recordingAvailable = !!session.framework && BROWSER_PLATFORMS.has(session.framework); - const goalsPreset = - session.onboardingGoals.length > 0 && !session.completedScreens.has(ScreenId.SelectGoal); + const goalsPreset = session.goalsPreset && !session.completedScreens.has(ScreenId.SelectGoal); const hasPresetRecordings = goalsPreset && session.onboardingGoals.includes('session-recordings'); diff --git a/packages/testing/src/scaffold/ide-scaffold.ts b/packages/testing/src/scaffold/ide-scaffold.ts new file mode 100644 index 0000000..fda8e18 --- /dev/null +++ b/packages/testing/src/scaffold/ide-scaffold.ts @@ -0,0 +1,33 @@ +import { mkdirSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; + +type ClaudeSettings = { + permissions?: { allow?: string[] }; + enabledMcpjsonServers?: string[]; +}; + +type CursorMcpConfig = { + mcpServers?: Record; +}; + +type CursorCliConfig = { + permissions?: { allow?: string[]; deny?: string[] }; +}; + +export function writeClaudeSettings(projectDir: string, settings: ClaudeSettings): void { + const dir = join(projectDir, '.claude'); + mkdirSync(dir, { recursive: true }); + writeFileSync(join(dir, 'settings.local.json'), JSON.stringify(settings, null, 2)); +} + +export function writeCursorMcpConfig(projectDir: string, config: CursorMcpConfig): void { + const dir = join(projectDir, '.cursor'); + mkdirSync(dir, { recursive: true }); + writeFileSync(join(dir, 'mcp.json'), JSON.stringify(config, null, 2)); +} + +export function writeCursorCliConfig(projectDir: string, config: CursorCliConfig): void { + const dir = join(projectDir, '.cursor'); + mkdirSync(dir, { recursive: true }); + writeFileSync(join(dir, 'cli.json'), JSON.stringify(config, null, 2)); +} diff --git a/packages/testing/src/scaffold/index.ts b/packages/testing/src/scaffold/index.ts index 53eb232..36e2b5f 100644 --- a/packages/testing/src/scaffold/index.ts +++ b/packages/testing/src/scaffold/index.ts @@ -1,3 +1,8 @@ export { createConfigDir } from './config-scaffold.js'; export { createProjectDir } from './project-scaffold.js'; +export { + writeClaudeSettings, + writeCursorMcpConfig, + writeCursorCliConfig, +} from './ide-scaffold.js'; export type { ProjectType } from './types.js'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c1629c0..6aa1d70 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,9 +46,15 @@ importers: packages/cli: dependencies: + '@inquirer/select': + specifier: ^5.2.5 + version: 5.2.5(@types/node@26.6.2) '@spotify-confidence/quickstart': specifier: workspace:* version: link:../quickstart + ora: + specifier: ^9.4.1 + version: 9.4.1 yargs: specifier: ^18.2.0 version: 18.2.0 @@ -95,6 +101,9 @@ importers: packages/core: dependencies: + '@modelcontextprotocol/sdk': + specifier: ^1.30.1 + version: 1.30.1(zod@4.4.3) '@spotify-confidence/shared-kernel': specifier: workspace:* version: link:../shared-kernel @@ -602,6 +611,12 @@ packages: resolution: {integrity: sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@hono/node-server@2.1.1': + resolution: {integrity: sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==} + engines: {node: '>=20'} + peerDependencies: + hono: ^4 + '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} @@ -632,6 +647,10 @@ packages: resolution: {integrity: sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + '@inquirer/ansi@2.0.8': + resolution: {integrity: sha512-WpQM+Ti6Z40EFwwt+uL2p4UabT+W179zHp6HhLVOzfbwnVn05IPO/eXIZXGNqcT1jbQ15SujNLzQ39k4QPPxBQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + '@inquirer/confirm@6.3.0': resolution: {integrity: sha512-pZHXJImFtERmSNMBHcjwuz8Ck5vEFEYNUZnwbb8aJpjHv/TwGuFErNxF2Hp8+V+pNJs2EYPMlyWscvFEqO9jOQ==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} @@ -650,10 +669,32 @@ packages: '@types/node': optional: true + '@inquirer/core@12.0.3': + resolution: {integrity: sha512-wsSy0sznmXwkty+2PzZwx00Cazc/E0r0B7mAzdGROz2Ct+DFZXaK7WDjGZvgjRldxH5ZhFVfF2lgkYrqgOw2KA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/figures@2.0.8': resolution: {integrity: sha512-tApbon79GM9ry56ja/Ud3SY2CL4TQsao9fIwDQbgTeNY55025GdMzQ2+UdegV/lx51VNGUB59M0v0nMpybYY4Q==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + '@inquirer/figures@2.0.9': + resolution: {integrity: sha512-EAWgUTGQ/Umgga51dE3B2PUHbufuXarDfg86uVgoSgNHNNQnyFKcOrQLWVqYMghuSyHh8+2HUH0Js9cTC1WAdg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + + '@inquirer/select@5.2.5': + resolution: {integrity: sha512-9kc15hr8r/kI+3DO/xLog5nOzTz1jqsHXa6JBFzmQKhkoJ8Slda1I1L/uD8ZSZ9tF1yp79wwXe7mclvX1rqR2Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/type@4.1.0': resolution: {integrity: sha512-FMiJpuHUG3Dk0ex+UIXkre7i+i4OcwHWk9YdcVtZHFwb/r2rnrU2ipTCNAB7A+QOP0ryzIcqOfy76fRyyvOEAw==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} @@ -663,6 +704,15 @@ packages: '@types/node': optional: true + '@inquirer/type@4.1.1': + resolution: {integrity: sha512-yJoHYrMnxIsJZCY+0Vb66Dy3he3kL3e2wOBKhoSwWWAzZAY82emlxwgprCtp6yRixvNRNq9ztfRWQYPNr3Go7A==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -688,6 +738,16 @@ packages: '@keyv/serialize@1.1.1': resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + '@modelcontextprotocol/sdk@1.30.1': + resolution: {integrity: sha512-H2HxLvC3HDNybePJaLdSrU1hhUK5iQw+WvV1b01myFyI7sdVGe1u/IPTE5D9fGCiJDVtgMV/lmFkQXLmQyIFYA==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@mswjs/interceptors@0.41.9': resolution: {integrity: sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==} engines: {node: '>=18'} @@ -1218,6 +1278,10 @@ packages: '@yuku-toolchain/types@0.8.7': resolution: {integrity: sha512-2Z53dNxAJL6UvFoIrDZvYf3zlO8s4VJK4O2hhaB4mXVwwpX/7ajtss3cmfqKvamlNLWyt9FSWs4eoYdlbxpnHA==} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1228,6 +1292,14 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} @@ -1289,6 +1361,10 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + body-parser@2.3.0: + resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} + engines: {node: '>=18'} + brace-expansion@5.0.12: resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} engines: {node: 20 || >=22} @@ -1298,6 +1374,10 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + cac@7.0.0: resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} engines: {node: '>=20.19.0'} @@ -1305,6 +1385,14 @@ packages: cacheable@2.5.0: resolution: {integrity: sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -1328,6 +1416,10 @@ packages: resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + cli-spinners@3.4.0: resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==} engines: {node: '>=18.20'} @@ -1359,6 +1451,18 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.1.0: + resolution: {integrity: sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==} + engines: {node: '>=18'} + conventional-changelog-angular@9.4.0: resolution: {integrity: sha512-HdxRxuS8bBXVIuo4V82gvSwAXT0vYQUizrjs/izmPg5JdDstr8v8I5hduGL3iQbG+o310dUDxC4+LetuS5hu9w==} engines: {node: '>=22'} @@ -1379,10 +1483,22 @@ packages: resolution: {integrity: sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + cosmiconfig-typescript-loader@6.3.0: resolution: {integrity: sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==} engines: {node: '>=v18'} @@ -1426,6 +1542,10 @@ packages: defu@6.1.7: resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -1442,6 +1562,13 @@ packages: oxc-resolver: optional: true + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + electron-to-chromium@1.5.415: resolution: {integrity: sha512-958V+Kbhtgz+SxXeEVKBjrlKRBIDAYvUJfwhjxMZ5S6ut9jAl7l9ZKBkBrvjyjZE36PabLUo2L8kEeV5O4vgJg==} @@ -1455,6 +1582,10 @@ packages: resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==} engines: {node: '>=14'} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -1466,9 +1597,21 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@2.3.2: resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + es-toolkit@1.51.0: resolution: {integrity: sha512-zC2lQGkM7QX+Gm6iM3+WIdZJzthsEd14LvRNJneSO2hzyz/zNBENR8+YXWo1cKxgPBtV6ksPYHELbcwBRzmdCw==} @@ -1484,6 +1627,9 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@2.0.0: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} @@ -1549,10 +1695,32 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventsource-parser@3.1.1: + resolution: {integrity: sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + expect-type@1.4.0: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} + express-rate-limit@8.7.0: + resolution: {integrity: sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1590,6 +1758,10 @@ packages: file-entry-cache@11.1.5: resolution: {integrity: sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -1600,11 +1772,22 @@ packages: flatted@3.4.4: resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -1617,6 +1800,14 @@ packages: resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} engines: {node: '>=18'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-tsconfig@5.0.0-beta.5: resolution: {integrity: sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ==} engines: {node: '>=20.20.0'} @@ -1629,14 +1820,26 @@ packages: resolution: {integrity: sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w==} engines: {node: '>=20'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graphql@16.14.2: resolution: {integrity: sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + hashery@1.5.1: resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} engines: {node: '>=20'} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + headers-polyfill@5.0.1: resolution: {integrity: sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==} @@ -1646,6 +1849,10 @@ packages: hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + hono@4.13.9: + resolution: {integrity: sha512-7dMkQmZoC4E6F7AtaQSPhlWAdnBti+j7rreMZl8QB4jFiEhP9TWbGWUMi8WYzBCgmgulxuvLQupKqo+Co6Omyg==} + engines: {node: '>=16.9.0'} + hookable@6.1.1: resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==} @@ -1655,11 +1862,19 @@ packages: hookified@2.2.0: resolution: {integrity: sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} hasBin: true + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1684,6 +1899,9 @@ packages: resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} engines: {node: '>=12'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ini@6.0.0: resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==} engines: {node: ^20.17.0 || >=22.9.0} @@ -1710,6 +1928,14 @@ packages: react-devtools-core: optional: true + ip-address@10.7.2: + resolution: {integrity: sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} @@ -1734,6 +1960,10 @@ packages: engines: {node: '>=20'} hasBin: true + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + is-node-process@1.2.0: resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} @@ -1741,6 +1971,9 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-unicode-supported@2.1.0: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} @@ -1756,6 +1989,9 @@ packages: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true + jose@6.2.12: + resolution: {integrity: sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1777,6 +2013,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -1804,6 +2043,10 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + log-symbols@7.0.1: + resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} + engines: {node: '>=18'} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -1814,10 +2057,34 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.1: + resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + minimatch@10.2.6: resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} @@ -1851,6 +2118,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@1.1.0: + resolution: {integrity: sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==} + engines: {node: '>=18'} + node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} @@ -1861,18 +2132,41 @@ packages: resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==} engines: {node: '>=18'} + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + obug@2.1.4: resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} engines: {node: '>=12.20.0'} + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + ora@9.4.1: + resolution: {integrity: sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==} + engines: {node: '>=20'} + outvariant@1.4.3: resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} @@ -1892,6 +2186,10 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + patch-console@2.0.0: resolution: {integrity: sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1907,6 +2205,9 @@ packages: path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -1917,6 +2218,10 @@ packages: resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} engines: {node: '>=12'} + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + postcss@8.5.28: resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} engines: {node: ^10 || ^12 || >=14} @@ -1934,6 +2239,10 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + proxy-addr@2.0.8: + resolution: {integrity: sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==} + engines: {node: '>= 0.10'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1942,9 +2251,21 @@ packages: resolution: {integrity: sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==} engines: {node: '>=20'} + qs@6.16.0: + resolution: {integrity: sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==} + engines: {node: '>=0.6'} + quansync@1.0.0: resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + range-parser@1.3.0: + resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + react-dom@19.2.7: resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} peerDependencies: @@ -1986,6 +2307,10 @@ packages: resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + rettime@0.11.11: resolution: {integrity: sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==} @@ -2018,6 +2343,13 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -2030,9 +2362,20 @@ packages: engines: {node: '>=10'} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + set-cookie-parser@3.1.2: resolution: {integrity: sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==} + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2041,6 +2384,22 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -2073,6 +2432,10 @@ packages: std-env@4.2.0: resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + stdin-discarder@0.3.2: + resolution: {integrity: sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==} + engines: {node: '>=18'} + strict-event-emitter@0.5.1: resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} @@ -2134,6 +2497,10 @@ packages: resolution: {integrity: sha512-aBiNayCfTQxuIJBm06M+xR14cYaYlDlSXZbgsnKzKNxDKUVq7KFwTjwBSsb7m9Y5xO8WfPnBc63WaYFMTGlvqw==} hasBin: true + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + tough-cookie@6.0.2: resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} engines: {node: '>=16'} @@ -2195,6 +2562,10 @@ packages: resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==} engines: {node: '>=20'} + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + typescript-eslint@8.70.1: resolution: {integrity: sha512-AcWG7KDjZ2THNXsgwttMaGmzVi0VFRlFYfqFHYQRbDpF3owuYbuiL8c7UUrd2k8s3PoSfIQrWfrGXfcElrWLYA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2213,6 +2584,10 @@ packages: undici-types@8.9.0: resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + until-async@3.0.2: resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} @@ -2225,6 +2600,10 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + verkit@0.3.2: resolution: {integrity: sha512-zj/ob3UsvJGN0whEAKFp53REA5X66hvffVqoCtVQAakJKnKlH+/PcOfMoFwIG/o4rElqLv/ycAFlx8ZlXUorCg==} engines: {node: '>=18.12.0'} @@ -2340,6 +2719,9 @@ packages: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.21.3: resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} engines: {node: '>=10.0.0'} @@ -2384,6 +2766,10 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yoctocolors@2.2.0: + resolution: {integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==} + engines: {node: '>=18'} + yoga-layout@3.2.1: resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} @@ -2396,6 +2782,11 @@ packages: yuku-parser@0.8.7: resolution: {integrity: sha512-vRD9nwt4L3aYpxNqeSC4WqLv58xrXef0Ong1Mc45CTXTIpvLafx7JO05sczmQZwdLEZvywrLOGdNC5+Rp5N1BQ==} + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + zod-validation-error@4.0.2: resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} engines: {node: '>=18.0.0'} @@ -2757,6 +3148,10 @@ snapshots: '@eslint/core': 1.2.1 levn: 0.4.1 + '@hono/node-server@2.1.1(hono@4.13.9)': + dependencies: + hono: 4.13.9 + '@humanfs/core@0.19.2': dependencies: '@humanfs/types': 0.15.0 @@ -2783,6 +3178,8 @@ snapshots: '@inquirer/ansi@2.0.7': {} + '@inquirer/ansi@2.0.8': {} + '@inquirer/confirm@6.3.0(@types/node@26.6.2)': dependencies: '@inquirer/core': 12.0.1(@types/node@26.6.2) @@ -2802,12 +3199,39 @@ snapshots: optionalDependencies: '@types/node': 26.6.2 + '@inquirer/core@12.0.3(@types/node@26.6.2)': + dependencies: + '@inquirer/ansi': 2.0.8 + '@inquirer/figures': 2.0.9 + '@inquirer/type': 4.1.1(@types/node@26.6.2) + cli-width: 4.1.0 + fast-wrap-ansi: 0.2.2 + mute-stream: 3.0.0 + signal-exit: 4.1.0 + optionalDependencies: + '@types/node': 26.6.2 + '@inquirer/figures@2.0.8': {} + '@inquirer/figures@2.0.9': {} + + '@inquirer/select@5.2.5(@types/node@26.6.2)': + dependencies: + '@inquirer/ansi': 2.0.8 + '@inquirer/core': 12.0.3(@types/node@26.6.2) + '@inquirer/figures': 2.0.9 + '@inquirer/type': 4.1.1(@types/node@26.6.2) + optionalDependencies: + '@types/node': 26.6.2 + '@inquirer/type@4.1.0(@types/node@26.6.2)': optionalDependencies: '@types/node': 26.6.2 + '@inquirer/type@4.1.1(@types/node@26.6.2)': + optionalDependencies: + '@types/node': 26.6.2 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2835,6 +3259,28 @@ snapshots: '@keyv/serialize@1.1.1': {} + '@modelcontextprotocol/sdk@1.30.1(zod@4.4.3)': + dependencies: + '@hono/node-server': 2.1.1(hono@4.13.9) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.1.1 + express: 5.2.1 + express-rate-limit: 8.7.0(express@5.2.1) + hono: 4.13.9 + jose: 6.2.12 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.4.3 + zod-to-json-schema: 3.25.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color + '@mswjs/interceptors@0.41.9': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -3261,12 +3707,21 @@ snapshots: '@yuku-toolchain/types@0.8.7': {} + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.1.0 + acorn-jsx@5.3.2(acorn@8.18.0): dependencies: acorn: 8.18.0 acorn@8.18.0: {} + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + ajv@6.15.0: dependencies: fast-deep-equal: 3.1.3 @@ -3315,6 +3770,20 @@ snapshots: baseline-browser-mapping@2.11.19: {} + body-parser@2.3.0: + dependencies: + bytes: 3.1.2 + content-type: 2.1.0 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + on-finished: 2.4.1 + qs: 6.16.0 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + brace-expansion@5.0.12: dependencies: balanced-match: 4.0.4 @@ -3327,6 +3796,8 @@ snapshots: node-releases: 2.0.53 update-browserslist-db: 1.3.1(browserslist@4.28.8) + bytes@3.1.2: {} + cac@7.0.0: {} cacheable@2.5.0: @@ -3337,6 +3808,16 @@ snapshots: keyv: 5.6.0 qified: 0.10.1 + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + callsites@3.1.0: {} caniuse-lite@1.0.30001810: {} @@ -3351,6 +3832,10 @@ snapshots: dependencies: restore-cursor: 4.0.0 + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + cli-spinners@3.4.0: {} cli-truncate@6.1.1: @@ -3382,6 +3867,12 @@ snapshots: color-name@1.1.4: {} + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + content-type@2.1.0: {} + conventional-changelog-angular@9.4.0: dependencies: '@conventional-changelog/template': 1.4.0 @@ -3399,8 +3890,17 @@ snapshots: convert-to-spaces@2.0.1: {} + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + cookie@1.1.1: {} + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cosmiconfig-typescript-loader@6.3.0(@types/node@26.6.2)(cosmiconfig@9.0.2(typescript@6.0.3))(typescript@6.0.3): dependencies: '@types/node': 26.6.2 @@ -3435,12 +3935,22 @@ snapshots: defu@6.1.7: {} + depd@2.0.0: {} + dequal@2.0.3: {} dom-accessibility-api@0.5.16: {} dts-resolver@3.0.0: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + electron-to-chromium@1.5.415: {} emoji-regex@10.6.0: {} @@ -3449,6 +3959,8 @@ snapshots: empathic@2.0.1: {} + encodeurl@2.0.0: {} + env-paths@2.2.1: {} environment@1.1.0: {} @@ -3457,8 +3969,16 @@ snapshots: dependencies: is-arrayish: 0.2.1 + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@2.3.2: {} + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + es-toolkit@1.51.0: {} es-toolkit@1.52.0: {} @@ -3494,6 +4014,8 @@ snapshots: escalade@3.2.0: {} + escape-html@1.0.3: {} + escape-string-regexp@2.0.0: {} escape-string-regexp@4.0.0: {} @@ -3583,8 +4105,57 @@ snapshots: esutils@2.0.3: {} + etag@1.8.1: {} + + eventsource-parser@3.1.1: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.1.1 + expect-type@1.4.0: {} + express-rate-limit@8.7.0(express@5.2.1): + dependencies: + debug: 4.4.3 + express: 5.2.1 + ip-address: 10.7.2 + transitivePeerDependencies: + - supports-color + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.3.0 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.8 + qs: 6.16.0 + range-parser: 1.3.0 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + fast-deep-equal@3.1.3: {} fast-json-stable-stringify@2.1.0: {} @@ -3615,6 +4186,17 @@ snapshots: dependencies: flat-cache: 6.1.23 + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -3628,15 +4210,39 @@ snapshots: flatted@3.4.4: {} + forwarded@0.2.0: {} + + fresh@2.0.0: {} + fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} get-east-asian-width@1.6.0: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + get-tsconfig@5.0.0-beta.5: dependencies: resolve-pkg-maps: 1.0.0 @@ -3649,12 +4255,20 @@ snapshots: dependencies: ini: 6.0.0 + gopd@1.2.0: {} + graphql@16.14.2: {} + has-symbols@1.1.0: {} + hashery@1.5.1: dependencies: hookified: 1.15.1 + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + headers-polyfill@5.0.1: dependencies: '@types/set-cookie-parser': 2.4.10 @@ -3666,14 +4280,28 @@ snapshots: dependencies: hermes-estree: 0.25.1 + hono@4.13.9: {} + hookable@6.1.1: {} hookified@1.15.1: {} hookified@2.2.0: {} + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + husky@9.1.7: {} + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + ignore@5.3.2: {} ignore@7.0.10: {} @@ -3689,6 +4317,8 @@ snapshots: indent-string@5.0.0: {} + inherits@2.0.4: {} + ini@6.0.0: {} ink-testing-library@4.0.0(@types/react@19.3.0): @@ -3729,6 +4359,10 @@ snapshots: - bufferutil - utf-8-validate + ip-address@10.7.2: {} + + ipaddr.js@1.9.1: {} + is-arrayish@0.2.1: {} is-extglob@2.1.1: {} @@ -3745,10 +4379,14 @@ snapshots: is-in-ci@2.0.0: {} + is-interactive@2.0.0: {} + is-node-process@1.2.0: {} is-plain-obj@4.1.0: {} + is-promise@4.0.0: {} + is-unicode-supported@2.1.0: {} isexe@2.0.0: {} @@ -3758,6 +4396,8 @@ snapshots: jiti@2.7.0: optional: true + jose@6.2.12: {} + js-tokens@4.0.0: {} js-yaml@5.4.2: @@ -3772,6 +4412,8 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema-typed@8.0.2: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@2.2.3: {} @@ -3799,6 +4441,11 @@ snapshots: dependencies: p-locate: 5.0.0 + log-symbols@7.0.1: + dependencies: + is-unicode-supported: 2.1.0 + yoctocolors: 2.2.0 + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -3809,8 +4456,22 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + math-intrinsics@1.1.0: {} + + media-typer@1.1.1: {} + + merge-descriptors@2.0.0: {} + + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + mimic-fn@2.1.0: {} + mimic-function@5.0.1: {} + minimatch@10.2.6: dependencies: brace-expansion: 5.0.12 @@ -3850,6 +4511,10 @@ snapshots: natural-compare@1.4.0: {} + negotiator@1.1.0: + dependencies: + content-type: 2.1.0 + node-addon-api@7.1.1: {} node-pty@1.2.0-beta.14: @@ -3858,12 +4523,28 @@ snapshots: node-releases@2.0.53: {} + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + obug@2.1.4: {} + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + onetime@5.1.2: dependencies: mimic-fn: 2.1.0 + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -3873,6 +4554,17 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + ora@9.4.1: + dependencies: + chalk: 5.6.2 + cli-cursor: 5.0.0 + cli-spinners: 3.4.0 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 7.0.1 + stdin-discarder: 0.3.2 + string-width: 8.2.2 + outvariant@1.4.3: {} p-limit@3.1.0: @@ -3894,6 +4586,8 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parseurl@1.3.3: {} + patch-console@2.0.0: {} path-exists@4.0.0: {} @@ -3902,12 +4596,16 @@ snapshots: path-to-regexp@6.3.0: {} + path-to-regexp@8.4.2: {} + pathe@2.0.3: {} picocolors@1.1.1: {} picomatch@4.0.7: {} + pkce-challenge@5.0.1: {} + postcss@8.5.28: dependencies: nanoid: 3.3.19 @@ -3924,14 +4622,33 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 + proxy-addr@2.0.8: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + punycode@2.3.1: {} qified@0.10.1: dependencies: hookified: 2.2.0 + qs@6.16.0: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + quansync@1.0.0: {} + range-parser@1.3.0: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + unpipe: 1.0.0 + react-dom@19.2.7(react@19.3.0): dependencies: react: 19.3.0 @@ -3961,6 +4678,11 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + rettime@0.11.11: {} rolldown-plugin-dts@0.27.14(rolldown@1.2.6)(typescript@6.0.3): @@ -4030,20 +4752,87 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.63.5 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + + safer-buffer@2.1.2: {} + scheduler@0.27.0: {} semver@6.3.1: {} semver@7.8.5: {} + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.3.0 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + set-cookie-parser@3.1.2: {} + setprototypeof@1.2.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 shebang-regex@3.0.0: {} + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@3.0.7: {} @@ -4067,6 +4856,8 @@ snapshots: std-env@4.2.0: {} + stdin-discarder@0.3.2: {} + strict-event-emitter@0.5.1: {} string-argv@0.3.2: {} @@ -4119,6 +4910,8 @@ snapshots: dependencies: tldts-core: 7.4.11 + toidentifier@1.0.1: {} + tough-cookie@6.0.2: dependencies: tldts: 7.4.11 @@ -4169,6 +4962,12 @@ snapshots: dependencies: tagged-tag: 1.0.0 + type-is@2.1.0: + dependencies: + content-type: 2.1.0 + media-typer: 1.1.1 + mime-types: 3.0.2 + typescript-eslint@8.70.1(eslint@10.11.0(jiti@2.7.0))(typescript@6.0.3): dependencies: '@typescript-eslint/eslint-plugin': 8.70.1(@typescript-eslint/parser@8.70.1(eslint@10.11.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.11.0(jiti@2.7.0))(typescript@6.0.3) @@ -4189,6 +4988,8 @@ snapshots: undici-types@8.9.0: {} + unpipe@1.0.0: {} + until-async@3.0.2: {} update-browserslist-db@1.3.1(browserslist@4.28.8): @@ -4201,6 +5002,8 @@ snapshots: dependencies: punycode: 2.3.1 + vary@1.1.2: {} + verkit@0.3.2: {} vite@7.3.6(@types/node@26.6.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1): @@ -4277,6 +5080,8 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.2.0 + wrappy@1.0.2: {} + ws@8.21.3: {} y18n@5.0.8: {} @@ -4311,6 +5116,8 @@ snapshots: yocto-queue@0.1.0: {} + yoctocolors@2.2.0: {} + yoga-layout@3.2.1: {} yuku-ast@0.8.7: @@ -4352,6 +5159,10 @@ snapshots: '@yuku-parser/binding-win32-arm64': 0.8.7 '@yuku-parser/binding-win32-x64': 0.8.7 + zod-to-json-schema@3.25.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + zod-validation-error@4.0.2(zod@4.4.3): dependencies: zod: 4.4.3