diff --git a/packages/agent/package.json b/packages/agent/package.json index fe594cecf..98759f6c9 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -39,8 +39,15 @@ "./adapters/claude/conversion/tool-use-to-acp": { "types": "./dist/adapters/claude/conversion/tool-use-to-acp.d.ts", "import": "./dist/adapters/claude/conversion/tool-use-to-acp.js" + }, + "./server": { + "types": "./dist/server/agent-server.d.ts", + "import": "./dist/server/agent-server.js" } }, + "bin": { + "agent-server": "./dist/server/bin.js" + }, "type": "module", "keywords": [ "posthog", @@ -68,18 +75,24 @@ "@types/bun": "latest", "@types/tar": "^6.1.13", "minimatch": "^10.0.3", + "msw": "^2.12.7", "tsup": "^8.5.1", "tsx": "^4.20.6", "typescript": "^5.5.0", "vitest": "^2.1.8" }, "dependencies": { - "@posthog/shared": "workspace:*", "@twig/git": "workspace:*", "@agentclientprotocol/sdk": "^0.13.1", + "@hono/node-server": "^1.19.9", + "@types/jsonwebtoken": "^9.0.10", + "hono": "^4.11.7", + "jsonwebtoken": "^9.0.2", "@anthropic-ai/claude-agent-sdk": "0.2.12", "@anthropic-ai/sdk": "^0.71.0", "@modelcontextprotocol/sdk": "^1.25.3", + "@posthog/shared": "workspace:*", + "commander": "^14.0.2", "diff": "^8.0.2", "dotenv": "^17.2.3", "tar": "^7.5.0", diff --git a/packages/agent/src/server/agent-server.test.ts b/packages/agent/src/server/agent-server.test.ts new file mode 100644 index 000000000..724e81dec --- /dev/null +++ b/packages/agent/src/server/agent-server.test.ts @@ -0,0 +1,216 @@ +import jwt from "jsonwebtoken"; +import { type SetupServerApi, setupServer } from "msw/node"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { createTestRepo, type TestRepo } from "../test/fixtures/api.js"; +import { createPostHogHandlers } from "../test/mocks/msw-handlers.js"; +import { AgentServer } from "./agent-server.js"; +import { type JwtPayload, SANDBOX_CONNECTION_AUDIENCE } from "./jwt.js"; + +function createTestJwt( + payload: JwtPayload, + privateKey: string, + expiresInSeconds = 3600, +): string { + return jwt.sign( + { ...payload, aud: SANDBOX_CONNECTION_AUDIENCE }, + privateKey, + { + algorithm: "RS256", + expiresIn: expiresInSeconds, + }, + ); +} + +// Test RSA key pair (2048-bit, for testing only) +const TEST_PRIVATE_KEY = `-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDqh94SYMFsvG4C +Co9BSGjtPr2/OxzuNGr41O4+AMkDQRd9pKO49DhTA4VzwnOvrH8y4eI9N8OQne7B +wpdoouSn4DoDAS/b3SUfij/RoFUSyZiTQoWz0H6o2Vuufiz0Hf+BzlZEVnhSQ1ru +vqSf+4l8cWgeMXaFXgdD5kQ8GjvR5uqKxvO2Env1hMJRKeOOEGgCep/0c6SkMUTX +SeC+VjypVg9+8yPxtIpOQ7XKv+7e/PA0ilqehRQh4fo9BAWjUW1+HnbtsjJAjjfv +ngzIjpajuQVyMi7G79v8OvijhLMJjJBh3TdbVIfi+RkVj/H94UUfKWRfJA0eLykA +VvTiFf0nAgMBAAECggEABkLBQWFW2IXBNAm/IEGEF408uH2l/I/mqSTaBUq1EwKq +U17RRg8y77hg2CHBP9fNf3i7NuIltNcaeA6vRwpOK1MXiVv/QJHLO2fP41Mx4jIC +gi/c7NtsfiprQaG5pnykhP0SnXlndd65bzUkpOasmWdXnbK5VL8ZV40uliInJafE +1Eo9qSYCJxHmivU/4AbiBgygOAo1QIiuuUHcx0YGknLrBaMQETuvWJGE3lxVQ30/ +EuRyA3r6BwN2T0z47PZBzvCpg/C1KeoYuKSMwMyEXfl+a8NclqdROkVaenmZpvVH +0lAvFDuPrBSDmU4XJbKCEfwfHjRkiWAFaTrKntGQtQKBgQD/ILoK4U9DkJoKTYvY +9lX7dg6wNO8jGLHNufU8tHhU+QnBMH3hBXrAtIKQ1sGs+D5rq/O7o0Balmct9vwb +CQZ1EpPfa83Thsv6Skd7lWK0JF7g2vVk8kT4nY/eqkgZUWgkfdMp+OMg2drYiIE8 +u+sRPTCdq4Tv5miRg0OToX2H/QKBgQDrVR2GXm6ZUyFbCy8A0kttXP1YyXqDVq7p +L4kqyUq43hmbjzIRM4YDN3EvgZvVf6eub6L/3HfKvWD/OvEhHovTvHb9jkwZ3FO+ +YQllB/ccAWJs/Dw5jLAsX9O+eIe4lfwROib3vYLnDTAmrXD5VL35R5F0MsdRoxk5 +lTCq1sYI8wKBgGA9ZjDIgXAJUjJkwkZb1l9/T1clALiKjjf+2AXIRkQ3lXhs5G9H +8+BRt5cPjAvFsTZIrS6xDIufhNiP/NXt96OeGG4FaqVKihOmhYSW+57cwXWs4zjr +Mx1dwnHKZlw2m0R4unlwy60OwUFBbQ8ODER6gqZXl1Qv5G5Px+Qe3Q25AoGAUl+s +wgfz9r9egZvcjBEQTeuq0pVTyP1ipET7YnqrKSK1G/p3sAW09xNFDzfy8DyK2UhC +agUl+VVoym47UTh8AVWK4R4aDUNOHOmifDbZjHf/l96CxjI0yJOSbq2J9FarsOwG +D9nKJE49eIxlayD6jnM6us27bxwEDF/odSRQlXkCgYEAxn9l/5kewWkeEA0Afe1c +Uf+mepHBLw1Pbg5GJYIZPC6e5+wRNvtFjM5J6h5LVhyb7AjKeLBTeohoBKEfUyUO +rl/ql9qDIh5lJFn3uNh7+r7tmG21Zl2pyh+O8GljjZ25mYhdiwl0uqzVZaINe2Wa +vbMnD1ZQKgL8LHgb02cbTsc= +-----END PRIVATE KEY-----`; + +const TEST_PUBLIC_KEY = `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6ofeEmDBbLxuAgqPQUho +7T69vzsc7jRq+NTuPgDJA0EXfaSjuPQ4UwOFc8Jzr6x/MuHiPTfDkJ3uwcKXaKLk +p+A6AwEv290lH4o/0aBVEsmYk0KFs9B+qNlbrn4s9B3/gc5WRFZ4UkNa7r6kn/uJ +fHFoHjF2hV4HQ+ZEPBo70ebqisbzthJ79YTCUSnjjhBoAnqf9HOkpDFE10ngvlY8 +qVYPfvMj8bSKTkO1yr/u3vzwNIpanoUUIeH6PQQFo1Ftfh527bIyQI43754MyI6W +o7kFcjIuxu/b/Dr4o4SzCYyQYd03W1SH4vkZFY/x/eFFHylkXyQNHi8pAFb04hX9 +JwIDAQAB +-----END PUBLIC KEY-----`; + +describe("AgentServer HTTP Mode", () => { + let repo: TestRepo; + let server: AgentServer; + let mswServer: SetupServerApi; + let appendLogCalls: unknown[][]; + const port = 3099; + + beforeEach(async () => { + repo = await createTestRepo("agent-server-http"); + appendLogCalls = []; + mswServer = setupServer( + ...createPostHogHandlers({ + baseUrl: "http://localhost:8000", + onAppendLog: (entries) => appendLogCalls.push(entries), + }), + ); + mswServer.listen({ onUnhandledRequest: "bypass" }); + }); + + afterEach(async () => { + if (server) { + await server.stop(); + } + mswServer.close(); + await repo.cleanup(); + }); + + const createServer = () => { + server = new AgentServer({ + port, + jwtPublicKey: TEST_PUBLIC_KEY, + repositoryPath: repo.path, + apiUrl: "http://localhost:8000", + apiKey: "test-api-key", + projectId: 1, + }); + return server; + }; + + const createToken = (overrides = {}) => { + return createTestJwt( + { + run_id: "test-run-id", + task_id: "test-task-id", + team_id: 1, + user_id: 1, + distinct_id: "test-distinct-id", + ...overrides, + }, + TEST_PRIVATE_KEY, + ); + }; + + describe("GET /health", () => { + it("returns ok status", async () => { + await createServer().start(); + + const response = await fetch(`http://localhost:${port}/health`); + const body = await response.json(); + + expect(response.status).toBe(200); + expect(body).toEqual({ status: "ok", hasSession: false }); + }); + }); + + describe("GET /events", () => { + it("returns 401 without authorization header", async () => { + await createServer().start(); + + const response = await fetch(`http://localhost:${port}/events`); + const body = await response.json(); + + expect(response.status).toBe(401); + expect(body.error).toBe("Missing authorization header"); + }); + + it("returns 401 with invalid token", async () => { + await createServer().start(); + + const response = await fetch(`http://localhost:${port}/events`, { + headers: { Authorization: "Bearer invalid-token" }, + }); + const body = await response.json(); + + expect(response.status).toBe(401); + expect(body.code).toBe("invalid_signature"); + }); + + it("accepts valid JWT and returns SSE stream", async () => { + await createServer().start(); + const token = createToken(); + + const response = await fetch(`http://localhost:${port}/events`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toBe("text/event-stream"); + }); + }); + + describe("POST /command", () => { + it("returns 401 without authorization", async () => { + await createServer().start(); + + const response = await fetch(`http://localhost:${port}/command`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + method: "user_message", + params: { content: "test" }, + }), + }); + + expect(response.status).toBe(401); + }); + + it("returns 400 when no session exists", async () => { + await createServer().start(); + const token = createToken(); + + const response = await fetch(`http://localhost:${port}/command`, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + jsonrpc: "2.0", + method: "user_message", + params: { content: "test" }, + }), + }); + + expect(response.status).toBe(400); + const body = await response.json(); + expect(body.error).toBe("No active session for this run"); + }); + }); + + describe("404 handling", () => { + it("returns 404 for unknown routes", async () => { + await createServer().start(); + + const response = await fetch(`http://localhost:${port}/unknown`); + const body = await response.json(); + + expect(response.status).toBe(404); + expect(body.error).toBe("Not found"); + }); + }); +}); diff --git a/packages/agent/src/server/agent-server.ts b/packages/agent/src/server/agent-server.ts new file mode 100644 index 000000000..8abb4b0fa --- /dev/null +++ b/packages/agent/src/server/agent-server.ts @@ -0,0 +1,636 @@ +import { + ClientSideConnection, + ndJsonStream, + PROTOCOL_VERSION, +} from "@agentclientprotocol/sdk"; +import { type ServerType, serve } from "@hono/node-server"; +import { Hono } from "hono"; +import { POSTHOG_NOTIFICATIONS } from "../acp-extensions.js"; +import { + createAcpConnection, + type InProcessAcpConnection, +} from "../adapters/acp-connection.js"; +import { PostHogAPIClient } from "../posthog-api.js"; +import { SessionLogWriter } from "../session-log-writer.js"; +import { TreeTracker } from "../tree-tracker.js"; +import type { DeviceInfo, TreeSnapshotEvent } from "../types.js"; +import { AsyncMutex } from "../utils/async-mutex.js"; +import { getLlmGatewayUrl } from "../utils/gateway.js"; +import { Logger } from "../utils/logger.js"; +import { type JwtPayload, JwtValidationError, validateJwt } from "./jwt.js"; +import { jsonRpcRequestSchema, validateCommandParams } from "./schemas.js"; +import type { AgentServerConfig } from "./types.js"; + +type MessageCallback = (message: unknown) => void; + +class NdJsonTap { + private decoder = new TextDecoder(); + private buffer = ""; + + constructor(private onMessage: MessageCallback) {} + + process(chunk: Uint8Array): void { + this.buffer += this.decoder.decode(chunk, { stream: true }); + const lines = this.buffer.split("\n"); + this.buffer = lines.pop() ?? ""; + + for (const line of lines) { + if (!line.trim()) continue; + try { + this.onMessage(JSON.parse(line)); + } catch { + // Not valid JSON, skip + } + } + } +} + +function createTappedReadableStream( + underlying: ReadableStream, + onMessage: MessageCallback, + logger?: Logger, +): ReadableStream { + const reader = underlying.getReader(); + const tap = new NdJsonTap(onMessage); + + return new ReadableStream({ + async pull(controller) { + try { + const { value, done } = await reader.read(); + if (done) { + controller.close(); + return; + } + tap.process(value); + controller.enqueue(value); + } catch (error) { + logger?.debug("Read failed, closing stream", error); + controller.close(); + } + }, + cancel() { + reader.releaseLock(); + }, + }); +} + +function createTappedWritableStream( + underlying: WritableStream, + onMessage: MessageCallback, + logger?: Logger, +): WritableStream { + const tap = new NdJsonTap(onMessage); + const mutex = new AsyncMutex(); + + return new WritableStream({ + async write(chunk) { + tap.process(chunk); + await mutex.acquire(); + try { + const writer = underlying.getWriter(); + await writer.write(chunk); + writer.releaseLock(); + } catch (error) { + logger?.debug("Write failed (stream may be closed)", error); + } finally { + mutex.release(); + } + }, + async close() { + await mutex.acquire(); + try { + const writer = underlying.getWriter(); + await writer.close(); + writer.releaseLock(); + } catch (error) { + logger?.debug("Close failed (stream may be closed)", error); + } finally { + mutex.release(); + } + }, + async abort(reason) { + await mutex.acquire(); + try { + const writer = underlying.getWriter(); + await writer.abort(reason); + writer.releaseLock(); + } catch (error) { + logger?.debug("Abort failed (stream may be closed)", error); + } finally { + mutex.release(); + } + }, + }); +} + +interface SseController { + send: (data: unknown) => void; + close: () => void; +} + +interface ActiveSession { + payload: JwtPayload; + acpConnection: InProcessAcpConnection; + clientConnection: ClientSideConnection; + treeTracker: TreeTracker; + sseController: SseController | null; + deviceInfo: DeviceInfo; + logWriter: SessionLogWriter; +} + +export class AgentServer { + private config: AgentServerConfig; + private logger: Logger; + private server: ServerType | null = null; + private session: ActiveSession | null = null; + private app: Hono; + + constructor(config: AgentServerConfig) { + this.config = config; + this.logger = new Logger({ debug: true, prefix: "[AgentServer]" }); + this.app = this.createApp(); + } + + private createApp(): Hono { + const app = new Hono(); + + app.get("/health", (c) => { + return c.json({ status: "ok", hasSession: !!this.session }); + }); + + app.get("/events", async (c) => { + let payload: JwtPayload; + + try { + payload = this.authenticateRequest(c.req.header.bind(c.req)); + } catch (error) { + return c.json( + { + error: + error instanceof JwtValidationError + ? error.message + : "Invalid token", + code: + error instanceof JwtValidationError + ? error.code + : "invalid_token", + }, + 401, + ); + } + + const stream = new ReadableStream({ + start: async (controller) => { + const sseController: SseController = { + send: (data: unknown) => { + try { + controller.enqueue( + new TextEncoder().encode(`data: ${JSON.stringify(data)}\n\n`), + ); + } catch (error) { + this.logger.debug( + "SSE send failed (stream may be closed)", + error, + ); + } + }, + close: () => { + try { + controller.close(); + } catch (error) { + this.logger.debug("SSE close failed (already closed)", error); + } + }, + }; + + if (!this.session || this.session.payload.run_id !== payload.run_id) { + await this.initializeSession(payload, sseController); + } else { + this.session.sseController = sseController; + } + + this.sendSseEvent(sseController, { + type: "connected", + run_id: payload.run_id, + }); + }, + cancel: () => { + this.logger.info("SSE connection closed"); + if (this.session?.sseController) { + this.session.sseController = null; + } + }, + }); + + return new Response(stream, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }, + }); + }); + + app.post("/command", async (c) => { + let payload: JwtPayload; + + try { + payload = this.authenticateRequest(c.req.header.bind(c.req)); + } catch (error) { + return c.json( + { + error: + error instanceof JwtValidationError + ? error.message + : "Invalid token", + }, + 401, + ); + } + + if (!this.session || this.session.payload.run_id !== payload.run_id) { + return c.json({ error: "No active session for this run" }, 400); + } + + const rawBody = await c.req.json().catch(() => null); + const parseResult = jsonRpcRequestSchema.safeParse(rawBody); + + if (!parseResult.success) { + return c.json({ error: "Invalid JSON-RPC request" }, 400); + } + + const command = parseResult.data; + const paramsValidation = validateCommandParams( + command.method, + command.params ?? {}, + ); + + if (!paramsValidation.success) { + return c.json( + { + jsonrpc: "2.0", + id: command.id, + error: { + code: -32602, + message: paramsValidation.error, + }, + }, + 200, + ); + } + + try { + const result = await this.executeCommand( + command.method, + (command.params as Record) || {}, + ); + return c.json({ + jsonrpc: "2.0", + id: command.id, + result, + }); + } catch (error) { + return c.json({ + jsonrpc: "2.0", + id: command.id, + error: { + code: -32000, + message: error instanceof Error ? error.message : "Unknown error", + }, + }); + } + }); + + app.notFound((c) => { + return c.json({ error: "Not found" }, 404); + }); + + return app; + } + + async start(): Promise { + return new Promise((resolve) => { + this.server = serve( + { + fetch: this.app.fetch, + port: this.config.port, + }, + () => { + this.logger.info(`HTTP server listening on port ${this.config.port}`); + resolve(); + }, + ); + }); + } + + async stop(): Promise { + this.logger.info("Stopping agent server..."); + + if (this.session) { + await this.cleanupSession(); + } + + if (this.server) { + this.server.close(); + this.server = null; + } + + this.logger.info("Agent server stopped"); + } + + private authenticateRequest( + getHeader: (name: string) => string | undefined, + ): JwtPayload { + // Always require JWT validation - never trust unverified headers + if (!this.config.jwtPublicKey) { + throw new JwtValidationError( + "Server not configured with JWT public key", + "server_error", + ); + } + + const authHeader = getHeader("authorization"); + if (!authHeader?.startsWith("Bearer ")) { + throw new JwtValidationError( + "Missing authorization header", + "invalid_token", + ); + } + + const token = authHeader.slice(7); + return validateJwt(token, this.config.jwtPublicKey); + } + + private async executeCommand( + method: string, + params: Record, + ): Promise { + if (!this.session) { + throw new Error("No active session"); + } + + switch (method) { + case POSTHOG_NOTIFICATIONS.USER_MESSAGE: + case "user_message": { + const content = params.content as string; + + this.logger.info( + `Processing user message: ${content.substring(0, 100)}...`, + ); + + const result = await this.session.clientConnection.prompt({ + sessionId: this.session.payload.run_id, + prompt: [{ type: "text", text: content }], + }); + + return { stopReason: result.stopReason }; + } + + case POSTHOG_NOTIFICATIONS.CANCEL: + case "cancel": { + this.logger.info("Cancel requested"); + await this.session.clientConnection.cancel({ + sessionId: this.session.payload.run_id, + }); + return { cancelled: true }; + } + + case POSTHOG_NOTIFICATIONS.CLOSE: + case "close": { + this.logger.info("Close requested"); + await this.cleanupSession(); + return { closed: true }; + } + + default: + throw new Error(`Unknown method: ${method}`); + } + } + + private async initializeSession( + payload: JwtPayload, + sseController: SseController, + ): Promise { + if (this.session) { + await this.cleanupSession(); + } + + this.logger.info("Initializing session", { + runId: payload.run_id, + taskId: payload.task_id, + }); + + const deviceInfo: DeviceInfo = { + type: "cloud", + name: process.env.HOSTNAME || "cloud-sandbox", + }; + + this.configureEnvironment(); + + const treeTracker = new TreeTracker({ + repositoryPath: this.config.repositoryPath, + taskId: payload.task_id, + runId: payload.run_id, + logger: new Logger({ debug: true, prefix: "[TreeTracker]" }), + }); + + const posthogAPI = new PostHogAPIClient({ + apiUrl: this.config.apiUrl, + projectId: this.config.projectId, + getApiKey: () => this.config.apiKey, + }); + + const logWriter = new SessionLogWriter( + posthogAPI, + new Logger({ debug: true, prefix: "[SessionLogWriter]" }), + ); + + const acpConnection = createAcpConnection({ + sessionId: payload.run_id, + taskId: payload.task_id, + logWriter, + }); + + // Tap both streams to broadcast all ACP messages via SSE (mimics local transport) + const onAcpMessage = (message: unknown) => { + this.broadcastEvent({ + type: "notification", + timestamp: new Date().toISOString(), + notification: message, + }); + }; + + const tappedReadable = createTappedReadableStream( + acpConnection.clientStreams.readable as ReadableStream, + onAcpMessage, + this.logger, + ); + + const tappedWritable = createTappedWritableStream( + acpConnection.clientStreams.writable as WritableStream, + onAcpMessage, + this.logger, + ); + + const clientStream = ndJsonStream(tappedWritable, tappedReadable); + + const clientConnection = new ClientSideConnection( + () => this.createCloudClient(payload), + clientStream, + ); + + await clientConnection.initialize({ + protocolVersion: PROTOCOL_VERSION, + clientCapabilities: {}, + }); + + await clientConnection.newSession({ + cwd: this.config.repositoryPath, + mcpServers: [], + _meta: { sessionId: payload.run_id }, + }); + + this.session = { + payload, + acpConnection, + clientConnection, + treeTracker, + sseController, + deviceInfo, + logWriter, + }; + + this.logger.info("Session initialized successfully"); + } + + private configureEnvironment(): void { + const { apiKey, apiUrl, projectId } = this.config; + const gatewayUrl = process.env.LLM_GATEWAY_URL || getLlmGatewayUrl(apiUrl); + const openaiBaseUrl = gatewayUrl.endsWith("/v1") + ? gatewayUrl + : `${gatewayUrl}/v1`; + + Object.assign(process.env, { + // PostHog + POSTHOG_API_KEY: apiKey, + POSTHOG_API_URL: apiUrl, + POSTHOG_API_HOST: apiUrl, + POSTHOG_AUTH_HEADER: `Bearer ${apiKey}`, + POSTHOG_PROJECT_ID: String(projectId), + // Anthropic + ANTHROPIC_API_KEY: apiKey, + ANTHROPIC_AUTH_TOKEN: apiKey, + ANTHROPIC_BASE_URL: gatewayUrl, + // OpenAI (for models like GPT-4, o1, etc.) + OPENAI_API_KEY: apiKey, + OPENAI_BASE_URL: openaiBaseUrl, + // Generic gateway + LLM_GATEWAY_URL: gatewayUrl, + }); + } + + private createCloudClient(_payload: JwtPayload) { + return { + requestPermission: async (params: { + options: Array<{ kind: string; optionId: string }>; + }) => { + const allowOption = params.options.find( + (o) => o.kind === "allow_once" || o.kind === "allow_always", + ); + return { + outcome: { + outcome: "selected" as const, + optionId: allowOption?.optionId ?? params.options[0].optionId, + }, + }; + }, + sessionUpdate: async (params: { + sessionId: string; + update?: Record; + }) => { + // session/update notifications flow through the tapped stream (like local transport) + // Only handle tree state capture for file changes here + if (params.update?.sessionUpdate === "tool_call_update") { + const meta = (params.update?._meta as Record) + ?.claudeCode as Record | undefined; + const toolName = meta?.toolName as string | undefined; + const toolResponse = meta?.toolResponse as + | Record + | undefined; + + if ( + (toolName === "Write" || toolName === "Edit") && + toolResponse?.filePath + ) { + await this.captureTreeState(); + } + } + }, + }; + } + + private async cleanupSession(): Promise { + if (!this.session) return; + + this.logger.info("Cleaning up session"); + + try { + await this.captureTreeState(); + } catch (error) { + this.logger.error("Failed to capture final tree state", error); + } + + try { + await this.session.logWriter.flush(this.session.payload.run_id); + } catch (error) { + this.logger.error("Failed to flush session logs", error); + } + + try { + await this.session.acpConnection.cleanup(); + } catch (error) { + this.logger.error("Failed to cleanup ACP connection", error); + } + + if (this.session.sseController) { + this.session.sseController.close(); + } + + this.session = null; + } + + private async captureTreeState(): Promise { + if (!this.session?.treeTracker) return; + + try { + const snapshot = await this.session.treeTracker.captureTree({}); + if (snapshot) { + const snapshotWithDevice: TreeSnapshotEvent = { + ...snapshot, + device: this.session.deviceInfo, + }; + this.broadcastEvent({ + type: "notification", + timestamp: new Date().toISOString(), + notification: { + jsonrpc: "2.0", + method: POSTHOG_NOTIFICATIONS.TREE_SNAPSHOT, + params: snapshotWithDevice, + }, + }); + } + } catch (error) { + this.logger.error("Failed to capture tree state", error); + } + } + + private broadcastEvent(event: Record): void { + if (this.session?.sseController) { + this.sendSseEvent(this.session.sseController, event); + } + } + + private sendSseEvent(controller: SseController, data: unknown): void { + controller.send(data); + } +} diff --git a/packages/agent/src/server/bin.ts b/packages/agent/src/server/bin.ts new file mode 100644 index 000000000..fcfbb951a --- /dev/null +++ b/packages/agent/src/server/bin.ts @@ -0,0 +1,76 @@ +#!/usr/bin/env node +import { Command } from "commander"; +import { z } from "zod"; +import { AgentServer } from "./agent-server.js"; + +const envSchema = z.object({ + JWT_PUBLIC_KEY: z + .string({ + required_error: + "JWT_PUBLIC_KEY is required for authenticating client connections", + }) + .min(1, "JWT_PUBLIC_KEY cannot be empty"), + POSTHOG_API_URL: z + .string({ + required_error: + "POSTHOG_API_URL is required for LLM gateway communication", + }) + .url("POSTHOG_API_URL must be a valid URL"), + POSTHOG_PERSONAL_API_KEY: z + .string({ + required_error: + "POSTHOG_PERSONAL_API_KEY is required for authenticating with PostHog services", + }) + .min(1, "POSTHOG_PERSONAL_API_KEY cannot be empty"), + POSTHOG_PROJECT_ID: z + .string({ + required_error: + "POSTHOG_PROJECT_ID is required for routing requests to the correct project", + }) + .regex(/^\d+$/, "POSTHOG_PROJECT_ID must be a numeric string") + .transform((val) => parseInt(val, 10)), +}); + +const program = new Command(); + +program + .name("agent-server") + .description("PostHog cloud agent server - runs in sandbox environments") + .option("--port ", "HTTP server port", "3001") + .requiredOption("--repositoryPath ", "Path to the repository") + .action(async (options) => { + const envResult = envSchema.safeParse(process.env); + + if (!envResult.success) { + const errors = envResult.error.issues + .map((issue) => ` - ${issue.message}`) + .join("\n"); + program.error(`Environment validation failed:\n${errors}`); + return; + } + + const env = envResult.data; + + const server = new AgentServer({ + port: parseInt(options.port, 10), + jwtPublicKey: env.JWT_PUBLIC_KEY, + repositoryPath: options.repositoryPath, + apiUrl: env.POSTHOG_API_URL, + apiKey: env.POSTHOG_PERSONAL_API_KEY, + projectId: env.POSTHOG_PROJECT_ID, + }); + + process.on("SIGINT", async () => { + await server.stop(); + process.exit(0); + }); + + process.on("SIGTERM", async () => { + await server.stop(); + process.exit(0); + }); + + await server.start(); + }); + +program.parse(); diff --git a/packages/agent/src/server/jwt.ts b/packages/agent/src/server/jwt.ts new file mode 100644 index 000000000..b75174fd3 --- /dev/null +++ b/packages/agent/src/server/jwt.ts @@ -0,0 +1,64 @@ +import jwt from "jsonwebtoken"; +import { z } from "zod"; + +export const SANDBOX_CONNECTION_AUDIENCE = "posthog:sandbox_connection"; + +export const userDataSchema = z.object({ + run_id: z.string(), + task_id: z.string(), + team_id: z.number(), + user_id: z.number(), + distinct_id: z.string(), +}); + +const jwtPayloadSchema = userDataSchema.extend({ + exp: z.number(), + iat: z.number().optional(), + aud: z.string().optional(), +}); + +export type JwtPayload = z.infer; + +export class JwtValidationError extends Error { + constructor( + message: string, + public code: + | "invalid_token" + | "expired" + | "invalid_signature" + | "server_error", + ) { + super(message); + this.name = "JwtValidationError"; + } +} + +export function validateJwt(token: string, publicKey: string): JwtPayload { + try { + const decoded = jwt.verify(token, publicKey, { + algorithms: ["RS256"], + audience: SANDBOX_CONNECTION_AUDIENCE, + }); + + const result = jwtPayloadSchema.safeParse(decoded); + if (!result.success) { + throw new JwtValidationError( + `Missing required fields: ${result.error.message}`, + "invalid_token", + ); + } + + return result.data; + } catch (error) { + if (error instanceof JwtValidationError) { + throw error; + } + if (error instanceof jwt.TokenExpiredError) { + throw new JwtValidationError("Token expired", "expired"); + } + if (error instanceof jwt.JsonWebTokenError) { + throw new JwtValidationError("Invalid signature", "invalid_signature"); + } + throw new JwtValidationError("Invalid token", "invalid_token"); + } +} diff --git a/packages/agent/src/server/schemas.ts b/packages/agent/src/server/schemas.ts new file mode 100644 index 000000000..9e841d49b --- /dev/null +++ b/packages/agent/src/server/schemas.ts @@ -0,0 +1,47 @@ +import { z } from "zod"; + +export const jsonRpcRequestSchema = z.object({ + jsonrpc: z.literal("2.0"), + method: z.string(), + params: z.record(z.unknown()).optional(), + id: z.union([z.string(), z.number()]).optional(), +}); + +export type JsonRpcRequest = z.infer; + +export const userMessageParamsSchema = z.object({ + content: z.string().min(1, "Content is required"), +}); + +export const commandParamsSchemas = { + user_message: userMessageParamsSchema, + "posthog/user_message": userMessageParamsSchema, + cancel: z.object({}).optional(), + "posthog/cancel": z.object({}).optional(), + close: z.object({}).optional(), + "posthog/close": z.object({}).optional(), +} as const; + +export type CommandMethod = keyof typeof commandParamsSchemas; + +export function validateCommandParams( + method: string, + params: unknown, +): { success: true; data: unknown } | { success: false; error: string } { + const schema = + commandParamsSchemas[method as CommandMethod] ?? + commandParamsSchemas[ + method.replace("posthog/", "") as keyof typeof commandParamsSchemas + ]; + + if (!schema) { + return { success: false, error: `Unknown method: ${method}` }; + } + + const result = schema.safeParse(params); + if (!result.success) { + return { success: false, error: result.error.message }; + } + + return { success: true, data: result.data }; +} diff --git a/packages/agent/src/server/types.ts b/packages/agent/src/server/types.ts new file mode 100644 index 000000000..169d3af8f --- /dev/null +++ b/packages/agent/src/server/types.ts @@ -0,0 +1,8 @@ +export interface AgentServerConfig { + port: number; + repositoryPath: string; + apiUrl: string; + apiKey: string; + projectId: number; + jwtPublicKey: string; // RS256 public key for JWT verification +} diff --git a/packages/agent/src/server/utils/retry.test.ts b/packages/agent/src/server/utils/retry.test.ts new file mode 100644 index 000000000..83862c80b --- /dev/null +++ b/packages/agent/src/server/utils/retry.test.ts @@ -0,0 +1,122 @@ +import { describe, expect, it, vi } from "vitest"; +import { retry } from "./retry.js"; + +describe("retry", () => { + it("returns result on first success", async () => { + const fn = vi.fn().mockResolvedValue("success"); + + const result = await retry(fn); + + expect(result).toBe("success"); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it("retries on transient error and succeeds", async () => { + const fn = vi + .fn() + .mockRejectedValueOnce(new Error("network error")) + .mockResolvedValue("success"); + + const result = await retry(fn, { baseDelayMs: 1 }); + + expect(result).toBe("success"); + expect(fn).toHaveBeenCalledTimes(2); + }); + + it("throws after max attempts", async () => { + const fn = vi.fn().mockRejectedValue(new Error("network error")); + + await expect(retry(fn, { maxAttempts: 3, baseDelayMs: 1 })).rejects.toThrow( + "network error", + ); + + expect(fn).toHaveBeenCalledTimes(3); + }); + + it("does not retry on non-transient error", async () => { + const fn = vi.fn().mockRejectedValue(new Error("validation failed")); + + await expect(retry(fn, { baseDelayMs: 1 })).rejects.toThrow( + "validation failed", + ); + + expect(fn).toHaveBeenCalledTimes(1); + }); + + it("respects custom shouldRetry", async () => { + const fn = vi + .fn() + .mockRejectedValueOnce(new Error("custom error")) + .mockResolvedValue("success"); + + const result = await retry(fn, { + baseDelayMs: 1, + shouldRetry: (err) => err.message === "custom error", + }); + + expect(result).toBe("success"); + expect(fn).toHaveBeenCalledTimes(2); + }); + + it("uses exponential backoff", async () => { + const fn = vi + .fn() + .mockRejectedValueOnce(new Error("timeout")) + .mockRejectedValueOnce(new Error("timeout")) + .mockResolvedValue("success"); + + const start = Date.now(); + await retry(fn, { baseDelayMs: 50, maxDelayMs: 200 }); + const elapsed = Date.now() - start; + + // First retry: 50ms, second retry: 100ms = 150ms minimum + expect(elapsed).toBeGreaterThanOrEqual(100); + }); + + it("caps delay at maxDelayMs", async () => { + const fn = vi + .fn() + .mockRejectedValueOnce(new Error("timeout")) + .mockRejectedValueOnce(new Error("timeout")) + .mockRejectedValueOnce(new Error("timeout")) + .mockResolvedValue("success"); + + const start = Date.now(); + await retry(fn, { maxAttempts: 4, baseDelayMs: 100, maxDelayMs: 150 }); + const elapsed = Date.now() - start; + + // With cap: 100 + 150 + 150 = 400ms max (not 100 + 200 + 400 = 700ms) + expect(elapsed).toBeLessThan(600); + }); + + it("retries on 429 rate limit", async () => { + const fn = vi + .fn() + .mockRejectedValueOnce(new Error("429 Too Many Requests")) + .mockResolvedValue("success"); + + const result = await retry(fn, { baseDelayMs: 1 }); + + expect(result).toBe("success"); + expect(fn).toHaveBeenCalledTimes(2); + }); + + it("retries on 502/503 server errors", async () => { + const fn = vi + .fn() + .mockRejectedValueOnce(new Error("502 Bad Gateway")) + .mockRejectedValueOnce(new Error("503 Service Unavailable")) + .mockResolvedValue("success"); + + const result = await retry(fn, { baseDelayMs: 1 }); + + expect(result).toBe("success"); + expect(fn).toHaveBeenCalledTimes(3); + }); + + it("converts non-Error throws to Error", async () => { + const fn = vi.fn().mockRejectedValue("string error"); + + await expect(retry(fn, { maxAttempts: 1 })).rejects.toThrow("string error"); + }); +}); diff --git a/packages/agent/src/server/utils/retry.ts b/packages/agent/src/server/utils/retry.ts new file mode 100644 index 000000000..99f938bd0 --- /dev/null +++ b/packages/agent/src/server/utils/retry.ts @@ -0,0 +1,61 @@ +export interface RetryOptions { + maxAttempts?: number; + baseDelayMs?: number; + maxDelayMs?: number; + shouldRetry?: (error: Error) => boolean; +} + +const DEFAULT_OPTIONS: Required> = { + maxAttempts: 3, + baseDelayMs: 1000, + maxDelayMs: 10000, +}; + +function isTransientError(error: Error): boolean { + const message = error.message.toLowerCase(); + return ( + message.includes("network") || + message.includes("timeout") || + message.includes("econnreset") || + message.includes("econnrefused") || + message.includes("socket") || + message.includes("503") || + message.includes("502") || + message.includes("429") + ); +} + +export async function retry( + fn: () => Promise, + options: RetryOptions = {}, +): Promise { + const opts = { ...DEFAULT_OPTIONS, ...options }; + const shouldRetry = opts.shouldRetry ?? isTransientError; + + let lastError: Error | null = null; + + for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) { + try { + return await fn(); + } catch (error) { + lastError = error instanceof Error ? error : new Error(String(error)); + + const isLastAttempt = attempt === opts.maxAttempts; + if (isLastAttempt || !shouldRetry(lastError)) { + throw lastError; + } + + const delay = Math.min( + opts.baseDelayMs * 2 ** (attempt - 1), + opts.maxDelayMs, + ); + await sleep(delay); + } + } + + throw lastError ?? new Error("Retry failed with no error"); +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/packages/agent/src/server/utils/sse-parser.test.ts b/packages/agent/src/server/utils/sse-parser.test.ts new file mode 100644 index 000000000..eefe0b859 --- /dev/null +++ b/packages/agent/src/server/utils/sse-parser.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it } from "vitest"; +import { SseEventParser } from "./sse-parser.js"; + +describe("SseEventParser", () => { + it("parses complete SSE event", () => { + const parser = new SseEventParser(); + const events = parser.parse('data: {"message":"hello"}\n\n'); + + expect(events).toHaveLength(1); + expect(events[0].data).toEqual({ message: "hello" }); + }); + + it("parses event with id", () => { + const parser = new SseEventParser(); + const events = parser.parse('id: 123\ndata: {"test":true}\n\n'); + + expect(events).toHaveLength(1); + expect(events[0].id).toBe("123"); + expect(events[0].data).toEqual({ test: true }); + }); + + it("handles chunked data", () => { + const parser = new SseEventParser(); + + const events1 = parser.parse("id: 1\n"); + expect(events1).toHaveLength(0); + + const events2 = parser.parse('data: {"part":"one"}'); + expect(events2).toHaveLength(0); + + const events3 = parser.parse("\n\n"); + expect(events3).toHaveLength(1); + expect(events3[0].id).toBe("1"); + expect(events3[0].data).toEqual({ part: "one" }); + }); + + it("parses multiple events in one chunk", () => { + const parser = new SseEventParser(); + const events = parser.parse('data: {"first":1}\n\ndata: {"second":2}\n\n'); + + expect(events).toHaveLength(2); + expect(events[0].data).toEqual({ first: 1 }); + expect(events[1].data).toEqual({ second: 2 }); + }); + + it("skips malformed JSON", () => { + const parser = new SseEventParser(); + const events = parser.parse('data: not json\n\ndata: {"valid":true}\n\n'); + + expect(events).toHaveLength(1); + expect(events[0].data).toEqual({ valid: true }); + }); + + it("handles empty data lines", () => { + const parser = new SseEventParser(); + const events = parser.parse("data: \n\n"); + + expect(events).toHaveLength(0); + }); + + it("resets state correctly", () => { + const parser = new SseEventParser(); + + parser.parse("id: 1\ndata: {"); + parser.reset(); + + const events = parser.parse('data: {"fresh":true}\n\n'); + + expect(events).toHaveLength(1); + expect(events[0].id).toBeUndefined(); + expect(events[0].data).toEqual({ fresh: true }); + }); + + it("handles events with whitespace in id", () => { + const parser = new SseEventParser(); + const events = parser.parse('id: abc123 \ndata: {"test":1}\n\n'); + + expect(events).toHaveLength(1); + expect(events[0].id).toBe("abc123"); + }); + + it("preserves incomplete line in buffer", () => { + const parser = new SseEventParser(); + + const events1 = parser.parse('data: {"complete":tr'); + expect(events1).toHaveLength(0); + + const events2 = parser.parse('ue}\n\ndata: {"next":1}\n\n'); + expect(events2).toHaveLength(2); + expect(events2[0].data).toEqual({ complete: true }); + expect(events2[1].data).toEqual({ next: 1 }); + }); +}); diff --git a/packages/agent/src/server/utils/sse-parser.ts b/packages/agent/src/server/utils/sse-parser.ts new file mode 100644 index 000000000..7a126131c --- /dev/null +++ b/packages/agent/src/server/utils/sse-parser.ts @@ -0,0 +1,46 @@ +export interface SseEvent { + id?: string; + data: unknown; +} + +export class SseEventParser { + private buffer = ""; + private currentEventId: string | null = null; + private currentData: string | null = null; + + parse(chunk: string): SseEvent[] { + this.buffer += chunk; + const lines = this.buffer.split("\n"); + this.buffer = lines.pop() || ""; + + const events: SseEvent[] = []; + + for (const line of lines) { + if (line.startsWith("id: ")) { + this.currentEventId = line.slice(4).trim(); + } else if (line.startsWith("data: ")) { + this.currentData = line.slice(6); + } else if (line === "" && this.currentData !== null) { + try { + const data = JSON.parse(this.currentData); + events.push({ + id: this.currentEventId ?? undefined, + data, + }); + } catch { + // Skip malformed data + } + this.currentData = null; + this.currentEventId = null; + } + } + + return events; + } + + reset(): void { + this.buffer = ""; + this.currentEventId = null; + this.currentData = null; + } +} diff --git a/packages/agent/src/session-log-writer.test.ts b/packages/agent/src/session-log-writer.test.ts new file mode 100644 index 000000000..28fd5e5f1 --- /dev/null +++ b/packages/agent/src/session-log-writer.test.ts @@ -0,0 +1,145 @@ +import { type SetupServerApi, setupServer } from "msw/node"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { PostHogAPIClient } from "./posthog-api.js"; +import { SessionLogWriter } from "./session-log-writer.js"; +import { createPostHogHandlers } from "./test/mocks/msw-handlers.js"; + +describe("SessionLogWriter", () => { + let mswServer: SetupServerApi; + let appendLogCalls: unknown[][]; + let apiClient: PostHogAPIClient; + let logWriter: SessionLogWriter; + + beforeEach(() => { + appendLogCalls = []; + mswServer = setupServer( + ...createPostHogHandlers({ + baseUrl: "http://localhost:8000", + onAppendLog: (entries) => appendLogCalls.push(entries), + }), + ); + mswServer.listen({ onUnhandledRequest: "bypass" }); + + apiClient = new PostHogAPIClient({ + apiUrl: "http://localhost:8000", + projectId: 1, + getApiKey: () => "test-api-key", + }); + logWriter = new SessionLogWriter(apiClient); + }); + + afterEach(() => { + mswServer.close(); + vi.restoreAllMocks(); + }); + + describe("appendRawLine", () => { + it("buffers entries until flush", async () => { + const sessionId = "test-session"; + logWriter.register(sessionId, { taskId: "task-1", runId: sessionId }); + + logWriter.appendRawLine( + sessionId, + JSON.stringify({ method: "test", params: {} }), + ); + logWriter.appendRawLine( + sessionId, + JSON.stringify({ method: "test2", params: {} }), + ); + + expect(appendLogCalls).toHaveLength(0); + + await logWriter.flush(sessionId); + + expect(appendLogCalls).toHaveLength(1); + expect(appendLogCalls[0]).toHaveLength(2); + }); + + it("wraps raw messages in StoredNotification format", async () => { + const sessionId = "test-session"; + logWriter.register(sessionId, { taskId: "task-1", runId: sessionId }); + + const message = { + jsonrpc: "2.0", + method: "session/update", + params: { foo: "bar" }, + }; + logWriter.appendRawLine(sessionId, JSON.stringify(message)); + + await logWriter.flush(sessionId); + + expect(appendLogCalls).toHaveLength(1); + const entry = appendLogCalls[0][0] as { + type: string; + timestamp: string; + notification: unknown; + }; + expect(entry.type).toBe("notification"); + expect(entry.timestamp).toBeDefined(); + expect(entry.notification).toEqual(message); + }); + + it("ignores unregistered sessions", async () => { + logWriter.appendRawLine( + "unknown-session", + JSON.stringify({ method: "test" }), + ); + await logWriter.flush("unknown-session"); + expect(appendLogCalls).toHaveLength(0); + }); + + it("ignores invalid JSON", async () => { + const sessionId = "test-session"; + logWriter.register(sessionId, { taskId: "task-1", runId: sessionId }); + + logWriter.appendRawLine(sessionId, "not valid json {{{"); + + await logWriter.flush(sessionId); + + expect(appendLogCalls).toHaveLength(0); + }); + }); + + describe("flush", () => { + it("clears pending entries after flush", async () => { + const sessionId = "test-session"; + logWriter.register(sessionId, { taskId: "task-1", runId: sessionId }); + + logWriter.appendRawLine(sessionId, JSON.stringify({ method: "test" })); + await logWriter.flush(sessionId); + + expect(appendLogCalls).toHaveLength(1); + + await logWriter.flush(sessionId); + + expect(appendLogCalls).toHaveLength(1); + }); + + it("does nothing when no pending entries", async () => { + const sessionId = "test-session"; + logWriter.register(sessionId, { taskId: "task-1", runId: sessionId }); + + await logWriter.flush(sessionId); + + expect(appendLogCalls).toHaveLength(0); + }); + }); + + describe("auto-flush scheduling", () => { + it("schedules flush after delay", async () => { + vi.useFakeTimers(); + const sessionId = "test-session"; + logWriter.register(sessionId, { taskId: "task-1", runId: sessionId }); + + logWriter.appendRawLine(sessionId, JSON.stringify({ method: "test" })); + + expect(appendLogCalls).toHaveLength(0); + + await vi.advanceTimersByTimeAsync(600); + + expect(appendLogCalls).toHaveLength(1); + + vi.useRealTimers(); + }); + }); +}); diff --git a/packages/agent/src/test/fixtures/config.ts b/packages/agent/src/test/fixtures/config.ts index 45741b70c..9051d537c 100644 --- a/packages/agent/src/test/fixtures/config.ts +++ b/packages/agent/src/test/fixtures/config.ts @@ -3,6 +3,17 @@ import type { TestRepo } from "./api.js"; export type { AgentServerConfig }; +// Test RSA public key (for testing only - matches TEST_PRIVATE_KEY in agent-server.test.ts) +export const TEST_PUBLIC_KEY = `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6ofeEmDBbLxuAgqPQUho +7T69vzsc7jRq+NTuPgDJA0EXfaSjuPQ4UwOFc8Jzr6x/MuHiPTfDkJ3uwcKXaKLk +p+A6AwEv290lH4o/0aBVEsmYk0KFs9B+qNlbrn4s9B3/gc5WRFZ4UkNa7r6kn/uJ +fHFoHjF2hV4HQ+ZEPBo70ebqisbzthJ79YTCUSnjjhBoAnqf9HOkpDFE10ngvlY8 +qVYPfvMj8bSKTkO1yr/u3vzwNIpanoUUIeH6PQQFo1Ftfh527bIyQI43754MyI6W +o7kFcjIuxu/b/Dr4o4SzCYyQYd03W1SH4vkZFY/x/eFFHylkXyQNHi8pAFb04hX9 +JwIDAQAB +-----END PUBLIC KEY-----`; + export function createAgentServerConfig( repo: TestRepo, overrides: Partial = {}, @@ -13,7 +24,7 @@ export function createAgentServerConfig( apiUrl: "http://localhost:8000", apiKey: "test-api-key", projectId: 1, - jwtSecret: "test-jwt-secret", + jwtPublicKey: TEST_PUBLIC_KEY, ...overrides, }; } diff --git a/packages/agent/src/test/setup.ts b/packages/agent/src/test/setup.ts new file mode 100644 index 000000000..e68cae489 --- /dev/null +++ b/packages/agent/src/test/setup.ts @@ -0,0 +1,114 @@ +import { type SetupServerApi, setupServer } from "msw/node"; +import { AgentServer } from "../server/agent-server.js"; +import { SseController } from "./controllers/sse-controller.js"; +import { createTestRepo, type TestRepo } from "./fixtures/api.js"; +import { + type AgentServerConfig, + createAgentServerConfig, +} from "./fixtures/config.js"; +import { createPostHogHandlers } from "./mocks/msw-handlers.js"; + +export interface TestContext { + repo: TestRepo; + sseController: SseController; + appendLogCalls: unknown[][]; + server: SetupServerApi; + agentServer: AgentServer; + config: AgentServerConfig; + createAgentServer: (overrides?: Partial) => AgentServer; + resetSseController: () => SseController; + cleanup: () => Promise; +} + +export interface CreateTestContextOptions { + configOverrides?: Partial; + autoStart?: boolean; +} + +export async function createTestContext( + options: CreateTestContextOptions = {}, +): Promise { + const repo = await createTestRepo("agent-server"); + let sseController = new SseController(); + const appendLogCalls: unknown[][] = []; + + const server = setupServer( + ...createPostHogHandlers({ + baseUrl: "http://localhost:8000", + onAppendLog: (entries) => appendLogCalls.push(entries), + }), + ); + + server.listen({ onUnhandledRequest: "bypass" }); + + const config = createAgentServerConfig(repo, options.configOverrides); + + const agentServer = new AgentServer(config); + + const createAgentServer = (overrides: Partial = {}) => { + return new AgentServer({ + ...config, + ...overrides, + }); + }; + + const resetSseController = () => { + sseController.close(); + sseController = new SseController(); + return sseController; + }; + + const cleanup = async () => { + sseController.close(); + server.close(); + await repo.cleanup(); + }; + + return { + repo, + sseController, + appendLogCalls, + server, + agentServer, + config, + createAgentServer, + resetSseController, + cleanup, + }; +} + +export { + expectNoNotification, + expectNotification, + findNotification, + hasNotification, +} from "./assertions.js"; +export { + createMockApiClient, + createTaskRun, + createTestRepo, + type TestRepo, +} from "./fixtures/api.js"; +export { createAgentServerConfig } from "./fixtures/config.js"; +export { + createAgentChunk, + createNotification, + createStatusNotification, + createToolCall, + createToolResult, + createTreeSnapshotNotification, + createUserMessage, +} from "./fixtures/notifications.js"; +export { + createErrorResult, + createInitMessage, + createMockQuery, + createSuccessResult, + type MockQuery, +} from "./mocks/claude-sdk.js"; +export { createPostHogHandlers, SseController } from "./mocks/msw-handlers.js"; +export { + waitForArrayLength, + waitForCallCount, + waitForCondition, +} from "./wait.js"; diff --git a/packages/agent/src/utils/async-mutex.test.ts b/packages/agent/src/utils/async-mutex.test.ts new file mode 100644 index 000000000..87c3de1e4 --- /dev/null +++ b/packages/agent/src/utils/async-mutex.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, it } from "vitest"; +import { AsyncMutex } from "./async-mutex.js"; + +describe("AsyncMutex", () => { + it("acquires lock when unlocked", async () => { + const mutex = new AsyncMutex(); + + expect(mutex.isLocked()).toBe(false); + await mutex.acquire(); + expect(mutex.isLocked()).toBe(true); + }); + + it("releases lock", async () => { + const mutex = new AsyncMutex(); + + await mutex.acquire(); + expect(mutex.isLocked()).toBe(true); + + mutex.release(); + expect(mutex.isLocked()).toBe(false); + }); + + it("queues concurrent acquires", async () => { + const mutex = new AsyncMutex(); + const order: number[] = []; + + await mutex.acquire(); + expect(mutex.queueLength).toBe(0); + + const promise1 = mutex.acquire().then(() => order.push(1)); + const promise2 = mutex.acquire().then(() => order.push(2)); + + expect(mutex.queueLength).toBe(2); + + mutex.release(); + await promise1; + expect(order).toEqual([1]); + + mutex.release(); + await promise2; + expect(order).toEqual([1, 2]); + }); + + it("processes queue in FIFO order", async () => { + const mutex = new AsyncMutex(); + const order: number[] = []; + + await mutex.acquire(); + + const promises = [1, 2, 3, 4, 5].map((n) => + mutex.acquire().then(() => { + order.push(n); + }), + ); + + expect(mutex.queueLength).toBe(5); + + for (let i = 0; i < 5; i++) { + mutex.release(); + await promises[i]; + } + + expect(order).toEqual([1, 2, 3, 4, 5]); + }); + + it("serializes concurrent operations", async () => { + const mutex = new AsyncMutex(); + const results: string[] = []; + + const operation = async (id: string, delay: number) => { + await mutex.acquire(); + try { + results.push(`${id}-start`); + await new Promise((r) => setTimeout(r, delay)); + results.push(`${id}-end`); + } finally { + mutex.release(); + } + }; + + await Promise.all([ + operation("a", 10), + operation("b", 5), + operation("c", 1), + ]); + + expect(results).toEqual([ + "a-start", + "a-end", + "b-start", + "b-end", + "c-start", + "c-end", + ]); + }); + + it("handles release without acquire gracefully", () => { + const mutex = new AsyncMutex(); + + expect(mutex.isLocked()).toBe(false); + mutex.release(); + expect(mutex.isLocked()).toBe(false); + }); +}); diff --git a/packages/agent/src/utils/async-mutex.ts b/packages/agent/src/utils/async-mutex.ts new file mode 100644 index 000000000..6f1fb7e11 --- /dev/null +++ b/packages/agent/src/utils/async-mutex.ts @@ -0,0 +1,31 @@ +export class AsyncMutex { + private locked = false; + private queue: Array<() => void> = []; + + async acquire(): Promise { + if (!this.locked) { + this.locked = true; + return; + } + return new Promise((resolve) => { + this.queue.push(resolve); + }); + } + + release(): void { + const next = this.queue.shift(); + if (next) { + next(); + } else { + this.locked = false; + } + } + + isLocked(): boolean { + return this.locked; + } + + get queueLength(): number { + return this.queue.length; + } +} diff --git a/packages/agent/tsup.config.ts b/packages/agent/tsup.config.ts index 98a9d3ef5..9d6023046 100644 --- a/packages/agent/tsup.config.ts +++ b/packages/agent/tsup.config.ts @@ -61,6 +61,8 @@ export default defineConfig({ "src/adapters/claude/permissions/permission-options.ts", "src/adapters/claude/tools.ts", "src/adapters/claude/conversion/tool-use-to-acp.ts", + "src/server/agent-server.ts", + "src/server/bin.ts", ], format: ["esm"], dts: true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2caafc21c..9dc666a39 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,7 +44,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.16 - version: 4.0.17(@opentelemetry/api@1.9.0)(@types/node@25.0.8)(@vitest/ui@4.0.17)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.17(@opentelemetry/api@1.9.0)(@types/node@25.0.8)(@vitest/ui@4.0.17)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.8)(typescript@5.9.3))(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2) apps/mobile: dependencies: @@ -469,10 +469,10 @@ importers: version: 10.2.0(storybook@10.2.0(@testing-library/dom@10.4.1)(prettier@3.8.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) '@storybook/addon-docs': specifier: 10.2.0 - version: 10.2.0(@types/react@19.2.8)(esbuild@0.27.2)(rollup@4.55.1)(storybook@10.2.0(@testing-library/dom@10.4.1)(prettier@3.8.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.4.1(@types/node@20.19.29)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.2)) + version: 10.2.0(@types/react@19.2.8)(esbuild@0.27.2)(rollup@4.55.1)(storybook@10.2.0(@testing-library/dom@10.4.1)(prettier@3.8.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.2)) '@storybook/react-vite': specifier: 10.2.0 - version: 10.2.0(esbuild@0.27.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.55.1)(storybook@10.2.0(@testing-library/dom@10.4.1)(prettier@3.8.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.29)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.2)) + version: 10.2.0(esbuild@0.27.2)(msw@2.12.7(@types/node@20.19.29)(typescript@5.9.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.55.1)(storybook@10.2.0(@testing-library/dom@10.4.1)(prettier@3.8.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.2)) '@testing-library/jest-dom': specifier: ^6.9.1 version: 6.9.1 @@ -496,7 +496,7 @@ importers: version: 9.0.8 '@vitejs/plugin-react': specifier: ^4.2.1 - version: 4.7.0(vite@6.4.1(@types/node@20.19.29)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.7.0(vite@6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.10 version: 4.0.17(vitest@4.0.17) @@ -541,13 +541,13 @@ importers: version: 5.9.3 vite: specifier: ^6.0.7 - version: 6.4.1(@types/node@20.19.29)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2) + version: 6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.29)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2)) vitest: specifier: ^4.0.10 - version: 4.0.17(@opentelemetry/api@1.9.0)(@types/node@20.19.29)(@vitest/ui@4.0.17)(jiti@1.21.7)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.17(@opentelemetry/api@1.9.0)(@types/node@20.19.29)(@vitest/ui@4.0.17)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(msw@2.12.7(@types/node@20.19.29)(typescript@5.9.3))(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2) yaml: specifier: ^2.8.1 version: 2.8.2 @@ -563,21 +563,36 @@ importers: '@anthropic-ai/sdk': specifier: ^0.71.0 version: 0.71.2(zod@3.25.76) + '@hono/node-server': + specifier: ^1.19.9 + version: 1.19.9(hono@4.11.7) '@modelcontextprotocol/sdk': specifier: ^1.25.3 - version: 1.25.3(hono@4.11.4)(zod@3.25.76) + version: 1.25.3(hono@4.11.7)(zod@3.25.76) '@posthog/shared': specifier: workspace:* version: link:../shared '@twig/git': specifier: workspace:* version: link:../git + '@types/jsonwebtoken': + specifier: ^9.0.10 + version: 9.0.10 + commander: + specifier: ^14.0.2 + version: 14.0.2 diff: specifier: ^8.0.2 version: 8.0.3 dotenv: specifier: ^17.2.3 version: 17.2.3 + hono: + specifier: ^4.11.7 + version: 4.11.7 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.3 tar: specifier: ^7.5.0 version: 7.5.6 @@ -603,6 +618,9 @@ importers: minimatch: specifier: ^10.0.3 version: 10.1.1 + msw: + specifier: ^2.12.7 + version: 2.12.7(@types/node@25.0.8)(typescript@5.9.3) tsup: specifier: ^8.5.1 version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) @@ -614,7 +632,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.1.8 - version: 2.1.9(@types/node@25.0.8)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.45.0) + version: 2.1.9(@types/node@25.0.8)(jsdom@26.1.0)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.8)(typescript@5.9.3))(terser@5.45.0) packages/core: dependencies: @@ -654,7 +672,7 @@ importers: version: 20.19.29 '@vitest/coverage-v8': specifier: ^0.34.0 - version: 0.34.6(vitest@2.1.9(@types/node@20.19.29)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.45.0)) + version: 0.34.6(vitest@2.1.9(@types/node@20.19.29)(jsdom@26.1.0)(lightningcss@1.30.2)(msw@2.12.7(@types/node@20.19.29)(typescript@5.9.3))(terser@5.45.0)) builtin-modules: specifier: ^3.3.0 version: 3.3.0 @@ -678,7 +696,7 @@ importers: version: 0.1.4(vite@6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2)) vitest: specifier: ^2.1.8 - version: 2.1.9(@types/node@20.19.29)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.45.0) + version: 2.1.9(@types/node@20.19.29)(jsdom@26.1.0)(lightningcss@1.30.2)(msw@2.12.7(@types/node@20.19.29)(typescript@5.9.3))(terser@5.45.0) zod: specifier: ^3.24.1 version: 3.25.76 @@ -2388,6 +2406,10 @@ packages: cpu: [x64] os: [win32] + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + '@inquirer/checkbox@3.0.1': resolution: {integrity: sha512-0hm2nrToWUdD6/UHnel/UKGdk1//ke5zGUpHIvk5ZWmaKezlGxZkOJXNSWsdxO/rEqTkbB3lNC2J6nBElV2aAQ==} engines: {node: '>=18'} @@ -2396,6 +2418,24 @@ packages: resolution: {integrity: sha512-46yL28o2NJ9doViqOy0VDcoTzng7rAb6yPQKU7VDLqkmbCaH4JqK4yk4XqlzNWy9PVC5pG1ZUXPBQv+VqnYs2w==} engines: {node: '>=18'} + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/core@9.2.1': resolution: {integrity: sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==} engines: {node: '>=18'} @@ -2457,6 +2497,15 @@ packages: resolution: {integrity: sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==} engines: {node: '>=18'} + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inversifyjs/common@1.5.2': resolution: {integrity: sha512-WlzR9xGadABS9gtgZQ+luoZ8V6qm4Ii6RQfcfC9Ho2SOlE6ZuemFo7PKJvKI0ikm8cmKbU8hw5UK6E4qovH21w==} @@ -2766,6 +2815,10 @@ packages: '@cfworker/json-schema': optional: true + '@mswjs/interceptors@0.40.0': + resolution: {integrity: sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==} + engines: {node: '>=18'} + '@napi-rs/wasm-runtime@1.1.1': resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} @@ -2917,6 +2970,15 @@ packages: '@octokit/types@6.41.0': resolution: {integrity: sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==} + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@opentelemetry/api-logs@0.208.0': resolution: {integrity: sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==} engines: {node: '>=8.0.0'} @@ -4663,6 +4725,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + '@types/keyv@3.1.4': resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} @@ -4719,6 +4784,9 @@ packages: '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/statuses@2.0.6': + resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + '@types/tar@6.1.13': resolution: {integrity: sha512-IznnlmU5f4WcGTh2ltRu/Ijpmk8wiWXfF0VA4s+HPjHZgvFggk1YaIkbo5krX/zUCzWF8N/l4+W/LNxnvAJ8nw==} @@ -5328,6 +5396,9 @@ packages: buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -5592,6 +5663,10 @@ packages: resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} engines: {node: '>=18'} + commander@14.0.2: + resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} + engines: {node: '>=20'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -5664,6 +5739,10 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + copy-anything@4.0.5: resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} engines: {node: '>=18'} @@ -5940,6 +6019,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -6820,6 +6902,9 @@ packages: hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + hermes-estree@0.29.1: resolution: {integrity: sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ==} @@ -6832,8 +6917,8 @@ packages: hermes-parser@0.32.0: resolution: {integrity: sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==} - hono@4.11.4: - resolution: {integrity: sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==} + hono@4.11.7: + resolution: {integrity: sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==} engines: {node: '>=16.9.0'} hosted-git-info@2.8.9: @@ -7062,6 +7147,9 @@ packages: is-my-json-valid@2.20.6: resolution: {integrity: sha512-1JQwulVNjx8UqkPE/bqDaxtH4PXCe/2VRh/y3p99heOV87HG4Id5/VfDswd+YiAfHcRTfDlWgISycnHuhZq1aw==} + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -7283,10 +7371,20 @@ packages: resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} engines: {node: '>=0.10.0'} + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + junk@3.1.0: resolution: {integrity: sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==} engines: {node: '>=8'} + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -7512,6 +7610,27 @@ packages: resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} @@ -7996,6 +8115,16 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msw@2.12.7: + resolution: {integrity: sha512-retd5i3xCZDVWMYjHEVuKTmhqY8lSsxujjVrZiGbbdoxxIBg5S7rCuYy/YQpfrTYIxpd/o0Kyb/3H+1udBMoYg==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + murmur-32@0.2.0: resolution: {integrity: sha512-ZkcWZudylwF+ir3Ld1n7gL6bI2mQAzXvSobPwVtu8aYi2sbXeipeSkdcanRLzIofLcM5F53lGaKm2dk7orBi7Q==} @@ -8003,6 +8132,10 @@ packages: resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -8224,6 +8357,9 @@ packages: outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + oxc-resolver@11.16.3: resolution: {integrity: sha512-goLOJH3x69VouGWGp5CgCIHyksmOZzXr36lsRmQz1APg3SPFORrvV2q7nsUHMzLVa6ZJgNwkgUSJFsbCpAWkCA==} @@ -8381,6 +8517,9 @@ packages: resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} engines: {node: 20 || >=22} + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-to-regexp@8.3.0: resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} @@ -9148,6 +9287,9 @@ packages: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} + rettime@0.7.0: + resolution: {integrity: sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -9477,6 +9619,9 @@ packages: resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==} engines: {node: '>= 0.10.0'} + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + strict-uri-encode@2.0.0: resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} engines: {node: '>=4'} @@ -9725,10 +9870,17 @@ packages: tldts-core@6.1.86: resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + tldts-core@7.0.19: + resolution: {integrity: sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==} + tldts@6.1.86: resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} hasBin: true + tldts@7.0.19: + resolution: {integrity: sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==} + hasBin: true + tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -9763,6 +9915,10 @@ packages: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} + tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + engines: {node: '>=16'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -10025,6 +10181,9 @@ packages: resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} engines: {node: '>=18.12.0'} + until-async@3.0.2: + resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -12678,9 +12837,9 @@ snapshots: '@gar/promisify@1.1.3': {} - '@hono/node-server@1.19.9(hono@4.11.4)': + '@hono/node-server@1.19.9(hono@4.11.7)': dependencies: - hono: 4.11.4 + hono: 4.11.7 '@img/sharp-darwin-arm64@0.33.5': optionalDependencies: @@ -12741,6 +12900,8 @@ snapshots: '@img/sharp-win32-x64@0.33.5': optional: true + '@inquirer/ansi@1.0.2': {} + '@inquirer/checkbox@3.0.1': dependencies: '@inquirer/core': 9.2.1 @@ -12754,6 +12915,48 @@ snapshots: '@inquirer/core': 9.2.1 '@inquirer/type': 2.0.0 + '@inquirer/confirm@5.1.21(@types/node@20.19.29)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@20.19.29) + '@inquirer/type': 3.0.10(@types/node@20.19.29) + optionalDependencies: + '@types/node': 20.19.29 + optional: true + + '@inquirer/confirm@5.1.21(@types/node@25.0.8)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@25.0.8) + '@inquirer/type': 3.0.10(@types/node@25.0.8) + optionalDependencies: + '@types/node': 25.0.8 + + '@inquirer/core@10.3.2(@types/node@20.19.29)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@20.19.29) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 20.19.29 + optional: true + + '@inquirer/core@10.3.2(@types/node@25.0.8)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@25.0.8) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 25.0.8 + '@inquirer/core@9.2.1': dependencies: '@inquirer/figures': 1.0.15 @@ -12848,6 +13051,15 @@ snapshots: dependencies: mute-stream: 1.0.0 + '@inquirer/type@3.0.10(@types/node@20.19.29)': + optionalDependencies: + '@types/node': 20.19.29 + optional: true + + '@inquirer/type@3.0.10(@types/node@25.0.8)': + optionalDependencies: + '@types/node': 25.0.8 + '@inversifyjs/common@1.5.2': {} '@inversifyjs/container@1.15.0(reflect-metadata@0.2.2)': @@ -13149,11 +13361,11 @@ snapshots: '@jimp/types': 1.6.0 tinycolor2: 1.6.0 - '@joshwooding/vite-plugin-react-docgen-typescript@0.6.3(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.29)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.3(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: glob: 11.1.0 react-docgen-typescript: 2.4.0(typescript@5.9.3) - vite: 6.4.1(@types/node@20.19.29)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2) optionalDependencies: typescript: 5.9.3 @@ -13317,9 +13529,9 @@ snapshots: '@types/react': 19.2.8 react: 19.1.0 - '@modelcontextprotocol/sdk@1.25.3(hono@4.11.4)(zod@3.25.76)': + '@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@3.25.76)': dependencies: - '@hono/node-server': 1.19.9(hono@4.11.4) + '@hono/node-server': 1.19.9(hono@4.11.7) ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) content-type: 1.0.5 @@ -13339,6 +13551,15 @@ snapshots: - hono - supports-color + '@mswjs/interceptors@0.40.0': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + '@napi-rs/wasm-runtime@1.1.1': dependencies: '@emnapi/core': 1.8.1 @@ -13525,6 +13746,15 @@ snapshots: dependencies: '@octokit/openapi-types': 12.11.0 + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + '@opentelemetry/api-logs@0.208.0': dependencies: '@opentelemetry/api': 1.9.0 @@ -14860,10 +15090,10 @@ snapshots: axe-core: 4.11.1 storybook: 10.2.0(@testing-library/dom@10.4.1)(prettier@3.8.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@storybook/addon-docs@10.2.0(@types/react@19.2.8)(esbuild@0.27.2)(rollup@4.55.1)(storybook@10.2.0(@testing-library/dom@10.4.1)(prettier@3.8.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.4.1(@types/node@20.19.29)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.2))': + '@storybook/addon-docs@10.2.0(@types/react@19.2.8)(esbuild@0.27.2)(rollup@4.55.1)(storybook@10.2.0(@testing-library/dom@10.4.1)(prettier@3.8.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.2))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.2.8)(react@19.1.0) - '@storybook/csf-plugin': 10.2.0(esbuild@0.27.2)(rollup@4.55.1)(storybook@10.2.0(@testing-library/dom@10.4.1)(prettier@3.8.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.4.1(@types/node@20.19.29)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.2)) + '@storybook/csf-plugin': 10.2.0(esbuild@0.27.2)(rollup@4.55.1)(storybook@10.2.0(@testing-library/dom@10.4.1)(prettier@3.8.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.2)) '@storybook/icons': 2.0.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@storybook/react-dom-shim': 10.2.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@10.2.0(@testing-library/dom@10.4.1)(prettier@3.8.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) react: 19.1.0 @@ -14877,27 +15107,27 @@ snapshots: - vite - webpack - '@storybook/builder-vite@10.2.0(esbuild@0.27.2)(rollup@4.55.1)(storybook@10.2.0(@testing-library/dom@10.4.1)(prettier@3.8.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.4.1(@types/node@20.19.29)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.2))': + '@storybook/builder-vite@10.2.0(esbuild@0.27.2)(msw@2.12.7(@types/node@20.19.29)(typescript@5.9.3))(rollup@4.55.1)(storybook@10.2.0(@testing-library/dom@10.4.1)(prettier@3.8.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.2))': dependencies: - '@storybook/csf-plugin': 10.2.0(esbuild@0.27.2)(rollup@4.55.1)(storybook@10.2.0(@testing-library/dom@10.4.1)(prettier@3.8.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.4.1(@types/node@20.19.29)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.2)) - '@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@20.19.29)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2)) + '@storybook/csf-plugin': 10.2.0(esbuild@0.27.2)(rollup@4.55.1)(storybook@10.2.0(@testing-library/dom@10.4.1)(prettier@3.8.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.2)) + '@vitest/mocker': 3.2.4(msw@2.12.7(@types/node@20.19.29)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2)) storybook: 10.2.0(@testing-library/dom@10.4.1)(prettier@3.8.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) ts-dedent: 2.2.0 - vite: 6.4.1(@types/node@20.19.29)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - esbuild - msw - rollup - webpack - '@storybook/csf-plugin@10.2.0(esbuild@0.27.2)(rollup@4.55.1)(storybook@10.2.0(@testing-library/dom@10.4.1)(prettier@3.8.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.4.1(@types/node@20.19.29)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.2))': + '@storybook/csf-plugin@10.2.0(esbuild@0.27.2)(rollup@4.55.1)(storybook@10.2.0(@testing-library/dom@10.4.1)(prettier@3.8.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.2))': dependencies: storybook: 10.2.0(@testing-library/dom@10.4.1)(prettier@3.8.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) unplugin: 2.3.11 optionalDependencies: esbuild: 0.27.2 rollup: 4.55.1 - vite: 6.4.1(@types/node@20.19.29)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2) webpack: 5.104.1(esbuild@0.27.2) '@storybook/global@5.0.0': {} @@ -14913,11 +15143,11 @@ snapshots: react-dom: 19.1.0(react@19.1.0) storybook: 10.2.0(@testing-library/dom@10.4.1)(prettier@3.8.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@storybook/react-vite@10.2.0(esbuild@0.27.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.55.1)(storybook@10.2.0(@testing-library/dom@10.4.1)(prettier@3.8.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.29)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.2))': + '@storybook/react-vite@10.2.0(esbuild@0.27.2)(msw@2.12.7(@types/node@20.19.29)(typescript@5.9.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.55.1)(storybook@10.2.0(@testing-library/dom@10.4.1)(prettier@3.8.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.2))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.3(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.29)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.3(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2)) '@rollup/pluginutils': 5.3.0(rollup@4.55.1) - '@storybook/builder-vite': 10.2.0(esbuild@0.27.2)(rollup@4.55.1)(storybook@10.2.0(@testing-library/dom@10.4.1)(prettier@3.8.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.4.1(@types/node@20.19.29)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.2)) + '@storybook/builder-vite': 10.2.0(esbuild@0.27.2)(msw@2.12.7(@types/node@20.19.29)(typescript@5.9.3))(rollup@4.55.1)(storybook@10.2.0(@testing-library/dom@10.4.1)(prettier@3.8.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.2)) '@storybook/react': 10.2.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@10.2.0(@testing-library/dom@10.4.1)(prettier@3.8.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(typescript@5.9.3) empathic: 2.0.0 magic-string: 0.30.21 @@ -14927,7 +15157,7 @@ snapshots: resolve: 1.22.11 storybook: 10.2.0(@testing-library/dom@10.4.1)(prettier@3.8.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) tsconfig-paths: 4.2.0 - vite: 6.4.1(@types/node@20.19.29)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - esbuild - msw @@ -15313,6 +15543,11 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 20.19.29 + '@types/keyv@3.1.4': dependencies: '@types/node': 20.19.29 @@ -15370,6 +15605,8 @@ snapshots: '@types/stack-utils@2.0.3': {} + '@types/statuses@2.0.6': {} + '@types/tar@6.1.13': dependencies: '@types/node': 20.19.29 @@ -15415,7 +15652,7 @@ snapshots: '@vercel/oidc@3.1.0': {} - '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@20.19.29)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2))': + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/core': 7.28.6 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.6) @@ -15423,11 +15660,11 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.1(@types/node@20.19.29)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@0.34.6(vitest@2.1.9(@types/node@20.19.29)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.45.0))': + '@vitest/coverage-v8@0.34.6(vitest@2.1.9(@types/node@20.19.29)(jsdom@26.1.0)(lightningcss@1.30.2)(msw@2.12.7(@types/node@20.19.29)(typescript@5.9.3))(terser@5.45.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 @@ -15440,7 +15677,7 @@ snapshots: std-env: 3.10.0 test-exclude: 6.0.0 v8-to-istanbul: 9.3.0 - vitest: 2.1.9(@types/node@20.19.29)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.45.0) + vitest: 2.1.9(@types/node@20.19.29)(jsdom@26.1.0)(lightningcss@1.30.2)(msw@2.12.7(@types/node@20.19.29)(typescript@5.9.3))(terser@5.45.0) transitivePeerDependencies: - supports-color @@ -15468,44 +15705,49 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@20.19.29)(lightningcss@1.30.2)(terser@5.45.0))': + '@vitest/mocker@2.1.9(msw@2.12.7(@types/node@20.19.29)(typescript@5.9.3))(vite@5.4.21(@types/node@20.19.29)(lightningcss@1.30.2)(terser@5.45.0))': dependencies: '@vitest/spy': 2.1.9 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: + msw: 2.12.7(@types/node@20.19.29)(typescript@5.9.3) vite: 5.4.21(@types/node@20.19.29)(lightningcss@1.30.2)(terser@5.45.0) - '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@25.0.8)(lightningcss@1.30.2)(terser@5.45.0))': + '@vitest/mocker@2.1.9(msw@2.12.7(@types/node@25.0.8)(typescript@5.9.3))(vite@5.4.21(@types/node@25.0.8)(lightningcss@1.30.2)(terser@5.45.0))': dependencies: '@vitest/spy': 2.1.9 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: + msw: 2.12.7(@types/node@25.0.8)(typescript@5.9.3) vite: 5.4.21(@types/node@25.0.8)(lightningcss@1.30.2)(terser@5.45.0) - '@vitest/mocker@3.2.4(vite@6.4.1(@types/node@20.19.29)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@3.2.4(msw@2.12.7(@types/node@20.19.29)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 6.4.1(@types/node@20.19.29)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2) + msw: 2.12.7(@types/node@20.19.29)(typescript@5.9.3) + vite: 6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2) - '@vitest/mocker@4.0.17(vite@6.4.1(@types/node@20.19.29)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@4.0.17(msw@2.12.7(@types/node@20.19.29)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 4.0.17 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 6.4.1(@types/node@20.19.29)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2) + msw: 2.12.7(@types/node@20.19.29)(typescript@5.9.3) + vite: 6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2) - '@vitest/mocker@4.0.17(vite@6.4.1(@types/node@25.0.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@4.0.17(msw@2.12.7(@types/node@25.0.8)(typescript@5.9.3))(vite@6.4.1(@types/node@25.0.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 4.0.17 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: + msw: 2.12.7(@types/node@25.0.8)(typescript@5.9.3) vite: 6.4.1(@types/node@25.0.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2) '@vitest/pretty-format@2.1.9': @@ -15561,7 +15803,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vitest: 4.0.17(@opentelemetry/api@1.9.0)(@types/node@25.0.8)(@vitest/ui@4.0.17)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2) + vitest: 4.0.17(@opentelemetry/api@1.9.0)(@types/node@25.0.8)(@vitest/ui@4.0.17)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.8)(typescript@5.9.3))(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2) '@vitest/utils@2.1.9': dependencies: @@ -16116,6 +16358,8 @@ snapshots: buffer-crc32@0.2.13: {} + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} buffer@5.7.1: @@ -16405,6 +16649,8 @@ snapshots: commander@13.1.0: {} + commander@14.0.2: {} + commander@2.20.3: {} commander@4.1.1: {} @@ -16476,6 +16722,8 @@ snapshots: cookie@0.7.2: {} + cookie@1.1.1: {} + copy-anything@4.0.5: dependencies: is-what: 5.5.0 @@ -16717,6 +16965,10 @@ snapshots: eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + ee-first@1.1.1: {} electron-installer-dmg@5.0.1: @@ -17776,6 +18028,8 @@ snapshots: dependencies: '@types/hast': 3.0.4 + headers-polyfill@4.0.3: {} + hermes-estree@0.29.1: {} hermes-estree@0.32.0: {} @@ -17788,7 +18042,7 @@ snapshots: dependencies: hermes-estree: 0.32.0 - hono@4.11.4: {} + hono@4.11.7: {} hosted-git-info@2.8.9: {} @@ -17992,6 +18246,8 @@ snapshots: xtend: 4.0.2 optional: true + is-node-process@1.2.0: {} + is-number@7.0.0: {} is-plain-obj@2.1.0: {} @@ -18269,8 +18525,32 @@ snapshots: jsonpointer@5.0.1: optional: true + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.3 + junk@3.1.0: {} + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -18470,6 +18750,20 @@ snapshots: lodash.get@4.4.2: {} + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.once@4.1.1: {} + lodash.startcase@4.4.0: {} lodash.throttle@4.1.1: {} @@ -19280,6 +19574,57 @@ snapshots: ms@2.1.3: {} + msw@2.12.7(@types/node@20.19.29)(typescript@5.9.3): + dependencies: + '@inquirer/confirm': 5.1.21(@types/node@20.19.29) + '@mswjs/interceptors': 0.40.0 + '@open-draft/deferred-promise': 2.2.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.12.0 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.7.0 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.0 + type-fest: 5.4.1 + until-async: 3.0.2 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/node' + optional: true + + msw@2.12.7(@types/node@25.0.8)(typescript@5.9.3): + dependencies: + '@inquirer/confirm': 5.1.21(@types/node@25.0.8) + '@mswjs/interceptors': 0.40.0 + '@open-draft/deferred-promise': 2.2.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.12.0 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.7.0 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.0 + type-fest: 5.4.1 + until-async: 3.0.2 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/node' + murmur-32@0.2.0: dependencies: encode-utf8: 1.0.3 @@ -19289,6 +19634,8 @@ snapshots: mute-stream@1.0.0: {} + mute-stream@2.0.0: {} + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -19515,6 +19862,8 @@ snapshots: outdent@0.5.0: {} + outvariant@1.4.3: {} + oxc-resolver@11.16.3: optionalDependencies: '@oxc-resolver/binding-android-arm-eabi': 11.16.3 @@ -19670,6 +20019,8 @@ snapshots: lru-cache: 11.2.4 minipass: 7.1.2 + path-to-regexp@6.3.0: {} + path-to-regexp@8.3.0: {} path-type@2.0.0: @@ -20576,6 +20927,8 @@ snapshots: retry@0.12.0: {} + rettime@0.7.0: {} + reusify@1.1.0: {} rfdc@1.4.1: {} @@ -20962,6 +21315,8 @@ snapshots: stream-buffers@2.2.0: {} + strict-event-emitter@0.5.1: {} + strict-uri-encode@2.0.0: {} string-argv@0.3.2: {} @@ -21214,10 +21569,16 @@ snapshots: tldts-core@6.1.86: {} + tldts-core@7.0.19: {} + tldts@6.1.86: dependencies: tldts-core: 6.1.86 + tldts@7.0.19: + dependencies: + tldts-core: 7.0.19 + tmp@0.0.33: dependencies: os-tmpdir: 1.0.2 @@ -21249,6 +21610,10 @@ snapshots: dependencies: tldts: 6.1.86 + tough-cookie@6.0.0: + dependencies: + tldts: 7.0.19 + tr46@0.0.3: {} tr46@5.1.1: @@ -21491,6 +21856,8 @@ snapshots: picomatch: 4.0.3 webpack-virtual-modules: 0.6.2 + until-async@3.0.2: {} + update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: browserslist: 4.28.1 @@ -21616,13 +21983,13 @@ snapshots: magic-string: 0.30.21 vite: 6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2) - vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.29)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2)): + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: - vite: 6.4.1(@types/node@20.19.29)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color - typescript @@ -21649,23 +22016,6 @@ snapshots: lightningcss: 1.30.2 terser: 5.45.0 - vite@6.4.1(@types/node@20.19.29)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2): - dependencies: - esbuild: 0.25.12 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.55.1 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 20.19.29 - fsevents: 2.3.3 - jiti: 1.21.7 - lightningcss: 1.30.2 - terser: 5.45.0 - tsx: 4.21.0 - yaml: 2.8.2 - vite@6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.25.12 @@ -21700,10 +22050,10 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vitest@2.1.9(@types/node@20.19.29)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.45.0): + vitest@2.1.9(@types/node@20.19.29)(jsdom@26.1.0)(lightningcss@1.30.2)(msw@2.12.7(@types/node@20.19.29)(typescript@5.9.3))(terser@5.45.0): dependencies: '@vitest/expect': 2.1.9 - '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@20.19.29)(lightningcss@1.30.2)(terser@5.45.0)) + '@vitest/mocker': 2.1.9(msw@2.12.7(@types/node@20.19.29)(typescript@5.9.3))(vite@5.4.21(@types/node@20.19.29)(lightningcss@1.30.2)(terser@5.45.0)) '@vitest/pretty-format': 2.1.9 '@vitest/runner': 2.1.9 '@vitest/snapshot': 2.1.9 @@ -21736,10 +22086,10 @@ snapshots: - supports-color - terser - vitest@2.1.9(@types/node@25.0.8)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.45.0): + vitest@2.1.9(@types/node@25.0.8)(jsdom@26.1.0)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.8)(typescript@5.9.3))(terser@5.45.0): dependencies: '@vitest/expect': 2.1.9 - '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@25.0.8)(lightningcss@1.30.2)(terser@5.45.0)) + '@vitest/mocker': 2.1.9(msw@2.12.7(@types/node@25.0.8)(typescript@5.9.3))(vite@5.4.21(@types/node@25.0.8)(lightningcss@1.30.2)(terser@5.45.0)) '@vitest/pretty-format': 2.1.9 '@vitest/runner': 2.1.9 '@vitest/snapshot': 2.1.9 @@ -21772,10 +22122,10 @@ snapshots: - supports-color - terser - vitest@4.0.17(@opentelemetry/api@1.9.0)(@types/node@20.19.29)(@vitest/ui@4.0.17)(jiti@1.21.7)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2): + vitest@4.0.17(@opentelemetry/api@1.9.0)(@types/node@20.19.29)(@vitest/ui@4.0.17)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(msw@2.12.7(@types/node@20.19.29)(typescript@5.9.3))(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.17 - '@vitest/mocker': 4.0.17(vite@6.4.1(@types/node@20.19.29)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 4.0.17(msw@2.12.7(@types/node@20.19.29)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 4.0.17 '@vitest/runner': 4.0.17 '@vitest/snapshot': 4.0.17 @@ -21792,7 +22142,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 6.4.1(@types/node@20.19.29)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 6.4.1(@types/node@20.19.29)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 @@ -21812,10 +22162,10 @@ snapshots: - tsx - yaml - vitest@4.0.17(@opentelemetry/api@1.9.0)(@types/node@25.0.8)(@vitest/ui@4.0.17)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2): + vitest@4.0.17(@opentelemetry/api@1.9.0)(@types/node@25.0.8)(@vitest/ui@4.0.17)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.8)(typescript@5.9.3))(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.17 - '@vitest/mocker': 4.0.17(vite@6.4.1(@types/node@25.0.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 4.0.17(msw@2.12.7(@types/node@25.0.8)(typescript@5.9.3))(vite@6.4.1(@types/node@25.0.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.45.0)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 4.0.17 '@vitest/runner': 4.0.17 '@vitest/snapshot': 4.0.17