diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 9fcdf051..b72eee3b 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -30,7 +30,9 @@ "scripts": { "build": "tsc", "dev": "tsc --watch", - "start": "node dist/index.js" + "start": "node dist/index.js", + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.27.1", @@ -38,7 +40,8 @@ }, "devDependencies": { "@types/node": "^22.19.15", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "vitest": "^4.1.1" }, "repository": { "type": "git", diff --git a/packages/mcp/src/__tests__/utils.test.ts b/packages/mcp/src/__tests__/utils.test.ts new file mode 100644 index 00000000..3a6ca40c --- /dev/null +++ b/packages/mcp/src/__tests__/utils.test.ts @@ -0,0 +1,218 @@ +import { describe, it, expect } from 'vitest'; +import { + isJsonResponse, + isLicenseError, + licenseErrorResult, + resolveInstanceId, + buildQuery, + formatProposalText, + getArgValue, + parseErrorResponse, +} from '../utils.js'; + +// --- isJsonResponse --- + +describe('isJsonResponse', () => { + const makeRes = (ct: string) => ({ + headers: { get: (key: string) => (key === 'content-type' ? ct : null) }, + }); + + it('returns true for application/json', () => { + expect(isJsonResponse(makeRes('application/json'))).toBe(true); + }); + + it('returns true for application/json; charset=utf-8', () => { + expect(isJsonResponse(makeRes('application/json; charset=utf-8'))).toBe(true); + }); + + it('returns false for text/html', () => { + expect(isJsonResponse(makeRes('text/html'))).toBe(false); + }); + + it('returns false when content-type is missing', () => { + expect(isJsonResponse(makeRes(''))).toBe(false); + }); +}); + +// --- isLicenseError --- + +describe('isLicenseError', () => { + it('returns true for a license error payload', () => { + expect( + isLicenseError({ + __licenseError: true, + requiredTier: 'Pro', + currentTier: 'community', + upgradeUrl: 'https://betterdb.com/pricing', + }), + ).toBe(true); + }); + + it('returns false for a normal object', () => { + expect(isLicenseError({ data: 'ok' })).toBe(false); + }); + + it('returns false for null', () => { + expect(isLicenseError(null)).toBe(false); + }); + + it('returns false for a string', () => { + expect(isLicenseError('error')).toBe(false); + }); + + it('returns false when __licenseError is false', () => { + expect(isLicenseError({ __licenseError: false })).toBe(false); + }); +}); + +// --- licenseErrorResult --- + +describe('licenseErrorResult', () => { + it('formats the message with all fields', () => { + const msg = licenseErrorResult({ + requiredTier: 'Pro or Enterprise', + currentTier: 'community', + upgradeUrl: 'https://betterdb.com/pricing', + }); + expect(msg).toBe( + 'This feature requires a Pro or Enterprise license (current tier: community). Upgrade at https://betterdb.com/pricing', + ); + }); +}); + +// --- resolveInstanceId --- + +describe('resolveInstanceId', () => { + it('returns the override id when provided', () => { + expect(resolveInstanceId('active-1', 'override-2')).toBe('override-2'); + }); + + it('falls back to activeInstanceId when no override', () => { + expect(resolveInstanceId('active-1')).toBe('active-1'); + }); + + it('throws when both activeInstanceId and override are absent', () => { + expect(() => resolveInstanceId(null)).toThrow( + 'No instance selected. Call list_instances then select_instance first.', + ); + }); + + it('throws for an id with invalid characters', () => { + expect(() => resolveInstanceId(null, 'bad id!')).toThrow('Invalid instance ID: bad id!'); + }); + + it('throws a clear error when override is an empty string', () => { + expect(() => resolveInstanceId('active-1', '')).toThrow( + 'Instance ID override must not be an empty string.', + ); + }); + + it('accepts alphanumeric, hyphens, and underscores', () => { + expect(resolveInstanceId(null, 'inst_abc-123')).toBe('inst_abc-123'); + }); +}); + +// --- buildQuery --- + +describe('buildQuery', () => { + it('returns empty string when all params are undefined', () => { + expect(buildQuery({ a: undefined, b: undefined })).toBe(''); + }); + + it('returns empty string for an empty object', () => { + expect(buildQuery({})).toBe(''); + }); + + it('builds a single-param query string', () => { + expect(buildQuery({ limit: 25 })).toBe('?limit=25'); + }); + + it('builds a multi-param query string', () => { + const qs = buildQuery({ startTime: 1000, endTime: 2000 }); + expect(qs).toBe('?startTime=1000&endTime=2000'); + }); + + it('omits undefined values', () => { + expect(buildQuery({ limit: 10, command: undefined })).toBe('?limit=10'); + }); + + it('percent-encodes spaces in values', () => { + expect(buildQuery({ q: 'a b' })).toBe('?q=a%20b'); + }); + + it('does not encode unreserved characters like dots', () => { + expect(buildQuery({ command: 'FT.SEARCH' })).toBe('?command=FT.SEARCH'); + }); + + it('percent-encodes special characters in keys', () => { + expect(buildQuery({ 'has space': 'val' })).toBe('?has%20space=val'); + }); +}); + +// --- formatProposalText --- + +describe('formatProposalText', () => { + const BASE = { + proposal_id: 'prop-abc', + status: 'pending', + expires_at: new Date('2025-01-01T00:00:00.000Z').getTime(), + warnings: [], + }; + + it('formats a proposal without warnings', () => { + const result = formatProposalText(BASE); + expect(result.content[0].text).toContain('Proposal created: prop-abc'); + expect(result.content[0].text).toContain('Status: pending'); + expect(result.content[0].text).toContain('Expires at: 2025-01-01T00:00:00.000Z'); + expect(result.content[0].text).not.toContain('Warnings:'); + }); + + it('includes warnings when present', () => { + const result = formatProposalText({ ...BASE, warnings: ['ttl too low', 'key missing'] }); + expect(result.content[0].text).toContain('Warnings: ttl too low; key missing'); + }); + + it('does not set isError', () => { + expect(formatProposalText(BASE).isError).toBeUndefined(); + }); +}); + +// --- getArgValue --- + +describe('getArgValue', () => { + it('returns the value following a flag', () => { + expect(getArgValue(['--port', '4000'], '--port', '3001')).toBe('4000'); + }); + + it('returns the fallback when the flag is absent', () => { + expect(getArgValue(['--storage', 'sqlite'], '--port', '3001')).toBe('3001'); + }); + + it('returns the fallback when the flag is the last arg (no value)', () => { + expect(getArgValue(['--port'], '--port', '3001')).toBe('3001'); + }); + + it('returns the fallback when the next token starts with --', () => { + expect(getArgValue(['--port', '--persist'], '--port', '3001')).toBe('3001'); + }); +}); + +// --- parseErrorResponse --- + +describe('parseErrorResponse', () => { + it('extracts error field from JSON', () => { + expect(parseErrorResponse(JSON.stringify({ error: 'not found' }), 404)).toBe('not found'); + }); + + it('extracts message field from JSON when error is absent', () => { + expect(parseErrorResponse(JSON.stringify({ message: 'forbidden' }), 403)).toBe('forbidden'); + }); + + it('returns raw text when not JSON', () => { + expect(parseErrorResponse('Bad gateway', 502)).toBe('Bad gateway'); + }); + + it('returns the status fallback when body is empty', () => { + expect(parseErrorResponse('', 500)).toBe('Request failed with status 500'); + }); +}); diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index 853af24f..d391125d 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -4,6 +4,17 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; import { initTelemetry, trackToolCall, stopTelemetry } from './telemetry.js'; +import { + isJsonResponse, + isLicenseError, + licenseErrorResult, + buildQuery, + formatProposalText, + getArgValue as getArgValuePure, + resolveInstanceId as resolveInstanceIdPure, + parseErrorResponse, + type ToolResult, +} from './utils.js'; // --- CLI arg parsing --- @@ -13,11 +24,7 @@ const PERSIST = args.includes('--persist'); const STOP = args.includes('--stop'); function getArgValue(flag: string, fallback: string): string { - const i = args.indexOf(flag); - if (i !== -1 && args[i + 1] && !args[i + 1].startsWith('--')) { - return args[i + 1]; - } - return fallback; + return getArgValuePure(args, flag, fallback); } const MONITOR_PORT = Number(getArgValue('--port', '3001')); @@ -64,11 +71,6 @@ async function rawFetch(prefix: string, path: string): Promise { return fetch(url, { headers, signal: AbortSignal.timeout(30_000) }); } -function isJsonResponse(res: Response): boolean { - const ct = res.headers.get('content-type') || ''; - return ct.includes('application/json'); -} - async function detectPrefix(): Promise { for (const prefix of API_PREFIXES) { try { @@ -116,15 +118,7 @@ async function apiRequest(method: string, path: string, body?: unknown): Promise if (!res.ok) { const errText = await res.text().catch(() => ''); - let message = `Request failed with status ${res.status}`; - try { - const parsed = JSON.parse(errText); - if (parsed.error) message = String(parsed.error); - else if (parsed.message) message = String(parsed.message); - } catch { - if (errText) message = errText; - } - throw new Error(message); + throw new Error(parseErrorResponse(errText, res.status)); } const text = await res.text(); @@ -135,29 +129,10 @@ async function apiFetch(path: string): Promise { return apiRequest('GET', path); } -function isLicenseError(data: unknown): data is { __licenseError: true; requiredTier: string; currentTier: string; upgradeUrl: string } { - return data != null && typeof data === 'object' && (data as any).__licenseError === true; -} - -function licenseErrorResult(data: { requiredTier: string; currentTier: string; upgradeUrl: string }): string { - return `This feature requires a ${data.requiredTier} license (current tier: ${data.currentTier}). Upgrade at ${data.upgradeUrl}`; -} - -const INSTANCE_ID_RE = /^[a-zA-Z0-9_-]+$/; - function resolveInstanceId(overrideId?: string): string { - const id = overrideId || activeInstanceId; - if (!id) { - throw new Error('No instance selected. Call list_instances then select_instance first.'); - } - if (!INSTANCE_ID_RE.test(id)) { - throw new Error(`Invalid instance ID: ${id}`); - } - return id; + return resolveInstanceIdPure(activeInstanceId, overrideId); } -type ToolResult = { content: Array<{ type: 'text'; text: string }>; isError?: boolean }; - async function withTelemetry(toolName: string, fn: () => Promise): Promise { const start = Date.now(); let success = true; @@ -446,14 +421,6 @@ server.tool( // --- Historical data tools --- -function buildQuery(params: Record): string { - const parts: string[] = []; - for (const [key, val] of Object.entries(params)) { - if (val !== undefined) parts.push(`${key}=${encodeURIComponent(String(val))}`); - } - return parts.length ? `?${parts.join('&')}` : ''; -} - server.tool( 'get_slowlog_patterns', 'Get analyzed slowlog patterns from persisted storage. Groups slow commands by normalized pattern, showing frequency, average duration, and example commands. Survives slowlog buffer rotation — data goes back as far as BetterDB has been running.', @@ -1190,21 +1157,6 @@ server.tool( }), ); -function formatProposalText(data: { proposal_id: string; status: string; expires_at: number; warnings: string[] }): ToolResult { - const expiresAtIso = new Date(data.expires_at).toISOString(); - const lines = [ - `Proposal created: ${data.proposal_id}`, - `Status: ${data.status}`, - `Expires at: ${expiresAtIso}`, - ]; - if (data.warnings && data.warnings.length > 0) { - lines.push(`Warnings: ${data.warnings.join('; ')}`); - } - return { - content: [{ type: 'text' as const, text: lines.join('\n') }], - }; -} - server.tool( 'stop_monitor', 'Stop a persistent BetterDB monitor process that was previously started with start_monitor or --autostart --persist.', diff --git a/packages/mcp/src/utils.ts b/packages/mcp/src/utils.ts new file mode 100644 index 00000000..374955ab --- /dev/null +++ b/packages/mcp/src/utils.ts @@ -0,0 +1,89 @@ +export type ToolResult = { content: Array<{ type: 'text'; text: string }>; isError?: boolean }; + +export type LicenseErrorPayload = { + __licenseError: true; + requiredTier: string; + currentTier: string; + upgradeUrl: string; +}; + +const INSTANCE_ID_RE = /^[a-zA-Z0-9_-]+$/; + +export function isJsonResponse(res: { headers: { get(name: string): string | null } }): boolean { + const ct = res.headers.get('content-type') || ''; + return ct.includes('application/json'); +} + +export function isLicenseError(data: unknown): data is LicenseErrorPayload { + return ( + data != null && + typeof data === 'object' && + (data as Record).__licenseError === true + ); +} + +export function licenseErrorResult(data: Pick): string { + return `This feature requires a ${data.requiredTier} license (current tier: ${data.currentTier}). Upgrade at ${data.upgradeUrl}`; +} + +export function resolveInstanceId(activeInstanceId: string | null, overrideId?: string): string { + if (overrideId === '') { + throw new Error('Instance ID override must not be an empty string.'); + } + const id = overrideId || activeInstanceId; + if (!id) { + throw new Error('No instance selected. Call list_instances then select_instance first.'); + } + if (!INSTANCE_ID_RE.test(id)) { + throw new Error(`Invalid instance ID: ${id}`); + } + return id; +} + +export function buildQuery(params: Record): string { + const parts: string[] = []; + for (const [key, val] of Object.entries(params)) { + if (val !== undefined) parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(val))}`); + } + return parts.length ? `?${parts.join('&')}` : ''; +} + +export function formatProposalText(data: { + proposal_id: string; + status: string; + expires_at: number; + warnings: string[]; +}): ToolResult { + const expiresAtIso = new Date(data.expires_at).toISOString(); + const lines = [ + `Proposal created: ${data.proposal_id}`, + `Status: ${data.status}`, + `Expires at: ${expiresAtIso}`, + ]; + if (data.warnings && data.warnings.length > 0) { + lines.push(`Warnings: ${data.warnings.join('; ')}`); + } + return { + content: [{ type: 'text' as const, text: lines.join('\n') }], + }; +} + +export function getArgValue(args: string[], flag: string, fallback: string): string { + const i = args.indexOf(flag); + if (i !== -1 && args[i + 1] && !args[i + 1].startsWith('--')) { + return args[i + 1]; + } + return fallback; +} + +export function parseErrorResponse(errText: string, status: number): string { + let message = `Request failed with status ${status}`; + try { + const parsed = JSON.parse(errText) as Record; + if (parsed.error) message = String(parsed.error); + else if (parsed.message) message = String(parsed.message); + } catch { + if (errText) message = errText; + } + return message; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2bd281c8..432d715e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -441,52 +441,6 @@ importers: specifier: ^5.9.3 version: 5.9.3 - packages/codebase-search: - dependencies: - '@modelcontextprotocol/sdk': - specifier: ^1.0.0 - version: 1.27.1(@cfworker/json-schema@4.1.1)(zod@3.25.76) - chokidar: - specifier: ^4.0.0 - version: 4.0.3 - glob: - specifier: 10.5.0 - version: 10.5.0 - ignore: - specifier: ^7.0.0 - version: 7.0.5 - iovalkey: - specifier: ^0.3.3 - version: 0.3.3 - tree-sitter: - specifier: ^0.22.0 - version: 0.22.4 - tree-sitter-go: - specifier: ^0.23.0 - version: 0.23.4(tree-sitter@0.22.4) - tree-sitter-javascript: - specifier: ^0.23.0 - version: 0.23.1(tree-sitter@0.22.4) - tree-sitter-python: - specifier: ^0.23.0 - version: 0.23.6(tree-sitter@0.22.4) - tree-sitter-rust: - specifier: ^0.23.0 - version: 0.23.3(tree-sitter@0.22.4) - tree-sitter-typescript: - specifier: ^0.23.0 - version: 0.23.2(tree-sitter@0.22.4) - zod: - specifier: ^3.23.0 - version: 3.25.76 - devDependencies: - '@types/node': - specifier: ^20.0.0 - version: 20.19.37 - typescript: - specifier: ^5.0.0 - version: 5.9.3 - packages/mcp: dependencies: '@modelcontextprotocol/sdk': @@ -502,6 +456,9 @@ importers: typescript: specifier: ^5.9.3 version: 5.9.3 + vitest: + specifier: ^4.1.1 + version: 4.1.1(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(happy-dom@20.8.9)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)) packages/semantic-cache: dependencies: @@ -1767,7 +1724,6 @@ packages: '@lancedb/lancedb@0.23.0': resolution: {integrity: sha512-aYrIoEG24AC+wILCL57Ius/Y4yU+xFHDPKLvmjzzN4byAjzeIGF0TC86S5RBt4Ji+dxS7yIWV5Q/gE5/fybIFQ==} engines: {node: '>= 18'} - cpu: [x64, arm64] os: [darwin, linux, win32] peerDependencies: apache-arrow: '>=15.0.0 <=18.1.0' @@ -4018,9 +3974,6 @@ packages: '@types/node@18.19.130': resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} - '@types/node@20.19.37': - resolution: {integrity: sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==} - '@types/node@22.19.15': resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} @@ -6842,10 +6795,6 @@ packages: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - node-gyp-build@4.8.4: - resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} - hasBin: true - node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} @@ -8142,49 +8091,6 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true - tree-sitter-go@0.23.4: - resolution: {integrity: sha512-iQaHEs4yMa/hMo/ZCGqLfG61F0miinULU1fFh+GZreCRtKylFLtvn798ocCZjO2r/ungNZgAY1s1hPFyAwkc7w==} - peerDependencies: - tree-sitter: ^0.21.1 - peerDependenciesMeta: - tree-sitter: - optional: true - - tree-sitter-javascript@0.23.1: - resolution: {integrity: sha512-/bnhbrTD9frUYHQTiYnPcxyHORIw157ERBa6dqzaKxvR/x3PC4Yzd+D1pZIMS6zNg2v3a8BZ0oK7jHqsQo9fWA==} - peerDependencies: - tree-sitter: ^0.21.1 - peerDependenciesMeta: - tree-sitter: - optional: true - - tree-sitter-python@0.23.6: - resolution: {integrity: sha512-yIM9z0oxKIxT7bAtPOhgoVl6gTXlmlIhue7liFT4oBPF/lha7Ha4dQBS82Av6hMMRZoVnFJI8M6mL+SwWoLD3A==} - peerDependencies: - tree-sitter: ^0.22.1 - peerDependenciesMeta: - tree-sitter: - optional: true - - tree-sitter-rust@0.23.3: - resolution: {integrity: sha512-uLdZJ1K26EuJTBMJlz1ltTlg7nJyAYThfouXgigf5ixKOasOL5wNrRCpuWTsl6rDcKlZK9UX+annFLqP/kchwQ==} - peerDependencies: - tree-sitter: ^0.22.1 - peerDependenciesMeta: - tree-sitter: - optional: true - - tree-sitter-typescript@0.23.2: - resolution: {integrity: sha512-e04JUUKxTT53/x3Uq1zIL45DoYKVfHH4CZqwgZhPg5qYROl5nQjV+85ruFzFGZxu+QeFVbRTPDRnqL9UbU4VeA==} - peerDependencies: - tree-sitter: ^0.21.0 - peerDependenciesMeta: - tree-sitter: - optional: true - - tree-sitter@0.22.4: - resolution: {integrity: sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg==} - ts-algebra@2.0.0: resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} @@ -12393,10 +12299,6 @@ snapshots: dependencies: undici-types: 5.26.5 - '@types/node@20.19.37': - dependencies: - undici-types: 6.21.0 - '@types/node@22.19.15': dependencies: undici-types: 6.21.0 @@ -14462,7 +14364,7 @@ snapshots: isstream: 0.1.2 jsonwebtoken: 9.0.3 mime-types: 2.1.35 - retry-axios: 2.6.0(axios@1.14.0(debug@4.4.3)) + retry-axios: 2.6.0(axios@1.14.0) tough-cookie: 4.1.4 transitivePeerDependencies: - supports-color @@ -15463,8 +15365,6 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 - node-gyp-build@4.8.4: {} - node-int64@0.4.0: {} node-releases@2.0.27: {} @@ -16286,7 +16186,7 @@ snapshots: ret@0.5.0: {} - retry-axios@2.6.0(axios@1.14.0(debug@4.4.3)): + retry-axios@2.6.0(axios@1.14.0): dependencies: axios: 1.14.0(debug@4.4.3) @@ -16889,47 +16789,6 @@ snapshots: tree-kill@1.2.2: {} - tree-sitter-go@0.23.4(tree-sitter@0.22.4): - dependencies: - node-addon-api: 8.5.0 - node-gyp-build: 4.8.4 - optionalDependencies: - tree-sitter: 0.22.4 - - tree-sitter-javascript@0.23.1(tree-sitter@0.22.4): - dependencies: - node-addon-api: 8.5.0 - node-gyp-build: 4.8.4 - optionalDependencies: - tree-sitter: 0.22.4 - - tree-sitter-python@0.23.6(tree-sitter@0.22.4): - dependencies: - node-addon-api: 8.5.0 - node-gyp-build: 4.8.4 - optionalDependencies: - tree-sitter: 0.22.4 - - tree-sitter-rust@0.23.3(tree-sitter@0.22.4): - dependencies: - node-addon-api: 8.5.0 - node-gyp-build: 4.8.4 - optionalDependencies: - tree-sitter: 0.22.4 - - tree-sitter-typescript@0.23.2(tree-sitter@0.22.4): - dependencies: - node-addon-api: 8.5.0 - node-gyp-build: 4.8.4 - tree-sitter-javascript: 0.23.1(tree-sitter@0.22.4) - optionalDependencies: - tree-sitter: 0.22.4 - - tree-sitter@0.22.4: - dependencies: - node-addon-api: 8.5.0 - node-gyp-build: 4.8.4 - ts-algebra@2.0.0: {} ts-api-utils@2.5.0(typescript@5.9.3): @@ -17204,11 +17063,11 @@ snapshots: magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 - picomatch: 4.0.3 + picomatch: 4.0.4 std-env: 4.0.0 tinybench: 2.9.0 tinyexec: 1.0.4 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 tinyrainbow: 3.1.0 vite: 8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2) why-is-node-running: 2.3.0