From f1d22d2032f995cc7bd78730cd343b6741149c49 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 20 Nov 2025 20:13:55 +0800 Subject: [PATCH] save upload report --- agents/package.json | 2 + agents/src/job.ts | 39 +++-- agents/src/telemetry/index.ts | 8 +- agents/src/telemetry/traces.ts | 281 ++++++++++++++++++++++++++++++++- agents/src/voice/report.ts | 6 +- pnpm-lock.yaml | 147 +++++++++++++++++ 6 files changed, 469 insertions(+), 14 deletions(-) diff --git a/agents/package.json b/agents/package.json index 8f2d95b5..9ded4cdc 100644 --- a/agents/package.json +++ b/agents/package.json @@ -61,9 +61,11 @@ "@opentelemetry/sdk-trace-base": "^1.28.0", "@opentelemetry/sdk-trace-node": "^1.28.0", "@opentelemetry/semantic-conventions": "^1.28.0", + "@types/form-data": "^2.5.2", "@types/pidusage": "^2.0.5", "commander": "^12.0.0", "fluent-ffmpeg": "^2.1.3", + "form-data": "^4.0.5", "heap-js": "^2.6.0", "json-schema": "^0.4.0", "livekit-server-sdk": "^2.14.1", diff --git a/agents/src/job.ts b/agents/src/job.ts index e14bfdd8..79a74516 100644 --- a/agents/src/job.ts +++ b/agents/src/job.ts @@ -14,7 +14,7 @@ import { AsyncLocalStorage } from 'node:async_hooks'; import type { Logger } from 'pino'; import type { InferenceExecutor } from './ipc/inference_executor.js'; import { log } from './log.js'; -import { setupCloudTracer } from './telemetry/index.js'; +import { setupCloudTracer, uploadSessionReport } from './telemetry/index.js'; import { isCloud } from './utils.js'; import type { AgentSession } from './voice/agent_session.js'; import { type SessionReport, createSessionReport } from './voice/report.js'; @@ -252,7 +252,6 @@ export class JobContext { } // TODO(brian): implement and check recorder io - // TODO(brian): PR5 - Ensure chat history serialization includes all required fields (use sessionReportToJSON helper) return createSessionReport({ jobId: this.job.id, @@ -275,14 +274,36 @@ export class JobContext { // TODO(brian): Implement CLI/console - // TODO(brian): PR5 - Call uploadSessionReport() if report.enableUserDataTraining is true - // TODO(brian): PR5 - Upload includes: multipart form with header (protobuf), chat_history (JSON), and audio recording (if available) + // Upload session report to LiveKit Cloud if enabled + const url = new URL(this.#info.url); - this.#logger.debug('Session ended, report generated', { - jobId: report.jobId, - roomId: report.roomId, - eventsCount: report.events.length, - }); + if (report.enableRecording && isCloud(url)) { + try { + await uploadSessionReport({ + agentName: this.job.agentName, + cloudHostname: url.hostname, + report, + }); + this.#logger.info( + { + jobId: report.jobId, + roomId: report.roomId, + }, + 'Session report uploaded to LiveKit Cloud', + ); + } catch (error) { + this.#logger.error({ error }, 'Failed to upload session report'); + } + } + + this.#logger.debug( + { + jobId: report.jobId, + roomId: report.roomId, + eventsCount: report.events.length, + }, + 'Session ended, report generated', + ); } /** diff --git a/agents/src/telemetry/index.ts b/agents/src/telemetry/index.ts index 1b397078..d81fd485 100644 --- a/agents/src/telemetry/index.ts +++ b/agents/src/telemetry/index.ts @@ -7,5 +7,11 @@ export { ExtraDetailsProcessor, MetadataLogProcessor } from './logging.js'; export { enablePinoOTELInstrumentation } from './pino_bridge.js'; export * as traceTypes from './trace_types.js'; -export { setTracerProvider, setupCloudTracer, tracer, type StartSpanOptions } from './traces.js'; +export { + setTracerProvider, + setupCloudTracer, + tracer, + uploadSessionReport, + type StartSpanOptions, +} from './traces.js'; export { recordException, recordRealtimeMetrics } from './utils.js'; diff --git a/agents/src/telemetry/traces.ts b/agents/src/telemetry/traces.ts index 1cab06e7..601b9996 100644 --- a/agents/src/telemetry/traces.ts +++ b/agents/src/telemetry/traces.ts @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2025 LiveKit, Inc. // // SPDX-License-Identifier: Apache-2.0 +import { MetricsRecordingHeader } from '@livekit/protocol'; import { type Attributes, type Context, @@ -11,7 +12,7 @@ import { context as otelContext, trace, } from '@opentelemetry/api'; -import { logs } from '@opentelemetry/api-logs'; +import { SeverityNumber, logs } from '@opentelemetry/api-logs'; import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http'; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; import { CompressionAlgorithm } from '@opentelemetry/otlp-exporter-base'; @@ -21,6 +22,7 @@ import type { ReadableSpan, SpanProcessor } from '@opentelemetry/sdk-trace-base' import { BatchSpanProcessor, NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions'; import { AccessToken } from 'livekit-server-sdk'; +import type { SessionReport } from '../voice/report.js'; import { ExtraDetailsProcessor, MetadataLogProcessor } from './logging.js'; import { enablePinoOTELInstrumentation } from './pino_bridge.js'; @@ -274,3 +276,280 @@ export async function setupCloudTracer(options: { throw error; } } + +/** + * Convert ChatItem to proto-compatible dictionary format. + * Matches Python's _to_proto_chat_item implementation. + * + * TODO: Use actual agent_session proto types once @livekit/protocol v1.43.1+ is published + * + * @internal + */ +function chatItemToProto(item: any): Record { + const itemDict: Record = {}; + + if (item.type === 'message') { + const roleMap: Record = { + developer: 'DEVELOPER', + system: 'SYSTEM', + user: 'USER', + assistant: 'ASSISTANT', + }; + + const msg: Record = { + id: item.id, + role: roleMap[item.role] || item.role.toUpperCase(), + content: item.content.map((c: string) => ({ text: c })), + interrupted: item.interrupted, + extra: item.extra || {}, + createdAt: toRFC3339(item.createdAt), + }; + + if (item.transcriptConfidence !== undefined && item.transcriptConfidence !== null) { + msg.transcriptConfidence = item.transcriptConfidence; + } + + // Add metrics if available + const metrics = item.metrics || {}; + if (Object.keys(metrics).length > 0) { + msg.metrics = {}; + if (metrics.started_speaking_at) { + msg.metrics.startedSpeakingAt = toRFC3339(metrics.started_speaking_at); + } + if (metrics.stopped_speaking_at) { + msg.metrics.stoppedSpeakingAt = toRFC3339(metrics.stopped_speaking_at); + } + if (metrics.transcription_delay !== undefined) { + msg.metrics.transcriptionDelay = metrics.transcription_delay; + } + if (metrics.end_of_turn_delay !== undefined) { + msg.metrics.endOfTurnDelay = metrics.end_of_turn_delay; + } + if (metrics.on_user_turn_completed_delay !== undefined) { + msg.metrics.onUserTurnCompletedDelay = metrics.on_user_turn_completed_delay; + } + if (metrics.llm_node_ttft !== undefined) { + msg.metrics.llmNodeTtft = metrics.llm_node_ttft; + } + if (metrics.tts_node_ttfb !== undefined) { + msg.metrics.ttsNodeTtfb = metrics.tts_node_ttfb; + } + if (metrics.e2e_latency !== undefined) { + msg.metrics.e2eLatency = metrics.e2e_latency; + } + } + + itemDict.message = msg; + } else if (item.type === 'function_call') { + itemDict.functionCall = { + id: item.id, + callId: item.callId, + arguments: item.arguments, + name: item.name, + createdAt: toRFC3339(item.createdAt), + }; + } else if (item.type === 'function_call_output') { + itemDict.functionCallOutput = { + id: item.id, + name: item.name, + callId: item.callId, + output: item.output, + isError: item.isError, + createdAt: toRFC3339(item.createdAt), + }; + } else if (item.type === 'agent_handoff') { + itemDict.agentHandoff = { + id: item.id, + oldAgentId: item.oldAgentId, + newAgentId: item.newAgentId, + createdAt: toRFC3339(item.createdAt), + }; + } + + // Patch arguments & output to make them indexable (parse JSON strings) + try { + if (item.type === 'function_call' && typeof itemDict.functionCall?.arguments === 'string') { + itemDict.functionCall.arguments = JSON.parse(itemDict.functionCall.arguments); + } else if ( + item.type === 'function_call_output' && + typeof itemDict.functionCallOutput?.output === 'string' + ) { + itemDict.functionCallOutput.output = JSON.parse(itemDict.functionCallOutput.output); + } + } catch { + // ignore parsing errors + } + + return itemDict; +} + +/** + * Convert timestamp to RFC3339 format matching Python's _to_rfc3339. + * @internal + */ +function toRFC3339(value: number | Date): string { + const dt = value instanceof Date ? value : new Date(value * 1000); + // Truncate to milliseconds + const ms = Math.floor(dt.getMilliseconds() / 1000) * 1000; + const truncated = new Date(dt); + truncated.setMilliseconds(ms); + return truncated.toISOString(); +} + +/** + * Upload session report to LiveKit Cloud observability. + * Matches Python's _upload_session_report implementation. + * + * @param options - Configuration with agentName, cloudHostname, and report + * @internal + */ +export async function uploadSessionReport(options: { + agentName: string; + cloudHostname: string; + report: SessionReport; +}): Promise { + const { agentName, cloudHostname, report } = options; + + // Get OTEL logger for chat_history + const chatLogger = logs.getLoggerProvider().getLogger('chat_history'); + + // Helper to emit logs + const emitLog = ( + body: string, + timestamp: number, + attributes: Record, + severityNumber = 0, // UNSPECIFIED + severityText = 'unspecified', + ) => { + // Add room/job metadata to attributes + const fullAttributes = { + ...attributes, + room_id: report.roomId, + job_id: report.jobId, + room: report.room, + }; + + chatLogger.emit({ + body, + timestamp: timestamp * 1000000, // Convert to nanoseconds + attributes: fullAttributes, + severityNumber, + severityText, + context: otelContext.active(), + }); + }; + + // Log session report + emitLog('session report', Math.floor((report.timestamp || Date.now()) * 1000), { + 'session.options': report.options, + 'session.report_timestamp': report.timestamp, + agent_name: agentName, + }); + + // Log each chat item + for (const item of report.chatHistory.items) { + const itemLog = chatItemToProto(item); + let severityNumber = SeverityNumber.UNSPECIFIED; + let severityText = 'unspecified'; + + if (item.type === 'function_call_output' && item.isError) { + severityNumber = SeverityNumber.ERROR; + severityText = 'error'; + } + + emitLog( + 'chat item', + Math.floor(item.createdAt * 1000), + { 'chat.item': itemLog }, + severityNumber, + severityText, + ); + } + + // Create JWT token for upload + const apiKey = process.env.LIVEKIT_API_KEY; + const apiSecret = process.env.LIVEKIT_API_SECRET; + + if (!apiKey || !apiSecret) { + throw new Error('LIVEKIT_API_KEY and LIVEKIT_API_SECRET must be set for session upload'); + } + + const token = new AccessToken(apiKey, apiSecret, { + identity: 'livekit-agents-telemetry', + ttl: '6h', + }); + token.addObservabilityGrant({ write: true }); + const jwt = await token.toJwt(); + + // Create multipart form data with explicit header control (like Python's aiohttp.MultipartWriter) + const FormData = (await import('form-data')).default; + const formData = new FormData(); + + // Add header (protobuf MetricsRecordingHeader) + const headerMsg = new MetricsRecordingHeader({ + roomId: report.roomId, + duration: BigInt(0), // TODO: Calculate actual duration from report + startTime: { + seconds: BigInt(Math.floor(report.timestamp / 1000)), + nanos: Math.floor((report.timestamp % 1000) * 1e6), + }, + }); + + const headerBytes = Buffer.from(headerMsg.toBinary()); + formData.append('header', headerBytes, { + filename: 'header.binpb', + contentType: 'application/protobuf', + knownLength: headerBytes.length, + header: { + 'Content-Type': 'application/protobuf', + 'Content-Length': headerBytes.length.toString(), + }, + }); + + // Add chat_history JSON + const chatHistoryJson = JSON.stringify(report.chatHistory.toJSON({ excludeTimestamp: false })); + const chatHistoryBuffer = Buffer.from(chatHistoryJson, 'utf-8'); + formData.append('chat_history', chatHistoryBuffer, { + filename: 'chat_history.json', + contentType: 'application/json', + knownLength: chatHistoryBuffer.length, + header: { + 'Content-Type': 'application/json', + 'Content-Length': chatHistoryBuffer.length.toString(), + }, + }); + + // TODO(brian): Add audio recording file when recorder IO is implemented + + // Upload to LiveKit Cloud using form-data's submit method + // This properly streams the multipart form with all headers including Content-Length + return new Promise((resolve, reject) => { + formData.submit( + { + protocol: 'https:', + host: cloudHostname, + path: '/observability/recordings/v0', + method: 'POST', + headers: { + Authorization: `Bearer ${jwt}`, + }, + }, + (err, res) => { + if (err) { + reject(new Error(`Failed to upload session report: ${err.message}`)); + return; + } + + if (res.statusCode && res.statusCode >= 400) { + reject( + new Error(`Failed to upload session report: ${res.statusCode} ${res.statusMessage}`), + ); + return; + } + + res.resume(); // Drain the response + res.on('end', () => resolve()); + }, + ); + }); +} diff --git a/agents/src/voice/report.ts b/agents/src/voice/report.ts index 375b0def..c0eafe84 100644 --- a/agents/src/voice/report.ts +++ b/agents/src/voice/report.ts @@ -12,7 +12,7 @@ export interface SessionReport { options: VoiceOptions; events: AgentEvent[]; chatHistory: ChatContext; - enableUserDataTraining: boolean; + enableRecording: boolean; timestamp: number; } @@ -35,7 +35,7 @@ export function createSessionReport(opts: SessionReportOptions): SessionReport { options: opts.options, events: opts.events, chatHistory: opts.chatHistory, - enableUserDataTraining: opts.enableUserDataTraining ?? false, + enableRecording: opts.enableUserDataTraining ?? false, timestamp: opts.timestamp ?? Date.now(), }; } @@ -71,7 +71,7 @@ export function sessionReportToJSON(report: SessionReport): Record=8.0.0'} @@ -2511,6 +2524,10 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + call-bind@1.0.7: resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} engines: {node: '>= 0.4'} @@ -2591,6 +2608,10 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@12.0.0: resolution: {integrity: sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==} engines: {node: '>=18'} @@ -2696,6 +2717,10 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -2735,6 +2760,10 @@ packages: resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} engines: {node: '>=10'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -2766,6 +2795,10 @@ packages: resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} engines: {node: '>= 0.4'} + 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'} @@ -2781,10 +2814,18 @@ packages: resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} engines: {node: '>= 0.4'} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + es-set-tostringtag@2.0.3: resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + es-shim-unscopables@1.0.2: resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} @@ -3113,6 +3154,10 @@ packages: resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} engines: {node: '>=14'} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -3154,6 +3199,14 @@ packages: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} engines: {node: '>= 0.4'} + 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-stream@8.0.1: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} @@ -3224,6 +3277,10 @@ packages: gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -3259,6 +3316,10 @@ packages: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + has-tostringtag@1.0.2: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} @@ -3681,6 +3742,10 @@ packages: resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} engines: {node: '>=10'} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -3696,6 +3761,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + mimic-fn@4.0.0: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} @@ -6132,6 +6205,10 @@ snapshots: dependencies: '@types/node': 22.15.30 + '@types/form-data@2.5.2': + dependencies: + form-data: 4.0.5 + '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} @@ -6478,6 +6555,8 @@ snapshots: async@0.2.10: {} + asynckit@0.4.0: {} + atomic-sleep@1.0.0: {} available-typed-arrays@1.0.7: @@ -6539,6 +6618,11 @@ snapshots: cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + call-bind@1.0.7: dependencies: es-define-property: 1.0.0 @@ -6629,6 +6713,10 @@ snapshots: colorette@2.0.20: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@12.0.0: {} commander@4.1.1: {} @@ -6714,6 +6802,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + delayed-stream@1.0.0: {} + dequal@2.0.3: {} detect-indent@6.1.0: {} @@ -6740,6 +6830,12 @@ snapshots: dotenv@8.6.0: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + eastasianwidth@0.2.0: {} ecdsa-sig-formatter@1.0.11: @@ -6817,6 +6913,8 @@ snapshots: dependencies: get-intrinsic: 1.2.4 + es-define-property@1.0.1: {} + es-errors@1.3.0: {} es-iterator-helpers@1.0.19: @@ -6842,12 +6940,23 @@ snapshots: dependencies: es-errors: 1.3.0 + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + es-set-tostringtag@2.0.3: dependencies: get-intrinsic: 1.2.4 has-tostringtag: 1.0.2 hasown: 2.0.2 + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + es-shim-unscopables@1.0.2: dependencies: hasown: 2.0.2 @@ -7320,6 +7429,14 @@ snapshots: cross-spawn: 7.0.3 signal-exit: 4.1.0 + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -7378,6 +7495,24 @@ snapshots: has-symbols: 1.0.3 hasown: 2.0.2 + 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.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-stream@8.0.1: {} get-symbol-description@1.0.2: @@ -7475,6 +7610,8 @@ snapshots: dependencies: get-intrinsic: 1.2.4 + gopd@1.2.0: {} + graceful-fs@4.2.11: {} graphemer@1.4.0: {} @@ -7503,6 +7640,8 @@ snapshots: has-symbols@1.0.3: {} + has-symbols@1.1.0: {} + has-tostringtag@1.0.2: dependencies: has-symbols: 1.0.3 @@ -7897,6 +8036,8 @@ snapshots: dependencies: escape-string-regexp: 4.0.0 + math-intrinsics@1.1.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -7911,6 +8052,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + mimic-fn@4.0.0: {} minimatch@3.0.8: