style: 完成所有文件的lint

This commit is contained in:
claude-code-best
2026-05-01 21:39:30 +08:00
parent d136872cc9
commit 6182015005
1333 changed files with 68255 additions and 77882 deletions

View File

@@ -13,7 +13,9 @@ describe('InProcessTransport', () => {
const [client, server] = createLinkedTransportPair()
let received: JSONRPCMessage | null = null
server.onmessage = (msg) => { received = msg }
server.onmessage = msg => {
received = msg
}
const message: JSONRPCMessage = {
jsonrpc: '2.0',
@@ -36,7 +38,9 @@ describe('InProcessTransport', () => {
const [client, server] = createLinkedTransportPair()
let received: JSONRPCMessage | null = null
client.onmessage = (msg) => { received = msg }
client.onmessage = msg => {
received = msg
}
await server.send({ jsonrpc: '2.0', result: 42, id: 1 } as any)
@@ -50,8 +54,12 @@ describe('InProcessTransport', () => {
let clientClosed = false
let serverClosed = false
client.onclose = () => { clientClosed = true }
server.onclose = () => { serverClosed = true }
client.onclose = () => {
clientClosed = true
}
server.onclose = () => {
serverClosed = true
}
await client.close()
@@ -63,7 +71,9 @@ describe('InProcessTransport', () => {
const [client] = createLinkedTransportPair()
let closeCount = 0
client.onclose = () => { closeCount++ }
client.onclose = () => {
closeCount++
}
await client.close()
await client.close()
@@ -75,6 +85,8 @@ describe('InProcessTransport', () => {
const [client] = createLinkedTransportPair()
await client.close()
expect(client.send({ jsonrpc: '2.0', method: 'test' } as any)).rejects.toThrow('Transport is closed')
expect(
client.send({ jsonrpc: '2.0', method: 'test' } as any),
).rejects.toThrow('Transport is closed')
})
})

View File

@@ -5,8 +5,11 @@ describe('memoizeWithLRU', () => {
test('caches results', () => {
let callCount = 0
const fn = memoizeWithLRU(
(x: number) => { callCount++; return x * 2 },
(x) => `key-${x}`,
(x: number) => {
callCount++
return x * 2
},
x => `key-${x}`,
10,
)
@@ -19,7 +22,7 @@ describe('memoizeWithLRU', () => {
test('evicts least recently used entries', () => {
const fn = memoizeWithLRU(
(x: number) => x,
(x) => `key-${x}`,
x => `key-${x}`,
2,
)
@@ -36,7 +39,7 @@ describe('memoizeWithLRU', () => {
test('cache.clear removes all entries', () => {
const fn = memoizeWithLRU(
(x: number) => x,
(x) => `key-${x}`,
x => `key-${x}`,
10,
)
@@ -51,7 +54,7 @@ describe('memoizeWithLRU', () => {
test('cache.delete removes specific entry', () => {
const fn = memoizeWithLRU(
(x: number) => x,
(x) => `key-${x}`,
x => `key-${x}`,
10,
)
@@ -65,7 +68,7 @@ describe('memoizeWithLRU', () => {
test('cache.get returns value without promoting', () => {
const fn = memoizeWithLRU(
(x: number) => x * 10,
(x) => `key-${x}`,
x => `key-${x}`,
2,
)

View File

@@ -21,7 +21,9 @@ describe('isTerminalConnectionError', () => {
})
test('detects ETIMEDOUT', () => {
expect(isTerminalConnectionError('Connection timed out: ETIMEDOUT')).toBe(true)
expect(isTerminalConnectionError('Connection timed out: ETIMEDOUT')).toBe(
true,
)
})
test('detects EPIPE', () => {
@@ -29,16 +31,22 @@ describe('isTerminalConnectionError', () => {
})
test('detects EHOSTUNREACH', () => {
expect(isTerminalConnectionError('Host unreachable: EHOSTUNREACH')).toBe(true)
expect(isTerminalConnectionError('Host unreachable: EHOSTUNREACH')).toBe(
true,
)
})
test('detects ECONNREFUSED', () => {
expect(isTerminalConnectionError('Connection refused: ECONNREFUSED')).toBe(true)
expect(isTerminalConnectionError('Connection refused: ECONNREFUSED')).toBe(
true,
)
})
test('detects SSE disconnection messages', () => {
expect(isTerminalConnectionError('SSE stream disconnected')).toBe(true)
expect(isTerminalConnectionError('Failed to reconnect SSE stream')).toBe(true)
expect(isTerminalConnectionError('Failed to reconnect SSE stream')).toBe(
true,
)
})
test('detects terminated', () => {
@@ -48,13 +56,17 @@ describe('isTerminalConnectionError', () => {
test('rejects non-terminal errors', () => {
expect(isTerminalConnectionError('some random error')).toBe(false)
expect(isTerminalConnectionError('')).toBe(false)
expect(isTerminalConnectionError('timeout waiting for response')).toBe(false)
expect(isTerminalConnectionError('timeout waiting for response')).toBe(
false,
)
})
})
describe('isMcpSessionExpiredError', () => {
test('detects 404 with JSON-RPC session-not-found code', () => {
const error = new Error('Not found: {"code":-32001,"message":"Session not found"}')
const error = new Error(
'Not found: {"code":-32001,"message":"Session not found"}',
)
Object.assign(error, { code: 404 })
expect(isMcpSessionExpiredError(error)).toBe(true)
})

View File

@@ -55,11 +55,20 @@ describe('discoverTools', () => {
expect(result).toHaveLength(1)
const tool = result[0]
expect(tool.name).toBe('mcp__my-server__search')
expect(tool.mcpInfo).toEqual({ serverName: 'my-server', toolName: 'search' })
expect(tool.mcpInfo).toEqual({
serverName: 'my-server',
toolName: 'search',
})
expect(tool.isMcp).toBe(true)
expect(tool.isReadOnly({} as any)).toBe(true)
expect(tool.userFacingName(undefined)).toBe('Search Items')
expect(await tool.description({} as any, { isNonInteractiveSession: false, toolPermissionContext: {}, tools: [] })).toBe('Search for items')
expect(
await tool.description({} as any, {
isNonInteractiveSession: false,
toolPermissionContext: {},
tools: [],
}),
).toBe('Search for items')
})
test('respects skipPrefix option', async () => {

View File

@@ -2,7 +2,11 @@ import { describe, expect, test, mock } from 'bun:test'
import { createMcpManager } from '../manager.js'
import type { McpManager } from '../manager.js'
import type { McpClientDependencies } from '../interfaces.js'
import type { ScopedMcpServerConfig, MCPServerConnection, ConnectedMCPServer } from '../types.js'
import type {
ScopedMcpServerConfig,
MCPServerConnection,
ConnectedMCPServer,
} from '../types.js'
import type { Client } from '@modelcontextprotocol/sdk/client/index.js'
function createMockDeps(): McpClientDependencies {
@@ -36,14 +40,17 @@ describe('createMcpManager', () => {
test('connect throws if connectFn not set', async () => {
const manager = createMcpManager(createMockDeps())
await expect(manager.connect('test', { command: 'npx', args: [] }))
.rejects.toThrow('connectFn not set')
await expect(
manager.connect('test', { command: 'npx', args: [] }),
).rejects.toThrow('connectFn not set')
})
test('connect calls connectFn and emits connected event', async () => {
const manager = createMcpManager(createMockDeps()) as any
let connectedEvent: string | null = null
manager.on('connected', (name: string) => { connectedEvent = name })
manager.on('connected', (name: string) => {
connectedEvent = name
})
const mockConnection: ConnectedMCPServer = {
type: 'connected',
@@ -53,17 +60,26 @@ describe('createMcpManager', () => {
onclose: null,
} as unknown as Client,
capabilities: {},
config: { command: 'npx', args: [], scope: 'dynamic' } as ScopedMcpServerConfig,
config: {
command: 'npx',
args: [],
scope: 'dynamic',
} as ScopedMcpServerConfig,
cleanup: mock(() => Promise.resolve()),
}
manager.setConnectFn(async (name: string, config: ScopedMcpServerConfig) => {
expect(name).toBe('test-server')
expect(config.scope).toBe('dynamic')
return mockConnection
})
manager.setConnectFn(
async (name: string, config: ScopedMcpServerConfig) => {
expect(name).toBe('test-server')
expect(config.scope).toBe('dynamic')
return mockConnection
},
)
const result = await manager.connect('test-server', { command: 'npx', args: [] })
const result = await manager.connect('test-server', {
command: 'npx',
args: [],
})
expect(result.type).toBe('connected')
expect(connectedEvent as unknown as string).toBe('test-server')
})
@@ -71,15 +87,23 @@ describe('createMcpManager', () => {
test('disconnect calls cleanup and emits disconnected', async () => {
const manager = createMcpManager(createMockDeps()) as any
let disconnected = false
manager.on('disconnected', () => { disconnected = true })
manager.on('disconnected', () => {
disconnected = true
})
const mockCleanup = mock(() => Promise.resolve())
const mockConnection: ConnectedMCPServer = {
type: 'connected',
name: 'test-server',
client: { request: mock(() => Promise.resolve({ tools: [] })) } as unknown as Client,
client: {
request: mock(() => Promise.resolve({ tools: [] })),
} as unknown as Client,
capabilities: {},
config: { command: 'npx', args: [], scope: 'dynamic' } as ScopedMcpServerConfig,
config: {
command: 'npx',
args: [],
scope: 'dynamic',
} as ScopedMcpServerConfig,
cleanup: mockCleanup,
}

View File

@@ -23,7 +23,9 @@ describe('normalizeNameForMCP', () => {
test('collapses underscores for claude.ai prefix', () => {
expect(normalizeNameForMCP('claude.ai Slack')).toBe('claude_ai_Slack')
expect(normalizeNameForMCP('claude.ai My Server')).toBe('claude_ai_My_Server')
expect(normalizeNameForMCP('claude.ai My Server')).toBe(
'claude_ai_My_Server',
)
})
})
@@ -33,7 +35,9 @@ describe('buildMcpToolName', () => {
})
test('normalizes server name with dots', () => {
expect(buildMcpToolName('test.server', 'tool')).toBe('mcp__test_server__tool')
expect(buildMcpToolName('test.server', 'tool')).toBe(
'mcp__test_server__tool',
)
})
})
@@ -68,10 +72,12 @@ describe('getMcpPrefix', () => {
describe('getToolNameForPermissionCheck', () => {
test('uses mcp prefix for MCP tools', () => {
expect(getToolNameForPermissionCheck({
name: 'query',
mcpInfo: { serverName: 'my-server', toolName: 'query' },
})).toBe('mcp__my-server__query')
expect(
getToolNameForPermissionCheck({
name: 'query',
mcpInfo: { serverName: 'my-server', toolName: 'query' },
}),
).toBe('mcp__my-server__query')
})
test('uses raw name for non-MCP tools', () => {
@@ -82,13 +88,17 @@ describe('getToolNameForPermissionCheck', () => {
describe('getMcpDisplayName', () => {
test('strips MCP prefix', () => {
// getMcpDisplayName normalizes server name before building prefix
expect(getMcpDisplayName('mcp__my_server__query', 'my.server')).toBe('query')
expect(getMcpDisplayName('mcp__my_server__query', 'my.server')).toBe(
'query',
)
})
})
describe('extractMcpToolDisplayName', () => {
test('removes MCP suffix', () => {
expect(extractMcpToolDisplayName('github - Add comment (MCP)')).toBe('Add comment')
expect(extractMcpToolDisplayName('github - Add comment (MCP)')).toBe(
'Add comment',
)
})
test('handles no dash', () => {
@@ -96,6 +106,8 @@ describe('extractMcpToolDisplayName', () => {
})
test('handles no suffix', () => {
expect(extractMcpToolDisplayName('github - Add comment')).toBe('Add comment')
expect(extractMcpToolDisplayName('github - Add comment')).toBe(
'Add comment',
)
})
})

View File

@@ -91,11 +91,7 @@ export async function withConnectionTimeout<T>(
const timeoutPromise = new Promise<never>((_, reject) => {
const timeoutId = setTimeout(async () => {
await onTimeout()
reject(
new Error(
`MCP connection timed out after ${timeoutMs}ms`,
),
)
reject(new Error(`MCP connection timed out after ${timeoutMs}ms`))
}, timeoutMs)
// Clean up timeout if connect resolves or rejects
@@ -119,7 +115,11 @@ export async function withConnectionTimeout<T>(
export function captureStderr(
transport: StdioClientTransport,
maxSize = 64 * 1024 * 1024,
): { getOutput: () => string; clearOutput: () => void; removeHandler: () => void } {
): {
getOutput: () => string
clearOutput: () => void
removeHandler: () => void
} {
let stderrOutput = ''
const handler = (data: Buffer) => {
@@ -136,8 +136,12 @@ export function captureStderr(
return {
getOutput: () => stderrOutput,
clearOutput: () => { stderrOutput = '' },
removeHandler: () => { transport.stderr?.off('data', handler) },
clearOutput: () => {
stderrOutput = ''
},
removeHandler: () => {
transport.stderr?.off('data', handler)
},
}
}
@@ -197,7 +201,13 @@ export function installConnectionMonitor(
client: Client,
options: ConnectionMonitorOptions,
): () => void {
const { serverName, transportType, logger, closeTransport, onConnectionClosed } = options
const {
serverName,
transportType,
logger,
closeTransport,
onConnectionClosed,
} = options
const connectionStartTime = Date.now()
let hasErrorOccurred = false
let consecutiveConnectionErrors = 0
@@ -310,6 +320,7 @@ export async function terminateWithSignalEscalation(
return
}
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: complex cleanup logic requires async in executor
await new Promise<void>(async resolve => {
let resolved = false
@@ -331,7 +342,9 @@ export async function terminateWithSignalEscalation(
if (!resolved) {
resolved = true
clearInterval(checkInterval)
logger.debug(`[${serverName}] Cleanup timeout reached, stopping process monitoring`)
logger.debug(
`[${serverName}] Cleanup timeout reached, stopping process monitoring`,
)
resolve()
}
}, 600)
@@ -348,7 +361,9 @@ export async function terminateWithSignalEscalation(
try {
process.kill(childPid, 'SIGTERM')
} catch (termError) {
logger.debug(`[${serverName}] Error sending SIGTERM: ${termError}`)
logger.debug(
`[${serverName}] Error sending SIGTERM: ${termError}`,
)
resolved = true
clearInterval(checkInterval)
clearTimeout(failsafeTimeout)
@@ -373,7 +388,9 @@ export async function terminateWithSignalEscalation(
try {
process.kill(childPid, 'SIGKILL')
} catch (killError) {
logger.debug(`[${serverName}] Error sending SIGKILL: ${killError}`)
logger.debug(
`[${serverName}] Error sending SIGKILL: ${killError}`,
)
}
} catch {
resolved = true
@@ -446,7 +463,9 @@ export function createCleanup(options: CleanupOptions): () => Promise<void> {
try {
await inProcessServer.close()
} catch (error) {
logger.debug(`[${serverName}] Error closing in-process server: ${error}`)
logger.debug(
`[${serverName}] Error closing in-process server: ${error}`,
)
}
try {
await client.close()
@@ -500,7 +519,8 @@ export function buildConnectedServer(
let instructions = rawInstructions
if (rawInstructions && rawInstructions.length > MAX_MCP_DESCRIPTION_LENGTH) {
instructions = rawInstructions.slice(0, MAX_MCP_DESCRIPTION_LENGTH) + '… [truncated]'
instructions =
rawInstructions.slice(0, MAX_MCP_DESCRIPTION_LENGTH) + '… [truncated]'
logger.debug(
`[${name}] Server instructions truncated from ${rawInstructions.length} to ${MAX_MCP_DESCRIPTION_LENGTH} chars`,
)

View File

@@ -44,7 +44,9 @@ export interface DiscoveryOptions {
* Fetches tools from a connected MCP server and converts them to CoreTool format.
* Returns empty array if the server doesn't support tools or if fetching fails.
*/
export async function discoverTools(options: DiscoveryOptions): Promise<CoreTool[]> {
export async function discoverTools(
options: DiscoveryOptions,
): Promise<CoreTool[]> {
const { serverName, client, capabilities, skipPrefix, deps } = options
if (!capabilities?.tools) {
@@ -89,7 +91,10 @@ export async function discoverTools(options: DiscoveryOptions): Promise<CoreTool
toAutoClassifierInput: () => '',
userFacingName: () => tool.annotations?.title ?? tool.name,
maxResultSizeChars: 100_000,
mapToolResultToToolResultBlockParam: (content: unknown, id: string) => ({
mapToolResultToToolResultBlockParam: (
content: unknown,
id: string,
) => ({
type: 'tool_result' as const,
tool_use_id: id,
content,
@@ -118,11 +123,17 @@ export function createCachedToolDiscovery(
deps: McpClientDependencies,
cacheSize: number = MCP_FETCH_CACHE_SIZE,
): {
discover: (server: ConnectedMCPServer, skipPrefix?: boolean) => Promise<CoreTool[]>
discover: (
server: ConnectedMCPServer,
skipPrefix?: boolean,
) => Promise<CoreTool[]>
cache: { delete(key: string): void; clear(): void }
} {
const discover = memoizeWithLRU(
async (server: ConnectedMCPServer, skipPrefix?: boolean): Promise<CoreTool[]> => {
async (
server: ConnectedMCPServer,
skipPrefix?: boolean,
): Promise<CoreTool[]> => {
if (server.type !== 'connected') return []
return discoverTools({
serverName: server.name,

View File

@@ -1,15 +1,10 @@
// MCP tool execution — call tools on connected MCP servers
// Extracted from src/services/mcp/client.ts (callMCPTool)
import {
CallToolResultSchema,
} from '@modelcontextprotocol/sdk/types.js'
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js'
import type { ConnectedMCPServer } from './types.js'
import type { McpClientDependencies } from './interfaces.js'
import {
McpToolCallError,
McpAuthError,
} from './errors.js'
import { McpToolCallError, McpAuthError } from './errors.js'
// ============================================================================
// Constants
@@ -34,7 +29,11 @@ export interface CallToolOptions {
/** Abort signal for cancellation */
signal: AbortSignal
/** Progress callback */
onProgress?: (data: { progress?: number; total?: number; message?: string }) => void
onProgress?: (data: {
progress?: number
total?: number
message?: string
}) => void
/** Tool call timeout in ms (defaults to ~27.8 hours) */
timeoutMs?: number
}
@@ -68,12 +67,9 @@ export async function callMcpTool(
deps.logger.debug(`[${serverName}] Calling MCP tool: ${tool}`)
// Progress logging for long-running tools (every 30 seconds)
progressInterval = setInterval(
() => {
deps.logger.debug(`[${serverName}] Tool '${tool}' still running`)
},
30_000,
)
progressInterval = setInterval(() => {
deps.logger.debug(`[${serverName}] Tool '${tool}' still running`)
}, 30_000)
const result = await Promise.race([
mcpClient.callTool(
@@ -126,9 +122,7 @@ export async function callMcpTool(
}
if (e instanceof Error && e.name !== 'AbortError') {
deps.logger.debug(
`[${serverName}] Tool '${tool}' failed: ${e.message}`,
)
deps.logger.debug(`[${serverName}] Tool '${tool}' failed: ${e.message}`)
}
// Check for 401 errors
@@ -167,16 +161,13 @@ function createTimeoutPromise(
timeoutMs: number,
): Promise<never> {
return new Promise((_, reject) => {
const timeoutId = setTimeout(
() => {
reject(
new Error(
`MCP server "${serverName}" tool "${tool}" timed out after ${Math.floor(timeoutMs / 1000)}s`,
),
)
},
timeoutMs,
)
const timeoutId = setTimeout(() => {
reject(
new Error(
`MCP server "${serverName}" tool "${tool}" timed out after ${Math.floor(timeoutMs / 1000)}s`,
),
)
}, timeoutMs)
timeoutId.unref?.()
})
}

View File

@@ -2,9 +2,7 @@
// Factory function that creates a manager instance with event-based notifications
import type { Client } from '@modelcontextprotocol/sdk/client/index.js'
import type {
ListToolsResult,
} from '@modelcontextprotocol/sdk/types.js'
import type { ListToolsResult } from '@modelcontextprotocol/sdk/types.js'
import memoize from 'lodash-es/memoize.js'
import { buildMcpToolName } from './strings.js'
import type { CoreTool } from '@claude-code-best/agent-tools'
@@ -17,11 +15,7 @@ import type {
NeedsAuthMCPServer,
} from './types.js'
import type { McpClientDependencies } from './interfaces.js'
import {
McpConnectionError,
McpAuthError,
McpTimeoutError,
} from './errors.js'
import { McpConnectionError, McpAuthError, McpTimeoutError } from './errors.js'
import { memoizeWithLRU } from './cache.js'
import { discoverTools } from './discovery.js'
import { callMcpTool } from './execution.js'
@@ -51,8 +45,15 @@ export interface McpManager {
getConnections(): Map<string, MCPServerConnection>
getTools(serverName: string): CoreTool[]
getAllTools(): CoreTool[]
callTool(serverName: string, toolName: string, args: unknown): Promise<unknown>
on<E extends keyof McpManagerEvents>(event: E, handler: McpManagerEvents[E]): void
callTool(
serverName: string,
toolName: string,
args: unknown,
): Promise<unknown>
on<E extends keyof McpManagerEvents>(
event: E,
handler: McpManagerEvents[E],
): void
off(event: string, handler: EventHandler): void
}
@@ -72,20 +73,35 @@ class McpManagerImpl implements McpManager {
private toolsCache = new Map<string, CoreTool[]>()
private listeners = new Map<string, Set<EventHandler>>()
private deps: McpClientDependencies
private connectFn: ((name: string, config: ScopedMcpServerConfig) => Promise<MCPServerConnection>) | null = null
private connectFn:
| ((
name: string,
config: ScopedMcpServerConfig,
) => Promise<MCPServerConnection>)
| null = null
constructor(deps: McpClientDependencies) {
this.deps = deps
}
/** Set the connect function — the host provides this with all transport logic */
setConnectFn(fn: (name: string, config: ScopedMcpServerConfig) => Promise<MCPServerConnection>): void {
setConnectFn(
fn: (
name: string,
config: ScopedMcpServerConfig,
) => Promise<MCPServerConnection>,
): void {
this.connectFn = fn
}
async connect(name: string, config: McpServerConfig): Promise<MCPServerConnection> {
async connect(
name: string,
config: McpServerConfig,
): Promise<MCPServerConnection> {
if (!this.connectFn) {
throw new Error('McpManager: connectFn not set. Call setConnectFn() first.')
throw new Error(
'McpManager: connectFn not set. Call setConnectFn() first.',
)
}
const scopedConfig: ScopedMcpServerConfig = { ...config, scope: 'dynamic' }
@@ -148,10 +164,17 @@ class McpManagerImpl implements McpManager {
return all
}
async callTool(serverName: string, toolName: string, args: unknown): Promise<unknown> {
async callTool(
serverName: string,
toolName: string,
args: unknown,
): Promise<unknown> {
const conn = this.connections.get(serverName)
if (!conn || conn.type !== 'connected') {
throw new McpConnectionError(serverName, `Server ${serverName} is not connected`)
throw new McpConnectionError(
serverName,
`Server ${serverName} is not connected`,
)
}
return callMcpTool(
@@ -165,7 +188,10 @@ class McpManagerImpl implements McpManager {
)
}
on<E extends keyof McpManagerEvents>(event: E, handler: McpManagerEvents[E]): void {
on<E extends keyof McpManagerEvents>(
event: E,
handler: McpManagerEvents[E],
): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set())
}
@@ -188,7 +214,10 @@ class McpManagerImpl implements McpManager {
})
}
private async refreshTools(name: string, conn: ConnectedMCPServer): Promise<void> {
private async refreshTools(
name: string,
conn: ConnectedMCPServer,
): Promise<void> {
try {
const tools = await discoverTools({
serverName: name,

View File

@@ -9,10 +9,13 @@ export function recursivelySanitizeUnicode<T>(data: T): T {
if (typeof data === 'string') {
// Remove control characters except \t, \n, \r
// Replace null bytes and other C0 controls
return data
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
.replace(/\uFFFD/g, '') // replacement character
.normalize('NFC') as unknown as T
return (
data
// biome-ignore lint/suspicious/noControlCharactersInRegex: intentional control character sanitization
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
.replace(/\uFFFD/g, '') // replacement character
.normalize('NFC') as unknown as T
)
}
if (Array.isArray(data)) {
@@ -21,7 +24,9 @@ export function recursivelySanitizeUnicode<T>(data: T): T {
if (data !== null && typeof data === 'object') {
const result = {} as Record<string, unknown>
for (const [key, value] of Object.entries(data as Record<string, unknown>)) {
for (const [key, value] of Object.entries(
data as Record<string, unknown>,
)) {
result[key] = recursivelySanitizeUnicode(value)
}
return result as T