diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json new file mode 100644 index 0000000..a1b9f10 --- /dev/null +++ b/packages/mcp-server/package.json @@ -0,0 +1,58 @@ +{ + "name": "@supaku/agentfactory-mcp-server", + "version": "0.7.52", + "type": "module", + "description": "MCP server exposing AgentFactory fleet capabilities to MCP-aware clients", + "author": "Supaku (https://supaku.com)", + "license": "MIT", + "engines": { "node": ">=22.0.0" }, + "repository": { + "type": "git", + "url": "https://github.com/supaku/agentfactory", + "directory": "packages/mcp-server" + }, + "publishConfig": { + "access": "public", + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js", + "default": "./dist/src/index.js" + } + }, + "main": "./dist/src/index.js", + "types": "./dist/src/index.d.ts" + }, + "main": "./dist/src/index.js", + "module": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./dist/src/index.js", + "default": "./dist/src/index.js" + } + }, + "bin": { + "af-mcp-server": "./dist/src/cli.js" + }, + "files": ["dist", "README.md", "LICENSE"], + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.1", + "@supaku/agentfactory-server": "workspace:*", + "zod": "^4.3.6" + }, + "devDependencies": { + "@types/node": "^22.5.4", + "typescript": "^5.7.3", + "vitest": "^3.2.3" + }, + "scripts": { + "build": "tsc", + "typecheck": "tsc --noEmit", + "test": "vitest run --passWithNoTests", + "test:watch": "vitest", + "clean": "rm -rf dist", + "prepublishOnly": "pnpm clean && pnpm build" + } +} diff --git a/packages/mcp-server/src/auth.ts b/packages/mcp-server/src/auth.ts new file mode 100644 index 0000000..15014d5 --- /dev/null +++ b/packages/mcp-server/src/auth.ts @@ -0,0 +1,32 @@ +import { extractBearerToken, verifyApiKey, isWorkerAuthConfigured } from '@supaku/agentfactory-server' + +const MCP_API_KEY_ENV = 'MCP_API_KEY' + +export function isMcpAuthConfigured(): boolean { + return isWorkerAuthConfigured(MCP_API_KEY_ENV) || isWorkerAuthConfigured('WORKER_API_KEY') +} + +export function verifyMcpAuth(authHeader: string | null | undefined): { authorized: boolean; error?: string } { + if (!isMcpAuthConfigured()) { + // No auth configured — allow all requests (dev mode) + return { authorized: true } + } + + const token = extractBearerToken(authHeader) + if (!token) { + return { authorized: false, error: 'Missing or invalid Authorization header. Expected: Bearer ' } + } + + // Try MCP-specific key first, then fall back to worker key + const mcpKey = process.env[MCP_API_KEY_ENV] + const workerKey = process.env.WORKER_API_KEY + + if (mcpKey && verifyApiKey(token, mcpKey)) { + return { authorized: true } + } + if (workerKey && verifyApiKey(token, workerKey)) { + return { authorized: true } + } + + return { authorized: false, error: 'Invalid API key' } +} diff --git a/packages/mcp-server/src/cli.ts b/packages/mcp-server/src/cli.ts new file mode 100644 index 0000000..09c6b0c --- /dev/null +++ b/packages/mcp-server/src/cli.ts @@ -0,0 +1,44 @@ +#!/usr/bin/env node + +import { createFleetMcpServer } from './server.js' +import { startStdioTransport, startHttpTransport } from './transport.js' +import { stopResourceNotificationPoller } from './resources.js' + +const args = process.argv.slice(2) +const transportType = args.includes('--stdio') ? 'stdio' : 'http' + +const server = createFleetMcpServer() + +if (transportType === 'stdio') { + console.error('[mcp-server] Starting in STDIO mode') + await startStdioTransport(server) + + // Graceful shutdown for STDIO mode + const shutdown = () => { + console.error('[mcp-server] Shutting down...') + stopResourceNotificationPoller() + process.exit(0) + } + process.on('SIGINT', shutdown) + process.on('SIGTERM', shutdown) +} else { + const portIdx = args.indexOf('--port') + const port = portIdx !== -1 ? parseInt(args[portIdx + 1], 10) : undefined + const hostIdx = args.indexOf('--host') + const host = hostIdx !== -1 ? args[hostIdx + 1] : undefined + + const httpServer = await startHttpTransport(server, { port, host }) + + // Graceful shutdown for HTTP mode — close server before exiting + const shutdown = () => { + console.log('[mcp-server] Shutting down...') + stopResourceNotificationPoller() + httpServer.close(() => { + process.exit(0) + }) + // Force exit after 5s if connections don't drain + setTimeout(() => process.exit(0), 5000).unref() + } + process.on('SIGINT', shutdown) + process.on('SIGTERM', shutdown) +} diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts new file mode 100644 index 0000000..9d48bcb --- /dev/null +++ b/packages/mcp-server/src/index.ts @@ -0,0 +1,5 @@ +export { createFleetMcpServer } from './server.js' +export { registerFleetTools } from './tools.js' +export { registerFleetResources, stopResourceNotificationPoller } from './resources.js' +export { startStdioTransport, startHttpTransport, type HttpTransportOptions } from './transport.js' +export { verifyMcpAuth, isMcpAuthConfigured } from './auth.js' diff --git a/packages/mcp-server/src/resources.ts b/packages/mcp-server/src/resources.ts new file mode 100644 index 0000000..9a3587a --- /dev/null +++ b/packages/mcp-server/src/resources.ts @@ -0,0 +1,190 @@ +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js' +import { + getAllSessions, + getSessionState, + getSessionStateByIssue, + listWorkers, + getTotalCapacity, +} from '@supaku/agentfactory-server' + +/** + * Registers MCP resources that expose AgentFactory fleet state to MCP-aware clients. + * + * Resources: + * - fleet://agents — Current fleet state (agents, statuses, costs) + * - fleet://issues/{id} — Issue details with agent progress + * - fleet://logs/{id} — Agent activity logs / session info + * + * Also starts a polling loop that emits resource-update notifications + * when fleet state changes, enabling MCP clients to subscribe to updates. + */ +export function registerFleetResources(server: McpServer): void { + // 1. fleet://agents — Current fleet state + server.resource( + 'fleet-agents', + 'fleet://agents', + { description: 'Current fleet state including all agent sessions, workers, and capacity' }, + async (uri) => { + const [sessions, workers, capacity] = await Promise.all([ + getAllSessions(), + listWorkers(), + getTotalCapacity(), + ]) + + const data = { sessions, workers, capacity } + + return { + contents: [ + { + uri: uri.href, + text: JSON.stringify(data, null, 2), + mimeType: 'application/json', + }, + ], + } + }, + ) + + // 2. fleet://issues/{id} — Issue details with agent progress + server.resource( + 'fleet-issue', + new ResourceTemplate('fleet://issues/{id}', { list: undefined }), + { description: 'Issue details with agent session progress' }, + async (uri, variables) => { + const id = Array.isArray(variables.id) ? variables.id[0] : variables.id + const session = await getSessionStateByIssue(id) + + const data = session + ? session + : { error: 'not_found', message: `No agent session found for issue: ${id}` } + + return { + contents: [ + { + uri: uri.href, + text: JSON.stringify(data, null, 2), + mimeType: 'application/json', + }, + ], + } + }, + ) + + // 3. fleet://logs/{id} — Agent activity logs + server.resource( + 'fleet-logs', + new ResourceTemplate('fleet://logs/{id}', { list: undefined }), + { description: 'Agent activity logs and session information' }, + async (uri, variables) => { + const id = Array.isArray(variables.id) ? variables.id[0] : variables.id + + // Try to find the session by linearSessionId first, then fall back to issue ID + let session = await getSessionState(id) + if (!session) { + session = await getSessionStateByIssue(id) + } + + if (!session) { + const data = { error: 'not_found', message: `No agent session found for id: ${id}` } + return { + contents: [ + { + uri: uri.href, + text: JSON.stringify(data, null, 2), + mimeType: 'application/json', + }, + ], + } + } + + const data: Record = { + ...session, + _logHint: session.worktreePath + ? `Activity logs may be available on disk at: ${session.worktreePath}/.agent/state.json` + : 'No worktree path available for this session', + } + + return { + contents: [ + { + uri: uri.href, + text: JSON.stringify(data, null, 2), + mimeType: 'application/json', + }, + ], + } + }, + ) + + // ─── Resource update notifications ──────────────────────────────────── + // Poll fleet state and emit MCP resource-update notifications when changes + // are detected, so subscribed clients automatically refresh. + startResourceNotificationPoller(server) +} + +// ─── Polling-based resource notifications ───────────────────────────────── + +const POLL_INTERVAL_MS = 5_000 + +/** Lightweight snapshot of fleet state for change detection */ +interface FleetSnapshot { + sessionCount: number + /** Sorted comma-separated status summary, e.g. "running:3,pending:2" */ + statusSummary: string +} + +let pollTimer: ReturnType | null = null + +function buildSnapshot(sessions: { status: string }[]): FleetSnapshot { + const counts = new Map() + for (const s of sessions) { + counts.set(s.status, (counts.get(s.status) ?? 0) + 1) + } + const statusSummary = [...counts.entries()] + .sort(([a], [b]) => a.localeCompare(b)) + .map(([k, v]) => `${k}:${v}`) + .join(',') + return { sessionCount: sessions.length, statusSummary } +} + +function snapshotsEqual(a: FleetSnapshot | null, b: FleetSnapshot): boolean { + if (!a) return false + return a.sessionCount === b.sessionCount && a.statusSummary === b.statusSummary +} + +function startResourceNotificationPoller(server: McpServer): void { + let lastSnapshot: FleetSnapshot | null = null + + pollTimer = setInterval(async () => { + try { + const sessions = await getAllSessions() + const snapshot = buildSnapshot(sessions) + + if (!snapshotsEqual(lastSnapshot, snapshot)) { + lastSnapshot = snapshot + // Notify subscribed clients that fleet://agents has changed + try { + await server.server.sendResourceUpdated({ uri: 'fleet://agents' }) + } catch { + // Client may not be subscribed — safe to ignore + } + } + } catch { + // Redis may be unavailable — skip this tick + } + }, POLL_INTERVAL_MS) + + // Ensure the timer doesn't prevent process exit + if (pollTimer && typeof pollTimer === 'object' && 'unref' in pollTimer) { + pollTimer.unref() + } +} + +/** Stop the resource notification poller (for graceful shutdown) */ +export function stopResourceNotificationPoller(): void { + if (pollTimer) { + clearInterval(pollTimer) + pollTimer = null + } +} diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts new file mode 100644 index 0000000..2a72c05 --- /dev/null +++ b/packages/mcp-server/src/server.ts @@ -0,0 +1,15 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { registerFleetResources } from './resources.js' +import { registerFleetTools } from './tools.js' + +export function createFleetMcpServer(): McpServer { + const server = new McpServer({ + name: 'agentfactory-fleet', + version: '0.7.52', + }) + + registerFleetTools(server) + registerFleetResources(server) + + return server +} diff --git a/packages/mcp-server/src/tools.test.ts b/packages/mcp-server/src/tools.test.ts new file mode 100644 index 0000000..33bf608 --- /dev/null +++ b/packages/mcp-server/src/tools.test.ts @@ -0,0 +1,432 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +// ── Mock @supaku/agentfactory-server ──────────────────────────────────────── +vi.mock('@supaku/agentfactory-server', () => ({ + getAllSessions: vi.fn(), + getSessionsByStatus: vi.fn(), + getSessionState: vi.fn(), + getSessionStateByIssue: vi.fn(), + storeSessionState: vi.fn(), + updateSessionStatus: vi.fn(), + storePendingPrompt: vi.fn(), +})) + +// ── Mock @modelcontextprotocol/sdk ────────────────────────────────────────── +vi.mock('@modelcontextprotocol/sdk/server/mcp.js', () => ({ + McpServer: vi.fn(), +})) + +import { + getAllSessions, + getSessionsByStatus, + getSessionState, + getSessionStateByIssue, + storeSessionState, + updateSessionStatus, + storePendingPrompt, +} from '@supaku/agentfactory-server' +import { registerFleetTools } from './tools.js' + +// ── Helpers ───────────────────────────────────────────────────────────────── + +type ToolHandler = (args: Record) => Promise<{ + content: { type: string; text: string }[] + isError?: boolean +}> + +/** Capture tool registrations from registerFleetTools */ +function captureTools(): Map { + const tools = new Map() + const fakeMcpServer = { + tool: (name: string, _description: string, _schema: unknown, handler: ToolHandler) => { + tools.set(name, handler) + }, + } + registerFleetTools(fakeMcpServer as never) + return tools +} + +function makeSession(overrides: Record = {}) { + return { + linearSessionId: 'ses-123', + issueId: 'issue-abc', + issueIdentifier: 'SUP-100', + providerSessionId: null, + worktreePath: '/tmp/wt', + status: 'running', + priority: 3, + workType: 'development', + queuedAt: 1000, + totalCostUsd: 0.5, + inputTokens: 1000, + outputTokens: 500, + ...overrides, + } +} + +function parseResult(result: { content: { type: string; text: string }[] }) { + return JSON.parse(result.content[0].text) +} + +// ── Tests ─────────────────────────────────────────────────────────────────── + +let tools: Map + +beforeEach(() => { + vi.resetAllMocks() + tools = captureTools() +}) + +describe('registerFleetTools', () => { + it('registers all 6 expected tools', () => { + expect(tools.size).toBe(6) + expect([...tools.keys()].sort()).toEqual([ + 'forward-prompt', + 'get-cost-report', + 'get-task-status', + 'list-fleet', + 'stop-agent', + 'submit-task', + ]) + }) +}) + +describe('submit-task', () => { + it('creates a pending session and returns task info', async () => { + const handler = tools.get('submit-task')! + vi.mocked(storeSessionState).mockResolvedValue({ + linearSessionId: 'mcp-123-issue-1', + issueId: 'issue-1', + status: 'pending', + priority: 2, + workType: 'research', + } as never) + + const result = await handler({ issueId: 'issue-1', workType: 'research', priority: 2 }) + const data = parseResult(result) + + expect(data.submitted).toBe(true) + expect(data.issueId).toBe('issue-1') + expect(data.status).toBe('pending') + expect(data.workType).toBe('research') + expect(data.priority).toBe(2) + expect(storeSessionState).toHaveBeenCalledOnce() + }) + + it('defaults to development work type and priority 3', async () => { + const handler = tools.get('submit-task')! + vi.mocked(storeSessionState).mockResolvedValue({ + linearSessionId: 'mcp-123-issue-2', + issueId: 'issue-2', + status: 'pending', + priority: 3, + workType: 'development', + } as never) + + await handler({ issueId: 'issue-2' }) + + const callArgs = vi.mocked(storeSessionState).mock.calls[0][1] as Record + expect(callArgs.workType).toBe('development') + expect(callArgs.priority).toBe(3) + }) + + it('returns error when storeSessionState throws', async () => { + const handler = tools.get('submit-task')! + vi.mocked(storeSessionState).mockRejectedValue(new Error('Redis down')) + + const result = await handler({ issueId: 'issue-1' }) + + expect(result.isError).toBe(true) + expect(result.content[0].text).toContain('Redis down') + }) +}) + +describe('get-task-status', () => { + it('returns session when found by session ID', async () => { + const handler = tools.get('get-task-status')! + const session = makeSession() + vi.mocked(getSessionState).mockResolvedValue(session as never) + + const result = await handler({ taskId: 'ses-123' }) + const data = parseResult(result) + + expect(data.linearSessionId).toBe('ses-123') + expect(result.isError).toBeUndefined() + }) + + it('falls back to issue-based lookup', async () => { + const handler = tools.get('get-task-status')! + vi.mocked(getSessionState).mockResolvedValue(null as never) + vi.mocked(getSessionStateByIssue).mockResolvedValue(makeSession() as never) + + const result = await handler({ taskId: 'issue-abc' }) + + expect(getSessionStateByIssue).toHaveBeenCalledWith('issue-abc') + expect(result.isError).toBeUndefined() + }) + + it('returns error when task not found', async () => { + const handler = tools.get('get-task-status')! + vi.mocked(getSessionState).mockResolvedValue(null as never) + vi.mocked(getSessionStateByIssue).mockResolvedValue(null as never) + + const result = await handler({ taskId: 'unknown' }) + + expect(result.isError).toBe(true) + expect(result.content[0].text).toContain('No task found') + }) +}) + +describe('list-fleet', () => { + it('returns all sessions when no filter', async () => { + const handler = tools.get('list-fleet')! + const sessions = [makeSession(), makeSession({ linearSessionId: 'ses-456' })] + vi.mocked(getAllSessions).mockResolvedValue(sessions as never) + + const result = await handler({}) + const data = parseResult(result) + + expect(data.total).toBe(2) + expect(data.returned).toBe(2) + expect(getAllSessions).toHaveBeenCalledOnce() + }) + + it('filters by status', async () => { + const handler = tools.get('list-fleet')! + vi.mocked(getSessionsByStatus).mockResolvedValue([makeSession()] as never) + + const result = await handler({ status: ['running'] }) + const data = parseResult(result) + + expect(getSessionsByStatus).toHaveBeenCalledWith(['running']) + expect(data.total).toBe(1) + }) + + it('respects limit parameter', async () => { + const handler = tools.get('list-fleet')! + const sessions = Array.from({ length: 30 }, (_, i) => + makeSession({ linearSessionId: `ses-${i}` }), + ) + vi.mocked(getAllSessions).mockResolvedValue(sessions as never) + + const result = await handler({ limit: 5 }) + const data = parseResult(result) + + expect(data.total).toBe(30) + expect(data.returned).toBe(5) + }) + + it('defaults limit to 20', async () => { + const handler = tools.get('list-fleet')! + const sessions = Array.from({ length: 25 }, (_, i) => + makeSession({ linearSessionId: `ses-${i}` }), + ) + vi.mocked(getAllSessions).mockResolvedValue(sessions as never) + + const result = await handler({}) + const data = parseResult(result) + + expect(data.returned).toBe(20) + }) +}) + +describe('get-cost-report', () => { + it('returns single-task cost when taskId provided', async () => { + const handler = tools.get('get-cost-report')! + vi.mocked(getSessionState).mockResolvedValue( + makeSession({ totalCostUsd: 1.23, inputTokens: 5000, outputTokens: 2000 }) as never, + ) + + const result = await handler({ taskId: 'ses-123' }) + const data = parseResult(result) + + expect(data.totalCostUsd).toBe(1.23) + expect(data.inputTokens).toBe(5000) + expect(data.outputTokens).toBe(2000) + }) + + it('returns fleet-wide cost when no taskId', async () => { + const handler = tools.get('get-cost-report')! + vi.mocked(getAllSessions).mockResolvedValue([ + makeSession({ totalCostUsd: 1.0, inputTokens: 1000, outputTokens: 500 }), + makeSession({ totalCostUsd: 2.0, inputTokens: 2000, outputTokens: 1000 }), + ] as never) + + const result = await handler({}) + const data = parseResult(result) + + expect(data.totalSessions).toBe(2) + expect(data.totalCostUsd).toBe(3.0) + expect(data.totalInputTokens).toBe(3000) + expect(data.totalOutputTokens).toBe(1500) + }) + + it('returns error when single task not found', async () => { + const handler = tools.get('get-cost-report')! + vi.mocked(getSessionState).mockResolvedValue(null as never) + vi.mocked(getSessionStateByIssue).mockResolvedValue(null as never) + + const result = await handler({ taskId: 'unknown' }) + + expect(result.isError).toBe(true) + }) +}) + +describe('stop-agent', () => { + it('stops a running task', async () => { + const handler = tools.get('stop-agent')! + vi.mocked(getSessionState).mockResolvedValue(makeSession({ status: 'running' }) as never) + vi.mocked(updateSessionStatus).mockResolvedValue(true as never) + + const result = await handler({ taskId: 'ses-123' }) + const data = parseResult(result) + + expect(data.stopped).toBe(true) + expect(data.previousStatus).toBe('running') + expect(data.newStatus).toBe('stopped') + expect(updateSessionStatus).toHaveBeenCalledWith('ses-123', 'stopped') + }) + + it('stops a pending task', async () => { + const handler = tools.get('stop-agent')! + vi.mocked(getSessionState).mockResolvedValue(makeSession({ status: 'pending' }) as never) + vi.mocked(updateSessionStatus).mockResolvedValue(true as never) + + const result = await handler({ taskId: 'ses-123' }) + const data = parseResult(result) + + expect(data.stopped).toBe(true) + expect(data.previousStatus).toBe('pending') + }) + + it('rejects stopping a completed task', async () => { + const handler = tools.get('stop-agent')! + vi.mocked(getSessionState).mockResolvedValue(makeSession({ status: 'completed' }) as never) + + const result = await handler({ taskId: 'ses-123' }) + + expect(result.isError).toBe(true) + expect(result.content[0].text).toContain('cannot be stopped') + }) + + it('returns error when task not found', async () => { + const handler = tools.get('stop-agent')! + vi.mocked(getSessionState).mockResolvedValue(null as never) + vi.mocked(getSessionStateByIssue).mockResolvedValue(null as never) + + const result = await handler({ taskId: 'unknown' }) + + expect(result.isError).toBe(true) + expect(result.content[0].text).toContain('No task found') + }) + + it('returns error when Redis update fails', async () => { + const handler = tools.get('stop-agent')! + vi.mocked(getSessionState).mockResolvedValue(makeSession({ status: 'running' }) as never) + vi.mocked(updateSessionStatus).mockResolvedValue(false as never) + + const result = await handler({ taskId: 'ses-123' }) + + expect(result.isError).toBe(true) + expect(result.content[0].text).toContain('Failed to update') + }) +}) + +describe('forward-prompt', () => { + it('forwards a prompt to a running session', async () => { + const handler = tools.get('forward-prompt')! + vi.mocked(getSessionState).mockResolvedValue(makeSession({ status: 'running' }) as never) + vi.mocked(storePendingPrompt).mockResolvedValue({ + id: 'prm_123', + sessionId: 'ses-123', + issueId: 'issue-abc', + prompt: 'Please also fix the tests', + createdAt: Date.now(), + } as never) + + const result = await handler({ taskId: 'ses-123', message: 'Please also fix the tests' }) + const data = parseResult(result) + + expect(data.forwarded).toBe(true) + expect(data.promptId).toBe('prm_123') + expect(data.taskId).toBe('ses-123') + expect(storePendingPrompt).toHaveBeenCalledWith('ses-123', 'issue-abc', 'Please also fix the tests') + }) + + it('forwards a prompt to a claimed session', async () => { + const handler = tools.get('forward-prompt')! + vi.mocked(getSessionState).mockResolvedValue(makeSession({ status: 'claimed' }) as never) + vi.mocked(storePendingPrompt).mockResolvedValue({ + id: 'prm_456', + sessionId: 'ses-123', + issueId: 'issue-abc', + prompt: 'extra context', + createdAt: Date.now(), + } as never) + + const result = await handler({ taskId: 'ses-123', message: 'extra context' }) + const data = parseResult(result) + + expect(data.forwarded).toBe(true) + expect(data.sessionStatus).toBe('claimed') + }) + + it('falls back to issue-based lookup', async () => { + const handler = tools.get('forward-prompt')! + vi.mocked(getSessionState).mockResolvedValue(null as never) + vi.mocked(getSessionStateByIssue).mockResolvedValue(makeSession() as never) + vi.mocked(storePendingPrompt).mockResolvedValue({ + id: 'prm_789', + sessionId: 'ses-123', + issueId: 'issue-abc', + prompt: 'msg', + createdAt: Date.now(), + } as never) + + const result = await handler({ taskId: 'issue-abc', message: 'msg' }) + + expect(getSessionStateByIssue).toHaveBeenCalledWith('issue-abc') + expect(result.isError).toBeUndefined() + }) + + it('rejects forwarding to a completed session', async () => { + const handler = tools.get('forward-prompt')! + vi.mocked(getSessionState).mockResolvedValue(makeSession({ status: 'completed' }) as never) + + const result = await handler({ taskId: 'ses-123', message: 'hello' }) + + expect(result.isError).toBe(true) + expect(result.content[0].text).toContain('Prompts can only be forwarded to running or claimed') + }) + + it('rejects forwarding to a stopped session', async () => { + const handler = tools.get('forward-prompt')! + vi.mocked(getSessionState).mockResolvedValue(makeSession({ status: 'stopped' }) as never) + + const result = await handler({ taskId: 'ses-123', message: 'hello' }) + + expect(result.isError).toBe(true) + }) + + it('returns error when task not found', async () => { + const handler = tools.get('forward-prompt')! + vi.mocked(getSessionState).mockResolvedValue(null as never) + vi.mocked(getSessionStateByIssue).mockResolvedValue(null as never) + + const result = await handler({ taskId: 'unknown', message: 'hello' }) + + expect(result.isError).toBe(true) + expect(result.content[0].text).toContain('No task found') + }) + + it('returns error when storePendingPrompt fails', async () => { + const handler = tools.get('forward-prompt')! + vi.mocked(getSessionState).mockResolvedValue(makeSession({ status: 'running' }) as never) + vi.mocked(storePendingPrompt).mockResolvedValue(null as never) + + const result = await handler({ taskId: 'ses-123', message: 'hello' }) + + expect(result.isError).toBe(true) + expect(result.content[0].text).toContain('Failed to store pending prompt') + }) +}) diff --git a/packages/mcp-server/src/tools.ts b/packages/mcp-server/src/tools.ts new file mode 100644 index 0000000..2892748 --- /dev/null +++ b/packages/mcp-server/src/tools.ts @@ -0,0 +1,423 @@ +import { z } from 'zod' +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { + getAllSessions, + getSessionsByStatus, + getSessionState, + getSessionStateByIssue, + storeSessionState, + updateSessionStatus, + storePendingPrompt, +} from '@supaku/agentfactory-server' +import type { AgentSessionStatus } from '@supaku/agentfactory-server' + +// Work type values for zod enum validation +const WORK_TYPES = [ + 'research', + 'backlog-creation', + 'development', + 'inflight', + 'qa', + 'acceptance', + 'refinement', + 'coordination', + 'qa-coordination', + 'acceptance-coordination', +] as const + +// Session status values for zod enum validation +const SESSION_STATUSES = [ + 'pending', + 'claimed', + 'running', + 'finalizing', + 'completed', + 'failed', + 'stopped', +] as const + +/** + * Register all fleet management tools on the MCP server. + * + * Tools registered: + * - submit-task: Submit a development task to the fleet work queue + * - get-task-status: Get current status of a task by session or issue ID + * - list-fleet: List agents and their statuses with optional filtering + * - get-cost-report: Get cost/token usage for a task or the entire fleet + * - stop-agent: Request to stop a running agent + * - forward-prompt: Forward a follow-up prompt to a running agent session + */ +export function registerFleetTools(server: McpServer): void { + // ─── submit-task ─────────────────────────────────────────────────── + server.tool( + 'submit-task', + 'Submit a development task to the fleet work queue. Creates a pending session that a worker will pick up.', + { + issueId: z.string().describe('Linear issue ID to work on'), + description: z.string().optional().describe('Optional description or prompt context for the task'), + workType: z.enum(WORK_TYPES).optional().describe('Type of work to perform (defaults to development)'), + priority: z.number().min(1).max(5).optional().describe('Priority 1-5 where 1 is highest (defaults to 3)'), + }, + async (args) => { + try { + const linearSessionId = `mcp-${Date.now()}-${args.issueId}` + const session = await storeSessionState(linearSessionId, { + issueId: args.issueId, + providerSessionId: null, + worktreePath: '', + status: 'pending', + priority: args.priority ?? 3, + promptContext: args.description, + workType: args.workType ?? 'development', + queuedAt: Math.floor(Date.now() / 1000), + }) + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + submitted: true, + taskId: session.linearSessionId, + issueId: session.issueId, + status: session.status, + priority: session.priority, + workType: session.workType, + }, + null, + 2, + ), + }, + ], + } + } catch (error) { + return { + content: [{ type: 'text' as const, text: `Error: ${error instanceof Error ? error.message : String(error)}` }], + isError: true, + } + } + }, + ) + + // ─── get-task-status ─────────────────────────────────────────────── + server.tool( + 'get-task-status', + 'Get the current status of a task. Accepts either a session ID (taskId) or a Linear issue ID.', + { + taskId: z.string().describe('Session ID or Linear issue ID to look up'), + }, + async (args) => { + try { + // Try direct session lookup first + let session = await getSessionState(args.taskId) + + // Fall back to issue-based lookup + if (!session) { + session = await getSessionStateByIssue(args.taskId) + } + + if (!session) { + return { + content: [{ type: 'text' as const, text: `Error: No task found for ID "${args.taskId}"` }], + isError: true, + } + } + + return { + content: [{ type: 'text' as const, text: JSON.stringify(session, null, 2) }], + } + } catch (error) { + return { + content: [{ type: 'text' as const, text: `Error: ${error instanceof Error ? error.message : String(error)}` }], + isError: true, + } + } + }, + ) + + // ─── list-fleet ──────────────────────────────────────────────────── + server.tool( + 'list-fleet', + 'List agents in the fleet with optional status filtering and result limiting.', + { + status: z.array(z.enum(SESSION_STATUSES)).optional().describe('Filter by one or more statuses'), + limit: z.number().min(1).optional().describe('Maximum number of results to return (defaults to 20)'), + }, + async (args) => { + try { + const limit = args.limit ?? 20 + + let sessions + if (args.status && args.status.length > 0) { + sessions = await getSessionsByStatus(args.status as AgentSessionStatus[]) + } else { + sessions = await getAllSessions() + } + + const limited = sessions.slice(0, limit) + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + total: sessions.length, + returned: limited.length, + sessions: limited, + }, + null, + 2, + ), + }, + ], + } + } catch (error) { + return { + content: [{ type: 'text' as const, text: `Error: ${error instanceof Error ? error.message : String(error)}` }], + isError: true, + } + } + }, + ) + + // ─── get-cost-report ─────────────────────────────────────────────── + server.tool( + 'get-cost-report', + 'Get cost and token usage report. If a taskId is provided, returns cost for that specific task. Otherwise, returns aggregate fleet costs.', + { + taskId: z.string().optional().describe('Session ID or issue ID for a specific task (omit for fleet-wide report)'), + }, + async (args) => { + try { + if (args.taskId) { + // Single-task cost report + let session = await getSessionState(args.taskId) + if (!session) { + session = await getSessionStateByIssue(args.taskId) + } + + if (!session) { + return { + content: [{ type: 'text' as const, text: `Error: No task found for ID "${args.taskId}"` }], + isError: true, + } + } + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + taskId: session.linearSessionId, + issueId: session.issueId, + issueIdentifier: session.issueIdentifier, + status: session.status, + totalCostUsd: session.totalCostUsd ?? 0, + inputTokens: session.inputTokens ?? 0, + outputTokens: session.outputTokens ?? 0, + }, + null, + 2, + ), + }, + ], + } + } + + // Fleet-wide cost report + const sessions = await getAllSessions() + let totalCostUsd = 0 + let totalInputTokens = 0 + let totalOutputTokens = 0 + let sessionsWithCost = 0 + + for (const session of sessions) { + if (session.totalCostUsd != null) { + totalCostUsd += session.totalCostUsd + sessionsWithCost++ + } + if (session.inputTokens != null) { + totalInputTokens += session.inputTokens + } + if (session.outputTokens != null) { + totalOutputTokens += session.outputTokens + } + } + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + totalSessions: sessions.length, + sessionsWithCostData: sessionsWithCost, + totalCostUsd, + totalInputTokens, + totalOutputTokens, + }, + null, + 2, + ), + }, + ], + } + } catch (error) { + return { + content: [{ type: 'text' as const, text: `Error: ${error instanceof Error ? error.message : String(error)}` }], + isError: true, + } + } + }, + ) + + // ─── forward-prompt ──────────────────────────────────────────────── + server.tool( + 'forward-prompt', + 'Forward a follow-up prompt to a running agent session. The prompt is queued and delivered to the agent via its message injection mechanism.', + { + taskId: z.string().describe('Session ID or Linear issue ID of the running agent'), + message: z.string().describe('The follow-up prompt or message to send to the agent'), + }, + async (args) => { + try { + // Resolve the session — try direct lookup, then issue-based + let session = await getSessionState(args.taskId) + if (!session) { + session = await getSessionStateByIssue(args.taskId) + } + + if (!session) { + return { + content: [{ type: 'text' as const, text: `Error: No task found for ID "${args.taskId}"` }], + isError: true, + } + } + + // Only allow forwarding to active sessions + const forwardableStatuses: AgentSessionStatus[] = ['running', 'claimed'] + if (!forwardableStatuses.includes(session.status)) { + return { + content: [ + { + type: 'text' as const, + text: `Error: Task "${session.linearSessionId}" is in status "${session.status}". Prompts can only be forwarded to running or claimed sessions.`, + }, + ], + isError: true, + } + } + + const pending = await storePendingPrompt( + session.linearSessionId, + session.issueId, + args.message, + ) + + if (!pending) { + return { + content: [{ type: 'text' as const, text: `Error: Failed to store pending prompt. Redis may not be configured.` }], + isError: true, + } + } + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + forwarded: true, + promptId: pending.id, + taskId: session.linearSessionId, + issueId: session.issueId, + sessionStatus: session.status, + }, + null, + 2, + ), + }, + ], + } + } catch (error) { + return { + content: [{ type: 'text' as const, text: `Error: ${error instanceof Error ? error.message : String(error)}` }], + isError: true, + } + } + }, + ) + + // ─── stop-agent ──────────────────────────────────────────────────── + server.tool( + 'stop-agent', + 'Request to stop a running agent. Updates the session status to stopped.', + { + taskId: z.string().describe('Session ID of the task to stop'), + }, + async (args) => { + try { + // Verify the session exists and is in a stoppable state + let session = await getSessionState(args.taskId) + if (!session) { + session = await getSessionStateByIssue(args.taskId) + } + + if (!session) { + return { + content: [{ type: 'text' as const, text: `Error: No task found for ID "${args.taskId}"` }], + isError: true, + } + } + + const stoppableStatuses: AgentSessionStatus[] = ['pending', 'claimed', 'running'] + if (!stoppableStatuses.includes(session.status)) { + return { + content: [ + { + type: 'text' as const, + text: `Error: Task "${session.linearSessionId}" is in status "${session.status}" and cannot be stopped. Only pending, claimed, or running tasks can be stopped.`, + }, + ], + isError: true, + } + } + + const updated = await updateSessionStatus(session.linearSessionId, 'stopped') + + if (!updated) { + return { + content: [{ type: 'text' as const, text: `Error: Failed to update task status. Redis may not be configured.` }], + isError: true, + } + } + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + stopped: true, + taskId: session.linearSessionId, + issueId: session.issueId, + previousStatus: session.status, + newStatus: 'stopped', + }, + null, + 2, + ), + }, + ], + } + } catch (error) { + return { + content: [{ type: 'text' as const, text: `Error: ${error instanceof Error ? error.message : String(error)}` }], + isError: true, + } + } + }, + ) +} diff --git a/packages/mcp-server/src/transport.ts b/packages/mcp-server/src/transport.ts new file mode 100644 index 0000000..4673342 --- /dev/null +++ b/packages/mcp-server/src/transport.ts @@ -0,0 +1,62 @@ +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js' +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import http from 'node:http' +import { verifyMcpAuth, isMcpAuthConfigured } from './auth.js' + +export interface HttpTransportOptions { + port?: number + host?: string +} + +export async function startStdioTransport(server: McpServer): Promise { + const transport = new StdioServerTransport() + await server.connect(transport) +} + +export async function startHttpTransport( + server: McpServer, + options: HttpTransportOptions = {} +): Promise { + const port = options.port ?? parseInt(process.env.MCP_PORT ?? '3100', 10) + const host = options.host ?? process.env.MCP_HOST ?? '0.0.0.0' + + const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => crypto.randomUUID() }) + + const httpServer = http.createServer(async (req, res) => { + // Health check endpoint + if (req.url === '/health' && req.method === 'GET') { + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ status: 'ok', server: 'agentfactory-fleet', auth: isMcpAuthConfigured() })) + return + } + + // MCP endpoint — verify auth for non-health endpoints + if (req.url === '/mcp') { + const authResult = verifyMcpAuth(req.headers.authorization) + if (!authResult.authorized) { + res.writeHead(401, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ error: authResult.error })) + return + } + + await transport.handleRequest(req, res) + return + } + + res.writeHead(404, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ error: 'Not found. Use /mcp for MCP protocol or /health for health check.' })) + }) + + await server.connect(transport) + + return new Promise((resolve) => { + httpServer.listen(port, host, () => { + console.log(`[mcp-server] AgentFactory Fleet MCP server listening on http://${host}:${port}`) + console.log(`[mcp-server] MCP endpoint: http://${host}:${port}/mcp`) + console.log(`[mcp-server] Health check: http://${host}:${port}/health`) + console.log(`[mcp-server] Auth: ${isMcpAuthConfigured() ? 'enabled' : 'disabled (dev mode)'}`) + resolve(httpServer) + }) + }) +} diff --git a/packages/mcp-server/tsconfig.json b/packages/mcp-server/tsconfig.json new file mode 100644 index 0000000..ea03de7 --- /dev/null +++ b/packages/mcp-server/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "dist", + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03721cd..f3040b9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -172,6 +172,28 @@ importers: specifier: ^3.2.3 version: 3.2.4(@types/node@22.19.10)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) + packages/mcp-server: + dependencies: + '@modelcontextprotocol/sdk': + specifier: ^1.12.1 + version: 1.27.1(zod@4.3.6) + '@supaku/agentfactory-server': + specifier: workspace:* + version: link:../server + zod: + specifier: ^4.3.6 + version: 4.3.6 + devDependencies: + '@types/node': + specifier: ^22.5.4 + version: 22.19.10 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vitest: + specifier: ^3.2.3 + version: 3.2.4(@types/node@22.19.10)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) + packages/nextjs: dependencies: '@supaku/agentfactory': @@ -420,6 +442,12 @@ packages: peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@hono/node-server@1.19.11': + resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@img/colour@1.0.0': resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} engines: {node: '>=18'} @@ -660,6 +688,16 @@ packages: resolution: {integrity: sha512-rC4UjnFbVfV5TlXIzOhJ6xZVpmeXR0de4ALRd3ftDo9jtxkj2YUveF5swZ/1/Cd/a1S6h8OcOFvqFEz2x6zOZw==} engines: {node: '>=18.x'} + '@modelcontextprotocol/sdk@1.27.1': + resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@next/env@15.5.12': resolution: {integrity: sha512-pUvdJN1on574wQHjaBfNGDt9Mz5utDSZFsIIQkMzPgNS8ZvT4H2mwOrOIClwsQOb6EGx5M76/CZr6G8i6pSpLg==} @@ -1276,6 +1314,21 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -1309,6 +1362,10 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -1318,10 +1375,22 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + camelcase-css@2.0.1: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} @@ -1363,6 +1432,26 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1392,6 +1481,10 @@ packages: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -1413,16 +1506,39 @@ packages: resolution: {integrity: sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==} engines: {node: '>=12'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + eciesjs@0.4.17: resolution: {integrity: sha512-TOOURki4G7sD1wDCjj7NfLaXZZ49dFOeEb5y39IXpb8p0hRzVvfvzZHOi5JcT+PpyAbi/Y+lxPb8eTag2WYH8w==} engines: {bun: '>=1', deno: '>=2', node: '>=16'} + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + electron-to-chromium@1.5.286: resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + esbuild@0.27.3: resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} engines: {node: '>=18'} @@ -1432,9 +1548,24 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -1443,10 +1574,26 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + express-rate-limit@8.3.1: + resolution: {integrity: sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -1463,9 +1610,21 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1474,10 +1633,18 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + get-nonce@1.0.1: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -1493,6 +1660,10 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graphql@16.12.0: resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} @@ -1502,22 +1673,49 @@ packages: engines: {node: '>=0.4.7'} hasBin: true + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hono@4.12.7: + resolution: {integrity: sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==} + engines: {node: '>=16.9.0'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ioredis@5.9.2: resolution: {integrity: sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ==} engines: {node: '>=12.22.0'} + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -1538,6 +1736,9 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -1553,9 +1754,18 @@ packages: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true + jose@6.2.1: + resolution: {integrity: sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==} + js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -1580,6 +1790,18 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -1591,6 +1813,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -1609,6 +1839,10 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -1652,14 +1886,29 @@ packages: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + object-treeify@1.1.33: resolution: {integrity: sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==} engines: {node: '>= 10'} + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -1667,6 +1916,9 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -1693,6 +1945,10 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + postcss-import@15.1.0: resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} @@ -1744,9 +2000,25 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + qs@6.15.0: + resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + engines: {node: '>=0.6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + react-dom@19.2.4: resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} peerDependencies: @@ -1801,6 +2073,10 @@ packages: resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} engines: {node: '>=4'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -1818,9 +2094,16 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -1829,6 +2112,17 @@ packages: engines: {node: '>=10'} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -1841,6 +2135,22 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -1861,6 +2171,10 @@ packages: standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} @@ -1939,6 +2253,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -1984,6 +2302,10 @@ packages: resolution: {integrity: sha512-8Osxz5Tu/Dw2kb31EAY+nhq/YZ3wzmQSmYa1nIArqxgCAldxv9TPlrAiaBUDVnKA4aiPn0OFBD1ACcpc5VFOAQ==} hasBin: true + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -1997,6 +2319,10 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -2031,6 +2357,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -2122,11 +2452,19 @@ packages: wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + yaml@2.8.2: resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} engines: {node: '>= 14.6'} hasBin: true + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} @@ -2267,6 +2605,10 @@ snapshots: dependencies: graphql: 16.12.0 + '@hono/node-server@1.19.11(hono@4.12.7)': + dependencies: + hono: 4.12.7 + '@img/colour@1.0.0': optional: true @@ -2445,6 +2787,28 @@ snapshots: transitivePeerDependencies: - graphql + '@modelcontextprotocol/sdk@1.27.1(zod@4.3.6)': + dependencies: + '@hono/node-server': 1.19.11(hono@4.12.7) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.3.1(express@5.2.1) + hono: 4.12.7 + jose: 6.2.1 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.3.6 + zod-to-json-schema: 3.25.1(zod@4.3.6) + transitivePeerDependencies: + - supports-color + '@next/env@15.5.12': {} '@next/swc-darwin-arm64@15.5.12': @@ -2962,6 +3326,22 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + any-promise@1.3.0: {} anymatch@3.1.3: @@ -2990,6 +3370,20 @@ snapshots: binary-extensions@2.3.0: {} + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.0 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -3002,8 +3396,20 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) + bytes@3.1.2: {} + cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + camelcase-css@2.0.1: {} caniuse-lite@1.0.30001769: {} @@ -3044,6 +3450,19 @@ snapshots: commander@4.1.1: {} + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -3062,6 +3481,8 @@ snapshots: denque@2.1.0: {} + depd@2.0.0: {} + dequal@2.0.3: {} detect-libc@2.1.2: @@ -3075,6 +3496,12 @@ snapshots: dotenv@17.2.4: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + eciesjs@0.4.17: dependencies: '@ecies/ciphers': 0.2.5(@noble/ciphers@1.3.0) @@ -3082,10 +3509,22 @@ snapshots: '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 + ee-first@1.1.1: {} + electron-to-chromium@1.5.286: {} + encodeurl@2.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + esbuild@0.27.3: optionalDependencies: '@esbuild/aix-ppc64': 0.27.3 @@ -3117,10 +3556,20 @@ snapshots: escalade@3.2.0: {} + escape-html@1.0.3: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 + etag@1.8.1: {} + + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -3135,6 +3584,46 @@ snapshots: expect-type@1.3.0: {} + express-rate-limit@8.3.1(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.1.0 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -3143,6 +3632,8 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-uri@3.1.0: {} + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -3155,15 +3646,48 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + forwarded@0.2.0: {} + fraction.js@5.3.4: {} + fresh@2.0.0: {} + fsevents@2.3.3: optional: true function-bind@1.1.2: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + get-nonce@1.0.1: {} + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-stream@6.0.1: {} get-tsconfig@4.13.6: @@ -3178,6 +3702,8 @@ snapshots: dependencies: is-glob: 4.0.3 + gopd@1.2.0: {} + graphql@16.12.0: {} handlebars@4.7.8: @@ -3189,14 +3715,32 @@ snapshots: optionalDependencies: uglify-js: 3.19.3 + has-symbols@1.1.0: {} + hasown@2.0.2: dependencies: function-bind: 1.1.2 + hono@4.12.7: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + human-signals@2.1.0: {} + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + ignore@5.3.2: {} + inherits@2.0.4: {} + ioredis@5.9.2: dependencies: '@ioredis/commands': 1.5.0 @@ -3211,6 +3755,10 @@ snapshots: transitivePeerDependencies: - supports-color + ip-address@10.1.0: {} + + ipaddr.js@1.9.1: {} + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 @@ -3227,6 +3775,8 @@ snapshots: is-number@7.0.0: {} + is-promise@4.0.0: {} + is-stream@2.0.1: {} isexe@2.0.0: {} @@ -3235,8 +3785,14 @@ snapshots: jiti@1.21.7: {} + jose@6.2.1: {} + js-tokens@9.0.1: {} + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -3255,6 +3811,12 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + math-intrinsics@1.1.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -3264,6 +3826,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + mimic-fn@2.1.0: {} minimist@1.2.8: {} @@ -3278,6 +3846,8 @@ snapshots: nanoid@3.3.11: {} + negotiator@1.0.0: {} + neo-async@2.6.2: {} next@15.5.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4): @@ -3315,16 +3885,30 @@ snapshots: object-hash@3.0.0: {} + object-inspect@1.13.4: {} + object-treeify@1.1.33: {} + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + onetime@5.1.2: dependencies: mimic-fn: 2.1.0 + parseurl@1.3.3: {} + path-key@3.1.1: {} path-parse@1.0.7: {} + path-to-regexp@8.3.0: {} + pathe@2.0.3: {} pathval@2.0.1: {} @@ -3339,6 +3923,8 @@ snapshots: pirates@4.0.7: {} + pkce-challenge@5.0.1: {} + postcss-import@15.1.0(postcss@8.5.6): dependencies: postcss: 8.5.6 @@ -3384,8 +3970,26 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + qs@6.15.0: + dependencies: + side-channel: 1.1.0 + queue-microtask@1.2.3: {} + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + react-dom@19.2.4(react@19.2.4): dependencies: react: 19.2.4 @@ -3434,6 +4038,8 @@ snapshots: dependencies: redis-errors: 1.2.0 + require-from-string@2.0.2: {} + resolve-pkg-maps@1.0.0: {} resolve@1.22.11: @@ -3475,15 +4081,54 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.57.1 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 + safer-buffer@2.1.2: {} + scheduler@0.27.0: {} semver@7.7.4: optional: true + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + sharp@0.34.5: dependencies: '@img/colour': 1.0.0 @@ -3522,6 +4167,34 @@ snapshots: shebang-regex@3.0.0: {} + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@3.0.7: {} @@ -3534,6 +4207,8 @@ snapshots: standard-as-callback@2.1.0: {} + statuses@2.0.2: {} + std-env@3.10.0: {} strip-final-newline@2.0.0: {} @@ -3622,6 +4297,8 @@ snapshots: dependencies: is-number: 7.0.0 + toidentifier@1.0.1: {} + ts-interface-checker@0.1.13: {} tslib@2.8.1: {} @@ -3660,6 +4337,12 @@ snapshots: turbo-windows-64: 2.8.3 turbo-windows-arm64: 2.8.3 + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + typescript@5.9.3: {} uglify-js@3.19.3: @@ -3667,6 +4350,8 @@ snapshots: undici-types@6.21.0: {} + unpipe@1.0.0: {} + update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: browserslist: 4.28.1 @@ -3694,6 +4379,8 @@ snapshots: util-deprecate@1.0.2: {} + vary@1.1.2: {} + vite-node@3.2.4(@types/node@22.19.10)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2): dependencies: cac: 6.7.14 @@ -3786,6 +4473,12 @@ snapshots: wordwrap@1.0.0: {} + wrappy@1.0.2: {} + yaml@2.8.2: {} + zod-to-json-schema@3.25.1(zod@4.3.6): + dependencies: + zod: 4.3.6 + zod@4.3.6: {}