commit f39f6ac8819305b371646b8693a48a2832ff6b51 Author: Jeffrey Wu Date: Mon Sep 7 09:57:33 2026 +0800 feat: initial commit diff --git a/.agents/skills/ads-analysis/SKILL.md b/.agents/skills/ads-analysis/SKILL.md new file mode 100644 index 0000000..a9e9a8b --- /dev/null +++ b/.agents/skills/ads-analysis/SKILL.md @@ -0,0 +1,12 @@ +--- +name: ads-analysis +description: 分析广告账户投放数据,识别异常 Campaign,并形成优化建议。 +--- + +# 广告账户分析 + +你必须先调用 `mock_ads_metrics` 获取指定账户和时间范围的数据,再进行分析。 + +- 不要声称拥有真实广告平台数据;本 Skill 只分析工具返回的课堂 fixture。 +- 以工具数值为指标事实来源,图片仅可作为可见内容的辅助信号。 +- 识别异常 Campaign 时说明判断依据,给出可执行且有优先级的建议。 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..52d72f7 --- /dev/null +++ b/.env.example @@ -0,0 +1,16 @@ +OPENAI_API_KEY= +ANTHROPIC_API_KEY= +AGENT_PROVIDER= +AGENT_MODEL= +AGENT_DISPLAY_NAME= +AGENT_SUPPORTS_IMAGES=false +AGENT_CONTEXT_WINDOW= +AGENT_MAX_TOKENS= +LLM_BASE_URL= +LLM_API=openai-responses +MODEL_CREDENTIAL_ENCRYPTION_KEY= +ALLOW_PRIVATE_LLM_BASE_URLS=false +SESSION_RUN_TIMEOUT_MS=120000 +APPROVAL_TIMEOUT_MS=60000 +MAX_IMAGE_BYTES=5242880 +MAX_IMAGES_PER_MESSAGE=4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8cc8814 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +.pnpm-store/ +dist/ +coverage/ +.env +workspace/sessions/ +workspace/model-credentials.enc.json +*.log +.DS_Store diff --git a/.pi/skills/report-writer/SKILL.md b/.pi/skills/report-writer/SKILL.md new file mode 100644 index 0000000..04c625c --- /dev/null +++ b/.pi/skills/report-writer/SKILL.md @@ -0,0 +1,10 @@ +--- +name: report-writer +description: 把分析结果整理为结构化中文报告,并保存最终草稿。 +--- + +# 中文报告撰写 + +报告必须包含:账户总结、关键指标、异常 Campaign、建议、数据时间范围。 + +完成草稿后调用 `save_report_draft` 保存。该操作需要人工审批;如果被拒绝或超时,仍可在聊天中输出正文,并明确说明未保存。 diff --git a/README.md b/README.md new file mode 100644 index 0000000..4a4350a --- /dev/null +++ b/README.md @@ -0,0 +1,197 @@ +# Agent Studio Mini + +Agent Studio Mini 是一个用于内部培训的、可运行的最小 AI Agent Harness。它用 Pi Coding Agent SDK 驱动 Agent Loop,以 Fastify + SSE 暴露受控协议,用 assistant-ui 展示聊天、Tool、审批、图片附件与完整事件时间线。 + +当前仓库未附产品截图;启动后,`http://127.0.0.1:5173` 即是桌面三栏和移动端 Tabs 的真实演示界面。 + +## 架构 + +```mermaid +flowchart LR + UI[assistant-ui\nComposer / Thread / Tool UI] -->|HTTP + SSE| API[Fastify API] + API --> REG[SessionRegistry] + REG --> PI[Pi AgentSession] + API --> STORE[EventStore / ApprovalBroker] + PI --> LOADER[DefaultResourceLoader / Tools] + STORE --> VIEW[Timeline / Approval UI] + LOADER --> RES[Skills / Workspace / ModelRuntime] +``` + +```mermaid +sequenceDiagram + participant U as User + participant W as Web + assistant-ui + participant H as Harness + participant P as Pi AgentSession + U->>W: 文本 + 图片 + W->>H: POST message (立即 202) + H->>P: prompt(text, { images }) + P-->>H: stream / tool events + H-->>W: HarnessEvent over SSE + P->>H: save_report_draft preflight + H-->>W: approval.required + U->>H: Approve / Deny + H-->>P: allow / block + P-->>H: tool result + completion +``` + +职责边界很明确:Pi `AgentSession` 是 Agent Loop、模型调用和 Tool 生命周期的事实来源;Harness 管理 Session、事件序列、安全守卫、附件、模型凭证和审批;assistant-ui 只把 Harness 状态投影为聊天和 Tool UI,不直接调用模型,也不在浏览器执行服务端 Tool。 + +## 目录 + +```text +apps/server Fastify HTTP/SSE 组合层 +apps/web React、assistant-ui 与三栏界面 +packages/harness Session、Skills、Tools、Approval、安全边界 +packages/shared 浏览器/服务端共享的 Zod 协议 +.agents/skills ads-analysis 示例 Skill +.pi/skills report-writer 示例 Skill +fixtures 离线广告账户数据 +workspace/sessions 运行时临时 Session(被 gitignore) +docs 架构、映射、安全、课堂脚本与路线图 +``` + +## 环境与启动 + +Pi 0.84.3 的实际 package engine 要求 Node.js `>=22.19.0`,因此本仓库采用该版本,而不是需求草案中的 Node 20 下限。需要 pnpm 11。 + +```bash +cp .env.example .env +pnpm install +pnpm dev +``` + +Server 默认监听 `127.0.0.1:3001`,Vite Web 默认监听 `127.0.0.1:5173` 并代理 `/api` 与 `/health`。打开 `http://127.0.0.1:5173`。根命令还包括: + +```bash +pnpm lint +pnpm typecheck +pnpm test +pnpm build +``` + +## 模型配置 + +服务器环境变量优先级低于当前 Web Session 的内存设置: + +```dotenv +OPENAI_API_KEY= +ANTHROPIC_API_KEY= +MODEL_CREDENTIAL_ENCRYPTION_KEY= +AGENT_PROVIDER= +AGENT_MODEL= +AGENT_DISPLAY_NAME= +AGENT_SUPPORTS_IMAGES=false +AGENT_CONTEXT_WINDOW= +AGENT_MAX_TOKENS= +LLM_BASE_URL= +LLM_API=openai-responses +ALLOW_PRIVATE_LLM_BASE_URLS=false +SESSION_RUN_TIMEOUT_MS=120000 +APPROVAL_TIMEOUT_MS=60000 +MAX_IMAGE_BYTES=5242880 +MAX_IMAGES_PER_MESSAGE=4 +``` + +服务启动时会从项目 `.env` 严格加载并校验上述默认模型字段;`AGENT_PROVIDER` 与 `AGENT_MODEL` 必须同时存在。当前 Web Session 的 Settings 覆盖环境默认值;未覆盖时新 Session 自动使用 `.env`,无需每次启动重新配置。也可在顶栏 Settings 配置 Provider ID、`openai-responses` / `openai-completions` / `anthropic-messages`、Base URL、Model ID、Vision 与 API Key。Preset 只是可编辑起点,不保证模型存在。没有显式模型或凭证时返回 `MODEL_NOT_CONFIGURED` / `MODEL_CREDENTIAL_NOT_CONFIGURED`,绝不选择“第一个可用模型”。 + +Provider、API 类型、Base URL、Model ID、Display Name、Vision、context/max token 等**非敏感模型配置**会以版本化 strict Zod 结构保存到浏览器 `localStorage`。刷新或 Reset 创建新 Session 后,在服务器没有环境模型配置时自动恢复这些字段;损坏、超限或旧格式数据会安全丢弃。Settings 中的“忘记本地配置”只清除这份浏览器配置,不影响当前 Session。 + +API Key 可直接由 `.env` 的 `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` 提供。若在前端提交,则服务端使用 `MODEL_CREDENTIAL_ENCRYPTION_KEY` 做 AES-256-GCM 认证加密,密文写入 gitignored 的 `workspace/model-credentials.enc.json`;下次启动会按 `.env` 默认模型的连接 identity 自动恢复。浏览器仍不会把 Key 写入 Web Storage、IndexedDB 或 Service Worker cache。刷新或 Reset 后无需重复输入。读取配置只返回 `configured`、安全模型信息、末四位 `keyHint` 与存储类型。Clear Credentials 会同时清除当前 Runtime credential 和对应持久密文;换模型会销毁既有 Pi Session,下一次 Run 按新配置创建。密钥不会写入 Pi 全局 `auth.json` / `models.json`、URL、SSE 或日志。 + +生成 master key:`openssl rand -base64 32`。该值必须稳定保存在服务器 `.env` 或 secret manager 中;丢失或更换后旧密文会 fail closed、无法解密。生产环境应迁移到 KMS/HSM 并设计密钥轮换。 + +自定义 Base URL 使用 WHATWG `URL`、协议/userinfo/fragment/query 检查与 DNS 解析;默认拒绝 loopback、private、link-local 和 metadata 地址。开发环境只放行显式 loopback HTTP;企业内网需明确设置 `ALLOW_PRIVATE_LLM_BASE_URLS=true`。模型 Provider SDK 的重定向策略不是本课堂 Harness 可控的公开扩展点,生产版应在出口代理处禁用跨主机重定向并重新校验每一跳。 + +连接测试只执行有界的本地配置校验并返回“首次运行验证”,不创建 Agent Run、不记录模型响应,也不伪造远端成功。 + +## Skills 与 progressive disclosure + +启动时 `DefaultResourceLoader.reload()` 发现 `.agents/skills`、`.pi/skills` 和 `AGENT_SKILLS_DIRS`(使用操作系统 `path.delimiter`)。额外目录会 canonicalize、去重、验证;错误进入 Catalog diagnostics,不拖垮服务。`GET /api/skills` 只返回名称、描述、来源、安全展示路径和 diagnostics。 + +Harness 不在启动时读取并拼接所有 `SKILL.md` 正文。Pi 仅先看到 Catalog,匹配任务后通过 `read` 按需加载正文。`skill.requested` / `skill.loaded` **不是 Pi 原生事件**:Harness 以已发现 Skill 的 canonical `SKILL.md` 路径索引,从成功的 `read` 请求/结果可靠推导;读取失败不会误报 loaded。 + +示例: + +- `ads-analysis`:要求先调用离线 `mock_ads_metrics`,不声称有真实平台数据。 +- `report-writer`:生成结构化中文报告,并请求 `save_report_draft`。 + +## Session、Events 与 SSE + +每个 Web Session 有随机 ID、`workspace/sessions/` 独立 cwd、`SessionManager.inMemory(cwd)`、事件 sequence、运行锁、AbortController、审批、附件与内存凭证。为避免无配置时触发 Pi 的模型 fallback,Web Session 先建立,Pi `AgentSession` 在首次已配置 Run 时惰性创建;每个 Web Session 至多对应一个独立 Pi Session。 + +消息 API 接受后立即返回 `202 + runId`;同 Session 并发返回 `409 SESSION_BUSY`。用户取消与系统超时分别记录。删除顺序是 abort、取消审批、关闭订阅/SSE、dispose、清凭证/附件、校验目录边界、删除临时 cwd。 + +EventStore 原子分配单 Session 单调 sequence,保留最近 1000 条。SSE 使用 `id/event/data`、15 秒 heartbeat、`Last-Event-ID` 优先于 `afterSequence`、历史重放、窗口 gap 控制事件、慢客户端断开策略与连接清理。前端 reducer 按 eventId/sequence 去重并合并高频 delta。 + +完整映射见 [docs/event-mapping.md](docs/event-mapping.md)。 + +## Tools 与 Approval + +显式 allowlist 只有 `read`、`grep`、`find`、`ls`、`mock_ads_metrics` 和 `save_report_draft`。默认不注册 bash/powershell/edit/write;未知 Tool deny。 + +- `mock_ads_metrics` 用当前 Pi `defineTool` + TypeBox,从 `fixtures/ads-account.json` 读取 1–30 天离线数据,计算除零安全、定点精度的指标。 +- `save_report_draft` 只接受标题/正文,只能原子写当前 Session 的固定 `report.md`。 + +Pi extension 的 `tool_call` preflight 在执行前阻塞 `save_report_draft`。服务器 `ApprovalBroker` 是唯一权威;Approve 才继续,Deny/Timeout/Abort 均 block,且生成 `approval.resolved`。重复相同决定幂等,冲突决定返回 409。前端卡片和 modal 只提交决定并等待 SSE 权威状态。 + +## 图片多模态链路 + +Composer 支持选择、拖拽、粘贴、上传状态、移除与对话框预览。自定义 assistant-ui `AttachmentAdapter` 立即流式上传,React 状态只保留 attachmentId 和临时 object URL;移除/卸载会 revoke URL。 + +服务端只接受 JPEG、PNG、WebP,并同时校验扩展名、声明 MIME 与 magic bytes。默认单图 5 MiB、单消息 4 张,并限制像素;Sharp 自动旋转、移除 EXIF/ICC 等元数据并重新编码。随机文件名只进入 Session 的 `attachments/`。SVG/GIF/PDF/视频、伪 MIME、跨 Session ID 与解压炸弹会被拒绝。 + +发送时服务器验证归属与 ready 状态,按已解析 Pi Model 的 image capability 拦截 text-only 模型,然后把规范化图片映射为安装版 Pi `ImageContent[]` 并调用 `session.prompt(text, { images })`。base64 只存在于这一步的服务端瞬时调用,不进入事件或 React 全局状态。 + +## Demo + +点击“填入 Demo Prompt”(不会自动执行),或粘贴: + +> 请使用 ads-analysis Skill,分析账户 demo-account 最近 7 天数据,找出异常 Campaign,并输出账户总结、关键指标和 3 条优化建议。最后使用 report-writer Skill 生成一份报告草稿。 + +可附一张广告素材/后台截图并补充: + +> 请同时观察我上传的图片,将可见内容作为辅助信号;不要根据图片编造图片中不存在的数据,数值指标以 mock_ads_metrics 返回的数据为准。 + +预期路径:Skill requested/loaded → mock metrics Tool 卡片 → 分析 → report-writer → save Tool 卡片与审批提醒。Approve 后只写 Session `report.md`;Deny/超时不写文件,聊天仍可呈现正文。10–15 分钟完整讲稿见 [docs/classroom-demo.md](docs/classroom-demo.md)。 + +## 安全边界 + +**Pi 没有内置 Sandbox。Project Trust 是项目资源加载防线,不是执行隔离。** Pi、Extension 和 Tool 继承 Node 进程权限。本项目通过最小 Tool allowlist、固定 Session cwd、canonical/realpath 路径守卫、符号链接拒绝、protected-path preflight、固定写入目标、审批、SSRF 检查、图片重编码和日志/事件脱敏降低风险,但这些都不等于 OS Sandbox。 + +不要把不可信 Skill、脚本、仓库或无人值守任务直接交给本课堂进程。生产环境必须放入 Docker、VM、micro-VM 或远程 Sandbox,并配置受控网络出口。更多边界见 [docs/security-boundaries.md](docs/security-boundaries.md)。 + +## 版本与 API 核对 + +编码前核对了 Pi 的 [SDK](https://pi.dev/docs/latest/sdk)、[Skills](https://pi.dev/docs/latest/skills)、[Extensions](https://pi.dev/docs/latest/extensions)、[Security](https://pi.dev/docs/latest/security)、[Models](https://pi.dev/docs/latest/models)、[Providers](https://pi.dev/docs/latest/providers),以及 assistant-ui 的 [Custom Runtime](https://www.assistant-ui.com/docs/runtimes/custom/external-store)、[Tools](https://www.assistant-ui.com/docs/tools) 与 [Attachments](https://www.assistant-ui.com/docs/guides/attachments)。安装后再以 `node_modules` 的公开 `.d.ts` 和可验证行为为准。 + +| 依赖 | 实装版本 | 使用的公开接口 | +| --- | --- | --- | +| Pi Coding Agent / pi-ai | 0.84.3 | `createAgentSession`, `DefaultResourceLoader`, `SettingsManager.inMemory`, `SessionManager.inMemory`, `ModelRuntime`, `InMemoryCredentialStore`, `defineTool`, `isToolCallEventType` | +| TypeBox | 1.3.7 | Pi Tool 参数 schema | +| assistant-ui React | 0.15.17 | `useExternalStoreRuntime`, Attachment Adapter, Toolkit + `Tools`, Thread/Composer primitives | +| Fastify | 5.12.1 | HTTP、multipart、SSE | +| React / Vite | 19.2.8 / 7.3.6 | Web UI / build | +| Zod | 4.4.3 | HTTP、事件、模型与消息边界 | + +发现的实际差异:Pi 文档示例中的图片结构仍展示嵌套 `source`,但 0.84.3 导出的稳定 `ImageContent` 是 `{ type: "image", data, mimeType }`,本实现服从安装包类型。Pi 0.84.3 也要求 Node `>=22.19.0`。assistant-ui 包仍导出部分历史 helper,但本项目没有使用弃用的 `makeAssistantTool` / `useAssistantToolUI`,采用稳定 External Store Runtime 和 Toolkit renderers;没有使用 `unstable_*` API。 + +## 测试与当前结果 + +默认测试使用 fake `AgentSessionPort`,不需要 API Key、不会访问模型或外网。覆盖 shared schemas、Skill discovery/diagnostics、canonical Skill 事件、Tools、安全路径、Approval 全状态、Session 并发/取消/清理、模型无 fallback/SSRF/脱敏、JPEG/PNG/WebP、伪 MIME/大小/跨 Session、SSE replay/heartbeat/cleanup、assistant-ui reducer/Tool UI/Settings/附件与可访问性。 + +2026-08-28 本机验证: + +- `pnpm lint`:通过,0 warning。 +- `pnpm typecheck`:4 个 workspace package 全部通过。 +- `pnpm test`:8 个 test files、88 个 tests 全部通过;其中包含 `.env` 默认模型严格加载、环境 API Key 脱敏、加密凭证按默认模型跨重启恢复、模型元数据恢复、AES-256-GCM 凭证跨 Session 恢复/清除/错误 master key fail-closed、API Key 不进入 Web Storage、Session 初始化竞态、真实重复 Tool/Approval 事件链去重与 UI error fallback,以及使用精确自定义模型、关闭模型网络刷新后创建并 dispose 真实 Pi `AgentSession` 的测试。 +- `pnpm build`:shared、harness、server、web 全部通过;Vite 提示单一 JS chunk 约 603 kB,可在生产化时拆包,但不影响构建。 +- `PORT=3010 pnpm --filter @agent-studio/server start`:编译产物启动成功,`GET /health` 返回 200,随后 SIGINT 优雅关闭。 + +人工浏览器验收:桌面三栏、无模型禁发、Settings password 显隐/脱敏回显、Vision badge、Demo Prompt 只填不发、SSE Timeline、390px 移动端 Tabs 与无横向溢出均通过;应用控制台无 error/warn。浏览器自动化环境拒绝设置本地 file chooser,因此上传点击的人工自动化未完成,实际 multipart 上传、附件 adapter、图片预览/移除和 text-only 拦截由通过的 Server/Web 测试覆盖。 + +## 已知限制与生产化 + +课堂版服务端仍是单进程:重启会丢 Session/Event/Approval,但模型 API Key 可通过本地 AES-256-GCM 密文恢复;浏览器只保留非敏感模型元数据。当前文件 vault 没有多用户/租户隔离、远程 KMS、密钥轮换或分布式锁。SSE 无跨实例协调;没有用户身份、CSRF、分布式 rate limit、持久审计、对象存储/KMS 或 OS Sandbox。自定义 provider 的 Vision 声明会被服务器注册成精确 Pi Model 并按该模型检查,但课堂版不会主动发远端 capability probe。 + +生产化路线见 [docs/production-roadmap.md](docs/production-roadmap.md):Redis/Postgres、对象存储、KMS、身份鉴权与 CSRF、速率/配额、容器/VM Sandbox、出口代理、不可变审计日志、队列/worker、分布式 SSE,以及完整可观测性。 diff --git a/apps/server/package.json b/apps/server/package.json new file mode 100644 index 0000000..aaafc1d --- /dev/null +++ b/apps/server/package.json @@ -0,0 +1,16 @@ +{ + "name": "@agent-studio/server", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { "predev": "pnpm --filter @agent-studio/shared build && pnpm --filter @agent-studio/harness build", "dev": "tsx watch src/main.ts", "prestart": "pnpm --filter @agent-studio/shared build && pnpm --filter @agent-studio/harness build", "start": "node dist/main.js", "typecheck": "tsc --noEmit", "build": "tsc -p tsconfig.json" }, + "dependencies": { + "@agent-studio/harness": "workspace:*", + "@agent-studio/shared": "workspace:*", + "@fastify/multipart": "^9.3.0", + "@fastify/static": "^8.3.0", + "fastify": "^5.6.2", + "zod": "^4.1.13" + }, + "devDependencies": { "@types/node": "^24.10.1", "typescript": "^5.9.3" } +} diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts new file mode 100644 index 0000000..edd5aeb --- /dev/null +++ b/apps/server/src/app.ts @@ -0,0 +1,123 @@ +import { randomUUID } from "node:crypto"; +import { mkdir } from "node:fs/promises"; +import path from "node:path"; +import multipart from "@fastify/multipart"; +import Fastify, { type FastifyInstance, type FastifyReply, type FastifyRequest } from "fastify"; +import { ZodError, z } from "zod"; +import { + FileAttachmentStore, + EncryptedFileCredentialVault, + HarnessError, + InMemoryEventStore, + InMemoryModelConnectionStore, + InMemorySessionRegistry, + PiAgentSessionFactory, + SkillCatalog, + safeErrorMessage, + type AgentSessionFactory, +} from "@agent-studio/harness"; +import { modelConnectionInputSchema, sendMessageSchema } from "@agent-studio/shared"; + +const idParams = z.object({ sessionId: z.string().min(1) }); +const attachmentParams = idParams.extend({ attachmentId: z.string().min(1) }); +const approvalParams = z.object({ approvalId: z.string().min(1) }); +const eventQuery = z.object({ afterSequence: z.coerce.number().int().nonnegative().optional() }); + +function parse(schema: z.ZodType, value: unknown): T { return schema.parse(value); } + +export interface AppServices { sessions: InMemorySessionRegistry; events: InMemoryEventStore; attachments: FileAttachmentStore; models: InMemoryModelConnectionStore; skills: SkillCatalog } +export interface AppOptions { projectRoot?: string; runTimeoutMs?: number; approvalTimeoutMs?: number; heartbeatMs?: number; agentFactory?: AgentSessionFactory; onReady?: (services: AppServices) => void } + +export async function buildApp(options: AppOptions = {}): Promise { + const projectRoot = options.projectRoot ?? path.resolve(import.meta.dirname, "../../.."); + const sessionsRoot = path.join(projectRoot, "workspace", "sessions"); + const agentDir = path.join(projectRoot, "workspace", ".pi-catalog"); + await mkdir(sessionsRoot, { recursive: true, mode: 0o700 }); + await mkdir(agentDir, { recursive: true, mode: 0o700 }); + + const events = new InMemoryEventStore(1_000); + const credentialVault = process.env.MODEL_CREDENTIAL_ENCRYPTION_KEY + ? new EncryptedFileCredentialVault(path.join(projectRoot, "workspace", "model-credentials.enc.json"), process.env.MODEL_CREDENTIAL_ENCRYPTION_KEY) + : undefined; + const models = new InMemoryModelConnectionStore(process.env, credentialVault); + // Validate and cache the .env default before any Web Session is accepted. + await models.initialize(); + const attachments = new FileAttachmentStore(sessionsRoot, Number(process.env.MAX_IMAGE_BYTES ?? 5 * 1024 * 1024)); + const skills = new SkillCatalog(projectRoot, agentDir); + await skills.reload(); + const sessions = new InMemorySessionRegistry(sessionsRoot, events, attachments, models, skills, undefined, options.approvalTimeoutMs ?? Number(process.env.APPROVAL_TIMEOUT_MS ?? 60_000), options.runTimeoutMs ?? Number(process.env.SESSION_RUN_TIMEOUT_MS ?? 120_000)); + sessions.setFactory(options.agentFactory ?? new PiAgentSessionFactory(path.join(projectRoot, "fixtures", "ads-account.json"), skills, models, sessions.approvals)); + options.onReady?.({ sessions, events, attachments, models, skills }); + + const app = Fastify({ + genReqId: () => `req_${randomUUID()}`, + logger: process.env.NODE_ENV === "test" ? false : { level: process.env.LOG_LEVEL ?? "info", redact: { paths: ["req.headers.authorization", "req.body.apiKey", "*.apiKey", "*.authorization"], censor: "[REDACTED]" } }, + bodyLimit: 1_000_000, + }); + await app.register(multipart, { limits: { files: 1, fileSize: Number(process.env.MAX_IMAGE_BYTES ?? 5 * 1024 * 1024) + 1, fields: 4 } }); + app.addHook("onRequest", async (request, reply) => { reply.header("x-request-id", request.id); }); + + app.setErrorHandler((error, request, reply) => { + if (error instanceof ZodError) return sendError(reply, request, 400, "VALIDATION_ERROR", "Request validation failed", { fields: z.flattenError(error).fieldErrors }); + if (error instanceof HarnessError) return sendError(reply, request, error.statusCode, error.code, error.message, error.details); + if ((error as { code?: string }).code === "FST_REQ_FILE_TOO_LARGE") return sendError(reply, request, 413, "ATTACHMENT_TOO_LARGE", "Image exceeds the configured size limit"); + request.log.error({ requestId: request.id, code: "INTERNAL_ERROR", message: safeErrorMessage(error) }, "request failed"); + return sendError(reply, request, 500, "INTERNAL_ERROR", "Internal server error"); + }); + + app.get("/health", async (request) => ({ ok: true, requestId: request.id })); + app.get("/api/skills", async (request) => ({ skills: skills.list(), requestId: request.id })); + app.post("/api/sessions", async (request, reply) => { const session = await sessions.create(); return reply.code(201).send({ sessionId: session.sessionId, requestId: request.id }); }); + app.delete("/api/sessions/:sessionId", async (request, reply) => { const { sessionId } = parse(idParams, request.params); await sessions.delete(sessionId); return reply.send({ deleted: true, requestId: request.id }); }); + app.post("/api/sessions/:sessionId/messages", async (request, reply) => { const { sessionId } = parse(idParams, request.params); const body = parse(sendMessageSchema, request.body); const result = await sessions.send(sessionId, body); return reply.code(202).send({ ...result, requestId: request.id }); }); + app.post("/api/sessions/:sessionId/cancel", async (request, reply) => { const { sessionId } = parse(idParams, request.params); if (!sessions.get(sessionId)) throw new HarnessError("SESSION_NOT_FOUND", "Session not found", 404); await sessions.cancel(sessionId); return reply.send({ cancelled: true, requestId: request.id }); }); + + app.get("/api/sessions/:sessionId/model-config", async (request) => { const { sessionId } = parse(idParams, request.params); if (!sessions.get(sessionId)) throw new HarnessError("SESSION_NOT_FOUND", "Session not found", 404); return { ...models.getRedacted(sessionId), requestId: request.id }; }); + app.put("/api/sessions/:sessionId/model-config", async (request) => { const { sessionId } = parse(idParams, request.params); const input = parse(modelConnectionInputSchema, request.body); return { ...(await sessions.configureModel(sessionId, input)), requestId: request.id }; }); + app.delete("/api/sessions/:sessionId/model-config/credentials", async (request) => { const { sessionId } = parse(idParams, request.params); await sessions.clearCredentials(sessionId); return { cleared: true, requestId: request.id }; }); + app.post("/api/sessions/:sessionId/model-config/test", async (request) => { const { sessionId } = parse(idParams, request.params); if (!sessions.get(sessionId)) throw new HarnessError("SESSION_NOT_FOUND", "Session not found", 404); const config = models.getRedacted(sessionId); if (!config.configured) throw new HarnessError("MODEL_NOT_CONFIGURED", "No model connection is configured", 422); return { status: "deferred", message: "配置已通过本地校验;为避免记录模型正文,将在首次运行时验证连接。", requestId: request.id }; }); + + app.post("/api/sessions/:sessionId/attachments", async (request, reply) => { + const { sessionId } = parse(idParams, request.params); + if (!sessions.get(sessionId)) throw new HarnessError("SESSION_NOT_FOUND", "Session not found", 404); + const file = await request.file(); + if (!file) throw new HarnessError("ATTACHMENT_REQUIRED", "Multipart image file is required", 400); + try { + const item = await attachments.add(sessionId, { filename: file.filename, mimetype: file.mimetype, stream: file.file }); + events.append(sessionId, { type: "attachment.uploaded", payload: { attachmentId: item.attachmentId, name: item.name, mediaType: item.mediaType, byteSize: item.byteSize, width: item.width, height: item.height, status: "ready" } }); + return reply.code(201).send({ ...item, requestId: request.id }); + } catch (error) { + events.append(sessionId, { type: "attachment.rejected", payload: { name: file.filename.slice(0, 200), code: error instanceof HarnessError ? error.code : "ATTACHMENT_REJECTED", message: error instanceof HarnessError ? error.message : "Attachment rejected" } }); + throw error; + } + }); + app.delete("/api/sessions/:sessionId/attachments/:attachmentId", async (request) => { const { sessionId, attachmentId } = parse(attachmentParams, request.params); if (!sessions.get(sessionId)) throw new HarnessError("SESSION_NOT_FOUND", "Session not found", 404); await attachments.remove(sessionId, attachmentId); return { deleted: true, requestId: request.id }; }); + + app.post("/api/approvals/:approvalId/approve", async (request) => { const { approvalId } = parse(approvalParams, request.params); const result = sessions.approvals.approve(approvalId); return { approval: result.request, requestId: request.id }; }); + app.post("/api/approvals/:approvalId/deny", async (request) => { const { approvalId } = parse(approvalParams, request.params); const result = sessions.approvals.deny(approvalId); return { approval: result.request, requestId: request.id }; }); + + app.get("/api/sessions/:sessionId/events", async (request, reply) => { + const { sessionId } = parse(idParams, request.params); + if (!sessions.get(sessionId)) throw new HarnessError("SESSION_NOT_FOUND", "Session not found", 404); + const query = parse(eventQuery, request.query); + const lastEventId = request.headers["last-event-id"]; + const cursor = typeof lastEventId === "string" ? { eventId: lastEventId } : query.afterSequence !== undefined ? { sequence: query.afterSequence } : {}; + reply.hijack(); + const raw = reply.raw; + raw.writeHead(200, { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-cache, no-transform", connection: "keep-alive", "x-accel-buffering": "no", "x-request-id": request.id }); + const write = (chunk: string) => { if (raw.writableLength > 1024 * 1024) { raw.destroy(); return; } raw.write(chunk); }; + const batch = events.listAfter(sessionId, cursor); + if (batch.gap) write(`event: stream.gap\ndata: ${JSON.stringify({ type: "stream.gap", ...batch.gap })}\n\n`); + for (const item of batch.events) write(serializeEvent(item)); + const unsubscribe = events.subscribe(sessionId, (item) => write(serializeEvent(item))); + const heartbeat = setInterval(() => write(`: heartbeat ${Date.now()}\n\n`), options.heartbeatMs ?? 15_000); + const cleanup = () => { clearInterval(heartbeat); unsubscribe(); }; + request.raw.once("close", cleanup); + }); + + app.addHook("onClose", async () => { await sessions.shutdown(); }); + return app; +} + +function serializeEvent(event: { eventId: string; type: string } & Record): string { return `id: ${event.eventId}\nevent: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`; } +function sendError(reply: FastifyReply, request: FastifyRequest, status: number, code: string, message: string, details: Record = {}) { return reply.code(status).send({ error: { code, message, requestId: request.id, details } }); } diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts new file mode 100644 index 0000000..3340a86 --- /dev/null +++ b/apps/server/src/main.ts @@ -0,0 +1,12 @@ +import path from "node:path"; +import { loadEnvFile } from "node:process"; +import { buildApp } from "./app.js"; + +try { loadEnvFile(path.resolve(import.meta.dirname, "../../../.env")); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; } +const app = await buildApp(); +const port = Number(process.env.PORT ?? 3001); +await app.listen({ host: "127.0.0.1", port }); + +const shutdown = async () => { await app.close(); process.exit(0); }; +process.once("SIGINT", () => void shutdown()); +process.once("SIGTERM", () => void shutdown()); diff --git a/apps/server/tests/app.test.ts b/apps/server/tests/app.test.ts new file mode 100644 index 0000000..43c2f79 --- /dev/null +++ b/apps/server/tests/app.test.ts @@ -0,0 +1,41 @@ +import { mkdtemp, mkdir } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { buildApp, type AppServices } from "../src/app.js"; +import type { AgentSessionFactory, AgentSessionPort } from "@agent-studio/harness"; +import type { FastifyInstance } from "fastify"; + +class ServerFakePort implements AgentSessionPort { + readonly modelId = "fake-model"; readonly supportsImages = true; disposed = false; private finish?: () => void; + prompt(): Promise { return new Promise((resolve) => { this.finish = resolve; }); } + async abort(): Promise { this.finish?.(); } + subscribe(): () => void { return () => undefined; } + dispose(): void { this.disposed = true; } + complete(): void { this.finish?.(); } +} +class ServerFakeFactory implements AgentSessionFactory { port = new ServerFakePort(); async create() { return this.port; } } +const apps: FastifyInstance[] = []; +afterEach(async () => { while (apps.length) await apps.pop()?.close(); }); + +async function fixture() { + const root = await mkdtemp(path.join(tmpdir(), "studio-server-")); await mkdir(path.join(root, ".agents/skills"), { recursive: true }); await mkdir(path.join(root, ".pi/skills"), { recursive: true }); await mkdir(path.join(root, "fixtures")); + const factory = new ServerFakeFactory(); let services: AppServices | undefined; const app = await buildApp({ projectRoot: root, agentFactory: factory, heartbeatMs: 20, onReady: (value) => { services = value; } }); apps.push(app); return { app, factory, services: services! }; +} +async function createSession(app: FastifyInstance) { const response = await app.inject({ method: "POST", url: "/api/sessions" }); return (response.json() as { sessionId: string }).sessionId; } +async function configure(app: FastifyInstance, sessionId: string, supportsImages = true) { return app.inject({ method: "PUT", url: `/api/sessions/${sessionId}/model-config`, payload: { providerId: "fake", api: "openai-responses", modelId: "fake-model", apiKey: "sk-test-never-return", supportsImages } }); } + +describe("Fastify API", () => { + it("returns health and a request id", async () => { const { app } = await fixture(); const response = await app.inject({ method: "GET", url: "/health" }); expect(response.statusCode).toBe(200); expect(response.headers["x-request-id"]).toMatch(/^req_/); expect(response.json()).toMatchObject({ ok: true }); }); + it("uses the unified 404 error envelope", async () => { const { app } = await fixture(); const response = await app.inject({ method: "POST", url: "/api/sessions/missing/cancel" }); expect(response.statusCode).toBe(404); expect(response.json()).toMatchObject({ error: { code: "SESSION_NOT_FOUND", message: "Session not found" } }); }); + it("returns safe Zod field errors", async () => { const { app } = await fixture(); const sessionId = await createSession(app); const response = await app.inject({ method: "POST", url: `/api/sessions/${sessionId}/messages`, payload: { text: "", attachmentIds: [] } }); expect(response.statusCode).toBe(400); expect(response.json()).toHaveProperty("error.details.fields"); }); + it("never returns the API key from model config", async () => { const { app } = await fixture(); const sessionId = await createSession(app); const saved = await configure(app, sessionId); expect(saved.statusCode).toBe(200); expect(saved.body).not.toContain("sk-test-never-return"); const read = await app.inject({ method: "GET", url: `/api/sessions/${sessionId}/model-config` }); expect(read.body).not.toContain("sk-test-never-return"); expect(read.json()).toMatchObject({ configured: true, keyConfigured: true, keyHint: "••••turn" }); }); + it("rejects SSRF base URLs", async () => { const { app } = await fixture(); const sessionId = await createSession(app); const response = await app.inject({ method: "PUT", url: `/api/sessions/${sessionId}/model-config`, payload: { providerId: "fake", api: "openai-responses", modelId: "fake", apiKey: "x", supportsImages: false, baseUrl: "https://169.254.169.254/latest" } }); expect(response.statusCode).toBe(400); expect(response.json()).toMatchObject({ error: { code: "SSRF_BLOCKED" } }); }); + it("returns 202 without waiting for the Agent run", async () => { const { app, factory } = await fixture(); const sessionId = await createSession(app); await configure(app, sessionId); const response = await app.inject({ method: "POST", url: `/api/sessions/${sessionId}/messages`, payload: { text: "hello", attachmentIds: [] } }); expect(response.statusCode).toBe(202); expect(response.json()).toHaveProperty("runId"); factory.port.complete(); }); + it("returns 409 for concurrent messages", async () => { const { app, factory } = await fixture(); const sessionId = await createSession(app); await configure(app, sessionId); await app.inject({ method: "POST", url: `/api/sessions/${sessionId}/messages`, payload: { text: "one", attachmentIds: [] } }); const response = await app.inject({ method: "POST", url: `/api/sessions/${sessionId}/messages`, payload: { text: "two", attachmentIds: [] } }); expect(response.statusCode).toBe(409); expect(response.json()).toMatchObject({ error: { code: "SESSION_BUSY" } }); factory.port.complete(); }); + it("supports approve and deny endpoints with conflict detection", async () => { const { app, services } = await fixture(); const sessionId = await createSession(app); const controller = new AbortController(); const pending = services.sessions.approvals.request({ sessionId, runId: "run_test", toolCallId: "tool_test", toolName: "save_report_draft", arguments: {}, riskLevel: "medium" }, controller.signal); const event = services.events.listAfter(sessionId).events.find((item) => item.type === "approval.required"); if (!event || event.type !== "approval.required") throw new Error("missing approval"); const approved = await app.inject({ method: "POST", url: `/api/approvals/${event.payload.approvalId}/approve` }); expect(approved.statusCode).toBe(200); expect((await pending).status).toBe("approved"); const conflict = await app.inject({ method: "POST", url: `/api/approvals/${event.payload.approvalId}/deny` }); expect(conflict.statusCode).toBe(409); }); + it("accepts a valid multipart PNG and rejects SVG", async () => { const { app } = await fixture(); const sessionId = await createSession(app); const png = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=", "base64"); const accepted = await app.inject({ method: "POST", url: `/api/sessions/${sessionId}/attachments`, headers: { "content-type": "multipart/form-data; boundary=studio" }, payload: multipart("studio", "image.png", "image/png", png) }); expect(accepted.statusCode).toBe(201); expect(accepted.json()).toMatchObject({ mediaType: "image/png", width: 1, status: "ready" }); const rejected = await app.inject({ method: "POST", url: `/api/sessions/${sessionId}/attachments`, headers: { "content-type": "multipart/form-data; boundary=studio" }, payload: multipart("studio", "x.svg", "image/svg+xml", Buffer.from("")) }); expect(rejected.statusCode).toBe(415); }); + it("serves SSE events and heartbeat comments", async () => { const { app } = await fixture(); const sessionId = await createSession(app); const address = await app.listen({ host: "127.0.0.1", port: 0 }); const controller = new AbortController(); const response = await fetch(`${address}/api/sessions/${sessionId}/events`, { signal: controller.signal }); expect(response.headers.get("content-type")).toContain("text/event-stream"); const reader = response.body!.getReader(); let text = ""; for (let count = 0; count < 4 && !text.includes(": heartbeat"); count++) { const chunk = await reader.read(); text += new TextDecoder().decode(chunk.value); } controller.abort(); expect(text).toContain("event: session.started"); expect(text).toContain(": heartbeat"); }); +}); + +function multipart(boundary: string, filename: string, mime: string, data: Buffer): Buffer { return Buffer.concat([Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${filename}"\r\nContent-Type: ${mime}\r\n\r\n`), data, Buffer.from(`\r\n--${boundary}--\r\n`)]); } diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json new file mode 100644 index 0000000..1e7755d --- /dev/null +++ b/apps/server/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "rootDir": "src", "outDir": "dist", "tsBuildInfoFile": "dist/.tsbuildinfo" }, + "include": ["src"] +} diff --git a/apps/web/index.html b/apps/web/index.html new file mode 100644 index 0000000..4e8fbea --- /dev/null +++ b/apps/web/index.html @@ -0,0 +1,5 @@ + + + Agent Studio Mini +
+ diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..281077f --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,17 @@ +{ + "name": "@agent-studio/web", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { "dev": "vite --host 127.0.0.1", "typecheck": "tsc --noEmit", "build": "vite build" }, + "dependencies": { + "@agent-studio/shared": "workspace:*", + "@assistant-ui/react": "0.15.17", + "@radix-ui/react-dialog": "^1.1.15", + "lucide-react": "^0.555.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "zod": "^4.1.13" + }, + "devDependencies": { "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", "typescript": "^5.9.3", "vite": "^7.2.6" } +} diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx new file mode 100644 index 0000000..cc24ee6 --- /dev/null +++ b/apps/web/src/App.tsx @@ -0,0 +1,12 @@ +import { useState } from "react"; +import { Bot, RotateCcw, Settings } from "lucide-react"; +import { ApprovalDialog } from "./components/ApprovalDialog"; +import { ChatPanel } from "./components/ChatPanel"; +import { SettingsDialog } from "./components/SettingsDialog"; +import { SkillsPanel } from "./components/SkillsPanel"; +import { TimelinePanel } from "./components/TimelinePanel"; +import { HarnessToolsProvider } from "./components/ToolUIs"; +import { useHarnessState, useHarnessStore } from "./assistant/HarnessContext"; + +type MobileTab = "skills" | "chat" | "events"; +export function App() { const store = useHarnessStore(); const state = useHarnessState(); const [settings, setSettings] = useState(false); const [tab, setTab] = useState("chat"); return
Agent Studio MiniPi Coding Agent Harness
{state.sessionId?.slice(0, 16) ?? "creating session…"}{state.model.modelId ?? "Model missing"}{state.model.supportsImages && Vision}
setSettings(true)}/>
{settings && }
; } diff --git a/apps/web/src/assistant/HarnessContext.ts b/apps/web/src/assistant/HarnessContext.ts new file mode 100644 index 0000000..3b1c2b2 --- /dev/null +++ b/apps/web/src/assistant/HarnessContext.ts @@ -0,0 +1,15 @@ +import { createContext, useContext, useSyncExternalStore } from "react"; +import { HarnessClientStore, type ClientState } from "./store"; + +export const StoreContext = createContext(null); + +export function useHarnessStore(): HarnessClientStore { + const store = useContext(StoreContext); + if (!store) throw new Error("Harness store is missing"); + return store; +} + +export function useHarnessState(): ClientState { + const store = useHarnessStore(); + return useSyncExternalStore(store.subscribe, store.getSnapshot); +} diff --git a/apps/web/src/assistant/HarnessRuntime.tsx b/apps/web/src/assistant/HarnessRuntime.tsx new file mode 100644 index 0000000..e68337e --- /dev/null +++ b/apps/web/src/assistant/HarnessRuntime.tsx @@ -0,0 +1,20 @@ +import { useMemo, useSyncExternalStore, type PropsWithChildren } from "react"; +import { AssistantRuntimeProvider, useExternalStoreRuntime } from "@assistant-ui/react"; +import { StoreContext } from "./HarnessContext"; +import { HarnessClientStore } from "./store"; + +export function HarnessRuntime({ store, children }: PropsWithChildren<{ store: HarnessClientStore }>) { + const state = useSyncExternalStore(store.subscribe, store.getSnapshot); + const runtime = useExternalStoreRuntime({ + messages: state.messages, + isRunning: state.isRunning, + isSendDisabled: !state.model.configured || !state.model.keyConfigured || state.connection !== "online", + setMessages: store.setMessages, + onNew: store.onNew, + onCancel: store.onCancel, + adapters: { attachments: store.attachmentAdapter }, + convertMessage: (message) => message, + }); + const value = useMemo(() => store, [store]); + return {children}; +} diff --git a/apps/web/src/assistant/store.ts b/apps/web/src/assistant/store.ts new file mode 100644 index 0000000..64b6a85 --- /dev/null +++ b/apps/web/src/assistant/store.ts @@ -0,0 +1,199 @@ +import type { AppendMessage, Attachment, AttachmentAdapter, CompleteAttachment, PendingAttachment, ThreadMessageLike } from "@assistant-ui/react"; +import { modelConnectionInputSchema, persistedModelConnectionSchema, type HarnessEvent, type ImageAttachment, type PersistedModelConnection, type RedactedModelConnection, type SkillCatalogItem } from "@agent-studio/shared"; + +export type ConnectionState = "connecting" | "online" | "reconnecting" | "offline"; +export interface ClientState { + sessionId?: string; + messages: ThreadMessageLike[]; + events: HarnessEvent[]; + skills: SkillCatalogItem[]; + model: RedactedModelConnection; + isRunning: boolean; + connection: ConnectionState; + error?: string | undefined; +} + +interface LocalAttachment extends ImageAttachment { objectUrl: string } +type Listener = () => void; +const eventTypes: HarnessEvent["type"][] = ["session.started", "session.ended", "user.message", "assistant.delta", "assistant.completed", "skill.requested", "skill.loaded", "tool.requested", "tool.completed", "tool.failed", "approval.required", "approval.resolved", "attachment.uploaded", "attachment.rejected", "model.configured", "model.validation_failed", "run.started", "run.cancelled", "run.completed", "run.failed"]; +const modelStorageKey = "agent-studio-mini:model-connection:v1"; + +export class HarnessClientStore { + private state: ClientState = { messages: [], events: [], skills: [], model: { configured: false, source: "missing", keyConfigured: false }, isRunning: false, connection: "connecting" }; + private readonly listeners = new Set(); + private readonly seen = new Set(); + private readonly attachments = new Map(); + private source?: EventSource; + private lastSequence = 0; + private initializing: Promise | undefined; + + getSnapshot = (): ClientState => this.state; + subscribe = (listener: Listener): (() => void) => { this.listeners.add(listener); return () => this.listeners.delete(listener); }; + private update(patch: Partial): void { this.state = { ...this.state, ...patch }; for (const listener of this.listeners) listener(); } + + initialize(): Promise { + if (this.state.sessionId) return Promise.resolve(); + if (!this.initializing) { + this.initializing = this.initializeOnce().finally(() => { this.initializing = undefined; }); + } + return this.initializing; + } + + private async initializeOnce(): Promise { + try { + // Session readiness must not depend on the optional Skills catalog request. + const created = await api<{ sessionId: string }>("/api/sessions", { method: "POST" }); + this.update({ sessionId: created.sessionId, connection: "connecting", error: undefined }); + this.connect(); + await this.refreshModel(); + await this.restorePersistedModel(); + void this.refreshSkills(); + } catch (error) { + this.update({ connection: "offline", error: `Session 初始化失败:${messageOf(error)}` }); + } + } + + private async refreshSkills(): Promise { + try { + const result = await api<{ skills: SkillCatalogItem[] }>("/api/skills"); + this.update({ skills: result.skills }); + } catch (error) { + this.update({ error: `Skills Catalog 加载失败:${messageOf(error)}` }); + } + } + + private async requireSession(): Promise { + if (!this.state.sessionId) await this.initialize(); + if (!this.state.sessionId) { + throw new Error(this.state.error ?? "Session 初始化失败,请确认 Fastify 服务已启动后重试。"); + } + return this.state.sessionId; + } + + private connect(): void { + if (!this.state.sessionId) return; + this.source?.close(); + const cursor = this.lastSequence ? `?afterSequence=${this.lastSequence}` : ""; + const source = new EventSource(`/api/sessions/${this.state.sessionId}/events${cursor}`); + this.source = source; + source.onopen = () => this.update({ connection: "online", error: undefined }); + source.onerror = () => this.update({ connection: source.readyState === EventSource.CLOSED ? "offline" : "reconnecting" }); + for (const type of eventTypes) source.addEventListener(type, (raw) => this.reduce(JSON.parse((raw as MessageEvent).data) as HarnessEvent)); + source.addEventListener("stream.gap", () => { this.update({ error: "事件历史窗口已发生缺口;当前视图已从最早可用事件恢复。" }); }); + } + + reduce(event: HarnessEvent): void { + if (this.seen.has(event.eventId) || event.sequence <= this.lastSequence) return; + this.seen.add(event.eventId); + this.lastSequence = event.sequence; + const events = [...this.state.events, event].slice(-1_000); + let messages = this.state.messages; + let isRunning = this.state.isRunning; + if (event.type === "user.message") { + const attachmentItems = event.payload.attachmentIds.map((id) => this.attachments.get(id)).filter((item): item is LocalAttachment => Boolean(item)); + messages = [...messages, { id: event.payload.messageId, role: "user", createdAt: new Date(event.timestamp), content: [{ type: "text", text: event.payload.text }, ...attachmentItems.map((item) => ({ type: "image" as const, image: item.objectUrl, filename: item.name }))], attachments: attachmentItems.map((item) => ({ id: item.attachmentId, type: "image" as const, name: item.name, contentType: item.mediaType, status: { type: "complete" as const }, content: [{ type: "image" as const, image: item.objectUrl, filename: item.name }] })) }]; + } else if (event.type === "assistant.delta") { + messages = updateAssistant(messages, event.payload.messageId, event.timestamp, (parts) => appendText(parts, event.payload.delta), { type: "running" }, event.runId); + } else if (event.type === "assistant.completed") { + messages = updateAssistant(messages, event.payload.messageId, event.timestamp, (parts) => parts, { type: "complete", reason: "stop" }); + } else if (event.type === "tool.requested") { + const id = assistantIdForRun(messages, event.runId); + messages = updateAssistant(messages, id, event.timestamp, (parts) => upsertToolCall(parts, event.payload.toolCallId, event.payload.toolName, asArgs(event.payload.arguments)), { type: "running" }, event.runId); + } else if (event.type === "tool.completed" || event.type === "tool.failed") { + messages = updateTool(messages, event.payload.toolCallId, { result: event.type === "tool.completed" ? event.payload.result : { error: event.payload.error }, isError: event.type === "tool.failed" }); + } else if (event.type === "approval.required") { + messages = updateTool(messages, event.payload.toolCallId, { approval: { id: event.payload.approvalId } }); + } else if (event.type === "approval.resolved") { + const approval = event.payload.status === "approved" ? { id: event.payload.approvalId, approved: true } : event.payload.status === "denied" ? { id: event.payload.approvalId, approved: false, reason: "denied" } : { id: event.payload.approvalId, resolution: event.payload.status }; + messages = updateTool(messages, event.payload.toolCallId, { approval }); + } else if (event.type === "run.started") isRunning = true; + else if (event.type === "run.completed") isRunning = false; + else if (event.type === "run.cancelled") { isRunning = false; messages = finishLast(messages, { type: "incomplete", reason: "cancelled" }); } + else if (event.type === "run.failed") { isRunning = false; messages = finishLast(messages, { type: "incomplete", reason: "error", error: event.payload.message }); } + this.update({ events, messages, isRunning }); + } + + onNew = async (message: AppendMessage): Promise => { + const sessionId = await this.requireSession(); + const text = message.content.filter((part) => part.type === "text").map((part) => part.text).join("\n"); + const attachmentIds = (message.attachments ?? []).map((item) => item.id).filter((id) => this.attachments.has(id)); + if (attachmentIds.length > 0 && this.state.model.supportsImages !== true) throw new Error("当前模型不支持图片输入,请移除图片或切换 Vision 模型。"); + await api(`/api/sessions/${sessionId}/messages`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ text, attachmentIds }) }); + }; + onCancel = async (): Promise => { if (this.state.sessionId) await api(`/api/sessions/${this.state.sessionId}/cancel`, { method: "POST" }); }; + setMessages = (messages: readonly ThreadMessageLike[]): void => this.update({ messages: [...messages] }); + + async reset(): Promise { + const old = this.state.sessionId; + this.source?.close(); + if (old) await api(`/api/sessions/${old}`, { method: "DELETE" }); + for (const item of this.attachments.values()) URL.revokeObjectURL(item.objectUrl); + this.attachments.clear(); this.seen.clear(); this.lastSequence = 0; + const created = await api<{ sessionId: string }>("/api/sessions", { method: "POST" }); + this.state = { ...this.state, sessionId: created.sessionId, messages: [], events: [], model: { configured: false, source: "missing", keyConfigured: false }, isRunning: false, connection: "connecting", error: undefined }; + for (const listener of this.listeners) listener(); + await this.refreshModel(); await this.restorePersistedModel(); this.connect(); + } + async refreshModel(): Promise { if (!this.state.sessionId) return; const model = await api(`/api/sessions/${this.state.sessionId}/model-config`); this.update({ model }); } + async saveModel(input: unknown): Promise { const sessionId = await this.requireSession(); const parsed = modelConnectionInputSchema.parse(input); const model = await api(`/api/sessions/${sessionId}/model-config`, { method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify(parsed) }); persistModel(withoutCredential(parsed)); this.update({ model }); } + async clearCredentials(): Promise { const sessionId = await this.requireSession(); await api(`/api/sessions/${sessionId}/model-config/credentials`, { method: "DELETE" }); await this.refreshModel(); } + forgetPersistedModel(): void { removePersistedModel(); } + async testModel(): Promise { const sessionId = await this.requireSession(); const result = await api<{ message: string }>(`/api/sessions/${sessionId}/model-config/test`, { method: "POST" }); return result.message; } + async resolveApproval(id: string, decision: "approve" | "deny"): Promise { await api(`/api/approvals/${id}/${decision}`, { method: "POST" }); } + + readonly attachmentAdapter: AttachmentAdapter = { + accept: "image/jpeg,image/png,image/webp,.jpg,.jpeg,.png,.webp", + add: async ({ file }): Promise => { + const sessionId = await this.requireSession(); + const form = new FormData(); form.append("file", file, file.name); + const item = await api(`/api/sessions/${sessionId}/attachments`, { method: "POST", body: form }); + const objectUrl = URL.createObjectURL(file); + this.attachments.set(item.attachmentId, { ...item, objectUrl }); + return { id: item.attachmentId, type: "image", name: item.name, contentType: item.mediaType, file, status: { type: "requires-action", reason: "composer-send" }, content: [{ type: "image", image: objectUrl, filename: item.name }] }; + }, + remove: async (attachment: Attachment): Promise => { + const item = this.attachments.get(attachment.id); if (!item || !this.state.sessionId) return; + await api(`/api/sessions/${this.state.sessionId}/attachments/${attachment.id}`, { method: "DELETE" }); URL.revokeObjectURL(item.objectUrl); this.attachments.delete(attachment.id); + }, + send: async (attachment: PendingAttachment): Promise => { + const item = this.attachments.get(attachment.id); if (!item) throw new Error("Uploaded attachment is missing"); + return { id: item.attachmentId, type: "image", name: item.name, contentType: item.mediaType, status: { type: "complete" }, content: [{ type: "image", image: item.objectUrl, filename: item.name }] }; + }, + }; + + private async restorePersistedModel(): Promise { + if (!this.state.sessionId || this.state.model.configured) return; + const persisted = readPersistedModel(); + if (!persisted) return; + try { + const model = await api(`/api/sessions/${this.state.sessionId}/model-config`, { method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify(persisted) }); + this.update({ model }); + } catch { + removePersistedModel(); + this.update({ error: "本地保存的模型配置已失效,请在 Settings 中重新配置。" }); + } + } +} + +type ContentParts = Exclude; +function updateAssistant(messages: ThreadMessageLike[], id: string, timestamp: string, change: (parts: ContentParts) => ContentParts, status: ThreadMessageLike["status"], runId?: string): ThreadMessageLike[] { + const index = messages.findIndex((item) => item.id === id); + if (index < 0) return [...messages, { id, role: "assistant", createdAt: new Date(timestamp), content: change([]), status, metadata: { custom: runId ? { runId } : {} } }]; + return messages.map((item, current) => current === index ? { ...item, content: change(typeof item.content === "string" ? [{ type: "text", text: item.content }] : item.content), status } : item); +} +function appendText(parts: ContentParts, delta: string): ContentParts { const last = parts.at(-1); return last?.type === "text" ? [...parts.slice(0, -1), { ...last, text: last.text + delta }] : [...parts, { type: "text", text: delta }]; } +function upsertToolCall(parts: ContentParts, toolCallId: string, toolName: string, args: Record): ContentParts { + const index = parts.findIndex((part) => part.type === "tool-call" && part.toolCallId === toolCallId); + if (index < 0) return [...parts, { type: "tool-call", toolCallId, toolName, args }]; + return parts.map((part, current) => current === index && part.type === "tool-call" ? { ...part, toolName, args } : part); +} +function updateTool(messages: ThreadMessageLike[], toolCallId: string, patch: Record): ThreadMessageLike[] { return messages.map((message) => ({ ...message, content: typeof message.content === "string" ? message.content : message.content.map((part) => part.type === "tool-call" && part.toolCallId === toolCallId ? { ...part, ...patch } : part) })); } +function assistantIdForRun(messages: ThreadMessageLike[], runId?: string): string { const existing = [...messages].reverse().find((item) => item.role === "assistant" && item.metadata?.custom?.runId === runId); return existing?.id ?? `assistant_${runId ?? crypto.randomUUID()}`; } +function finishLast(messages: ThreadMessageLike[], status: ThreadMessageLike["status"]): ThreadMessageLike[] { const index = messages.findLastIndex((item) => item.role === "assistant"); return index < 0 ? messages : messages.map((item, current) => current === index ? { ...item, status } : item); } +function asArgs(value: unknown): Record { if (!value || typeof value !== "object" || Array.isArray(value)) return {}; return Object.fromEntries(Object.entries(value).filter((entry): entry is [string, string | number | boolean | null] => entry[1] === null || ["string", "number", "boolean"].includes(typeof entry[1]))); } +async function api>(url: string, init?: RequestInit): Promise { const response = await fetch(url, { ...init, headers: { accept: "application/json", ...init?.headers } }); const data = await response.json().catch(() => ({})) as { error?: { message?: string; code?: string } }; if (!response.ok) throw new Error(data.error?.message ?? `Request failed (${response.status})`); return data as T; } +function messageOf(error: unknown): string { return error instanceof Error ? error.message : "Unexpected error"; } +function withoutCredential(input: unknown): PersistedModelConnection { const value = modelConnectionInputSchema.parse(input); return persistedModelConnectionSchema.parse(Object.fromEntries(Object.entries(value).filter(([key]) => key !== "apiKey"))); } +function readPersistedModel(): PersistedModelConnection | undefined { if (typeof window === "undefined") return undefined; try { const raw = window.localStorage.getItem(modelStorageKey); if (!raw) return undefined; if (raw.length > 16_384) { removePersistedModel(); return undefined; } const parsed = persistedModelConnectionSchema.safeParse(JSON.parse(raw)); if (!parsed.success) { removePersistedModel(); return undefined; } return parsed.data; } catch { removePersistedModel(); return undefined; } } +function persistModel(input: PersistedModelConnection): void { if (typeof window === "undefined") return; try { window.localStorage.setItem(modelStorageKey, JSON.stringify(input)); } catch { /* Storage may be disabled or full; the active Session remains configured. */ } } +function removePersistedModel(): void { if (typeof window === "undefined") return; try { window.localStorage.removeItem(modelStorageKey); } catch { /* Storage may be unavailable. */ } } diff --git a/apps/web/src/components/AppErrorBoundary.tsx b/apps/web/src/components/AppErrorBoundary.tsx new file mode 100644 index 0000000..8c88287 --- /dev/null +++ b/apps/web/src/components/AppErrorBoundary.tsx @@ -0,0 +1,24 @@ +import { Component, type ReactNode } from "react"; + +interface Props { children: ReactNode } +interface State { failed: boolean } + +export class AppErrorBoundary extends Component { + override state: State = { failed: false }; + + static getDerivedStateFromError(): State { + return { failed: true }; + } + + override render(): ReactNode { + if (!this.state.failed) return this.props.children; + return
+
+ UI RECOVERY +

界面渲染出现异常

+

Agent Run 仍由服务端管理。重新加载页面会创建新的 Web Session,不会泄露模型密钥。

+ +
+
; + } +} diff --git a/apps/web/src/components/ApprovalDialog.tsx b/apps/web/src/components/ApprovalDialog.tsx new file mode 100644 index 0000000..10aaa42 --- /dev/null +++ b/apps/web/src/components/ApprovalDialog.tsx @@ -0,0 +1,11 @@ +import { useState } from "react"; +import * as Dialog from "@radix-ui/react-dialog"; +import { useHarnessState, useHarnessStore } from "../assistant/HarnessContext"; + +export function ApprovalDialog() { + const state = useHarnessState(); const store = useHarnessStore(); const [submitting, setSubmitting] = useState(false); + const pending = [...state.events].reverse().find((event) => event.type === "approval.required" && !state.events.some((candidate) => candidate.type === "approval.resolved" && candidate.payload.approvalId === event.payload.approvalId)); + if (!pending || pending.type !== "approval.required") return null; + const act = async (decision: "approve" | "deny") => { setSubmitting(true); try { await store.resolveApproval(pending.payload.approvalId, decision); } finally { setSubmitting(false); } }; + return event.preventDefault()} onPointerDownOutside={(event) => event.preventDefault()} aria-describedby="approval-risk">HUMAN APPROVAL REQUIRED允许保存报告草稿?Tool {pending.payload.toolName} 将向当前 Session 的固定文件 report.md 写入内容。审批不会放宽任何路径安全检查。
{JSON.stringify(pending.payload.arguments, null, 2).slice(0, 1500)}
; +} diff --git a/apps/web/src/components/ChatPanel.tsx b/apps/web/src/components/ChatPanel.tsx new file mode 100644 index 0000000..2ad6a32 --- /dev/null +++ b/apps/web/src/components/ChatPanel.tsx @@ -0,0 +1,48 @@ +import { useState } from "react"; +import { AttachmentPrimitive, ComposerPrimitive, MessagePrimitive, ThreadPrimitive, useAui, useAuiState } from "@assistant-ui/react"; +import { ImagePlus, Send, Square, X } from "lucide-react"; +import * as Dialog from "@radix-ui/react-dialog"; +import { ToolFallback } from "./ToolUIs"; +import { useHarnessState } from "../assistant/HarnessContext"; + +const demoPrompt = "请使用 ads-analysis Skill,分析账户 demo-account 最近 7 天数据,找出异常 Campaign,并输出账户总结、关键指标和 3 条优化建议。最后使用 report-writer Skill 生成一份报告草稿。"; + +function TextPart({ text }: { text: string }) { return
{text}
; } +function ImagePart({ image, filename }: { image: string; filename?: string }) { return ; } +const partComponents = { Text: TextPart, Image: ImagePart, tools: { Fallback: ToolFallback } }; +function UserMessage() { return
You
{() => null}
; } +function AssistantMessage() { return
Agent
; } + +function ImagePreview({ src, name }: { src: string; name: string }) { + return {name}{name}/; +} +function ComposerAttachment() { + const preview = useAuiState((state) => { + const part = state.attachment.content?.find((item) => item.type === "image"); + return part?.type === "image" ? part.image : undefined; + }); + return {preview ? : }; +} + +export function ChatPanel({ openSettings }: { openSettings: () => void }) { + const state = useHarnessState(); + const aui = useAui(); + const [dragging, setDragging] = useState(false); + const modelReady = state.model.configured && state.model.keyConfigured; + const fillDemo = () => aui.composer().setText(demoPrompt); + return
+
{state.isRunning ? "Agent 正在运行" : "准备就绪"}{state.connection}
+ {!modelReady && } + {state.error &&
{state.error}
} +
π

Agent Loop 从这里开始

选择 Demo Prompt,观察 Skill 按需加载、Tool 事件和人工审批。

+ + setDragging(true)} onDragLeave={() => setDragging(false)} onDrop={() => setDragging(false)}> + + +
最多 4 张 · 5 MiB/张{state.isRunning ? 停止 : 发送}
+
+
+
+
{state.isRunning ? "" : state.messages.at(-1)?.role === "assistant" ? "Assistant 回复已完成" : ""}
+
; +} diff --git a/apps/web/src/components/SettingsDialog.tsx b/apps/web/src/components/SettingsDialog.tsx new file mode 100644 index 0000000..fa2d1d4 --- /dev/null +++ b/apps/web/src/components/SettingsDialog.tsx @@ -0,0 +1,21 @@ +import { useState, type FormEvent } from "react"; +import * as Dialog from "@radix-ui/react-dialog"; +import { Eye, EyeOff, X } from "lucide-react"; +import { useHarnessState, useHarnessStore } from "../assistant/HarnessContext"; + +const presets = { + openai: { providerId: "openai", api: "openai-responses", baseUrl: "https://api.openai.com/v1", modelId: "gpt-5.4", supportsImages: true }, + anthropic: { providerId: "anthropic", api: "anthropic-messages", baseUrl: "https://api.anthropic.com", modelId: "claude-sonnet-4-6", supportsImages: true }, + compatible: { providerId: "openai-compatible", api: "openai-completions", baseUrl: "https://example.com/v1", modelId: "your-model-id", supportsImages: false }, +} as const; +type ApiType = "openai-responses" | "openai-completions" | "anthropic-messages"; +interface FormState { providerId: string; api: ApiType; baseUrl: string; apiKey: string; modelId: string; displayName: string; supportsImages: boolean } + +export function SettingsDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) { + const store = useHarnessStore(); const state = useHarnessState(); const [showKey, setShowKey] = useState(false); const [busy, setBusy] = useState(false); const [notice, setNotice] = useState(""); + const [form, setForm] = useState(() => ({ providerId: state.model.providerId ?? "openai", api: state.model.api ?? "openai-responses", baseUrl: state.model.baseUrl ?? "https://api.openai.com/v1", apiKey: "", modelId: state.model.modelId ?? "gpt-5.4", displayName: state.model.displayName ?? "", supportsImages: state.model.supportsImages ?? true })); + const applyPreset = (name: keyof typeof presets) => setForm((value) => ({ ...value, ...presets[name], apiKey: "", displayName: "" })); + const submit = async (event: FormEvent) => { event.preventDefault(); setBusy(true); setNotice(""); try { await store.saveModel({ providerId: form.providerId, api: form.api, modelId: form.modelId, supportsImages: form.supportsImages, ...(form.apiKey ? { apiKey: form.apiKey } : {}), ...(form.baseUrl ? { baseUrl: form.baseUrl } : {}), ...(form.displayName ? { displayName: form.displayName } : {}) }); setForm((value) => ({ ...value, apiKey: "" })); setNotice(store.getSnapshot().model.credentialStorage === "encrypted" ? "模型配置已保存;API Key 已由服务器加密持久化。" : "模型配置已保存;当前服务器未启用加密凭证持久化,API Key 仅保存在 Session 内存。"); } catch (error) { setNotice(error instanceof Error ? error.message : "保存失败"); } finally { setBusy(false); } }; + const storageLabel = state.model.credentialStorage === "encrypted" ? "服务端加密" : state.model.credentialStorage === "environment" ? "环境变量" : state.model.credentialStorage === "memory" ? "Session 内存" : "无"; + return { if (!busy) onOpenChange(next); }}>
SESSION RUNTIME模型连接服务器优先加载 .env 默认配置;前端覆盖的非敏感字段保存在浏览器本地。API Key 不进入 Web Storage,由服务器使用 AES-256-GCM 加密持久化。
void submit(event)}>
{notice &&

{notice}

}
来源:{state.model.source}密钥:{state.model.keyConfigured ? state.model.keyHint ?? "已配置" : "未配置"}存储:{storageLabel}
; +} diff --git a/apps/web/src/components/SkillsPanel.tsx b/apps/web/src/components/SkillsPanel.tsx new file mode 100644 index 0000000..98af3d1 --- /dev/null +++ b/apps/web/src/components/SkillsPanel.tsx @@ -0,0 +1,7 @@ +import { useHarnessState } from "../assistant/HarnessContext"; + +export function SkillsPanel() { + const { skills, events, connection } = useHarnessState(); + const active = new Set(events.filter((event) => event.type === "skill.requested" || event.type === "skill.loaded").map((event) => event.payload.name)); + return ; +} diff --git a/apps/web/src/components/TimelinePanel.tsx b/apps/web/src/components/TimelinePanel.tsx new file mode 100644 index 0000000..3b1c647 --- /dev/null +++ b/apps/web/src/components/TimelinePanel.tsx @@ -0,0 +1,12 @@ +import { useEffect, useRef, useState } from "react"; +import { useHarnessState } from "../assistant/HarnessContext"; +import type { HarnessEvent } from "@agent-studio/shared"; + +type Filter = "all" | "run" | "skill" | "tool" | "approval" | "model"; +export function TimelinePanel() { + const { events } = useHarnessState(); const [filter, setFilter] = useState("all"); const viewport = useRef(null); const atBottom = useRef(true); + const visible = events.filter((event) => filter === "all" || event.type.startsWith(`${filter}.`) || (filter === "tool" && event.type.startsWith("attachment."))); + useEffect(() => { if (atBottom.current) viewport.current?.scrollTo({ top: viewport.current.scrollHeight }); }, [visible.length]); + return ; +} +function EventRow({ event }: { event: HarnessEvent }) { const [open, setOpen] = useState(false); return
{open &&
{JSON.stringify(event.payload, null, 2)}
}
; } diff --git a/apps/web/src/components/ToolUIs.tsx b/apps/web/src/components/ToolUIs.tsx new file mode 100644 index 0000000..3fd8151 --- /dev/null +++ b/apps/web/src/components/ToolUIs.tsx @@ -0,0 +1,48 @@ +import { useState } from "react"; +import { AuiConfig, AuiProvider, defineToolkit, Tools, useAui, type Toolkit, type ToolCallMessagePartProps } from "@assistant-ui/react"; +import type { PropsWithChildren } from "react"; +import { z } from "zod"; +import { useHarnessStore } from "../assistant/HarnessContext"; + +type JsonObject = Record; + +function stateLabel(props: ToolCallMessagePartProps): string { + if (props.isError) return "执行失败"; + if (props.approval?.resolution) return props.approval.resolution === "expired" ? "审批超时" : "已取消"; + if (props.approval?.approved === false) return "已拒绝"; + if (props.approval?.approved === true && props.result === undefined) return "已批准 · 执行中"; + if (props.approval && props.approval.approved === undefined) return "等待审批"; + if (props.result !== undefined) return "完成"; + return "执行中"; +} + +function ApprovalActions({ approval }: Pick, "approval">) { + const store = useHarnessStore(); + const [submitting, setSubmitting] = useState(false); + if (!approval || approval.approved !== undefined || approval.resolution) return null; + const act = async (decision: "approve" | "deny") => { setSubmitting(true); try { await store.resolveApproval(approval.id, decision); } finally { setSubmitting(false); } }; + return
; +} + +export function MockAdsMetricsUI(props: ToolCallMessagePartProps) { + const result = asRecord(asRecord(props.result).details ?? props.result); + return
mock_ads_metrics{stateLabel(props)}

账户 {String(props.args?.accountId ?? "—")} · {String(props.args?.days ?? "—")} 天

{Object.keys(result).length > 0 &&
}
; +} +export function SaveReportDraftUI(props: ToolCallMessagePartProps) { + const content = typeof props.args?.content === "string" ? props.args.content : ""; + return
save_report_draft{stateLabel(props)}

将写入当前 Session 的固定文件 report.md

{content.slice(0, 180)}{content.length > 180 ? "…" : ""}

{props.approval && }
; +} +export function ToolFallback(props: ToolCallMessagePartProps) { return
{props.toolName}{stateLabel(props)}
{JSON.stringify(props.args ?? {}, null, 2)}
; } +function Metric({ label, value, suffix = "" }: { label: string; value: unknown; suffix?: string }) { return
{label}{typeof value === "number" ? value.toLocaleString() : "—"}{suffix}
; } +function asRecord(value: unknown): JsonObject { return value && typeof value === "object" && !Array.isArray(value) ? value as JsonObject : {}; } + +const authoredToolkit = defineToolkit({ + mock_ads_metrics: { description: "显示服务端广告指标工具结果", parameters: z.object({ accountId: z.string(), days: z.number() }), render: MockAdsMetricsUI }, + save_report_draft: { description: "显示服务端报告保存审批与结果", parameters: z.object({ content: z.string(), title: z.string().optional() }), render: SaveReportDraftUI }, +}); +const harnessToolkit = authoredToolkit as Toolkit; +export function HarnessToolsProvider({ children }: PropsWithChildren) { + const parent = useAui(); + const config = AuiConfig({ tools: Tools({ toolkit: harnessToolkit }) }); + return {children}; +} diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx new file mode 100644 index 0000000..4288acc --- /dev/null +++ b/apps/web/src/main.tsx @@ -0,0 +1,11 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { App } from "./App"; +import { HarnessRuntime } from "./assistant/HarnessRuntime"; +import { HarnessClientStore } from "./assistant/store"; +import { AppErrorBoundary } from "./components/AppErrorBoundary"; +import "./styles/app.css"; + +const store = new HarnessClientStore(); +void store.initialize(); +createRoot(document.getElementById("root")!).render(); diff --git a/apps/web/src/styles/app.css b/apps/web/src/styles/app.css new file mode 100644 index 0000000..7f296f8 --- /dev/null +++ b/apps/web/src/styles/app.css @@ -0,0 +1,106 @@ +:root { + color-scheme: dark; + --black: #111111; --cyan: #25f4ee; --pink: #fe2c55; + --surface-0: #0c0d0f; --surface-1: #131519; --surface-2: #1a1d22; --surface-3: #23272e; + --border: #2c3139; --border-strong: #3d444f; --text: #f3f5f7; --muted: #929aa6; + --accent: var(--cyan); --danger: var(--pink); --focus: #a8fffc; + --radius: 10px; --shadow: 0 18px 50px rgb(0 0 0 / .34); + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + background: var(--surface-0); color: var(--text); font-synthesis: none; +} +* { box-sizing: border-box; } +html, body, #root { width: 100%; min-width: 320px; height: 100%; margin: 0; overflow: hidden; } +body { background: radial-gradient(circle at 50% -20%, #183136 0, var(--surface-0) 34%); } +button, input, select, textarea { font: inherit; } +button { color: inherit; } +button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; } +button:disabled { opacity: .46; cursor: not-allowed; } +.app-shell { display: grid; grid-template-rows: 64px 1fr; height: 100%; } +.topbar { min-width: 0; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 0 16px; border-bottom: 1px solid var(--border); background: rgb(12 13 15 / .9); backdrop-filter: blur(14px); z-index: 10; } +.brand, .session-summary, .brand > div, .chat-toolbar > div { display: flex; align-items: center; gap: 10px; min-width: 0; } +.brand > div { align-items: flex-start; flex-direction: column; gap: 1px; } +.brand strong { letter-spacing: -.02em; } +.brand small { color: var(--muted); font-size: 11px; } +.brand-mark { width: 36px; height: 36px; display: grid; place-items: center; border: 1px solid #267a7a; border-radius: 10px; color: var(--cyan); background: #132426; box-shadow: inset 0 0 16px rgb(37 244 238 / .08); } +.session-summary { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: var(--muted); } +.run-dot { width: 8px; height: 8px; border-radius: 50%; background: #59616d; } +.run-dot.running { background: var(--cyan); box-shadow: 0 0 12px var(--cyan); animation: pulse 1.4s ease-in-out infinite; } +.model-badge, .vision-badge { border: 1px solid var(--border-strong); border-radius: 999px; padding: 5px 9px; color: var(--text); background: var(--surface-2); } +.vision-badge { color: var(--cyan); border-color: #267a7a; } +.settings-button, .send-button, .text-button, .accent, .danger, .dialog-actions button, .presets button { min-height: 36px; border: 1px solid var(--border-strong); border-radius: 8px; background: var(--surface-2); padding: 0 12px; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; gap: 7px; } +.settings-button svg, .icon-button svg { width: 17px; height: 17px; } +.settings-button:hover, .text-button:hover, .dialog-actions button:hover, .presets button:hover { background: var(--surface-3); border-color: #59616d; } +.icon-button { width: 40px; height: 40px; display: inline-grid; place-items: center; border: 0; border-radius: 8px; background: transparent; cursor: pointer; } +.icon-button:hover { background: var(--surface-3); } +.workspace-grid { min-height: 0; display: grid; grid-template-columns: minmax(220px, 280px) minmax(420px, 1fr) minmax(260px, 340px); } +.mobile-panel { min-height: 0; min-width: 0; } +.side-panel { height: 100%; min-height: 0; display: flex; flex-direction: column; background: rgb(19 21 25 / .88); } +.skills-panel { border-right: 1px solid var(--border); } +.timeline-panel { border-left: 1px solid var(--border); } +.panel-heading { padding: 20px 16px 14px; border-bottom: 1px solid var(--border); } +.panel-heading h2 { margin: 4px 0 7px; font-size: 16px; } +.panel-heading p { margin: 0; color: var(--muted); line-height: 1.45; font-size: 12px; } +.eyebrow { font-size: 10px; font-weight: 800; letter-spacing: .13em; color: var(--cyan); } +.skill-list, .timeline { min-height: 0; overflow: auto; padding: 8px; } +.skill-item { padding: 13px 10px; border-left: 2px solid transparent; border-bottom: 1px solid var(--border); } +.skill-item.active { border-left-color: var(--cyan); background: linear-gradient(90deg, rgb(37 244 238 / .08), transparent); } +.skill-title { display: flex; justify-content: space-between; align-items: center; gap: 8px; } +.skill-title strong { font-family: ui-monospace, SFMono-Regular, monospace; font-size: 13px; } +.skill-item p { margin: 7px 0; color: #cbd0d6; line-height: 1.45; font-size: 12px; } +.skill-item small { color: var(--muted); word-break: break-word; } +.diagnostic { border-radius: 999px; padding: 2px 6px; font-size: 9px; text-transform: uppercase; border: 1px solid; } +.diagnostic.valid { color: var(--cyan); border-color: #267a7a; }.diagnostic.warning { color: #ffd166; border-color: #806c2d; }.diagnostic.error { color: var(--pink); border-color: #8d2940; } +.diagnostic-message { color: #ffb4c4; font-size: 11px; margin-top: 8px; } +.panel-state { padding: 20px; color: var(--muted); font-size: 13px; text-align: center; } +.chat-panel { height: 100%; min-height: 0; display: grid; grid-template-rows: auto auto auto 1fr; background: var(--surface-0); } +.chat-toolbar { min-height: 50px; display: flex; align-items: center; justify-content: space-between; padding: 8px 16px; border-bottom: 1px solid var(--border); font-size: 12px; } +.connection { text-transform: capitalize; color: var(--muted); }.connection.online { color: var(--cyan); }.connection.reconnecting { color: #ffd166; }.connection.offline { color: var(--pink); } +.text-button { min-height: 32px; color: var(--cyan); } +.config-banner, .error-banner { margin: 10px 16px 0; border-radius: 8px; padding: 10px 12px; text-align: left; } +.config-banner { border: 1px solid #756326; background: #28230f; color: #ffe395; cursor: pointer; }.error-banner { border: 1px solid #7e293b; background: #2b1118; color: #ffbdca; } +.thread-root, .thread-viewport { min-height: 0; height: 100%; } +.thread-viewport { overflow-y: auto; padding: 16px 0 0; scroll-padding-bottom: 170px; } +.empty-state { min-height: 52vh; display: grid; place-items: center; align-content: center; text-align: center; padding: 32px; color: var(--muted); } +.empty-state h2 { margin: 16px 0 6px; color: var(--text); font-size: 22px; }.empty-state p { max-width: 460px; line-height: 1.55; } +.orb { width: 70px; height: 70px; display: grid; place-items: center; border-radius: 50%; color: var(--cyan); font: 700 28px Georgia, serif; background: radial-gradient(circle at 35% 30%, #27535a, #121c20 62%); border: 1px solid #2e6f74; box-shadow: 0 0 50px rgb(37 244 238 / .12); } +.message { width: min(760px, calc(100% - 32px)); margin: 0 auto 22px; } +.message .role { margin-bottom: 7px; color: var(--muted); font: 700 10px ui-monospace, monospace; letter-spacing: .12em; text-transform: uppercase; } +.message.user { display: grid; justify-items: end; }.message.user .role { text-align: right; } +.message-text { white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.62; } +.user .message-text { max-width: min(86%, 620px); padding: 11px 14px; border-radius: 14px 14px 3px 14px; background: #20252b; border: 1px solid #333a44; } +.assistant .message-text { color: #e7eaee; } +.image-preview { border: 1px solid var(--border-strong); padding: 0; border-radius: 9px; background: var(--surface-2); overflow: hidden; cursor: zoom-in; margin: 8px 5px 0 0; } +.image-preview img { display: block; width: 160px; height: 110px; object-fit: cover; } +.tool-card { margin: 12px 0; border: 1px solid var(--border-strong); border-radius: var(--radius); background: var(--surface-1); overflow: hidden; } +.tool-card.risk { border-color: #713044; box-shadow: inset 3px 0 0 var(--pink); } +.tool-card header { padding: 10px 12px; display: flex; justify-content: space-between; background: var(--surface-2); font: 600 12px ui-monospace, monospace; } +.tool-card .status { color: var(--cyan); }.tool-card.risk .status { color: #ff91a8; }.tool-card p, .tool-card pre { margin: 0; padding: 10px 12px; font-size: 12px; line-height: 1.5; }.tool-card pre { overflow: auto; color: #cbd0d6; }.tool-card .summary { max-height: 95px; overflow: hidden; color: var(--muted); } +.metrics { display: grid; grid-template-columns: repeat(4, 1fr); border-top: 1px solid var(--border); }.metrics div { padding: 10px; border-right: 1px solid var(--border); }.metrics span, .metrics strong { display: block; }.metrics span { color: var(--muted); font-size: 10px; }.metrics strong { margin-top: 4px; font-size: 14px; } +.approval-actions { display: flex; justify-content: flex-end; gap: 8px; padding: 10px 12px; border-top: 1px solid var(--border); } +.accent { color: #071011; background: var(--cyan); border-color: var(--cyan); font-weight: 750; }.accent:hover { background: #66fffa !important; }.danger { color: #ffb3c3; border-color: #7e2d40; background: #291319; }.danger:hover { border-color: var(--pink) !important; background: #36151e !important; } +.composer-footer { position: sticky; bottom: 0; padding: 16px; background: linear-gradient(transparent, var(--surface-0) 18%); } +.composer { width: min(780px, 100%); margin: 0 auto; border: 1px solid var(--border-strong); border-radius: 14px; background: var(--surface-1); box-shadow: var(--shadow); overflow: hidden; } +.composer.dragging { border-color: var(--cyan); box-shadow: 0 0 0 3px rgb(37 244 238 / .12); } +.dropzone { padding: 8px; } +.composer-input { display: block; width: 100%; min-height: 62px; max-height: 160px; resize: none; border: 0; padding: 10px; background: transparent; color: var(--text); outline: none !important; } +.composer-actions { display: flex; align-items: center; gap: 5px; }.composer-hint { flex: 1; color: var(--muted); font-size: 10px; }.send-button { background: var(--cyan); color: #061112; border-color: var(--cyan); font-weight: 750; }.send-button.stop { background: var(--surface-2); color: #ff9caf; border-color: #7e2d40; } +.composer-attachment { width: 92px; min-height: 76px; position: relative; display: inline-grid; place-items: center; margin: 4px; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; background: var(--surface-2); font-size: 10px; } +.composer-attachment img { width: 100%; height: 54px; object-fit: cover; }.remove-attachment { position: absolute; top: 3px; right: 3px; width: 25px; height: 25px; border: 0; border-radius: 50%; display: grid; place-items: center; background: rgb(0 0 0 / .72); cursor: pointer; } +.filters { padding: 8px; display: flex; gap: 5px; flex-wrap: wrap; border-bottom: 1px solid var(--border); }.filters button { min-height: 29px; padding: 0 8px; border-radius: 6px; border: 1px solid var(--border); background: transparent; color: var(--muted); font-size: 10px; cursor: pointer; }.filters button.selected { color: var(--cyan); border-color: #267a7a; background: #122326; } +.event-row { position: relative; border-left: 1px solid var(--border-strong); padding: 0 0 8px 8px; }.event-row.risk { border-left-color: var(--pink); }.event-row > button { width: 100%; min-height: 44px; display: grid; grid-template-columns: 35px 1fr; gap: 6px; align-items: center; text-align: left; border: 0; background: transparent; border-radius: 7px; cursor: pointer; }.event-row > button:hover { background: var(--surface-2); }.event-row .sequence { color: var(--muted); font: 10px ui-monospace, monospace; }.event-row strong, .event-row small { display: block; }.event-row strong { font: 600 11px ui-monospace, monospace; }.event-row small { margin-top: 3px; color: var(--muted); font-size: 9px; }.event-row pre { margin: 3px 0 8px 36px; max-height: 200px; overflow: auto; padding: 8px; border-radius: 6px; background: #0b0c0e; color: #bac1ca; font-size: 9px; white-space: pre-wrap; } +.dialog-overlay { position: fixed; inset: 0; z-index: 30; background: rgb(2 3 4 / .72); backdrop-filter: blur(5px); animation: fade .15s ease; } +.settings-dialog, .approval-dialog, .image-dialog { position: fixed; z-index: 31; left: 50%; top: 50%; transform: translate(-50%, -50%); width: min(620px, calc(100vw - 32px)); max-height: calc(100vh - 32px); overflow: auto; border: 1px solid var(--border-strong); border-radius: 14px; background: #15171b; box-shadow: 0 30px 100px rgb(0 0 0 / .65); padding: 20px; } +.dialog-header { display: flex; justify-content: space-between; gap: 16px; }.dialog-header h2, .approval-dialog h2 { margin: 5px 0 7px; }.dialog-header p, .approval-dialog > p { margin: 0; color: var(--muted); font-size: 12px; line-height: 1.5; } +.presets { display: flex; gap: 8px; margin: 18px 0 14px; }.presets button { flex: 1; font-size: 12px; } +.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }.form-grid label { display: grid; gap: 6px; color: #c2c7ce; font-size: 11px; }.form-grid input, .form-grid select { width: 100%; min-height: 42px; border: 1px solid var(--border-strong); border-radius: 7px; padding: 0 10px; color: var(--text); background: #101216; }.span-2 { grid-column: span 2 !important; }.password-field { position: relative; }.password-field input { padding-right: 48px; }.password-field .icon-button { position: absolute; right: 1px; top: 1px; }.check { grid-template-columns: auto 1fr !important; align-items: center; min-height: 40px; }.check input { width: 18px; min-height: 18px; accent-color: var(--cyan); } +.notice { padding: 10px; border-radius: 7px; background: var(--surface-2); font-size: 12px; }.config-status { display: flex; gap: 16px; color: var(--muted); font-size: 11px; margin: 12px 0; }.dialog-actions { display: flex; justify-content: flex-end; gap: 9px; padding-top: 14px; border-top: 1px solid var(--border); }.dialog-actions .danger { margin-right: auto; } +.approval-dialog { width: min(500px, calc(100vw - 32px)); border-color: #8b3045; }.approval-dialog pre { max-height: 200px; overflow: auto; margin: 16px 0; padding: 10px; border-radius: 8px; background: #0c0d0f; color: #cbd0d6; font-size: 10px; white-space: pre-wrap; }.risk-text { color: var(--pink); } +.image-dialog { width: min(900px, calc(100vw - 32px)); padding: 12px; }.image-dialog h2 { padding: 0 36px 8px 5px; margin: 0; font-size: 14px; }.image-dialog img { display: block; max-width: 100%; max-height: calc(100vh - 100px); margin: auto; }.image-dialog .icon-button { position: absolute; top: 4px; right: 4px; } +.sr-live { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); }.mobile-tabs { display: none; } +.fatal-fallback { min-height: 100vh; display: grid; place-items: center; padding: 24px; background: var(--surface-0); color: var(--text); }.fatal-fallback > div { width: min(520px, 100%); border: 1px solid #8b3045; border-radius: 14px; padding: 24px; background: var(--surface-1); box-shadow: var(--shadow); }.fatal-fallback h1 { margin: 8px 0; font-size: 22px; }.fatal-fallback p { margin: 0 0 18px; color: var(--muted); line-height: 1.6; } +@keyframes pulse { 50% { opacity: .45; transform: scale(.8); } } @keyframes fade { from { opacity: 0; } } +@media (max-width: 1050px) { .workspace-grid { grid-template-columns: 230px minmax(380px, 1fr); }.workspace-grid > .mobile-panel:last-child { display: none; }.session-summary > span:nth-child(2) { display: none; } } +@media (max-width: 720px) { + html, body, #root { overflow: hidden; }.app-shell { grid-template-rows: 58px 44px 1fr; }.brand small, .model-badge, .vision-badge, .session-summary > .run-dot { display: none; }.topbar { padding: 0 10px; }.settings-button { width: 42px; padding: 0; font-size: 0; }.settings-button svg { width: 18px; }.mobile-tabs { display: grid; grid-template-columns: repeat(3, 1fr); border-bottom: 1px solid var(--border); background: var(--surface-1); }.mobile-tabs button { border: 0; background: transparent; color: var(--muted); text-transform: capitalize; }.mobile-tabs button.active { color: var(--cyan); box-shadow: inset 0 -2px var(--cyan); }.workspace-grid { display: block; min-height: 0; }.mobile-panel { display: none !important; height: 100%; }.mobile-panel.shown { display: block !important; }.side-panel { border: 0; }.chat-toolbar { padding: 6px 10px; }.text-button { padding: 0 8px; }.composer-footer { padding: 10px; }.message { width: calc(100% - 20px); }.metrics { grid-template-columns: repeat(2, 1fr); }.form-grid { grid-template-columns: 1fr; }.span-2 { grid-column: span 1 !important; }.presets { overflow-x: auto; }.presets button { flex: 0 0 auto; }.dialog-actions { flex-wrap: wrap; }.dialog-actions button { flex: 1; }.dialog-actions .danger { margin: 0; }.composer-hint { display: none; } +} +@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; scroll-behavior: auto !important; } } diff --git a/apps/web/tests/store.test.ts b/apps/web/tests/store.test.ts new file mode 100644 index 0000000..5bd2978 --- /dev/null +++ b/apps/web/tests/store.test.ts @@ -0,0 +1,48 @@ +// @vitest-environment jsdom +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { HarnessEvent } from "@agent-studio/shared"; +import { HarnessClientStore } from "../src/assistant/store"; + +function event(sequence: number, type: T, payload: Extract["payload"], runId = "run_1"): HarnessEvent { return { eventId: `evt_${sequence}`, sessionId: "ses_1", runId, timestamp: new Date().toISOString(), sequence, type, payload } as HarnessEvent; } + +afterEach(() => { localStorage.clear(); sessionStorage.clear(); vi.unstubAllGlobals(); vi.restoreAllMocks(); }); + +describe("HarnessClientStore reducer", () => { + it("merges streaming deltas into one assistant text part", () => { const store = new HarnessClientStore(); store.reduce(event(1, "assistant.delta", { messageId: "msg_a", delta: "你" })); store.reduce(event(2, "assistant.delta", { messageId: "msg_a", delta: "好" })); expect(store.getSnapshot().messages[0]?.content).toEqual([{ type: "text", text: "你好" }]); }); + it("de-duplicates replayed events by id and sequence", () => { const store = new HarnessClientStore(); const value = event(1, "run.started", { modelId: "fake" }); store.reduce(value); store.reduce(value); expect(store.getSnapshot().events).toHaveLength(1); }); + it("maps tool and approval state by toolCallId", () => { const store = new HarnessClientStore(); store.reduce(event(1, "tool.requested", { toolCallId: "t1", toolName: "save_report_draft", arguments: { content: "x" } })); store.reduce(event(2, "approval.required", { approvalId: "a1", sessionId: "ses_1", runId: "run_1", toolCallId: "t1", toolName: "save_report_draft", arguments: {}, riskLevel: "medium", createdAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 1000).toISOString(), status: "pending" })); store.reduce(event(3, "approval.resolved", { approvalId: "a1", toolCallId: "t1", status: "approved", resolvedAt: new Date().toISOString() })); const content = store.getSnapshot().messages[0]?.content; expect(typeof content === "string" ? undefined : content?.[0]).toMatchObject({ type: "tool-call", toolCallId: "t1", approval: { approved: true } }); }); + it("upserts duplicate tool requests by toolCallId", () => { const store = new HarnessClientStore(); store.reduce(event(1, "tool.requested", { toolCallId: "t1", toolName: "read", arguments: { path: "" } })); store.reduce(event(2, "tool.requested", { toolCallId: "t1", toolName: "read", arguments: { path: ".agents/skills/ads-analysis/SKILL.md" } })); const content = store.getSnapshot().messages[0]?.content; expect(typeof content === "string" ? [] : content?.filter((part) => part.type === "tool-call")).toHaveLength(1); expect(typeof content === "string" ? undefined : content?.[0]).toMatchObject({ toolCallId: "t1", args: { path: ".agents/skills/ads-analysis/SKILL.md" } }); }); + it("maps cancelled and failed runs to incomplete status", () => { const store = new HarnessClientStore(); store.reduce(event(1, "assistant.delta", { messageId: "m", delta: "x" })); store.reduce(event(2, "run.cancelled", { reason: "user" })); expect(store.getSnapshot().messages[0]?.status).toMatchObject({ type: "incomplete", reason: "cancelled" }); }); + it("blocks image messages locally for text-only models", async () => { const store = new HarnessClientStore(); const snapshot = store.getSnapshot(); snapshot.sessionId = "ses_1"; snapshot.model = { configured: true, source: "session", providerId: "x", modelId: "x", supportsImages: false, keyConfigured: true }; vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true, json: async () => ({ attachmentId: "att_uploaded", sessionId: "ses_1", name: "x.png", mediaType: "image/png", byteSize: 3, width: 1, height: 1, status: "ready" }) })); vi.stubGlobal("URL", { ...URL, createObjectURL: () => "blob:x", revokeObjectURL: vi.fn() }); const pending = await store.attachmentAdapter.add({ file: new File(["png"], "x.png", { type: "image/png" }) }); if (!("id" in pending)) throw new Error("unexpected generator"); await expect(store.onNew({ id: "ignored", role: "user", parentId: null, sourceId: null, createdAt: new Date(), content: [{ type: "text", text: "x" }], attachments: [{ id: pending.id, type: "image", name: "x", status: { type: "complete" }, content: [{ type: "image", image: "blob:x" }] }], metadata: { custom: {} }, runConfig: undefined })).rejects.toThrow("不支持图片"); expect(fetch).toHaveBeenCalledTimes(1); vi.unstubAllGlobals(); }); + it("persists model metadata without the API key", async () => { const store = new HarnessClientStore(); store.getSnapshot().sessionId = "ses_1"; vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true, json: async () => ({ configured: true, source: "session", providerId: "openai", api: "openai-responses", modelId: "gpt", supportsImages: true, keyConfigured: true, keyHint: "••••cret" }) })); await store.saveModel({ providerId: "openai", api: "openai-responses", baseUrl: "https://api.openai.com/v1", apiKey: "top-secret", modelId: "gpt", supportsImages: true }); const raw = localStorage.getItem("agent-studio-mini:model-connection:v1"); expect(raw).not.toContain("top-secret"); expect(JSON.parse(raw!)).toEqual({ providerId: "openai", api: "openai-responses", baseUrl: "https://api.openai.com/v1", modelId: "gpt", supportsImages: true }); }); + it("restores saved metadata into a new Session without credentials", async () => { localStorage.setItem("agent-studio-mini:model-connection:v1", JSON.stringify({ providerId: "saved", api: "openai-completions", baseUrl: "https://example.com/v1", modelId: "saved-model", supportsImages: false })); const requests: Array<{ url: string; init?: RequestInit }> = []; vi.stubGlobal("fetch", vi.fn(async (input: string | URL | Request, init?: RequestInit) => { const url = String(input); requests.push({ url, init }); const body = url === "/api/skills" ? { skills: [] } : url === "/api/sessions" ? { sessionId: "ses_new" } : init?.method === "PUT" ? { configured: true, source: "session", providerId: "saved", api: "openai-completions", baseUrl: "https://example.com/v1", modelId: "saved-model", supportsImages: false, keyConfigured: false } : { configured: false, source: "missing", keyConfigured: false }; return { ok: true, json: async () => body }; })); class FakeEventSource { static readonly CLOSED = 2; readonly readyState = 1; onopen: (() => void) | null = null; onerror: (() => void) | null = null; addEventListener(): void {} close(): void {} } vi.stubGlobal("EventSource", FakeEventSource); const store = new HarnessClientStore(); await store.initialize(); expect(store.getSnapshot().model).toMatchObject({ modelId: "saved-model", keyConfigured: false }); const restored = requests.find((item) => item.init?.method === "PUT"); expect(restored?.init?.body).not.toContain("apiKey"); }); + it("drops corrupted persisted configuration without breaking initialization", async () => { localStorage.setItem("agent-studio-mini:model-connection:v1", "not-json"); vi.stubGlobal("fetch", vi.fn(async (input: string | URL | Request) => { const url = String(input); const body = url === "/api/skills" ? { skills: [] } : url === "/api/sessions" ? { sessionId: "ses_new" } : { configured: false, source: "missing", keyConfigured: false }; return { ok: true, json: async () => body }; })); class FakeEventSource { static readonly CLOSED = 2; readonly readyState = 1; onopen: (() => void) | null = null; onerror: (() => void) | null = null; addEventListener(): void {} close(): void {} } vi.stubGlobal("EventSource", FakeEventSource); const store = new HarnessClientStore(); await store.initialize(); expect(localStorage.getItem("agent-studio-mini:model-connection:v1")).toBeNull(); expect(store.getSnapshot().model.configured).toBe(false); }); + it("waits for an in-flight Session before saving the model", async () => { + let releaseSession!: () => void; + const sessionGate = new Promise((resolve) => { releaseSession = resolve; }); + const requests: Array<{ url: string; init?: RequestInit }> = []; + vi.stubGlobal("fetch", vi.fn(async (input: string | URL | Request, init?: RequestInit) => { + const url = String(input); requests.push({ url, init }); + if (url === "/api/sessions" && init?.method === "POST") { await sessionGate; return { ok: true, json: async () => ({ sessionId: "ses_delayed" }) }; } + if (url === "/api/skills") return { ok: true, json: async () => ({ skills: [] }) }; + if (init?.method === "PUT") return { ok: true, json: async () => ({ configured: true, source: "session", providerId: "openai", api: "openai-responses", modelId: "gpt", supportsImages: true, keyConfigured: true, keyHint: "••••cret" }) }; + return { ok: true, json: async () => ({ configured: false, source: "missing", keyConfigured: false }) }; + })); + class FakeEventSource { static readonly CLOSED = 2; readonly readyState = 1; onopen: (() => void) | null = null; onerror: (() => void) | null = null; addEventListener(): void {} close(): void {} } + vi.stubGlobal("EventSource", FakeEventSource); + const store = new HarnessClientStore(); + const initializing = store.initialize(); + const saving = store.saveModel({ providerId: "openai", api: "openai-responses", apiKey: "top-secret", modelId: "gpt", supportsImages: true }); + expect(requests.filter((item) => item.init?.method === "POST")).toHaveLength(1); + expect(requests.some((item) => item.init?.method === "PUT")).toBe(false); + releaseSession(); + await Promise.all([initializing, saving]); + expect(requests.find((item) => item.init?.method === "PUT")?.url).toBe("/api/sessions/ses_delayed/model-config"); + expect(store.getSnapshot().model).toMatchObject({ configured: true, keyConfigured: true }); + }); + it("reports the Session initialization failure instead of a false save success", async () => { + vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new TypeError("Failed to fetch"))); + const store = new HarnessClientStore(); + await expect(store.saveModel({ providerId: "openai", api: "openai-responses", apiKey: "top-secret", modelId: "gpt", supportsImages: true })).rejects.toThrow("Session 初始化失败:Failed to fetch"); + }); +}); diff --git a/apps/web/tests/ui.test.tsx b/apps/web/tests/ui.test.tsx new file mode 100644 index 0000000..fe05484 --- /dev/null +++ b/apps/web/tests/ui.test.tsx @@ -0,0 +1,42 @@ +// @vitest-environment jsdom +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { HarnessEvent } from "@agent-studio/shared"; +import { App } from "../src/App"; +import { HarnessRuntime } from "../src/assistant/HarnessRuntime"; +import { HarnessClientStore } from "../src/assistant/store"; +import { MockAdsMetricsUI, SaveReportDraftUI } from "../src/components/ToolUIs"; +import { AppErrorBoundary } from "../src/components/AppErrorBoundary"; + +afterEach(() => { localStorage.clear(); sessionStorage.clear(); vi.restoreAllMocks(); }); +describe("web UI", () => { + it("renders the Skills, Chat, and Timeline operating areas", () => { const store = new HarnessClientStore(); render(); expect(screen.getByLabelText("Skills Catalog")).toBeInTheDocument(); expect(screen.getByLabelText("Streaming Chat")).toBeInTheDocument(); expect(screen.getByLabelText("Event Timeline")).toBeInTheDocument(); expect(screen.getByRole("button", { name: /尚未配置模型/ })).toBeInTheDocument(); }); + it("renders the mock metrics Tool UI", () => { render(); expect(screen.getByText("demo-account", { exact: false })).toBeInTheDocument(); expect(screen.getByText("ROAS")).toBeInTheDocument(); }); + it("renders approval actions inside save_report_draft", () => { const store = new HarnessClientStore(); render(); expect(screen.getByRole("button", { name: "批准一次" })).toBeInTheDocument(); expect(screen.getByRole("button", { name: "拒绝" })).toBeInTheDocument(); }); + it("opens settings with password input and does not use Web Storage", async () => { const user = userEvent.setup(); const store = new HarnessClientStore(); render(); await user.click(screen.getByRole("button", { name: /尚未配置模型/ })); expect(screen.getByLabelText("API Key")).toHaveAttribute("type", "password"); expect(localStorage.length).toBe(0); expect(sessionStorage.length).toBe(0); }); + it("prompts for a new key when local model metadata was restored", () => { const store = new HarnessClientStore(); store.getSnapshot().model = { configured: true, source: "session", providerId: "saved", api: "openai-completions", modelId: "saved-model", supportsImages: false, keyConfigured: false }; render(); expect(screen.getByRole("button", { name: /模型配置已恢复/ })).toBeInTheDocument(); expect(screen.getByLabelText("消息输入")).toHaveAttribute("placeholder", "请重新输入 API Key"); }); + it("omits empty optional model fields when saving settings", async () => { const user = userEvent.setup(); const store = new HarnessClientStore(); const save = vi.spyOn(store, "saveModel").mockResolvedValue(); render(); await user.click(screen.getByRole("button", { name: /尚未配置模型/ })); await user.type(screen.getByLabelText("API Key"), "sk-fake"); await user.click(screen.getByRole("button", { name: "保存配置" })); expect(save).toHaveBeenCalledWith({ providerId: "openai", api: "openai-responses", baseUrl: "https://api.openai.com/v1", apiKey: "sk-fake", modelId: "gpt-5.4", supportsImages: true }); }); + it("fills the classroom demo prompt without sending it", async () => { const user = userEvent.setup(); const store = new HarnessClientStore(); render(); await user.click(screen.getByRole("button", { name: "填入 Demo Prompt" })); expect((screen.getByLabelText("消息输入") as HTMLTextAreaElement).value).toContain("ads-analysis"); expect(store.getSnapshot().events).toHaveLength(0); }); + it("shows a recoverable fallback instead of a black screen for render failures", () => { vi.spyOn(console, "error").mockImplementation(() => undefined); const Broken = () => { throw new Error("tool renderer failed"); }; render(); expect(screen.getByRole("alert")).toHaveTextContent("界面渲染出现异常"); expect(screen.getByRole("button", { name: "重新加载页面" })).toBeInTheDocument(); }); + it("renders a duplicated Pi Tool sequence and approval without crashing", () => { + const store = new HarnessClientStore(); + store.reduce(harnessEvent(1, "user.message", { messageId: "user_1", text: "demo", attachmentIds: [] })); + store.reduce(harnessEvent(2, "run.started", { modelId: "fake" })); + store.reduce(harnessEvent(3, "assistant.completed", { messageId: "assistant_1" })); + store.reduce(harnessEvent(4, "tool.requested", { toolCallId: "read_1", toolName: "read", arguments: { path: "" } })); + store.reduce(harnessEvent(5, "tool.requested", { toolCallId: "read_1", toolName: "read", arguments: { path: ".agents/skills/ads-analysis/SKILL.md" } })); + store.reduce(harnessEvent(6, "tool.completed", { toolCallId: "read_1", toolName: "read", result: { status: "success" } })); + store.reduce(harnessEvent(7, "tool.requested", { toolCallId: "metrics_1", toolName: "mock_ads_metrics", arguments: { accountId: "demo-account", days: 7 } })); + store.reduce(harnessEvent(8, "tool.completed", { toolCallId: "metrics_1", toolName: "mock_ads_metrics", result: { details: { spend: 100, ctr: 2, cpa: 4, roas: 5 } } })); + store.reduce(harnessEvent(9, "tool.requested", { toolCallId: "save_1", toolName: "save_report_draft", arguments: { content: "报告正文" } })); + store.reduce(harnessEvent(10, "approval.required", { approvalId: "apr_1", sessionId: "ses_1", runId: "run_1", toolCallId: "save_1", toolName: "save_report_draft", arguments: { content: "报告正文" }, riskLevel: "medium", createdAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 60_000).toISOString(), status: "pending" })); + render(); + expect(screen.queryByText("界面渲染出现异常")).not.toBeInTheDocument(); + expect(screen.getByText("ROAS")).toBeInTheDocument(); + expect(screen.getByRole("heading", { name: "允许保存报告草稿?" })).toBeInTheDocument(); + }); +}); + +function toolProps(toolName: string, args: Record, result?: unknown, approval?: { id: string }) { return { type: "tool-call" as const, toolCallId: "t1", toolName, args, argsText: JSON.stringify(args), result, isError: false, status: { type: "complete" as const }, addResult: vi.fn(), resume: vi.fn(), respondToApproval: vi.fn(), ...(approval ? { approval } : {}) }; } +function harnessEvent(sequence: number, type: T, payload: Extract["payload"]): HarnessEvent { return { eventId: `evt_${sequence}`, sessionId: "ses_1", runId: "run_1", timestamp: new Date().toISOString(), sequence, type, payload } as HarnessEvent; } diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json new file mode 100644 index 0000000..b60ce20 --- /dev/null +++ b/apps/web/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "module": "ESNext", "moduleResolution": "Bundler", "jsx": "react-jsx", "noEmit": true, "types": ["vite/client"] }, + "include": ["src", "vite.config.ts"] +} diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts new file mode 100644 index 0000000..a620ea0 --- /dev/null +++ b/apps/web/vite.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + server: { port: 5173, proxy: { "/api": "http://127.0.0.1:3001", "/health": "http://127.0.0.1:3001" } }, + build: { outDir: "dist" }, +}); diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..88f0ab7 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,55 @@ +# Architecture + +## Component map + +```mermaid +flowchart TB + subgraph Browser + C[assistant-ui Composer] + T[Thread + Tool UI] + E[Event Timeline] + S[HarnessClientStore] + C --> S + S --> T + S --> E + end + subgraph Server + F[Fastify protocol layer] + R[SessionRegistry] + ES[EventStore] + AB[ApprovalBroker] + AS[AttachmentStore] + MS[ModelConnectionStore] + P[Pi AgentSession] + L[DefaultResourceLoader] + W[SafeWorkspace] + end + S -->|JSON / multipart| F + F -->|SSE| S + F --> R + F --> AS + F --> MS + R --> P + R --> ES + P --> L + P --> AB + P --> W +``` + +`apps/server` 只做请求校验、错误映射、SSE framing 与对象组合。`packages/harness` 拥有 Agent 生命周期和所有核心状态接口。`packages/shared` 不依赖 Node,定义前后端唯一协议。`apps/web` 不拥有另一套 Agent 状态机,只用 sequence reducer 投影 EventStore。 + +## Session creation and run + +1. Web 创建 Harness Session;服务器建立独立 cwd、事件序列、附件和凭证命名空间。 +2. 服务启动时严格加载并缓存 `.env` 默认模型;新 Session 优先使用该配置。没有环境配置时,Web 可把 localStorage 中通过 strict Zod 校验的非敏感模型元数据恢复到该 Session。API Key 不参与浏览器持久化,来自环境变量或服务器 AES-256-GCM 凭证库。 +3. 首次 Run 使用精确模型创建独立 Pi `AgentSession`、`SessionManager.inMemory(cwd)` 与安全 extension。 +4. message 立即得到 202;后台调用 `prompt`,Pi 事件由单一 adapter 映射并写 EventStore。 +5. SSE 将历史和实时事件交给 `HarnessClientStore`;assistant-ui External Store Runtime 只负责呈现。 + +## Storage replacement seams + +`EventStore`、`ApprovalBroker`、`AttachmentStore`、`ModelConnectionStore`、`SessionRegistry` 都有接口。内存/本地临时文件实现可分别替换为 Redis streams、Postgres、对象存储、KMS 与分布式 worker,而不改变 Web 协议。 + +## Failure and shutdown + +Run timeout 和用户 cancel 使用不同 reason;Approval wait 监听同一 AbortSignal。删除先 abort,再 cancel pending approval,关闭订阅与 SSE,调用 Pi `dispose()`,清凭证/附件,最后在 canonical sessions root 校验后删除 cwd。单个 SSE 断开不取消 Run。 diff --git a/docs/classroom-demo.md b/docs/classroom-demo.md new file mode 100644 index 0000000..9685948 --- /dev/null +++ b/docs/classroom-demo.md @@ -0,0 +1,37 @@ +# 10–15 minute classroom demo + +## 0:00–2:00 — boundaries + +启动 `pnpm dev`,展示三栏:左侧只显示 Skill catalog,不显示正文;中间由 assistant-ui 渲染;右侧是 EventStore 的审计时间线。强调 Pi 是 Agent Loop 事实来源,assistant-ui 不直接访问模型。 + +## 2:00–4:00 — model connection + +打开 Settings,比较 OpenAI、Anthropic、OpenAI-compatible preset。说明模型元数据会保存在 localStorage,但 key 不回填、不进 Web Storage;刷新后字段自动恢复,必须重输 Key,Composer 才会启用。课堂无需真实调用时可只演示表单和测试返回的“首次运行验证”。 + +## 4:00–8:00 — main path + +点击 Demo Prompt(它只填入,不自动执行): + +> 请使用 ads-analysis Skill,分析账户 demo-account 最近 7 天数据,找出异常 Campaign,并输出账户总结、关键指标和 3 条优化建议。最后使用 report-writer Skill 生成一份报告草稿。 + +有可用课堂模型时发送,观察:`run.started` → `skill.requested` → `skill.loaded` → `mock_ads_metrics` Tool 卡片 → streaming delta。指出指标来自 fixture,不是真实广告平台。 + +## 8:00–11:00 — approval + +当 `save_report_draft` 出现时,展示上下文 Tool 卡片与 modal。先讲清 preflight 尚未写文件。 + +- Approve 路径:点击“批准一次”,等待 SSE 权威状态,再查看当前 Session 的 `report.md`。 +- Deny 路径:重开一个 Session,再拒绝;确认没有文件但 Agent 可在聊天给出正文。 +- Timeout 可将 `APPROVAL_TIMEOUT_MS` 临时缩短后演示;最终状态以服务器事件为准,不以浏览器倒计时为准。 + +## 11:00–13:00 — image path + +选择/拖拽/粘贴一张 JPEG、PNG 或 WebP,打开缩略图 Dialog,再附加: + +> 请同时观察我上传的图片,将可见内容作为辅助信号;不要根据图片编造图片中不存在的数据,数值指标以 mock_ads_metrics 返回的数据为准。 + +说明 browser object URL、流式 upload、Sharp 重编码、attachmentId、服务端 ImageContent 的链路。切换到 text-only 配置,演示发送前阻止图片。 + +## 13:00–15:00 — failure and production boundary + +停止一个 Run,查看 `run.cancelled`。断开/刷新页面说明 SSE 重放与去重。最后强调:Pi Project Trust 不是 Sandbox;不可信任务需要容器/VM/micro-VM,课堂内存 Store 也不是生产持久层。 diff --git a/docs/event-mapping.md b/docs/event-mapping.md new file mode 100644 index 0000000..6a9dfc8 --- /dev/null +++ b/docs/event-mapping.md @@ -0,0 +1,25 @@ +# Event mapping + +Pi 原生事件到 HarnessEvent 的转换集中在 `packages/harness/src/pi-event-adapter.ts`。未识别事件安全忽略,不会让 Run 崩溃。 + +| Pi / Harness fact | HarnessEvent | assistant-ui / UI state | +| --- | --- | --- | +| message text delta | `assistant.delta` | 按 messageId 合并文本,status running | +| assistant message end | `assistant.completed` | message status complete,polite live announcement | +| extension `tool_call` preflight | `tool.requested` | Tool part running / approval pending | +| tool execution success | `tool.completed` | Tool result success | +| tool execution error | `tool.failed` | Tool result error | +| canonical Skill `read` request | `skill.requested` | Catalog 高亮 requested | +| 同一 read 成功结果 | `skill.loaded` | Catalog 高亮 loaded | +| ApprovalBroker request | `approval.required` | Tool card + modal pending | +| broker decision/timeout/cancel | `approval.resolved` | approved/denied/expired/cancelled | +| registry accepts message | `user.message`, `run.started` | user message + running | +| prompt resolves | `run.completed` | run idle | +| abort by user/system | `run.cancelled` | incomplete,显示 reason | +| prompt/create failure | `run.failed` | incomplete/error | + +Skill requested/loaded 是 Harness 推导事件,不是 Pi 原生事件。匹配依据是 `DefaultResourceLoader` 已发现的 canonical `SKILL.md` 索引;仅文件名相同不算命中。 + +`tool.requested` 在 extension preflight 写入,后续 Pi `tool_execution_start` 按 toolCallId 去重。读取类 Tool 的 completed event 只暴露安全摘要,避免把 Skill 正文或文件内容复制到 Timeline。参数会限长、敏感字段脱敏,绝对路径转换为项目相对路径或 ``。 + +SSE 事件 id 是 eventId,事件名是 HarnessEvent type。`Last-Event-ID` 比 query `afterSequence` 优先;若 cursor 早于 1000 条保留窗口,先发送 `stream.gap`,客户端重新建立可识别边界,而不是静默丢事件。 diff --git a/docs/production-roadmap.md b/docs/production-roadmap.md new file mode 100644 index 0000000..c68beaf --- /dev/null +++ b/docs/production-roadmap.md @@ -0,0 +1,33 @@ +# Production roadmap + +## Persistence and distribution + +1. EventStore 迁移到 Redis Streams 或 Kafka,并把长期审计写 Postgres/不可变对象存储。 +2. Session/Approval 元数据进入 Postgres,运行锁、timeout、SSE fan-out 使用 Redis。 +3. AttachmentStore 使用带生命周期策略的对象存储、短期签名 URL、恶意内容扫描和租户配额。 +4. 把 Agent Run 放入 durable queue/worker,支持幂等恢复、worker lease 与跨实例取消。 + +## Secrets and identity + +1. KMS/Vault envelope encryption,短期 scoped credential,不在长期 Store 保存明文。 +2. OIDC/SSO、租户/RBAC/ABAC、每个 Session 所有权校验。 +3. CSRF 防护、严格 same-site cookie、CSP、Origin 检查、rate limit、配额与 abuse detection。 +4. 管理员批准策略、双人审批和不可抵赖审计。 + +## Isolation + +1. 每 Run 使用无特权容器、VM 或 micro-VM;只读输入、最小可写输出。 +2. seccomp/AppArmor、PID/CPU/内存/磁盘/time limits、无 host socket。 +3. egress allowlist proxy,DNS pinning,重定向逐跳校验,metadata 永久 deny。 +4. Skill 签名、来源策略、版本锁与离线扫描。 + +## Reliability and observability + +1. 分布式 SSE/WebSocket gateway,cursor 持久化、backpressure 和 gap recovery。 +2. OpenTelemetry trace:HTTP → run → model → tool → approval;指标不含 prompt/key/image body。 +3. Provider circuit breaker、预算、token/cost accounting、重试分类和 dead-letter queue。 +4. chaos tests、断电恢复、跨版本 event schema migration 和数据保留策略。 + +## Frontend scale + +代码分割 assistant-ui/设置/附件预览,虚拟化长 Timeline 与 Thread;增加离线恢复、上传续传和生产级国际化。保持 HarnessClientStore 是唯一投影状态,不在 Runtime 中复制 Agent 状态机。 diff --git a/docs/security-boundaries.md b/docs/security-boundaries.md new file mode 100644 index 0000000..567dd6a --- /dev/null +++ b/docs/security-boundaries.md @@ -0,0 +1,41 @@ +# Security boundaries + +## Trust model + +Pi 没有内置 Sandbox。Project Trust 只控制项目资源加载,不隔离进程、文件系统或网络。Pi、Extension 与 Tool 都继承 Node 进程权限。本项目是安全教学基线,不是恶意代码隔离器。 + +## Credentials and logs + +- API Key 只经同源 body 进入服务端;Pi 运行时仍使用 Session 专属 `InMemoryCredentialStore`。 +- 前端提交的 API Key 使用 `MODEL_CREDENTIAL_ENCRYPTION_KEY` 提供的 32-byte master key 做 AES-256-GCM 加密,采用随机 96-bit IV、认证标签和模型连接 identity AAD;密文文件使用 `0600` 权限与临时文件原子 rename。master key 只来自 `.env`/进程环境,绝不写入密文文件。 +- 浏览器刷新创建新 Session 时,只提交非敏感模型 identity;服务端按 identity 解密恢复对应凭证。删除 Session 不删除持久凭证,只有 Clear Credentials 会删除。 +- 浏览器 localStorage 只保存白名单模型元数据;保存前主动移除 `apiKey` 并用不接受额外字段的 Zod schema 校验。损坏或超限数据会删除。 +- 不写 Pi 全局 auth/models 文件,不执行 `!command`,不接受自定义 headers。 +- 响应仅回传配置状态和末四位 hint;SSE、事件、日志和测试快照禁止原始 key。 +- Fastify 只记录安全元数据并对 Authorization/API Key 路径 redaction;生产错误不返回 stack/绝对路径。 + +## Filesystem + +- 每个 Session 独立 canonical cwd;resolve 后必须仍在 root 内。 +- `realpath` 验证父目录,拒绝 `..`、绝对路径逃逸与符号链接路径。 +- `.env`、`.git`、SSH、用户主目录和系统路径受到 preflight 保护。 +- `save_report_draft` 目标固定为 `report.md`,临时排他创建后原子 rename;审批不能替代路径检查。 +- 默认不注册 shell、write、edit。 + +课堂实现使用 Node 文件 API,无法完全消除恶意同机进程造成的 TOCTOU。生产版应把每个任务放进独立 mount namespace / micro-VM,并使用只读输入卷和受控输出卷。 + +## Images + +multipart 流有字节上限;扩展名、MIME、magic bytes 三重匹配。Sharp 限制像素、解码并重新编码,自动旋转且移除多余 metadata。随机文件名仅写 Session attachments,预览不提供任意路径静态服务。事件仅记录 ID、MIME、大小和尺寸。 + +## Model Base URL and SSRF + +默认 HTTPS;拒绝 URL credentials、fragment、危险 credential query、畸形 URL、metadata hostname,以及 DNS 解析得到的 private/loopback/link-local 地址。开发环境可使用 loopback HTTP,企业内网必须显式 opt-in。DNS rebinding、重定向逐跳策略和出口 DNS 固定应由生产 egress proxy 强制执行。 + +## Approval + +审批发生在 Pi `tool_call` preflight,Tool 尚未执行。Broker 校验当前 Session/Run、响应 abort、释放 timer/Promise,并将 approve/deny/expired/cancelled 作为服务器事实发送。未知工具 deny,未注册高风险工具无法被模型调用。 + +## Required production isolation + +不可信 Skill、仓库、脚本、附件解析或无人值守 Agent 必须在 Docker(配合 seccomp/AppArmor 和无特权用户)、VM、micro-VM 或远程 Sandbox 运行,并施加 CPU/内存/磁盘/时间/网络配额。Project Trust 不能替代这些措施。 diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..e4edbb6 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,21 @@ +import js from "@eslint/js"; +import globals from "globals"; +import tseslint from "typescript-eslint"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; + +export default tseslint.config( + { ignores: ["**/dist/**", "**/coverage/**", "workspace/**", "node_modules/**"] }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ["**/*.{ts,tsx}"], + languageOptions: { globals: { ...globals.node, ...globals.browser } }, + rules: { "@typescript-eslint/no-explicit-any": "error" }, + }, + { + files: ["apps/web/**/*.{ts,tsx}"], + plugins: { "react-hooks": reactHooks, "react-refresh": reactRefresh }, + rules: { ...reactHooks.configs.recommended.rules, "react-refresh/only-export-components": ["warn", { allowConstantExport: true }] }, + }, +); diff --git a/fixtures/ads-account.json b/fixtures/ads-account.json new file mode 100644 index 0000000..3a29669 --- /dev/null +++ b/fixtures/ads-account.json @@ -0,0 +1,10 @@ +{ + "accountId": "demo-account", + "currency": "USD", + "timeRange": { "from": "2026-08-21", "to": "2026-08-27" }, + "campaigns": [ + { "id": "cmp_brand", "name": "Brand Search", "spend": 840.25, "impressions": 121400, "clicks": 5630, "conversions": 392, "revenue": 8232.00 }, + { "id": "cmp_prospect", "name": "Prospecting Video", "spend": 2310.50, "impressions": 540200, "clicks": 7280, "conversions": 118, "revenue": 3186.00 }, + { "id": "cmp_retarget", "name": "Retargeting", "spend": 1264.75, "impressions": 98200, "clicks": 4515, "conversions": 244, "revenue": 6832.00 } + ] +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..364d6ca --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "agent-studio-mini", + "version": "0.1.0", + "private": true, + "type": "module", + "packageManager": "pnpm@11.24.0", + "engines": { "node": ">=22.19.0" }, + "scripts": { + "dev": "pnpm --parallel --filter @agent-studio/server --filter @agent-studio/web dev", + "lint": "eslint . --max-warnings 0", + "typecheck": "pnpm -r typecheck", + "test": "vitest run", + "build": "pnpm -r build" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "jsdom": "^27.2.0", + "tsx": "^4.21.0", + "typescript": "^5.9.3", + "typescript-eslint": "^8.48.1", + "vite": "^7.2.6", + "vitest": "^4.0.15" + } +} diff --git a/packages/harness/package.json b/packages/harness/package.json new file mode 100644 index 0000000..e004455 --- /dev/null +++ b/packages/harness/package.json @@ -0,0 +1,17 @@ +{ + "name": "@agent-studio/harness", + "version": "0.1.0", + "private": true, + "type": "module", + "exports": { ".": { "types": "./src/index.ts", "development": "./src/index.ts", "import": "./dist/index.js", "default": "./dist/index.js" } }, + "scripts": { "typecheck": "tsc --noEmit", "build": "tsc -p tsconfig.json" }, + "dependencies": { + "@agent-studio/shared": "workspace:*", + "@earendil-works/pi-ai": "0.84.3", + "@earendil-works/pi-coding-agent": "0.84.3", + "sharp": "^0.34.5", + "typebox": "1.3.7", + "zod": "^4.1.13" + }, + "devDependencies": { "@types/node": "^24.10.1", "typescript": "^5.9.3" } +} diff --git a/packages/harness/src/approval.ts b/packages/harness/src/approval.ts new file mode 100644 index 0000000..fa2e7e5 --- /dev/null +++ b/packages/harness/src/approval.ts @@ -0,0 +1,74 @@ +import { randomUUID } from "node:crypto"; +import type { ApprovalRequest } from "@agent-studio/shared"; +import { HarnessError, redactValue } from "./errors.js"; + +export type ApprovalStatus = "approved" | "denied" | "expired" | "cancelled"; +export interface ApprovalDecision { status: ApprovalStatus; resolvedAt: string } +export interface ApprovalRequestInput { sessionId: string; runId: string; toolCallId: string; toolName: string; arguments: unknown; riskLevel: "low" | "medium" | "high" } +export interface ApprovalResolution { request: ApprovalRequest; decision: ApprovalDecision } + +export interface ApprovalBroker { + request(input: ApprovalRequestInput, signal: AbortSignal): Promise; + approve(approvalId: string): ApprovalResolution; + deny(approvalId: string): ApprovalResolution; + cancelForSession(sessionId: string): void; + get(approvalId: string): ApprovalRequest | undefined; + listPending(sessionId: string): ApprovalRequest[]; +} + +interface Pending { request: ApprovalRequest; resolve: (value: ApprovalDecision) => void; timer: ReturnType; signal: AbortSignal; abort: () => void; decision?: ApprovalDecision } + +export class InMemoryApprovalBroker implements ApprovalBroker { + private readonly approvals = new Map(); + constructor( + private readonly timeoutMs = 60_000, + private readonly onResolution?: (value: ApprovalResolution) => void, + private readonly onRequest?: (value: ApprovalRequest) => void, + ) {} + + request(input: ApprovalRequestInput, signal: AbortSignal): Promise { + const now = Date.now(); + const approvalId = `apr_${randomUUID()}`; + const request: ApprovalRequest = { + ...input, + approvalId, + arguments: redactValue(input.arguments), + createdAt: new Date(now).toISOString(), + expiresAt: new Date(now + this.timeoutMs).toISOString(), + status: "pending", + }; + return new Promise((resolve) => { + const entry: Pending = { request, resolve, timer: setTimeout(() => this.finish(approvalId, "expired"), this.timeoutMs), signal, abort: () => this.finish(approvalId, "cancelled") }; + this.approvals.set(approvalId, entry); + this.onRequest?.(request); + if (signal.aborted) entry.abort(); + else signal.addEventListener("abort", entry.abort, { once: true }); + }); + } + + private finish(approvalId: string, status: ApprovalStatus): ApprovalResolution { + const entry = this.approvals.get(approvalId); + if (!entry) throw new HarnessError("APPROVAL_NOT_FOUND", "Approval not found", 404); + if (entry.decision) { + if (entry.decision.status !== status) throw new HarnessError("APPROVAL_ALREADY_RESOLVED", "Approval already resolved", 409); + return { request: entry.request, decision: entry.decision }; + } + clearTimeout(entry.timer); + entry.signal.removeEventListener("abort", entry.abort); + const decision = { status, resolvedAt: new Date().toISOString() }; + entry.decision = decision; + entry.request = { ...entry.request, status }; + entry.resolve(decision); + const resolution = { request: entry.request, decision }; + this.onResolution?.(resolution); + return resolution; + } + + approve(id: string): ApprovalResolution { return this.finish(id, "approved"); } + deny(id: string): ApprovalResolution { return this.finish(id, "denied"); } + cancelForSession(sessionId: string): void { + for (const [id, entry] of this.approvals) if (entry.request.sessionId === sessionId && !entry.decision) this.finish(id, "cancelled"); + } + get(id: string): ApprovalRequest | undefined { return this.approvals.get(id)?.request; } + listPending(sessionId: string): ApprovalRequest[] { return [...this.approvals.values()].filter((item) => item.request.sessionId === sessionId && !item.decision).map((item) => item.request); } +} diff --git a/packages/harness/src/attachments.ts b/packages/harness/src/attachments.ts new file mode 100644 index 0000000..4244156 --- /dev/null +++ b/packages/harness/src/attachments.ts @@ -0,0 +1,68 @@ +import { randomUUID } from "node:crypto"; +import { mkdir, readFile, rename, rm, unlink, writeFile } from "node:fs/promises"; +import path from "node:path"; +import type { ImageAttachment } from "@agent-studio/shared"; +import sharp from "sharp"; +import { HarnessError } from "./errors.js"; + +export interface UploadStream { filename: string; mimetype: string; stream: AsyncIterable } +export interface StoredAttachment extends ImageAttachment { filePath: string } +export interface AttachmentStore { + add(sessionId: string, upload: UploadStream): Promise; + get(sessionId: string, attachmentId: string): ImageAttachment | undefined; + getStored(sessionId: string, attachmentId: string): StoredAttachment | undefined; + remove(sessionId: string, attachmentId: string): Promise; + removeAll(sessionId: string): Promise; +} + +const mediaExtensions = new Map([["image/jpeg", new Set([".jpg", ".jpeg"])], ["image/png", new Set([".png"])], ["image/webp", new Set([".webp"])]]); +function magicMedia(buffer: Buffer): "image/jpeg" | "image/png" | "image/webp" | undefined { + if (buffer.subarray(0, 3).equals(Buffer.from([0xff, 0xd8, 0xff]))) return "image/jpeg"; + if (buffer.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) return "image/png"; + if (buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === "WEBP") return "image/webp"; + return undefined; +} + +export class FileAttachmentStore implements AttachmentStore { + private readonly items = new Map(); + constructor(private readonly sessionsRoot: string, private readonly maxBytes = 5 * 1024 * 1024, private readonly maxPixels = 40_000_000) {} + private key(sessionId: string, attachmentId: string): string { return `${sessionId}:${attachmentId}`; } + + async add(sessionId: string, upload: UploadStream): Promise { + const declared = upload.mimetype.toLowerCase(); + if (!mediaExtensions.has(declared)) throw new HarnessError("ATTACHMENT_TYPE_UNSUPPORTED", "Only JPEG, PNG, and WebP images are accepted", 415); + const extension = path.extname(upload.filename).toLowerCase(); + if (!mediaExtensions.get(declared)?.has(extension)) throw new HarnessError("ATTACHMENT_EXTENSION_MISMATCH", "File extension does not match its MIME type", 415); + const chunks: Buffer[] = []; + let size = 0; + for await (const chunk of upload.stream) { + size += chunk.byteLength; + if (size > this.maxBytes) throw new HarnessError("ATTACHMENT_TOO_LARGE", "Image exceeds the configured size limit", 413); + chunks.push(Buffer.from(chunk)); + } + const source = Buffer.concat(chunks); + const detected = magicMedia(source); + if (!detected || detected !== declared) throw new HarnessError("ATTACHMENT_MAGIC_MISMATCH", "Image content does not match its declared type", 415); + const metadata = await sharp(source, { limitInputPixels: this.maxPixels, failOn: "error" }).metadata().catch(() => { throw new HarnessError("ATTACHMENT_INVALID_IMAGE", "Image cannot be decoded safely", 422); }); + if (!metadata.width || !metadata.height || metadata.width * metadata.height > this.maxPixels) throw new HarnessError("ATTACHMENT_DIMENSIONS_EXCEEDED", "Image dimensions exceed the configured limit", 413); + const pipeline = sharp(source, { limitInputPixels: this.maxPixels, failOn: "error" }).rotate(); + const normalized = detected === "image/png" ? await pipeline.png({ compressionLevel: 9 }).toBuffer() : detected === "image/webp" ? await pipeline.webp({ quality: 88 }).toBuffer() : await pipeline.jpeg({ quality: 90, mozjpeg: true }).toBuffer(); + const outputMetadata = await sharp(normalized).metadata(); + const attachmentId = `att_${randomUUID()}`; + const directory = path.join(this.sessionsRoot, sessionId, "attachments"); + await mkdir(directory, { recursive: true, mode: 0o700 }); + const finalPath = path.join(directory, `${attachmentId}${extension}`); + const temporary = `${finalPath}.tmp`; + await writeFile(temporary, normalized, { mode: 0o600, flag: "wx" }); + await rename(temporary, finalPath); + const item: StoredAttachment = { attachmentId, sessionId, name: path.basename(upload.filename).slice(0, 200), mediaType: detected, byteSize: normalized.byteLength, width: outputMetadata.width ?? metadata.width, height: outputMetadata.height ?? metadata.height, status: "ready", filePath: finalPath }; + this.items.set(this.key(sessionId, attachmentId), item); + return this.public(item); + } + get(sessionId: string, id: string): ImageAttachment | undefined { const item = this.items.get(this.key(sessionId, id)); return item ? this.public(item) : undefined; } + getStored(sessionId: string, id: string): StoredAttachment | undefined { return this.items.get(this.key(sessionId, id)); } + async readBase64(sessionId: string, id: string): Promise { const item = this.getStored(sessionId, id); if (!item) throw new HarnessError("ATTACHMENT_NOT_FOUND", "Attachment not found", 404); return (await readFile(item.filePath)).toString("base64"); } + async remove(sessionId: string, id: string): Promise { const key = this.key(sessionId, id); const item = this.items.get(key); if (!item) return; await unlink(item.filePath).catch(() => undefined); this.items.delete(key); } + async removeAll(sessionId: string): Promise { for (const [key] of this.items) if (key.startsWith(`${sessionId}:`)) this.items.delete(key); await rm(path.join(this.sessionsRoot, sessionId, "attachments"), { recursive: true, force: true }); } + private public(item: StoredAttachment): ImageAttachment { return { attachmentId: item.attachmentId, sessionId: item.sessionId, name: item.name, mediaType: item.mediaType, byteSize: item.byteSize, width: item.width, height: item.height, status: item.status }; } +} diff --git a/packages/harness/src/credential-vault.ts b/packages/harness/src/credential-vault.ts new file mode 100644 index 0000000..59edb91 --- /dev/null +++ b/packages/harness/src/credential-vault.ts @@ -0,0 +1,127 @@ +import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID } from "node:crypto"; +import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { z } from "zod"; +import type { ModelConnectionInput } from "@agent-studio/shared"; +import { HarnessError } from "./errors.js"; + +const encryptedValueSchema = z.object({ + iv: z.string().min(1), + tag: z.string().min(1), + ciphertext: z.string().min(1), +}).strict(); + +const vaultDocumentSchema = z.object({ + version: z.literal(1), + entries: z.record(z.string(), encryptedValueSchema), +}).strict(); + +type VaultDocument = z.infer; + +export interface CredentialVault { + get(identity: string): Promise; + set(identity: string, apiKey: string): Promise; + delete(identity: string): Promise; +} + +export function modelCredentialIdentity(input: ModelConnectionInput): string { + const stable = JSON.stringify({ + providerId: input.providerId, + api: input.api, + baseUrl: input.baseUrl ?? null, + modelId: input.modelId, + }); + return createHash("sha256").update(stable).digest("base64url"); +} + +export class EncryptedFileCredentialVault implements CredentialVault { + private readonly key: Buffer; + private document: VaultDocument = { version: 1, entries: {} }; + private loaded: Promise | undefined; + private mutation: Promise = Promise.resolve(); + + constructor(private readonly filePath: string, encodedMasterKey: string) { + this.key = Buffer.from(encodedMasterKey.trim(), "base64"); + if (this.key.byteLength !== 32) { + throw new HarnessError("INVALID_CREDENTIAL_ENCRYPTION_KEY", "MODEL_CREDENTIAL_ENCRYPTION_KEY must be a base64-encoded 32-byte key", 500); + } + } + + async get(identity: string): Promise { + await this.mutation; + await this.ensureLoaded(); + const value = this.document.entries[identity]; + if (!value) return undefined; + try { + const decipher = createDecipheriv("aes-256-gcm", this.key, Buffer.from(value.iv, "base64")); + decipher.setAAD(this.aad(identity)); + decipher.setAuthTag(Buffer.from(value.tag, "base64")); + return Buffer.concat([decipher.update(Buffer.from(value.ciphertext, "base64")), decipher.final()]).toString("utf8"); + } catch { + throw new HarnessError("CREDENTIAL_DECRYPTION_FAILED", "Stored model credential could not be decrypted with the configured master key", 500); + } + } + + set(identity: string, apiKey: string): Promise { + return this.enqueue(async () => { + await this.ensureLoaded(); + const iv = randomBytes(12); + const cipher = createCipheriv("aes-256-gcm", this.key, iv); + cipher.setAAD(this.aad(identity)); + const ciphertext = Buffer.concat([cipher.update(apiKey, "utf8"), cipher.final()]); + this.document.entries[identity] = { + iv: iv.toString("base64"), + tag: cipher.getAuthTag().toString("base64"), + ciphertext: ciphertext.toString("base64"), + }; + await this.persist(); + }); + } + + delete(identity: string): Promise { + return this.enqueue(async () => { + await this.ensureLoaded(); + if (!(identity in this.document.entries)) return; + delete this.document.entries[identity]; + await this.persist(); + }); + } + + private ensureLoaded(): Promise { + if (!this.loaded) this.loaded = this.load(); + return this.loaded; + } + + private async load(): Promise { + try { + const raw = await readFile(this.filePath, "utf8"); + this.document = vaultDocumentSchema.parse(JSON.parse(raw)); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") return; + if (error instanceof HarnessError) throw error; + throw new HarnessError("CREDENTIAL_STORE_CORRUPT", "Encrypted model credential store is invalid", 500); + } + } + + private enqueue(operation: () => Promise): Promise { + const next = this.mutation.then(operation, operation); + this.mutation = next.catch(() => undefined); + return next; + } + + private async persist(): Promise { + const directory = path.dirname(this.filePath); + await mkdir(directory, { recursive: true, mode: 0o700 }); + const temporary = `${this.filePath}.${process.pid}.${randomUUID()}.tmp`; + try { + await writeFile(temporary, `${JSON.stringify(this.document)}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 }); + await rename(temporary, this.filePath); + } finally { + await rm(temporary, { force: true }).catch(() => undefined); + } + } + + private aad(identity: string): Buffer { + return Buffer.from(`agent-studio-mini:model-credential:v1:${identity}`, "utf8"); + } +} diff --git a/packages/harness/src/errors.ts b/packages/harness/src/errors.ts new file mode 100644 index 0000000..de2f6b3 --- /dev/null +++ b/packages/harness/src/errors.ts @@ -0,0 +1,31 @@ +export class HarnessError extends Error { + constructor( + public readonly code: string, + message: string, + public readonly statusCode = 400, + public readonly details: Record = {}, + ) { + super(message); + this.name = "HarnessError"; + } +} + +export function safeErrorMessage(error: unknown): string { + if (!(error instanceof Error)) return "Unexpected error"; + return error.message + .replace(/(?:sk|pk|key)-[A-Za-z0-9_-]{8,}/g, "[REDACTED]") + .replace(/Bearer\s+\S+/gi, "Bearer [REDACTED]") + .replace(/[A-Za-z0-9+/]{200,}={0,2}/g, "[REDACTED_DATA]"); +} + +const sensitive = /api[-_]?key|authorization|token|secret|password|credential/i; + +export function redactValue(value: unknown, depth = 0): unknown { + if (depth > 6) return "[TRUNCATED]"; + if (typeof value === "string") return value.length > 2_000 ? `${value.slice(0, 2_000)}…` : safeErrorMessage(new Error(value)); + if (Array.isArray(value)) return value.slice(0, 50).map((item) => redactValue(item, depth + 1)); + if (value && typeof value === "object") { + return Object.fromEntries(Object.entries(value).slice(0, 50).map(([key, item]) => [key, sensitive.test(key) ? "[REDACTED]" : redactValue(item, depth + 1)])); + } + return value; +} diff --git a/packages/harness/src/event-store.ts b/packages/harness/src/event-store.ts new file mode 100644 index 0000000..ff11c51 --- /dev/null +++ b/packages/harness/src/event-store.ts @@ -0,0 +1,65 @@ +import { randomUUID } from "node:crypto"; +import type { EventInput, HarnessEvent, HarnessEventType } from "@agent-studio/shared"; + +export interface EventCursor { sequence?: number; eventId?: string } +export interface EventBatch { events: HarnessEvent[]; gap?: { requestedSequence: number; oldestAvailableSequence: number } } + +export interface EventStore { + append(sessionId: string, event: EventInput): Extract; + listAfter(sessionId: string, cursor?: EventCursor): EventBatch; + subscribe(sessionId: string, listener: (event: HarnessEvent) => void): () => void; + clear(sessionId: string): void; +} + +interface Bucket { sequence: number; events: HarnessEvent[]; listeners: Set<(event: HarnessEvent) => void> } + +export class InMemoryEventStore implements EventStore { + private readonly buckets = new Map(); + constructor(private readonly limit = 1_000) {} + + private bucket(sessionId: string): Bucket { + let bucket = this.buckets.get(sessionId); + if (!bucket) { + bucket = { sequence: 0, events: [], listeners: new Set() }; + this.buckets.set(sessionId, bucket); + } + return bucket; + } + + append(sessionId: string, input: EventInput): Extract { + const bucket = this.bucket(sessionId); + const value = { + ...input, + eventId: `evt_${randomUUID()}`, + sessionId, + timestamp: new Date().toISOString(), + sequence: ++bucket.sequence, + } as Extract; + bucket.events.push(value); + if (bucket.events.length > this.limit) bucket.events.splice(0, bucket.events.length - this.limit); + for (const listener of bucket.listeners) listener(value); + return value; + } + + listAfter(sessionId: string, cursor: EventCursor = {}): EventBatch { + const bucket = this.bucket(sessionId); + let requested = cursor.sequence ?? 0; + if (cursor.eventId) { + const found = bucket.events.find((item) => item.eventId === cursor.eventId); + if (found) requested = found.sequence; + } + const oldest = bucket.events[0]?.sequence; + if (oldest !== undefined && requested > 0 && requested < oldest - 1) { + return { events: bucket.events.slice(), gap: { requestedSequence: requested, oldestAvailableSequence: oldest } }; + } + return { events: bucket.events.filter((item) => item.sequence > requested) }; + } + + subscribe(sessionId: string, listener: (event: HarnessEvent) => void): () => void { + const listeners = this.bucket(sessionId).listeners; + listeners.add(listener); + return () => listeners.delete(listener); + } + + clear(sessionId: string): void { this.buckets.delete(sessionId); } +} diff --git a/packages/harness/src/index.ts b/packages/harness/src/index.ts new file mode 100644 index 0000000..e2046e3 --- /dev/null +++ b/packages/harness/src/index.ts @@ -0,0 +1,11 @@ +export * from "./approval.js"; +export * from "./attachments.js"; +export * from "./credential-vault.js"; +export * from "./errors.js"; +export * from "./event-store.js"; +export * from "./model-store.js"; +export * from "./pi-event-adapter.js"; +export * from "./safe-workspace.js"; +export * from "./session.js"; +export * from "./skills.js"; +export * from "./tools.js"; diff --git a/packages/harness/src/model-store.ts b/packages/harness/src/model-store.ts new file mode 100644 index 0000000..3540304 --- /dev/null +++ b/packages/harness/src/model-store.ts @@ -0,0 +1,178 @@ +import { lookup } from "node:dns/promises"; +import { isIP } from "node:net"; +import type { Model } from "@earendil-works/pi-ai"; +import { InMemoryCredentialStore } from "@earendil-works/pi-ai"; +import { ModelRuntime } from "@earendil-works/pi-coding-agent"; +import { modelConnectionInputSchema, type ModelConnectionInput, type RedactedModelConnection } from "@agent-studio/shared"; +import { HarnessError } from "./errors.js"; +import { modelCredentialIdentity, type CredentialVault } from "./credential-vault.js"; + +interface Entry { input: ModelConnectionInput; apiKey?: string; runtime?: ModelRuntime; credentialStorage?: "memory" | "encrypted" | "environment" } +export interface ResolvedPiModelRuntime { runtime: ModelRuntime; model: Model; supportsImages: boolean } +export interface ModelConnectionStore { + configure(sessionId: string, input: ModelConnectionInput): Promise; + getRedacted(sessionId: string): RedactedModelConnection; + clearCredentials(sessionId: string): Promise; + createRuntime(sessionId: string): Promise; + remove(sessionId: string): Promise; +} + +const metadataHosts = new Set(["169.254.169.254", "100.100.100.200", "metadata.google.internal", "metadata.azure.internal"]); +function restrictedIp(address: string): boolean { + if (!isIP(address)) return true; + if (address === "::1" || address === "0.0.0.0" || address.startsWith("fe80:") || address.startsWith("fc") || address.startsWith("fd")) return true; + const parts = address.split(".").map(Number); + if (parts.length === 4) return parts[0] === 10 || parts[0] === 127 || parts[0] === 0 || (parts[0] === 169 && parts[1] === 254) || (parts[0] === 172 && (parts[1] ?? 0) >= 16 && (parts[1] ?? 0) <= 31) || (parts[0] === 192 && parts[1] === 168) || (parts[0] === 100 && (parts[1] ?? 0) >= 64 && (parts[1] ?? 0) <= 127); + return false; +} + +export async function validateBaseUrl(input: string, allowPrivate = false, development = process.env.NODE_ENV !== "production"): Promise { + let url: URL; + try { url = new URL(input); } catch { throw new HarnessError("INVALID_BASE_URL", "Base URL is not a valid URL", 400); } + if (url.username || url.password || url.hash) throw new HarnessError("INVALID_BASE_URL", "Base URL cannot contain credentials or a fragment", 400); + const hostname = url.hostname.replace(/^\[|\]$/g, ""); + const developmentLoopback = development && url.protocol === "http:" && ["localhost", "127.0.0.1", "::1"].includes(hostname); + if (url.protocol !== "https:" && !developmentLoopback) throw new HarnessError("UNSAFE_BASE_URL", "Base URL must use HTTPS (loopback HTTP is allowed only in development)", 400); + if (metadataHosts.has(hostname.toLowerCase())) throw new HarnessError("SSRF_BLOCKED", "Cloud metadata endpoints are not allowed", 400); + for (const key of url.searchParams.keys()) if (/key|token|secret|auth|password/i.test(key)) throw new HarnessError("CREDENTIAL_IN_URL", "Base URL must not contain credential query parameters", 400); + const addresses = isIP(hostname) ? [{ address: hostname }] : await lookup(hostname, { all: true, verbatim: true }).catch(() => { throw new HarnessError("BASE_URL_DNS_FAILED", "Base URL hostname could not be resolved", 400); }); + if (!allowPrivate && !developmentLoopback && addresses.some(({ address }) => restrictedIp(address))) throw new HarnessError("SSRF_BLOCKED", "Base URL resolves to a private, loopback, or link-local address", 400); + return url.toString().replace(/\/$/, ""); +} + +export class InMemoryModelConnectionStore implements ModelConnectionStore { + private readonly entries = new Map(); + private environmentEntry: Entry | undefined; + constructor(private readonly env: NodeJS.ProcessEnv = process.env, private readonly vault?: CredentialVault) {} + + async initialize(): Promise { + this.environmentEntry = await this.resolveEnvironmentInput(); + } + + async configure(sessionId: string, raw: ModelConnectionInput): Promise { + const input = modelConnectionInputSchema.parse(raw); + const baseUrl = input.baseUrl ? await validateBaseUrl(input.baseUrl, this.env.ALLOW_PRIVATE_LLM_BASE_URLS === "true") : undefined; + const previous = this.entries.get(sessionId); + if (previous?.runtime) await previous.runtime.removeRuntimeApiKey(previous.input.providerId).catch(() => undefined); + const normalized: ModelConnectionInput = { ...input, ...(baseUrl ? { baseUrl } : {}) }; + const identity = modelCredentialIdentity(normalized); + let apiKey = input.apiKey; + let credentialStorage: Entry["credentialStorage"] = apiKey ? this.vault ? "encrypted" : "memory" : undefined; + if (apiKey && this.vault) await this.vault.set(identity, apiKey); + if (!apiKey && previous?.apiKey && modelCredentialIdentity(previous.input) === identity) { + apiKey = previous.apiKey; + credentialStorage = previous.credentialStorage; + } + if (!apiKey && this.vault) { + apiKey = await this.vault.get(identity); + if (apiKey) credentialStorage = "encrypted"; + } + const entry: Entry = { input: normalized }; + if (apiKey) entry.apiKey = apiKey; + if (credentialStorage) entry.credentialStorage = credentialStorage; + this.entries.set(sessionId, entry); + return this.redact(entry, "session"); + } + + getRedacted(sessionId: string): RedactedModelConnection { + const entry = this.entries.get(sessionId); + if (entry) return this.redact(entry, "session"); + return this.environmentEntry ? this.redact(this.environmentEntry, "environment") : { configured: false, source: "missing", keyConfigured: false }; + } + + async clearCredentials(sessionId: string): Promise { + let entry = this.entries.get(sessionId); + if (!entry && this.environmentEntry) { + entry = this.cloneEntry(this.environmentEntry); + this.entries.set(sessionId, entry); + } + if (!entry) return; + if (entry.runtime) await entry.runtime.removeRuntimeApiKey(entry.input.providerId).catch(() => undefined); + if (this.vault) await this.vault.delete(modelCredentialIdentity(entry.input)); + delete entry.apiKey; + delete entry.credentialStorage; + } + + async createRuntime(sessionId: string): Promise { + let entry = this.entries.get(sessionId); + if (!entry && this.environmentEntry) { + entry = this.cloneEntry(this.environmentEntry); + this.entries.set(sessionId, entry); + } + if (!entry) throw new HarnessError("MODEL_NOT_CONFIGURED", "No model connection is configured", 422); + if (!entry.apiKey) throw new HarnessError("MODEL_CREDENTIAL_NOT_CONFIGURED", "The selected provider has no API key", 422); + const checkedBaseUrl = entry.input.baseUrl ? await validateBaseUrl(entry.input.baseUrl, this.env.ALLOW_PRIVATE_LLM_BASE_URLS === "true") : undefined; + const credentials = new InMemoryCredentialStore(); + const runtime = await ModelRuntime.create({ credentials, modelsPath: null, refreshOnCreate: false, allowModelNetwork: false, signal: AbortSignal.timeout(10_000), modelRefreshTimeoutMs: 10_000 }); + const defaultBaseUrl = checkedBaseUrl ?? (entry.input.api === "anthropic-messages" ? "https://api.anthropic.com" : "https://api.openai.com/v1"); + runtime.registerProvider(entry.input.providerId, { + name: entry.input.displayName ?? entry.input.providerId, + baseUrl: defaultBaseUrl, + api: entry.input.api, + models: [{ id: entry.input.modelId, name: entry.input.displayName ?? entry.input.modelId, api: entry.input.api, baseUrl: defaultBaseUrl, reasoning: false, input: entry.input.supportsImages ? ["text", "image"] : ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: entry.input.contextWindow ?? 128_000, maxTokens: entry.input.maxTokens ?? 8_192 }], + }); + await runtime.setRuntimeApiKey(entry.input.providerId, entry.apiKey); + const model = runtime.getModel(entry.input.providerId, entry.input.modelId); + if (!model || model.api !== entry.input.api) throw new HarnessError("MODEL_RESOLUTION_FAILED", "Configured model could not be resolved exactly", 422); + entry.runtime = runtime; + return { runtime, model, supportsImages: model.input.includes("image") }; + } + async remove(sessionId: string): Promise { + const entry = this.entries.get(sessionId); + if (entry?.runtime) await entry.runtime.removeRuntimeApiKey(entry.input.providerId).catch(() => undefined); + this.entries.delete(sessionId); + } + + private async resolveEnvironmentInput(): Promise { + const providerId = this.env.AGENT_PROVIDER; + const modelId = this.env.AGENT_MODEL; + if (!providerId && !modelId) return undefined; + if (!providerId || !modelId) throw new HarnessError("INVALID_ENV_MODEL_CONFIG", "AGENT_PROVIDER and AGENT_MODEL must be configured together", 500); + try { + const input = modelConnectionInputSchema.parse({ + providerId, + modelId, + api: this.env.LLM_API ?? "openai-responses", + supportsImages: parseEnvironmentBoolean(this.env.AGENT_SUPPORTS_IMAGES, false, "AGENT_SUPPORTS_IMAGES"), + ...(this.env.LLM_BASE_URL ? { baseUrl: this.env.LLM_BASE_URL } : {}), + ...(this.env.AGENT_DISPLAY_NAME ? { displayName: this.env.AGENT_DISPLAY_NAME } : {}), + ...(this.env.AGENT_CONTEXT_WINDOW ? { contextWindow: parseEnvironmentInteger(this.env.AGENT_CONTEXT_WINDOW, "AGENT_CONTEXT_WINDOW") } : {}), + ...(this.env.AGENT_MAX_TOKENS ? { maxTokens: parseEnvironmentInteger(this.env.AGENT_MAX_TOKENS, "AGENT_MAX_TOKENS") } : {}), + }); + const baseUrl = input.baseUrl ? await validateBaseUrl(input.baseUrl, this.env.ALLOW_PRIVATE_LLM_BASE_URLS === "true") : undefined; + const normalized: ModelConnectionInput = { ...input, ...(baseUrl ? { baseUrl } : {}) }; + let apiKey = input.api === "anthropic-messages" ? this.env.ANTHROPIC_API_KEY : this.env.OPENAI_API_KEY; + let credentialStorage: Entry["credentialStorage"] = apiKey ? "environment" : undefined; + if (!apiKey && this.vault) { + apiKey = await this.vault.get(modelCredentialIdentity(normalized)); + if (apiKey) credentialStorage = "encrypted"; + } + const entry: Entry = { input: normalized }; + if (apiKey) entry.apiKey = apiKey; + if (credentialStorage) entry.credentialStorage = credentialStorage; + return entry; + } catch (error) { + if (error instanceof HarnessError) throw error; + throw new HarnessError("INVALID_ENV_MODEL_CONFIG", "Environment model configuration is invalid", 500); + } + } + private cloneEntry(entry: Entry): Entry { + return { input: { ...entry.input }, ...(entry.apiKey ? { apiKey: entry.apiKey } : {}), ...(entry.credentialStorage ? { credentialStorage: entry.credentialStorage } : {}) }; + } + private redact(entry: Entry, source: "session" | "environment"): RedactedModelConnection { + const { input, apiKey } = entry; + return { configured: true, source, providerId: input.providerId, api: input.api, modelId: input.modelId, supportsImages: input.supportsImages, keyConfigured: Boolean(apiKey), ...(input.baseUrl ? { baseUrl: input.baseUrl } : {}), ...(input.displayName ? { displayName: input.displayName } : {}), ...(apiKey ? { keyHint: `••••${apiKey.slice(-4)}` } : {}), ...(entry.credentialStorage ? { credentialStorage: entry.credentialStorage } : {}) }; + } +} + +function parseEnvironmentBoolean(value: string | undefined, fallback: boolean, name: string): boolean { + if (value === undefined || value === "") return fallback; + if (value === "true") return true; + if (value === "false") return false; + throw new HarnessError("INVALID_ENV_MODEL_CONFIG", `${name} must be true or false`, 500); +} + +function parseEnvironmentInteger(value: string, name: string): number { + if (!/^\d+$/.test(value)) throw new HarnessError("INVALID_ENV_MODEL_CONFIG", `${name} must be a positive integer`, 500); + return Number(value); +} diff --git a/packages/harness/src/pi-event-adapter.ts b/packages/harness/src/pi-event-adapter.ts new file mode 100644 index 0000000..bca8f94 --- /dev/null +++ b/packages/harness/src/pi-event-adapter.ts @@ -0,0 +1,30 @@ +import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent"; +import type { EventInput, HarnessEventType } from "@agent-studio/shared"; +import { redactValue } from "./errors.js"; + +export interface PiAdapterContext { runId: string; assistantMessageId: string } +export type MappedEvent = { [T in HarnessEventType]: EventInput }[HarnessEventType]; + +export function mapPiEvent(event: AgentSessionEvent, context: PiAdapterContext): MappedEvent[] { + switch (event.type) { + case "message_update": + if (event.assistantMessageEvent.type === "text_delta") return [{ type: "assistant.delta", runId: context.runId, payload: { messageId: context.assistantMessageId, delta: event.assistantMessageEvent.delta } }]; + return []; + case "message_end": + if (event.message.role === "assistant") return [{ type: "assistant.completed", runId: context.runId, payload: { messageId: context.assistantMessageId } }]; + return []; + case "tool_execution_start": + return [{ type: "tool.requested", runId: context.runId, payload: { toolCallId: event.toolCallId, toolName: event.toolName, arguments: redactValue(event.args) } }]; + case "tool_execution_end": + return event.isError + ? [{ type: "tool.failed", runId: context.runId, payload: { toolCallId: event.toolCallId, toolName: event.toolName, error: "Tool execution failed" } }] + : [{ type: "tool.completed", runId: context.runId, payload: { toolCallId: event.toolCallId, toolName: event.toolName, result: summarizeToolResult(event.toolName, event.result) } }]; + default: + return []; + } +} + +function summarizeToolResult(toolName: string, result: unknown): unknown { + if (["read", "grep", "find", "ls"].includes(toolName)) return { status: "success" }; + return redactValue(result); +} diff --git a/packages/harness/src/safe-workspace.ts b/packages/harness/src/safe-workspace.ts new file mode 100644 index 0000000..338e6b2 --- /dev/null +++ b/packages/harness/src/safe-workspace.ts @@ -0,0 +1,58 @@ +import { randomUUID } from "node:crypto"; +import { constants } from "node:fs"; +import { access, lstat, mkdir, open, realpath, rename, unlink } from "node:fs/promises"; +import path from "node:path"; +import { HarnessError } from "./errors.js"; + +export class SafeWorkspace { + private constructor(public readonly root: string) {} + + static async create(root: string): Promise { + await mkdir(root, { recursive: true, mode: 0o700 }); + return new SafeWorkspace(await realpath(root)); + } + + private contains(candidate: string): boolean { + const relative = path.relative(this.root, candidate); + return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative)); + } + + private async assertParentsNoSymlink(candidate: string): Promise { + const relative = path.relative(this.root, path.dirname(candidate)); + let current = this.root; + for (const segment of relative.split(path.sep).filter(Boolean)) { + current = path.join(current, segment); + const stat = await lstat(current); + if (stat.isSymbolicLink()) throw new HarnessError("PATH_SYMLINK_DENIED", "Symbolic links are not allowed", 403); + } + } + + async resolveExisting(input: string): Promise { + const absolute = path.resolve(this.root, input); + if (!this.contains(absolute)) throw new HarnessError("PATH_OUTSIDE_WORKSPACE", "Path is outside the session workspace", 403); + const canonical = await realpath(absolute); + if (!this.contains(canonical)) throw new HarnessError("PATH_OUTSIDE_WORKSPACE", "Path resolves outside the session workspace", 403); + const stat = await lstat(absolute); + if (stat.isSymbolicLink()) throw new HarnessError("PATH_SYMLINK_DENIED", "Symbolic links are not allowed", 403); + return canonical; + } + + async assertReadPath(input: string): Promise { + if (/(^|[/\\])(?:\.env(?:\.|$)|\.git|\.ssh)([/\\]|$)/i.test(input)) throw new HarnessError("PROTECTED_PATH", "Protected path is not readable", 403); + await this.resolveExisting(input); + } + + async writeReport(content: string, title?: string): Promise<{ path: "report.md"; byteSize: number }> { + const target = path.join(this.root, "report.md"); + if (!this.contains(target)) throw new HarnessError("PATH_OUTSIDE_WORKSPACE", "Unsafe report path", 403); + await this.assertParentsNoSymlink(target); + try { if ((await lstat(target)).isSymbolicLink()) throw new HarnessError("PATH_SYMLINK_DENIED", "Report path is a symbolic link", 403); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; } + const body = title ? `# ${title}\n\n${content}` : content; + const temporary = path.join(this.root, `.report-${randomUUID()}.tmp`); + const handle = await open(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o600); + try { await handle.writeFile(body, "utf8"); await handle.sync(); } finally { await handle.close(); } + try { await rename(temporary, target); } catch (error) { await unlink(temporary).catch(() => undefined); throw error; } + await access(target, constants.R_OK); + return { path: "report.md", byteSize: Buffer.byteLength(body) }; + } +} diff --git a/packages/harness/src/session.ts b/packages/harness/src/session.ts new file mode 100644 index 0000000..22e5d7e --- /dev/null +++ b/packages/harness/src/session.ts @@ -0,0 +1,303 @@ +import { randomUUID } from "node:crypto"; +import { mkdir, realpath, rm } from "node:fs/promises"; +import path from "node:path"; +import type { ImageContent } from "@earendil-works/pi-ai"; +import { + SessionManager, + SettingsManager, + createAgentSession, + isToolCallEventType, + type AgentSession, + type AgentSessionEvent, + type InlineExtension, +} from "@earendil-works/pi-coding-agent"; +import type { ModelConnectionInput, SendMessageInput } from "@agent-studio/shared"; +import { InMemoryApprovalBroker, type ApprovalBroker } from "./approval.js"; +import type { FileAttachmentStore } from "./attachments.js"; +import { HarnessError, redactValue, safeErrorMessage } from "./errors.js"; +import type { EventStore } from "./event-store.js"; +import type { ModelConnectionStore } from "./model-store.js"; +import { mapPiEvent } from "./pi-event-adapter.js"; +import { SafeWorkspace } from "./safe-workspace.js"; +import type { SkillCatalog } from "./skills.js"; +import type { SkillIndexEntry } from "./skills.js"; +import { createMockAdsMetricsTool, createSaveReportDraftTool } from "./tools.js"; + +export interface AgentSessionPort { + readonly modelId: string; + readonly supportsImages: boolean; + prompt(text: string, options?: { images?: unknown[] }): Promise; + abort(): Promise; + subscribe(listener: (event: unknown) => void): () => void; + dispose(): void; +} + +export interface AgentSessionFactoryInput { + sessionId: string; + cwd: string; + workspace: SafeWorkspace; + getRun: () => { runId: string; signal: AbortSignal; assistantMessageId: string } | undefined; + skillRequested: (skill: SkillIndexEntry, toolCallId: string) => void; + skillLoaded: (skill: SkillIndexEntry, toolCallId: string) => void; + toolRequested: (toolCallId: string, toolName: string, argumentsValue: unknown) => void; +} +export interface AgentSessionFactory { create(input: AgentSessionFactoryInput): Promise } + +class PiAgentSessionPort implements AgentSessionPort { + readonly modelId: string; + readonly supportsImages: boolean; + constructor(private readonly session: AgentSession) { + if (!session.model) throw new Error("Pi AgentSession was created without an explicit model"); + this.modelId = session.model.id; + this.supportsImages = session.model.input.includes("image"); + } + async prompt(text: string, options?: { images?: unknown[] }): Promise { + const images = options?.images as ImageContent[] | undefined; + await this.session.prompt(text, images ? { images } : undefined); + } + abort(): Promise { return this.session.abort(); } + subscribe(listener: (event: unknown) => void): () => void { return this.session.subscribe(listener); } + dispose(): void { this.session.dispose(); } +} + +export class PiAgentSessionFactory implements AgentSessionFactory { + constructor( + private readonly fixturePath: string, + private readonly catalog: SkillCatalog, + private readonly models: ModelConnectionStore, + private readonly approvals: ApprovalBroker, + ) {} + + async create(input: AgentSessionFactoryInput): Promise { + const resolved = await this.models.createRuntime(input.sessionId); + const extension: InlineExtension = { name: "agent-studio-safety", hidden: true, factory: (pi) => { + const pendingSkills = new Map(); + pi.on("tool_call", async (event) => { + const run = input.getRun(); + if (!run) return { block: true, reason: "No active Harness run" }; + const known = new Set(["read", "grep", "find", "ls", "mock_ads_metrics", "save_report_draft"]); + if (!known.has(event.toolName)) { + input.toolRequested(event.toolCallId, event.toolName, publicToolArguments(event.input, input.cwd)); + return { block: true, reason: "Tool is not allowed by the Harness policy", terminate: true }; + } + let matchedSkill: SkillIndexEntry | undefined; + if (isToolCallEventType("read", event)) { + const skill = await this.catalog.matchReadPath(event.input.path, input.cwd); + if (skill) { + matchedSkill = skill; + pendingSkills.set(event.toolCallId, skill); + input.skillRequested(skill, event.toolCallId); + } else await input.workspace.assertReadPath(event.input.path); + } else if (["grep", "find", "ls"].includes(event.toolName)) { + const maybePath = (event.input as Record).path; + if (typeof maybePath === "string") await input.workspace.assertReadPath(maybePath); + } + input.toolRequested(event.toolCallId, event.toolName, publicToolArguments(event.input, input.cwd, matchedSkill)); + if (event.toolName === "save_report_draft") { + const decision = await this.approvals.request({ sessionId: input.sessionId, runId: run.runId, toolCallId: event.toolCallId, toolName: event.toolName, arguments: event.input, riskLevel: "medium" }, run.signal); + if (decision.status !== "approved") return { block: true, reason: `Human approval ${decision.status}` }; + } + return undefined; + }); + pi.on("tool_result", async (event) => { + const run = input.getRun(); + const skill = pendingSkills.get(event.toolCallId); + if (run && skill && !event.isError && event.toolName === "read") { + input.skillLoaded(skill, event.toolCallId); + pendingSkills.delete(event.toolCallId); + } + }); + } }; + const loader = await this.catalog.createSessionLoader(input.cwd, path.join(input.cwd, ".pi-agent"), [extension]); + const { session } = await createAgentSession({ + cwd: input.cwd, + agentDir: path.join(input.cwd, ".pi-agent"), + modelRuntime: resolved.runtime, + model: resolved.model, + tools: ["read", "grep", "find", "ls", "mock_ads_metrics", "save_report_draft"], + customTools: [createMockAdsMetricsTool(this.fixturePath), createSaveReportDraftTool(input.workspace)], + resourceLoader: loader, + sessionManager: SessionManager.inMemory(input.cwd), + settingsManager: SettingsManager.inMemory(), + }); + return new PiAgentSessionPort(session); + } +} + +export interface HarnessSession { + sessionId: string; + cwd: string; + workspace: SafeWorkspace; + busy: boolean; + currentRun: { runId: string; controller: AbortController; assistantMessageId: string; cancelReason?: "user" | "timeout" | "session_deleted" | "shutdown" } | undefined; +} +export interface CreateSessionInput { id?: string } +export interface SessionRegistry { + create(input?: CreateSessionInput): Promise; + get(sessionId: string): HarnessSession | undefined; + delete(sessionId: string): Promise; +} + +interface InternalSession extends HarnessSession { agent: AgentSessionPort | undefined; unsubscribe: (() => void) | undefined; timeout: ReturnType | undefined; disposed: boolean; requestedToolCalls: Set } + +export class InMemorySessionRegistry implements SessionRegistry { + private readonly sessions = new Map(); + readonly approvals: ApprovalBroker; + constructor( + private readonly sessionsRoot: string, + private readonly events: EventStore, + private readonly attachments: FileAttachmentStore, + private readonly models: ModelConnectionStore, + private readonly catalog: SkillCatalog, + private factory: AgentSessionFactory | undefined, + approvalTimeoutMs = 60_000, + private readonly runTimeoutMs = 120_000, + ) { + this.approvals = new InMemoryApprovalBroker(approvalTimeoutMs, (resolution) => { + this.events.append(resolution.request.sessionId, { type: "approval.resolved", runId: resolution.request.runId, payload: { approvalId: resolution.request.approvalId, toolCallId: resolution.request.toolCallId, status: resolution.decision.status, resolvedAt: resolution.decision.resolvedAt } }); + }, (request) => { + this.events.append(request.sessionId, { type: "approval.required", runId: request.runId, payload: request }); + }); + } + setFactory(factory: AgentSessionFactory): void { this.factory = factory; } + + async create(input: CreateSessionInput = {}): Promise { + const sessionId = input.id ?? `ses_${randomUUID()}`; + const cwd = path.join(this.sessionsRoot, sessionId); + await mkdir(cwd, { recursive: false, mode: 0o700 }); + const workspace = await SafeWorkspace.create(cwd); + const session: InternalSession = { sessionId, cwd: workspace.root, workspace, busy: false, currentRun: undefined, agent: undefined, unsubscribe: undefined, timeout: undefined, disposed: false, requestedToolCalls: new Set() }; + this.sessions.set(sessionId, session); + this.events.append(sessionId, { type: "session.started", payload: { cwd: `workspace/sessions/${sessionId}` } }); + return session; + } + get(id: string): HarnessSession | undefined { return this.sessions.get(id); } + private internal(id: string): InternalSession { const session = this.sessions.get(id); if (!session) throw new HarnessError("SESSION_NOT_FOUND", "Session not found", 404); return session; } + + async configureModel(id: string, input: ModelConnectionInput) { + const session = this.internal(id); + if (session.busy) throw new HarnessError("SESSION_BUSY", "Cannot change model configuration while a run is active", 409); + await this.disposeAgent(session); + const value = await this.models.configure(id, input); + this.events.append(id, { type: "model.configured", payload: { providerId: input.providerId, modelId: input.modelId, api: input.api, supportsImages: input.supportsImages } }); + return value; + } + async clearCredentials(id: string): Promise { const session = this.internal(id); if (session.busy) throw new HarnessError("SESSION_BUSY", "Cannot clear credentials while a run is active", 409); await this.disposeAgent(session); await this.models.clearCredentials(id); } + + async send(id: string, message: SendMessageInput): Promise<{ runId: string }> { + const session = this.internal(id); + if (session.busy) throw new HarnessError("SESSION_BUSY", "A run is already active for this session", 409); + const configuredModel = this.models.getRedacted(id); + if (!configuredModel.configured) throw new HarnessError("MODEL_NOT_CONFIGURED", "No model connection is configured", 422); + if (!configuredModel.keyConfigured) throw new HarnessError("MODEL_CREDENTIAL_NOT_CONFIGURED", "The selected provider has no API key", 422); + const maxImages = Number(process.env.MAX_IMAGES_PER_MESSAGE ?? 4); + if (message.attachmentIds.length > maxImages) throw new HarnessError("TOO_MANY_ATTACHMENTS", `A message can include at most ${maxImages} images`, 413); + const selected = message.attachmentIds.map((attachmentId) => { + const item = this.attachments.getStored(id, attachmentId); + if (!item) throw new HarnessError("ATTACHMENT_NOT_FOUND", "Attachment not found for this session", 404); + return item; + }); + const runId = `run_${randomUUID()}`; + const assistantMessageId = `msg_${randomUUID()}`; + const controller = new AbortController(); + session.currentRun = { runId, controller, assistantMessageId }; + session.busy = true; + try { await this.ensureAgent(session); } catch (error) { session.busy = false; session.currentRun = undefined; this.events.append(id, { type: "run.failed", runId, payload: { code: "AGENT_SESSION_CREATE_FAILED", message: safeErrorMessage(error) } }); throw error; } + if (selected.length > 0 && !session.agent?.supportsImages) { + session.busy = false; + session.currentRun = undefined; + throw new HarnessError("MODEL_DOES_NOT_SUPPORT_IMAGES", "The selected model does not support image input", 422); + } + const agent = session.agent; + if (!agent) throw new HarnessError("AGENT_SESSION_CREATE_FAILED", "Agent session was not created", 500); + this.events.append(id, { type: "user.message", runId, payload: { messageId: `msg_${randomUUID()}`, text: message.text, attachmentIds: message.attachmentIds } }); + this.events.append(id, { type: "run.started", runId, payload: { modelId: agent.modelId } }); + const images: ImageContent[] = await Promise.all(selected.map(async (item) => ({ type: "image" as const, mimeType: item.mediaType, data: await this.attachments.readBase64(id, item.attachmentId) }))); + session.timeout = setTimeout(() => void this.cancel(id, "timeout"), this.runTimeoutMs); + void this.executeRun(session, message.text, images); + return { runId }; + } + + private async ensureAgent(session: InternalSession): Promise { + if (session.agent) return; + if (!this.factory) throw new Error("Agent session factory is not configured"); + session.agent = await this.factory.create({ + sessionId: session.sessionId, + cwd: session.cwd, + workspace: session.workspace, + getRun: () => session.currentRun ? { runId: session.currentRun.runId, signal: session.currentRun.controller.signal, assistantMessageId: session.currentRun.assistantMessageId } : undefined, + skillRequested: (skill, toolCallId) => { const run = session.currentRun; if (run) this.events.append(session.sessionId, { type: "skill.requested", runId: run.runId, payload: { name: skill.name, filePath: skill.filePath, toolCallId } }); }, + skillLoaded: (skill, toolCallId) => { const run = session.currentRun; if (run) this.events.append(session.sessionId, { type: "skill.loaded", runId: run.runId, payload: { name: skill.name, filePath: skill.filePath, toolCallId } }); }, + toolRequested: (toolCallId, toolName, argumentsValue) => { const run = session.currentRun; if (run && !session.requestedToolCalls.has(toolCallId)) { session.requestedToolCalls.add(toolCallId); this.events.append(session.sessionId, { type: "tool.requested", runId: run.runId, payload: { toolCallId, toolName, arguments: redactValue(argumentsValue) } }); } }, + }); + session.unsubscribe = session.agent.subscribe((event) => this.onPiEvent(session, event as AgentSessionEvent)); + } + private onPiEvent(session: InternalSession, event: AgentSessionEvent): void { + const run = session.currentRun; + if (!run) return; + // The blocking Extension preflight is the single source for tool.requested. + // Pi's later execution-start event has the same toolCallId and would create + // duplicate assistant-ui Tool parts (and can expose pre-normalized paths). + if (event.type === "tool_execution_start") return; + for (const mapped of mapPiEvent(event, { runId: run.runId, assistantMessageId: run.assistantMessageId })) this.events.append(session.sessionId, mapped); + } + private async executeRun(session: InternalSession, text: string, images: ImageContent[]): Promise { + const run = session.currentRun; + if (!run || !session.agent) return; + try { + await session.agent.prompt(text, images.length ? { images } : undefined); + if (!run.controller.signal.aborted) this.events.append(session.sessionId, { type: "run.completed", runId: run.runId, payload: {} }); + } catch (error) { + if (!run.controller.signal.aborted) this.events.append(session.sessionId, { type: "run.failed", runId: run.runId, payload: { code: "AGENT_RUN_FAILED", message: safeErrorMessage(error) } }); + } finally { + if (session.timeout) clearTimeout(session.timeout); + session.timeout = undefined; + session.busy = false; + if (session.currentRun?.runId === run.runId) session.currentRun = undefined; + } + } + async cancel(id: string, reason: "user" | "timeout" | "session_deleted" | "shutdown" = "user"): Promise { + const session = this.internal(id); + const run = session.currentRun; + if (!run || !session.busy || run.controller.signal.aborted) return; + run.cancelReason = reason; + run.controller.abort(reason); + this.approvals.cancelForSession(id); + await session.agent?.abort().catch(() => undefined); + this.events.append(id, { type: "run.cancelled", runId: run.runId, payload: { reason } }); + } + async delete(id: string, endReason: "deleted" | "shutdown" = "deleted"): Promise { + const session = this.internal(id); + if (session.busy) await this.cancel(id, endReason === "shutdown" ? "shutdown" : "session_deleted"); + this.approvals.cancelForSession(id); + this.events.append(id, { type: "session.ended", payload: { reason: endReason } }); + await this.disposeAgent(session); + await this.attachments.removeAll(id); + await this.models.remove(id); + const root = await realpath(this.sessionsRoot); + const target = await realpath(session.cwd); + const relative = path.relative(root, target); + if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) throw new HarnessError("UNSAFE_SESSION_DELETE", "Refusing unsafe session directory deletion", 500); + await rm(target, { recursive: true, force: true }); + session.disposed = true; + this.sessions.delete(id); + } + async shutdown(): Promise { for (const id of [...this.sessions.keys()]) await this.delete(id, "shutdown"); } + private async disposeAgent(session: InternalSession): Promise { session.unsubscribe?.(); session.unsubscribe = undefined; session.agent?.dispose(); session.agent = undefined; } +} + +function publicToolArguments(value: unknown, cwd: string, skill?: SkillIndexEntry): unknown { + const redacted = redactValue(value); + if (!redacted || typeof redacted !== "object" || Array.isArray(redacted)) return redacted; + const result = { ...(redacted as Record) }; + for (const key of ["path", "cwd", "filePath"]) { + const candidate = result[key]; + if (typeof candidate !== "string" || !path.isAbsolute(candidate)) continue; + if (skill && key === "path") result[key] = skill.filePath; + else { + const relative = path.relative(cwd, candidate); + result[key] = relative && !relative.startsWith("..") && !path.isAbsolute(relative) ? relative : ""; + } + } + return result; +} diff --git a/packages/harness/src/skills.ts b/packages/harness/src/skills.ts new file mode 100644 index 0000000..8ed18e6 --- /dev/null +++ b/packages/harness/src/skills.ts @@ -0,0 +1,91 @@ +import { access, realpath, stat } from "node:fs/promises"; +import path from "node:path"; +import { DefaultResourceLoader, SettingsManager, type InlineExtension, type ResourceDiagnostic, type Skill } from "@earendil-works/pi-coding-agent"; +import type { SkillCatalogItem } from "@agent-studio/shared"; + +export interface SkillIndexEntry extends SkillCatalogItem { canonicalPath: string } + +export class SkillCatalog { + private items: SkillIndexEntry[] = []; + private loader?: DefaultResourceLoader; + private canonicalDirectories: string[] = []; + constructor(private readonly projectRoot: string, private readonly agentDir: string, private readonly envDirs = process.env.AGENT_SKILLS_DIRS) {} + + async reload(): Promise { + const diagnostics: Array<{ directory: string; diagnostic: ResourceDiagnostic }> = []; + const candidates = [path.join(this.projectRoot, ".agents", "skills"), path.join(this.projectRoot, ".pi", "skills")]; + if (this.envDirs) candidates.push(...this.envDirs.split(path.delimiter).map((item) => item.trim()).filter(Boolean)); + const canonical = new Map(); + for (const candidate of candidates) { + try { + const resolved = await realpath(path.resolve(candidate)); + if (!(await stat(resolved)).isDirectory()) throw new Error("not a directory"); + await access(resolved); + canonical.set(resolved, candidate); + } catch { + diagnostics.push({ directory: candidate, diagnostic: { type: "error", message: `Skill directory is missing, unreadable, or not a directory: ${this.displayPath(candidate)}`, path: candidate } }); + } + } + this.loader = new DefaultResourceLoader({ + cwd: this.projectRoot, + agentDir: this.agentDir, + settingsManager: SettingsManager.inMemory(), + additionalSkillPaths: [...canonical.keys()], + noExtensions: true, + noPromptTemplates: true, + noThemes: true, + noContextFiles: true, + }); + this.canonicalDirectories = [...canonical.keys()]; + await this.loader.reload(); + const discovered = this.loader.getSkills(); + const byPath = new Map(); + for (const skill of discovered.skills) { + const skillPath = await realpath(skill.filePath).catch(() => path.resolve(skill.filePath)); + const related = discovered.diagnostics.filter((item) => item.path && path.resolve(item.path) === path.resolve(skill.filePath)); + byPath.set(skillPath, this.toItem(skill, skillPath, related)); + } + for (const { directory, diagnostic } of diagnostics) { + const key = `diagnostic:${path.resolve(directory)}`; + byPath.set(key, { name: path.basename(directory) || "unknown", description: "Skill directory diagnostic", source: "environment", filePath: this.displayPath(directory), canonicalPath: key, diagnostics: [{ severity: "error", message: diagnostic.message }] }); + } + for (const diagnostic of discovered.diagnostics.filter((item) => ![...byPath.values()].some((skill) => item.path && path.resolve(item.path) === path.resolve(skill.canonicalPath)))) { + const key = `diagnostic:${diagnostic.path ?? diagnostic.message}`; + byPath.set(key, { name: diagnostic.path ? path.basename(path.dirname(diagnostic.path)) : "unknown", description: "Invalid Skill", source: "pi-loader", filePath: this.displayPath(diagnostic.path ?? "unknown"), canonicalPath: key, diagnostics: [{ severity: diagnostic.type === "warning" ? "warning" : "error", message: diagnostic.message }] }); + } + this.items = [...byPath.values()].sort((a, b) => a.name.localeCompare(b.name)); + } + + getResourceLoader(): DefaultResourceLoader { + if (!this.loader) throw new Error("Skill catalog has not been loaded"); + return this.loader; + } + async createSessionLoader(cwd: string, agentDir: string, extensionFactories: InlineExtension[]): Promise { + const loader = new DefaultResourceLoader({ + cwd, + agentDir, + settingsManager: SettingsManager.inMemory(), + additionalSkillPaths: this.canonicalDirectories, + extensionFactories, + noPromptTemplates: true, + noThemes: true, + noContextFiles: true, + }); + await loader.reload({ resolveProjectTrust: async () => true }); + return loader; + } + list(): SkillCatalogItem[] { return this.items.map((item) => ({ name: item.name, description: item.description, source: item.source, filePath: item.filePath, diagnostics: item.diagnostics })); } + findCanonical(filePath: string): SkillIndexEntry | undefined { return this.items.find((item) => item.canonicalPath === filePath); } + async matchReadPath(inputPath: string, cwd: string): Promise { + const canonical = await realpath(path.resolve(cwd, inputPath)).catch(() => undefined); + return canonical ? this.findCanonical(canonical) : undefined; + } + private toItem(skill: Skill, canonicalPath: string, diagnostics: ResourceDiagnostic[]): SkillIndexEntry { + return { name: skill.name, description: skill.description, source: skill.sourceInfo.source, filePath: this.displayPath(canonicalPath), canonicalPath, diagnostics: diagnostics.map((item) => ({ severity: item.type === "warning" ? "warning" : "error", message: item.message })) }; + } + private displayPath(input: string): string { + const absolute = path.resolve(input); + const relative = path.relative(this.projectRoot, absolute); + return relative && !relative.startsWith("..") && !path.isAbsolute(relative) ? relative : `/${path.basename(absolute)}`; + } +} diff --git a/packages/harness/src/tools.ts b/packages/harness/src/tools.ts new file mode 100644 index 0000000..55660e8 --- /dev/null +++ b/packages/harness/src/tools.ts @@ -0,0 +1,59 @@ +import { readFile } from "node:fs/promises"; +import { defineTool } from "@earendil-works/pi-coding-agent"; +import { Type } from "typebox"; +import { SafeWorkspace } from "./safe-workspace.js"; + +interface CampaignInput { id: string; name: string; spend: number; impressions: number; clicks: number; conversions: number; revenue: number } +interface Fixture { accountId: string; currency: string; timeRange: { from: string; to: string }; campaigns: CampaignInput[] } +export interface CampaignMetrics extends CampaignInput { ctr: number; cpa: number; roas: number } +export interface AdsMetrics { accountId: string; days: number; currency: string; timeRange: { from: string; to: string }; spend: number; impressions: number; clicks: number; ctr: number; conversions: number; cpa: number; revenue: number; roas: number; campaigns: CampaignMetrics[] } + +const round = (value: number, digits = 2): number => Number(value.toFixed(digits)); +const ratio = (numerator: number, denominator: number, scale = 1): number => denominator === 0 ? 0 : round((numerator / denominator) * scale, 4); + +export const mockAdsMetricsParameters = Type.Object({ + accountId: Type.String({ minLength: 1, maxLength: 100, pattern: "^[a-zA-Z0-9_-]+$" }), + days: Type.Integer({ minimum: 1, maximum: 30 }), +}, { additionalProperties: false }); + +export async function getMockAdsMetrics(fixturePath: string, accountId: string, days: number): Promise { + const fixture = JSON.parse(await readFile(fixturePath, "utf8")) as Fixture; + if (accountId !== fixture.accountId) throw new Error(`Unknown demo account: ${accountId}`); + const campaigns = fixture.campaigns.map((item) => ({ ...item, spend: round(item.spend), revenue: round(item.revenue), ctr: ratio(item.clicks, item.impressions, 100), cpa: ratio(item.spend, item.conversions), roas: ratio(item.revenue, item.spend) })); + const totals = campaigns.reduce((sum, item) => ({ spend: sum.spend + item.spend, impressions: sum.impressions + item.impressions, clicks: sum.clicks + item.clicks, conversions: sum.conversions + item.conversions, revenue: sum.revenue + item.revenue }), { spend: 0, impressions: 0, clicks: 0, conversions: 0, revenue: 0 }); + return { accountId, days, currency: fixture.currency, timeRange: fixture.timeRange, spend: round(totals.spend), impressions: totals.impressions, clicks: totals.clicks, ctr: ratio(totals.clicks, totals.impressions, 100), conversions: totals.conversions, cpa: ratio(totals.spend, totals.conversions), revenue: round(totals.revenue), roas: ratio(totals.revenue, totals.spend), campaigns }; +} + +export function createMockAdsMetricsTool(fixturePath: string) { + return defineTool({ + name: "mock_ads_metrics", + label: "Mock Ads Metrics", + description: "Read deterministic classroom advertising metrics for a demo account. This is not real platform data.", + promptSnippet: "Read classroom advertising metrics", + parameters: mockAdsMetricsParameters, + async execute(_toolCallId, params) { + const data = await getMockAdsMetrics(fixturePath, params.accountId, params.days); + return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], details: data }; + }, + }); +} + +export const saveReportDraftParameters = Type.Object({ + content: Type.String({ minLength: 1, maxLength: 100_000 }), + title: Type.Optional(Type.String({ minLength: 1, maxLength: 200 })), +}, { additionalProperties: false }); + +export function createSaveReportDraftTool(workspace: SafeWorkspace) { + return defineTool({ + name: "save_report_draft", + label: "Save Report Draft", + description: "Save a report draft to the fixed report.md file in this session workspace. Requires human approval.", + promptSnippet: "Save an approved report draft", + parameters: saveReportDraftParameters, + executionMode: "sequential", + async execute(_toolCallId, params) { + const result = await workspace.writeReport(params.content, params.title); + return { content: [{ type: "text", text: `Report draft saved to ${result.path} (${result.byteSize} bytes).` }], details: result }; + }, + }); +} diff --git a/packages/harness/tests/attachments.test.ts b/packages/harness/tests/attachments.test.ts new file mode 100644 index 0000000..9f03df4 --- /dev/null +++ b/packages/harness/tests/attachments.test.ts @@ -0,0 +1,17 @@ +import { mkdtemp } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import sharp from "sharp"; +import { FileAttachmentStore } from "../src/index.js"; + +async function image(format: "jpeg" | "png" | "webp") { const pipeline = sharp({ create: { width: 4, height: 3, channels: 3, background: "#25f4ee" } }); return format === "jpeg" ? pipeline.jpeg().toBuffer() : format === "png" ? pipeline.png().toBuffer() : pipeline.webp().toBuffer(); } +function upload(filename: string, mimetype: string, data: Buffer) { return { filename, mimetype, stream: (async function* () { yield data; })() }; } + +describe("FileAttachmentStore", () => { + it.each([["jpeg", "photo.jpg", "image/jpeg"], ["png", "photo.png", "image/png"], ["webp", "photo.webp", "image/webp"]] as const)("normalizes valid %s", async (format, name, mime) => { const root = await mkdtemp(path.join(tmpdir(), "studio-images-")); const store = new FileAttachmentStore(root); const item = await store.add("s", upload(name, mime, await image(format))); expect(item).toMatchObject({ sessionId: "s", mediaType: mime, width: 4, height: 3, status: "ready" }); }); + it("rejects spoofed MIME and SVG", async () => { const root = await mkdtemp(path.join(tmpdir(), "studio-images-")); const store = new FileAttachmentStore(root); await expect(store.add("s", upload("fake.jpg", "image/jpeg", Buffer.from("not jpeg")))).rejects.toMatchObject({ code: "ATTACHMENT_MAGIC_MISMATCH" }); await expect(store.add("s", upload("x.svg", "image/svg+xml", Buffer.from("")))).rejects.toMatchObject({ code: "ATTACHMENT_TYPE_UNSUPPORTED" }); }); + it("rejects an oversized stream before decoding", async () => { const root = await mkdtemp(path.join(tmpdir(), "studio-images-")); const store = new FileAttachmentStore(root, 10); await expect(store.add("s", upload("x.jpg", "image/jpeg", Buffer.alloc(11)))).rejects.toMatchObject({ code: "ATTACHMENT_TOO_LARGE" }); }); + it("isolates attachments across sessions", async () => { const root = await mkdtemp(path.join(tmpdir(), "studio-images-")); const store = new FileAttachmentStore(root); const item = await store.add("one", upload("x.png", "image/png", await image("png"))); expect(store.get("two", item.attachmentId)).toBeUndefined(); }); + it("removes all session attachment state", async () => { const root = await mkdtemp(path.join(tmpdir(), "studio-images-")); const store = new FileAttachmentStore(root); const item = await store.add("one", upload("x.png", "image/png", await image("png"))); await store.removeAll("one"); expect(store.get("one", item.attachmentId)).toBeUndefined(); }); +}); diff --git a/packages/harness/tests/core.test.ts b/packages/harness/tests/core.test.ts new file mode 100644 index 0000000..a973d33 --- /dev/null +++ b/packages/harness/tests/core.test.ts @@ -0,0 +1,121 @@ +import { mkdtemp, mkdir, readFile, symlink, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { describe, expect, it, vi } from "vitest"; +import { + InMemoryApprovalBroker, + InMemoryEventStore, + SafeWorkspace, + SkillCatalog, + createSaveReportDraftTool, + getMockAdsMetrics, + mockAdsMetricsParameters, + redactValue, + validateBaseUrl, +} from "../src/index.js"; +import { Value } from "typebox/value"; + +const projectRoot = path.resolve(import.meta.dirname, "../../.."); + +describe("InMemoryEventStore", () => { + it("allocates monotonic per-session sequences atomically", () => { + const store = new InMemoryEventStore(); + const first = store.append("s", { type: "run.completed", runId: "r", payload: {} }); + const second = store.append("s", { type: "run.completed", runId: "r", payload: {} }); + expect([first.sequence, second.sequence]).toEqual([1, 2]); + expect(store.append("other", { type: "run.completed", runId: "r", payload: {} }).sequence).toBe(1); + }); + it("replays by sequence and event id", () => { + const store = new InMemoryEventStore(); + const one = store.append("s", { type: "run.completed", payload: {} }); + store.append("s", { type: "run.completed", payload: {} }); + expect(store.listAfter("s", { sequence: 1 }).events).toHaveLength(1); + expect(store.listAfter("s", { eventId: one.eventId }).events).toHaveLength(1); + }); + it("reports a retained-history gap", () => { + const store = new InMemoryEventStore(2); + for (let index = 0; index < 4; index++) store.append("s", { type: "run.completed", payload: {} }); + expect(store.listAfter("s", { sequence: 1 }).gap?.oldestAvailableSequence).toBe(3); + }); + it("unsubscribes listeners", () => { + const store = new InMemoryEventStore(); const listener = vi.fn(); const unsubscribe = store.subscribe("s", listener); unsubscribe(); store.append("s", { type: "run.completed", payload: {} }); expect(listener).not.toHaveBeenCalled(); + }); +}); + +describe("ApprovalBroker", () => { + const request = { sessionId: "s", runId: "r", toolCallId: "t", toolName: "save_report_draft", arguments: { content: "x" }, riskLevel: "medium" as const }; + it("approves and is idempotent for the same decision", async () => { + let id = ""; const broker = new InMemoryApprovalBroker(1_000, undefined, (value) => { id = value.approvalId; }); + const pending = broker.request(request, new AbortController().signal); const first = broker.approve(id); const second = broker.approve(id); + expect((await pending).status).toBe("approved"); expect(second.decision).toEqual(first.decision); + }); + it("denies and rejects a conflicting repeat", async () => { + let id = ""; const broker = new InMemoryApprovalBroker(1_000, undefined, (value) => { id = value.approvalId; }); const pending = broker.request(request, new AbortController().signal); broker.deny(id); expect((await pending).status).toBe("denied"); expect(() => broker.approve(id)).toThrow(/already resolved/i); + }); + it("expires", async () => { + const broker = new InMemoryApprovalBroker(5); const decision = await broker.request(request, new AbortController().signal); expect(decision.status).toBe("expired"); + }); + it("cancels immediately with AbortSignal", async () => { + const controller = new AbortController(); const broker = new InMemoryApprovalBroker(1_000); const pending = broker.request(request, controller.signal); controller.abort(); expect((await pending).status).toBe("cancelled"); + }); + it("redacts sensitive arguments", () => { + expect(redactValue({ apiKey: "secret", nested: { authorization: "Bearer x" } })).toEqual({ apiKey: "[REDACTED]", nested: { authorization: "[REDACTED]" } }); + }); +}); + +describe("SafeWorkspace", () => { + it("writes only fixed report.md through an atomic operation", async () => { + const root = await mkdtemp(path.join(tmpdir(), "studio-safe-")); const workspace = await SafeWorkspace.create(root); const result = await workspace.writeReport("正文", "报告"); expect(result.path).toBe("report.md"); expect(await readFile(path.join(root, "report.md"), "utf8")).toContain("# 报告"); + }); + it("rejects .. traversal and absolute escape", async () => { + const root = await mkdtemp(path.join(tmpdir(), "studio-safe-")); const workspace = await SafeWorkspace.create(root); await expect(workspace.resolveExisting("../outside")).rejects.toMatchObject({ code: "PATH_OUTSIDE_WORKSPACE" }); await expect(workspace.resolveExisting("/etc/passwd")).rejects.toMatchObject({ code: "PATH_OUTSIDE_WORKSPACE" }); + }); + it("rejects a symlink escape", async () => { + const root = await mkdtemp(path.join(tmpdir(), "studio-safe-")); const workspace = await SafeWorkspace.create(root); await symlink("/etc/passwd", path.join(root, "linked")); await expect(workspace.resolveExisting("linked")).rejects.toMatchObject({ code: "PATH_OUTSIDE_WORKSPACE" }); + }); + it("rejects protected paths", async () => { + const root = await mkdtemp(path.join(tmpdir(), "studio-safe-")); const workspace = await SafeWorkspace.create(root); await writeFile(path.join(root, ".env"), "KEY=x"); await expect(workspace.assertReadPath(".env")).rejects.toMatchObject({ code: "PROTECTED_PATH" }); + }); + it("save tool schema exposes no output path", async () => { + const root = await mkdtemp(path.join(tmpdir(), "studio-safe-")); const workspace = await SafeWorkspace.create(root); const tool = createSaveReportDraftTool(workspace); expect((tool.parameters as { properties: Record }).properties).not.toHaveProperty("path"); + }); +}); + +describe("mock_ads_metrics", () => { + it("validates days in the TypeBox schema", () => { + expect(Value.Check(mockAdsMetricsParameters, { accountId: "demo-account", days: 7 })).toBe(true); expect(Value.Check(mockAdsMetricsParameters, { accountId: "demo-account", days: 31 })).toBe(false); + }); + it("computes deterministic metrics and handles precision", async () => { + const metrics = await getMockAdsMetrics(path.join(projectRoot, "fixtures", "ads-account.json"), "demo-account", 7); expect(metrics.spend).toBe(4415.5); expect(metrics.campaigns).toHaveLength(3); expect(metrics.roas).toBeGreaterThan(4); + }); + it("rejects unknown accounts", async () => { + await expect(getMockAdsMetrics(path.join(projectRoot, "fixtures", "ads-account.json"), "missing", 7)).rejects.toThrow("Unknown demo account"); + }); +}); + +describe("SkillCatalog", () => { + it("discovers .agents and .pi skills without reading bodies into the catalog", async () => { + const root = await mkdtemp(path.join(tmpdir(), "studio-skills-")); await createSkill(root, ".agents/skills/one", "one", "First skill"); await createSkill(root, ".pi/skills/two", "two", "Second skill"); const catalog = new SkillCatalog(root, path.join(root, "agent-dir")); await catalog.reload(); expect(catalog.list().filter((item) => ["one", "two"].includes(item.name))).toHaveLength(2); expect(JSON.stringify(catalog.list())).not.toContain("SECRET BODY"); + }); + it("loads path-delimited AGENT_SKILLS_DIRS and de-duplicates canonical paths", async () => { + const root = await mkdtemp(path.join(tmpdir(), "studio-skills-")); await mkdir(path.join(root, ".agents/skills"), { recursive: true }); await mkdir(path.join(root, ".pi/skills"), { recursive: true }); const extra = path.join(root, "extra"); await createSkill(root, "extra/custom", "custom", "Extra skill"); const catalog = new SkillCatalog(root, path.join(root, "agent-dir"), [extra, extra].join(path.delimiter)); await catalog.reload(); expect(catalog.list().filter((item) => item.name === "custom")).toHaveLength(1); + }); + it("turns an unreadable extra directory into a visible diagnostic", async () => { + const root = await mkdtemp(path.join(tmpdir(), "studio-skills-")); await mkdir(path.join(root, ".agents/skills"), { recursive: true }); await mkdir(path.join(root, ".pi/skills"), { recursive: true }); const catalog = new SkillCatalog(root, path.join(root, "agent-dir"), path.join(root, "missing")); await catalog.reload(); expect(catalog.list().some((item) => item.diagnostics.some((diagnostic) => diagnostic.severity === "error"))).toBe(true); + }); + it("matches requested skill reads by canonical path", async () => { + const root = await mkdtemp(path.join(tmpdir(), "studio-skills-")); await createSkill(root, ".agents/skills/one", "one", "First skill"); await mkdir(path.join(root, ".pi/skills"), { recursive: true }); const catalog = new SkillCatalog(root, path.join(root, "agent-dir")); await catalog.reload(); expect((await catalog.matchReadPath(path.join(root, ".agents/skills/one/SKILL.md"), root))?.name).toBe("one"); expect(await catalog.matchReadPath("SKILL.md", root)).toBeUndefined(); + }); +}); + +describe("Base URL safety", () => { + it("rejects metadata, userinfo, dangerous protocols, and private HTTPS", async () => { + await expect(validateBaseUrl("https://169.254.169.254/latest", false, false)).rejects.toMatchObject({ code: "SSRF_BLOCKED" }); await expect(validateBaseUrl("https://user:pass@example.com", false, false)).rejects.toMatchObject({ code: "INVALID_BASE_URL" }); await expect(validateBaseUrl("file:///etc/passwd", false, false)).rejects.toMatchObject({ code: "UNSAFE_BASE_URL" }); await expect(validateBaseUrl("https://127.0.0.1/v1", false, false)).rejects.toMatchObject({ code: "SSRF_BLOCKED" }); + }); + it("allows explicit loopback HTTP only in development", async () => { + await expect(validateBaseUrl("http://localhost:11434/v1", false, true)).resolves.toContain("localhost"); await expect(validateBaseUrl("http://localhost:11434/v1", false, false)).rejects.toMatchObject({ code: "UNSAFE_BASE_URL" }); + }); + it("rejects credential query parameters", async () => { await expect(validateBaseUrl("https://8.8.8.8/v1?api_key=x", false, false)).rejects.toMatchObject({ code: "CREDENTIAL_IN_URL" }); }); +}); + +async function createSkill(root: string, relative: string, name: string, description: string) { const directory = path.join(root, relative); await mkdir(directory, { recursive: true }); await writeFile(path.join(directory, "SKILL.md"), `---\nname: ${name}\ndescription: ${description}\n---\n\nSECRET BODY\n`); } diff --git a/packages/harness/tests/credential-vault.test.ts b/packages/harness/tests/credential-vault.test.ts new file mode 100644 index 0000000..71cd8d6 --- /dev/null +++ b/packages/harness/tests/credential-vault.test.ts @@ -0,0 +1,101 @@ +import { randomBytes } from "node:crypto"; +import { mkdtemp, readFile, stat } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { EncryptedFileCredentialVault, InMemoryModelConnectionStore, modelCredentialIdentity } from "../src/index.js"; + +const model = { + providerId: "training", + api: "openai-responses" as const, + baseUrl: "https://8.8.8.8/v1", + modelId: "training-model", + supportsImages: true, +}; + +describe("EncryptedFileCredentialVault", () => { + it("persists only authenticated ciphertext and decrypts after restart", async () => { + const root = await mkdtemp(path.join(tmpdir(), "studio-vault-")); + const filePath = path.join(root, "credentials.enc.json"); + const masterKey = randomBytes(32).toString("base64"); + const identity = modelCredentialIdentity(model); + const first = new EncryptedFileCredentialVault(filePath, masterKey); + await first.set(identity, "secret-model-key"); + const raw = await readFile(filePath, "utf8"); + expect(raw).not.toContain("secret-model-key"); + expect((await stat(filePath)).mode & 0o777).toBe(0o600); + const restarted = new EncryptedFileCredentialVault(filePath, masterKey); + await expect(restarted.get(identity)).resolves.toBe("secret-model-key"); + }); + + it("restores a credential for a new Web Session and preserves it when the old Session is removed", async () => { + const root = await mkdtemp(path.join(tmpdir(), "studio-vault-")); + const vault = new EncryptedFileCredentialVault(path.join(root, "credentials.enc.json"), randomBytes(32).toString("base64")); + const first = new InMemoryModelConnectionStore({}, vault); + await first.configure("session-one", { ...model, apiKey: "persistent-secret" }); + expect(first.getRedacted("session-one")).toMatchObject({ keyConfigured: true, credentialStorage: "encrypted" }); + await first.remove("session-one"); + const restarted = new InMemoryModelConnectionStore({}, vault); + await restarted.configure("session-two", model); + expect(restarted.getRedacted("session-two")).toMatchObject({ keyConfigured: true, keyHint: "••••cret", credentialStorage: "encrypted" }); + }); + + it("deletes the persisted credential only through Clear Credentials", async () => { + const root = await mkdtemp(path.join(tmpdir(), "studio-vault-")); + const vault = new EncryptedFileCredentialVault(path.join(root, "credentials.enc.json"), randomBytes(32).toString("base64")); + const store = new InMemoryModelConnectionStore({}, vault); + await store.configure("session-one", { ...model, apiKey: "persistent-secret" }); + await store.clearCredentials("session-one"); + const restarted = new InMemoryModelConnectionStore({}, vault); + await restarted.configure("session-two", model); + expect(restarted.getRedacted("session-two")).toMatchObject({ keyConfigured: false }); + }); + + it("fails closed for an invalid or different master key", async () => { + expect(() => new EncryptedFileCredentialVault("unused", "not-a-32-byte-key")).toThrow(/32-byte key/); + const root = await mkdtemp(path.join(tmpdir(), "studio-vault-")); + const filePath = path.join(root, "credentials.enc.json"); + const identity = modelCredentialIdentity(model); + const first = new EncryptedFileCredentialVault(filePath, randomBytes(32).toString("base64")); + await first.set(identity, "secret-model-key"); + const wrong = new EncryptedFileCredentialVault(filePath, randomBytes(32).toString("base64")); + await expect(wrong.get(identity)).rejects.toMatchObject({ code: "CREDENTIAL_DECRYPTION_FAILED" }); + }); + + it("loads a strict environment default and restores its encrypted credential", async () => { + const root = await mkdtemp(path.join(tmpdir(), "studio-vault-")); + const vault = new EncryptedFileCredentialVault(path.join(root, "credentials.enc.json"), randomBytes(32).toString("base64")); + await vault.set(modelCredentialIdentity(model), "environment-default-secret"); + const store = new InMemoryModelConnectionStore({ + AGENT_PROVIDER: model.providerId, + AGENT_MODEL: model.modelId, + LLM_API: model.api, + LLM_BASE_URL: model.baseUrl, + AGENT_DISPLAY_NAME: "Training Model", + AGENT_SUPPORTS_IMAGES: "true", + }, vault); + await store.initialize(); + expect(store.getRedacted("fresh-session")).toMatchObject({ source: "environment", modelId: model.modelId, displayName: "Training Model", supportsImages: true, keyConfigured: true, credentialStorage: "encrypted" }); + }); + + it("uses an environment API key without returning the raw credential", async () => { + const store = new InMemoryModelConnectionStore({ + AGENT_PROVIDER: model.providerId, + AGENT_MODEL: model.modelId, + LLM_API: model.api, + LLM_BASE_URL: model.baseUrl, + OPENAI_API_KEY: "environment-api-secret", + }); + await store.initialize(); + const redacted = store.getRedacted("fresh-session"); + expect(redacted).toMatchObject({ source: "environment", keyConfigured: true, keyHint: "••••cret", credentialStorage: "environment" }); + expect(JSON.stringify(redacted)).not.toContain("environment-api-secret"); + }); + + it("fails startup for partial or malformed environment defaults", async () => { + const partial = new InMemoryModelConnectionStore({ AGENT_PROVIDER: "openai" }); + await expect(partial.initialize()).rejects.toMatchObject({ code: "INVALID_ENV_MODEL_CONFIG" }); + const malformed = new InMemoryModelConnectionStore({ AGENT_PROVIDER: "openai", AGENT_MODEL: "model", AGENT_SUPPORTS_IMAGES: "yes" }); + await expect(malformed.initialize()).rejects.toMatchObject({ code: "INVALID_ENV_MODEL_CONFIG" }); + }); +}); diff --git a/packages/harness/tests/session.test.ts b/packages/harness/tests/session.test.ts new file mode 100644 index 0000000..cde0a1d --- /dev/null +++ b/packages/harness/tests/session.test.ts @@ -0,0 +1,122 @@ +import { mkdtemp, mkdir, readFile, stat } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { describe, expect, it, vi } from "vitest"; +import sharp from "sharp"; +import { + FileAttachmentStore, + InMemoryApprovalBroker, + InMemoryEventStore, + InMemoryModelConnectionStore, + InMemorySessionRegistry, + PiAgentSessionFactory, + SafeWorkspace, + SkillCatalog, + type AgentSessionFactory, + type AgentSessionFactoryInput, + type AgentSessionPort, +} from "../src/index.js"; + +class FakePort implements AgentSessionPort { + readonly modelId = "fake-model"; + constructor(public readonly supportsImages = true) {} + listeners = new Set<(event: unknown) => void>(); + promptOptions: { images?: unknown[] } | undefined; + disposed = false; aborted = false; + private finish: (() => void) | undefined; + prompt(_text: string, options?: { images?: unknown[] }): Promise { this.promptOptions = options; return new Promise((resolve) => { this.finish = resolve; }); } + async abort(): Promise { this.aborted = true; this.finish?.(); } + complete(): void { this.finish?.(); } + subscribe(listener: (event: unknown) => void): () => void { this.listeners.add(listener); return () => this.listeners.delete(listener); } + dispose(): void { this.disposed = true; } + emit(event: unknown): void { for (const listener of this.listeners) listener(event); } +} +class FakeFactory implements AgentSessionFactory { + port: FakePort; + input?: AgentSessionFactoryInput; + constructor(supportsImages = true) { this.port = new FakePort(supportsImages); } + async create(input: AgentSessionFactoryInput): Promise { this.input = input; return this.port; } +} + +async function setup(supportsImages = true) { + const root = await mkdtemp(path.join(tmpdir(), "studio-session-")); const sessionsRoot = path.join(root, "sessions"); await mkdir(sessionsRoot); + await mkdir(path.join(root, ".agents/skills"), { recursive: true }); await mkdir(path.join(root, ".pi/skills"), { recursive: true }); + const catalog = new SkillCatalog(root, path.join(root, "agent-dir")); await catalog.reload(); + const events = new InMemoryEventStore(); const attachments = new FileAttachmentStore(sessionsRoot); const models = new InMemoryModelConnectionStore({}); const factory = new FakeFactory(supportsImages); + const registry = new InMemorySessionRegistry(sessionsRoot, events, attachments, models, catalog, factory, 500, 5_000); + const session = await registry.create(); await registry.configureModel(session.sessionId, { providerId: "fake", api: "openai-responses", apiKey: "test-secret", modelId: "fake-model", supportsImages }); + return { root, sessionsRoot, events, attachments, models, factory, registry, session }; +} + +describe("session lifecycle", () => { + it("creates a real Pi AgentSession for the exact configured model without model network access", async () => { + const root = await mkdtemp(path.join(tmpdir(), "studio-real-pi-")); + await mkdir(path.join(root, ".agents/skills"), { recursive: true }); + await mkdir(path.join(root, ".pi/skills"), { recursive: true }); + const cwd = path.join(root, "session"); await mkdir(cwd); + const catalog = new SkillCatalog(root, path.join(root, "catalog")); await catalog.reload(); + const models = new InMemoryModelConnectionStore({ ALLOW_PRIVATE_LLM_BASE_URLS: "false" }); + await models.configure("ses_real", { providerId: "training", api: "openai-responses", baseUrl: "http://127.0.0.1:9/v1", apiKey: "fake-no-network", modelId: "training-model", supportsImages: true }); + const workspace = await SafeWorkspace.create(cwd); + const factory = new PiAgentSessionFactory(path.join(root, "unused-fixture.json"), catalog, models, new InMemoryApprovalBroker()); + const port = await factory.create({ sessionId: "ses_real", cwd, workspace, getRun: () => undefined, skillRequested: vi.fn(), skillLoaded: vi.fn(), toolRequested: vi.fn() }); + expect(port.modelId).toBe("training-model"); expect(port.supportsImages).toBe(true); port.dispose(); await models.remove("ses_real"); + }); + it("rejects unconfigured models instead of falling back", async () => { + const root = await mkdtemp(path.join(tmpdir(), "studio-session-")); const sessionsRoot = path.join(root, "sessions"); await mkdir(sessionsRoot); await mkdir(path.join(root, ".agents/skills"), { recursive: true }); await mkdir(path.join(root, ".pi/skills"), { recursive: true }); const catalog = new SkillCatalog(root, path.join(root, "agent")); await catalog.reload(); const models = new InMemoryModelConnectionStore({}); const registry = new InMemorySessionRegistry(sessionsRoot, new InMemoryEventStore(), new FileAttachmentStore(sessionsRoot), models, catalog, new FakeFactory()); const session = await registry.create(); await expect(registry.send(session.sessionId, { text: "hello", attachmentIds: [] })).rejects.toMatchObject({ code: "MODEL_NOT_CONFIGURED" }); await expect(models.createRuntime(session.sessionId)).rejects.toMatchObject({ code: "MODEL_NOT_CONFIGURED" }); + }); + it("accepts a run without waiting for prompt completion and rejects concurrency", async () => { + const context = await setup(); const result = await context.registry.send(context.session.sessionId, { text: "hello", attachmentIds: [] }); expect(result.runId).toMatch(/^run_/); expect(context.session.busy).toBe(true); await expect(context.registry.send(context.session.sessionId, { text: "again", attachmentIds: [] })).rejects.toMatchObject({ code: "SESSION_BUSY" }); context.factory.port.complete(); await vi.waitFor(() => expect(context.session.busy).toBe(false)); + }); + it("cancels an active run and disposes on deletion", async () => { + const context = await setup(); await context.registry.send(context.session.sessionId, { text: "hello", attachmentIds: [] }); await context.registry.cancel(context.session.sessionId, "user"); expect(context.factory.port.aborted).toBe(true); await vi.waitFor(() => expect(context.session.busy).toBe(false)); await context.registry.delete(context.session.sessionId); expect(context.factory.port.disposed).toBe(true); await expect(stat(context.session.cwd)).rejects.toThrow(); + }); + it("maps fake Pi deltas and tool events centrally", async () => { + const context = await setup(); const { runId } = await context.registry.send(context.session.sessionId, { text: "hello", attachmentIds: [] }); context.factory.port.emit({ type: "message_update", message: { role: "assistant" }, assistantMessageEvent: { type: "text_delta", delta: "流式" } }); context.factory.input?.toolRequested("tool_1", "mock_ads_metrics", { accountId: "demo-account", days: 7 }); context.factory.port.emit({ type: "tool_execution_start", toolCallId: "tool_1", toolName: "mock_ads_metrics", args: {} }); context.factory.port.emit({ type: "tool_execution_end", toolCallId: "tool_1", toolName: "mock_ads_metrics", result: { ok: true }, isError: false }); context.factory.port.complete(); await vi.waitFor(() => expect(context.session.busy).toBe(false)); const runEvents = context.events.listAfter(context.session.sessionId).events.filter((event) => event.runId === runId); expect(runEvents.map((event) => event.type)).toContain("assistant.delta"); expect(runEvents.filter((event) => event.type === "tool.requested")).toHaveLength(1); expect(runEvents.map((event) => event.type)).toContain("tool.completed"); + }); + it("uses Extension preflight as the sole tool request source when Pi start arrives first", async () => { + const context = await setup(); const { runId } = await context.registry.send(context.session.sessionId, { text: "hello", attachmentIds: [] }); context.factory.port.emit({ type: "tool_execution_start", toolCallId: "tool_race", toolName: "read", args: { path: "/absolute/private/path" } }); context.factory.input?.toolRequested("tool_race", "read", { path: ".agents/skills/ads-analysis/SKILL.md" }); context.factory.port.complete(); await vi.waitFor(() => expect(context.session.busy).toBe(false)); const requests = context.events.listAfter(context.session.sessionId).events.filter((event) => event.runId === runId && event.type === "tool.requested"); expect(requests).toHaveLength(1); expect(requests[0]?.payload.arguments).toEqual({ path: ".agents/skills/ads-analysis/SKILL.md" }); + }); + it("derives skill requested/loaded callbacks with the same toolCallId", async () => { + const context = await setup(); await context.registry.send(context.session.sessionId, { text: "hello", attachmentIds: [] }); const skill = { name: "ads-analysis", description: "x", source: "project", filePath: ".agents/skills/ads-analysis/SKILL.md", canonicalPath: "/tmp/SKILL.md", diagnostics: [] }; context.factory.input?.skillRequested(skill, "read_1"); context.factory.input?.skillLoaded(skill, "read_1"); const skillEvents = context.events.listAfter(context.session.sessionId).events.filter((event) => event.type.startsWith("skill.")); expect(skillEvents.map((event) => event.type)).toEqual(["skill.requested", "skill.loaded"]); context.factory.port.complete(); + }); +}); + +describe("multimodal session path", () => { + it("maps a normalized image to the installed Pi ImageContent shape", async () => { + const context = await setup(true); const data = await sharp({ create: { width: 3, height: 2, channels: 3, background: "red" } }).png().toBuffer(); const item = await context.attachments.add(context.session.sessionId, { filename: "x.png", mimetype: "image/png", stream: (async function* () { yield data; })() }); await context.registry.send(context.session.sessionId, { text: "看图", attachmentIds: [item.attachmentId] }); await vi.waitFor(() => expect(context.factory.port.promptOptions).toBeDefined()); const imagePart = context.factory.port.promptOptions?.images?.[0] as { type: string; mimeType: string; data: string }; expect(imagePart).toMatchObject({ type: "image", mimeType: "image/png" }); expect(imagePart.data).toBe(Buffer.from(imagePart.data, "base64").toString("base64")); const serializedEvents = JSON.stringify(context.events.listAfter(context.session.sessionId).events); expect(serializedEvents).not.toContain(imagePart.data); context.factory.port.complete(); + }); + it("returns 422 semantics for a text-only model", async () => { + const context = await setup(false); const data = await sharp({ create: { width: 3, height: 2, channels: 3, background: "red" } }).png().toBuffer(); const item = await context.attachments.add(context.session.sessionId, { filename: "x.png", mimetype: "image/png", stream: (async function* () { yield data; })() }); await expect(context.registry.send(context.session.sessionId, { text: "看图", attachmentIds: [item.attachmentId] })).rejects.toMatchObject({ code: "MODEL_DOES_NOT_SUPPORT_IMAGES", statusCode: 422 }); + }); + it("rejects too many images before creating a run", async () => { const context = await setup(); const ids = Array.from({ length: 5 }, (_, index) => `att_${index}`); await expect(context.registry.send(context.session.sessionId, { text: "x", attachmentIds: ids })).rejects.toMatchObject({ code: "TOO_MANY_ATTACHMENTS" }); }); + it("cleans attachments when deleting the session", async () => { const context = await setup(); const data = await sharp({ create: { width: 2, height: 2, channels: 3, background: "blue" } }).png().toBuffer(); const item = await context.attachments.add(context.session.sessionId, { filename: "x.png", mimetype: "image/png", stream: (async function* () { yield data; })() }); await context.registry.delete(context.session.sessionId); expect(context.attachments.get(context.session.sessionId, item.attachmentId)).toBeUndefined(); }); +}); + +describe("fake Pi integration flow", () => { + it("runs message → tool → approval → report → completion without a real model", async () => { + const context = await setup(true); + const { runId } = await context.registry.send(context.session.sessionId, { text: "生成报告", attachmentIds: [] }); + context.factory.port.emit({ type: "message_update", message: { role: "assistant" }, assistantMessageEvent: { type: "text_delta", delta: "正在分析" } }); + context.factory.input?.toolRequested("metrics_1", "mock_ads_metrics", { accountId: "demo-account", days: 7 }); + context.factory.port.emit({ type: "tool_execution_end", toolCallId: "metrics_1", toolName: "mock_ads_metrics", result: { spend: 1 }, isError: false }); + context.factory.input?.toolRequested("save_1", "save_report_draft", { content: "报告正文" }); + const signal = context.session.currentRun?.controller.signal; + expect(signal).toBeDefined(); + const pending = context.registry.approvals.request({ sessionId: context.session.sessionId, runId, toolCallId: "save_1", toolName: "save_report_draft", arguments: { content: "报告正文" }, riskLevel: "medium" }, signal!); + const required = context.events.listAfter(context.session.sessionId).events.findLast((event) => event.type === "approval.required"); + expect(required?.type).toBe("approval.required"); + if (!required || required.type !== "approval.required") throw new Error("approval event missing"); + context.registry.approvals.approve(required.payload.approvalId); + expect((await pending).status).toBe("approved"); + await context.session.workspace.writeReport("报告正文"); + context.factory.port.emit({ type: "tool_execution_end", toolCallId: "save_1", toolName: "save_report_draft", result: { path: "report.md" }, isError: false }); + context.factory.port.complete(); + await vi.waitFor(() => expect(context.session.busy).toBe(false)); + expect(await readFile(path.join(context.session.cwd, "report.md"), "utf8")).toBe("报告正文"); + const types = context.events.listAfter(context.session.sessionId).events.map((event) => event.type); + expect(types.indexOf("approval.required")).toBeLessThan(types.indexOf("approval.resolved")); + expect(types.at(-1)).toBe("run.completed"); + await context.registry.delete(context.session.sessionId); + }); +}); diff --git a/packages/harness/tsconfig.json b/packages/harness/tsconfig.json new file mode 100644 index 0000000..1e7755d --- /dev/null +++ b/packages/harness/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "rootDir": "src", "outDir": "dist", "tsBuildInfoFile": "dist/.tsbuildinfo" }, + "include": ["src"] +} diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 0000000..47e1a2e --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,10 @@ +{ + "name": "@agent-studio/shared", + "version": "0.1.0", + "private": true, + "type": "module", + "exports": { ".": { "types": "./src/index.ts", "development": "./src/index.ts", "import": "./dist/index.js", "default": "./dist/index.js" } }, + "scripts": { "typecheck": "tsc --noEmit", "build": "tsc -p tsconfig.json" }, + "dependencies": { "zod": "^4.1.13" }, + "devDependencies": { "typescript": "^5.9.3" } +} diff --git a/packages/shared/src/events.ts b/packages/shared/src/events.ts new file mode 100644 index 0000000..77f5eca --- /dev/null +++ b/packages/shared/src/events.ts @@ -0,0 +1,51 @@ +import { z } from "zod"; +import { approvalRequestSchema, imageAttachmentSchema, llmApiSchema } from "./schemas.js"; + +const baseShape = { + eventId: z.string(), + sessionId: z.string(), + runId: z.string().optional(), + timestamp: z.string().datetime(), + sequence: z.number().int().positive(), +}; + +const event = (type: T, payload: S) => + z.object({ ...baseShape, type: z.literal(type), payload }).strict(); + +const empty = z.object({}).strict(); +const errorPayload = z.object({ code: z.string(), message: z.string() }).strict(); +const toolBase = z.object({ toolCallId: z.string(), toolName: z.string(), arguments: z.unknown().optional() }).strict(); + +export const harnessEventSchema = z.discriminatedUnion("type", [ + event("session.started", z.object({ cwd: z.string() }).strict()), + event("session.ended", z.object({ reason: z.enum(["deleted", "shutdown", "error"]) }).strict()), + event("user.message", z.object({ messageId: z.string(), text: z.string(), attachmentIds: z.array(z.string()) }).strict()), + event("assistant.delta", z.object({ messageId: z.string(), delta: z.string() }).strict()), + event("assistant.completed", z.object({ messageId: z.string() }).strict()), + event("skill.requested", z.object({ name: z.string(), filePath: z.string(), toolCallId: z.string() }).strict()), + event("skill.loaded", z.object({ name: z.string(), filePath: z.string(), toolCallId: z.string() }).strict()), + event("tool.requested", toolBase), + event("tool.completed", z.object({ toolCallId: z.string(), toolName: z.string(), result: z.unknown().optional() }).strict()), + event("tool.failed", z.object({ toolCallId: z.string(), toolName: z.string(), error: z.string() }).strict()), + event("approval.required", approvalRequestSchema), + event("approval.resolved", z.object({ approvalId: z.string(), toolCallId: z.string(), status: z.enum(["approved", "denied", "expired", "cancelled"]), resolvedAt: z.string().datetime() }).strict()), + event("attachment.uploaded", imageAttachmentSchema.omit({ sessionId: true })), + event("attachment.rejected", z.object({ name: z.string(), code: z.string(), message: z.string() }).strict()), + event("model.configured", z.object({ providerId: z.string(), modelId: z.string(), api: llmApiSchema, supportsImages: z.boolean() }).strict()), + event("model.validation_failed", errorPayload), + event("run.started", z.object({ modelId: z.string() }).strict()), + event("run.cancelled", z.object({ reason: z.enum(["user", "timeout", "session_deleted", "shutdown"]) }).strict()), + event("run.completed", empty), + event("run.failed", errorPayload), +]); + +export type HarnessEvent = z.infer; +export type HarnessEventType = HarnessEvent["type"]; +export type EventOfType = Extract; +export type EventInput = Omit, "eventId" | "sessionId" | "timestamp" | "sequence">; + +export const sseControlEventSchema = z.object({ + type: z.literal("stream.gap"), + oldestAvailableSequence: z.number().int().positive(), + requestedSequence: z.number().int().nonnegative(), +}).strict(); diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 0000000..e5ba2be --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1,2 @@ +export * from "./schemas.js"; +export * from "./events.js"; diff --git a/packages/shared/src/schemas.ts b/packages/shared/src/schemas.ts new file mode 100644 index 0000000..c526014 --- /dev/null +++ b/packages/shared/src/schemas.ts @@ -0,0 +1,91 @@ +import { z } from "zod"; + +export const llmApiSchema = z.enum(["openai-responses", "openai-completions", "anthropic-messages"]); + +export const modelConnectionInputSchema = z.object({ + providerId: z.string().trim().min(1).max(80).regex(/^[a-zA-Z0-9._-]+$/), + api: llmApiSchema, + baseUrl: z.string().trim().url().max(2048).optional(), + apiKey: z.string().min(1).max(8192).optional(), + modelId: z.string().trim().min(1).max(200), + displayName: z.string().trim().min(1).max(200).optional(), + supportsImages: z.boolean(), + contextWindow: z.number().int().min(1_024).max(10_000_000).optional(), + maxTokens: z.number().int().min(1).max(1_000_000).optional(), +}).strict(); +export type ModelConnectionInput = z.infer; + +export const persistedModelConnectionSchema = modelConnectionInputSchema.omit({ apiKey: true }); +export type PersistedModelConnection = z.infer; + +export const redactedModelConnectionSchema = z.object({ + configured: z.boolean(), + source: z.enum(["session", "environment", "missing"]), + providerId: z.string().optional(), + api: llmApiSchema.optional(), + baseUrl: z.string().optional(), + modelId: z.string().optional(), + displayName: z.string().optional(), + supportsImages: z.boolean().optional(), + keyConfigured: z.boolean(), + keyHint: z.string().optional(), + credentialStorage: z.enum(["memory", "encrypted", "environment"]).optional(), +}).strict(); +export type RedactedModelConnection = z.infer; + +export const sendMessageSchema = z.object({ + text: z.string().max(100_000).default(""), + attachmentIds: z.array(z.string().regex(/^att_[a-zA-Z0-9_-]+$/)).max(16).default([]), +}).strict().refine((value) => value.text.trim().length > 0 || value.attachmentIds.length > 0, { + message: "Text or at least one attachment is required", +}); +export type SendMessageInput = z.infer; + +export const imageAttachmentSchema = z.object({ + attachmentId: z.string(), + sessionId: z.string(), + name: z.string(), + mediaType: z.enum(["image/jpeg", "image/png", "image/webp"]), + byteSize: z.number().int().nonnegative(), + width: z.number().int().positive(), + height: z.number().int().positive(), + status: z.literal("ready"), +}).strict(); +export type ImageAttachment = z.infer; + +export const approvalRequestSchema = z.object({ + approvalId: z.string(), + sessionId: z.string(), + runId: z.string(), + toolCallId: z.string(), + toolName: z.string(), + arguments: z.unknown(), + riskLevel: z.enum(["low", "medium", "high"]), + createdAt: z.string().datetime(), + expiresAt: z.string().datetime(), + status: z.enum(["pending", "approved", "denied", "expired", "cancelled"]), +}).strict(); +export type ApprovalRequest = z.infer; + +export const apiErrorSchema = z.object({ + error: z.object({ + code: z.string(), + message: z.string(), + requestId: z.string(), + details: z.record(z.string(), z.unknown()), + }).strict(), +}).strict(); +export type ApiError = z.infer; + +export const skillDiagnosticSchema = z.object({ + severity: z.enum(["warning", "error"]), + message: z.string(), +}).strict(); +export const skillCatalogItemSchema = z.object({ + name: z.string(), + description: z.string(), + source: z.string(), + filePath: z.string(), + diagnostics: z.array(skillDiagnosticSchema), +}).strict(); +export type SkillCatalogItem = z.infer; diff --git a/packages/shared/tests/schemas.test.ts b/packages/shared/tests/schemas.test.ts new file mode 100644 index 0000000..c4c0c40 --- /dev/null +++ b/packages/shared/tests/schemas.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest"; +import { apiErrorSchema, harnessEventSchema, modelConnectionInputSchema, persistedModelConnectionSchema, sendMessageSchema } from "../src/index.js"; + +describe("shared boundary schemas", () => { + it("accepts a typed HarnessEvent variant", () => { + expect(harnessEventSchema.parse({ eventId: "evt_1", sessionId: "ses_1", runId: "run_1", timestamp: new Date().toISOString(), sequence: 1, type: "assistant.delta", payload: { messageId: "msg_1", delta: "Hi" } }).type).toBe("assistant.delta"); + }); + it("rejects the wrong discriminated payload", () => { + expect(() => harnessEventSchema.parse({ eventId: "e", sessionId: "s", timestamp: new Date().toISOString(), sequence: 1, type: "run.completed", payload: { unexpected: true } })).toThrow(); + }); + it("strictly validates model configuration", () => { + expect(() => modelConnectionInputSchema.parse({ providerId: "openai", api: "made-up", modelId: "x", supportsImages: false })).toThrow(); + expect(() => modelConnectionInputSchema.parse({ providerId: "openai", api: "openai-responses", modelId: "x", supportsImages: false, headers: {} })).toThrow(); + }); + it("accepts valid model configuration", () => { + expect(modelConnectionInputSchema.parse({ providerId: "openai", api: "openai-responses", modelId: "gpt", supportsImages: true }).modelId).toBe("gpt"); + }); + it("persists only non-sensitive model configuration", () => { + const value = { providerId: "openai", api: "openai-responses", modelId: "gpt", supportsImages: true }; + expect(persistedModelConnectionSchema.parse(value)).toEqual(value); + expect(() => persistedModelConnectionSchema.parse({ ...value, apiKey: "secret" })).toThrow(); + }); + it("requires text or attachments", () => { + expect(() => sendMessageSchema.parse({ text: "", attachmentIds: [] })).toThrow(); + expect(sendMessageSchema.parse({ text: "", attachmentIds: ["att_ok"] }).attachmentIds).toEqual(["att_ok"]); + }); + it("rejects malformed attachment ids", () => { + expect(() => sendMessageSchema.parse({ text: "x", attachmentIds: ["../escape"] })).toThrow(); + }); + it("validates the unified error envelope", () => { + expect(apiErrorSchema.parse({ error: { code: "SESSION_NOT_FOUND", message: "Session not found", requestId: "req_1", details: {} } }).error.code).toBe("SESSION_NOT_FOUND"); + }); +}); diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 0000000..1e7755d --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "rootDir": "src", "outDir": "dist", "tsBuildInfoFile": "dist/.tsbuildinfo" }, + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..41b0d54 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,6777 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@eslint/js': + specifier: ^9.39.1 + version: 9.39.5 + '@testing-library/jest-dom': + specifier: ^6.9.1 + version: 6.10.0(@testing-library/dom@10.4.1) + '@testing-library/react': + specifier: ^16.3.0 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@testing-library/user-event': + specifier: ^14.6.1 + version: 14.6.6(@testing-library/dom@10.4.1) + '@types/node': + specifier: ^24.10.1 + version: 24.13.3 + '@types/react': + specifier: ^19.2.7 + version: 19.2.18 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.5(@types/react@19.2.18) + '@vitejs/plugin-react': + specifier: ^5.1.1 + version: 5.2.0(supports-color@7.2.0)(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + eslint: + specifier: ^9.39.1 + version: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + eslint-plugin-react-hooks: + specifier: ^7.0.1 + version: 7.1.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) + eslint-plugin-react-refresh: + specifier: ^0.4.24 + version: 0.4.26(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)) + globals: + specifier: ^16.5.0 + version: 16.5.0 + jsdom: + specifier: ^27.2.0 + version: 27.4.0(supports-color@7.2.0) + tsx: + specifier: ^4.21.0 + version: 4.23.12 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + typescript-eslint: + specifier: ^8.48.1 + version: 8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + vite: + specifier: ^7.2.6 + version: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) + vitest: + specifier: ^4.0.15 + version: 4.1.11(@types/node@24.13.3)(jsdom@27.4.0(supports-color@7.2.0))(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + + apps/server: + dependencies: + '@agent-studio/harness': + specifier: workspace:* + version: link:../../packages/harness + '@agent-studio/shared': + specifier: workspace:* + version: link:../../packages/shared + '@fastify/multipart': + specifier: ^9.3.0 + version: 9.4.0 + '@fastify/static': + specifier: ^8.3.0 + version: 8.3.0 + fastify: + specifier: ^5.6.2 + version: 5.12.1 + zod: + specifier: ^4.1.13 + version: 4.4.3 + devDependencies: + '@types/node': + specifier: ^24.10.1 + version: 24.13.3 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + apps/web: + dependencies: + '@agent-studio/shared': + specifier: workspace:* + version: link:../../packages/shared + '@assistant-ui/react': + specifier: 0.15.17 + version: 0.15.17(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-dialog': + specifier: ^1.1.15 + version: 1.1.23(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + lucide-react: + specifier: ^0.555.0 + version: 0.555.0(react@19.2.8) + react: + specifier: ^19.2.0 + version: 19.2.8 + react-dom: + specifier: ^19.2.0 + version: 19.2.8(react@19.2.8) + zod: + specifier: ^4.1.13 + version: 4.4.3 + devDependencies: + '@types/react': + specifier: ^19.2.7 + version: 19.2.18 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.5(@types/react@19.2.18) + '@vitejs/plugin-react': + specifier: ^5.1.1 + version: 5.2.0(supports-color@7.2.0)(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vite: + specifier: ^7.2.6 + version: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) + + packages/harness: + dependencies: + '@agent-studio/shared': + specifier: workspace:* + version: link:../shared + '@earendil-works/pi-ai': + specifier: 0.84.3 + version: 0.84.3(supports-color@7.2.0)(ws@8.21.3)(zod@4.4.3) + '@earendil-works/pi-coding-agent': + specifier: 0.84.3 + version: 0.84.3(supports-color@7.2.0)(ws@8.21.3)(zod@4.4.3) + sharp: + specifier: ^0.34.5 + version: 0.34.5 + typebox: + specifier: 1.3.7 + version: 1.3.7 + zod: + specifier: ^4.1.13 + version: 4.4.3 + devDependencies: + '@types/node': + specifier: ^24.10.1 + version: 24.13.3 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + packages/shared: + dependencies: + zod: + specifier: ^4.1.13 + version: 4.4.3 + devDependencies: + typescript: + specifier: ^5.9.3 + version: 5.9.3 + +packages: + + '@acemir/cssom@0.9.31': + resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} + + '@adobe/css-tools@4.5.0': + resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + + '@anthropic-ai/sdk@0.91.1': + resolution: {integrity: sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==} + hasBin: true + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + + '@asamuzakjp/css-color@4.1.2': + resolution: {integrity: sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==} + + '@asamuzakjp/dom-selector@6.8.1': + resolution: {integrity: sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + + '@assistant-ui/core@0.3.16': + resolution: {integrity: sha512-fZ9ud+z7hEjegu6NrHyMEcQKCdij2KRcqpLsyOYzHE9LhnBv59Q29jU287EkD0NZE0SG4YTo5/klzz7C/lKA+Q==} + peerDependencies: + '@assistant-ui/store': ^0.3.0 + '@assistant-ui/tap': ^0.9.0 + '@types/react': '*' + assistant-cloud: ^0.1.42 + react: ^18 || ^19 + zustand: ^5.0.11 + peerDependenciesMeta: + '@types/react': + optional: true + assistant-cloud: + optional: true + react: + optional: true + zustand: + optional: true + + '@assistant-ui/react@0.15.17': + resolution: {integrity: sha512-lCjukORuR2olq+XZYj8fF1MCYttqk90299CUrvruoKDr0cAPQzyIAxpTwOkJDhte4U+GyDHaov/KNb7NQe1EJg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^18 || ^19 + react-dom: ^18 || ^19 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@assistant-ui/store@0.3.11': + resolution: {integrity: sha512-P9xHWIUIcpcPYJTunRWw4dAOZLONMx0OZ+aWnHUt7zwjH9UDQ1VCGK0JBorD4ZqhHgX/MUKYB4YRMwgF6yjXnA==} + peerDependencies: + '@assistant-ui/tap': ^0.9.12 + '@types/react': '*' + react: ^18 || ^19 + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + + '@assistant-ui/tap@0.9.15': + resolution: {integrity: sha512-3rTHh1xUedyrgP0pAYKs0LsOmnJfV9HoQN1D2pLXkIjyJvgyuoAW4vPiJ627obbJnlj0QwvIlw9IKoLdO+ecbA==} + peerDependencies: + '@types/react': '*' + react: ^18 || ^19 + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-bedrock-runtime@3.1048.0': + resolution: {integrity: sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.977.9': + resolution: {integrity: sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.70': + resolution: {integrity: sha512-H404B7dJl2mCrBqahDEYsanB0xhdDp6tXnXcTUnXmmpy2Q3J0Ho0bUajZ2jr/RdwzCyS59Gi8xXIFwPLGBl6Uw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.72': + resolution: {integrity: sha512-X98zYOrVOeuosCX+6ktf29FC2N2GHPLia7qv6mzPzTc+RPAuHWCDS++Z6JK7eGYqb/v6uaW7bAXaOvDBfol+0w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.973.15': + resolution: {integrity: sha512-Rykg6s5ceBuynMOGWgoowO4N+27JfnqXAnVaSunZl0hOO1XodSrxGNz6sCEbnmS0lAfQZDKyb3fbr46gSuv6Sg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.77': + resolution: {integrity: sha512-Jb59xfEISoN5mmbnA+HYqdtrSX3CgCtJoof+V5D8/TgUI56W63GEEd5Y58WijU3Ou6+WEgaLD1feVzaRXV5IDQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.81': + resolution: {integrity: sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.70': + resolution: {integrity: sha512-2ry03fGRJr4sV3jI+ocjj5JqALnFD6ymM5KiNCDZMvq8bX2GSbE0vji4aM43TVCl2nXqqLRZaUxdq/KeWRAY4Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.973.14': + resolution: {integrity: sha512-jkhg/8ocAAoc0RFyLMhCw+/zZh7gystQgd4F4hznNa8P4Cc501PQmxd+jGLiMHodPJ+7Zv/3znM62gZojyasmA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.76': + resolution: {integrity: sha512-d3AGyVu759PGr35mEB2s22xxlNEA5rpdxtSPJthfPFJvoQ8dt357iVPECqWfUxXp1toJAvKmbtcIYVGigaGsCA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/eventstream-handler-node@3.972.34': + resolution: {integrity: sha512-cTeVzpu1xEAkryTZBYhGwnQ6gOGyp8ZYZvmn0Sg/nI/ABmy/CRHHxPDJDUi9PxwxUtGGaatvfRUB3FCgT/rSWw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-eventstream@3.972.29': + resolution: {integrity: sha512-dlRzHCgyB8W6hLuDC5pcT5q+ziPt00n4QGgGBE17ucLVU4zMa6lsbuUdQ2Pm75Z5VA8GF+R/+SgrRcaTdIzSIQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-websocket@3.972.52': + resolution: {integrity: sha512-vsPPM+nMbKJlUCFU+eoGZbdxdxDIAX9LbpjSXaR5Ufpmqgp8TdYQnoExhLu4T3umW/JIIPny1ydbhWidZZYokQ==} + engines: {node: '>= 14.0.0'} + + '@aws-sdk/nested-clients@3.997.44': + resolution: {integrity: sha512-NhEgryjlBF9w38ZXqGymQV28IhkYa1mKhlbYnqIis57AYwWGVYfUPgg/qC2rLRqOUfblxx++irvju10kVTa8Vw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.46': + resolution: {integrity: sha512-L+2xZTye/2T96f3lwCws0Zw6GG2JHZW9e8FpVgGBeeExSKyeoZ6CWRpBml/7DNiK/O26jrgPM9F+Ay8VkgzUWQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1048.0': + resolution: {integrity: sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1116.0': + resolution: {integrity: sha512-ygIivKqh8aHzNkucOCXHyIBgBpLPfrSI0mCqXF+vLBsPTUKqj0VSqAY0GFPe7lQl4HntjOcQ+KSyS7oUV2C54Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.974.5': + resolution: {integrity: sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.10': + resolution: {integrity: sha512-ycwH6Zd2GhuSqdXX9ihbCjeGTB6xOJs+O3+Jb8/zDG9978XU80qs75dfkPJRMNKe5MvBZPuNeFpd4JZKPoUF4g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/xml-builder@3.972.40': + resolution: {integrity: sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.3.0': + resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==} + engines: {node: '>=18.0.0'} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.29.7': + resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.29.7': + resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.8': + resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@csstools/color-helpers@6.1.1': + resolution: {integrity: sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.3.0': + resolution: {integrity: sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.2.1': + resolution: {integrity: sha512-YpAJZhaHplYQkG8ib+/Fx5Y0eF2lVWi3tIvMJA6i39TLyUNp2439cifzW8VMjhlqrBjHzK5hVGugRRm2zTKI/A==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.9': + resolution: {integrity: sha512-iGGw4OsAYsS6pD29MdJ2bX/nJx65a04ZZiw6x+VwWlP2DdXf6f++Zmuv/OzALpdyfVhjbduIIF2cXM7HWBIe9A==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + + '@earendil-works/pi-agent-core@0.84.3': + resolution: {integrity: sha512-VURr+xBRl3RxYcw3kT9Pn3yfi6LbRoCJgHF7h1mAblMjtLNV/MfG/RyF0uJizBAM886AEakSiw3j9c/aSngppg==} + engines: {node: '>=22.19.0'} + + '@earendil-works/pi-ai@0.84.3': + resolution: {integrity: sha512-M0YUV8vNO3y2WwWSyY8ijKJV5W4gkSUixuvk+Z00ZBjsyMfsdXfITsHEwP1UIf09YRWXT6oGn0GlCamt+P32XQ==} + engines: {node: '>=22.19.0'} + hasBin: true + + '@earendil-works/pi-client@0.84.3': + resolution: {integrity: sha512-zfErYane+390W0xpBJ/FWCp6aktPpkpcIcXUeZiAziWLoxE80ZNQALRyOSa/gGS5V+1OkNnMYxRxbzN0zUvnOA==} + engines: {node: '>=22.19.0'} + + '@earendil-works/pi-coding-agent@0.84.3': + resolution: {integrity: sha512-Yr2p9PubrbFZmYEPYI+C8KmZP9xlFuLDnAG64RtU0ZDgrdiXYWa+y7WGyJO5OlqPliOkVCMd9IzVszO3/t0D0w==} + engines: {node: '>=22.19.0'} + hasBin: true + + '@earendil-works/pi-protocol@0.84.3': + resolution: {integrity: sha512-9a4g6WhLOvRqvsIOFaWxg/2gdrbY4Thclwj5ipLUPAWChfsDJ/8XdPc2sRhSOkD6EsxpEFJz3xppcfwI6EcZDg==} + engines: {node: '>=22.19.0'} + + '@earendil-works/pi-telemetry@0.84.3': + resolution: {integrity: sha512-sgEkWoKrvSGaKn+YfLLFZmn+/A7B/w62eLwTD57nI+C9to8ITlFFVbgC2OtwvPnT3NFGHdCd53qhBEMIlptD1g==} + engines: {node: '>=22.19.0'} + + '@earendil-works/pi-tui@0.84.3': + resolution: {integrity: sha512-fS6OEQKEEALnKa6Uw8LcgZZ+9CWck7f3MQSCETQp6leUgIFwMEDtKmOUnL9nsYm+RIPmy7OmplVxYRbV6hiaFg==} + engines: {node: '>=22.19.0'} + + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.6': + resolution: {integrity: sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.5': + resolution: {integrity: sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@exodus/bytes@1.15.1': + resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + + '@fastify/accept-negotiator@2.1.0': + resolution: {integrity: sha512-F3EVbzWt+xcnVaOHmWyIlpuFtbxOln7HDZQsh09MtMmMm/CipMayNt8hnIL8VQi54u2ZociDbf+iluGYkf7B1A==} + + '@fastify/ajv-compiler@4.0.6': + resolution: {integrity: sha512-NtuzM0SfaMJbGlnjr9LWQUN5LzgSrbB8tf/wRZNas+4E1O/Nmzl53e7ruT61HDZyRCJGC6FxIogmNZO1c5ETBA==} + + '@fastify/busboy@3.2.2': + resolution: {integrity: sha512-yXSS27qPExaXeuLvMRMXOLtpipzfQYNjG3FkunDWKGfMYjKuhFXko9CVzqxm8jcF+lmtS9Fd89QNdh9XDjnbNg==} + + '@fastify/deepmerge@3.2.1': + resolution: {integrity: sha512-N5Oqvltoa2r9z1tbx4xjky0oRR60v+T47Ic4J1ukoVQcptLOrIdRnCSdTGmOmajZuHVKlTnfcmrjyqsGEW1ztA==} + + '@fastify/error@4.2.0': + resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} + + '@fastify/fast-json-stringify-compiler@5.1.0': + resolution: {integrity: sha512-PxcYtKLbQ8Z+yApiqjK8FwxIwvEj38k2OiLc17u8dkJSlmfi2wHHPaSnaoqBPQqtvF8YVsDgDpP2snDCfFrpfw==} + + '@fastify/forwarded@3.0.2': + resolution: {integrity: sha512-NE8HgKLgYejV9lDpqkEFaDKMLYelJBVfHekhB0UKvX0ghagXRJqg68feg8er1NPXxG4N9i6vPxzt8E+3wHfcmA==} + + '@fastify/merge-json-schemas@0.2.1': + resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + + '@fastify/multipart@9.4.0': + resolution: {integrity: sha512-Z404bzZeLSXTBmp/trCBuoVFX28pM7rhv849Q5TsbTFZHuk1lc4QjQITTPK92DKVpXmNtJXeHSSc7GYvqFpxAQ==} + + '@fastify/proxy-addr@5.1.0': + resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + + '@fastify/send@4.1.1': + resolution: {integrity: sha512-BYo+EiaKwlxH+WetGk6hAs1d39iP0y1gqB8lGF/qwkJ9ZZ/cBY1vx5NvExb9Sc3yRMFjD5X4Eyh4e4+TzRkzdw==} + + '@fastify/static@8.3.0': + resolution: {integrity: sha512-yKxviR5PH1OKNnisIzZKmgZSus0r2OZb8qCSbqmw34aolT4g3UlzYfeBRym+HJ1J471CR8e2ldNub4PubD1coA==} + + '@floating-ui/core@1.8.0': + resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} + + '@floating-ui/dom@1.8.0': + resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} + + '@floating-ui/react-dom@2.1.9': + resolution: {integrity: sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.12': + resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} + + '@google/genai@1.52.0': + resolution: {integrity: sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.25.2 + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@isaacs/cliui@9.0.0': + resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} + engines: {node: '>=18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@lukeed/ms@2.0.2': + resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} + engines: {node: '>=8'} + + '@mariozechner/clipboard-darwin-arm64@0.3.9': + resolution: {integrity: sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@mariozechner/clipboard-darwin-universal@0.3.9': + resolution: {integrity: sha512-BGGR4iA9Z2shAjI65eI5xtyb3LYNlDW9X3gxKxDbqtbnREohsrqznov6zpKoIrsRWpzlYVEdKphS7ksJ0/ndSQ==} + engines: {node: '>= 10'} + os: [darwin] + + '@mariozechner/clipboard-darwin-x64@0.3.9': + resolution: {integrity: sha512-4kURmCbS6nt8uYhtmWpUcJWyPHfmAr5dTpXD1nO3pIfa+TSQ9DbrGOYCKH+aEFW47XhQ4Vp8ZTszie+wfFvDKg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@mariozechner/clipboard-linux-arm64-gnu@0.3.9': + resolution: {integrity: sha512-g59OkUGP2DDfCOIKypHeYgv2M55u/cKvXa5dSxFbEJ34XvIQMdcVmpKCkGUro3ZgefXiGVdwguvTMQGpHWzIXw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@mariozechner/clipboard-linux-arm64-musl@0.3.9': + resolution: {integrity: sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@mariozechner/clipboard-linux-riscv64-gnu@0.3.9': + resolution: {integrity: sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@mariozechner/clipboard-linux-x64-gnu@0.3.9': + resolution: {integrity: sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@mariozechner/clipboard-linux-x64-musl@0.3.9': + resolution: {integrity: sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@mariozechner/clipboard-win32-arm64-msvc@0.3.9': + resolution: {integrity: sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@mariozechner/clipboard-win32-x64-msvc@0.3.9': + resolution: {integrity: sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@mariozechner/clipboard@0.3.9': + resolution: {integrity: sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA==} + engines: {node: '>= 10'} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.2': + resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==} + + '@radix-ui/number@1.1.3': + resolution: {integrity: sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==} + + '@radix-ui/primitive@1.1.7': + resolution: {integrity: sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==} + + '@radix-ui/react-accessible-icon@1.1.15': + resolution: {integrity: sha512-WTQwcAvQf5sOcuUyi90lKPbhwcvQ+j55cjrSmeaN+L2vKU3DooOvlKw2MDeiJ5IkV5N905KW0/fGojKOBhD11A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-accordion@1.2.20': + resolution: {integrity: sha512-jDhG9FvAEnlhnjrsINbNXcUa4G+L1KqSkJSunkbKEzFRcAb52jvM0PjPxPRvhe1HNc5F5yc0yzzWeeqlH4yBIg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-alert-dialog@1.1.23': + resolution: {integrity: sha512-VAYOiQRqj3GPpYJE0I9J+X8Ip05cyVlNdKOFeiGS2Ou1HHGfpl0BxOyZm6nmVDyU+W+NF3/XLzmjHmVGydhwgA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-arrow@1.1.15': + resolution: {integrity: sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-aspect-ratio@1.1.15': + resolution: {integrity: sha512-fy+dyVR+90nelK8rqIznFlxzx7uPcGbhxH8Nfr2bHb4UfSe+e3hklOC0luK0hDwVwnRX7xTRySpsrQVeW+/oNQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-avatar@1.2.6': + resolution: {integrity: sha512-4ULOTJ/mqy2hT9GlWa/MFHxHSvH3nJzHnZM1waNsc5Bonv7i70aNenghXmD97S6OJ81ekXONGGt4nT1r0PfEdA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-checkbox@1.3.11': + resolution: {integrity: sha512-Gnptr9pDDQxD3hgq2dtPbtrp/c2qH1mBwIzw3X/ivrMb2e1t0jMTi606fVEqFPaQR1ggXIVQWKj3P2WW9v7zGQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collapsible@1.1.20': + resolution: {integrity: sha512-mcGesGplBnzN2sbvJETzpCNfSMyPnb29q1GRLU+Ib7bJrpIG2ywmRoh2V5VbA2uNvKikKUlVbAPks7JDjz4A8Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.15': + resolution: {integrity: sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.5': + resolution: {integrity: sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context-menu@2.3.7': + resolution: {integrity: sha512-CtXP35dxaB5T3zXSd+E3uHe/QpXcpYnZmxp6OaIbfthtfW4wyb77M23BG+bwIJDtsMwEP/YssdsmNyZu7jhWew==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-context@1.2.2': + resolution: {integrity: sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.23': + resolution: {integrity: sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.1.4': + resolution: {integrity: sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.19': + resolution: {integrity: sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.1.24': + resolution: {integrity: sha512-geq8l2rJkxvkXsT9RMgtUE3P8pITFpTsvYpbySi1IH4fZEABD/Gp85myayFgxk0ktljGMJnCbeFkyTusvSvv7g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.6': + resolution: {integrity: sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.16': + resolution: {integrity: sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-form@0.1.16': + resolution: {integrity: sha512-Q4TLEn2A7TAypxwmd6R9EwrlXDvkfYSDMrq9/887AXAGh+G1rH+kYJKSTv+Si9Y0JPKTwKYv6PviAJosysNimA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-hover-card@1.1.23': + resolution: {integrity: sha512-H8qONfZd3ltrU3+jHCIgITbWo6e1iTKvP9DHdrvYbX48ooRM5FjEDTn16AMwdfuOGkWdZEhpl3PLL/Wk/AnHDQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.4': + resolution: {integrity: sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.15': + resolution: {integrity: sha512-o/rdYEwZTTo5tjknnPeyQFU45kUC4i/XyeDPP+HGyi6XqpOP6Zf5Ya5vh/Yfe9Id5JiuWnnAx2XqIeD3UYZt0g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menu@2.1.24': + resolution: {integrity: sha512-uW7RVuU6Lp/ZtfeY4b3kL32zccgEWvPv1+cf17ubYzHa9cL8AHokmk36cG/XEiH/smbQvumnieXX9j/e9RqJWA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menubar@1.1.24': + resolution: {integrity: sha512-eeVs0vf7cuqXaM0qLQCPcufImiJNVBXdJDLu7ZGYl2732UH23Qat/foNGrr6vYV3/DdTsBqASoggUFgH14OcZA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-navigation-menu@1.2.22': + resolution: {integrity: sha512-ou7iLEJ+yrhQndkkA4U21XIdS/CS45F4iXIkTZcb6/Ne9EMsOuDudVmCwmDnfFZZ+y1FZqXRNSIgBy+YMvZVZg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-one-time-password-field@0.1.16': + resolution: {integrity: sha512-Tj9P6ntAJEw52oq/F0AGknXR4XncxEt7XU47O3xJQOiWfLzEy3d9gtgKfvjSzGxzHkfL+VzvxGu2KTFsloJqXw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-password-toggle-field@0.1.11': + resolution: {integrity: sha512-4gvFnmDXu3dgj21CqsufzIameRvlRd4SBqaWhcrlrNhRo0Y5i/49AmRJYe1fdAM3G2VNBbmin4b0D6cdQocwgw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.23': + resolution: {integrity: sha512-mw58MrBlyHWFisTOYignD0vf/3gdcgAR+9of1s9G/38CbFiUwH1nCDkc0AUM9IrXFgN5Ue8n45j9WCgyM1sbiQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.3.7': + resolution: {integrity: sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.17': + resolution: {integrity: sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.10': + resolution: {integrity: sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.10': + resolution: {integrity: sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-progress@1.1.16': + resolution: {integrity: sha512-5XnomAsoZZCY+KNTxbIghpGqPruZvKFNlvcAljVAOdDRDsH4/OZQxhtwo5wdtoDM5R6MhJBb2sPnDuRFep3lzg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-radio-group@1.4.7': + resolution: {integrity: sha512-cgYFEkntCxppHZgtSZ+7vh0wbZQ+IC7PPMw8DSnRG27B6kDd32/Zw0OJt7dGDigCoprMuWHjg2PvUn3PYvPFoQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.19': + resolution: {integrity: sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-scroll-area@1.2.18': + resolution: {integrity: sha512-Zn5Cd171wxsO3Dfg8HaW6RifTb9CYTKQJHs/G4+LN1GfmJpaQMZQyQxMprVPHpaz7QY4l9BxK2JwQuzHsXC8nA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.3.7': + resolution: {integrity: sha512-WFGImkmbzcfxeIwq/+4HvRN0pizBwbwQUED4I13ezQsDdfl38ZntN6TmR8XaSzPBqoCToe8rF75j6NPNDSzhbg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.15': + resolution: {integrity: sha512-jOLO4lssEzWpoDu7G+Ze4VjwMRUBt291pnZD0gmalREZipnTX3wadQo7Fy48GCTfe14/YRN6rw/rOJqrE85Wxw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slider@1.4.7': + resolution: {integrity: sha512-mTSLf1GC/C0moWjTbvCM6Qn/gBjvlFt1azuWF2v7MN5C3Zq2U2J2lN3ZEYkpujuOU5Ro7A28wkviSxaKnG0BYg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.3.3': + resolution: {integrity: sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.3.7': + resolution: {integrity: sha512-48tB/4dn2UVLBCYhTu9AuR63IHl73l/qLbLgxd86noTUor4/K4LFDAcYjK+isP5313qxaFpjPVogE7+Y0/V3Kw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tabs@1.1.21': + resolution: {integrity: sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toast@1.2.23': + resolution: {integrity: sha512-ofhyAsYaocRGOs/n0XWdUOSVzEAG6BfrMVM8z0c0kLEWY38w/0WuMFPTJP/HVaZPYkMvHZoKIIhNcjbTCBILPg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle-group@1.1.19': + resolution: {integrity: sha512-OtnwuSVjd1Ofi+AdnvhsjQdyuhCDwYs1w9RyB5BN/OavXOVQo42SYqQjwUnbPnaiPFBpQ9aX70dWeee+v2oBLA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle@1.1.18': + resolution: {integrity: sha512-7lonPlKfSacd20GlOBx2ltuVKz9oqWYZz+oMQyOltw6t1y2nyftj2ZmwwUHYn49kqfDWcp8dNZm5NgV+5Z+mug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toolbar@1.1.19': + resolution: {integrity: sha512-Ph0IvtYw4VB12ZnZg+YtrGs8yJQsnizwo/zu0R4Y/nWugtJzA7Pg1eWeuDR9+LSqn+xjamss+UOSOJJJ4gx8jw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.2.16': + resolution: {integrity: sha512-6EamKFRRnlpdadndbZ6LMwycfwkwPte1B42hs6QA0gYhjaOKqW4PZ4pjaW9UrlDX5eVt/OjncE7BFTPL5nmZhg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.4': + resolution: {integrity: sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.6': + resolution: {integrity: sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.5': + resolution: {integrity: sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.5': + resolution: {integrity: sha512-ge3ipobwSXTj4JyVtswQ7qZj0ZHdtbGuOno/LrgAAeSxtsJ6Vs4Gz5IkPH2bmqpjcLUFoqGhA/mueuIf63UXlA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-is-hydrated@0.1.3': + resolution: {integrity: sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.4': + resolution: {integrity: sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.4': + resolution: {integrity: sha512-XoSLhbRbqxFtgJoi2fNHA3C6pDlY34x508vUpUGoFZfvePfHXHbE1lC4FYFMnJWgiCRroSTw6fOsXQoVS9RwZg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.4': + resolution: {integrity: sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.4': + resolution: {integrity: sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.11': + resolution: {integrity: sha512-NFS86RYYZb4/exihaESBGOpMJFz8MGLAfu3mOBSGByVnVPC9JPASfYubxd/8KbkQK0sYAv8lVQDEQukDX/qXvQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.3': + resolution: {integrity: sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==} + + '@rolldown/pluginutils@1.0.0-rc.3': + resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} + + '@rollup/rollup-android-arm-eabi@4.63.0': + resolution: {integrity: sha512-70TeIFezKKy65LgAVyQh+w94/gjWhvPWaLaGGeMEgVrPkQhuj/M5bAYYZzIFUj9Y69oHyTm5Um/R6gcLh4A8JA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.63.0': + resolution: {integrity: sha512-YC86tYIHK6M1IV+wbzO+Bxk8RCBr6ZyWYgWxUCzaZD8mc8rrFoIJDNzDrkHBYRc/wKdrsIXmm6/F7NzrAO+OrA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.63.0': + resolution: {integrity: sha512-oI+ECtUcli0y0fi4xpW82GdPIXdTkI8G8DSjG2LRuw09fPAGykaWYH/hXxiKuTxiAjiPSTIIuYUqof5Z2hShWw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.63.0': + resolution: {integrity: sha512-NwV+1s7TiKrMe4owHyKB/dTLD7ZJD0YEBEhIz+hvav1Cu1GReJjF+rsdNwjzENQeIAbE/CoNiaAc5Vz2h5DPAA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.63.0': + resolution: {integrity: sha512-tWtHBTu5gOPK4u4Urtk4qAHW3zZ9rQAmbssO8gp7ELvGTGI3aCiq6NqyTQ0PCIg7KbHJF2UkGDDs77YZGxfjCA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.63.0': + resolution: {integrity: sha512-2qPoJiwTvtHQ27NnYvTnsgk8laXWYuVmNESG8WFZBcEPKLfZ3I27qBJarjVRQtwGeYyRfq5ZowHXih9lm2BItw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.63.0': + resolution: {integrity: sha512-FQwsTRvLNuHoTdICABJQfbPUSEueISGmnpT06tXTMpfprf5NiKLSXKA0A+w45wJnCmZAnzgqBwbt6ARFuyOi5w==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.63.0': + resolution: {integrity: sha512-BBVTXziw8mY1a4ZbWME9tZyfzqXCDPqaC7Z3heQ29p5dkvXzwL0NwelO8zLa8c3RBKvl3YTuSnBgsBhYBtwjIw==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.63.0': + resolution: {integrity: sha512-w2Iyy9+RqKwx3d9qWMKsJg0FfRBsY0/pXNv0mCQ3ueRvJI6+QAScfD4nrMlzFLs2HNVW6Ew+mtZfDl9b7Ew5/Q==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.63.0': + resolution: {integrity: sha512-YK++KtrFRHYE0P6/RtYEAy9t8F37znP+K03RrIuLPYOL6SVlObRumf/0OE4V/h63xL9DwkWbNssZfmA9hawuDA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.63.0': + resolution: {integrity: sha512-aBfOG6fP7YkkPmTqPwufRJeFyz7WPpECv9XNbnsk9+vg7rxdih0lbtEel7jcRng4LZrrmU3FfitCFyEj4BWDWg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.63.0': + resolution: {integrity: sha512-LGaHEOeHNAag9VuS1Crs5DFg4RrU9MPi2nVnNJk9DTePx/B6RRYKVmrIXt2h7YOJlwjaFJ6lwtFDliZxScTLrQ==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.63.0': + resolution: {integrity: sha512-jClvk+J0FC3b7Udvegiw5/4hErbHtmsNsQgENnKXDWtNCJXsJYZH5WURvu7imDOO38xYml24eeh5x3A04ppwCw==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.63.0': + resolution: {integrity: sha512-0OJlaGK+8+B777Ql5okIpD7ua5Ro9+VB9Ve0OKa28OQJZ1RbuUBVNHK/e3pr4BROqsyPl1JrPO1ZxJseCNffcA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.63.0': + resolution: {integrity: sha512-Ygsx+HoNH7afwi1bTIXbnTvVnsO+zurPLSYxybV1hHFVU72OWOCl6v05ql/z0hkpAPx+DK7Kn9Bi7MayCcjLTA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.63.0': + resolution: {integrity: sha512-pDQxtMGb+OvG3fLwR2OkZlSd47hW+kWg4BYMG/++sR6RqorQccwPTDsxda5hPwiIeIErAnCF9ma3SAU06bdQtQ==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.63.0': + resolution: {integrity: sha512-0BnUG9mS8I4SSHr3XsxVhuCMEiu+rX61xxZF5vujso4LaiAGFZFxvDjg6Xn6tLPNTUAfuCvQYas4LMQMVsKRSQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.63.0': + resolution: {integrity: sha512-Adu/VttB1dpPNW+FEacrZ+xVm9tFty84+RrFzsqlFaPxoJB+9XXyDGtp5dCOoBwGBIEVH0To7lExFXEx0BIF4A==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.63.0': + resolution: {integrity: sha512-NQ3bDvjUbFKmP23671xUlXtKmqVsUBd6M4PQCvbmNtOy06hnQIdKHy8oG/6S3R/S6He1JgPk6A5VT+prAJMYEw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.63.0': + resolution: {integrity: sha512-u2eDAl4+0aFvA13GxlGBtTI3SS3sdgwgtV0HyjZ0QaQVCgNE+jqNGey+GtxWiq+wxr/UycAx/OnfJzApCFamvA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.63.0': + resolution: {integrity: sha512-XvRb5vfW3wAZQ+ZUG21AnHHDKtNcw99eigzEhjr//NZ3u7SoBaPP0seSc7FgP7p1epAEdAoZckMW9WY/+4w70w==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.63.0': + resolution: {integrity: sha512-iZPmniy4kNBf5yo2RezbkYNNK5HPbXE9+g+twnbqSng7dtLEJy1SKoxiE/ni4FDacjyuZpEeb9U054N4EoKHYw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.63.0': + resolution: {integrity: sha512-mFBBd+LF37fnE8JnYUOH+imj0aPFPK30vpar4ehJkgnLj9sZn8ZxiRENmLtgIwxK7TC8klF6N57fxdNBwQoqOA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.63.0': + resolution: {integrity: sha512-ujeqEY3B+zbGn3Z4Q03cUBG/LGWnBJncVT36WER31LcOsQk9+1dmINKKtvmmfChUvRbK1G0R8OhMWFgHgaZtAw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.63.0': + resolution: {integrity: sha512-hncn90N4sOky0L2LKE5oESKLbxCPeVo4eLA2LSMoDzM+879ml4WSr+Rr4DWknNIVVvS1Hirkc9hx02W6YxS8rQ==} + cpu: [x64] + os: [win32] + + '@silvia-odwyer/photon-node@0.3.4': + resolution: {integrity: sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==} + + '@smithy/core@3.33.3': + resolution: {integrity: sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.5.2': + resolution: {integrity: sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.7.2': + resolution: {integrity: sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/node-http-handler@4.11.3': + resolution: {integrity: sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.7.3': + resolution: {integrity: sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.7.3': + resolution: {integrity: sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.17.2': + resolution: {integrity: sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.10.0': + resolution: {integrity: sha512-HQwu0KaB2zyT0iLzBL+8CLyZDL3KlZlZJ+2iyc9uCUnlJVskJU/UlPuVCyIPhtukjPQdT2QNoR5nCP5FqTmmDQ==} + engines: {node: '>=22', npm: '>=6', yarn: '>=1'} + deprecated: Incorrect minor release with breaking changes (Node >=22 and required @testing-library/dom peer). Use 6.9.1 for the 6.x line, or upgrade to 7.0.0. + peerDependencies: + '@testing-library/dom': '>=10 <11' + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@testing-library/user-event@14.6.6': + resolution: {integrity: sha512-Jbs9FpkkIDw8FgSc6kOVsOv8JuuqGAL7J4X1oot77JxAoDlkNn2GRkd0aYRVuQ+pVQAiHWVkE4rX/dkF5fBiCw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@24.13.3': + resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} + + '@types/react-dom@19.2.5': + resolution: {integrity: sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.18': + resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} + + '@types/retry@0.12.0': + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + + '@typescript-eslint/eslint-plugin@8.68.0': + resolution: {integrity: sha512-WASHDpCm6qO5jj9g1a+8NiW5+GCkAyLReR56/4VruYmNgfUmqpxOfZ2Yfb8xGfJPWv5Qi6LSD8sXdces3vbp/Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.68.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.68.0': + resolution: {integrity: sha512-fHq2VC1kpyYfvEcbiMjOpySY4WS7voEp89yAThrHRX5sm9j2lzYppCb2umFMEed4fWcyeLjHxrz0mpjNBaBxMQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.68.0': + resolution: {integrity: sha512-5GQtWZCXFcFYux955pvoS02WLc49pXNlvIxocKjS0clvwo3in1RdlzVKyiqQH9vE5AKWFLTaUgeQkOrTS+0Qxw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.68.0': + resolution: {integrity: sha512-T5eXpcaJNg8bhjHJ8Rjp68Vq/QBteYtTKY8TZqVNPaUbuz0f6jI9t6aDkylwvalpAB9XTTFeFOjrjXAZ3YvmVA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.68.0': + resolution: {integrity: sha512-F7zrGQfiJHojPwi8vhxZQC1tWtJzvL74cK/nqri2lk8YUXvYaYwl263xOJ69jDWPUk1hmcdoayFwk9lX09npVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.68.0': + resolution: {integrity: sha512-X77zqoY1EjeWGs/0JNxeaMfp5C5lIz4Tw8y66F1Ne8Faq6g424sBNYM6xBAqElfGZPLpWS+CZAp0DXyKDzWiHg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.68.0': + resolution: {integrity: sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.68.0': + resolution: {integrity: sha512-OKKsD0tYmoNiU5PW2zehO1yO56jYOm1ShYlxon/Z0SJNidAkdVg86eg9ruRuoXf8xfnuWZGbwDsStkoXbZtIIA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.68.0': + resolution: {integrity: sha512-PB5gJMMOg0Q5P1tsgWtEAqQacJXq0qEqRHDX/YJ4FaTMLfZPpHB3gjl2EJuiZyPABxmj4ZQYiY9m1bdAJ5y7tQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.68.0': + resolution: {integrity: sha512-YR65gGdGvTUAWLldC3xLOvOzamdGzB4A5/N8rehEaHs3Zvoe39BhgY+u0SPch1OvrVTfLcc55wsSgK2NcnTS/A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-react@5.2.0': + resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@vitest/expect@4.1.11': + resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} + + '@vitest/mocker@4.1.11': + resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.11': + resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} + + '@vitest/runner@4.1.11': + resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} + + '@vitest/snapshot@4.1.11': + resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} + + '@vitest/spy@4.1.11': + resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} + + '@vitest/utils@4.1.11': + resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} + + abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + assistant-cloud@0.1.42: + resolution: {integrity: sha512-w76ACM0705SNmWgz3Zl0SwX95rukju30PUPNzeEFEXhLi/NRQyWshYF8izdaHeoy9h4dgSuJtnS5v1Qh6IMClA==} + + assistant-stream@0.3.40: + resolution: {integrity: sha512-gAQZC80N6jfTH5eIbCmAM94DKpEgmnLnIZmk3UtmrpogePGuCFX73u5brASvDYnekZK/71Ed9mwvCzFWIyzdZg==} + peerDependencies: + ioredis: ^5.10.1 || ^6.0.0 + redis: ^5.12.1 + peerDependenciesMeta: + ioredis: + optional: true + redis: + optional: true + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + avvio@9.3.0: + resolution: {integrity: sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.11.19: + resolution: {integrity: sha512-Grytf1xOxOEMTGRwx6rLGKkTabd4vMg3VrKdj/7joCmV0qgh4QwMMO6xh34YEXQqirAuUdgQGa5orJQQ+69RBw==} + engines: {node: '>=6.0.0'} + hasBin: true + + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + + brace-expansion@1.1.18: + resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==} + + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} + + browserslist@4.28.8: + resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001810: + resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + cssstyle@5.3.7: + resolution: {integrity: sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==} + engines: {node: '>=20'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + data-urls@6.0.1: + resolution: {integrity: sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==} + engines: {node: '>=20'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + 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'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + electron-to-chromium@1.5.415: + resolution: {integrity: sha512-958V+Kbhtgz+SxXeEVKBjrlKRBIDAYvUJfwhjxMZ5S6ut9jAl7l9ZKBkBrvjyjZE36PabLUo2L8kEeV5O4vgJg==} + + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 + + eslint-plugin-react-refresh@0.4.26: + resolution: {integrity: sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==} + peerDependencies: + eslint: '>=8.40' + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.5: + resolution: {integrity: sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-json-stringify@7.0.1: + resolution: {integrity: sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + + fast-uri@3.1.6: + resolution: {integrity: sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==} + + fast-uri@4.1.3: + resolution: {integrity: sha512-7+72G6vLt7jjNas8SmSATx2qeyRIjxeqO3i4IkmDTxlqYZRKANhOe1bnovcp4WZmvsYrp60WyqPyHqgRiX0yXw==} + + fastify-plugin@5.1.0: + resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==} + + fastify@5.12.1: + resolution: {integrity: sha512-FWi+tQvwxR/PeRX7Z2mhfEF5ozJ3jn9asiiclzKXNSzJRHAYcU924aIOKAdHFJ+YIKieh3cqr1IwCOvTr41B3Q==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-my-way@9.9.0: + resolution: {integrity: sha512-sJsgZ1sQH2UDuowPuMKg8az7Qc8F0jnj+SKkFWU/+T0xcFlgV5skgXOGUqmQzOdmW6ALA7AhJINWx3qFBkbLHA==} + engines: {node: '>=20'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gaxios@7.3.1: + resolution: {integrity: sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==} + engines: {node: '>=18'} + + gcp-metadata@8.1.2: + resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} + engines: {node: '>=18'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@11.1.0: + resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} + engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} + engines: {node: '>=18'} + + google-auth-library@10.9.1: + resolution: {integrity: sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw==} + engines: {node: '>=18'} + + google-logging-utils@1.1.3: + resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} + engines: {node: '>=14'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + grok-mermaid@0.2.2: + resolution: {integrity: sha512-XcJEP5dDC8liHBh52mlLjU18fNvu1ckFsu0QpIG3+APZ270fsj9wxpiA6cOURmbUEuoMVgjbC2+UYgTdCqqgzA==} + engines: {node: '>=18'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + + hosted-git-info@9.0.3: + resolution: {integrity: sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==} + engines: {node: ^20.17.0 || >=22.9.0} + + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + ignore@7.0.6: + resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@2.5.0: + resolution: {integrity: sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==} + engines: {node: '>= 10'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@4.2.3: + resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} + engines: {node: 20 || >=22} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} + hasBin: true + + jsdom@27.4.0: + resolution: {integrity: sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-ref-resolver@3.0.0: + resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} + + json-schema-to-ts@3.1.1: + resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} + engines: {node: '>=16'} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + 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==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + light-my-request@6.6.0: + resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@0.555.0: + resolution: {integrity: sha512-D8FvHUGbxWBRQM90NZeIyhAvkFfsh3u9ekrMvJ30Z6gnpBHS6HC6ldLg7tL45hwiIz/u66eKDtdA23gwwGsAHA==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + marked@18.0.5: + resolution: {integrity: sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==} + engines: {node: '>= 20'} + hasBin: true + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@6.0.1: + resolution: {integrity: sha512-3wVS3i51pE2pi1k5FFL/95BGfVS0kSsvDVuGXHOtxox/TywUmtgq+3qiTOTbs9J7KfHaXPiN171k/A6dBnaXFw==} + engines: {node: ^22 || ^24 || >=26} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + node-releases@2.0.53: + resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==} + engines: {node: '>=18'} + + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + openai@6.40.0: + resolution: {integrity: sha512-MWtTjd/gQt4jpbji61NTgFWJLoY/PdRJ6wG9/ZDRMYNMlBKrCrSlkLI+KgHP1vR1qT6LKSAyAqIxno6lcK9JiA==} + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + + partial-json@0.1.7: + resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.1: + resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + hasBin: true + + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} + + process-warning@5.1.0: + resolution: {integrity: sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==} + + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + + protobufjs@7.6.5: + resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} + engines: {node: '>=12.0.0'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + radix-ui@1.6.7: + resolution: {integrity: sha512-QBdhh1arIEUvPC0dQ5+nwWAxt7+N+oP/9jPwjJkGFoSk/sqxg32gJtSXGtFh8frAIcS6oC9cx2Q+7KYCQLOAeA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + react-dom@19.2.8: + resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} + peerDependencies: + react: ^19.2.8 + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} + engines: {node: '>=0.10.0'} + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-textarea-autosize@8.5.9: + resolution: {integrity: sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==} + engines: {node: '>=10'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react@19.2.8: + resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} + engines: {node: '>=0.10.0'} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + real-require@1.0.0: + resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rollup@4.63.0: + resolution: {integrity: sha512-T5vnZ2y4QqC3/4P+w2+JO+Q/OVdnPsv4XcSYJYMEn0R9/jjl5AgLwO9LAZMzP2lN71O6pypn91rB7lDstUkfrQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-content-frame@0.0.28: + resolution: {integrity: sha512-P8QAY8NZOEGRNMR8ybzKKELuz3DA46NOjhZXybGcaNejLMrMBSqvrUdqNHoR1kfL0iFtWYoD7wgIzp3qRysgtQ==} + + safe-regex2@5.1.1: + resolution: {integrity: sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==} + hasBin: true + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + engines: {node: '>=10'} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + 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} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + thread-stream@4.2.0: + resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==} + engines: {node: '>=20'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + + tldts-core@7.4.11: + resolution: {integrity: sha512-CW3WN2rIIE/Of21mulhgnGOwoDyEFNygyIBOONSdyAuSATgMMUCpLeUlB+E8sAwA5xRV9hYPl+kyZ9citHCaKg==} + + tldts@7.4.11: + resolution: {integrity: sha512-aBiNayCfTQxuIJBm06M+xR14cYaYlDlSXZbgsnKzKNxDKUVq7KFwTjwBSsb7m9Y5xO8WfPnBc63WaYFMTGlvqw==} + hasBin: true + + toad-cache@3.7.4: + resolution: {integrity: sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==} + engines: {node: '>=20'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + + ts-algebra@2.0.0: + resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.23.12: + resolution: {integrity: sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typebox@1.3.7: + resolution: {integrity: sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg==} + + typescript-eslint@8.68.0: + resolution: {integrity: sha512-MHy0Y0ynqeEbx/S45+i/bBssdy3X6KNBfmJAP35GrgtNxu2TQ5K5xsFDhAnmsq1jvpdoZOPG1LGtJo0HWqYCrQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + undici@8.9.0: + resolution: {integrity: sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==} + engines: {node: '>=22.19.0'} + + update-browserslist-db@1.3.1: + resolution: {integrity: sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-composed-ref@1.4.0: + resolution: {integrity: sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-isomorphic-layout-effect@1.2.1: + resolution: {integrity: sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-latest@1.3.0: + resolution: {integrity: sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.11: + resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.11 + '@vitest/browser-preview': 4.1.11 + '@vitest/browser-webdriverio': 4.1.11 + '@vitest/coverage-istanbul': 4.1.11 + '@vitest/coverage-v8': 4.1.11 + '@vitest/ui': 4.1.11 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@15.1.0: + resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} + engines: {node: '>=20'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + + zustand@5.0.15: + resolution: {integrity: sha512-MpSEjRiBkA9crSYeOUH32rJC7SVqAbm0Fqcqge/bUi2PPoLcBWKOsG+C8mevmpr8TwXHBVkChbbJiyvkE+i/3A==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + +snapshots: + + '@acemir/cssom@0.9.31': {} + + '@adobe/css-tools@4.5.0': {} + + '@anthropic-ai/sdk@0.91.1(zod@4.4.3)': + dependencies: + json-schema-to-ts: 3.1.1 + optionalDependencies: + zod: 4.4.3 + + '@asamuzakjp/css-color@4.1.2': + dependencies: + '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + lru-cache: 11.5.2 + + '@asamuzakjp/dom-selector@6.8.1': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.2 + + '@asamuzakjp/nwsapi@2.3.9': {} + + '@assistant-ui/core@0.3.16(@assistant-ui/store@0.3.11(@assistant-ui/tap@0.9.15(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(react@19.2.8))(@assistant-ui/tap@0.9.15(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(assistant-cloud@0.1.42)(react@19.2.8)(zustand@5.0.15(@types/react@19.2.18)(react@19.2.8))': + dependencies: + '@assistant-ui/store': 0.3.11(@assistant-ui/tap@0.9.15(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(react@19.2.8) + '@assistant-ui/tap': 0.9.15(@types/react@19.2.18)(react@19.2.8) + assistant-stream: 0.3.40 + nanoid: 6.0.1 + optionalDependencies: + '@types/react': 19.2.18 + assistant-cloud: 0.1.42 + react: 19.2.8 + zustand: 5.0.15(@types/react@19.2.18)(react@19.2.8) + transitivePeerDependencies: + - ioredis + - redis + + '@assistant-ui/react@0.15.17(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@assistant-ui/core': 0.3.16(@assistant-ui/store@0.3.11(@assistant-ui/tap@0.9.15(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(react@19.2.8))(@assistant-ui/tap@0.9.15(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(assistant-cloud@0.1.42)(react@19.2.8)(zustand@5.0.15(@types/react@19.2.18)(react@19.2.8)) + '@assistant-ui/store': 0.3.11(@assistant-ui/tap@0.9.15(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(react@19.2.8) + '@assistant-ui/tap': 0.9.15(@types/react@19.2.18)(react@19.2.8) + assistant-cloud: 0.1.42 + assistant-stream: 0.3.40 + radix-ui: 1.6.7(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-textarea-autosize: 8.5.9(@types/react@19.2.18)(react@19.2.8) + safe-content-frame: 0.0.28 + zod: 4.4.3 + zustand: 5.0.15(@types/react@19.2.18)(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + transitivePeerDependencies: + - immer + - ioredis + - redis + - use-sync-external-store + + '@assistant-ui/store@0.3.11(@assistant-ui/tap@0.9.15(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(react@19.2.8)': + dependencies: + '@assistant-ui/tap': 0.9.15(@types/react@19.2.18)(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + react: 19.2.8 + + '@assistant-ui/tap@0.9.15(@types/react@19.2.18)(react@19.2.8)': + optionalDependencies: + '@types/react': 19.2.18 + react: 19.2.8 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.974.5 + '@aws-sdk/util-locate-window': 3.965.10 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.974.5 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.974.5 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-bedrock-runtime@3.1048.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.977.9 + '@aws-sdk/credential-provider-node': 3.972.81 + '@aws-sdk/eventstream-handler-node': 3.972.34 + '@aws-sdk/middleware-eventstream': 3.972.29 + '@aws-sdk/middleware-websocket': 3.972.52 + '@aws-sdk/token-providers': 3.1048.0 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/node-http-handler': 4.11.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/core@3.977.9': + dependencies: + '@aws-sdk/types': 3.974.5 + '@aws-sdk/xml-builder': 3.972.40 + '@aws/lambda-invoke-store': 0.3.0 + '@smithy/core': 3.33.3 + '@smithy/signature-v4': 5.7.3 + '@smithy/types': 4.17.2 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.70': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.72': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/node-http-handler': 4.11.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.973.15': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/credential-provider-env': 3.972.70 + '@aws-sdk/credential-provider-http': 3.972.72 + '@aws-sdk/credential-provider-login': 3.972.77 + '@aws-sdk/credential-provider-process': 3.972.70 + '@aws-sdk/credential-provider-sso': 3.973.14 + '@aws-sdk/credential-provider-web-identity': 3.972.76 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/credential-provider-imds': 4.5.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-login@3.972.77': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-node@3.972.81': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.70 + '@aws-sdk/credential-provider-http': 3.972.72 + '@aws-sdk/credential-provider-ini': 3.973.15 + '@aws-sdk/credential-provider-process': 3.972.70 + '@aws-sdk/credential-provider-sso': 3.973.14 + '@aws-sdk/credential-provider-web-identity': 3.972.76 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/credential-provider-imds': 4.5.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.70': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.973.14': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/token-providers': 3.1116.0 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-web-identity@3.972.76': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/eventstream-handler-node@3.972.34': + dependencies: + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-eventstream@3.972.29': + dependencies: + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-websocket@3.972.52': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/signature-v4': 5.7.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.44': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/signature-v4-multi-region': 3.996.46 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/node-http-handler': 4.11.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.46': + dependencies: + '@aws-sdk/types': 3.974.5 + '@smithy/signature-v4': 5.7.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1048.0': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1116.0': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/types@3.974.5': + dependencies: + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.10': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.40': + dependencies: + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.3.0': {} + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7(supports-color@7.2.0)': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@7.2.0) + '@babel/types': 7.29.8 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3(supports-color@7.2.0) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.8': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.8 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7(supports-color@7.2.0)': + dependencies: + '@babel/traverse': 7.29.8(supports-color@7.2.0) + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-module-imports': 7.29.7(supports-color@7.2.0) + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + + '@babel/parser@7.29.8': + dependencies: + '@babel/types': 7.29.8 + + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/runtime@7.29.7': {} + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + + '@babel/traverse@7.29.8(supports-color@7.2.0)': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + debug: 4.4.3(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@csstools/color-helpers@6.1.1': {} + + '@csstools/css-calc@3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.1.1 + '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.9(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + + '@earendil-works/pi-agent-core@0.84.3(supports-color@7.2.0)(ws@8.21.3)(zod@4.4.3)': + dependencies: + '@earendil-works/pi-ai': 0.84.3(supports-color@7.2.0)(ws@8.21.3)(zod@4.4.3) + '@earendil-works/pi-telemetry': 0.84.3 + diff: 8.0.4 + ignore: 7.0.5 + typebox: 1.3.7 + yaml: 2.9.0 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-ai@0.84.3(supports-color@7.2.0)(ws@8.21.3)(zod@4.4.3)': + dependencies: + '@anthropic-ai/sdk': 0.91.1(zod@4.4.3) + '@aws-sdk/client-bedrock-runtime': 3.1048.0 + '@earendil-works/pi-telemetry': 0.84.3 + '@google/genai': 1.52.0(supports-color@7.2.0) + '@smithy/node-http-handler': 4.7.3 + http-proxy-agent: 7.0.2(supports-color@7.2.0) + https-proxy-agent: 7.0.6(supports-color@7.2.0) + openai: 6.40.0(ws@8.21.3)(zod@4.4.3) + partial-json: 0.1.7 + typebox: 1.3.7 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-client@0.84.3': + dependencies: + '@earendil-works/pi-protocol': 0.84.3 + + '@earendil-works/pi-coding-agent@0.84.3(supports-color@7.2.0)(ws@8.21.3)(zod@4.4.3)': + dependencies: + '@earendil-works/pi-agent-core': 0.84.3(supports-color@7.2.0)(ws@8.21.3)(zod@4.4.3) + '@earendil-works/pi-ai': 0.84.3(supports-color@7.2.0)(ws@8.21.3)(zod@4.4.3) + '@earendil-works/pi-client': 0.84.3 + '@earendil-works/pi-protocol': 0.84.3 + '@earendil-works/pi-tui': 0.84.3 + '@silvia-odwyer/photon-node': 0.3.4 + chalk: 5.6.2 + cross-spawn: 7.0.6 + diff: 8.0.4 + grok-mermaid: 0.2.2 + highlight.js: 10.7.3 + hosted-git-info: 9.0.3 + ignore: 7.0.5 + jiti: 2.7.0 + minimatch: 10.2.5 + proper-lockfile: 4.1.2 + semver: 7.8.0 + typebox: 1.3.7 + undici: 8.9.0 + yaml: 2.9.0 + optionalDependencies: + '@mariozechner/clipboard': 0.3.9 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-protocol@0.84.3': + dependencies: + typebox: 1.3.7 + + '@earendil-works/pi-telemetry@0.84.3': {} + + '@earendil-works/pi-tui@0.84.3': + dependencies: + get-east-asian-width: 1.6.0 + marked: 18.0.5 + + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@eslint-community/eslint-utils@4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))': + dependencies: + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2(supports-color@7.2.0)': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3(supports-color@7.2.0) + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.6(supports-color@7.2.0)': + dependencies: + ajv: 6.15.0 + debug: 4.4.3(supports-color@7.2.0) + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.3.2 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.5': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@exodus/bytes@1.15.1': {} + + '@fastify/accept-negotiator@2.1.0': {} + + '@fastify/ajv-compiler@4.0.6': + dependencies: + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + fast-uri: 4.1.3 + + '@fastify/busboy@3.2.2': {} + + '@fastify/deepmerge@3.2.1': {} + + '@fastify/error@4.2.0': {} + + '@fastify/fast-json-stringify-compiler@5.1.0': + dependencies: + fast-json-stringify: 7.0.1 + + '@fastify/forwarded@3.0.2': {} + + '@fastify/merge-json-schemas@0.2.1': + dependencies: + dequal: 2.0.3 + + '@fastify/multipart@9.4.0': + dependencies: + '@fastify/busboy': 3.2.2 + '@fastify/deepmerge': 3.2.1 + '@fastify/error': 4.2.0 + fastify-plugin: 5.1.0 + secure-json-parse: 4.1.0 + + '@fastify/proxy-addr@5.1.0': + dependencies: + '@fastify/forwarded': 3.0.2 + ipaddr.js: 2.5.0 + + '@fastify/send@4.1.1': + dependencies: + '@lukeed/ms': 2.0.2 + escape-html: 1.0.3 + fast-decode-uri-component: 1.0.1 + http-errors: 2.0.1 + mime: 3.0.0 + + '@fastify/static@8.3.0': + dependencies: + '@fastify/accept-negotiator': 2.1.0 + '@fastify/send': 4.1.1 + content-disposition: 0.5.4 + fastify-plugin: 5.1.0 + fastq: 1.20.1 + glob: 11.1.0 + + '@floating-ui/core@1.8.0': + dependencies: + '@floating-ui/utils': 0.2.12 + + '@floating-ui/dom@1.8.0': + dependencies: + '@floating-ui/core': 1.8.0 + '@floating-ui/utils': 0.2.12 + + '@floating-ui/react-dom@2.1.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@floating-ui/dom': 1.8.0 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@floating-ui/utils@0.2.12': {} + + '@google/genai@1.52.0(supports-color@7.2.0)': + dependencies: + google-auth-library: 10.9.1(supports-color@7.2.0) + p-retry: 4.6.2 + protobufjs: 7.6.5 + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@isaacs/cliui@9.0.0': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@lukeed/ms@2.0.2': {} + + '@mariozechner/clipboard-darwin-arm64@0.3.9': + optional: true + + '@mariozechner/clipboard-darwin-universal@0.3.9': + optional: true + + '@mariozechner/clipboard-darwin-x64@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-arm64-gnu@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-arm64-musl@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-riscv64-gnu@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-x64-gnu@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-x64-musl@0.3.9': + optional: true + + '@mariozechner/clipboard-win32-arm64-msvc@0.3.9': + optional: true + + '@mariozechner/clipboard-win32-x64-msvc@0.3.9': + optional: true + + '@mariozechner/clipboard@0.3.9': + optionalDependencies: + '@mariozechner/clipboard-darwin-arm64': 0.3.9 + '@mariozechner/clipboard-darwin-universal': 0.3.9 + '@mariozechner/clipboard-darwin-x64': 0.3.9 + '@mariozechner/clipboard-linux-arm64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-arm64-musl': 0.3.9 + '@mariozechner/clipboard-linux-riscv64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-x64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-x64-musl': 0.3.9 + '@mariozechner/clipboard-win32-arm64-msvc': 0.3.9 + '@mariozechner/clipboard-win32-x64-msvc': 0.3.9 + optional: true + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@pinojs/redact@0.4.0': {} + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.5': {} + + '@protobufjs/eventemitter@1.1.1': {} + + '@protobufjs/fetch@1.1.1': + dependencies: + '@protobufjs/aspromise': 1.1.2 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.2': {} + + '@radix-ui/number@1.1.3': {} + + '@radix-ui/primitive@1.1.7': {} + + '@radix-ui/react-accessible-icon@1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-accordion@1.2.20(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collapsible': 1.1.20(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-alert-dialog@1.1.23(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-dialog': 1.1.23(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-arrow@1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-aspect-ratio@1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-avatar@1.2.6(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-checkbox@1.3.11(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-collapsible@1.1.20(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-collection@1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-compose-refs@1.1.5(@types/react@19.2.18)(react@19.2.8)': + dependencies: + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.18 + + '@radix-ui/react-context-menu@2.3.7(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-menu': 2.1.24(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-context@1.2.2(@types/react@19.2.18)(react@19.2.8)': + dependencies: + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.18 + + '@radix-ui/react-dialog@1.1.23(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + aria-hidden: 1.2.6 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-remove-scroll: 2.7.2(@types/react@19.2.18)(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-direction@1.1.4(@types/react@19.2.18)(react@19.2.8)': + dependencies: + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.18 + + '@radix-ui/react-dismissable-layer@1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-dropdown-menu@2.1.24(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-menu': 2.1.24(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-focus-guards@1.1.6(@types/react@19.2.18)(react@19.2.8)': + dependencies: + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.18 + + '@radix-ui/react-focus-scope@1.1.16(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-form@0.1.16(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-label': 2.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-hover-card@1.1.23(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-id@1.1.4(@types/react@19.2.18)(react@19.2.8)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.18 + + '@radix-ui/react-label@2.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-menu@2.1.24(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + aria-hidden: 1.2.6 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-remove-scroll: 2.7.2(@types/react@19.2.18)(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-menubar@1.1.24(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-menu': 2.1.24(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-navigation-menu@1.2.22(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-previous': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-one-time-password-field@0.1.16(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/number': 1.1.3 + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-password-toggle-field@0.1.11(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-popover@1.1.23(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + aria-hidden: 1.2.6 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-remove-scroll: 2.7.2(@types/react@19.2.18)(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-popper@1.3.7(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@floating-ui/react-dom': 2.1.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-arrow': 1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-rect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/rect': 1.1.3 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-portal@1.1.17(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-presence@1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-primitive@2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-progress@1.1.16(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-radio-group@1.4.7(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-roving-focus@1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-scroll-area@1.2.18(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/number': 1.1.3 + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-select@2.3.7(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/number': 1.1.3 + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-previous': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + aria-hidden: 1.2.6 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-remove-scroll: 2.7.2(@types/react@19.2.18)(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-separator@1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-slider@1.4.7(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/number': 1.1.3 + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-previous': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-slot@1.3.3(@types/react@19.2.18)(react@19.2.8)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.18 + + '@radix-ui/react-switch@1.3.7(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-tabs@1.1.21(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-toast@1.2.23(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-toggle-group@1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-toggle': 1.1.18(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-toggle@1.1.18(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-toolbar@1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-separator': 1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-toggle-group': 1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-tooltip@1.2.16(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-use-callback-ref@1.1.4(@types/react@19.2.18)(react@19.2.8)': + dependencies: + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.18 + + '@radix-ui/react-use-controllable-state@1.2.6(@types/react@19.2.18)(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.18 + + '@radix-ui/react-use-effect-event@0.0.5(@types/react@19.2.18)(react@19.2.8)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.18 + + '@radix-ui/react-use-escape-keydown@1.1.5(@types/react@19.2.18)(react@19.2.8)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.18 + + '@radix-ui/react-use-is-hydrated@0.1.3(@types/react@19.2.18)(react@19.2.8)': + dependencies: + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.18 + + '@radix-ui/react-use-layout-effect@1.1.4(@types/react@19.2.18)(react@19.2.8)': + dependencies: + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.18 + + '@radix-ui/react-use-previous@1.1.4(@types/react@19.2.18)(react@19.2.8)': + dependencies: + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.18 + + '@radix-ui/react-use-rect@1.1.4(@types/react@19.2.18)(react@19.2.8)': + dependencies: + '@radix-ui/rect': 1.1.3 + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.18 + + '@radix-ui/react-use-size@1.1.4(@types/react@19.2.18)(react@19.2.8)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.18 + + '@radix-ui/react-visually-hidden@1.2.11(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/rect@1.1.3': {} + + '@rolldown/pluginutils@1.0.0-rc.3': {} + + '@rollup/rollup-android-arm-eabi@4.63.0': + optional: true + + '@rollup/rollup-android-arm64@4.63.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.63.0': + optional: true + + '@rollup/rollup-darwin-x64@4.63.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.63.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.63.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.63.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.63.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.63.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.63.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.63.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.63.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.63.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.63.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.63.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.63.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.63.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.63.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.63.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.63.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.63.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.63.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.63.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.63.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.63.0': + optional: true + + '@silvia-odwyer/photon-node@0.3.4': {} + + '@smithy/core@3.33.3': + dependencies: + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.5.2': + dependencies: + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.7.2': + dependencies: + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/node-http-handler@4.11.3': + dependencies: + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.7.3': + dependencies: + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/signature-v4@5.7.3': + dependencies: + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/types@4.17.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + + '@standard-schema/spec@1.1.0': {} + + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.10.0(@testing-library/dom@10.4.1)': + dependencies: + '@adobe/css-tools': 4.5.0 + '@testing-library/dom': 10.4.1 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@babel/runtime': 7.29.7 + '@testing-library/dom': 10.4.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@testing-library/user-event@14.6.6(@testing-library/dom@10.4.1)': + dependencies: + '@testing-library/dom': 10.4.1 + + '@types/aria-query@5.0.4': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.8 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.8 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@24.13.3': + dependencies: + undici-types: 7.18.2 + + '@types/react-dom@19.2.5(@types/react@19.2.18)': + dependencies: + '@types/react': 19.2.18 + + '@types/react@19.2.18': + dependencies: + csstype: 3.2.3 + + '@types/retry@0.12.0': {} + + '@typescript-eslint/eslint-plugin@8.68.0(@typescript-eslint/parser@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.68.0 + '@typescript-eslint/type-utils': 8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.68.0 + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + ignore: 7.0.6 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.68.0 + '@typescript-eslint/types': 8.68.0 + '@typescript-eslint/typescript-estree': 8.68.0(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.68.0 + debug: 4.4.3(supports-color@7.2.0) + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.68.0(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.68.0(typescript@5.9.3) + '@typescript-eslint/types': 8.68.0 + debug: 4.4.3(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.68.0': + dependencies: + '@typescript-eslint/types': 8.68.0 + '@typescript-eslint/visitor-keys': 8.68.0 + + '@typescript-eslint/tsconfig-utils@8.68.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.68.0 + '@typescript-eslint/typescript-estree': 8.68.0(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + debug: 4.4.3(supports-color@7.2.0) + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.68.0': {} + + '@typescript-eslint/typescript-estree@8.68.0(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.68.0(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.68.0(typescript@5.9.3) + '@typescript-eslint/types': 8.68.0 + '@typescript-eslint/visitor-keys': 8.68.0 + debug: 4.4.3(supports-color@7.2.0) + minimatch: 10.2.6 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)) + '@typescript-eslint/scope-manager': 8.68.0 + '@typescript-eslint/types': 8.68.0 + '@typescript-eslint/typescript-estree': 8.68.0(supports-color@7.2.0)(typescript@5.9.3) + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.68.0': + dependencies: + '@typescript-eslint/types': 8.68.0 + eslint-visitor-keys: 5.0.1 + + '@vitejs/plugin-react@5.2.0(supports-color@7.2.0)(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@rolldown/pluginutils': 1.0.0-rc.3 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 + vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@4.1.11': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + chai: 6.2.2 + tinyrainbow: 3.1.1 + + '@vitest/mocker@4.1.11(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))': + dependencies: + '@vitest/spy': 4.1.11 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) + + '@vitest/pretty-format@4.1.11': + dependencies: + tinyrainbow: 3.1.1 + + '@vitest/runner@4.1.11': + dependencies: + '@vitest/utils': 4.1.11 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.11': + dependencies: + '@vitest/pretty-format': 4.1.11 + '@vitest/utils': 4.1.11 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.11': {} + + '@vitest/utils@4.1.11': + dependencies: + '@vitest/pretty-format': 4.1.11 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 + + abstract-logging@2.0.1: {} + + acorn-jsx@5.3.2(acorn@8.18.0): + dependencies: + acorn: 8.18.0 + + acorn@8.18.0: {} + + agent-base@7.1.4: {} + + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.6 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + argparse@2.0.1: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + + assertion-error@2.0.1: {} + + assistant-cloud@0.1.42: + dependencies: + assistant-stream: 0.3.40 + transitivePeerDependencies: + - ioredis + - redis + + assistant-stream@0.3.40: + dependencies: + '@standard-schema/spec': 1.1.0 + nanoid: 6.0.1 + secure-json-parse: 4.1.0 + + atomic-sleep@1.0.0: {} + + avvio@9.3.0: + dependencies: + '@fastify/error': 4.2.0 + fastq: 1.20.1 + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.11.19: {} + + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + + bignumber.js@9.3.1: {} + + bowser@2.14.1: {} + + brace-expansion@1.1.18: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@5.0.9: + dependencies: + balanced-match: 4.0.4 + + browserslist@4.28.8: + dependencies: + baseline-browser-mapping: 2.11.19 + caniuse-lite: 1.0.30001810 + electron-to-chromium: 1.5.415 + node-releases: 2.0.53 + update-browserslist-db: 1.3.1(browserslist@4.28.8) + + buffer-equal-constant-time@1.0.1: {} + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001810: {} + + chai@6.2.2: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + convert-source-map@2.0.0: {} + + cookie@1.1.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + css.escape@1.5.1: {} + + cssstyle@5.3.7: + dependencies: + '@asamuzakjp/css-color': 4.1.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.9(css-tree@3.2.1) + css-tree: 3.2.1 + lru-cache: 11.5.2 + + csstype@3.2.3: {} + + data-uri-to-buffer@4.0.1: {} + + data-urls@6.0.1: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 15.1.0 + + debug@4.4.3(supports-color@7.2.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 7.2.0 + + decimal.js@10.6.0: {} + + deep-is@0.1.4: {} + + depd@2.0.0: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + detect-node-es@1.1.0: {} + + diff@8.0.4: {} + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + electron-to-chromium@1.5.415: {} + + entities@8.0.0: {} + + es-module-lexer@2.3.2: {} + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-react-hooks@7.1.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0): + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/parser': 7.29.8 + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + hermes-parser: 0.25.1 + zod: 4.4.3 + zod-validation-error: 4.0.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-refresh@0.4.26(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)): + dependencies: + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0): + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2(supports-color@7.2.0) + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.6(supports-color@7.2.0) + '@eslint/js': 9.39.5 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@7.2.0) + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + esutils@2.0.3: {} + + expect-type@1.4.0: {} + + extend@3.0.2: {} + + fast-decode-uri-component@1.0.1: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-json-stringify@7.0.1: + dependencies: + '@fastify/merge-json-schemas': 0.2.1 + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + fast-uri: 4.1.3 + json-schema-ref-resolver: 3.0.0 + rfdc: 1.4.1 + + fast-levenshtein@2.0.6: {} + + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + + fast-uri@3.1.6: {} + + fast-uri@4.1.3: {} + + fastify-plugin@5.1.0: {} + + fastify@5.12.1: + dependencies: + '@fastify/ajv-compiler': 4.0.6 + '@fastify/error': 4.2.0 + '@fastify/fast-json-stringify-compiler': 5.1.0 + '@fastify/proxy-addr': 5.1.0 + abstract-logging: 2.0.1 + avvio: 9.3.0 + fast-json-stringify: 7.0.1 + find-my-way: 9.9.0 + light-my-request: 6.6.0 + pino: 10.3.1 + process-warning: 5.1.0 + rfdc: 1.4.1 + secure-json-parse: 4.1.0 + semver: 7.8.5 + toad-cache: 3.7.4 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-my-way@9.9.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 5.1.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.4 + keyv: 4.5.4 + + flatted@3.4.4: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + fsevents@2.3.3: + optional: true + + gaxios@7.3.1(supports-color@7.2.0): + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6(supports-color@7.2.0) + node-fetch: 3.3.2 + transitivePeerDependencies: + - supports-color + + gcp-metadata@8.1.2(supports-color@7.2.0): + dependencies: + gaxios: 7.3.1(supports-color@7.2.0) + google-logging-utils: 1.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + + gensync@1.0.0-beta.2: {} + + get-east-asian-width@1.6.0: {} + + get-nonce@1.0.1: {} + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@11.1.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.2.3 + minimatch: 10.2.6 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.2 + + globals@14.0.0: {} + + globals@16.5.0: {} + + google-auth-library@10.9.1(supports-color@7.2.0): + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.3.1(supports-color@7.2.0) + gcp-metadata: 8.1.2(supports-color@7.2.0) + google-logging-utils: 1.1.3 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + + google-logging-utils@1.1.3: {} + + graceful-fs@4.2.11: {} + + grok-mermaid@0.2.2: {} + + has-flag@4.0.0: {} + + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + highlight.js@10.7.3: {} + + hosted-git-info@9.0.3: + dependencies: + lru-cache: 11.5.2 + + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.15.1 + transitivePeerDependencies: + - '@noble/hashes' + + 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 + + http-proxy-agent@7.0.2(supports-color@7.2.0): + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6(supports-color@7.2.0): + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + ignore@7.0.6: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + inherits@2.0.4: {} + + ipaddr.js@2.5.0: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-potential-custom-element-name@1.0.1: {} + + isexe@2.0.0: {} + + jackspeak@4.2.3: + dependencies: + '@isaacs/cliui': 9.0.0 + + jiti@2.7.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.3.2: + dependencies: + argparse: 2.0.1 + + jsdom@27.4.0(supports-color@7.2.0): + dependencies: + '@acemir/cssom': 0.9.31 + '@asamuzakjp/dom-selector': 6.8.1 + '@exodus/bytes': 1.15.1 + cssstyle: 5.3.7 + data-urls: 6.0.1 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + http-proxy-agent: 7.0.2(supports-color@7.2.0) + https-proxy-agent: 7.0.6(supports-color@7.2.0) + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + ws: 8.21.3 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + - bufferutil + - supports-color + - utf-8-validate + + jsesc@3.1.0: {} + + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + + json-buffer@3.0.1: {} + + json-schema-ref-resolver@3.0.0: + dependencies: + dequal: 2.0.3 + + json-schema-to-ts@3.1.1: + dependencies: + '@babel/runtime': 7.29.7 + ts-algebra: 2.0.0 + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + 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 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + light-my-request@6.6.0: + dependencies: + cookie: 1.1.1 + process-warning: 4.0.1 + set-cookie-parser: 2.7.2 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + long@5.3.2: {} + + lru-cache@11.5.2: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@0.555.0(react@19.2.8): + dependencies: + react: 19.2.8 + + lz-string@1.5.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + marked@18.0.5: {} + + mdn-data@2.27.1: {} + + mime@3.0.0: {} + + min-indent@1.0.1: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.9 + + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.9 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.18 + + minipass@7.1.3: {} + + ms@2.1.3: {} + + nanoid@3.3.18: {} + + nanoid@6.0.1: {} + + natural-compare@1.4.0: {} + + node-domexception@1.0.0: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + node-releases@2.0.53: {} + + obug@2.1.4: {} + + on-exit-leak-free@2.1.2: {} + + openai@6.40.0(ws@8.21.3)(zod@4.4.3): + optionalDependencies: + ws: 8.21.3 + zod: 4.4.3 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-retry@4.6.2: + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + + package-json-from-dist@1.0.1: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse5@8.0.1: + dependencies: + entities: 8.0.0 + + partial-json@0.1.7: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.2 + minipass: 7.1.3 + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.1.0: {} + + pino@10.3.1: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.1.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 4.2.0 + + postcss@8.5.26: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + process-warning@4.0.1: {} + + process-warning@5.1.0: {} + + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + + protobufjs@7.6.5: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 + '@protobufjs/float': 1.0.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.2 + '@types/node': 24.13.3 + long: 5.3.2 + + punycode@2.3.1: {} + + quick-format-unescaped@4.0.4: {} + + radix-ui@1.6.7(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-accessible-icon': 1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-accordion': 1.2.20(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-alert-dialog': 1.1.23(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-arrow': 1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-aspect-ratio': 1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-avatar': 1.2.6(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-checkbox': 1.3.11(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-collapsible': 1.1.20(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context-menu': 2.3.7(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-dialog': 1.1.23(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-dropdown-menu': 2.1.24(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-form': 0.1.16(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-hover-card': 1.1.23(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-label': 2.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-menu': 2.1.24(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-menubar': 1.1.24(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-navigation-menu': 1.2.22(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-one-time-password-field': 0.1.16(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-password-toggle-field': 0.1.11(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-popover': 1.1.23(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-progress': 1.1.16(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-radio-group': 1.4.7(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-scroll-area': 1.2.18(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-select': 2.3.7(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-separator': 1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-slider': 1.4.7(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-switch': 1.3.7(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-tabs': 1.1.21(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-toast': 1.2.23(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-toggle': 1.1.18(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-toggle-group': 1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-toolbar': 1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-tooltip': 1.2.16(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-escape-keydown': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + react-dom@19.2.8(react@19.2.8): + dependencies: + react: 19.2.8 + scheduler: 0.27.0 + + react-is@17.0.2: {} + + react-refresh@0.18.0: {} + + react-remove-scroll-bar@2.3.8(@types/react@19.2.18)(react@19.2.8): + dependencies: + react: 19.2.8 + react-style-singleton: 2.2.3(@types/react@19.2.18)(react@19.2.8) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.18 + + react-remove-scroll@2.7.2(@types/react@19.2.18)(react@19.2.8): + dependencies: + react: 19.2.8 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.18)(react@19.2.8) + react-style-singleton: 2.2.3(@types/react@19.2.18)(react@19.2.8) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.18)(react@19.2.8) + use-sidecar: 1.1.3(@types/react@19.2.18)(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + + react-style-singleton@2.2.3(@types/react@19.2.18)(react@19.2.8): + dependencies: + get-nonce: 1.0.1 + react: 19.2.8 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.18 + + react-textarea-autosize@8.5.9(@types/react@19.2.18)(react@19.2.8): + dependencies: + '@babel/runtime': 7.29.7 + react: 19.2.8 + use-composed-ref: 1.4.0(@types/react@19.2.18)(react@19.2.8) + use-latest: 1.3.0(@types/react@19.2.18)(react@19.2.8) + transitivePeerDependencies: + - '@types/react' + + react@19.2.8: {} + + real-require@0.2.0: {} + + real-require@1.0.0: {} + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + ret@0.5.0: {} + + retry@0.12.0: {} + + retry@0.13.1: {} + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + + rollup@4.63.0: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.63.0 + '@rollup/rollup-android-arm64': 4.63.0 + '@rollup/rollup-darwin-arm64': 4.63.0 + '@rollup/rollup-darwin-x64': 4.63.0 + '@rollup/rollup-freebsd-arm64': 4.63.0 + '@rollup/rollup-freebsd-x64': 4.63.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.63.0 + '@rollup/rollup-linux-arm-musleabihf': 4.63.0 + '@rollup/rollup-linux-arm64-gnu': 4.63.0 + '@rollup/rollup-linux-arm64-musl': 4.63.0 + '@rollup/rollup-linux-loong64-gnu': 4.63.0 + '@rollup/rollup-linux-loong64-musl': 4.63.0 + '@rollup/rollup-linux-ppc64-gnu': 4.63.0 + '@rollup/rollup-linux-ppc64-musl': 4.63.0 + '@rollup/rollup-linux-riscv64-gnu': 4.63.0 + '@rollup/rollup-linux-riscv64-musl': 4.63.0 + '@rollup/rollup-linux-s390x-gnu': 4.63.0 + '@rollup/rollup-linux-x64-gnu': 4.63.0 + '@rollup/rollup-linux-x64-musl': 4.63.0 + '@rollup/rollup-openbsd-x64': 4.63.0 + '@rollup/rollup-openharmony-arm64': 4.63.0 + '@rollup/rollup-win32-arm64-msvc': 4.63.0 + '@rollup/rollup-win32-ia32-msvc': 4.63.0 + '@rollup/rollup-win32-x64-gnu': 4.63.0 + '@rollup/rollup-win32-x64-msvc': 4.63.0 + fsevents: 2.3.3 + + safe-buffer@5.2.1: {} + + safe-content-frame@0.0.28: {} + + safe-regex2@5.1.1: + dependencies: + ret: 0.5.0 + + safe-stable-stringify@2.5.0: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.27.0: {} + + secure-json-parse@4.1.0: {} + + semver@6.3.1: {} + + semver@7.8.0: {} + + semver@7.8.5: {} + + set-cookie-parser@2.7.2: {} + + setprototypeof@1.2.0: {} + + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + + source-map-js@1.2.1: {} + + split2@4.2.0: {} + + stackback@0.0.2: {} + + statuses@2.0.2: {} + + std-env@4.2.0: {} + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + symbol-tree@3.2.4: {} + + thread-stream@4.2.0: + dependencies: + real-require: 1.0.0 + + tinybench@2.9.0: {} + + tinyexec@1.3.0: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinyrainbow@3.1.1: {} + + tldts-core@7.4.11: {} + + tldts@7.4.11: + dependencies: + tldts-core: 7.4.11 + + toad-cache@3.7.4: {} + + toidentifier@1.0.1: {} + + tough-cookie@6.0.2: + dependencies: + tldts: 7.4.11 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + + ts-algebra@2.0.0: {} + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + tslib@2.8.1: {} + + tsx@4.23.12: + dependencies: + esbuild: 0.28.2 + optionalDependencies: + fsevents: 2.3.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typebox@1.3.7: {} + + typescript-eslint@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.68.0(@typescript-eslint/parser@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.68.0(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + undici-types@7.18.2: {} + + undici@8.9.0: {} + + update-browserslist-db@1.3.1(browserslist@4.28.8): + dependencies: + browserslist: 4.28.8 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-callback-ref@1.3.3(@types/react@19.2.18)(react@19.2.8): + dependencies: + react: 19.2.8 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.18 + + use-composed-ref@1.4.0(@types/react@19.2.18)(react@19.2.8): + dependencies: + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.18 + + use-isomorphic-layout-effect@1.2.1(@types/react@19.2.18)(react@19.2.8): + dependencies: + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.18 + + use-latest@1.3.0(@types/react@19.2.18)(react@19.2.8): + dependencies: + react: 19.2.8 + use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.18)(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + + use-sidecar@1.1.3(@types/react@19.2.18)(react@19.2.8): + dependencies: + detect-node-es: 1.1.0 + react: 19.2.8 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.18 + + vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0): + dependencies: + esbuild: 0.28.2 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + postcss: 8.5.26 + rollup: 4.63.0 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.13.3 + fsevents: 2.3.3 + jiti: 2.7.0 + tsx: 4.23.12 + yaml: 2.9.0 + + vitest@4.1.11(@types/node@24.13.3)(jsdom@27.4.0(supports-color@7.2.0))(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)): + dependencies: + '@vitest/expect': 4.1.11 + '@vitest/mocker': 4.1.11(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.11 + '@vitest/runner': 4.1.11 + '@vitest/snapshot': 4.1.11 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.3.0 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 + vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.13.3 + jsdom: 27.4.0(supports-color@7.2.0) + transitivePeerDependencies: + - msw + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + web-streams-polyfill@3.3.3: {} + + webidl-conversions@8.0.1: {} + + whatwg-mimetype@4.0.0: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@15.1.0: + dependencies: + tr46: 6.0.0 + webidl-conversions: 8.0.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + ws@8.21.3: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + yallist@3.1.1: {} + + yaml@2.9.0: {} + + yocto-queue@0.1.0: {} + + zod-validation-error@4.0.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + + zod@4.4.3: {} + + zustand@5.0.15(@types/react@19.2.18)(react@19.2.8): + optionalDependencies: + '@types/react': 19.2.18 + react: 19.2.8 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..4a7f387 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,16 @@ +packages: + - apps/* + - packages/* +allowBuilds: + '@google/genai': true + esbuild: true + protobufjs: true + sharp: true +minimumReleaseAgeExclude: + - '@assistant-ui/core@0.3.16' + - '@assistant-ui/react@0.15.17' + - '@assistant-ui/store@0.3.11' + - '@assistant-ui/tap@0.9.15' + - assistant-cloud@0.1.42 + - assistant-stream@0.3.40 + - safe-content-frame@0.0.28 diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..e573007 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2023", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "useUnknownInCatchVariables": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "declaration": true, + "sourceMap": true + } +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..678d8bf --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environmentMatchGlobs: [["apps/web/**", "jsdom"]], + setupFiles: ["./vitest.setup.ts"], + coverage: { reporter: ["text", "html"] }, + }, +}); diff --git a/vitest.setup.ts b/vitest.setup.ts new file mode 100644 index 0000000..f73449e --- /dev/null +++ b/vitest.setup.ts @@ -0,0 +1,11 @@ +import "@testing-library/jest-dom/vitest"; + +if (typeof window !== "undefined") { + class TestResizeObserver implements ResizeObserver { + observe(): void {} + unobserve(): void {} + disconnect(): void {} + } + globalThis.ResizeObserver = TestResizeObserver; + if (!HTMLElement.prototype.scrollTo) HTMLElement.prototype.scrollTo = () => undefined; +}