-[English](../../README.md) | [中文](../../README.md#中文版) | [Français](README_FR.md) | [Русский](README_RU.md) | [हिन्दी](README_HI.md) | [العربية](README_AR.md) | **Português**
+[English](../../README.md) | [中文](README_ZH.md) | [Français](README_FR.md) | [Русский](README_RU.md) | [हिन्दी](README_HI.md) | [العربية](README_AR.md) | **Português**
# 🚀 Claude Code Python
diff --git a/docs/i18n/README_RU.md b/docs/i18n/README_RU.md
index 5eb3dd632..4b1fdad29 100644
--- a/docs/i18n/README_RU.md
+++ b/docs/i18n/README_RU.md
@@ -1,6 +1,6 @@
-[English](../../README.md) | [中文](../../README.md#中文版) | [Français](README_FR.md) | **Русский** | [हिन्दी](README_HI.md) | [العربية](README_AR.md) | [Português](README_PT.md)
+[English](../../README.md) | [中文](README_ZH.md) | [Français](README_FR.md) | **Русский** | [हिन्दी](README_HI.md) | [العربية](README_AR.md) | [Português](README_PT.md)
# 🚀 Claude Code Python
diff --git a/docs/i18n/README_ZH.md b/docs/i18n/README_ZH.md
new file mode 100644
index 000000000..4c8058ee3
--- /dev/null
+++ b/docs/i18n/README_ZH.md
@@ -0,0 +1,442 @@
+
+
+[English](../../README.md) | **中文** | [Français](README_FR.md) | [Русский](README_RU.md) | [हिन्दी](README_HI.md) | [العربية](README_AR.md) | [Português](README_PT.md)
+
+# ClawCodex
+
+**面向真实使用的 Claude Code Python 重构版 — 真实架构、可运行的 CLI Agent**
+
+*从 TypeScript 参考实现移植,并在 Python 侧扩展了完整的运行时能力*
+
+***
+
+[](https://github.com/agentforce314/clawcodex/stargazers)
+[](https://github.com/agentforce314/clawcodex/network/members)
+[](https://opensource.org/licenses/MIT)
+[](https://www.python.org/downloads/)
+
+**🔥 活跃开发中 • 每周更新新功能 🔥**
+
+## FLEXIBLE SKILL SYSTEMS
+
+**基于 Markdown 的斜杠技能系统,支持参数替换、工具限制,以及项目级 / 用户级技能加载。**
+
+
+
+***
+
+## 🎯 为什么是 ClawCodex?
+
+**ClawCodex** 是一个面向真实使用的 **Claude Code Python 重构版**:它基于**真实 TypeScript 架构**移植而来,并且交付的是一个**可运行的 CLI Agent**,而不只是源码镜像。
+
+- **真实 Agent Runtime** — 具备工具调用循环、流式 REPL、会话历史与多轮执行能力
+- **高保真移植** — 尽可能保留 Claude Code 的原始架构,同时做符合 Python 风格的实现
+- **适合继续开发** — 代码可读、测试完善,并支持基于 Markdown 的技能扩展
+- **多 LLM 提供商** — 相对上游最大的进展之一:Claude Code 主要面向 **Claude 系列模型**,而 ClawCodex 致力于接入**各大主流 LLM 提供商**,在保持 Agent 能力的前提下,为用户提供更**灵活**、更**具性价比**的 agentic 编程体验
+
+**这是一个真正可跑的 Claude Code 风格 Python 终端工作流:能流式回答、调工具、抓外部上下文,并通过 skills 扩展行为。**
+
+**🚀 立即试用!Fork 它、修改它、让它成为你的!欢迎提交 Pull Request!**
+
+***
+
+## ⭐ Star 历史
+
+[在 star-history.com 查看 Star 历史图表](https://www.star-history.com/?repos=agentforce314%2Fclawcodex&type=date&legend=top-left)
+
+## ✨ 特性
+
+### Streaming Agent Experience
+
+```text
+>>> /stream on
+>>> 解释 tests/test_agent_loop.py
+[流式回答中...]
+• Read (tests/test_agent_loop.py) running...
+ ↳ lines 1-180
+>>> /render-last
+```
+
+- 直接回答支持真实 API 流式输出,带工具的 agent loop 也具备更完整的流式体验
+- 内置 `/stream` 开关用于实时输出,`/render-last` 可按需把上一条回答重新渲染为 Markdown
+- 专门为终端演示优化:一边看回答流出,一边看到工具调用,并保留稳定回退路径
+
+### 可编程 Skill Runtime
+
+```md
+---
+description: 用类比 + 图示解释代码
+allowed-tools:
+ - Read
+ - Grep
+ - Glob
+arguments: [path]
+---
+
+请解释 $path 的实现:先给一个类比,再画一个结构示意图。
+```
+
+- 基于 `SKILL.md` 的 Markdown 斜杠命令
+- 支持项目级技能、用户级技能、命名参数替换与工具限制
+
+### 多提供商支持
+
+ClawCodex 的核心优势之一是**多模型 / 多提供商**:Claude Code 以 **Claude** 为主线,我们则希望在同一套 Agent 运行时上覆盖**各大主流 LLM 提供商**,便于按场景切换厂商、区域与价位,而不牺牲工具、技能与编码闭环——这也是让 agentic 编程在成本与灵活性上真正可用的基础。
+
+```python
+providers = ["anthropic", "openai", "glm", "minimax"] # 可继续扩展
+```
+
+### 交互式 REPL(默认)与 Textual TUI(可选)
+
+**默认**为 **prompt_toolkit + Rich** 行内 REPL(滚动区 + 状态行)。使用 **`clawcodex --tui`** 或 REPL 内的 **`/tui`** 可进入 **Textual** 全屏界面。
+
+```text
+>>> 你好!
+Assistant: 嗨!我是 ClawCodex,一个 Python 重实现...
+
+>>> /help # 显示命令
+>>> /tools # 列出已注册工具
+>>> /tui # 切换到 Textual TUI
+>>> /stream on # 流式渲染开关
+>>> /save # 保存会话
+>>> Tab # 自动补全
+>>> /explain-code qsort.py # 运行 SKILL.md 技能(或 /skill …)
+
+# 多行输入:Shift+Enter、Meta/Alt+Enter,或 `\` 后 Enter;单独 Enter 提交。
+```
+
+### 完整的 CLI
+
+```bash
+clawcodex # 行内 REPL(默认)
+clawcodex --tui # Textual TUI
+clawcodex --stream # 开启实时渲染的 REPL
+clawcodex login # 交互式配置 API
+clawcodex config # 查看配置
+clawcodex --version # 版本信息
+
+# 非交互 / 脚本(管道、CI、自动化)
+clawcodex -p "用中文总结 src/cli.py"
+clawcodex -p "Hello" --output-format json
+
+clawcodex --provider anthropic --model claude-sonnet-4-6 -p "Hi"
+clawcodex --max-turns 10 --allowed-tools Read,Grep -p "查找 TODO"
+```
+
+***
+
+## 📊 状态
+
+| 组件 | 状态 | 数量 |
+| ----- | ------ | ------ |
+| REPL 命令 | ✅ 完成 | 内置命令 + `/tools`、`/stream`、`/context`、`/compact`、技能等 |
+| 工具系统 | ✅ 完成 | 30+ 工具 |
+| 自动化测试 | ✅ 已覆盖 | 工具、agent loop、providers、parity、REPL 等 |
+| 文档 | ✅ 完成 | 指南、多语言 README、[FEATURE_LIST.md](../../FEATURE_LIST.md) |
+
+### 核心系统
+
+| 系统 | 状态 | 描述 |
+|------|------|------|
+| CLI 入口 | ✅ | `clawcodex`、`login`、`config`、`-p`、`--tui`、`--stream`、`--version` |
+| 交互式 REPL | ✅ | 默认行内 REPL;可选 Textual;历史、Tab、多行输入 |
+| 多提供商支持 | ✅ | Anthropic、OpenAI、智谱 GLM、Minimax |
+| 会话持久化 | ✅ | 本地保存/加载会话 |
+| Agent Loop | ✅ | 工具调用循环;支持流式与无头模式 |
+| Skill 系统 | ✅ | SKILL.md 斜杠技能:参数与工具白名单 |
+| 上下文构建 | 🟡 | workspace / git / `CLAUDE.md` 注入;更丰富的摘要与 memory 仍在演进 |
+| 权限系统 | 🟡 | 框架与检查逻辑已有;全面集成进行中 |
+| MCP | 🟡 | MCP 相关工具与接线已有;协议层/运行时仍在完善 |
+
+### 工具系统(已实现 30+ 工具)
+
+| 类别 | 工具 | 状态 |
+|------|------|------|
+| 文件操作 | Read, Write, Edit, Glob, Grep | ✅ 完成 |
+| 系统 | Bash 执行 | ✅ 完成 |
+| 网络 | WebFetch, WebSearch | ✅ 完成 |
+| 交互 | AskUserQuestion, SendMessage | ✅ 完成 |
+| 任务管理 | TodoWrite, TaskManager, TaskStop | ✅ 完成 |
+| Agent 工具 | Agent, Brief, Team | ✅ 完成 |
+| 配置 | Config, PlanMode, Cron | ✅ 完成 |
+| MCP | MCP 工具与资源 | 🟡 工具已接线;完整 client/runtime 仍在演进 |
+| 其他 | LSP, Worktree, Skill(SKILL.md), ToolSearch | ✅ 完成 |
+
+### 路线图进度
+
+- ✅ **阶段 0**:可安装、可运行的 CLI
+- ✅ **阶段 1**:Claude Code 核心 MVP 体验
+- ✅ **阶段 2**:真实工具调用闭环
+- 🟡 **阶段 3**:上下文深度、权限集成、类 `/resume` 的恢复能力(进行中)
+- 🟡 **阶段 4**:MCP 运行时、插件与扩展(工具已有,平台能力持续推进)
+- ⏳ **阶段 5**:Python 原生差异化特性
+
+**详细功能状态和 PR 指南请查看 [FEATURE_LIST.md](../../FEATURE_LIST.md)。**
+
+## 🚀 快速开始
+
+### 安装
+
+```bash
+git clone https://github.com/agentforce314/clawcodex.git
+cd clawcodex
+
+# 创建虚拟环境(推荐使用 uv)
+uv venv --python 3.11
+source .venv/bin/activate
+
+# 安装包与 console 入口(推荐)
+uv pip install -e ".[dev]"
+
+# 或:先装依赖再 editable
+# uv pip install -r requirements.txt && uv pip install -e .
+```
+
+### 配置
+
+#### 方式 1:交互式(推荐)
+
+```bash
+clawcodex login
+# 或: python -m src.cli login
+```
+
+这个流程会:
+
+1. 让你选择 provider:anthropic / openai / glm / minimax
+2. 让你输入该 provider 的 API key
+3. 可选:保存自定义 base URL
+4. 可选:保存默认 model
+5. 将该 provider 设为默认
+
+配置文件会保存在 `~/.clawcodex/config.json`。示例结构:
+
+```json
+{
+ "default_provider": "openai",
+ "providers": {
+ "anthropic": {
+ "api_key": "base64-encoded-key",
+ "base_url": "https://api.anthropic.com",
+ "default_model": "claude-sonnet-4-6"
+ },
+ "openai": {
+ "api_key": "base64-encoded-key",
+ "base_url": "https://api.openai.com/v1",
+ "default_model": "gpt-5.4"
+ },
+ "glm": {
+ "api_key": "base64-encoded-key",
+ "base_url": "https://open.bigmodel.cn/api/paas/v4",
+ "default_model": "zai/glm-5"
+ },
+ "minimax": {
+ "api_key": "base64-encoded-key",
+ "base_url": "https://api.minimaxi.com/anthropic",
+ "default_model": "MiniMax-M2.7"
+ }
+ }
+}
+```
+
+### 运行
+
+```bash
+clawcodex # 启动行内 REPL(等同于 python -m src.cli)
+clawcodex --help # 含 --tui、-p、--provider、--model 等
+```
+
+**就这样!** 配置密钥后即可使用 CLI 或 REPL。
+
+***
+
+## 💡 使用
+
+### REPL 命令
+
+| 命令 | 描述 |
+| --- | --- |
+| `/` | 显示命令与技能 |
+| `/help` | 帮助 |
+| `/tools` | 列出已注册工具名 |
+| `/tool
` | 以 JSON 输入直接调用工具 |
+| `/stream` | 流式渲染:`/stream on`、`off` 或 `toggle` |
+| `/render-last` | 将上一条助手回复重新渲染为 Markdown |
+| `/save`、`/load ` | 保存或加载会话 |
+| `/clear` | 清空对话(亦支持 `/reset`、`/new`) |
+| `/tui` | 进入 Textual TUI |
+| `/skill` | 技能启动流程 |
+| `/context` | 工作区 / 提示上下文(若可用) |
+| `/compact` | 压缩或清空对话(不可用时回退为清空) |
+| `/exit`、`/quit`、`/q` | 退出 |
+
+### Skills(技能 / 斜杠命令)教程
+
+技能是存放在 `.clawcodex/skills` 下的 Markdown 斜杠命令。每个技能对应一个目录,并且文件名固定为 `SKILL.md`。
+
+**1)创建项目技能**
+
+创建:
+
+```text
+/.clawcodex/skills//SKILL.md
+```
+
+示例:
+
+```md
+---
+description: 用类比 + 图示解释代码
+when_to_use: 当用户问“这段代码怎么工作?”时使用
+allowed-tools:
+ - Read
+ - Grep
+ - Glob
+arguments: [path]
+---
+
+请解释 $path 的实现:先给一个类比,再画一个结构示意图。
+```
+
+**2)在 REPL 中使用**
+
+```text
+❯ /
+❯ /
+```
+
+示例:
+
+```text
+❯ /explain-code qsort.py
+```
+
+**补充说明**
+
+- 用户级技能:`~/.clawcodex/skills//SKILL.md`
+- 工具限制:`allowed-tools` 用于限制技能允许调用的工具集合
+- 参数替换:支持 `$ARGUMENTS`、`$0`、`$1`、以及命名参数(例如 `$path`,来自 `arguments`)
+- 占位符写法:请使用 `$path`,不要写成 `${path}`
+
+***
+
+## 🎓 为什么选择 ClawCodex?
+
+### 基于真实源码
+
+- **不是克隆** — 从真实的 TypeScript 实现移植而来
+- **架构保真** — 保持经过验证的设计模式
+- **持续改进** — 更好的错误处理、更多测试、更清晰的代码
+
+### 原生 Python
+
+- **类型提示** — 完整的类型注解
+- **现代 Python** — 使用 3.10+ 特性
+- **符合习惯** — 干净的 Python 风格代码
+
+### 以用户为中心
+
+- **3 步设置** — 克隆、`clawcodex login` 配置、`clawcodex` 运行
+- **交互式配置** — 选择 provider、Base URL、默认模型
+- **行内或 TUI** — 默认终端原生 REPL;可选 Textual
+- **可脚本化** — `-p`、JSON、NDJSON 便于自动化
+- **会话持久化** — 保存与恢复对话
+
+***
+
+## 📦 项目结构
+
+```text
+clawcodex/
+├── src/
+│ ├── cli.py # CLI 入口(控制台命令 clawcodex)
+│ ├── entrypoints/ # 无头 (-p) 与 TUI 启动
+│ ├── repl/ # 行内 REPL
+│ ├── tui/ # Textual UI(--tui、/tui)
+│ ├── providers/ # Anthropic、OpenAI、GLM、Minimax
+│ ├── agent/ # 会话、对话、提示
+│ ├── tool_system/ # Agent loop、工具与 schema
+│ ├── skills/ # SKILL.md 与 Skill 工具
+│ ├── services/ # MCP、compact、IDE、工具执行等
+│ ├── context_system/ # 工作区 / git / CLAUDE.md
+│ ├── permissions/ # 权限模式与 bash 解析
+│ ├── hooks/ # Hook 类型与执行
+│ └── command_system/ # 斜杠命令与参数替换
+├── typescript/ # 参考 / 对等源码(运行 Python CLI 非必需)
+├── tests/ # pytest
+├── docs/ # 指南、多语言 README、重构笔记
+├── .clawcodex/skills/ # 项目级技能(可选)
+├── FEATURE_LIST.md # 能力矩阵与路线图
+└── pyproject.toml # 包元数据与 clawcodex 入口
+```
+
+***
+
+## 🤝 贡献
+
+**我们欢迎贡献!**
+
+```bash
+# 快速开发设置
+pip install -e .[dev]
+python -m pytest tests/ -v
+```
+
+查看 [CONTRIBUTING.md](../../CONTRIBUTING.md) 了解指南。
+
+***
+
+## 📖 文档
+
+- **[SETUP_GUIDE.md](../guide/SETUP_GUIDE.md)** — 详细安装说明
+- **[CONTRIBUTING.md](../../CONTRIBUTING.md)** — 开发指南
+- **[TESTING.md](../guide/TESTING.md)** — 测试指南
+- **[CHANGELOG.md](../../CHANGELOG.md)** — 版本历史
+
+***
+
+## ⚡ 性能
+
+- **启动时间**:< 1 秒
+- **内存占用**:< 50MB
+- **响应**:回合式输出,支持 Rich Markdown 渲染
+
+***
+
+## 🔒 安全
+
+✅ **基础本地安全实践**
+
+- Git 中无敏感数据
+- API 密钥在配置中做了基础混淆
+- `.env` 文件被忽略
+- 适合本地开发工作流
+
+***
+
+## 📄 许可证
+
+MIT 许可证 — 查看 [LICENSE](../../LICENSE)
+
+***
+
+## 🙏 致谢
+
+- 基于 Claude Code TypeScript 源码
+- 独立的教育项目
+- 未隶属于 Anthropic
+
+***
+
+
+
+### 🌟 支持我们
+
+如果你觉得这个项目有用,请给个 **star** ⭐!
+
+**用 ❤️ 制作 by ClawCodex 团队**
+
+[⬆ 回到顶部](#clawcodex)
+
+
From da4c9919ac63a0f7975b89cb7ed3cb50ffc1746e Mon Sep 17 00:00:00 2001
From: agentforce314
Date: Tue, 21 Apr 2026 20:20:48 -0700
Subject: [PATCH 002/251] Wire --dangerously-skip-permissions through all
entrypoints
Closes the gap where the flag was parsed but only consumed by the
headless (-p) path. Adds the resolver helper, the root/sudo safety
gate from the TS reference, and the companion --allow-dangerously-skip-
permissions / --permission-mode flags so REPL, TUI, and headless all
honor the same resolved PermissionMode.
Co-Authored-By: Claude Opus 4.6
---
README.md | 10 +
src/cli.py | 130 ++++++-
src/entrypoints/headless.py | 38 ++-
src/entrypoints/tui.py | 27 +-
src/permissions/dangerous_safety.py | 81 +++++
src/permissions/modes.py | 67 ++++
src/repl/core.py | 41 ++-
tests/test_dangerous_skip_permissions.py | 412 +++++++++++++++++++++++
8 files changed, 784 insertions(+), 22 deletions(-)
create mode 100644 src/permissions/dangerous_safety.py
create mode 100644 tests/test_dangerous_skip_permissions.py
diff --git a/README.md b/README.md
index 3d10142c4..ec9fef7e7 100644
--- a/README.md
+++ b/README.md
@@ -121,8 +121,18 @@ clawcodex -p --output-format stream-json --input-format stream-json < events.ndj
# Overrides for a single run
clawcodex --provider anthropic --model claude-sonnet-4-6 -p "Hi"
clawcodex --max-turns 10 --allowed-tools Read,Grep -p "Find TODOs"
+
+# Permission control (REPL, TUI, and -p all honor these)
+clawcodex --permission-mode plan # plan / acceptEdits / dontAsk
+clawcodex --dangerously-skip-permissions -p "ls" # bypass all permission checks
+clawcodex --allow-dangerously-skip-permissions # allow /permission-mode bypass later
```
+> **`--dangerously-skip-permissions`** disables every tool permission check
+> for the session. Recommended only inside sandboxed containers/VMs with no
+> internet access. The flag is refused when the process is running as
+> root/sudo unless `IS_SANDBOX=1` or `CLAUDE_CODE_BUBBLEWRAP=1` is set.
+
***
## 📊 Status
diff --git a/src/cli.py b/src/cli.py
index 215e14bf0..51985f7f2 100644
--- a/src/cli.py
+++ b/src/cli.py
@@ -53,6 +53,11 @@ def main():
if args.config:
return show_config()
+ # Resolve permission state ONCE here so all modes (print/TUI/REPL) honor
+ # ``--dangerously-skip-permissions`` consistently. Mirrors
+ # ``typescript/src/main.tsx:1383-1389``.
+ _resolve_permission_state(args)
+
if args.print:
return _run_print_mode(args)
@@ -69,7 +74,11 @@ def main():
if should_use_tui(explicit_tui):
return _run_tui_mode(args)
- return start_repl(stream=args.stream)
+ return start_repl(
+ stream=args.stream,
+ permission_mode=args._resolved_permission_mode,
+ is_bypass_permissions_mode_available=args._resolved_is_bypass_available,
+ )
def _build_parser() -> argparse.ArgumentParser:
@@ -146,12 +155,6 @@ def _build_parser() -> argparse.ArgumentParser:
action='store_true',
help='Include incremental assistant text chunks in stream-json output',
)
- noninteractive.add_argument(
- '--dangerously-skip-permissions',
- dest='dangerously_skip_permissions',
- action='store_true',
- help='Bypass all tool permission checks (use with care)',
- )
noninteractive.add_argument(
'--max-turns',
type=int,
@@ -188,6 +191,39 @@ def _build_parser() -> argparse.ArgumentParser:
help='Emit verbose diagnostics to stderr',
)
+ # ---- Permissions ----
+ # ``--dangerously-skip-permissions`` and ``--allow-dangerously-skip-permissions``
+ # apply to all UI modes (REPL, TUI, headless), so they live in a top-level
+ # group rather than under ``noninteractive``. Mirrors the TS reference at
+ # ``typescript/src/main.tsx:970``.
+ permissions_group = parser.add_argument_group("permissions")
+ permissions_group.add_argument(
+ '--dangerously-skip-permissions',
+ dest='dangerously_skip_permissions',
+ action='store_true',
+ help=(
+ 'Bypass all permission checks. Recommended only for sandboxes '
+ 'with no internet access.'
+ ),
+ )
+ permissions_group.add_argument(
+ '--allow-dangerously-skip-permissions',
+ dest='allow_dangerously_skip_permissions',
+ action='store_true',
+ help=(
+ 'Enable bypassing all permission checks as an option, without it '
+ 'being enabled by default. Recommended only for sandboxes with '
+ 'no internet access.'
+ ),
+ )
+ permissions_group.add_argument(
+ '--permission-mode',
+ dest='permission_mode',
+ choices=('default', 'plan', 'acceptEdits', 'bypassPermissions', 'dontAsk'),
+ default=None,
+ help='Initial permission mode (default: default)',
+ )
+
# Subcommands are intercepted in ``main`` before argparse runs so that a
# free-form prompt argument cannot be misinterpreted as a subcommand.
# Listing them here purely for ``--help`` documentation.
@@ -204,6 +240,60 @@ def _build_parser() -> argparse.ArgumentParser:
return parser
+def _resolve_permission_state(args) -> None:
+ """Resolve and stash permission state on ``args``.
+
+ Computes the effective :class:`PermissionMode` from the CLI flags and
+ settings, runs the root/sudo safety gate, and emits a single log line
+ when either bypass flag was passed. Stashes the result on ``args`` so
+ every downstream mode (print, TUI, REPL) can read it without re-deriving.
+
+ Mirrors the wiring in ``typescript/src/main.tsx`` lines 1087-1392 plus
+ the safety check in ``typescript/src/setup.ts:382-401``.
+ """
+ import logging as _logging
+
+ from src.permissions.dangerous_safety import (
+ enforce_dangerous_skip_permissions_safety,
+ )
+ from src.permissions.modes import (
+ has_allow_bypass_permissions_mode,
+ initial_permission_mode_from_cli,
+ )
+
+ dangerously = bool(getattr(args, 'dangerously_skip_permissions', False))
+ allow_dangerously = bool(getattr(args, 'allow_dangerously_skip_permissions', False))
+ permission_mode_cli = getattr(args, 'permission_mode', None)
+
+ # Safety gate first — refuse to run as root outside a sandbox.
+ enforce_dangerous_skip_permissions_safety(
+ bypass_requested=dangerously or allow_dangerously,
+ )
+
+ mode = initial_permission_mode_from_cli(
+ permission_mode_cli=permission_mode_cli,
+ dangerously_skip_permissions=dangerously,
+ )
+
+ is_bypass_available = (
+ dangerously
+ or allow_dangerously
+ or has_allow_bypass_permissions_mode()
+ )
+
+ # Stash on args so downstream entrypoints don't need to re-derive.
+ args._resolved_permission_mode = mode
+ args._resolved_is_bypass_available = is_bypass_available
+
+ if dangerously or allow_dangerously:
+ _logging.getLogger("clawcodex.permissions").info(
+ "permission flags: dangerously_skip=%s allow_dangerously_skip=%s mode=%s",
+ dangerously,
+ allow_dangerously,
+ mode,
+ )
+
+
def _run_print_mode(args) -> int:
"""Delegate to the headless entrypoint."""
@@ -233,6 +323,8 @@ def _run_print_mode(args) -> int:
model=args.model,
max_turns=args.max_turns,
skip_permissions=bool(args.dangerously_skip_permissions),
+ permission_mode=args._resolved_permission_mode,
+ is_bypass_permissions_mode_available=args._resolved_is_bypass_available,
allowed_tools=tuple(allowed),
disallowed_tools=tuple(disallowed),
include_partial_messages=bool(args.include_partial_messages),
@@ -256,6 +348,8 @@ def _run_tui_mode(args) -> int:
allowed_tools=tuple(allowed),
disallowed_tools=tuple(disallowed),
stream=True,
+ permission_mode=args._resolved_permission_mode,
+ is_bypass_permissions_mode_available=args._resolved_is_bypass_available,
)
return run_tui(options)
@@ -371,13 +465,29 @@ def show_config():
return 0
-def start_repl(stream: bool = False):
- """Start interactive REPL."""
+def start_repl(
+ stream: bool = False,
+ *,
+ permission_mode: str = "default",
+ is_bypass_permissions_mode_available: bool = False,
+):
+ """Start interactive REPL.
+
+ ``permission_mode`` and ``is_bypass_permissions_mode_available`` are
+ resolved by :func:`_resolve_permission_state`. They control whether
+ the in-process tool registry will short-circuit permission checks
+ for the user (when ``--dangerously-skip-permissions`` is set).
+ """
from src.config import get_default_provider
from src.repl import ClawcodexREPL
provider = get_default_provider()
- repl = ClawcodexREPL(provider_name=provider, stream=stream)
+ repl = ClawcodexREPL(
+ provider_name=provider,
+ stream=stream,
+ permission_mode=permission_mode,
+ is_bypass_permissions_mode_available=is_bypass_permissions_mode_available,
+ )
repl.run()
return 0
diff --git a/src/entrypoints/headless.py b/src/entrypoints/headless.py
index 3edf38eed..4ca9d13e5 100644
--- a/src/entrypoints/headless.py
+++ b/src/entrypoints/headless.py
@@ -70,7 +70,15 @@ class HeadlessOptions:
provider_name: str | None = None
model: str | None = None
max_turns: int = 20
+ # ``skip_permissions`` is a backward-compat alias for the boolean form
+ # of ``--dangerously-skip-permissions``. ``permission_mode`` and
+ # ``is_bypass_permissions_mode_available`` were added in round 5 to
+ # mirror the TS reference's resolved state. When ``skip_permissions``
+ # is True we treat it as ``permission_mode='bypassPermissions'`` and
+ # ``is_bypass_permissions_mode_available=True``.
skip_permissions: bool = False
+ permission_mode: str = "default"
+ is_bypass_permissions_mode_available: bool = False
allowed_tools: tuple[str, ...] = ()
disallowed_tools: tuple[str, ...] = ()
include_partial_messages: bool = False
@@ -137,9 +145,31 @@ def run_headless(options: HeadlessOptions) -> int:
_filter_registry(tool_registry, keep=lambda n: n.lower() not in deny)
workspace_root = options.workspace_root or Path.cwd()
- tool_context = ToolContext(workspace_root=workspace_root)
- tool_context.options.is_non_interactive_session = True
+
+ # Compute the effective permission context. ``skip_permissions=True`` is
+ # the legacy alias and means "user passed --dangerously-skip-permissions";
+ # ``permission_mode`` / ``is_bypass_permissions_mode_available`` are the
+ # round-5 fields. When skip_permissions wins, force bypass mode + bypass
+ # availability so the registry's ``has_permissions_to_use_tool`` check
+ # short-circuits to ``allow``.
+ from src.permissions.types import ToolPermissionContext
+
if options.skip_permissions:
+ effective_mode: str = "bypassPermissions"
+ bypass_available = True
+ else:
+ effective_mode = options.permission_mode or "default"
+ bypass_available = bool(options.is_bypass_permissions_mode_available)
+
+ tool_context = ToolContext(
+ workspace_root=workspace_root,
+ permission_context=ToolPermissionContext(
+ mode=effective_mode, # type: ignore[arg-type]
+ is_bypass_permissions_mode_available=bypass_available,
+ ),
+ )
+ tool_context.options.is_non_interactive_session = True
+ if options.skip_permissions or effective_mode == "bypassPermissions":
tool_context.allow_docs = True
tool_context.permission_handler = None
else:
@@ -172,9 +202,7 @@ def run_headless(options: HeadlessOptions) -> int:
provider=provider_name,
cwd=str(workspace_root),
tools=tools,
- permission_mode="bypassPermissions"
- if options.skip_permissions
- else "default",
+ permission_mode=effective_mode,
)
)
diff --git a/src/entrypoints/tui.py b/src/entrypoints/tui.py
index 31fa89b47..f2e380952 100644
--- a/src/entrypoints/tui.py
+++ b/src/entrypoints/tui.py
@@ -35,6 +35,12 @@ class TUIOptions:
disallowed_tools: tuple[str, ...] = ()
workspace_root: Path | None = None
stream: bool = True
+ # Resolved permission state from --dangerously-skip-permissions /
+ # --allow-dangerously-skip-permissions / --permission-mode. Threaded
+ # in by ``cli._run_tui_mode`` so the TUI tool context honors the same
+ # flags as the headless entrypoint.
+ permission_mode: str = "default"
+ is_bypass_permissions_mode_available: bool = False
# Test hook: replace the provider instance we'd otherwise build from config.
provider_factory: Callable[[], object] | None = None
@@ -99,10 +105,23 @@ def run_tui(options: TUIOptions) -> int:
deny = {name.lower() for name in options.disallowed_tools}
_filter_registry(tool_registry, keep=lambda n: n.lower() not in deny)
- tool_context = ToolContext(workspace_root=workspace_root)
- # In the TUI we *do* want tool permission prompts to show up as dialogs,
- # but until we ship ``PermissionDialog`` we default to the same
- # bypassPermissions behavior the current Rich REPL uses.
+ # Apply the resolved permission state (from ``--dangerously-skip-permissions``
+ # or ``--permission-mode``). When bypass is in effect we also flip
+ # ``allow_docs`` so the doc-write gate in write.py / edit.py doesn't
+ # second-guess the user's explicit opt-in.
+ from src.permissions.types import ToolPermissionContext
+
+ tool_context = ToolContext(
+ workspace_root=workspace_root,
+ permission_context=ToolPermissionContext(
+ mode=options.permission_mode or "default", # type: ignore[arg-type]
+ is_bypass_permissions_mode_available=bool(
+ options.is_bypass_permissions_mode_available
+ ),
+ ),
+ )
+ if options.permission_mode == "bypassPermissions":
+ tool_context.allow_docs = True
tool_context.options.is_non_interactive_session = False
# Build and run app ---------------------------------------------------
diff --git a/src/permissions/dangerous_safety.py b/src/permissions/dangerous_safety.py
new file mode 100644
index 000000000..e3dc5901a
--- /dev/null
+++ b/src/permissions/dangerous_safety.py
@@ -0,0 +1,81 @@
+"""Safety gate for ``--dangerously-skip-permissions``.
+
+Mirrors ``typescript/src/setup.ts:382-401``. The bypass flag must not be
+silently honored when the process is running with elevated privileges
+(root/sudo) outside a sandboxed environment — that combination would let
+the agent perform arbitrary destructive actions on the host.
+"""
+
+from __future__ import annotations
+
+import os
+import sys
+from typing import IO
+
+
+SANDBOX_ENV_VARS: tuple[str, ...] = ("IS_SANDBOX", "CLAUDE_CODE_BUBBLEWRAP")
+
+
+def _is_truthy_env(value: str | None) -> bool:
+ if value is None:
+ return False
+ return value.strip().lower() not in ("", "0", "false", "no", "off")
+
+
+def is_sandbox_environment() -> bool:
+ """True when an env var marks the process as running in a sandbox.
+
+ The TS reference checks ``IS_SANDBOX === '1'`` and the truthy form of
+ ``CLAUDE_CODE_BUBBLEWRAP``. We accept the same set of truthy values
+ (``isEnvTruthy`` semantics).
+ """
+ return any(_is_truthy_env(os.environ.get(name)) for name in SANDBOX_ENV_VARS)
+
+
+def _is_running_as_root() -> bool:
+ if sys.platform == "win32":
+ return False
+ getuid = getattr(os, "getuid", None)
+ if getuid is None:
+ return False
+ try:
+ return getuid() == 0
+ except OSError: # pragma: no cover - extremely unlikely
+ return False
+
+
+def enforce_dangerous_skip_permissions_safety(
+ *,
+ bypass_requested: bool,
+ stderr: IO[str] | None = None,
+) -> None:
+ """Refuse to start in bypass mode when running as root outside a sandbox.
+
+ Mirrors the safety check in ``typescript/src/setup.ts``:
+
+ * Skipped on Windows (no concept of root for this check).
+ * Skipped if neither ``--dangerously-skip-permissions`` nor
+ ``--allow-dangerously-skip-permissions`` was passed.
+ * Skipped if the process is not running as uid 0.
+ * Skipped when ``IS_SANDBOX`` or ``CLAUDE_CODE_BUBBLEWRAP`` is truthy.
+
+ Otherwise prints the same error message used by the TS reference and
+ raises :class:`SystemExit` with code 1 so callers can be unit-tested.
+ """
+ if not bypass_requested:
+ return
+ if not _is_running_as_root():
+ return
+ if is_sandbox_environment():
+ return
+
+ out = stderr if stderr is not None else sys.stderr
+ out.write(
+ "--dangerously-skip-permissions cannot be used with root/sudo "
+ "privileges for security reasons\n"
+ )
+ try:
+ out.flush()
+ except Exception: # pragma: no cover - flushing test buffers is best-effort
+ pass
+ raise SystemExit(1)
diff --git a/src/permissions/modes.py b/src/permissions/modes.py
index 8623e9ede..ca38cde6f 100644
--- a/src/permissions/modes.py
+++ b/src/permissions/modes.py
@@ -1,7 +1,11 @@
from __future__ import annotations
+import logging
+
from .types import PERMISSION_MODES, PermissionMode
+log = logging.getLogger(__name__)
+
_MODE_CONFIG: dict[PermissionMode, dict[str, str]] = {
"default": {"title": "Default", "short_title": "Default", "symbol": ""},
@@ -36,3 +40,66 @@ def permission_mode_from_string(s: str) -> PermissionMode:
def is_default_mode(mode: PermissionMode | None) -> bool:
return mode is None or mode == "default"
+
+
+def initial_permission_mode_from_cli(
+ *,
+ permission_mode_cli: str | None = None,
+ dangerously_skip_permissions: bool = False,
+ settings_default_mode: str | None = None,
+) -> PermissionMode:
+ """Resolve the effective :class:`PermissionMode` from CLI flags + settings.
+
+ Mirrors ``initialPermissionModeFromCLI`` in
+ ``typescript/src/utils/permissions/permissionSetup.ts:690``.
+
+ Priority order (first match wins):
+
+ 1. ``--dangerously-skip-permissions`` -> ``bypassPermissions``
+ 2. ``--permission-mode `` -> the parsed mode
+ 3. ``settings.permissions.defaultMode``
+ 4. fallback to ``default``
+
+ Unknown / mistyped mode strings degrade to ``default`` via
+ :func:`permission_mode_from_string`.
+ """
+ candidates: list[PermissionMode] = []
+ if dangerously_skip_permissions:
+ candidates.append("bypassPermissions")
+ if permission_mode_cli:
+ candidates.append(permission_mode_from_string(permission_mode_cli))
+ if settings_default_mode:
+ candidates.append(permission_mode_from_string(settings_default_mode))
+ if candidates:
+ return candidates[0]
+ return "default"
+
+
+def has_allow_bypass_permissions_mode() -> bool:
+ """Return True if any trusted settings source enables bypass mode availability.
+
+ Mirrors ``hasAllowBypassPermissionsMode`` in
+ ``typescript/src/utils/settings/settings.ts:897``.
+
+ The TS reference reads ``permissions.allowBypassPermissionsMode`` from
+ user, local, flag, and policy settings — projectSettings is intentionally
+ excluded because a malicious project could otherwise auto-enable bypass.
+
+ Python today merges all settings sources into a single ``SettingsSchema``
+ (see ``src/settings/settings.py``). We read the merged ``extra`` dict for
+ the same key. When source-segmented settings land we can tighten this.
+ """
+ try:
+ from src.settings.settings import get_settings
+ except Exception:
+ return False
+
+ try:
+ settings = get_settings()
+ except Exception:
+ return False
+
+ perms = settings.extra.get("permissions") if hasattr(settings, "extra") else None
+ if isinstance(perms, dict):
+ return bool(perms.get("allowBypassPermissionsMode"))
+ return False
diff --git a/src/repl/core.py b/src/repl/core.py
index 5902cedb2..47b59f39a 100644
--- a/src/repl/core.py
+++ b/src/repl/core.py
@@ -262,7 +262,14 @@ def _format_edit_summary_text(adds: int, removes: int) -> str:
class ClawcodexREPL:
"""Interactive REPL for Claw Codex."""
- def __init__(self, provider_name: str = "glm", stream: bool = False):
+ def __init__(
+ self,
+ provider_name: str = "glm",
+ stream: bool = False,
+ *,
+ permission_mode: str = "default",
+ is_bypass_permissions_mode_available: bool = False,
+ ):
# Mark this process as running an interactive session BEFORE we build
# the tool registry. Tools like TaskCreate / TaskUpdate / TodoWrite
# toggle themselves on/off via ``is_todo_v2_enabled()`` which reads
@@ -271,6 +278,14 @@ def __init__(self, provider_name: str = "glm", stream: bool = False):
set_is_interactive(True)
+ # Stash the resolved permission state so ``ToolContext`` honors
+ # ``--dangerously-skip-permissions`` / ``--permission-mode`` flags
+ # passed at startup. See ``src/cli.py:_resolve_permission_state``.
+ self._permission_mode = permission_mode
+ self._is_bypass_permissions_mode_available = bool(
+ is_bypass_permissions_mode_available
+ )
+
self.console = Console()
self.provider_name = provider_name
self.stream = stream
@@ -298,11 +313,31 @@ def __init__(self, provider_name: str = "glm", stream: bool = False):
self.tool_registry = build_default_registry(provider=self.provider)
self._engine_messages: list[Any] = []
- self.tool_context = ToolContext(workspace_root=Path.cwd())
+ from src.permissions.types import ToolPermissionContext
+
+ self.tool_context = ToolContext(
+ workspace_root=Path.cwd(),
+ permission_context=ToolPermissionContext(
+ mode=self._permission_mode, # type: ignore[arg-type]
+ is_bypass_permissions_mode_available=(
+ self._is_bypass_permissions_mode_available
+ ),
+ ),
+ )
self.tool_context.ask_user = self._ask_user_questions
# Permission handler with status control for proper input handling
self._current_status = None
- self.tool_context.permission_handler = self._handle_permission_request
+ if self._permission_mode == "bypassPermissions":
+ # The bypass mode short-circuits the registry's permission check
+ # before the handler is ever consulted, but a few tools call the
+ # handler directly (e.g. the doc-write gate). Auto-allow there
+ # too so the user's explicit opt-in is honored end-to-end.
+ self.tool_context.allow_docs = True
+ self.tool_context.permission_handler = (
+ lambda _tn, _msg, _sug: (True, False)
+ )
+ else:
+ self.tool_context.permission_handler = self._handle_permission_request
# Persistent bottom-toolbar accumulators. Mirrors the TS Ink
# status line that always shows model · provider · cwd · turn /
diff --git a/tests/test_dangerous_skip_permissions.py b/tests/test_dangerous_skip_permissions.py
new file mode 100644
index 000000000..ef597fa38
--- /dev/null
+++ b/tests/test_dangerous_skip_permissions.py
@@ -0,0 +1,412 @@
+"""Tests for the ``--dangerously-skip-permissions`` wiring (round 5).
+
+Mirrors the behavior of the TS reference's ``initialPermissionModeFromCLI``,
+``setup.ts`` root/sudo gate, and the runtime permission check in
+``has_permissions_to_use_tool``.
+"""
+
+from __future__ import annotations
+
+import io
+import os
+import sys
+from pathlib import Path
+from unittest.mock import patch
+
+import pytest
+
+from src.permissions.dangerous_safety import (
+ enforce_dangerous_skip_permissions_safety,
+ is_sandbox_environment,
+)
+from src.permissions.modes import (
+ has_allow_bypass_permissions_mode,
+ initial_permission_mode_from_cli,
+)
+
+
+# ---------------------------------------------------------------------------
+# initial_permission_mode_from_cli
+
+
+def test_dsp_flag_resolves_to_bypass_permissions():
+ mode = initial_permission_mode_from_cli(dangerously_skip_permissions=True)
+ assert mode == "bypassPermissions"
+
+
+def test_no_flags_falls_back_to_default():
+ mode = initial_permission_mode_from_cli()
+ assert mode == "default"
+
+
+def test_permission_mode_cli_used_when_dsp_absent():
+ mode = initial_permission_mode_from_cli(permission_mode_cli="plan")
+ assert mode == "plan"
+
+
+def test_dsp_flag_takes_priority_over_permission_mode_cli():
+ mode = initial_permission_mode_from_cli(
+ permission_mode_cli="plan",
+ dangerously_skip_permissions=True,
+ )
+ assert mode == "bypassPermissions"
+
+
+def test_settings_default_mode_used_as_third_priority():
+ mode = initial_permission_mode_from_cli(settings_default_mode="acceptEdits")
+ assert mode == "acceptEdits"
+
+
+def test_unknown_permission_mode_string_falls_back_to_default():
+ mode = initial_permission_mode_from_cli(permission_mode_cli="garbage")
+ assert mode == "default"
+
+
+def test_priority_dsp_then_cli_then_settings():
+ mode = initial_permission_mode_from_cli(
+ permission_mode_cli="plan",
+ settings_default_mode="acceptEdits",
+ )
+ # CLI beats settings
+ assert mode == "plan"
+
+
+# ---------------------------------------------------------------------------
+# Root/sudo safety gate
+
+
+def test_safety_gate_no_op_when_bypass_not_requested():
+ # Should never raise regardless of uid.
+ enforce_dangerous_skip_permissions_safety(bypass_requested=False)
+
+
+def test_safety_gate_no_op_when_not_root(monkeypatch):
+ monkeypatch.setattr(os, "getuid", lambda: 1000, raising=False)
+ enforce_dangerous_skip_permissions_safety(bypass_requested=True)
+
+
+@pytest.mark.skipif(sys.platform == "win32", reason="root check is no-op on Windows")
+def test_safety_gate_aborts_when_root_outside_sandbox(monkeypatch):
+ monkeypatch.setattr(os, "getuid", lambda: 0, raising=False)
+ monkeypatch.delenv("IS_SANDBOX", raising=False)
+ monkeypatch.delenv("CLAUDE_CODE_BUBBLEWRAP", raising=False)
+ err = io.StringIO()
+ with pytest.raises(SystemExit) as excinfo:
+ enforce_dangerous_skip_permissions_safety(bypass_requested=True, stderr=err)
+ assert excinfo.value.code == 1
+ assert "root/sudo" in err.getvalue()
+
+
+@pytest.mark.skipif(sys.platform == "win32", reason="root check is no-op on Windows")
+def test_safety_gate_allows_root_when_is_sandbox_set(monkeypatch):
+ monkeypatch.setattr(os, "getuid", lambda: 0, raising=False)
+ monkeypatch.setenv("IS_SANDBOX", "1")
+ enforce_dangerous_skip_permissions_safety(bypass_requested=True)
+
+
+@pytest.mark.skipif(sys.platform == "win32", reason="root check is no-op on Windows")
+def test_safety_gate_allows_root_when_bubblewrap_set(monkeypatch):
+ monkeypatch.setattr(os, "getuid", lambda: 0, raising=False)
+ monkeypatch.delenv("IS_SANDBOX", raising=False)
+ monkeypatch.setenv("CLAUDE_CODE_BUBBLEWRAP", "1")
+ enforce_dangerous_skip_permissions_safety(bypass_requested=True)
+
+
+def test_is_sandbox_environment_falsy_for_zero_or_empty(monkeypatch):
+ monkeypatch.setenv("IS_SANDBOX", "0")
+ monkeypatch.delenv("CLAUDE_CODE_BUBBLEWRAP", raising=False)
+ assert is_sandbox_environment() is False
+ monkeypatch.setenv("IS_SANDBOX", "")
+ assert is_sandbox_environment() is False
+
+
+def test_is_sandbox_environment_truthy_for_one(monkeypatch):
+ monkeypatch.setenv("IS_SANDBOX", "1")
+ monkeypatch.delenv("CLAUDE_CODE_BUBBLEWRAP", raising=False)
+ assert is_sandbox_environment() is True
+
+
+# ---------------------------------------------------------------------------
+# has_allow_bypass_permissions_mode (settings reader)
+
+
+def test_has_allow_bypass_permissions_mode_default_false():
+ # The default settings should not enable bypass mode availability.
+ # (Whatever's in the actual user config is fine — we just ensure this
+ # function doesn't crash and returns a bool.)
+ result = has_allow_bypass_permissions_mode()
+ assert isinstance(result, bool)
+
+
+# ---------------------------------------------------------------------------
+# Headless wiring
+
+
+def test_headless_dsp_flag_flips_tool_context_to_bypass(tmp_path, monkeypatch):
+ """Smoke test the new HeadlessOptions fields without booting an LLM."""
+ from src.entrypoints.headless import HeadlessOptions
+
+ # We don't run the full headless loop — too noisy. Instead exercise the
+ # path that builds the tool_context by inspecting HeadlessOptions and
+ # the default values.
+ opts = HeadlessOptions(
+ prompt="hi",
+ skip_permissions=True,
+ permission_mode="default",
+ is_bypass_permissions_mode_available=False,
+ )
+ # ``skip_permissions`` is the legacy alias and is honored.
+ assert opts.skip_permissions is True
+
+
+def test_headless_options_defaults():
+ from src.entrypoints.headless import HeadlessOptions
+
+ opts = HeadlessOptions(prompt="hi")
+ assert opts.skip_permissions is False
+ assert opts.permission_mode == "default"
+ assert opts.is_bypass_permissions_mode_available is False
+
+
+def test_headless_run_skip_permissions_sets_bypass_mode(tmp_path, monkeypatch):
+ """Smoke that run_headless threads `skip_permissions` -> bypass mode."""
+ from src.entrypoints import headless as headless_mod
+ from src.entrypoints.headless import HeadlessOptions, run_headless
+ from src.providers.base import ChatResponse
+
+ class _FakeProvider:
+ def __init__(self, api_key, base_url=None, model=None):
+ self.model = model or "fake"
+
+ def chat(self, messages, tools=None, **kw):
+ return ChatResponse(
+ content="ok",
+ model="fake",
+ usage={"input_tokens": 1, "output_tokens": 1},
+ finish_reason="end_turn",
+ tool_uses=None,
+ )
+
+ class _FakeRegistry:
+ def list_tools(self):
+ return []
+
+ monkeypatch.setattr(
+ headless_mod, "get_provider_class", lambda n: _FakeProvider
+ )
+ monkeypatch.setattr(
+ headless_mod, "get_provider_config",
+ lambda n: {"api_key": "x", "default_model": "fake"},
+ )
+ monkeypatch.setattr(headless_mod, "get_default_provider", lambda: "anthropic")
+ monkeypatch.setattr(
+ headless_mod, "build_default_registry", lambda provider=None: _FakeRegistry()
+ )
+
+ captured: dict = {}
+ original = headless_mod.run_agent_loop
+
+ def _capture(*args, **kw):
+ captured["tool_context"] = kw["tool_context"]
+ return original(*args, **kw)
+
+ monkeypatch.setattr(headless_mod, "run_agent_loop", _capture)
+
+ code = run_headless(
+ HeadlessOptions(
+ prompt="hi",
+ output_format="text",
+ skip_permissions=True,
+ stdout=io.StringIO(),
+ stderr=io.StringIO(),
+ workspace_root=tmp_path,
+ )
+ )
+ assert code == 0
+ ctx = captured["tool_context"]
+ assert ctx.permission_context.mode == "bypassPermissions"
+ assert ctx.permission_context.is_bypass_permissions_mode_available is True
+ assert ctx.permission_handler is None
+ assert ctx.allow_docs is True
+
+
+def test_headless_run_default_mode_keeps_auto_deny_handler(tmp_path, monkeypatch):
+ from src.entrypoints import headless as headless_mod
+ from src.entrypoints.headless import HeadlessOptions, run_headless
+ from src.providers.base import ChatResponse
+
+ class _FakeProvider:
+ def __init__(self, api_key, base_url=None, model=None):
+ self.model = model or "fake"
+
+ def chat(self, messages, tools=None, **kw):
+ return ChatResponse(
+ content="ok",
+ model="fake",
+ usage={"input_tokens": 1, "output_tokens": 1},
+ finish_reason="end_turn",
+ tool_uses=None,
+ )
+
+ class _FakeRegistry:
+ def list_tools(self):
+ return []
+
+ monkeypatch.setattr(
+ headless_mod, "get_provider_class", lambda n: _FakeProvider
+ )
+ monkeypatch.setattr(
+ headless_mod, "get_provider_config",
+ lambda n: {"api_key": "x", "default_model": "fake"},
+ )
+ monkeypatch.setattr(headless_mod, "get_default_provider", lambda: "anthropic")
+ monkeypatch.setattr(
+ headless_mod, "build_default_registry", lambda provider=None: _FakeRegistry()
+ )
+
+ captured: dict = {}
+ original = headless_mod.run_agent_loop
+
+ def _capture(*args, **kw):
+ captured["tool_context"] = kw["tool_context"]
+ return original(*args, **kw)
+
+ monkeypatch.setattr(headless_mod, "run_agent_loop", _capture)
+
+ code = run_headless(
+ HeadlessOptions(
+ prompt="hi",
+ output_format="text",
+ stdout=io.StringIO(),
+ stderr=io.StringIO(),
+ workspace_root=tmp_path,
+ )
+ )
+ assert code == 0
+ ctx = captured["tool_context"]
+ # Default mode keeps the auto-deny handler.
+ assert ctx.permission_context.mode == "default"
+ assert ctx.permission_handler is not None
+ allowed, _ = ctx.permission_handler("Bash", "needs approval", None)
+ assert allowed is False
+
+
+# ---------------------------------------------------------------------------
+# CLI parser
+
+
+def test_cli_parser_accepts_dangerously_skip_permissions():
+ from src.cli import _build_parser
+
+ parser = _build_parser()
+ args = parser.parse_args(["--dangerously-skip-permissions"])
+ assert args.dangerously_skip_permissions is True
+ assert args.allow_dangerously_skip_permissions is False
+
+
+def test_cli_parser_accepts_allow_dangerously_skip_permissions():
+ from src.cli import _build_parser
+
+ parser = _build_parser()
+ args = parser.parse_args(["--allow-dangerously-skip-permissions"])
+ assert args.allow_dangerously_skip_permissions is True
+ assert args.dangerously_skip_permissions is False
+
+
+def test_cli_parser_accepts_permission_mode():
+ from src.cli import _build_parser
+
+ parser = _build_parser()
+ args = parser.parse_args(["--permission-mode", "plan"])
+ assert args.permission_mode == "plan"
+
+
+def test_cli_parser_default_permission_state():
+ from src.cli import _build_parser
+
+ parser = _build_parser()
+ args = parser.parse_args([])
+ assert args.dangerously_skip_permissions is False
+ assert args.allow_dangerously_skip_permissions is False
+ assert args.permission_mode is None
+
+
+def test_resolve_permission_state_stashes_resolved_mode_on_args():
+ from src.cli import _build_parser, _resolve_permission_state
+
+ parser = _build_parser()
+ args = parser.parse_args(["--dangerously-skip-permissions"])
+ _resolve_permission_state(args)
+ assert args._resolved_permission_mode == "bypassPermissions"
+ assert args._resolved_is_bypass_available is True
+
+
+def test_resolve_permission_state_default_mode_when_no_flag():
+ from src.cli import _build_parser, _resolve_permission_state
+
+ parser = _build_parser()
+ args = parser.parse_args([])
+ _resolve_permission_state(args)
+ assert args._resolved_permission_mode == "default"
+ # is_bypass_available depends on settings; default config has no bypass.
+ assert isinstance(args._resolved_is_bypass_available, bool)
+
+
+def test_resolve_permission_state_allow_dangerously_only_does_not_flip_mode():
+ from src.cli import _build_parser, _resolve_permission_state
+
+ parser = _build_parser()
+ args = parser.parse_args(["--allow-dangerously-skip-permissions"])
+ _resolve_permission_state(args)
+ assert args._resolved_permission_mode == "default"
+ assert args._resolved_is_bypass_available is True
+
+
+# ---------------------------------------------------------------------------
+# Runtime permission check honors bypass mode
+
+
+def test_runtime_check_returns_allow_in_bypass_mode():
+ """End-to-end: `has_permissions_to_use_tool` should allow without prompt."""
+ from src.permissions.check import has_permissions_to_use_tool
+ from src.permissions.types import (
+ PermissionAllowDecision,
+ ToolPermissionContext,
+ )
+
+ class _StubTool:
+ name = "Bash"
+ is_mcp = False
+
+ def check_permissions(self, tool_input, context):
+ from src.permissions.types import PermissionPassthroughResult
+
+ return PermissionPassthroughResult(behavior="passthrough")
+
+ ctx = ToolPermissionContext(mode="bypassPermissions")
+ decision = has_permissions_to_use_tool(_StubTool(), {}, ctx)
+ assert isinstance(decision, PermissionAllowDecision)
+ assert decision.behavior == "allow"
+
+
+def test_runtime_check_returns_ask_in_default_mode():
+ """End-to-end: default mode returns ask for tools that passthrough."""
+ from src.permissions.check import has_permissions_to_use_tool
+ from src.permissions.types import (
+ PermissionAskDecision,
+ ToolPermissionContext,
+ )
+
+ class _StubTool:
+ name = "Bash"
+ is_mcp = False
+
+ def check_permissions(self, tool_input, context):
+ from src.permissions.types import PermissionPassthroughResult
+
+ return PermissionPassthroughResult(behavior="passthrough")
+
+ ctx = ToolPermissionContext(mode="default")
+ decision = has_permissions_to_use_tool(_StubTool(), {}, ctx)
+ assert isinstance(decision, PermissionAskDecision)
+ assert decision.behavior == "ask"
From e5a00dea0361c2be58670b1ad7d3c0fc5f0dd1cd Mon Sep 17 00:00:00 2001
From: agentforce314
Date: Thu, 23 Apr 2026 10:41:36 -0700
Subject: [PATCH 003/251] Add demo apps (CRM, LinkedIn, Minecraft) and ignore
their build artifacts
Check in source for the three Vite demo apps under demos/. Replace the
blanket /demos/ ignore with explicit demos/*/node_modules/ and
demos/*/dist/ patterns so build output and dependencies stay untracked.
Co-Authored-By: Claude Opus 4.7
---
.gitignore | 5 +-
demos/crm-app/index.html | 12 +
demos/crm-app/package-lock.json | 4255 +++++++++++++++++
demos/crm-app/package.json | 27 +
demos/crm-app/src/App.jsx | 28 +
demos/crm-app/src/__tests__/App.test.jsx | 42 +
demos/crm-app/src/__tests__/Contacts.test.jsx | 103 +
.../crm-app/src/__tests__/Dashboard.test.jsx | 51 +
demos/crm-app/src/__tests__/Deals.test.jsx | 83 +
demos/crm-app/src/components/ContactForm.jsx | 59 +
demos/crm-app/src/components/DealForm.jsx | 64 +
demos/crm-app/src/context/CRMContext.jsx | 75 +
demos/crm-app/src/index.css | 278 ++
demos/crm-app/src/main.jsx | 16 +
demos/crm-app/src/pages/Contacts.jsx | 100 +
demos/crm-app/src/pages/Dashboard.jsx | 73 +
demos/crm-app/src/pages/Deals.jsx | 91 +
demos/crm-app/src/setupTests.js | 1 +
demos/crm-app/vite.config.js | 11 +
demos/linkedin-app/index.html | 12 +
demos/linkedin-app/package-lock.json | 1836 +++++++
demos/linkedin-app/package.json | 20 +
demos/linkedin-app/src/App.jsx | 26 +
.../src/components/ConnectionCard.jsx | 23 +
.../src/components/CreatePost.jsx | 49 +
demos/linkedin-app/src/components/JobCard.jsx | 33 +
demos/linkedin-app/src/components/Navbar.jsx | 56 +
.../linkedin-app/src/components/PostCard.jsx | 56 +
.../src/components/ProfileCard.jsx | 31 +
.../src/context/LinkedInContext.jsx | 183 +
demos/linkedin-app/src/index.css | 954 ++++
demos/linkedin-app/src/main.jsx | 16 +
demos/linkedin-app/src/pages/Feed.jsx | 51 +
demos/linkedin-app/src/pages/Jobs.jsx | 22 +
demos/linkedin-app/src/pages/Messaging.jsx | 30 +
demos/linkedin-app/src/pages/Network.jsx | 22 +
demos/linkedin-app/src/pages/Profile.jsx | 56 +
demos/linkedin-app/vite.config.js | 6 +
demos/minecraft-app/index.html | 17 +
demos/minecraft-app/package-lock.json | 2657 ++++++++++
demos/minecraft-app/package.json | 22 +
demos/minecraft-app/src/App.jsx | 83 +
.../src/components/Crosshair.jsx | 21 +
demos/minecraft-app/src/components/HUD.jsx | 98 +
.../src/components/MiningIndicator.jsx | 53 +
demos/minecraft-app/src/components/Player.jsx | 321 ++
demos/minecraft-app/src/components/Sky.jsx | 49 +
.../src/components/StartScreen.jsx | 74 +
demos/minecraft-app/src/components/World.jsx | 102 +
.../minecraft-app/src/hooks/useWorldState.js | 94 +
demos/minecraft-app/src/main.jsx | 9 +
demos/minecraft-app/src/utils/blocks.js | 54 +
demos/minecraft-app/src/utils/noise.js | 79 +
demos/minecraft-app/src/utils/terrain.js | 112 +
demos/minecraft-app/vite.config.js | 6 +
55 files changed, 12705 insertions(+), 2 deletions(-)
create mode 100644 demos/crm-app/index.html
create mode 100644 demos/crm-app/package-lock.json
create mode 100644 demos/crm-app/package.json
create mode 100644 demos/crm-app/src/App.jsx
create mode 100644 demos/crm-app/src/__tests__/App.test.jsx
create mode 100644 demos/crm-app/src/__tests__/Contacts.test.jsx
create mode 100644 demos/crm-app/src/__tests__/Dashboard.test.jsx
create mode 100644 demos/crm-app/src/__tests__/Deals.test.jsx
create mode 100644 demos/crm-app/src/components/ContactForm.jsx
create mode 100644 demos/crm-app/src/components/DealForm.jsx
create mode 100644 demos/crm-app/src/context/CRMContext.jsx
create mode 100644 demos/crm-app/src/index.css
create mode 100644 demos/crm-app/src/main.jsx
create mode 100644 demos/crm-app/src/pages/Contacts.jsx
create mode 100644 demos/crm-app/src/pages/Dashboard.jsx
create mode 100644 demos/crm-app/src/pages/Deals.jsx
create mode 100644 demos/crm-app/src/setupTests.js
create mode 100644 demos/crm-app/vite.config.js
create mode 100644 demos/linkedin-app/index.html
create mode 100644 demos/linkedin-app/package-lock.json
create mode 100644 demos/linkedin-app/package.json
create mode 100644 demos/linkedin-app/src/App.jsx
create mode 100644 demos/linkedin-app/src/components/ConnectionCard.jsx
create mode 100644 demos/linkedin-app/src/components/CreatePost.jsx
create mode 100644 demos/linkedin-app/src/components/JobCard.jsx
create mode 100644 demos/linkedin-app/src/components/Navbar.jsx
create mode 100644 demos/linkedin-app/src/components/PostCard.jsx
create mode 100644 demos/linkedin-app/src/components/ProfileCard.jsx
create mode 100644 demos/linkedin-app/src/context/LinkedInContext.jsx
create mode 100644 demos/linkedin-app/src/index.css
create mode 100644 demos/linkedin-app/src/main.jsx
create mode 100644 demos/linkedin-app/src/pages/Feed.jsx
create mode 100644 demos/linkedin-app/src/pages/Jobs.jsx
create mode 100644 demos/linkedin-app/src/pages/Messaging.jsx
create mode 100644 demos/linkedin-app/src/pages/Network.jsx
create mode 100644 demos/linkedin-app/src/pages/Profile.jsx
create mode 100644 demos/linkedin-app/vite.config.js
create mode 100644 demos/minecraft-app/index.html
create mode 100644 demos/minecraft-app/package-lock.json
create mode 100644 demos/minecraft-app/package.json
create mode 100644 demos/minecraft-app/src/App.jsx
create mode 100644 demos/minecraft-app/src/components/Crosshair.jsx
create mode 100644 demos/minecraft-app/src/components/HUD.jsx
create mode 100644 demos/minecraft-app/src/components/MiningIndicator.jsx
create mode 100644 demos/minecraft-app/src/components/Player.jsx
create mode 100644 demos/minecraft-app/src/components/Sky.jsx
create mode 100644 demos/minecraft-app/src/components/StartScreen.jsx
create mode 100644 demos/minecraft-app/src/components/World.jsx
create mode 100644 demos/minecraft-app/src/hooks/useWorldState.js
create mode 100644 demos/minecraft-app/src/main.jsx
create mode 100644 demos/minecraft-app/src/utils/blocks.js
create mode 100644 demos/minecraft-app/src/utils/noise.js
create mode 100644 demos/minecraft-app/src/utils/terrain.js
create mode 100644 demos/minecraft-app/vite.config.js
diff --git a/.gitignore b/.gitignore
index 4142c9cc1..17ff42093 100644
--- a/.gitignore
+++ b/.gitignore
@@ -87,8 +87,9 @@ CLAUDE.local.md
/reference_projects/
/my-prompts/
-# Demos
-/demos/
+# Demos - source committed; build output and dependencies covered by node_modules/ and dist/ above
+demos/*/node_modules/
+demos/*/dist/
#typescript sources
/typescript/
diff --git a/demos/crm-app/index.html b/demos/crm-app/index.html
new file mode 100644
index 000000000..8dc8c46a2
--- /dev/null
+++ b/demos/crm-app/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Simple CRM
+
+
+
+
+
+
diff --git a/demos/crm-app/package-lock.json b/demos/crm-app/package-lock.json
new file mode 100644
index 000000000..a21df82f4
--- /dev/null
+++ b/demos/crm-app/package-lock.json
@@ -0,0 +1,4255 @@
+{
+ "name": "crm-app",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "crm-app",
+ "version": "1.0.0",
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^6.28.0"
+ },
+ "devDependencies": {
+ "@testing-library/jest-dom": "^6.6.3",
+ "@testing-library/react": "^16.1.0",
+ "@testing-library/user-event": "^14.5.2",
+ "@vitejs/plugin-react": "^4.3.4",
+ "jsdom": "^25.0.1",
+ "vite": "^6.0.0",
+ "vitest": "^2.1.0"
+ }
+ },
+ "node_modules/@adobe/css-tools": {
+ "version": "4.4.4",
+ "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz",
+ "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
+ "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/css-calc": "^2.1.3",
+ "@csstools/css-color-parser": "^3.0.9",
+ "@csstools/css-parser-algorithms": "^3.0.4",
+ "@csstools/css-tokenizer": "^3.0.3",
+ "lru-cache": "^10.4.3"
+ }
+ },
+ "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
+ "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@csstools/color-helpers": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
+ "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
+ "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
+ "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/color-helpers": "^5.1.0",
+ "@csstools/css-calc": "^2.1.4"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
+ "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
+ "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@remix-run/router": {
+ "version": "1.23.2",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
+ "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz",
+ "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz",
+ "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz",
+ "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz",
+ "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz",
+ "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz",
+ "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz",
+ "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz",
+ "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz",
+ "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz",
+ "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz",
+ "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz",
+ "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz",
+ "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz",
+ "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz",
+ "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz",
+ "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz",
+ "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz",
+ "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz",
+ "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz",
+ "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz",
+ "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz",
+ "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz",
+ "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz",
+ "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz",
+ "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@testing-library/dom": {
+ "version": "10.4.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
+ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/runtime": "^7.12.5",
+ "@types/aria-query": "^5.0.1",
+ "aria-query": "5.3.0",
+ "dom-accessibility-api": "^0.5.9",
+ "lz-string": "^1.5.0",
+ "picocolors": "1.1.1",
+ "pretty-format": "^27.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@testing-library/jest-dom": {
+ "version": "6.9.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz",
+ "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@adobe/css-tools": "^4.4.0",
+ "aria-query": "^5.0.0",
+ "css.escape": "^1.5.1",
+ "dom-accessibility-api": "^0.6.3",
+ "picocolors": "^1.1.1",
+ "redent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ }
+ },
+ "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
+ "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@testing-library/react": {
+ "version": "16.3.2",
+ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz",
+ "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@testing-library/user-event": {
+ "version": "14.6.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz",
+ "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": ">=7.21.4"
+ }
+ },
+ "node_modules/@types/aria-query": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
+ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz",
+ "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "2.1.9",
+ "@vitest/utils": "2.1.9",
+ "chai": "^5.1.2",
+ "tinyrainbow": "^1.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz",
+ "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^1.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz",
+ "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "2.1.9",
+ "pathe": "^1.1.2"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz",
+ "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "2.1.9",
+ "magic-string": "^0.30.12",
+ "pathe": "^1.1.2"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz",
+ "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyspy": "^3.0.2"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz",
+ "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "2.1.9",
+ "loupe": "^3.1.2",
+ "tinyrainbow": "^1.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/aria-query": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
+ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.20",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz",
+ "integrity": "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001788",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz",
+ "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chai": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
+ "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assertion-error": "^2.0.1",
+ "check-error": "^2.1.1",
+ "deep-eql": "^5.0.1",
+ "loupe": "^3.1.0",
+ "pathval": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/check-error": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
+ "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/css.escape": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
+ "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cssstyle": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
+ "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/css-color": "^3.2.0",
+ "rrweb-cssom": "^0.8.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/cssstyle/node_modules/rrweb-cssom": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
+ "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/data-urls": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
+ "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/deep-eql": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
+ "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/dom-accessibility-api": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
+ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.340",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.340.tgz",
+ "integrity": "sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
+ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-encoding": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/jsdom": {
+ "version": "25.0.1",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz",
+ "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssstyle": "^4.1.0",
+ "data-urls": "^5.0.0",
+ "decimal.js": "^10.4.3",
+ "form-data": "^4.0.0",
+ "html-encoding-sniffer": "^4.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.5",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.12",
+ "parse5": "^7.1.2",
+ "rrweb-cssom": "^0.7.1",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^5.0.0",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^3.1.1",
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.0.0",
+ "ws": "^8.18.0",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "canvas": "^2.11.2"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/loupe": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
+ "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lz-string": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
+ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "lz-string": "bin/bin.js"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/min-indent": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
+ "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.37",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
+ "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nwsapi": {
+ "version": "2.2.23",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz",
+ "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/pathe": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
+ "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathval": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
+ "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.16"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.10",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
+ "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/pretty-format": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
+ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^17.0.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "6.30.3",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
+ "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.30.3",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
+ "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.2",
+ "react-router": "6.30.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/redent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
+ "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "indent-string": "^4.0.0",
+ "strip-indent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz",
+ "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.60.2",
+ "@rollup/rollup-android-arm64": "4.60.2",
+ "@rollup/rollup-darwin-arm64": "4.60.2",
+ "@rollup/rollup-darwin-x64": "4.60.2",
+ "@rollup/rollup-freebsd-arm64": "4.60.2",
+ "@rollup/rollup-freebsd-x64": "4.60.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.2",
+ "@rollup/rollup-linux-arm64-musl": "4.60.2",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.2",
+ "@rollup/rollup-linux-loong64-musl": "4.60.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.2",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.2",
+ "@rollup/rollup-linux-x64-gnu": "4.60.2",
+ "@rollup/rollup-linux-x64-musl": "4.60.2",
+ "@rollup/rollup-openbsd-x64": "4.60.2",
+ "@rollup/rollup-openharmony-arm64": "4.60.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.2",
+ "@rollup/rollup-win32-x64-gnu": "4.60.2",
+ "@rollup/rollup-win32-x64-msvc": "4.60.2",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/rrweb-cssom": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
+ "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/strip-indent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
+ "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "min-indent": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinypool": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
+ "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz",
+ "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tinyspy": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
+ "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tldts": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
+ "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^6.1.86"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
+ "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tough-cookie": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
+ "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^6.1.32"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.4.2",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
+ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "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
+ }
+ }
+ },
+ "node_modules/vite-node": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz",
+ "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cac": "^6.7.14",
+ "debug": "^4.3.7",
+ "es-module-lexer": "^1.5.4",
+ "pathe": "^1.1.2",
+ "vite": "^5.0.0"
+ },
+ "bin": {
+ "vite-node": "vite-node.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/vite-node/node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz",
+ "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "2.1.9",
+ "@vitest/mocker": "2.1.9",
+ "@vitest/pretty-format": "^2.1.9",
+ "@vitest/runner": "2.1.9",
+ "@vitest/snapshot": "2.1.9",
+ "@vitest/spy": "2.1.9",
+ "@vitest/utils": "2.1.9",
+ "chai": "^5.1.2",
+ "debug": "^4.3.7",
+ "expect-type": "^1.1.0",
+ "magic-string": "^0.30.12",
+ "pathe": "^1.1.2",
+ "std-env": "^3.8.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^0.3.1",
+ "tinypool": "^1.0.1",
+ "tinyrainbow": "^1.2.0",
+ "vite": "^5.0.0",
+ "vite-node": "2.1.9",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "@vitest/browser": "2.1.9",
+ "@vitest/ui": "2.1.9",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@vitest/mocker": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz",
+ "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "2.1.9",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.12"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest/node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/vitest/node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
+ "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^5.1.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
+ "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
+ "dev": true,
+ "license": "MIT",
+ "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
+ }
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ }
+ }
+}
diff --git a/demos/crm-app/package.json b/demos/crm-app/package.json
new file mode 100644
index 000000000..38d599c2d
--- /dev/null
+++ b/demos/crm-app/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "crm-app",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview",
+ "test": "vitest run",
+ "test:watch": "vitest"
+ },
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^6.28.0"
+ },
+ "devDependencies": {
+ "@testing-library/jest-dom": "^6.6.3",
+ "@testing-library/react": "^16.1.0",
+ "@testing-library/user-event": "^14.5.2",
+ "@vitejs/plugin-react": "^4.3.4",
+ "jsdom": "^25.0.1",
+ "vite": "^6.0.0",
+ "vitest": "^2.1.0"
+ }
+}
diff --git a/demos/crm-app/src/App.jsx b/demos/crm-app/src/App.jsx
new file mode 100644
index 000000000..36c7569e2
--- /dev/null
+++ b/demos/crm-app/src/App.jsx
@@ -0,0 +1,28 @@
+import { Routes, Route, NavLink } from 'react-router-dom'
+import Dashboard from './pages/Dashboard'
+import Contacts from './pages/Contacts'
+import Deals from './pages/Deals'
+
+function App() {
+ return (
+
+
+ SimpleCRM
+
+ Dashboard
+ Contacts
+ Deals
+
+
+
+
+ } />
+ } />
+ } />
+
+
+
+ )
+}
+
+export default App
diff --git a/demos/crm-app/src/__tests__/App.test.jsx b/demos/crm-app/src/__tests__/App.test.jsx
new file mode 100644
index 000000000..d7d8444b4
--- /dev/null
+++ b/demos/crm-app/src/__tests__/App.test.jsx
@@ -0,0 +1,42 @@
+import { render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { BrowserRouter } from 'react-router-dom'
+import { describe, it, expect } from 'vitest'
+import { CRMProvider } from '../context/CRMContext'
+import App from '../App'
+
+function renderApp() {
+ return render(
+
+
+
+
+
+ )
+}
+
+describe('App', () => {
+ it('renders the sidebar with navigation links', () => {
+ renderApp()
+ expect(screen.getByText('SimpleCRM')).toBeInTheDocument()
+ expect(screen.getByRole('link', { name: 'Dashboard' })).toBeInTheDocument()
+ expect(screen.getByRole('link', { name: 'Contacts' })).toBeInTheDocument()
+ expect(screen.getByRole('link', { name: 'Deals' })).toBeInTheDocument()
+ })
+
+ it('navigates to contacts page', async () => {
+ const user = userEvent.setup()
+ renderApp()
+
+ await user.click(screen.getByText('Contacts'))
+ expect(screen.getByText('Add Contact')).toBeInTheDocument()
+ })
+
+ it('navigates to deals page', async () => {
+ const user = userEvent.setup()
+ renderApp()
+
+ await user.click(screen.getByText('Deals'))
+ expect(screen.getByText('Add Deal')).toBeInTheDocument()
+ })
+})
diff --git a/demos/crm-app/src/__tests__/Contacts.test.jsx b/demos/crm-app/src/__tests__/Contacts.test.jsx
new file mode 100644
index 000000000..ae28bc56f
--- /dev/null
+++ b/demos/crm-app/src/__tests__/Contacts.test.jsx
@@ -0,0 +1,103 @@
+import { render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { BrowserRouter } from 'react-router-dom'
+import { describe, it, expect } from 'vitest'
+import { CRMProvider } from '../context/CRMContext'
+import Contacts from '../pages/Contacts'
+
+function renderWithProviders(ui) {
+ return render(
+
+ {ui}
+
+ )
+}
+
+describe('Contacts', () => {
+ it('renders the contacts page with initial data', () => {
+ renderWithProviders( )
+ expect(screen.getByText('Contacts')).toBeInTheDocument()
+ expect(screen.getByText('Alice Johnson')).toBeInTheDocument()
+ expect(screen.getByText('Bob Smith')).toBeInTheDocument()
+ expect(screen.getByText('Carol White')).toBeInTheDocument()
+ })
+
+ it('filters contacts by search input', async () => {
+ const user = userEvent.setup()
+ renderWithProviders( )
+
+ const searchInput = screen.getByPlaceholderText('Search contacts...')
+ await user.type(searchInput, 'alice')
+
+ expect(screen.getByText('Alice Johnson')).toBeInTheDocument()
+ expect(screen.queryByText('Bob Smith')).not.toBeInTheDocument()
+ expect(screen.queryByText('Carol White')).not.toBeInTheDocument()
+ })
+
+ it('shows empty message when no contacts match', async () => {
+ const user = userEvent.setup()
+ renderWithProviders( )
+
+ const searchInput = screen.getByPlaceholderText('Search contacts...')
+ await user.type(searchInput, 'zzzzz')
+
+ expect(screen.getByText('No contacts found')).toBeInTheDocument()
+ })
+
+ it('opens add contact form when clicking Add Contact', async () => {
+ const user = userEvent.setup()
+ renderWithProviders( )
+
+ await user.click(screen.getByText('Add Contact'))
+ expect(screen.getByText('New Contact')).toBeInTheDocument()
+ })
+
+ it('adds a new contact via the form', async () => {
+ const user = userEvent.setup()
+ renderWithProviders( )
+
+ await user.click(screen.getByText('Add Contact'))
+
+ await user.type(screen.getByLabelText(/Name/), 'Dave Brown')
+ await user.type(screen.getByLabelText(/Email/), 'dave@example.com')
+ await user.type(screen.getByLabelText(/Phone/), '555-0104')
+ await user.type(screen.getByLabelText(/Company/), 'Wayne Enterprises')
+ await user.click(screen.getByText('Save'))
+
+ expect(screen.getByText('Dave Brown')).toBeInTheDocument()
+ expect(screen.getByText('dave@example.com')).toBeInTheDocument()
+ })
+
+ it('cancels adding a contact', async () => {
+ const user = userEvent.setup()
+ renderWithProviders( )
+
+ await user.click(screen.getByText('Add Contact'))
+ expect(screen.getByText('New Contact')).toBeInTheDocument()
+
+ await user.click(screen.getByText('Cancel'))
+ expect(screen.queryByText('New Contact')).not.toBeInTheDocument()
+ })
+
+ it('deletes a contact', async () => {
+ const user = userEvent.setup()
+ renderWithProviders( )
+
+ const deleteButtons = screen.getAllByText('Delete')
+ await user.click(deleteButtons[0])
+
+ expect(screen.queryByText('Alice Johnson')).not.toBeInTheDocument()
+ })
+
+ it('opens edit form with contact data', async () => {
+ const user = userEvent.setup()
+ renderWithProviders( )
+
+ const editButtons = screen.getAllByText('Edit')
+ await user.click(editButtons[0])
+
+ expect(screen.getByText('Edit Contact')).toBeInTheDocument()
+ expect(screen.getByDisplayValue('Alice Johnson')).toBeInTheDocument()
+ expect(screen.getByDisplayValue('alice@example.com')).toBeInTheDocument()
+ })
+})
diff --git a/demos/crm-app/src/__tests__/Dashboard.test.jsx b/demos/crm-app/src/__tests__/Dashboard.test.jsx
new file mode 100644
index 000000000..f4e164176
--- /dev/null
+++ b/demos/crm-app/src/__tests__/Dashboard.test.jsx
@@ -0,0 +1,51 @@
+import { render, screen } from '@testing-library/react'
+import { BrowserRouter } from 'react-router-dom'
+import { describe, it, expect } from 'vitest'
+import { CRMProvider } from '../context/CRMContext'
+import Dashboard from '../pages/Dashboard'
+
+function renderWithProviders(ui) {
+ return render(
+
+ {ui}
+
+ )
+}
+
+describe('Dashboard', () => {
+ it('renders the dashboard heading', () => {
+ renderWithProviders( )
+ expect(screen.getByText('Dashboard')).toBeInTheDocument()
+ })
+
+ it('displays stat cards with correct counts', () => {
+ renderWithProviders( )
+ const statCards = document.querySelectorAll('.stat-card')
+ expect(statCards.length).toBe(6)
+ expect(screen.getByText('Total Contacts')).toBeInTheDocument()
+ expect(screen.getByText('Total Deals')).toBeInTheDocument()
+ expect(screen.getByText('Active Contacts')).toBeInTheDocument()
+ expect(screen.getByText('Leads')).toBeInTheDocument()
+ })
+
+ it('displays pipeline value', () => {
+ renderWithProviders( )
+ expect(screen.getByText('Pipeline Value')).toBeInTheDocument()
+ expect(screen.getByText('$80,000')).toBeInTheDocument()
+ })
+
+ it('renders pipeline stages', () => {
+ renderWithProviders( )
+ expect(screen.getByText('qualified')).toBeInTheDocument()
+ expect(screen.getByText('proposal')).toBeInTheDocument()
+ expect(screen.getByText('negotiation')).toBeInTheDocument()
+ expect(screen.getByText('closed won')).toBeInTheDocument()
+ })
+
+ it('shows deal cards in the pipeline', () => {
+ renderWithProviders( )
+ expect(screen.getByText('Enterprise License')).toBeInTheDocument()
+ expect(screen.getByText('Consulting Package')).toBeInTheDocument()
+ expect(screen.getByText('Starter Plan')).toBeInTheDocument()
+ })
+})
diff --git a/demos/crm-app/src/__tests__/Deals.test.jsx b/demos/crm-app/src/__tests__/Deals.test.jsx
new file mode 100644
index 000000000..161ab6b03
--- /dev/null
+++ b/demos/crm-app/src/__tests__/Deals.test.jsx
@@ -0,0 +1,83 @@
+import { render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { BrowserRouter } from 'react-router-dom'
+import { describe, it, expect } from 'vitest'
+import { CRMProvider } from '../context/CRMContext'
+import Deals from '../pages/Deals'
+
+function renderWithProviders(ui) {
+ return render(
+
+ {ui}
+
+ )
+}
+
+describe('Deals', () => {
+ it('renders the deals page with initial data', () => {
+ renderWithProviders( )
+ expect(screen.getByText('Deals')).toBeInTheDocument()
+ expect(screen.getByText('Enterprise License')).toBeInTheDocument()
+ expect(screen.getByText('Consulting Package')).toBeInTheDocument()
+ expect(screen.getByText('Starter Plan')).toBeInTheDocument()
+ })
+
+ it('displays deal values', () => {
+ renderWithProviders( )
+ expect(screen.getByText('$50,000')).toBeInTheDocument()
+ expect(screen.getByText('$25,000')).toBeInTheDocument()
+ expect(screen.getByText('$5,000')).toBeInTheDocument()
+ })
+
+ it('opens add deal form', async () => {
+ const user = userEvent.setup()
+ renderWithProviders( )
+
+ await user.click(screen.getByText('Add Deal'))
+ expect(screen.getByText('New Deal')).toBeInTheDocument()
+ })
+
+ it('adds a new deal', async () => {
+ const user = userEvent.setup()
+ renderWithProviders( )
+
+ await user.click(screen.getByText('Add Deal'))
+ await user.type(screen.getByLabelText(/Title/), 'Premium Support')
+ await user.type(screen.getByLabelText(/Value/), '15000')
+ await user.click(screen.getByText('Save'))
+
+ expect(screen.getByText('Premium Support')).toBeInTheDocument()
+ })
+
+ it('deletes a deal', async () => {
+ const user = userEvent.setup()
+ renderWithProviders( )
+
+ const deleteButtons = screen.getAllByText('Delete')
+ await user.click(deleteButtons[0])
+
+ expect(screen.queryByText('Enterprise License')).not.toBeInTheDocument()
+ })
+
+ it('opens edit form for an existing deal', async () => {
+ const user = userEvent.setup()
+ renderWithProviders( )
+
+ const editButtons = screen.getAllByText('Edit')
+ await user.click(editButtons[0])
+
+ expect(screen.getByText('Edit Deal')).toBeInTheDocument()
+ expect(screen.getByDisplayValue('Enterprise License')).toBeInTheDocument()
+ })
+
+ it('cancels adding a deal', async () => {
+ const user = userEvent.setup()
+ renderWithProviders( )
+
+ await user.click(screen.getByText('Add Deal'))
+ expect(screen.getByText('New Deal')).toBeInTheDocument()
+
+ await user.click(screen.getByText('Cancel'))
+ expect(screen.queryByText('New Deal')).not.toBeInTheDocument()
+ })
+})
diff --git a/demos/crm-app/src/components/ContactForm.jsx b/demos/crm-app/src/components/ContactForm.jsx
new file mode 100644
index 000000000..641c74b8c
--- /dev/null
+++ b/demos/crm-app/src/components/ContactForm.jsx
@@ -0,0 +1,59 @@
+import { useState } from 'react'
+
+function ContactForm({ contact, onSave, onCancel }) {
+ const [form, setForm] = useState({
+ name: contact?.name || '',
+ email: contact?.email || '',
+ phone: contact?.phone || '',
+ company: contact?.company || '',
+ status: contact?.status || 'lead',
+ })
+
+ function handleChange(e) {
+ setForm({ ...form, [e.target.name]: e.target.value })
+ }
+
+ function handleSubmit(e) {
+ e.preventDefault()
+ if (!form.name.trim() || !form.email.trim()) return
+ onSave(form)
+ }
+
+ return (
+
+ )
+}
+
+export default ContactForm
diff --git a/demos/crm-app/src/components/DealForm.jsx b/demos/crm-app/src/components/DealForm.jsx
new file mode 100644
index 000000000..2df1509ac
--- /dev/null
+++ b/demos/crm-app/src/components/DealForm.jsx
@@ -0,0 +1,64 @@
+import { useState } from 'react'
+
+const STAGES = ['qualified', 'proposal', 'negotiation', 'closed-won', 'closed-lost']
+
+function DealForm({ deal, contacts, onSave, onCancel }) {
+ const [form, setForm] = useState({
+ title: deal?.title || '',
+ contactId: deal?.contactId || (contacts[0]?.id ?? ''),
+ value: deal?.value || '',
+ stage: deal?.stage || 'qualified',
+ createdAt: deal?.createdAt || new Date().toISOString().slice(0, 10),
+ })
+
+ function handleChange(e) {
+ const value = e.target.name === 'value' ? Number(e.target.value) || ''
+ : e.target.name === 'contactId' ? Number(e.target.value)
+ : e.target.value
+ setForm({ ...form, [e.target.name]: value })
+ }
+
+ function handleSubmit(e) {
+ e.preventDefault()
+ if (!form.title.trim() || !form.value) return
+ onSave(form)
+ }
+
+ return (
+
+ )
+}
+
+export default DealForm
diff --git a/demos/crm-app/src/context/CRMContext.jsx b/demos/crm-app/src/context/CRMContext.jsx
new file mode 100644
index 000000000..c35c2f483
--- /dev/null
+++ b/demos/crm-app/src/context/CRMContext.jsx
@@ -0,0 +1,75 @@
+import { createContext, useContext, useReducer } from 'react'
+
+const CRMContext = createContext()
+
+const initialState = {
+ contacts: [
+ { id: 1, name: 'Alice Johnson', email: 'alice@example.com', phone: '555-0101', company: 'Acme Corp', status: 'active' },
+ { id: 2, name: 'Bob Smith', email: 'bob@example.com', phone: '555-0102', company: 'Globex Inc', status: 'active' },
+ { id: 3, name: 'Carol White', email: 'carol@example.com', phone: '555-0103', company: 'Initech', status: 'lead' },
+ ],
+ deals: [
+ { id: 1, title: 'Enterprise License', contactId: 1, value: 50000, stage: 'proposal', createdAt: '2026-03-15' },
+ { id: 2, title: 'Consulting Package', contactId: 2, value: 25000, stage: 'negotiation', createdAt: '2026-04-01' },
+ { id: 3, title: 'Starter Plan', contactId: 3, value: 5000, stage: 'qualified', createdAt: '2026-04-10' },
+ ],
+ nextContactId: 4,
+ nextDealId: 4,
+}
+
+function crmReducer(state, action) {
+ switch (action.type) {
+ case 'ADD_CONTACT':
+ return {
+ ...state,
+ contacts: [...state.contacts, { ...action.payload, id: state.nextContactId }],
+ nextContactId: state.nextContactId + 1,
+ }
+ case 'UPDATE_CONTACT':
+ return {
+ ...state,
+ contacts: state.contacts.map(c => c.id === action.payload.id ? action.payload : c),
+ }
+ case 'DELETE_CONTACT':
+ return {
+ ...state,
+ contacts: state.contacts.filter(c => c.id !== action.payload),
+ deals: state.deals.filter(d => d.contactId !== action.payload),
+ }
+ case 'ADD_DEAL':
+ return {
+ ...state,
+ deals: [...state.deals, { ...action.payload, id: state.nextDealId }],
+ nextDealId: state.nextDealId + 1,
+ }
+ case 'UPDATE_DEAL':
+ return {
+ ...state,
+ deals: state.deals.map(d => d.id === action.payload.id ? action.payload : d),
+ }
+ case 'DELETE_DEAL':
+ return {
+ ...state,
+ deals: state.deals.filter(d => d.id !== action.payload),
+ }
+ default:
+ return state
+ }
+}
+
+export function CRMProvider({ children }) {
+ const [state, dispatch] = useReducer(crmReducer, initialState)
+ return (
+
+ {children}
+
+ )
+}
+
+export function useCRM() {
+ const context = useContext(CRMContext)
+ if (!context) {
+ throw new Error('useCRM must be used within a CRMProvider')
+ }
+ return context
+}
diff --git a/demos/crm-app/src/index.css b/demos/crm-app/src/index.css
new file mode 100644
index 000000000..b4615829a
--- /dev/null
+++ b/demos/crm-app/src/index.css
@@ -0,0 +1,278 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ background: #f0f2f5;
+ color: #1a1a2e;
+}
+
+.app {
+ display: flex;
+ min-height: 100vh;
+}
+
+/* Sidebar */
+.sidebar {
+ width: 220px;
+ background: #1a1a2e;
+ color: #fff;
+ padding: 24px 0;
+ flex-shrink: 0;
+}
+
+.logo {
+ font-size: 1.4rem;
+ font-weight: 700;
+ padding: 0 24px 24px;
+ border-bottom: 1px solid #2a2a4a;
+}
+
+.sidebar ul {
+ list-style: none;
+ margin-top: 16px;
+}
+
+.sidebar a {
+ display: block;
+ padding: 12px 24px;
+ color: #a0a0c0;
+ text-decoration: none;
+ transition: background 0.2s, color 0.2s;
+}
+
+.sidebar a:hover,
+.sidebar a.active {
+ background: #2a2a4a;
+ color: #fff;
+}
+
+/* Main */
+.main-content {
+ flex: 1;
+ padding: 32px;
+ overflow-x: auto;
+}
+
+.page {
+ max-width: 1100px;
+}
+
+.page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+h2 { margin-bottom: 20px; font-size: 1.6rem; }
+h3 { margin: 24px 0 12px; }
+
+/* Stats */
+.stats-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
+ gap: 16px;
+ margin-bottom: 24px;
+}
+
+.stat-card {
+ background: #fff;
+ border-radius: 10px;
+ padding: 20px;
+ display: flex;
+ flex-direction: column;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.08);
+}
+
+.stat-label { font-size: 0.85rem; color: #888; margin-bottom: 4px; }
+.stat-value { font-size: 1.5rem; font-weight: 700; }
+
+/* Pipeline */
+.pipeline {
+ display: flex;
+ gap: 12px;
+ overflow-x: auto;
+ padding-bottom: 12px;
+}
+
+.pipeline-stage {
+ background: #fff;
+ border-radius: 10px;
+ padding: 16px;
+ min-width: 200px;
+ flex: 1;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.08);
+}
+
+.pipeline-stage h4 {
+ text-transform: capitalize;
+ font-size: 0.9rem;
+ margin-bottom: 8px;
+ color: #555;
+}
+
+.badge {
+ display: inline-block;
+ background: #e8e8f0;
+ border-radius: 12px;
+ padding: 2px 10px;
+ font-size: 0.8rem;
+ font-weight: 600;
+ margin-bottom: 12px;
+}
+
+.pipeline-card {
+ background: #f8f9fb;
+ border-radius: 8px;
+ padding: 12px;
+ margin-bottom: 8px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.pipeline-card strong { font-size: 0.9rem; }
+.pipeline-card span { font-size: 0.85rem; color: #16a34a; font-weight: 600; }
+.pipeline-card small { font-size: 0.8rem; color: #888; }
+
+/* Table */
+.data-table {
+ width: 100%;
+ border-collapse: collapse;
+ background: #fff;
+ border-radius: 10px;
+ overflow: hidden;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.08);
+}
+
+.data-table th,
+.data-table td {
+ padding: 12px 16px;
+ text-align: left;
+ border-bottom: 1px solid #f0f0f0;
+}
+
+.data-table th {
+ background: #fafafa;
+ font-size: 0.85rem;
+ font-weight: 600;
+ color: #666;
+ text-transform: uppercase;
+ letter-spacing: 0.03em;
+}
+
+.data-table tr:last-child td { border-bottom: none; }
+
+.empty-row {
+ text-align: center !important;
+ color: #999;
+ padding: 24px !important;
+}
+
+/* Badges */
+.status-badge, .stage-badge {
+ display: inline-block;
+ padding: 3px 10px;
+ border-radius: 12px;
+ font-size: 0.8rem;
+ font-weight: 600;
+ text-transform: capitalize;
+}
+
+.status-active { background: #dcfce7; color: #166534; }
+.status-lead { background: #dbeafe; color: #1e40af; }
+.status-inactive { background: #f3f4f6; color: #6b7280; }
+
+.stage-qualified { background: #dbeafe; color: #1e40af; }
+.stage-proposal { background: #fef3c7; color: #92400e; }
+.stage-negotiation { background: #fce7f3; color: #9d174d; }
+.stage-closed-won { background: #dcfce7; color: #166534; }
+.stage-closed-lost { background: #fee2e2; color: #991b1b; }
+
+/* Search */
+.search-input {
+ width: 100%;
+ max-width: 400px;
+ padding: 10px 16px;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ font-size: 0.95rem;
+ margin-bottom: 16px;
+ outline: none;
+}
+
+.search-input:focus { border-color: #6366f1; box-shadow: 0 0 0 3px rgba(99,102,241,0.1); }
+
+/* Forms */
+.form-card {
+ background: #fff;
+ border-radius: 10px;
+ padding: 24px;
+ margin-bottom: 20px;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.08);
+}
+
+.form-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
+ gap: 16px;
+ margin: 16px 0;
+}
+
+.form-grid label {
+ display: flex;
+ flex-direction: column;
+ font-size: 0.85rem;
+ font-weight: 600;
+ color: #555;
+}
+
+.form-grid input,
+.form-grid select {
+ margin-top: 4px;
+ padding: 8px 12px;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ font-size: 0.95rem;
+}
+
+.form-grid input:focus,
+.form-grid select:focus {
+ outline: none;
+ border-color: #6366f1;
+}
+
+.form-actions {
+ display: flex;
+ gap: 8px;
+}
+
+/* Buttons */
+.btn {
+ padding: 8px 16px;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ background: #fff;
+ cursor: pointer;
+ font-size: 0.9rem;
+ transition: background 0.15s;
+}
+
+.btn:hover { background: #f5f5f5; }
+
+.btn-primary {
+ background: #6366f1;
+ color: #fff;
+ border-color: #6366f1;
+}
+
+.btn-primary:hover { background: #4f46e5; }
+
+.btn-danger { color: #dc2626; border-color: #fecaca; }
+.btn-danger:hover { background: #fee2e2; }
+
+.btn-sm { padding: 4px 10px; font-size: 0.82rem; margin-right: 4px; }
diff --git a/demos/crm-app/src/main.jsx b/demos/crm-app/src/main.jsx
new file mode 100644
index 000000000..79df8e02b
--- /dev/null
+++ b/demos/crm-app/src/main.jsx
@@ -0,0 +1,16 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import { BrowserRouter } from 'react-router-dom'
+import App from './App'
+import { CRMProvider } from './context/CRMContext'
+import './index.css'
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+
+
+
+
+ ,
+)
diff --git a/demos/crm-app/src/pages/Contacts.jsx b/demos/crm-app/src/pages/Contacts.jsx
new file mode 100644
index 000000000..6ad579463
--- /dev/null
+++ b/demos/crm-app/src/pages/Contacts.jsx
@@ -0,0 +1,100 @@
+import { useState } from 'react'
+import { useCRM } from '../context/CRMContext'
+import ContactForm from '../components/ContactForm'
+
+function Contacts() {
+ const { state, dispatch } = useCRM()
+ const [search, setSearch] = useState('')
+ const [showForm, setShowForm] = useState(false)
+ const [editingContact, setEditingContact] = useState(null)
+
+ const filtered = state.contacts.filter(c =>
+ c.name.toLowerCase().includes(search.toLowerCase()) ||
+ c.email.toLowerCase().includes(search.toLowerCase()) ||
+ c.company.toLowerCase().includes(search.toLowerCase())
+ )
+
+ function handleSave(contact) {
+ if (editingContact) {
+ dispatch({ type: 'UPDATE_CONTACT', payload: { ...contact, id: editingContact.id } })
+ } else {
+ dispatch({ type: 'ADD_CONTACT', payload: contact })
+ }
+ setShowForm(false)
+ setEditingContact(null)
+ }
+
+ function handleEdit(contact) {
+ setEditingContact(contact)
+ setShowForm(true)
+ }
+
+ function handleDelete(id) {
+ dispatch({ type: 'DELETE_CONTACT', payload: id })
+ }
+
+ function handleCancel() {
+ setShowForm(false)
+ setEditingContact(null)
+ }
+
+ return (
+
+
+
Contacts
+ setShowForm(true)}>
+ Add Contact
+
+
+
+ {showForm && (
+
+ )}
+
+
setSearch(e.target.value)}
+ />
+
+
+
+
+ Name
+ Email
+ Phone
+ Company
+ Status
+ Actions
+
+
+
+ {filtered.map(contact => (
+
+ {contact.name}
+ {contact.email}
+ {contact.phone}
+ {contact.company}
+ {contact.status}
+
+ handleEdit(contact)}>Edit
+ handleDelete(contact.id)}>Delete
+
+
+ ))}
+ {filtered.length === 0 && (
+ No contacts found
+ )}
+
+
+
+ )
+}
+
+export default Contacts
diff --git a/demos/crm-app/src/pages/Dashboard.jsx b/demos/crm-app/src/pages/Dashboard.jsx
new file mode 100644
index 000000000..a5321557d
--- /dev/null
+++ b/demos/crm-app/src/pages/Dashboard.jsx
@@ -0,0 +1,73 @@
+import { useCRM } from '../context/CRMContext'
+
+const STAGES = ['qualified', 'proposal', 'negotiation', 'closed-won', 'closed-lost']
+
+function Dashboard() {
+ const { state } = useCRM()
+ const { contacts, deals } = state
+
+ const totalValue = deals.reduce((sum, d) => sum + d.value, 0)
+ const wonDeals = deals.filter(d => d.stage === 'closed-won')
+ const wonValue = wonDeals.reduce((sum, d) => sum + d.value, 0)
+ const activeContacts = contacts.filter(c => c.status === 'active').length
+ const leads = contacts.filter(c => c.status === 'lead').length
+
+ const dealsByStage = STAGES.reduce((acc, stage) => {
+ acc[stage] = deals.filter(d => d.stage === stage)
+ return acc
+ }, {})
+
+ return (
+
+
Dashboard
+
+
+ Total Contacts
+ {contacts.length}
+
+
+ Active Contacts
+ {activeContacts}
+
+
+ Leads
+ {leads}
+
+
+ Total Deals
+ {deals.length}
+
+
+ Pipeline Value
+ ${totalValue.toLocaleString()}
+
+
+ Won Value
+ ${wonValue.toLocaleString()}
+
+
+
+
Deal Pipeline
+
+ {STAGES.map(stage => (
+
+
{stage.replace('-', ' ')}
+
{dealsByStage[stage].length}
+ {dealsByStage[stage].map(deal => {
+ const contact = contacts.find(c => c.id === deal.contactId)
+ return (
+
+ {deal.title}
+ ${deal.value.toLocaleString()}
+ {contact?.name || 'Unknown'}
+
+ )
+ })}
+
+ ))}
+
+
+ )
+}
+
+export default Dashboard
diff --git a/demos/crm-app/src/pages/Deals.jsx b/demos/crm-app/src/pages/Deals.jsx
new file mode 100644
index 000000000..c4058ebf5
--- /dev/null
+++ b/demos/crm-app/src/pages/Deals.jsx
@@ -0,0 +1,91 @@
+import { useState } from 'react'
+import { useCRM } from '../context/CRMContext'
+import DealForm from '../components/DealForm'
+
+const STAGES = ['qualified', 'proposal', 'negotiation', 'closed-won', 'closed-lost']
+
+function Deals() {
+ const { state, dispatch } = useCRM()
+ const [showForm, setShowForm] = useState(false)
+ const [editingDeal, setEditingDeal] = useState(null)
+
+ function handleSave(deal) {
+ if (editingDeal) {
+ dispatch({ type: 'UPDATE_DEAL', payload: { ...deal, id: editingDeal.id } })
+ } else {
+ dispatch({ type: 'ADD_DEAL', payload: deal })
+ }
+ setShowForm(false)
+ setEditingDeal(null)
+ }
+
+ function handleEdit(deal) {
+ setEditingDeal(deal)
+ setShowForm(true)
+ }
+
+ function handleDelete(id) {
+ dispatch({ type: 'DELETE_DEAL', payload: id })
+ }
+
+ function handleCancel() {
+ setShowForm(false)
+ setEditingDeal(null)
+ }
+
+ return (
+
+
+
Deals
+ setShowForm(true)}>
+ Add Deal
+
+
+
+ {showForm && (
+
+ )}
+
+
+
+
+ Title
+ Contact
+ Value
+ Stage
+ Created
+ Actions
+
+
+
+ {state.deals.map(deal => {
+ const contact = state.contacts.find(c => c.id === deal.contactId)
+ return (
+
+ {deal.title}
+ {contact?.name || 'Unknown'}
+ ${deal.value.toLocaleString()}
+ {deal.stage.replace('-', ' ')}
+ {deal.createdAt}
+
+ handleEdit(deal)}>Edit
+ handleDelete(deal.id)}>Delete
+
+
+ )
+ })}
+ {state.deals.length === 0 && (
+ No deals found
+ )}
+
+
+
+ )
+}
+
+export default Deals
diff --git a/demos/crm-app/src/setupTests.js b/demos/crm-app/src/setupTests.js
new file mode 100644
index 000000000..c44951a68
--- /dev/null
+++ b/demos/crm-app/src/setupTests.js
@@ -0,0 +1 @@
+import '@testing-library/jest-dom'
diff --git a/demos/crm-app/vite.config.js b/demos/crm-app/vite.config.js
new file mode 100644
index 000000000..3b1ab3c8d
--- /dev/null
+++ b/demos/crm-app/vite.config.js
@@ -0,0 +1,11 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()],
+ test: {
+ globals: true,
+ environment: 'jsdom',
+ setupFiles: './src/setupTests.js',
+ },
+})
diff --git a/demos/linkedin-app/index.html b/demos/linkedin-app/index.html
new file mode 100644
index 000000000..e1de56070
--- /dev/null
+++ b/demos/linkedin-app/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ LinkedIn App
+
+
+
+
+
+
diff --git a/demos/linkedin-app/package-lock.json b/demos/linkedin-app/package-lock.json
new file mode 100644
index 000000000..9eddb901b
--- /dev/null
+++ b/demos/linkedin-app/package-lock.json
@@ -0,0 +1,1836 @@
+{
+ "name": "linkedin-app",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "linkedin-app",
+ "version": "1.0.0",
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^6.28.0"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-react": "^4.3.4",
+ "vite": "^6.0.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@remix-run/router": {
+ "version": "1.23.2",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
+ "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz",
+ "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz",
+ "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz",
+ "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz",
+ "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz",
+ "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz",
+ "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz",
+ "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz",
+ "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz",
+ "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz",
+ "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz",
+ "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz",
+ "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz",
+ "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz",
+ "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz",
+ "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz",
+ "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz",
+ "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz",
+ "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz",
+ "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz",
+ "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz",
+ "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz",
+ "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz",
+ "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz",
+ "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz",
+ "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.20",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz",
+ "integrity": "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001788",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz",
+ "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.343",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.343.tgz",
+ "integrity": "sha512-YHnQ3MXI08icvL9ZKnEBy05F2EQ8ob01UaMOuMbM8l+4UcAq6MPPbBTJBbsBUg3H8JeZNt+O4fjsoWth3p6IFg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.37",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
+ "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.10",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
+ "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "6.30.3",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
+ "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.30.3",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
+ "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.2",
+ "react-router": "6.30.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz",
+ "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.60.2",
+ "@rollup/rollup-android-arm64": "4.60.2",
+ "@rollup/rollup-darwin-arm64": "4.60.2",
+ "@rollup/rollup-darwin-x64": "4.60.2",
+ "@rollup/rollup-freebsd-arm64": "4.60.2",
+ "@rollup/rollup-freebsd-x64": "4.60.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.2",
+ "@rollup/rollup-linux-arm64-musl": "4.60.2",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.2",
+ "@rollup/rollup-linux-loong64-musl": "4.60.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.2",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.2",
+ "@rollup/rollup-linux-x64-gnu": "4.60.2",
+ "@rollup/rollup-linux-x64-musl": "4.60.2",
+ "@rollup/rollup-openbsd-x64": "4.60.2",
+ "@rollup/rollup-openharmony-arm64": "4.60.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.2",
+ "@rollup/rollup-win32-x64-gnu": "4.60.2",
+ "@rollup/rollup-win32-x64-msvc": "4.60.2",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.4.2",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
+ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "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
+ }
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ }
+ }
+}
diff --git a/demos/linkedin-app/package.json b/demos/linkedin-app/package.json
new file mode 100644
index 000000000..70b415248
--- /dev/null
+++ b/demos/linkedin-app/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "linkedin-app",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^6.28.0"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-react": "^4.3.4",
+ "vite": "^6.0.0"
+ }
+}
diff --git a/demos/linkedin-app/src/App.jsx b/demos/linkedin-app/src/App.jsx
new file mode 100644
index 000000000..11b01d800
--- /dev/null
+++ b/demos/linkedin-app/src/App.jsx
@@ -0,0 +1,26 @@
+import { Routes, Route } from 'react-router-dom'
+import Navbar from './components/Navbar'
+import Feed from './pages/Feed'
+import Profile from './pages/Profile'
+import Network from './pages/Network'
+import Jobs from './pages/Jobs'
+import Messaging from './pages/Messaging'
+
+function App() {
+ return (
+
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+ )
+}
+
+export default App
diff --git a/demos/linkedin-app/src/components/ConnectionCard.jsx b/demos/linkedin-app/src/components/ConnectionCard.jsx
new file mode 100644
index 000000000..1b29187c7
--- /dev/null
+++ b/demos/linkedin-app/src/components/ConnectionCard.jsx
@@ -0,0 +1,23 @@
+import { useLinkedIn } from '../context/LinkedInContext'
+
+function ConnectionCard({ person }) {
+ const { connectedIds, toggleConnect } = useLinkedIn()
+ const isConnected = connectedIds.has(person.id)
+
+ return (
+
+
+
{person.name}
+
{person.headline}
+
{person.mutual} mutual connections
+
toggleConnect(person.id)}
+ >
+ {isConnected ? 'Connected' : '+ Connect'}
+
+
+ )
+}
+
+export default ConnectionCard
diff --git a/demos/linkedin-app/src/components/CreatePost.jsx b/demos/linkedin-app/src/components/CreatePost.jsx
new file mode 100644
index 000000000..eaae3d0d0
--- /dev/null
+++ b/demos/linkedin-app/src/components/CreatePost.jsx
@@ -0,0 +1,49 @@
+import { useState } from 'react'
+import { useLinkedIn } from '../context/LinkedInContext'
+
+function CreatePost() {
+ const { currentUser, addPost } = useLinkedIn()
+ const [content, setContent] = useState('')
+ const [isExpanded, setIsExpanded] = useState(false)
+
+ function handleSubmit(e) {
+ e.preventDefault()
+ if (content.trim()) {
+ addPost(content.trim())
+ setContent('')
+ setIsExpanded(false)
+ }
+ }
+
+ return (
+
+
+
+
setIsExpanded(true)}>
+ Start a post
+
+
+ {isExpanded && (
+
+ )}
+
+ )
+}
+
+export default CreatePost
diff --git a/demos/linkedin-app/src/components/JobCard.jsx b/demos/linkedin-app/src/components/JobCard.jsx
new file mode 100644
index 000000000..35b04d23f
--- /dev/null
+++ b/demos/linkedin-app/src/components/JobCard.jsx
@@ -0,0 +1,33 @@
+import { useLinkedIn } from '../context/LinkedInContext'
+
+function JobCard({ job }) {
+ const { savedJobIds, toggleSaveJob } = useLinkedIn()
+ const isSaved = savedJobIds.has(job.id)
+
+ return (
+
+
+
+
{job.title}
+
{job.company}
+
{job.location}
+
{job.salary}
+
+ {job.posted}
+ {job.applicants} applicants
+
+
+
toggleSaveJob(job.id)}
+ title={isSaved ? 'Unsave job' : 'Save job'}
+ >
+
+
+
+
+
+ )
+}
+
+export default JobCard
diff --git a/demos/linkedin-app/src/components/Navbar.jsx b/demos/linkedin-app/src/components/Navbar.jsx
new file mode 100644
index 000000000..ba8324262
--- /dev/null
+++ b/demos/linkedin-app/src/components/Navbar.jsx
@@ -0,0 +1,56 @@
+import { NavLink } from 'react-router-dom'
+import { useLinkedIn } from '../context/LinkedInContext'
+
+function Navbar() {
+ const { currentUser, messages } = useLinkedIn()
+ const unreadCount = messages.filter(m => m.unread).length
+
+ return (
+
+
+
+
+
+
+
+
+ Home
+
+
+
+
+
+ Network
+
+
+
+
+
+ Jobs
+
+
+
+
+
+ Messaging
+ {unreadCount > 0 && {unreadCount} }
+
+
+
+ Me
+
+
+
+
+ )
+}
+
+export default Navbar
diff --git a/demos/linkedin-app/src/components/PostCard.jsx b/demos/linkedin-app/src/components/PostCard.jsx
new file mode 100644
index 000000000..c6549ed2c
--- /dev/null
+++ b/demos/linkedin-app/src/components/PostCard.jsx
@@ -0,0 +1,56 @@
+import { useLinkedIn } from '../context/LinkedInContext'
+
+function PostCard({ post }) {
+ const { toggleLike } = useLinkedIn()
+
+ return (
+
+
+
+
+
{post.author.name}
+
{post.author.headline}
+
{post.timestamp}
+
+
+
+
+ {post.likes > 0 && `${post.likes} likes`}
+
+ {post.comments > 0 && `${post.comments} comments`}
+ {post.reposts > 0 && ` \u00b7 ${post.reposts} reposts`}
+
+
+
+
toggleLike(post.id)}>
+
+
+
+ Like
+
+
+
+
+
+ Comment
+
+
+
+
+
+ Repost
+
+
+
+
+
+ Send
+
+
+
+ )
+}
+
+export default PostCard
diff --git a/demos/linkedin-app/src/components/ProfileCard.jsx b/demos/linkedin-app/src/components/ProfileCard.jsx
new file mode 100644
index 000000000..b3d30f4a7
--- /dev/null
+++ b/demos/linkedin-app/src/components/ProfileCard.jsx
@@ -0,0 +1,31 @@
+import { Link } from 'react-router-dom'
+import { useLinkedIn } from '../context/LinkedInContext'
+
+function ProfileCard() {
+ const { currentUser } = useLinkedIn()
+
+ return (
+
+
+
+
+
+
+
{currentUser.name}
+
{currentUser.headline}
+
+
+
+ Connections
+ {currentUser.connections}
+
+
+ Profile views
+ 124
+
+
+
+ )
+}
+
+export default ProfileCard
diff --git a/demos/linkedin-app/src/context/LinkedInContext.jsx b/demos/linkedin-app/src/context/LinkedInContext.jsx
new file mode 100644
index 000000000..3b4939845
--- /dev/null
+++ b/demos/linkedin-app/src/context/LinkedInContext.jsx
@@ -0,0 +1,183 @@
+import { createContext, useContext, useState } from 'react'
+
+const LinkedInContext = createContext()
+
+const initialCurrentUser = {
+ id: 'user-1',
+ name: 'Alex Johnson',
+ headline: 'Senior Software Engineer at TechCorp',
+ avatar: 'https://i.pravatar.cc/150?u=alex',
+ banner: 'https://picsum.photos/seed/alex-banner/800/200',
+ location: 'San Francisco, CA',
+ connections: 487,
+ about: 'Passionate software engineer with 8+ years of experience building scalable web applications. Skilled in React, Node.js, and cloud infrastructure.',
+ experience: [
+ { title: 'Senior Software Engineer', company: 'TechCorp', duration: '2022 - Present', logo: 'https://i.pravatar.cc/40?u=techcorp' },
+ { title: 'Software Engineer', company: 'StartupXYZ', duration: '2019 - 2022', logo: 'https://i.pravatar.cc/40?u=startupxyz' },
+ { title: 'Junior Developer', company: 'WebAgency', duration: '2017 - 2019', logo: 'https://i.pravatar.cc/40?u=webagency' },
+ ],
+}
+
+const initialPeople = [
+ { id: 'user-2', name: 'Sarah Chen', headline: 'Product Manager at InnovateCo', avatar: 'https://i.pravatar.cc/150?u=sarah', mutual: 12 },
+ { id: 'user-3', name: 'Marcus Williams', headline: 'UX Designer at DesignHub', avatar: 'https://i.pravatar.cc/150?u=marcus', mutual: 8 },
+ { id: 'user-4', name: 'Priya Patel', headline: 'Data Scientist at DataFlow', avatar: 'https://i.pravatar.cc/150?u=priya', mutual: 23 },
+ { id: 'user-5', name: 'James O\'Brien', headline: 'Engineering Manager at CloudScale', avatar: 'https://i.pravatar.cc/150?u=james', mutual: 5 },
+ { id: 'user-6', name: 'Emily Zhang', headline: 'Frontend Developer at PixelPerfect', avatar: 'https://i.pravatar.cc/150?u=emily', mutual: 15 },
+ { id: 'user-7', name: 'David Kim', headline: 'CTO at NextGen Startup', avatar: 'https://i.pravatar.cc/150?u=david', mutual: 31 },
+ { id: 'user-8', name: 'Rachel Green', headline: 'HR Director at PeopleFirst', avatar: 'https://i.pravatar.cc/150?u=rachel', mutual: 3 },
+]
+
+const initialPosts = [
+ {
+ id: 'post-1',
+ author: { name: 'Sarah Chen', headline: 'Product Manager at InnovateCo', avatar: 'https://i.pravatar.cc/150?u=sarah' },
+ content: 'Excited to announce that our team just shipped a major product update! After 6 months of hard work, we\'ve completely redesigned the onboarding experience. Early metrics show a 40% improvement in user activation. So proud of this team!',
+ timestamp: '2h ago',
+ likes: 142,
+ comments: 28,
+ reposts: 12,
+ liked: false,
+ },
+ {
+ id: 'post-2',
+ author: { name: 'Marcus Williams', headline: 'UX Designer at DesignHub', avatar: 'https://i.pravatar.cc/150?u=marcus' },
+ content: 'Hot take: The best design systems are the ones nobody notices. If users are thinking about your UI instead of their task, something went wrong.\n\nWhat are your thoughts on invisible design? Would love to hear different perspectives.',
+ timestamp: '5h ago',
+ likes: 89,
+ comments: 45,
+ reposts: 7,
+ liked: false,
+ },
+ {
+ id: 'post-3',
+ author: { name: 'Priya Patel', headline: 'Data Scientist at DataFlow', avatar: 'https://i.pravatar.cc/150?u=priya' },
+ content: 'Just published my latest article on implementing RAG (Retrieval-Augmented Generation) pipelines in production. Key takeaways:\n\n1. Chunking strategy matters more than model choice\n2. Hybrid search outperforms pure vector search\n3. Evaluation frameworks are non-negotiable\n\nLink in comments!',
+ timestamp: '8h ago',
+ likes: 256,
+ comments: 67,
+ reposts: 34,
+ liked: false,
+ },
+ {
+ id: 'post-4',
+ author: { name: 'David Kim', headline: 'CTO at NextGen Startup', avatar: 'https://i.pravatar.cc/150?u=david' },
+ content: 'We\'re hiring! Looking for talented engineers who want to work on cutting-edge distributed systems. Fully remote, competitive comp, and a team that genuinely cares about engineering excellence.\n\nDM me or check out our careers page.',
+ timestamp: '1d ago',
+ likes: 312,
+ comments: 89,
+ reposts: 56,
+ liked: false,
+ },
+ {
+ id: 'post-5',
+ author: { name: 'Emily Zhang', headline: 'Frontend Developer at PixelPerfect', avatar: 'https://i.pravatar.cc/150?u=emily' },
+ content: 'TIL: React Server Components and Server Actions have completely changed how I think about data fetching. The mental model shift is real but worth it. No more useEffect waterfalls!\n\nAnyone else making the transition?',
+ timestamp: '1d ago',
+ likes: 178,
+ comments: 52,
+ reposts: 19,
+ liked: false,
+ },
+]
+
+const initialJobs = [
+ { id: 'job-1', title: 'Senior Frontend Engineer', company: 'TechCorp', location: 'San Francisco, CA (Hybrid)', salary: '$160k - $200k', posted: '2 days ago', logo: 'https://i.pravatar.cc/40?u=techcorp', applicants: 47 },
+ { id: 'job-2', title: 'Full Stack Developer', company: 'StartupXYZ', location: 'Remote', salary: '$130k - $170k', posted: '1 week ago', logo: 'https://i.pravatar.cc/40?u=startupxyz', applicants: 128 },
+ { id: 'job-3', title: 'React Developer', company: 'PixelPerfect', location: 'New York, NY', salary: '$140k - $180k', posted: '3 days ago', logo: 'https://i.pravatar.cc/40?u=pixelperfect', applicants: 63 },
+ { id: 'job-4', title: 'Staff Engineer', company: 'CloudScale', location: 'Seattle, WA (On-site)', salary: '$190k - $240k', posted: '5 days ago', logo: 'https://i.pravatar.cc/40?u=cloudscale', applicants: 92 },
+ { id: 'job-5', title: 'Engineering Manager', company: 'DataFlow', location: 'Remote', salary: '$180k - $220k', posted: '1 day ago', logo: 'https://i.pravatar.cc/40?u=dataflow', applicants: 34 },
+ { id: 'job-6', title: 'Junior Software Engineer', company: 'InnovateCo', location: 'Austin, TX (Hybrid)', salary: '$90k - $120k', posted: '4 days ago', logo: 'https://i.pravatar.cc/40?u=innovateco', applicants: 215 },
+]
+
+const initialMessages = [
+ { id: 'msg-1', contact: { name: 'Sarah Chen', avatar: 'https://i.pravatar.cc/150?u=sarah' }, lastMessage: 'Thanks for the referral! I really appreciate it.', timestamp: '10:30 AM', unread: true },
+ { id: 'msg-2', contact: { name: 'David Kim', avatar: 'https://i.pravatar.cc/150?u=david' }, lastMessage: 'Would love to chat about the role. Are you free this week?', timestamp: 'Yesterday', unread: true },
+ { id: 'msg-3', contact: { name: 'Emily Zhang', avatar: 'https://i.pravatar.cc/150?u=emily' }, lastMessage: 'That React conf talk was amazing!', timestamp: 'Monday', unread: false },
+ { id: 'msg-4', contact: { name: 'James O\'Brien', avatar: 'https://i.pravatar.cc/150?u=james' }, lastMessage: 'Let me know if you need anything else for the project.', timestamp: 'Apr 15', unread: false },
+]
+
+export function LinkedInProvider({ children }) {
+ const [currentUser] = useState(initialCurrentUser)
+ const [posts, setPosts] = useState(initialPosts)
+ const [people] = useState(initialPeople)
+ const [jobs] = useState(initialJobs)
+ const [messages] = useState(initialMessages)
+ const [connectedIds, setConnectedIds] = useState(new Set())
+ const [savedJobIds, setSavedJobIds] = useState(new Set())
+
+ function addPost(content) {
+ const newPost = {
+ id: `post-${Date.now()}`,
+ author: { name: currentUser.name, headline: currentUser.headline, avatar: currentUser.avatar },
+ content,
+ timestamp: 'Just now',
+ likes: 0,
+ comments: 0,
+ reposts: 0,
+ liked: false,
+ }
+ setPosts([newPost, ...posts])
+ }
+
+ function toggleLike(postId) {
+ setPosts(posts.map(post => {
+ if (post.id === postId) {
+ return { ...post, liked: !post.liked, likes: post.liked ? post.likes - 1 : post.likes + 1 }
+ }
+ return post
+ }))
+ }
+
+ function toggleConnect(personId) {
+ setConnectedIds(prev => {
+ const next = new Set(prev)
+ if (next.has(personId)) {
+ next.delete(personId)
+ } else {
+ next.add(personId)
+ }
+ return next
+ })
+ }
+
+ function toggleSaveJob(jobId) {
+ setSavedJobIds(prev => {
+ const next = new Set(prev)
+ if (next.has(jobId)) {
+ next.delete(jobId)
+ } else {
+ next.add(jobId)
+ }
+ return next
+ })
+ }
+
+ const value = {
+ currentUser,
+ posts,
+ people,
+ jobs,
+ messages,
+ connectedIds,
+ savedJobIds,
+ addPost,
+ toggleLike,
+ toggleConnect,
+ toggleSaveJob,
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+export function useLinkedIn() {
+ const context = useContext(LinkedInContext)
+ if (!context) {
+ throw new Error('useLinkedIn must be used within a LinkedInProvider')
+ }
+ return context
+}
diff --git a/demos/linkedin-app/src/index.css b/demos/linkedin-app/src/index.css
new file mode 100644
index 000000000..a1de91fa7
--- /dev/null
+++ b/demos/linkedin-app/src/index.css
@@ -0,0 +1,954 @@
+/* Reset & Base */
+*,
+*::before,
+*::after {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+:root {
+ --linkedin-blue: #0a66c2;
+ --linkedin-blue-hover: #004182;
+ --linkedin-green: #057642;
+ --bg-color: #f4f2ee;
+ --card-bg: #ffffff;
+ --text-primary: #191919;
+ --text-secondary: #666666;
+ --text-muted: #00000099;
+ --border-color: #e0e0e0;
+ --nav-height: 52px;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
+ background-color: var(--bg-color);
+ color: var(--text-primary);
+ line-height: 1.5;
+}
+
+a {
+ text-decoration: none;
+ color: inherit;
+}
+
+button {
+ cursor: pointer;
+ border: none;
+ background: none;
+ font-family: inherit;
+}
+
+/* Navbar */
+.navbar {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: var(--nav-height);
+ background-color: var(--card-bg);
+ border-bottom: 1px solid var(--border-color);
+ z-index: 100;
+}
+
+.navbar-inner {
+ max-width: 1128px;
+ margin: 0 auto;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 16px;
+}
+
+.navbar-left {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.navbar-logo {
+ background-color: var(--linkedin-blue);
+ color: white;
+ font-weight: 700;
+ font-size: 24px;
+ width: 34px;
+ height: 34px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 4px;
+}
+
+.navbar-search {
+ display: flex;
+ align-items: center;
+ background-color: #eef3f8;
+ border-radius: 4px;
+ padding: 0 8px;
+ gap: 8px;
+}
+
+.navbar-search input {
+ border: none;
+ background: none;
+ padding: 8px 4px;
+ font-size: 14px;
+ width: 200px;
+ outline: none;
+}
+
+.navbar-search svg {
+ color: var(--text-secondary);
+}
+
+.navbar-links {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.nav-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 4px 12px;
+ color: var(--text-secondary);
+ font-size: 12px;
+ position: relative;
+ border-bottom: 2px solid transparent;
+ transition: color 0.15s;
+}
+
+.nav-item:hover {
+ color: var(--text-primary);
+}
+
+.nav-item.active {
+ color: var(--text-primary);
+ border-bottom-color: var(--text-primary);
+}
+
+.nav-avatar {
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ object-fit: cover;
+}
+
+.badge {
+ position: absolute;
+ top: 0;
+ right: 4px;
+ background-color: #cc1016;
+ color: white;
+ font-size: 10px;
+ font-weight: 600;
+ min-width: 16px;
+ height: 16px;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0 4px;
+}
+
+/* Main Content */
+.main-content {
+ max-width: 1128px;
+ margin: 0 auto;
+ padding-top: calc(var(--nav-height) + 24px);
+ padding-left: 16px;
+ padding-right: 16px;
+ padding-bottom: 40px;
+}
+
+/* Feed Layout */
+.feed-layout {
+ display: grid;
+ grid-template-columns: 225px 1fr 300px;
+ gap: 24px;
+}
+
+/* Profile Card (Sidebar) */
+.profile-card {
+ background: var(--card-bg);
+ border-radius: 8px;
+ border: 1px solid var(--border-color);
+ overflow: hidden;
+}
+
+.profile-card-banner {
+ height: 56px;
+ background-size: cover;
+ background-position: center;
+ background-color: var(--linkedin-blue);
+}
+
+.profile-card-avatar-link {
+ display: block;
+ margin: -36px auto 0;
+ width: 72px;
+ text-align: center;
+}
+
+.profile-card-avatar {
+ width: 72px;
+ height: 72px;
+ border-radius: 50%;
+ border: 2px solid var(--card-bg);
+ object-fit: cover;
+}
+
+.profile-card-info {
+ text-align: center;
+ padding: 8px 16px 12px;
+}
+
+.profile-card-name {
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.profile-card-name:hover {
+ text-decoration: underline;
+ color: var(--linkedin-blue);
+}
+
+.profile-card-headline {
+ font-size: 12px;
+ color: var(--text-secondary);
+ margin-top: 4px;
+}
+
+.profile-card-stats {
+ border-top: 1px solid var(--border-color);
+ padding: 12px 16px;
+}
+
+.profile-stat {
+ display: flex;
+ justify-content: space-between;
+ font-size: 12px;
+ margin-bottom: 4px;
+}
+
+.profile-stat-label {
+ color: var(--text-secondary);
+}
+
+.profile-stat-value {
+ color: var(--linkedin-blue);
+ font-weight: 600;
+}
+
+/* Create Post */
+.create-post {
+ background: var(--card-bg);
+ border-radius: 8px;
+ border: 1px solid var(--border-color);
+ padding: 16px;
+ margin-bottom: 8px;
+}
+
+.create-post-top {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.create-post-avatar {
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ object-fit: cover;
+}
+
+.create-post-input {
+ flex: 1;
+ padding: 12px 16px;
+ border: 1px solid var(--border-color);
+ border-radius: 35px;
+ font-size: 14px;
+ color: var(--text-secondary);
+ text-align: left;
+ transition: background 0.15s;
+}
+
+.create-post-input:hover {
+ background: #f3f2ef;
+}
+
+.create-post-form {
+ margin-top: 12px;
+}
+
+.create-post-form textarea {
+ width: 100%;
+ padding: 12px;
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ font-size: 14px;
+ font-family: inherit;
+ resize: vertical;
+ outline: none;
+}
+
+.create-post-form textarea:focus {
+ border-color: var(--linkedin-blue);
+}
+
+.create-post-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+ margin-top: 8px;
+}
+
+.create-post-cancel {
+ padding: 6px 16px;
+ border-radius: 16px;
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--text-secondary);
+}
+
+.create-post-cancel:hover {
+ background: #f3f2ef;
+}
+
+.create-post-submit {
+ padding: 6px 16px;
+ border-radius: 16px;
+ font-size: 14px;
+ font-weight: 600;
+ background-color: var(--linkedin-blue);
+ color: white;
+}
+
+.create-post-submit:hover {
+ background-color: var(--linkedin-blue-hover);
+}
+
+.create-post-submit:disabled {
+ background-color: #e0e0e0;
+ color: #999;
+ cursor: not-allowed;
+}
+
+/* Post Card */
+.post-card {
+ background: var(--card-bg);
+ border-radius: 8px;
+ border: 1px solid var(--border-color);
+ margin-bottom: 8px;
+ padding: 16px;
+}
+
+.post-header {
+ display: flex;
+ gap: 12px;
+ margin-bottom: 12px;
+}
+
+.post-avatar {
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ object-fit: cover;
+}
+
+.post-author-name {
+ font-size: 14px;
+ font-weight: 600;
+}
+
+.post-author-headline {
+ font-size: 12px;
+ color: var(--text-secondary);
+}
+
+.post-timestamp {
+ font-size: 12px;
+ color: var(--text-muted);
+}
+
+.post-content p {
+ font-size: 14px;
+ line-height: 1.6;
+ white-space: pre-wrap;
+}
+
+.post-stats {
+ display: flex;
+ justify-content: space-between;
+ padding: 8px 0;
+ margin-top: 8px;
+ font-size: 12px;
+ color: var(--text-secondary);
+ border-bottom: 1px solid var(--border-color);
+}
+
+.post-actions {
+ display: flex;
+ justify-content: space-around;
+ padding-top: 4px;
+}
+
+.post-action-btn {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 10px 8px;
+ border-radius: 4px;
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--text-secondary);
+ transition: background 0.15s, color 0.15s;
+}
+
+.post-action-btn:hover {
+ background: #f3f2ef;
+ color: var(--text-primary);
+}
+
+.post-action-btn.liked {
+ color: var(--linkedin-blue);
+}
+
+/* Trending / News sidebar */
+.trending-card {
+ background: var(--card-bg);
+ border-radius: 8px;
+ border: 1px solid var(--border-color);
+ padding: 16px;
+}
+
+.trending-card h3 {
+ font-size: 16px;
+ margin-bottom: 12px;
+}
+
+.trending-list {
+ list-style: none;
+}
+
+.trending-list li {
+ padding: 8px 0;
+ border-bottom: 1px solid #f3f2ef;
+}
+
+.trending-list li:last-child {
+ border-bottom: none;
+}
+
+.trending-list h4 {
+ font-size: 14px;
+ font-weight: 600;
+ margin-bottom: 2px;
+}
+
+.trending-list span {
+ font-size: 12px;
+ color: var(--text-secondary);
+}
+
+/* Network Page */
+.network-page {
+ max-width: 900px;
+ margin: 0 auto;
+}
+
+.network-header {
+ margin-bottom: 24px;
+}
+
+.network-header h2 {
+ font-size: 20px;
+ margin-bottom: 4px;
+}
+
+.network-header p {
+ color: var(--text-secondary);
+ font-size: 14px;
+}
+
+.network-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+ gap: 16px;
+}
+
+/* Connection Card */
+.connection-card {
+ background: var(--card-bg);
+ border-radius: 8px;
+ border: 1px solid var(--border-color);
+ padding: 24px 16px;
+ text-align: center;
+}
+
+.connection-avatar {
+ width: 72px;
+ height: 72px;
+ border-radius: 50%;
+ object-fit: cover;
+ margin-bottom: 12px;
+}
+
+.connection-name {
+ font-size: 14px;
+ font-weight: 600;
+ margin-bottom: 4px;
+}
+
+.connection-headline {
+ font-size: 12px;
+ color: var(--text-secondary);
+ margin-bottom: 8px;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+.connection-mutual {
+ font-size: 12px;
+ color: var(--text-muted);
+ margin-bottom: 16px;
+}
+
+.connect-btn {
+ padding: 6px 16px;
+ border: 1.5px solid var(--linkedin-blue);
+ border-radius: 16px;
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--linkedin-blue);
+ background: transparent;
+ transition: background 0.15s, border-color 0.15s, color 0.15s;
+}
+
+.connect-btn:hover {
+ background: rgba(10, 102, 194, 0.08);
+ border-color: var(--linkedin-blue-hover);
+}
+
+.connect-btn.connected {
+ background: var(--linkedin-blue);
+ color: white;
+ border-color: var(--linkedin-blue);
+}
+
+/* Jobs Page */
+.jobs-page {
+ max-width: 750px;
+ margin: 0 auto;
+}
+
+.jobs-header {
+ margin-bottom: 24px;
+}
+
+.jobs-header h2 {
+ font-size: 20px;
+ margin-bottom: 4px;
+}
+
+.jobs-header p {
+ color: var(--text-secondary);
+ font-size: 14px;
+}
+
+.jobs-list {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+/* Job Card */
+.job-card {
+ background: var(--card-bg);
+ border-radius: 8px;
+ border: 1px solid var(--border-color);
+ padding: 16px 20px;
+ display: flex;
+ gap: 16px;
+ align-items: flex-start;
+ transition: box-shadow 0.15s;
+}
+
+.job-card:hover {
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08), 0 4px 8px rgba(0, 0, 0, 0.1);
+}
+
+.job-logo {
+ width: 48px;
+ height: 48px;
+ border-radius: 4px;
+ object-fit: cover;
+ flex-shrink: 0;
+}
+
+.job-info {
+ flex: 1;
+ min-width: 0;
+}
+
+.job-title {
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--linkedin-blue);
+ margin-bottom: 2px;
+}
+
+.job-company {
+ font-size: 14px;
+ margin-bottom: 2px;
+}
+
+.job-location {
+ font-size: 13px;
+ color: var(--text-secondary);
+}
+
+.job-salary {
+ font-size: 13px;
+ color: var(--linkedin-green);
+ font-weight: 600;
+ margin-top: 4px;
+}
+
+.job-meta {
+ display: flex;
+ gap: 12px;
+ margin-top: 8px;
+ font-size: 12px;
+ color: var(--text-muted);
+}
+
+.save-job-btn {
+ color: var(--text-secondary);
+ padding: 8px;
+ border-radius: 50%;
+ transition: background 0.15s, color 0.15s;
+ flex-shrink: 0;
+}
+
+.save-job-btn:hover {
+ background: #f3f2ef;
+}
+
+.save-job-btn.saved {
+ color: var(--linkedin-blue);
+}
+
+/* Messaging Page */
+.messaging-page {
+ max-width: 700px;
+ margin: 0 auto;
+ background: var(--card-bg);
+ border-radius: 8px;
+ border: 1px solid var(--border-color);
+ overflow: hidden;
+}
+
+.messaging-header {
+ padding: 16px 20px;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.messaging-header h2 {
+ font-size: 18px;
+}
+
+.message-list {
+ display: flex;
+ flex-direction: column;
+}
+
+.message-item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 20px;
+ border-bottom: 1px solid #f3f2ef;
+ cursor: pointer;
+ transition: background 0.15s;
+}
+
+.message-item:hover {
+ background: #f3f2ef;
+}
+
+.message-item:last-child {
+ border-bottom: none;
+}
+
+.message-avatar {
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ object-fit: cover;
+ flex-shrink: 0;
+}
+
+.message-content {
+ flex: 1;
+ min-width: 0;
+}
+
+.message-top {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 4px;
+}
+
+.message-contact-name {
+ font-size: 14px;
+ font-weight: 600;
+}
+
+.message-timestamp {
+ font-size: 12px;
+ color: var(--text-muted);
+}
+
+.message-preview {
+ font-size: 13px;
+ color: var(--text-secondary);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.message-item.unread .message-contact-name {
+ color: var(--text-primary);
+}
+
+.message-item.unread .message-preview {
+ color: var(--text-primary);
+ font-weight: 500;
+}
+
+.unread-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background-color: var(--linkedin-blue);
+ flex-shrink: 0;
+}
+
+/* Profile Page */
+.profile-page {
+ max-width: 800px;
+ margin: 0 auto;
+}
+
+.profile-header-card {
+ background: var(--card-bg);
+ border-radius: 8px;
+ border: 1px solid var(--border-color);
+ overflow: hidden;
+ margin-bottom: 8px;
+}
+
+.profile-banner {
+ height: 200px;
+ background-size: cover;
+ background-position: center;
+ background-color: var(--linkedin-blue);
+}
+
+.profile-header-content {
+ padding: 0 24px 24px;
+ position: relative;
+}
+
+.profile-page-avatar {
+ width: 152px;
+ height: 152px;
+ border-radius: 50%;
+ border: 4px solid var(--card-bg);
+ object-fit: cover;
+ margin-top: -76px;
+}
+
+.profile-header-info {
+ margin-top: 8px;
+}
+
+.profile-header-info h1 {
+ font-size: 24px;
+}
+
+.profile-headline {
+ font-size: 16px;
+ color: var(--text-primary);
+ margin-top: 4px;
+}
+
+.profile-location {
+ font-size: 14px;
+ color: var(--text-secondary);
+ margin-top: 4px;
+}
+
+.profile-header-actions {
+ display: flex;
+ gap: 8px;
+ margin-top: 16px;
+}
+
+.btn-primary {
+ padding: 6px 16px;
+ background: var(--linkedin-blue);
+ color: white;
+ border-radius: 16px;
+ font-size: 14px;
+ font-weight: 600;
+ border: none;
+ cursor: pointer;
+ transition: background 0.15s;
+}
+
+.btn-primary:hover {
+ background: var(--linkedin-blue-hover);
+}
+
+.btn-secondary {
+ padding: 6px 16px;
+ background: transparent;
+ color: var(--linkedin-blue);
+ border: 1.5px solid var(--linkedin-blue);
+ border-radius: 16px;
+ font-size: 14px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.15s;
+}
+
+.btn-secondary:hover {
+ background: rgba(10, 102, 194, 0.08);
+}
+
+.profile-section {
+ background: var(--card-bg);
+ border-radius: 8px;
+ border: 1px solid var(--border-color);
+ padding: 24px;
+ margin-bottom: 8px;
+}
+
+.profile-section h2 {
+ font-size: 20px;
+ margin-bottom: 16px;
+}
+
+.profile-section > p {
+ font-size: 14px;
+ color: var(--text-primary);
+ line-height: 1.6;
+}
+
+/* Experience */
+.experience-item {
+ display: flex;
+ gap: 12px;
+ padding: 12px 0;
+ border-bottom: 1px solid #f3f2ef;
+}
+
+.experience-item:last-child {
+ border-bottom: none;
+ padding-bottom: 0;
+}
+
+.experience-item:first-child {
+ padding-top: 0;
+}
+
+.experience-logo {
+ width: 48px;
+ height: 48px;
+ border-radius: 4px;
+ object-fit: cover;
+ flex-shrink: 0;
+}
+
+.experience-info h4 {
+ font-size: 14px;
+ font-weight: 600;
+}
+
+.experience-company {
+ font-size: 14px;
+ color: var(--text-primary);
+}
+
+.experience-duration {
+ font-size: 13px;
+ color: var(--text-secondary);
+}
+
+/* Skills */
+.skills-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.skill-tag {
+ padding: 6px 14px;
+ background: #f3f2ef;
+ border-radius: 16px;
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--text-primary);
+}
+
+/* Responsive */
+@media (max-width: 1024px) {
+ .feed-layout {
+ grid-template-columns: 1fr;
+ }
+
+ .feed-sidebar-left,
+ .feed-sidebar-right {
+ display: none;
+ }
+
+ .navbar-search input {
+ width: 140px;
+ }
+}
+
+@media (max-width: 768px) {
+ .navbar-search {
+ display: none;
+ }
+
+ .nav-item span {
+ display: none;
+ }
+
+ .nav-item {
+ padding: 4px 8px;
+ }
+
+ .profile-banner {
+ height: 120px;
+ }
+
+ .profile-page-avatar {
+ width: 100px;
+ height: 100px;
+ margin-top: -50px;
+ }
+
+ .network-grid {
+ grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
+ }
+}
diff --git a/demos/linkedin-app/src/main.jsx b/demos/linkedin-app/src/main.jsx
new file mode 100644
index 000000000..e08a20472
--- /dev/null
+++ b/demos/linkedin-app/src/main.jsx
@@ -0,0 +1,16 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import { BrowserRouter } from 'react-router-dom'
+import App from './App'
+import { LinkedInProvider } from './context/LinkedInContext'
+import './index.css'
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+
+
+
+
+ ,
+)
diff --git a/demos/linkedin-app/src/pages/Feed.jsx b/demos/linkedin-app/src/pages/Feed.jsx
new file mode 100644
index 000000000..a2387c3c7
--- /dev/null
+++ b/demos/linkedin-app/src/pages/Feed.jsx
@@ -0,0 +1,51 @@
+import { useLinkedIn } from '../context/LinkedInContext'
+import ProfileCard from '../components/ProfileCard'
+import CreatePost from '../components/CreatePost'
+import PostCard from '../components/PostCard'
+
+function Feed() {
+ const { posts } = useLinkedIn()
+
+ return (
+
+
+
+
+ {posts.map(post => (
+
+ ))}
+
+
+
+
LinkedIn News
+
+
+ Tech layoffs slow in Q2
+ 4h ago · 12,847 readers
+
+
+ AI skills demand surges 300%
+ 6h ago · 8,293 readers
+
+
+ Remote work debate continues
+ 1d ago · 24,512 readers
+
+
+ New startup funding records
+ 2d ago · 5,671 readers
+
+
+ Developer salaries on the rise
+ 3d ago · 18,943 readers
+
+
+
+
+
+ )
+}
+
+export default Feed
diff --git a/demos/linkedin-app/src/pages/Jobs.jsx b/demos/linkedin-app/src/pages/Jobs.jsx
new file mode 100644
index 000000000..2f94a081e
--- /dev/null
+++ b/demos/linkedin-app/src/pages/Jobs.jsx
@@ -0,0 +1,22 @@
+import { useLinkedIn } from '../context/LinkedInContext'
+import JobCard from '../components/JobCard'
+
+function Jobs() {
+ const { jobs } = useLinkedIn()
+
+ return (
+
+
+
Jobs picked for you
+
Based on your profile, preferences, and activity
+
+
+ {jobs.map(job => (
+
+ ))}
+
+
+ )
+}
+
+export default Jobs
diff --git a/demos/linkedin-app/src/pages/Messaging.jsx b/demos/linkedin-app/src/pages/Messaging.jsx
new file mode 100644
index 000000000..8f4d25d74
--- /dev/null
+++ b/demos/linkedin-app/src/pages/Messaging.jsx
@@ -0,0 +1,30 @@
+import { useLinkedIn } from '../context/LinkedInContext'
+
+function Messaging() {
+ const { messages } = useLinkedIn()
+
+ return (
+
+
+
Messaging
+
+
+ {messages.map(msg => (
+
+
+
+
+
{msg.contact.name}
+ {msg.timestamp}
+
+
{msg.lastMessage}
+
+ {msg.unread &&
}
+
+ ))}
+
+
+ )
+}
+
+export default Messaging
diff --git a/demos/linkedin-app/src/pages/Network.jsx b/demos/linkedin-app/src/pages/Network.jsx
new file mode 100644
index 000000000..ca69cae4e
--- /dev/null
+++ b/demos/linkedin-app/src/pages/Network.jsx
@@ -0,0 +1,22 @@
+import { useLinkedIn } from '../context/LinkedInContext'
+import ConnectionCard from '../components/ConnectionCard'
+
+function Network() {
+ const { people } = useLinkedIn()
+
+ return (
+
+
+
Grow your network
+
People you may know based on your profile and connections
+
+
+ {people.map(person => (
+
+ ))}
+
+
+ )
+}
+
+export default Network
diff --git a/demos/linkedin-app/src/pages/Profile.jsx b/demos/linkedin-app/src/pages/Profile.jsx
new file mode 100644
index 000000000..4bd09009c
--- /dev/null
+++ b/demos/linkedin-app/src/pages/Profile.jsx
@@ -0,0 +1,56 @@
+import { useLinkedIn } from '../context/LinkedInContext'
+
+function Profile() {
+ const { currentUser } = useLinkedIn()
+
+ return (
+
+
+
+
+
+
+
{currentUser.name}
+
{currentUser.headline}
+
{currentUser.location} · {currentUser.connections}+ connections
+
+ Open to
+ Add profile section
+ More
+
+
+
+
+
+
+
About
+
{currentUser.about}
+
+
+
+
Experience
+ {currentUser.experience.map((exp, index) => (
+
+
+
+
{exp.title}
+
{exp.company}
+
{exp.duration}
+
+
+ ))}
+
+
+
+
Skills
+
+ {['React', 'JavaScript', 'TypeScript', 'Node.js', 'Python', 'AWS', 'Docker', 'GraphQL', 'PostgreSQL'].map(skill => (
+ {skill}
+ ))}
+
+
+
+ )
+}
+
+export default Profile
diff --git a/demos/linkedin-app/vite.config.js b/demos/linkedin-app/vite.config.js
new file mode 100644
index 000000000..9ffcc6757
--- /dev/null
+++ b/demos/linkedin-app/vite.config.js
@@ -0,0 +1,6 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()],
+})
diff --git a/demos/minecraft-app/index.html b/demos/minecraft-app/index.html
new file mode 100644
index 000000000..21fe5b090
--- /dev/null
+++ b/demos/minecraft-app/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+ Minecraft - Single Player
+
+
+
+
+
+
+
diff --git a/demos/minecraft-app/package-lock.json b/demos/minecraft-app/package-lock.json
new file mode 100644
index 000000000..4eae79e33
--- /dev/null
+++ b/demos/minecraft-app/package-lock.json
@@ -0,0 +1,2657 @@
+{
+ "name": "minecraft-app",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "minecraft-app",
+ "version": "1.0.0",
+ "dependencies": {
+ "@react-three/drei": "^9.114.3",
+ "@react-three/fiber": "^8.17.10",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "three": "^0.170.0"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-react": "^4.3.4",
+ "vite": "^6.0.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
+ "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@dimforge/rapier3d-compat": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
+ "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@mediapipe/tasks-vision": {
+ "version": "0.10.17",
+ "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz",
+ "integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@monogrid/gainmap-js": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz",
+ "integrity": "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==",
+ "license": "MIT",
+ "dependencies": {
+ "promise-worker-transferable": "^1.0.4"
+ },
+ "peerDependencies": {
+ "three": ">= 0.159.0"
+ }
+ },
+ "node_modules/@react-spring/animated": {
+ "version": "9.7.5",
+ "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz",
+ "integrity": "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/shared": "~9.7.5",
+ "@react-spring/types": "~9.7.5"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/@react-spring/core": {
+ "version": "9.7.5",
+ "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.5.tgz",
+ "integrity": "sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/animated": "~9.7.5",
+ "@react-spring/shared": "~9.7.5",
+ "@react-spring/types": "~9.7.5"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-spring/donate"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/@react-spring/rafz": {
+ "version": "9.7.5",
+ "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.5.tgz",
+ "integrity": "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==",
+ "license": "MIT"
+ },
+ "node_modules/@react-spring/shared": {
+ "version": "9.7.5",
+ "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.5.tgz",
+ "integrity": "sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/rafz": "~9.7.5",
+ "@react-spring/types": "~9.7.5"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/@react-spring/three": {
+ "version": "9.7.5",
+ "resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.7.5.tgz",
+ "integrity": "sha512-RxIsCoQfUqOS3POmhVHa1wdWS0wyHAUway73uRLp3GAL5U2iYVNdnzQsep6M2NZ994BlW8TcKuMtQHUqOsy6WA==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/animated": "~9.7.5",
+ "@react-spring/core": "~9.7.5",
+ "@react-spring/shared": "~9.7.5",
+ "@react-spring/types": "~9.7.5"
+ },
+ "peerDependencies": {
+ "@react-three/fiber": ">=6.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "three": ">=0.126"
+ }
+ },
+ "node_modules/@react-spring/types": {
+ "version": "9.7.5",
+ "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.5.tgz",
+ "integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==",
+ "license": "MIT"
+ },
+ "node_modules/@react-three/drei": {
+ "version": "9.122.0",
+ "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-9.122.0.tgz",
+ "integrity": "sha512-SEO/F/rBCTjlLez7WAlpys+iGe9hty4rNgjZvgkQeXFSiwqD4Hbk/wNHMAbdd8vprO2Aj81mihv4dF5bC7D0CA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.26.0",
+ "@mediapipe/tasks-vision": "0.10.17",
+ "@monogrid/gainmap-js": "^3.0.6",
+ "@react-spring/three": "~9.7.5",
+ "@use-gesture/react": "^10.3.1",
+ "camera-controls": "^2.9.0",
+ "cross-env": "^7.0.3",
+ "detect-gpu": "^5.0.56",
+ "glsl-noise": "^0.0.0",
+ "hls.js": "^1.5.17",
+ "maath": "^0.10.8",
+ "meshline": "^3.3.1",
+ "react-composer": "^5.0.3",
+ "stats-gl": "^2.2.8",
+ "stats.js": "^0.17.0",
+ "suspend-react": "^0.1.3",
+ "three-mesh-bvh": "^0.7.8",
+ "three-stdlib": "^2.35.6",
+ "troika-three-text": "^0.52.0",
+ "tunnel-rat": "^0.1.2",
+ "utility-types": "^3.11.0",
+ "zustand": "^5.0.1"
+ },
+ "peerDependencies": {
+ "@react-three/fiber": "^8",
+ "react": "^18",
+ "react-dom": "^18",
+ "three": ">=0.137"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@react-three/fiber": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.18.0.tgz",
+ "integrity": "sha512-FYZZqD0UUHUswKz3LQl2Z7H24AhD14XGTsIRw3SJaXUxyfVMi+1yiZGmqTcPt/CkPpdU7rrxqcyQ1zJE5DjvIQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.17.8",
+ "@types/react-reconciler": "^0.26.7",
+ "@types/webxr": "*",
+ "base64-js": "^1.5.1",
+ "buffer": "^6.0.3",
+ "its-fine": "^1.0.6",
+ "react-reconciler": "^0.27.0",
+ "react-use-measure": "^2.1.7",
+ "scheduler": "^0.21.0",
+ "suspend-react": "^0.1.3",
+ "zustand": "^3.7.1"
+ },
+ "peerDependencies": {
+ "expo": ">=43.0",
+ "expo-asset": ">=8.4",
+ "expo-file-system": ">=11.0",
+ "expo-gl": ">=11.0",
+ "react": ">=18 <19",
+ "react-dom": ">=18 <19",
+ "react-native": ">=0.64",
+ "three": ">=0.133"
+ },
+ "peerDependenciesMeta": {
+ "expo": {
+ "optional": true
+ },
+ "expo-asset": {
+ "optional": true
+ },
+ "expo-file-system": {
+ "optional": true
+ },
+ "expo-gl": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@react-three/fiber/node_modules/zustand": {
+ "version": "3.7.2",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz",
+ "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz",
+ "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz",
+ "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz",
+ "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz",
+ "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz",
+ "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz",
+ "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz",
+ "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz",
+ "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz",
+ "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz",
+ "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz",
+ "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz",
+ "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz",
+ "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz",
+ "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz",
+ "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz",
+ "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz",
+ "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz",
+ "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz",
+ "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz",
+ "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz",
+ "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz",
+ "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz",
+ "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz",
+ "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz",
+ "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@tweenjs/tween.js": {
+ "version": "23.1.3",
+ "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
+ "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/draco3d": {
+ "version": "1.4.10",
+ "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz",
+ "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/offscreencanvas": {
+ "version": "2019.7.3",
+ "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz",
+ "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.14",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
+ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-reconciler": {
+ "version": "0.26.7",
+ "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.26.7.tgz",
+ "integrity": "sha512-mBDYl8x+oyPX/VBb3E638N0B7xG+SPk/EAMcVPeexqus/5aTpTphQi0curhhshOqRrc9t6OPoJfEUkbymse/lQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/stats.js": {
+ "version": "0.17.4",
+ "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
+ "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/three": {
+ "version": "0.184.0",
+ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.184.0.tgz",
+ "integrity": "sha512-4mY2tZAu0y0B0567w7013BBXSpsP0+Z48NJvmNo4Y/Pf76yCyz6Jw4P3tUVs10WuYNXXZ+wmHyGWpCek3amJxA==",
+ "license": "MIT",
+ "dependencies": {
+ "@dimforge/rapier3d-compat": "~0.12.0",
+ "@tweenjs/tween.js": "~23.1.3",
+ "@types/stats.js": "*",
+ "@types/webxr": ">=0.5.17",
+ "fflate": "~0.8.2",
+ "meshoptimizer": "~1.1.1"
+ }
+ },
+ "node_modules/@types/webxr": {
+ "version": "0.5.24",
+ "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
+ "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
+ "license": "MIT"
+ },
+ "node_modules/@use-gesture/core": {
+ "version": "10.3.1",
+ "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz",
+ "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==",
+ "license": "MIT"
+ },
+ "node_modules/@use-gesture/react": {
+ "version": "10.3.1",
+ "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz",
+ "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==",
+ "license": "MIT",
+ "dependencies": {
+ "@use-gesture/core": "10.3.1"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.20",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz",
+ "integrity": "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/bidi-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
+ "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
+ "license": "MIT",
+ "dependencies": {
+ "require-from-string": "^2.0.2"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
+ "node_modules/camera-controls": {
+ "version": "2.10.1",
+ "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-2.10.1.tgz",
+ "integrity": "sha512-KnaKdcvkBJ1Irbrzl8XD6WtZltkRjp869Jx8c0ujs9K+9WD+1D7ryBsCiVqJYUqt6i/HR5FxT7RLASieUD+Q5w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">=0.126.1"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001788",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz",
+ "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-env": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
+ "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.1"
+ },
+ "bin": {
+ "cross-env": "src/bin/cross-env.js",
+ "cross-env-shell": "src/bin/cross-env-shell.js"
+ },
+ "engines": {
+ "node": ">=10.14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/detect-gpu": {
+ "version": "5.0.70",
+ "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz",
+ "integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==",
+ "license": "MIT",
+ "dependencies": {
+ "webgl-constants": "^1.1.1"
+ }
+ },
+ "node_modules/draco3d": {
+ "version": "1.5.7",
+ "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz",
+ "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.343",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.343.tgz",
+ "integrity": "sha512-YHnQ3MXI08icvL9ZKnEBy05F2EQ8ob01UaMOuMbM8l+4UcAq6MPPbBTJBbsBUg3H8JeZNt+O4fjsoWth3p6IFg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fflate": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
+ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
+ "license": "MIT"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/glsl-noise": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz",
+ "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==",
+ "license": "MIT"
+ },
+ "node_modules/hls.js": {
+ "version": "1.6.16",
+ "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.16.tgz",
+ "integrity": "sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
+ "license": "MIT"
+ },
+ "node_modules/is-promise": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
+ "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "license": "ISC"
+ },
+ "node_modules/its-fine": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.2.5.tgz",
+ "integrity": "sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/react-reconciler": "^0.28.0"
+ },
+ "peerDependencies": {
+ "react": ">=18.0"
+ }
+ },
+ "node_modules/its-fine/node_modules/@types/react-reconciler": {
+ "version": "0.28.9",
+ "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz",
+ "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/lie": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+ "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "immediate": "~3.0.5"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/maath": {
+ "version": "0.10.8",
+ "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz",
+ "integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/three": ">=0.134.0",
+ "three": ">=0.134.0"
+ }
+ },
+ "node_modules/meshline": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz",
+ "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">=0.137"
+ }
+ },
+ "node_modules/meshoptimizer": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz",
+ "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==",
+ "license": "MIT"
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.38",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz",
+ "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.10",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
+ "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/potpack": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz",
+ "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==",
+ "license": "ISC"
+ },
+ "node_modules/promise-worker-transferable": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz",
+ "integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "is-promise": "^2.1.0",
+ "lie": "^3.0.2"
+ }
+ },
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-composer": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/react-composer/-/react-composer-5.0.3.tgz",
+ "integrity": "sha512-1uWd07EME6XZvMfapwZmc7NgCZqDemcvicRi3wMJzXsQLvZ3L7fTHVyPy1bZdnWXM4iPjYuNE+uJ41MLKeTtnA==",
+ "license": "MIT",
+ "dependencies": {
+ "prop-types": "^15.6.0"
+ },
+ "peerDependencies": {
+ "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-dom/node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
+ "node_modules/react-reconciler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.27.0.tgz",
+ "integrity": "sha512-HmMDKciQjYmBRGuuhIaKA1ba/7a+UsM5FzOZsMO2JYHt9Jh8reCb7j1eDC95NOyUlKM9KRyvdx0flBuDvYSBoA==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.21.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-use-measure": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz",
+ "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.13",
+ "react-dom": ">=16.13"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz",
+ "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.60.2",
+ "@rollup/rollup-android-arm64": "4.60.2",
+ "@rollup/rollup-darwin-arm64": "4.60.2",
+ "@rollup/rollup-darwin-x64": "4.60.2",
+ "@rollup/rollup-freebsd-arm64": "4.60.2",
+ "@rollup/rollup-freebsd-x64": "4.60.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.2",
+ "@rollup/rollup-linux-arm64-musl": "4.60.2",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.2",
+ "@rollup/rollup-linux-loong64-musl": "4.60.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.2",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.2",
+ "@rollup/rollup-linux-x64-gnu": "4.60.2",
+ "@rollup/rollup-linux-x64-musl": "4.60.2",
+ "@rollup/rollup-openbsd-x64": "4.60.2",
+ "@rollup/rollup-openharmony-arm64": "4.60.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.2",
+ "@rollup/rollup-win32-x64-gnu": "4.60.2",
+ "@rollup/rollup-win32-x64-msvc": "4.60.2",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.21.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz",
+ "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stats-gl": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz",
+ "integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/three": "*",
+ "three": "^0.170.0"
+ },
+ "peerDependencies": {
+ "@types/three": "*",
+ "three": "*"
+ }
+ },
+ "node_modules/stats.js": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz",
+ "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==",
+ "license": "MIT"
+ },
+ "node_modules/suspend-react": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz",
+ "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=17.0"
+ }
+ },
+ "node_modules/three": {
+ "version": "0.170.0",
+ "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz",
+ "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==",
+ "license": "MIT"
+ },
+ "node_modules/three-mesh-bvh": {
+ "version": "0.7.8",
+ "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.7.8.tgz",
+ "integrity": "sha512-BGEZTOIC14U0XIRw3tO4jY7IjP7n7v24nv9JXS1CyeVRWOCkcOMhRnmENUjuV39gktAw4Ofhr0OvIAiTspQrrw==",
+ "deprecated": "Deprecated due to three.js version incompatibility. Please use v0.8.0, instead.",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">= 0.151.0"
+ }
+ },
+ "node_modules/three-stdlib": {
+ "version": "2.36.1",
+ "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz",
+ "integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/draco3d": "^1.4.0",
+ "@types/offscreencanvas": "^2019.6.4",
+ "@types/webxr": "^0.5.2",
+ "draco3d": "^1.4.1",
+ "fflate": "^0.6.9",
+ "potpack": "^1.0.1"
+ },
+ "peerDependencies": {
+ "three": ">=0.128.0"
+ }
+ },
+ "node_modules/three-stdlib/node_modules/fflate": {
+ "version": "0.6.10",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz",
+ "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==",
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/troika-three-text": {
+ "version": "0.52.4",
+ "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz",
+ "integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==",
+ "license": "MIT",
+ "dependencies": {
+ "bidi-js": "^1.0.2",
+ "troika-three-utils": "^0.52.4",
+ "troika-worker-utils": "^0.52.0",
+ "webgl-sdf-generator": "1.1.1"
+ },
+ "peerDependencies": {
+ "three": ">=0.125.0"
+ }
+ },
+ "node_modules/troika-three-utils": {
+ "version": "0.52.4",
+ "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz",
+ "integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">=0.125.0"
+ }
+ },
+ "node_modules/troika-worker-utils": {
+ "version": "0.52.0",
+ "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz",
+ "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==",
+ "license": "MIT"
+ },
+ "node_modules/tunnel-rat": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz",
+ "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==",
+ "license": "MIT",
+ "dependencies": {
+ "zustand": "^4.3.2"
+ }
+ },
+ "node_modules/tunnel-rat/node_modules/zustand": {
+ "version": "4.5.7",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+ "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.2.2"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0.6",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/utility-types": {
+ "version": "3.11.0",
+ "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz",
+ "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.4.2",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
+ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "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
+ }
+ }
+ },
+ "node_modules/webgl-constants": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz",
+ "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg=="
+ },
+ "node_modules/webgl-sdf-generator": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz",
+ "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==",
+ "license": "MIT"
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/zustand": {
+ "version": "5.0.12",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz",
+ "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==",
+ "license": "MIT",
+ "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
+ }
+ }
+ }
+ }
+}
diff --git a/demos/minecraft-app/package.json b/demos/minecraft-app/package.json
new file mode 100644
index 000000000..376dcc484
--- /dev/null
+++ b/demos/minecraft-app/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "minecraft-app",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@react-three/drei": "^9.114.3",
+ "@react-three/fiber": "^8.17.10",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "three": "^0.170.0"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-react": "^4.3.4",
+ "vite": "^6.0.0"
+ }
+}
diff --git a/demos/minecraft-app/src/App.jsx b/demos/minecraft-app/src/App.jsx
new file mode 100644
index 000000000..ce61d93d8
--- /dev/null
+++ b/demos/minecraft-app/src/App.jsx
@@ -0,0 +1,83 @@
+import { useState, useRef, Suspense } from 'react';
+import { Canvas } from '@react-three/fiber';
+import World from './components/World';
+import Player from './components/Player';
+import Sky from './components/Sky';
+import HUD from './components/HUD';
+import Crosshair from './components/Crosshair';
+import MiningIndicator from './components/MiningIndicator';
+import StartScreen from './components/StartScreen';
+import { useWorldState } from './hooks/useWorldState';
+
+function Game() {
+ const {
+ blocks,
+ spawnPos,
+ selectedBlockIndex,
+ addBlock,
+ removeBlock,
+ selectBlock,
+ cycleBlock,
+ loadChunksAround,
+ } = useWorldState();
+
+ const miningState = useRef({ progress: 0, target: null });
+
+ if (!spawnPos) {
+ return (
+
+ Generating world...
+
+ );
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export default function App() {
+ const [started, setStarted] = useState(false);
+
+ if (!started) {
+ return setStarted(true)} />;
+ }
+
+ return ;
+}
diff --git a/demos/minecraft-app/src/components/Crosshair.jsx b/demos/minecraft-app/src/components/Crosshair.jsx
new file mode 100644
index 000000000..deb874e92
--- /dev/null
+++ b/demos/minecraft-app/src/components/Crosshair.jsx
@@ -0,0 +1,21 @@
+export default function Crosshair() {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/demos/minecraft-app/src/components/HUD.jsx b/demos/minecraft-app/src/components/HUD.jsx
new file mode 100644
index 000000000..28dd3c517
--- /dev/null
+++ b/demos/minecraft-app/src/components/HUD.jsx
@@ -0,0 +1,98 @@
+import { useEffect } from 'react';
+import { INVENTORY_BLOCKS, BlockColors, BlockNames } from '../utils/blocks';
+
+export default function HUD({ selectedBlockIndex, onSelectBlock, onCycleBlock }) {
+ useEffect(() => {
+ const onKeyDown = (e) => {
+ const num = parseInt(e.key);
+ if (num >= 1 && num <= 9) {
+ onSelectBlock(num - 1);
+ }
+ };
+
+ const onWheel = (e) => {
+ onCycleBlock(e.deltaY > 0 ? 1 : -1);
+ };
+
+ document.addEventListener('keydown', onKeyDown);
+ document.addEventListener('wheel', onWheel);
+ return () => {
+ document.removeEventListener('keydown', onKeyDown);
+ document.removeEventListener('wheel', onWheel);
+ };
+ }, [onSelectBlock, onCycleBlock]);
+
+ return (
+
+ {INVENTORY_BLOCKS.map((blockType, index) => (
+
onSelectBlock(index)}
+ >
+
+
+ {index + 1}
+
+
+ ))}
+
+ {BlockNames[INVENTORY_BLOCKS[selectedBlockIndex]]}
+
+
+ );
+}
diff --git a/demos/minecraft-app/src/components/MiningIndicator.jsx b/demos/minecraft-app/src/components/MiningIndicator.jsx
new file mode 100644
index 000000000..e97defe1e
--- /dev/null
+++ b/demos/minecraft-app/src/components/MiningIndicator.jsx
@@ -0,0 +1,53 @@
+import { useRef, useEffect } from 'react';
+
+export default function MiningIndicator({ miningState }) {
+ const barRef = useRef(null);
+ const containerRef = useRef(null);
+
+ useEffect(() => {
+ let animId;
+ const update = () => {
+ const state = miningState.current;
+ if (state && state.progress > 0 && state.target) {
+ containerRef.current.style.display = 'block';
+ barRef.current.style.width = `${state.progress * 100}%`;
+ } else {
+ containerRef.current.style.display = 'none';
+ }
+ animId = requestAnimationFrame(update);
+ };
+ animId = requestAnimationFrame(update);
+ return () => cancelAnimationFrame(animId);
+ }, [miningState]);
+
+ return (
+
+ );
+}
diff --git a/demos/minecraft-app/src/components/Player.jsx b/demos/minecraft-app/src/components/Player.jsx
new file mode 100644
index 000000000..a8c878691
--- /dev/null
+++ b/demos/minecraft-app/src/components/Player.jsx
@@ -0,0 +1,321 @@
+import { useRef, useEffect, useCallback } from 'react';
+import { useThree, useFrame } from '@react-three/fiber';
+import * as THREE from 'three';
+
+const MOVE_SPEED = 8;
+const SWIM_SPEED = 4;
+const JUMP_FORCE = 8;
+const SWIM_UP_FORCE = 4;
+const GRAVITY = -20;
+const WATER_GRAVITY = -4;
+const PLAYER_HEIGHT = 1.6;
+const PLAYER_RADIUS = 0.3;
+const MOUSE_SENSITIVITY = 0.002;
+const REACH_DISTANCE = 6;
+
+// Block breaking times in seconds (by block type)
+const BREAK_TIMES = {
+ 1: 0.6, // grass
+ 2: 0.5, // dirt
+ 3: 1.5, // stone
+ 4: 1.0, // wood
+ 5: 0.3, // leaves
+ 6: 0.5, // sand
+ 8: 1.5, // cobblestone
+ 9: 0.8, // planks
+ 10: 1.5, // brick
+ 11: 0.4, // snow
+};
+
+function raycastBlock(camera, blocks) {
+ const dir = new THREE.Vector3();
+ camera.getWorldDirection(dir);
+ const origin = camera.position.clone();
+ const step = 0.05;
+ let prevPos = null;
+
+ for (let d = 0; d < REACH_DISTANCE; d += step) {
+ const x = origin.x + dir.x * d;
+ const y = origin.y + dir.y * d;
+ const z = origin.z + dir.z * d;
+ const bx = Math.floor(x);
+ const by = Math.floor(y);
+ const bz = Math.floor(z);
+ const key = `${bx},${by},${bz}`;
+
+ if (blocks[key] && blocks[key] !== 7) {
+ return { hit: [bx, by, bz], prev: prevPos, type: blocks[key] };
+ }
+ prevPos = [bx, by, bz];
+ }
+ return null;
+}
+
+export default function Player({ spawnPos, blocks, onAddBlock, onRemoveBlock, onLoadChunks, miningState }) {
+ const { camera, gl } = useThree();
+ const velocity = useRef(new THREE.Vector3());
+ const keys = useRef({});
+ const isLocked = useRef(false);
+ const euler = useRef(new THREE.Euler(0, 0, 0, 'YXZ'));
+ const lastChunkCheck = useRef({ x: 0, z: 0 });
+ const isGrounded = useRef(false);
+ const isInWater = useRef(false);
+
+ // Mining state
+ const leftMouseDown = useRef(false);
+ const miningTarget = useRef(null); // { x, y, z, type }
+ const miningProgress = useRef(0);
+
+ // Set initial position
+ useEffect(() => {
+ if (spawnPos) {
+ camera.position.set(spawnPos[0], spawnPos[1], spawnPos[2]);
+ euler.current.set(0, 0, 0);
+ }
+ }, [spawnPos, camera]);
+
+ const getBlockAt = useCallback((x, y, z) => {
+ const bx = Math.floor(x);
+ const by = Math.floor(y);
+ const bz = Math.floor(z);
+ const key = `${bx},${by},${bz}`;
+ return blocks[key] || 0;
+ }, [blocks]);
+
+ const isBlockSolid = useCallback((x, y, z) => {
+ const block = getBlockAt(x, y, z);
+ return block !== 0 && block !== 7; // not air and not water
+ }, [getBlockAt]);
+
+ const isWater = useCallback((x, y, z) => {
+ return getBlockAt(x, y, z) === 7;
+ }, [getBlockAt]);
+
+ // Pointer lock
+ useEffect(() => {
+ const canvas = gl.domElement;
+
+ const onClick = () => {
+ if (!isLocked.current) {
+ canvas.requestPointerLock();
+ }
+ };
+
+ const onLockChange = () => {
+ isLocked.current = document.pointerLockElement === canvas;
+ };
+
+ const onMouseMove = (e) => {
+ if (!isLocked.current) return;
+ euler.current.y -= e.movementX * MOUSE_SENSITIVITY;
+ euler.current.x -= e.movementY * MOUSE_SENSITIVITY;
+ euler.current.x = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, euler.current.x));
+ camera.quaternion.setFromEuler(euler.current);
+ };
+
+ const onKeyDown = (e) => {
+ keys.current[e.code] = true;
+ };
+
+ const onKeyUp = (e) => {
+ keys.current[e.code] = false;
+ };
+
+ canvas.addEventListener('click', onClick);
+ document.addEventListener('pointerlockchange', onLockChange);
+ document.addEventListener('mousemove', onMouseMove);
+ document.addEventListener('keydown', onKeyDown);
+ document.addEventListener('keyup', onKeyUp);
+
+ return () => {
+ canvas.removeEventListener('click', onClick);
+ document.removeEventListener('pointerlockchange', onLockChange);
+ document.removeEventListener('mousemove', onMouseMove);
+ document.removeEventListener('keydown', onKeyDown);
+ document.removeEventListener('keyup', onKeyUp);
+ };
+ }, [camera, gl]);
+
+ // Mouse button tracking + right-click block placement
+ useEffect(() => {
+ const onMouseDown = (e) => {
+ if (!isLocked.current) return;
+ if (e.button === 0) {
+ leftMouseDown.current = true;
+ } else if (e.button === 2) {
+ const result = raycastBlock(camera, blocks);
+ if (result && result.prev) {
+ onAddBlock(result.prev[0], result.prev[1], result.prev[2]);
+ }
+ }
+ };
+
+ const onMouseUp = (e) => {
+ if (e.button === 0) {
+ leftMouseDown.current = false;
+ miningTarget.current = null;
+ miningProgress.current = 0;
+ miningState.current = { progress: 0, target: null };
+ }
+ };
+
+ const onContextMenu = (e) => e.preventDefault();
+
+ document.addEventListener('mousedown', onMouseDown);
+ document.addEventListener('mouseup', onMouseUp);
+ document.addEventListener('contextmenu', onContextMenu);
+
+ return () => {
+ document.removeEventListener('mousedown', onMouseDown);
+ document.removeEventListener('mouseup', onMouseUp);
+ document.removeEventListener('contextmenu', onContextMenu);
+ };
+ }, [camera, blocks, onAddBlock, miningState]);
+
+ useFrame((_, delta) => {
+ if (!isLocked.current) return;
+
+ const dt = Math.min(delta, 0.05);
+ const pos = camera.position.clone();
+
+ // --- Mining logic ---
+ if (leftMouseDown.current) {
+ const result = raycastBlock(camera, blocks);
+ if (result) {
+ const [bx, by, bz] = result.hit;
+ const target = miningTarget.current;
+
+ // Check if we're still looking at the same block
+ if (target && target.x === bx && target.y === by && target.z === bz) {
+ const breakTime = BREAK_TIMES[result.type] || 1.0;
+ miningProgress.current += dt;
+ const progress = Math.min(miningProgress.current / breakTime, 1);
+ miningState.current = { progress, target: [bx, by, bz] };
+
+ if (miningProgress.current >= breakTime) {
+ onRemoveBlock(bx, by, bz);
+ miningTarget.current = null;
+ miningProgress.current = 0;
+ miningState.current = { progress: 0, target: null };
+ leftMouseDown.current = false;
+ }
+ } else {
+ // Started looking at a different block, reset
+ miningTarget.current = { x: bx, y: by, z: bz, type: result.type };
+ miningProgress.current = 0;
+ miningState.current = { progress: 0, target: [bx, by, bz] };
+ }
+ } else {
+ miningTarget.current = null;
+ miningProgress.current = 0;
+ miningState.current = { progress: 0, target: null };
+ }
+ }
+
+ // --- Check if player is in water ---
+ const feetBlockY = pos.y - PLAYER_HEIGHT;
+ const headBlockY = pos.y;
+ const inWaterFeet = isWater(pos.x, feetBlockY, pos.z);
+ const inWaterHead = isWater(pos.x, headBlockY, pos.z);
+ isInWater.current = inWaterFeet || inWaterHead;
+
+ // --- Movement direction ---
+ const forward = new THREE.Vector3();
+ camera.getWorldDirection(forward);
+ forward.y = 0;
+ forward.normalize();
+
+ const right = new THREE.Vector3();
+ right.crossVectors(forward, new THREE.Vector3(0, 1, 0)).normalize();
+
+ const speed = isInWater.current ? SWIM_SPEED : MOVE_SPEED;
+
+ const moveDir = new THREE.Vector3();
+ if (keys.current['KeyW'] || keys.current['ArrowUp']) moveDir.add(forward);
+ if (keys.current['KeyS'] || keys.current['ArrowDown']) moveDir.sub(forward);
+ if (keys.current['KeyA'] || keys.current['ArrowLeft']) moveDir.sub(right);
+ if (keys.current['KeyD'] || keys.current['ArrowRight']) moveDir.add(right);
+ if (moveDir.length() > 0) moveDir.normalize();
+
+ // --- Gravity / buoyancy ---
+ if (isInWater.current) {
+ // Water drag: slow down vertical velocity
+ velocity.current.y *= 0.9;
+ velocity.current.y += WATER_GRAVITY * dt;
+ // Clamp downward speed in water
+ if (velocity.current.y < -3) velocity.current.y = -3;
+
+ // Space to swim up
+ if (keys.current['Space']) {
+ velocity.current.y = SWIM_UP_FORCE;
+ }
+ } else {
+ // Normal gravity
+ velocity.current.y += GRAVITY * dt;
+
+ // Jump
+ if (keys.current['Space'] && isGrounded.current) {
+ velocity.current.y = JUMP_FORCE;
+ isGrounded.current = false;
+ }
+ }
+
+ // --- Resolve Y axis first, so X/Z checks use correct feet position ---
+ let newY = pos.y + velocity.current.y * dt;
+
+ // Ground collision (falling)
+ if (velocity.current.y < 0) {
+ const checkFeetY = newY - PLAYER_HEIGHT;
+ if (isBlockSolid(pos.x, checkFeetY, pos.z)) {
+ newY = Math.floor(checkFeetY) + 1 + PLAYER_HEIGHT;
+ velocity.current.y = 0;
+ isGrounded.current = true;
+ } else {
+ if (!isInWater.current) {
+ isGrounded.current = false;
+ }
+ }
+ }
+
+ // Ceiling collision (rising)
+ if (velocity.current.y > 0) {
+ if (isBlockSolid(pos.x, newY + 0.1, pos.z)) {
+ velocity.current.y = 0;
+ }
+ }
+
+ // --- Now resolve X/Z using the corrected Y ---
+ // Use a small epsilon above the ground so feet checks don't clip into the floor
+ const resolvedFeetY = newY - PLAYER_HEIGHT + 0.01;
+
+ // X-axis collision
+ let newX = pos.x + moveDir.x * speed * dt;
+ if (isBlockSolid(newX + PLAYER_RADIUS, resolvedFeetY, pos.z) ||
+ isBlockSolid(newX + PLAYER_RADIUS, resolvedFeetY + 1, pos.z) ||
+ isBlockSolid(newX - PLAYER_RADIUS, resolvedFeetY, pos.z) ||
+ isBlockSolid(newX - PLAYER_RADIUS, resolvedFeetY + 1, pos.z)) {
+ newX = pos.x;
+ }
+
+ // Z-axis collision
+ let newZ = pos.z + moveDir.z * speed * dt;
+ if (isBlockSolid(newX, resolvedFeetY, newZ + PLAYER_RADIUS) ||
+ isBlockSolid(newX, resolvedFeetY + 1, newZ + PLAYER_RADIUS) ||
+ isBlockSolid(newX, resolvedFeetY, newZ - PLAYER_RADIUS) ||
+ isBlockSolid(newX, resolvedFeetY + 1, newZ - PLAYER_RADIUS)) {
+ newZ = pos.z;
+ }
+
+ camera.position.set(newX, newY, newZ);
+
+ // Load chunks as player moves
+ const cx = Math.floor(newX / 16);
+ const cz = Math.floor(newZ / 16);
+ if (cx !== lastChunkCheck.current.x || cz !== lastChunkCheck.current.z) {
+ lastChunkCheck.current = { x: cx, z: cz };
+ onLoadChunks(newX, newZ);
+ }
+ });
+
+ return null;
+}
diff --git a/demos/minecraft-app/src/components/Sky.jsx b/demos/minecraft-app/src/components/Sky.jsx
new file mode 100644
index 000000000..8b3b412cc
--- /dev/null
+++ b/demos/minecraft-app/src/components/Sky.jsx
@@ -0,0 +1,49 @@
+import { useRef } from 'react';
+import { useFrame } from '@react-three/fiber';
+import * as THREE from 'three';
+
+export default function Sky() {
+ const sunRef = useRef();
+
+ useFrame(({ clock }) => {
+ if (sunRef.current) {
+ const t = clock.getElapsedTime() * 0.02;
+ sunRef.current.position.set(
+ Math.cos(t) * 200,
+ Math.sin(t) * 150 + 100,
+ Math.sin(t) * 200
+ );
+ }
+ });
+
+ return (
+ <>
+ {/* Sky dome color */}
+
+
+
+ {/* Sun light */}
+
+
+ {/* Ambient light for shadows */}
+
+
+ {/* Hemisphere light for natural sky coloring */}
+
+ >
+ );
+}
diff --git a/demos/minecraft-app/src/components/StartScreen.jsx b/demos/minecraft-app/src/components/StartScreen.jsx
new file mode 100644
index 000000000..1e29c8ba5
--- /dev/null
+++ b/demos/minecraft-app/src/components/StartScreen.jsx
@@ -0,0 +1,74 @@
+export default function StartScreen({ onStart }) {
+ return (
+
+
+ MINECRAFT
+
+
+ REACT EDITION
+
+
+
{
+ e.target.style.background = 'rgba(92, 184, 92, 1)';
+ e.target.style.transform = 'scale(1.05)';
+ }}
+ onMouseLeave={(e) => {
+ e.target.style.background = 'rgba(92, 184, 92, 0.8)';
+ e.target.style.transform = 'scale(1)';
+ }}
+ >
+ Singleplayer
+
+
+
+
WASD - Move | Space - Jump | Mouse - Look
+
Left Click - Break Block | Right Click - Place Block
+
Scroll / 1-9 - Select Block
+
+
+ );
+}
diff --git a/demos/minecraft-app/src/components/World.jsx b/demos/minecraft-app/src/components/World.jsx
new file mode 100644
index 000000000..7c80d536e
--- /dev/null
+++ b/demos/minecraft-app/src/components/World.jsx
@@ -0,0 +1,102 @@
+import { useMemo } from 'react';
+import * as THREE from 'three';
+import { BlockColors, BlockType } from '../utils/blocks';
+
+const blockGeometry = new THREE.BoxGeometry(1, 1, 1);
+
+// Create materials for each block type
+const blockMaterials = {};
+for (const [type, color] of Object.entries(BlockColors)) {
+ const t = Number(type);
+ if (t === BlockType.WATER) {
+ blockMaterials[t] = new THREE.MeshLambertMaterial({
+ color,
+ transparent: true,
+ opacity: 0.6,
+ });
+ } else if (t === BlockType.LEAVES) {
+ blockMaterials[t] = new THREE.MeshLambertMaterial({
+ color,
+ transparent: true,
+ opacity: 0.85,
+ });
+ } else if (t === BlockType.GRASS) {
+ // Grass block: green top, brown sides/bottom
+ blockMaterials[t] = [
+ new THREE.MeshLambertMaterial({ color: '#6b5a2a' }), // right
+ new THREE.MeshLambertMaterial({ color: '#6b5a2a' }), // left
+ new THREE.MeshLambertMaterial({ color }), // top (green)
+ new THREE.MeshLambertMaterial({ color: '#8b6914' }), // bottom (dirt)
+ new THREE.MeshLambertMaterial({ color: '#6b5a2a' }), // front
+ new THREE.MeshLambertMaterial({ color: '#6b5a2a' }), // back
+ ];
+ } else {
+ blockMaterials[t] = new THREE.MeshLambertMaterial({ color });
+ }
+}
+
+// Group blocks by type and create instanced meshes
+function createInstancedMeshData(blocks) {
+ const grouped = {};
+
+ for (const [key, type] of Object.entries(blocks)) {
+ if (type === BlockType.AIR) continue;
+ if (!grouped[type]) grouped[type] = [];
+ const [x, y, z] = key.split(',').map(Number);
+
+ // Simple face culling: only render blocks with at least one exposed face
+ const hasExposedFace =
+ !blocks[`${x + 1},${y},${z}`] ||
+ !blocks[`${x - 1},${y},${z}`] ||
+ !blocks[`${x},${y + 1},${z}`] ||
+ !blocks[`${x},${y - 1},${z}`] ||
+ !blocks[`${x},${y},${z + 1}`] ||
+ !blocks[`${x},${y},${z - 1}`];
+
+ if (hasExposedFace) {
+ grouped[type].push([x, y, z]);
+ }
+ }
+
+ return grouped;
+}
+
+function BlockInstances({ type, positions }) {
+ const mesh = useMemo(() => {
+ const mat = blockMaterials[type];
+ if (!mat) return null;
+
+ // For grass with multi-material, use regular meshes approach via InstancedMesh per face isn't practical
+ // So for grass we use a single green-brown material
+ const material = Array.isArray(mat) ? mat : mat;
+ const instancedMesh = new THREE.InstancedMesh(blockGeometry, material, positions.length);
+
+ const dummy = new THREE.Object3D();
+ for (let i = 0; i < positions.length; i++) {
+ const [x, y, z] = positions[i];
+ dummy.position.set(x + 0.5, y + 0.5, z + 0.5);
+ dummy.updateMatrix();
+ instancedMesh.setMatrixAt(i, dummy.matrix);
+ }
+ instancedMesh.instanceMatrix.needsUpdate = true;
+ instancedMesh.castShadow = true;
+ instancedMesh.receiveShadow = true;
+
+ return instancedMesh;
+ }, [type, positions]);
+
+ if (!mesh) return null;
+ return ;
+}
+
+export default function World({ blocks }) {
+ const grouped = useMemo(() => createInstancedMeshData(blocks), [blocks]);
+
+ return (
+
+ {Object.entries(grouped).map(([type, positions]) => (
+
+ ))}
+
+ );
+}
diff --git a/demos/minecraft-app/src/hooks/useWorldState.js b/demos/minecraft-app/src/hooks/useWorldState.js
new file mode 100644
index 000000000..d11f56f93
--- /dev/null
+++ b/demos/minecraft-app/src/hooks/useWorldState.js
@@ -0,0 +1,94 @@
+import { useState, useCallback, useRef, useEffect } from 'react';
+import { generateChunkBlocks, getSpawnPosition, CHUNK_SIZE } from '../utils/terrain';
+import { BlockType, INVENTORY_BLOCKS } from '../utils/blocks';
+
+const RENDER_DISTANCE = 3;
+
+function getChunkKey(cx, cz) {
+ return `${cx},${cz}`;
+}
+
+export function useWorldState() {
+ const [blocks, setBlocks] = useState({});
+ const [selectedBlockIndex, setSelectedBlockIndex] = useState(0);
+ const [spawnPos, setSpawnPos] = useState(null);
+ const loadedChunks = useRef(new Set());
+
+ const loadChunksAround = useCallback((playerX, playerZ) => {
+ const cx = Math.floor(playerX / CHUNK_SIZE);
+ const cz = Math.floor(playerZ / CHUNK_SIZE);
+ let newBlocks = {};
+ let anyNew = false;
+
+ for (let dx = -RENDER_DISTANCE; dx <= RENDER_DISTANCE; dx++) {
+ for (let dz = -RENDER_DISTANCE; dz <= RENDER_DISTANCE; dz++) {
+ const key = getChunkKey(cx + dx, cz + dz);
+ if (!loadedChunks.current.has(key)) {
+ loadedChunks.current.add(key);
+ const chunkBlocks = generateChunkBlocks(cx + dx, cz + dz);
+ Object.assign(newBlocks, chunkBlocks);
+ anyNew = true;
+ }
+ }
+ }
+
+ if (anyNew) {
+ setBlocks(prev => ({ ...prev, ...newBlocks }));
+ }
+ }, []);
+
+ // Initial world generation
+ useEffect(() => {
+ const initialBlocks = {};
+ for (let dx = -RENDER_DISTANCE; dx <= RENDER_DISTANCE; dx++) {
+ for (let dz = -RENDER_DISTANCE; dz <= RENDER_DISTANCE; dz++) {
+ const key = getChunkKey(dx, dz);
+ loadedChunks.current.add(key);
+ const chunkBlocks = generateChunkBlocks(dx, dz);
+ Object.assign(initialBlocks, chunkBlocks);
+ }
+ }
+ setBlocks(initialBlocks);
+ setSpawnPos(getSpawnPosition(initialBlocks));
+ }, []);
+
+ const addBlock = useCallback((x, y, z) => {
+ const key = `${x},${y},${z}`;
+ const blockType = INVENTORY_BLOCKS[selectedBlockIndex];
+ setBlocks(prev => ({ ...prev, [key]: blockType }));
+ }, [selectedBlockIndex]);
+
+ const removeBlock = useCallback((x, y, z) => {
+ const key = `${x},${y},${z}`;
+ setBlocks(prev => {
+ const next = { ...prev };
+ delete next[key];
+ return next;
+ });
+ }, []);
+
+ const selectBlock = useCallback((index) => {
+ setSelectedBlockIndex(index);
+ }, []);
+
+ const cycleBlock = useCallback((direction) => {
+ setSelectedBlockIndex(prev => {
+ const next = prev + direction;
+ if (next < 0) return INVENTORY_BLOCKS.length - 1;
+ if (next >= INVENTORY_BLOCKS.length) return 0;
+ return next;
+ });
+ }, []);
+
+ return {
+ blocks,
+ spawnPos,
+ selectedBlockIndex,
+ selectedBlockType: INVENTORY_BLOCKS[selectedBlockIndex],
+ addBlock,
+ removeBlock,
+ selectBlock,
+ cycleBlock,
+ loadChunksAround,
+ };
+}
diff --git a/demos/minecraft-app/src/main.jsx b/demos/minecraft-app/src/main.jsx
new file mode 100644
index 000000000..d76b75874
--- /dev/null
+++ b/demos/minecraft-app/src/main.jsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+
+);
diff --git a/demos/minecraft-app/src/utils/blocks.js b/demos/minecraft-app/src/utils/blocks.js
new file mode 100644
index 000000000..6aed391b1
--- /dev/null
+++ b/demos/minecraft-app/src/utils/blocks.js
@@ -0,0 +1,54 @@
+export const BlockType = {
+ AIR: 0,
+ GRASS: 1,
+ DIRT: 2,
+ STONE: 3,
+ WOOD: 4,
+ LEAVES: 5,
+ SAND: 6,
+ WATER: 7,
+ COBBLESTONE: 8,
+ PLANKS: 9,
+ BRICK: 10,
+ SNOW: 11,
+};
+
+export const BlockColors = {
+ [BlockType.GRASS]: '#4a8c2a',
+ [BlockType.DIRT]: '#8b6914',
+ [BlockType.STONE]: '#808080',
+ [BlockType.WOOD]: '#6b4226',
+ [BlockType.LEAVES]: '#2d6b1e',
+ [BlockType.SAND]: '#d4c36a',
+ [BlockType.WATER]: '#3074b5',
+ [BlockType.COBBLESTONE]: '#6a6a6a',
+ [BlockType.PLANKS]: '#b8945a',
+ [BlockType.BRICK]: '#9b4a3c',
+ [BlockType.SNOW]: '#f0f0f0',
+};
+
+export const BlockNames = {
+ [BlockType.GRASS]: 'Grass',
+ [BlockType.DIRT]: 'Dirt',
+ [BlockType.STONE]: 'Stone',
+ [BlockType.WOOD]: 'Wood',
+ [BlockType.LEAVES]: 'Leaves',
+ [BlockType.SAND]: 'Sand',
+ [BlockType.WATER]: 'Water',
+ [BlockType.COBBLESTONE]: 'Cobblestone',
+ [BlockType.PLANKS]: 'Planks',
+ [BlockType.BRICK]: 'Brick',
+ [BlockType.SNOW]: 'Snow',
+};
+
+export const INVENTORY_BLOCKS = [
+ BlockType.GRASS,
+ BlockType.DIRT,
+ BlockType.STONE,
+ BlockType.WOOD,
+ BlockType.PLANKS,
+ BlockType.COBBLESTONE,
+ BlockType.BRICK,
+ BlockType.SAND,
+ BlockType.SNOW,
+];
diff --git a/demos/minecraft-app/src/utils/noise.js b/demos/minecraft-app/src/utils/noise.js
new file mode 100644
index 000000000..a9d45bb75
--- /dev/null
+++ b/demos/minecraft-app/src/utils/noise.js
@@ -0,0 +1,79 @@
+// Simplex-style noise implementation for terrain generation
+// Based on improved Perlin noise
+
+const F2 = 0.5 * (Math.sqrt(3.0) - 1.0);
+const G2 = (3.0 - Math.sqrt(3.0)) / 6.0;
+
+const grad3 = [
+ [1, 1, 0], [-1, 1, 0], [1, -1, 0], [-1, -1, 0],
+ [1, 0, 1], [-1, 0, 1], [1, 0, -1], [-1, 0, -1],
+ [0, 1, 1], [0, -1, 1], [0, 1, -1], [0, -1, -1],
+];
+
+function buildPermutationTable(seed) {
+ const p = new Array(256);
+ for (let i = 0; i < 256; i++) p[i] = i;
+ // Fisher-Yates shuffle with seed
+ let s = seed;
+ for (let i = 255; i > 0; i--) {
+ s = (s * 16807 + 0) % 2147483647;
+ const j = s % (i + 1);
+ [p[i], p[j]] = [p[j], p[i]];
+ }
+ const perm = new Array(512);
+ const permMod12 = new Array(512);
+ for (let i = 0; i < 512; i++) {
+ perm[i] = p[i & 255];
+ permMod12[i] = perm[i] % 12;
+ }
+ return { perm, permMod12 };
+}
+
+function dot2(g, x, y) {
+ return g[0] * x + g[1] * y;
+}
+
+export function createNoise2D(seed = 42) {
+ const { perm, permMod12 } = buildPermutationTable(seed);
+
+ return function noise2D(xin, yin) {
+ let n0, n1, n2;
+ const s = (xin + yin) * F2;
+ const i = Math.floor(xin + s);
+ const j = Math.floor(yin + s);
+ const t = (i + j) * G2;
+ const X0 = i - t;
+ const Y0 = j - t;
+ const x0 = xin - X0;
+ const y0 = yin - Y0;
+
+ let i1, j1;
+ if (x0 > y0) { i1 = 1; j1 = 0; }
+ else { i1 = 0; j1 = 1; }
+
+ const x1 = x0 - i1 + G2;
+ const y1 = y0 - j1 + G2;
+ const x2 = x0 - 1.0 + 2.0 * G2;
+ const y2 = y0 - 1.0 + 2.0 * G2;
+
+ const ii = i & 255;
+ const jj = j & 255;
+ const gi0 = permMod12[ii + perm[jj]];
+ const gi1 = permMod12[ii + i1 + perm[jj + j1]];
+ const gi2 = permMod12[ii + 1 + perm[jj + 1]];
+
+ let t0 = 0.5 - x0 * x0 - y0 * y0;
+ if (t0 < 0) n0 = 0.0;
+ else { t0 *= t0; n0 = t0 * t0 * dot2(grad3[gi0], x0, y0); }
+
+ let t1 = 0.5 - x1 * x1 - y1 * y1;
+ if (t1 < 0) n1 = 0.0;
+ else { t1 *= t1; n1 = t1 * t1 * dot2(grad3[gi1], x1, y1); }
+
+ let t2 = 0.5 - x2 * x2 - y2 * y2;
+ if (t2 < 0) n2 = 0.0;
+ else { t2 *= t2; n2 = t2 * t2 * dot2(grad3[gi2], x2, y2); }
+
+ return 70.0 * (n0 + n1 + n2);
+ };
+}
diff --git a/demos/minecraft-app/src/utils/terrain.js b/demos/minecraft-app/src/utils/terrain.js
new file mode 100644
index 000000000..245158871
--- /dev/null
+++ b/demos/minecraft-app/src/utils/terrain.js
@@ -0,0 +1,112 @@
+import { createNoise2D } from './noise';
+import { BlockType } from './blocks';
+
+const CHUNK_SIZE = 16;
+const WORLD_HEIGHT = 32;
+const SEA_LEVEL = 10;
+const BASE_HEIGHT = 8;
+
+const noise = createNoise2D(12345);
+const treeNoise = createNoise2D(67890);
+
+function getHeight(worldX, worldZ) {
+ const scale1 = 0.02;
+ const scale2 = 0.05;
+ const scale3 = 0.1;
+
+ const n1 = noise(worldX * scale1, worldZ * scale1) * 12;
+ const n2 = noise(worldX * scale2, worldZ * scale2) * 6;
+ const n3 = noise(worldX * scale3, worldZ * scale3) * 3;
+
+ return Math.floor(BASE_HEIGHT + n1 + n2 + n3);
+}
+
+function shouldPlaceTree(worldX, worldZ, height) {
+ if (height <= SEA_LEVEL) return false;
+ const v = treeNoise(worldX * 0.5, worldZ * 0.5);
+ return v > 0.85;
+}
+
+export function generateChunkBlocks(chunkX, chunkZ) {
+ const blocks = {};
+ const trees = [];
+
+ for (let x = 0; x < CHUNK_SIZE; x++) {
+ for (let z = 0; z < CHUNK_SIZE; z++) {
+ const worldX = chunkX * CHUNK_SIZE + x;
+ const worldZ = chunkZ * CHUNK_SIZE + z;
+ const height = getHeight(worldX, worldZ);
+
+ for (let y = 0; y < WORLD_HEIGHT; y++) {
+ let blockType = BlockType.AIR;
+
+ if (y === 0) {
+ blockType = BlockType.STONE;
+ } else if (y < height - 3) {
+ blockType = BlockType.STONE;
+ } else if (y < height) {
+ blockType = BlockType.DIRT;
+ } else if (y === height) {
+ if (height <= SEA_LEVEL) {
+ blockType = BlockType.SAND;
+ } else {
+ blockType = BlockType.GRASS;
+ }
+ } else if (y <= SEA_LEVEL && y > height) {
+ blockType = BlockType.WATER;
+ }
+
+ if (blockType !== BlockType.AIR) {
+ const key = `${worldX},${y},${worldZ}`;
+ blocks[key] = blockType;
+ }
+ }
+
+ if (shouldPlaceTree(worldX, worldZ, height) && height > SEA_LEVEL) {
+ trees.push({ x: worldX, z: worldZ, baseY: height + 1 });
+ }
+ }
+ }
+
+ // Place trees
+ for (const tree of trees) {
+ const trunkHeight = 4 + Math.floor(Math.abs(treeNoise(tree.x * 0.3, tree.z * 0.3)) * 3);
+ // Trunk
+ for (let y = 0; y < trunkHeight; y++) {
+ const key = `${tree.x},${tree.baseY + y},${tree.z}`;
+ blocks[key] = BlockType.WOOD;
+ }
+ // Leaves canopy
+ const leafBase = tree.baseY + trunkHeight - 2;
+ for (let dy = 0; dy < 4; dy++) {
+ const radius = dy < 3 ? 2 : 1;
+ for (let dx = -radius; dx <= radius; dx++) {
+ for (let dz = -radius; dz <= radius; dz++) {
+ if (dx === 0 && dz === 0 && dy < 2) continue; // trunk space
+ if (Math.abs(dx) === radius && Math.abs(dz) === radius && Math.random() > 0.6) continue;
+ const key = `${tree.x + dx},${leafBase + dy},${tree.z + dz}`;
+ if (!blocks[key]) {
+ blocks[key] = BlockType.LEAVES;
+ }
+ }
+ }
+ }
+ }
+
+ return blocks;
+}
+
+export function getSpawnPosition(worldBlocks) {
+ // Find a safe spawn point near origin
+ for (let x = 0; x < 5; x++) {
+ for (let z = 0; z < 5; z++) {
+ const height = getHeight(x, z);
+ if (height > SEA_LEVEL) {
+ return [x + 0.5, height + 2, z + 0.5];
+ }
+ }
+ }
+ return [8, 20, 8];
+}
+
+export { CHUNK_SIZE, WORLD_HEIGHT };
diff --git a/demos/minecraft-app/vite.config.js b/demos/minecraft-app/vite.config.js
new file mode 100644
index 000000000..9ffcc6757
--- /dev/null
+++ b/demos/minecraft-app/vite.config.js
@@ -0,0 +1,6 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()],
+})
From a1d500b232a67f2f412803e18037e94ba0e23d1c Mon Sep 17 00:00:00 2001
From: agentforce314
Date: Thu, 23 Apr 2026 10:43:28 -0700
Subject: [PATCH 004/251] Sync uv.lock with renamed package (claw-codex ->
clawcodex)
Co-Authored-By: Claude Opus 4.7
---
uv.lock | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/uv.lock b/uv.lock
index b3b4b2772..a85e345d9 100644
--- a/uv.lock
+++ b/uv.lock
@@ -234,7 +234,7 @@ wheels = [
]
[[package]]
-name = "claw-codex"
+name = "clawcodex"
version = "0.1.0"
source = { editable = "." }
dependencies = [
From cd3342ec8848026df13d0f4f585d221ac8c10535 Mon Sep 17 00:00:00 2001
From: agentforce314
Date: Thu, 23 Apr 2026 10:47:07 -0700
Subject: [PATCH 005/251] Add Demos section to README showcasing apps generated
by ClawCodex
Co-Authored-By: Claude Opus 4.7
---
README.md | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/README.md b/README.md
index ef563f362..d8dbc3916 100644
--- a/README.md
+++ b/README.md
@@ -330,6 +330,26 @@ Example:
+***
+
+## 🎨 Demos
+
+**Every app under [`demos/`](demos/) was generated end-to-end by ClawCodex itself** — same CLI you just installed, same agent loop, same tools. No hand-edits 🙂
+
+| Demo | Stack | Description |
+| ---- | ----- | ----------- |
+| [`demos/crm-app`](demos/crm-app) | React 18 + Vite + Vitest | Mini CRM with contacts, deals, dashboard, and a full test suite |
+| [`demos/linkedin-app`](demos/linkedin-app) | React 18 + Vite + React Router | LinkedIn-style feed: profile, network, jobs, messaging |
+| [`demos/minecraft-app`](demos/minecraft-app) | React + three.js + @react-three/fiber | Browser voxel sandbox with terrain, mining, HUD, and player controls |
+
+```bash
+cd demos/crm-app # or linkedin-app / minecraft-app
+npm install
+npm run dev # vite dev server
+```
+
+Want to see how it's done? Open ClawCodex in any empty directory and ask it to build something — these three were generated exactly that way.
+
***
## 🎓 Why ClawCodex?
From 39131b8853988395b83919bde7976bdff2810a80 Mon Sep 17 00:00:00 2001
From: agentforce314
Date: Sat, 25 Apr 2026 00:57:50 -0700
Subject: [PATCH 006/251] Add DeepSeek V4 Pro/Flash via new OpenRouter provider
Wires OpenRouter through the provider registry, login flow, env-var
lookup, and CLI help so users can select it via `clawcodex login` or
`--provider openrouter`. Curated model list features the new
deepseek/deepseek-v4-pro and deepseek/deepseek-v4-flash at the top,
alongside Claude, GPT, Gemini, Llama, Mistral, and Grok variants.
Uses the OpenAI-compatible base URL at https://openrouter.ai/api/v1,
with optional HTTP-Referer / X-Title headers for OpenRouter ranking
attribution.
Co-Authored-By: Claude Opus 4.7
---
README.md | 15 ++--
src/auth/auth.py | 3 +
src/cli.py | 2 +-
src/providers/__init__.py | 45 +++++++++++
src/providers/openrouter_provider.py | 109 +++++++++++++++++++++++++++
5 files changed, 168 insertions(+), 6 deletions(-)
create mode 100644 src/providers/openrouter_provider.py
diff --git a/README.md b/README.md
index d8dbc3916..26cff781e 100644
--- a/README.md
+++ b/README.md
@@ -81,7 +81,7 @@ Explain the code in $path. Start with an analogy, then draw a diagram.
ClawCodex’s main advantage is **multi-provider support**: while Claude Code targets **Claude** models, we aim to support **every major LLM provider** behind the same agent runtime—so you can swap vendors, regions, and price tiers without giving up tools, skills, or the coding loop. That flexibility is what makes agentic coding practical at scale.
```python
-providers = ["anthropic", "openai", "glm", "minimax"] # OpenAI-compatible & GLM APIs; more can be added
+providers = ["anthropic", "openai", "glm", "minimax", "openrouter"] # OpenAI-compatible & GLM APIs; more can be added
```
### Interactive REPL (default) and Textual TUI (opt-in)
@@ -150,7 +150,7 @@ clawcodex --allow-dangerously-skip-permissions # allow /permission-mode
|--------|--------|-------------|
| CLI Entry | ✅ | `clawcodex`, `login`, `config`, `-p` / `--print`, `--tui`, `--stream`, `--version` |
| Interactive REPL | ✅ | Default inline REPL; optional Textual TUI; history, tab completion, multiline |
-| Multi-Provider | ✅ | Anthropic, OpenAI, Zhipu GLM, Minimax |
+| Multi-Provider | ✅ | Anthropic, OpenAI, Zhipu GLM, Minimax, OpenRouter |
| Session Persistence | ✅ | Save/load sessions locally |
| Agent Loop | ✅ | Tool calling loop with streaming and headless mode |
| Skill System | ✅ | SKILL.md-based slash-command skills with args + tool limits |
@@ -213,7 +213,7 @@ clawcodex login
This flow will:
-1. ask you to choose a provider: anthropic / openai / glm / minimax
+1. ask you to choose a provider: anthropic / openai / glm / minimax / openrouter
2. ask for that provider's API key
3. optionally save a custom base URL
4. optionally save a default model
@@ -223,7 +223,7 @@ The configuration file is saved in in `~/.clawcodex/config.json`. Example struct
```json
{
- "default_provider": "glm",
+ "default_provider": "anthropic",
"providers": {
"anthropic": {
"api_key": "base64-encoded-key",
@@ -244,6 +244,11 @@ The configuration file is saved in in `~/.clawcodex/config.json`. Example struct
"api_key": "base64-encoded-key",
"base_url": "https://api.minimaxi.com/anthropic",
"default_model": "MiniMax-M2.7"
+ },
+ "openrouter": {
+ "api_key": "base64-encoded-key",
+ "base_url": "https://openrouter.ai/api/v1",
+ "default_model": "deepseek/deepseek-v4-pro"
}
}
}
@@ -385,7 +390,7 @@ clawcodex/
│ ├── entrypoints/ # Headless (-p) and TUI bootstraps
│ ├── repl/ # Inline REPL (prompt_toolkit + Rich)
│ ├── tui/ # Textual UI (--tui, /tui)
-│ ├── providers/ # Anthropic, OpenAI, GLM, Minimax
+│ ├── providers/ # Anthropic, OpenAI, GLM, Minimax, OpenRouter
│ ├── agent/ # Conversation, session, prompts
│ ├── tool_system/ # Agent loop, tools, schemas
│ ├── skills/ # SKILL.md loading and skill tool
diff --git a/src/auth/auth.py b/src/auth/auth.py
index 8b9030baf..7883c5ab3 100644
--- a/src/auth/auth.py
+++ b/src/auth/auth.py
@@ -20,6 +20,7 @@
_OPENAI_KEY_ENV_VARS = ("OPENAI_API_KEY",)
_AWS_KEY_ENV_VARS = ("AWS_ACCESS_KEY_ID",)
_GEMINI_KEY_ENV_VARS = ("GEMINI_API_KEY", "GOOGLE_API_KEY")
+_OPENROUTER_KEY_ENV_VARS = ("OPENROUTER_API_KEY",)
# Provider → env var lists
_PROVIDER_ENV_VARS: dict[str, tuple[str, ...]] = {
@@ -27,12 +28,14 @@
"openai": _OPENAI_KEY_ENV_VARS,
"aws": _AWS_KEY_ENV_VARS,
"gemini": _GEMINI_KEY_ENV_VARS,
+ "openrouter": _OPENROUTER_KEY_ENV_VARS,
}
# Simple format validators
_KEY_PATTERNS: dict[str, re.Pattern[str]] = {
"anthropic": re.compile(r"^sk-ant-[a-zA-Z0-9_-]{20,}$"),
"openai": re.compile(r"^sk-[a-zA-Z0-9_-]{20,}$"),
+ "openrouter": re.compile(r"^sk-or-[a-zA-Z0-9_-]{20,}$"),
}
diff --git a/src/cli.py b/src/cli.py
index 51985f7f2..f5c85cce2 100644
--- a/src/cli.py
+++ b/src/cli.py
@@ -171,7 +171,7 @@ def _build_parser() -> argparse.ArgumentParser:
'--provider',
type=str,
default=None,
- help='Override the provider (anthropic, openai, glm, minimax)',
+ help='Override the provider (anthropic, openai, glm, minimax, openrouter)',
)
noninteractive.add_argument(
'--allowed-tools',
diff --git a/src/providers/__init__.py b/src/providers/__init__.py
index a9670a72e..fb33c94b5 100644
--- a/src/providers/__init__.py
+++ b/src/providers/__init__.py
@@ -106,6 +106,47 @@ class ProviderInfo(TypedDict):
"MiniMax-M2",
],
},
+ "openrouter": {
+ "label": "OpenRouter (multi-vendor proxy)",
+ "default_base_url": "https://openrouter.ai/api/v1",
+ "default_model": "anthropic/claude-sonnet-4.5",
+ "available_models": [
+ # DeepSeek V4 (latest, strongest — top of the list)
+ "deepseek/deepseek-v4-pro",
+ "deepseek/deepseek-v4-flash",
+ # Anthropic
+ "anthropic/claude-sonnet-4.5",
+ "anthropic/claude-opus-4.1",
+ "anthropic/claude-haiku-4.5",
+ "anthropic/claude-3.5-sonnet",
+ "anthropic/claude-3.5-haiku",
+ # OpenAI
+ "openai/gpt-5",
+ "openai/gpt-5-mini",
+ "openai/gpt-4o",
+ "openai/gpt-4o-mini",
+ "openai/o1",
+ "openai/o1-mini",
+ # Google
+ "google/gemini-2.5-pro",
+ "google/gemini-2.5-flash",
+ "google/gemini-2.0-flash",
+ # Meta
+ "meta-llama/llama-3.3-70b-instruct",
+ "meta-llama/llama-3.1-405b-instruct",
+ # Mistral
+ "mistralai/mistral-large",
+ "mistralai/mixtral-8x22b-instruct",
+ # DeepSeek (V3.x line — V4 is at top of list)
+ "deepseek/deepseek-v3.2",
+ "deepseek/deepseek-v3.2-speciale",
+ "deepseek/deepseek-v3.1-terminus",
+ "deepseek/deepseek-chat-v3.1",
+ "deepseek/deepseek-r1-0528",
+ # xAI
+ "x-ai/grok-2",
+ ],
+ },
}
@@ -134,6 +175,10 @@ def get_provider_class(provider_name: str):
from .minimax_provider import MinimaxProvider
return MinimaxProvider
+ if provider_name == "openrouter":
+ from .openrouter_provider import OpenRouterProvider
+
+ return OpenRouterProvider
raise ValueError(f"Unknown provider: {provider_name}")
diff --git a/src/providers/openrouter_provider.py b/src/providers/openrouter_provider.py
new file mode 100644
index 000000000..a5e86da6d
--- /dev/null
+++ b/src/providers/openrouter_provider.py
@@ -0,0 +1,109 @@
+"""OpenRouter provider implementation.
+
+OpenRouter exposes an OpenAI-compatible API at https://openrouter.ai/api/v1
+that proxies models from many vendors (Anthropic, OpenAI, Google, Meta, etc.).
+Model names follow ``vendor/model`` (e.g. ``anthropic/claude-sonnet-4.5``).
+"""
+
+from __future__ import annotations
+
+from typing import Any, Optional
+
+try:
+ from openai import OpenAI # type: ignore
+except ModuleNotFoundError: # pragma: no cover
+ OpenAI = None
+
+from .openai_compatible import OpenAICompatibleProvider
+
+
+class OpenRouterProvider(OpenAICompatibleProvider):
+ """OpenRouter provider using the OpenAI SDK against the OpenRouter base URL."""
+
+ DEFAULT_BASE_URL = "https://openrouter.ai/api/v1"
+
+ def __init__(
+ self, api_key: str, base_url: Optional[str] = None, model: Optional[str] = None
+ ):
+ """Initialize OpenRouter provider.
+
+ Args:
+ api_key: OpenRouter API key (sk-or-...)
+ base_url: Base URL (optional, defaults to https://openrouter.ai/api/v1)
+ model: Default model in ``vendor/model`` form (default: anthropic/claude-sonnet-4.5)
+ """
+ super().__init__(
+ api_key,
+ base_url or self.DEFAULT_BASE_URL,
+ model or "anthropic/claude-sonnet-4.5",
+ )
+
+ def _create_client(self) -> Any:
+ """Create OpenAI SDK client pointed at OpenRouter."""
+ if OpenAI is None: # pragma: no cover
+ raise ModuleNotFoundError(
+ "openai package is not installed. Install optional dependencies to use OpenRouterProvider."
+ )
+ kwargs: dict[str, Any] = {
+ "api_key": self.api_key,
+ "base_url": self.base_url or self.DEFAULT_BASE_URL,
+ }
+ # Optional ranking/attribution headers honored by OpenRouter.
+ import os
+ default_headers: dict[str, str] = {}
+ referer = os.environ.get("OPENROUTER_HTTP_REFERER")
+ title = os.environ.get("OPENROUTER_X_TITLE")
+ if referer:
+ default_headers["HTTP-Referer"] = referer
+ if title:
+ default_headers["X-Title"] = title
+ if default_headers:
+ kwargs["default_headers"] = default_headers
+
+ if os.environ.get("CLAWCODEX_SSL_VERIFY", "").lower() in ("0", "false", "no"):
+ import httpx
+ kwargs["http_client"] = httpx.Client(verify=False)
+ return OpenAI(**kwargs)
+
+ def get_available_models(self) -> list[str]:
+ """Return a curated list of popular OpenRouter model IDs.
+
+ OpenRouter supports hundreds of models; this list is a starting point —
+ any valid ``vendor/model`` ID accepted by OpenRouter can be used.
+ """
+ return [
+ # DeepSeek V4 (latest, strongest — top of the list)
+ "deepseek/deepseek-v4-pro",
+ "deepseek/deepseek-v4-flash",
+ # Anthropic
+ "anthropic/claude-sonnet-4.5",
+ "anthropic/claude-opus-4.1",
+ "anthropic/claude-haiku-4.5",
+ "anthropic/claude-3.5-sonnet",
+ "anthropic/claude-3.5-haiku",
+ # OpenAI
+ "openai/gpt-5",
+ "openai/gpt-5-mini",
+ "openai/gpt-4o",
+ "openai/gpt-4o-mini",
+ "openai/o1",
+ "openai/o1-mini",
+ # Google
+ "google/gemini-2.5-pro",
+ "google/gemini-2.5-flash",
+ "google/gemini-2.0-flash",
+ # Meta
+ "meta-llama/llama-3.3-70b-instruct",
+ "meta-llama/llama-3.1-405b-instruct",
+ # Mistral
+ "mistralai/mistral-large",
+ "mistralai/mixtral-8x22b-instruct",
+ # DeepSeek (V3.x line — V4 is at top of list)
+ "deepseek/deepseek-v3.2",
+ "deepseek/deepseek-v3.2-speciale",
+ "deepseek/deepseek-v3.1-terminus",
+ "deepseek/deepseek-chat-v3.1",
+ "deepseek/deepseek-r1-0528",
+ # xAI
+ "x-ai/grok-2",
+ ]
From 71d8b455bbba20e5a973fb66943a67747b3052e7 Mon Sep 17 00:00:00 2001
From: agentforce314
Date: Sat, 25 Apr 2026 01:10:11 -0700
Subject: [PATCH 007/251] Add direct DeepSeek provider (V4 Pro/Flash via
api.deepseek.com)
Adds a first-class DeepSeek provider so users can hit DeepSeek's
OpenAI-compatible endpoint directly without going through OpenRouter.
Default model is deepseek-v4-pro; deepseek-v4-flash is also offered
alongside the legacy deepseek-chat / deepseek-reasoner aliases.
Wires DeepSeek through the provider registry, login flow, the
DEEPSEEK_API_KEY env-var lookup, and the CLI --provider help string.
Co-Authored-By: Claude Opus 4.7
---
README.md | 13 ++++--
src/auth/auth.py | 3 ++
src/cli.py | 2 +-
src/providers/__init__.py | 17 +++++++
src/providers/deepseek_provider.py | 72 ++++++++++++++++++++++++++++++
5 files changed, 102 insertions(+), 5 deletions(-)
create mode 100644 src/providers/deepseek_provider.py
diff --git a/README.md b/README.md
index 26cff781e..25b858c4f 100644
--- a/README.md
+++ b/README.md
@@ -81,7 +81,7 @@ Explain the code in $path. Start with an analogy, then draw a diagram.
ClawCodex’s main advantage is **multi-provider support**: while Claude Code targets **Claude** models, we aim to support **every major LLM provider** behind the same agent runtime—so you can swap vendors, regions, and price tiers without giving up tools, skills, or the coding loop. That flexibility is what makes agentic coding practical at scale.
```python
-providers = ["anthropic", "openai", "glm", "minimax", "openrouter"] # OpenAI-compatible & GLM APIs; more can be added
+providers = ["anthropic", "openai", "glm", "minimax", "openrouter", "deepseek"] # OpenAI-compatible & GLM APIs; more can be added
```
### Interactive REPL (default) and Textual TUI (opt-in)
@@ -150,7 +150,7 @@ clawcodex --allow-dangerously-skip-permissions # allow /permission-mode
|--------|--------|-------------|
| CLI Entry | ✅ | `clawcodex`, `login`, `config`, `-p` / `--print`, `--tui`, `--stream`, `--version` |
| Interactive REPL | ✅ | Default inline REPL; optional Textual TUI; history, tab completion, multiline |
-| Multi-Provider | ✅ | Anthropic, OpenAI, Zhipu GLM, Minimax, OpenRouter |
+| Multi-Provider | ✅ | Anthropic, OpenAI, Zhipu GLM, Minimax, OpenRouter, DeepSeek |
| Session Persistence | ✅ | Save/load sessions locally |
| Agent Loop | ✅ | Tool calling loop with streaming and headless mode |
| Skill System | ✅ | SKILL.md-based slash-command skills with args + tool limits |
@@ -213,7 +213,7 @@ clawcodex login
This flow will:
-1. ask you to choose a provider: anthropic / openai / glm / minimax / openrouter
+1. ask you to choose a provider: anthropic / openai / glm / minimax / openrouter / deepseek
2. ask for that provider's API key
3. optionally save a custom base URL
4. optionally save a default model
@@ -249,6 +249,11 @@ The configuration file is saved in in `~/.clawcodex/config.json`. Example struct
"api_key": "base64-encoded-key",
"base_url": "https://openrouter.ai/api/v1",
"default_model": "deepseek/deepseek-v4-pro"
+ },
+ "deepseek": {
+ "api_key": "base64-encoded-key",
+ "base_url": "https://api.deepseek.com",
+ "default_model": "deepseek-v4-pro"
}
}
}
@@ -390,7 +395,7 @@ clawcodex/
│ ├── entrypoints/ # Headless (-p) and TUI bootstraps
│ ├── repl/ # Inline REPL (prompt_toolkit + Rich)
│ ├── tui/ # Textual UI (--tui, /tui)
-│ ├── providers/ # Anthropic, OpenAI, GLM, Minimax, OpenRouter
+│ ├── providers/ # Anthropic, OpenAI, GLM, Minimax, OpenRouter, DeepSeek
│ ├── agent/ # Conversation, session, prompts
│ ├── tool_system/ # Agent loop, tools, schemas
│ ├── skills/ # SKILL.md loading and skill tool
diff --git a/src/auth/auth.py b/src/auth/auth.py
index 7883c5ab3..41ef082f9 100644
--- a/src/auth/auth.py
+++ b/src/auth/auth.py
@@ -21,6 +21,7 @@
_AWS_KEY_ENV_VARS = ("AWS_ACCESS_KEY_ID",)
_GEMINI_KEY_ENV_VARS = ("GEMINI_API_KEY", "GOOGLE_API_KEY")
_OPENROUTER_KEY_ENV_VARS = ("OPENROUTER_API_KEY",)
+_DEEPSEEK_KEY_ENV_VARS = ("DEEPSEEK_API_KEY",)
# Provider → env var lists
_PROVIDER_ENV_VARS: dict[str, tuple[str, ...]] = {
@@ -29,6 +30,7 @@
"aws": _AWS_KEY_ENV_VARS,
"gemini": _GEMINI_KEY_ENV_VARS,
"openrouter": _OPENROUTER_KEY_ENV_VARS,
+ "deepseek": _DEEPSEEK_KEY_ENV_VARS,
}
# Simple format validators
@@ -36,6 +38,7 @@
"anthropic": re.compile(r"^sk-ant-[a-zA-Z0-9_-]{20,}$"),
"openai": re.compile(r"^sk-[a-zA-Z0-9_-]{20,}$"),
"openrouter": re.compile(r"^sk-or-[a-zA-Z0-9_-]{20,}$"),
+ "deepseek": re.compile(r"^sk-[a-zA-Z0-9_-]{20,}$"),
}
diff --git a/src/cli.py b/src/cli.py
index f5c85cce2..c29cb3ca3 100644
--- a/src/cli.py
+++ b/src/cli.py
@@ -171,7 +171,7 @@ def _build_parser() -> argparse.ArgumentParser:
'--provider',
type=str,
default=None,
- help='Override the provider (anthropic, openai, glm, minimax, openrouter)',
+ help='Override the provider (anthropic, openai, glm, minimax, openrouter, deepseek)',
)
noninteractive.add_argument(
'--allowed-tools',
diff --git a/src/providers/__init__.py b/src/providers/__init__.py
index fb33c94b5..dd629247d 100644
--- a/src/providers/__init__.py
+++ b/src/providers/__init__.py
@@ -106,6 +106,19 @@ class ProviderInfo(TypedDict):
"MiniMax-M2",
],
},
+ "deepseek": {
+ "label": "DeepSeek",
+ "default_base_url": "https://api.deepseek.com",
+ "default_model": "deepseek-v4-pro",
+ "available_models": [
+ # V4 series (current)
+ "deepseek-v4-pro",
+ "deepseek-v4-flash",
+ # Legacy aliases (being deprecated; map to v4-flash modes)
+ "deepseek-chat",
+ "deepseek-reasoner",
+ ],
+ },
"openrouter": {
"label": "OpenRouter (multi-vendor proxy)",
"default_base_url": "https://openrouter.ai/api/v1",
@@ -179,6 +192,10 @@ def get_provider_class(provider_name: str):
from .openrouter_provider import OpenRouterProvider
return OpenRouterProvider
+ if provider_name == "deepseek":
+ from .deepseek_provider import DeepSeekProvider
+
+ return DeepSeekProvider
raise ValueError(f"Unknown provider: {provider_name}")
diff --git a/src/providers/deepseek_provider.py b/src/providers/deepseek_provider.py
new file mode 100644
index 000000000..bcd4d48c3
--- /dev/null
+++ b/src/providers/deepseek_provider.py
@@ -0,0 +1,72 @@
+"""DeepSeek provider implementation.
+
+DeepSeek exposes an OpenAI-compatible API at https://api.deepseek.com.
+Current production models are ``deepseek-v4-pro`` and ``deepseek-v4-flash``;
+the legacy aliases ``deepseek-chat`` / ``deepseek-reasoner`` are being
+deprecated and resolve to the non-thinking / thinking modes of
+``deepseek-v4-flash`` respectively.
+"""
+
+from __future__ import annotations
+
+from typing import Any, Optional
+
+try:
+ from openai import OpenAI # type: ignore
+except ModuleNotFoundError: # pragma: no cover
+ OpenAI = None
+
+from .openai_compatible import OpenAICompatibleProvider
+
+
+class DeepSeekProvider(OpenAICompatibleProvider):
+ """DeepSeek provider using the OpenAI SDK against the DeepSeek base URL."""
+
+ DEFAULT_BASE_URL = "https://api.deepseek.com"
+
+ def __init__(
+ self, api_key: str, base_url: Optional[str] = None, model: Optional[str] = None
+ ):
+ """Initialize DeepSeek provider.
+
+ Args:
+ api_key: DeepSeek API key (sk-...)
+ base_url: Base URL (optional, defaults to https://api.deepseek.com)
+ model: Default model (default: deepseek-v4-pro)
+ """
+ super().__init__(
+ api_key,
+ base_url or self.DEFAULT_BASE_URL,
+ model or "deepseek-v4-pro",
+ )
+
+ def _create_client(self) -> Any:
+ """Create OpenAI SDK client pointed at DeepSeek."""
+ if OpenAI is None: # pragma: no cover
+ raise ModuleNotFoundError(
+ "openai package is not installed. Install optional dependencies to use DeepSeekProvider."
+ )
+ kwargs: dict[str, Any] = {
+ "api_key": self.api_key,
+ "base_url": self.base_url or self.DEFAULT_BASE_URL,
+ }
+ import os
+ if os.environ.get("CLAWCODEX_SSL_VERIFY", "").lower() in ("0", "false", "no"):
+ import httpx
+ kwargs["http_client"] = httpx.Client(verify=False)
+ return OpenAI(**kwargs)
+
+ def get_available_models(self) -> list[str]:
+ """Return DeepSeek's current production models.
+
+ ``deepseek-chat`` and ``deepseek-reasoner`` are kept for backward
+ compatibility but DeepSeek has announced they will be deprecated.
+ """
+ return [
+ # V4 series (current)
+ "deepseek-v4-pro",
+ "deepseek-v4-flash",
+ # Legacy aliases (being deprecated; map to v4-flash modes)
+ "deepseek-chat",
+ "deepseek-reasoner",
+ ]
From dccaca40a2a8e691dc4b9be201f1aaad8f9a78a9 Mon Sep 17 00:00:00 2001
From: Jiatong Chen
Date: Sat, 25 Apr 2026 23:33:02 -0700
Subject: [PATCH 008/251] Fix AskUserQuestion options schema for API
validation.
Define structured items for options and add multiSelect so tool parameters satisfy OpenAI-compatible JSON Schema validation.
Made-with: Cursor
---
src/tool_system/tools/ask_user_question.py | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/src/tool_system/tools/ask_user_question.py b/src/tool_system/tools/ask_user_question.py
index 99fc1d9fe..e4e1f25f5 100644
--- a/src/tool_system/tools/ask_user_question.py
+++ b/src/tool_system/tools/ask_user_question.py
@@ -46,7 +46,19 @@ def _ask_user_question_call(tool_input: dict[str, Any], context: ToolContext) ->
"properties": {
"question": {"type": "string"},
"header": {"type": "string"},
- "options": {"type": "array"},
+ "multiSelect": {"type": "boolean"},
+ "options": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "label": {"type": "string"},
+ "description": {"type": "string"},
+ "preview": {"type": "string"},
+ },
+ "required": ["label"],
+ },
+ },
},
"required": ["question"],
},
From 920e14d8fa22fc314e57048a0dbb09f189f0872c Mon Sep 17 00:00:00 2001
From: Fumian Chen <475615524@qq.com>
Date: Sun, 26 Apr 2026 22:17:59 -0400
Subject: [PATCH 009/251] fix: Fixed DeepSeek thinking-mode replay failures and
noisy permission prompts
---
src/providers/openai_compatible.py | 5 +
src/query/query.py | 3 +
src/repl/core.py | 134 +++++++++++++++-----------
src/tool_system/agent_loop.py | 4 +
src/types/messages.py | 12 ++-
tests/test_claude_code_tool_parity.py | 43 +++++++++
tests/test_message_normalization.py | 9 ++
tests/test_providers.py | 23 +++++
tests/test_query_loop.py | 56 +++++++++++
tests/test_repl.py | 90 +++++++++++++++++
10 files changed, 320 insertions(+), 59 deletions(-)
diff --git a/src/providers/openai_compatible.py b/src/providers/openai_compatible.py
index d32fb8150..4aa279ac4 100644
--- a/src/providers/openai_compatible.py
+++ b/src/providers/openai_compatible.py
@@ -67,6 +67,11 @@ def _convert_anthropic_messages_to_openai(
"role": "assistant",
"tool_calls": tool_calls,
}
+ # DeepSeek thinking mode requires reasoning_content to be
+ # replayed together with assistant tool call messages.
+ reasoning_content = msg.get("reasoning_content")
+ if isinstance(reasoning_content, str) and reasoning_content:
+ assistant_msg["reasoning_content"] = reasoning_content
text_content = "\n".join(text_parts) if text_parts else None
if text_content:
assistant_msg["content"] = text_content
diff --git a/src/query/query.py b/src/query/query.py
index 330f7a3bb..be8f29e6a 100644
--- a/src/query/query.py
+++ b/src/query/query.py
@@ -270,6 +270,9 @@ async def _call_model_sync(
content=assistant_blocks if assistant_blocks else "",
stop_reason=stop_reason,
)
+ if response.reasoning_content:
+ # Preserve provider thinking metadata for follow-up turns.
+ assistant_msg.reasoning_content = response.reasoning_content # type: ignore[attr-defined]
if stop_reason == "max_tokens":
assistant_msg._api_error = "max_output_tokens" # type: ignore[attr-defined]
diff --git a/src/repl/core.py b/src/repl/core.py
index 47b59f39a..a6ddb5328 100644
--- a/src/repl/core.py
+++ b/src/repl/core.py
@@ -354,6 +354,13 @@ def __init__(
# reference's "type while it's still thinking" affordance.
self._queued_prompts: list[str] = []
self._queued_prompts_lock = threading.Lock()
+ # Permission dialogs can be requested from different worker paths
+ # (e.g. subagents/tools). Serialize interactive prompts so we never
+ # mount competing prompt_toolkit applications at once.
+ self._permission_prompt_lock = threading.Lock()
+ # Session-level cache for permission decisions (tool_name -> allow/deny)
+ # so identical prompts in loops don't repeatedly interrupt the user.
+ self._permission_decision_cache: dict[str, bool] = {}
# The currently mounted ``LiveStatus`` (if any). ``_safe_input``
# pauses it before reading a synchronous answer (e.g. permission
@@ -671,65 +678,76 @@ def _handle_permission_request(
Tuple of (allowed: bool, continue_without_caching: bool).
continue_without_caching is always False since we don't cache in REPL.
"""
- # Stop the Rich status spinner if running, so we can get clean input
- if self._current_status is not None:
- try:
- self._current_status.stop()
- except Exception:
- pass
+ with self._permission_prompt_lock:
+ cache_key = tool_name.strip().lower()
+ cached = self._permission_decision_cache.get(cache_key)
+ if cached is not None:
+ return cached, False
+
+ # Stop the Rich status spinner if running, so we can get clean input
+ if self._current_status is not None:
+ try:
+ self._current_status.stop()
+ except Exception:
+ pass
- self.console.print("")
- self.console.print("[bold yellow]⚠ Permission Required[/bold yellow]")
- self.console.print(f" {message}")
- self.console.print("")
-
- # Determine if this is a setting that can be enabled
- can_enable_setting = False
- setting_to_enable: str | None = None
-
- msg_lower = message.lower()
- if "allow_docs" in msg_lower or "documentation files" in msg_lower:
- if not self.tool_context.allow_docs:
- can_enable_setting = True
- setting_to_enable = "allow_docs"
-
- # Build options
- options: list[tuple[str, str]] = [
- ("y", "Yes, allow this action"),
- ("n", "No, deny this action"),
- ]
- if can_enable_setting:
- options.insert(0, ("e", f"Enable {setting_to_enable} and allow"))
-
- self.console.print("[bold]Options:[/bold]")
- for i, (key, desc) in enumerate(options, start=1):
- self.console.print(f" {i}. [{key}] {desc}")
- self.console.print("")
-
- # Get input via prompt_toolkit so it cooperates with patch_stdout()
- # and the LiveStatus bottom region.
- choice = self._safe_input("Select option> ").strip().lower()
-
- # Parse choice based on the actual displayed options
- if can_enable_setting:
- # Menu: 1=Enable, 2=Yes, 3=No
- if choice in ("1", "e", "enable"):
- self._enable_permission_setting(setting_to_enable)
- return True, False
- elif choice in ("2", "y", "yes", ""):
- return True, False
- elif choice in ("3", "n", "no"):
- return False, False
- else:
- # Menu: 1=Yes, 2=No
- if choice in ("1", "y", "yes", ""):
- return True, False
- elif choice in ("2", "n", "no"):
- return False, False
-
- # Default to deny for invalid input
- self.console.print("[dim]Invalid choice, defaulting to deny.[/dim]")
- return False, False
+ self.console.print("")
+ self.console.print("[bold yellow]⚠ Permission Required[/bold yellow]")
+ self.console.print(f" {message}")
+ self.console.print("")
+
+ # Determine if this is a setting that can be enabled
+ can_enable_setting = False
+ setting_to_enable: str | None = None
+
+ msg_lower = message.lower()
+ if "allow_docs" in msg_lower or "documentation files" in msg_lower:
+ if not self.tool_context.allow_docs:
+ can_enable_setting = True
+ setting_to_enable = "allow_docs"
+
+ # Build options
+ options: list[tuple[str, str]] = [
+ ("y", "Yes, allow this action"),
+ ("n", "No, deny this action"),
+ ]
+ if can_enable_setting:
+ options.insert(0, ("e", f"Enable {setting_to_enable} and allow"))
+
+ self.console.print("[bold]Options:[/bold]")
+ for i, (key, desc) in enumerate(options, start=1):
+ self.console.print(f" {i}. [{key}] {desc}")
+ self.console.print("")
+
+ # Get input via prompt_toolkit so it cooperates with patch_stdout()
+ # and the LiveStatus bottom region.
+ choice = self._safe_input("Select option> ").strip().lower()
+
+ # Parse choice based on the actual displayed options
+ if can_enable_setting:
+ # Menu: 1=Enable, 2=Yes, 3=No
+ if choice in ("1", "e", "enable"):
+ self._enable_permission_setting(setting_to_enable)
+ self._permission_decision_cache[cache_key] = True
+ return True, False
+ elif choice in ("2", "y", "yes", ""):
+ self._permission_decision_cache[cache_key] = True
+ return True, False
+ elif choice in ("3", "n", "no"):
+ self._permission_decision_cache[cache_key] = False
+ return False, False
+ else:
+ # Menu: 1=Yes, 2=No
+ if choice in ("1", "y", "yes", ""):
+ self._permission_decision_cache[cache_key] = True
+ return True, False
+ elif choice in ("2", "n", "no"):
+ self._permission_decision_cache[cache_key] = False
+ return False, False
+
+ # Default to deny for invalid input
+ self.console.print("[dim]Invalid choice, defaulting to deny.[/dim]")
+ return False, False
def _enable_permission_setting(self, setting_name: str | None) -> None:
"""Enable a permission setting in the tool context."""
diff --git a/src/tool_system/agent_loop.py b/src/tool_system/agent_loop.py
index f5402b4da..eccc54d17 100644
--- a/src/tool_system/agent_loop.py
+++ b/src/tool_system/agent_loop.py
@@ -363,6 +363,10 @@ def _check_cancel() -> None:
conversation.add_assistant_message(final_assistant_content)
# Add assistant message to OpenAI messages (text only)
openai_assistant_msg: dict[str, Any] = {"role": "assistant", "content": final_assistant_content}
+ # DeepSeek/GLM thinking modes require reasoning_content to be replayed
+ # on subsequent turns when the assistant response included it.
+ if response.reasoning_content:
+ openai_assistant_msg["reasoning_content"] = response.reasoning_content
# If there are tool_uses, add them in OpenAI format
if response.tool_uses:
# Build OpenAI tool_calls
diff --git a/src/types/messages.py b/src/types/messages.py
index 4aae46762..3b763a97a 100644
--- a/src/types/messages.py
+++ b/src/types/messages.py
@@ -344,7 +344,14 @@ def normalize_message_for_api(message: MessageLike) -> dict[str, Any] | None:
else:
normalized_content = str(content)
- return {"role": api_role, "content": normalized_content}
+ result: dict[str, Any] = {"role": api_role, "content": normalized_content}
+ # DeepSeek/GLM thinking modes may require replaying assistant
+ # reasoning_content on follow-up turns.
+ if api_role == "assistant":
+ reasoning_content = _get_field(message, "reasoning_content", None)
+ if isinstance(reasoning_content, str) and reasoning_content:
+ result["reasoning_content"] = reasoning_content
+ return result
def ensure_tool_result_pairing(
@@ -407,6 +414,9 @@ def ensure_tool_result_pairing(
final_content = [{"type": "text", "text": "[Tool use interrupted]"}]
assistant_msg = {"role": "assistant", "content": final_content}
+ reasoning_content = msg.get("reasoning_content")
+ if isinstance(reasoning_content, str) and reasoning_content:
+ assistant_msg["reasoning_content"] = reasoning_content
result.append(assistant_msg)
tool_use_ids = list(seen_tool_use_ids)
diff --git a/tests/test_claude_code_tool_parity.py b/tests/test_claude_code_tool_parity.py
index 59601fb8d..e1a0e30f4 100644
--- a/tests/test_claude_code_tool_parity.py
+++ b/tests/test_claude_code_tool_parity.py
@@ -132,6 +132,49 @@ def test_todo_write_roundtrip(self) -> None:
)
self.assertEqual(self.ctx.todos, [])
+ def test_openai_messages_preserve_reasoning_content_across_tool_turns(self) -> None:
+ conversation = Conversation()
+ conversation.add_user_message("hi")
+
+ mock_provider = MagicMock()
+ mock_provider.__class__.__name__ = "DeepSeekProvider"
+ first = ChatResponse(
+ content="Let me check",
+ model="deepseek-v4-pro",
+ usage={"input_tokens": 1, "output_tokens": 1},
+ finish_reason="tool_calls",
+ reasoning_content="hidden chain of thought token stream",
+ tool_uses=[{"id": "toolu_1", "name": "SendUserMessage", "input": {"message": "working"}}],
+ )
+ second = ChatResponse(
+ content="done",
+ model="deepseek-v4-pro",
+ usage={"input_tokens": 1, "output_tokens": 1},
+ finish_reason="stop",
+ tool_uses=None,
+ )
+ mock_provider.chat.side_effect = [first, second]
+
+ out = run_agent_loop(
+ conversation=conversation,
+ provider=mock_provider,
+ tool_registry=self.registry,
+ tool_context=self.ctx,
+ verbose=False,
+ )
+ self.assertEqual(out.response_text, "done")
+ self.assertEqual(mock_provider.chat.call_count, 2)
+ second_call_messages = mock_provider.chat.call_args_list[1].args[0]
+ assistant_with_tool_call = next(
+ msg
+ for msg in second_call_messages
+ if msg.get("role") == "assistant" and msg.get("tool_calls")
+ )
+ self.assertEqual(
+ assistant_with_tool_call.get("reasoning_content"),
+ "hidden chain of thought token stream",
+ )
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/test_message_normalization.py b/tests/test_message_normalization.py
index dff1e3f28..f66aa6873 100644
--- a/tests/test_message_normalization.py
+++ b/tests/test_message_normalization.py
@@ -94,6 +94,15 @@ def test_normalizes_legacy_message_objects(self):
self.assertEqual(normalized[0]["content"][0]["text"], "legacy")
self.assertEqual(normalized[0]["content"][1]["name"], "Grep")
+ def test_preserves_assistant_reasoning_content(self):
+ assistant = AssistantMessage(content=[TextBlock(text="working")])
+ assistant.reasoning_content = "internal reasoning trace" # type: ignore[attr-defined]
+
+ normalized = normalize_messages_for_api([assistant])
+
+ self.assertEqual(normalized[0]["role"], "assistant")
+ self.assertEqual(normalized[0]["reasoning_content"], "internal reasoning trace")
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/test_providers.py b/tests/test_providers.py
index 7019817ed..40b5c4edc 100644
--- a/tests/test_providers.py
+++ b/tests/test_providers.py
@@ -8,6 +8,7 @@
from src.providers import get_provider_class
from src.providers.anthropic_provider import AnthropicProvider
from src.providers.glm_provider import GLMProvider
+from src.providers.openai_compatible import _convert_anthropic_messages_to_openai
from src.providers.openai_provider import OpenAIProvider
from src.providers.base import ChatMessage, ChatResponse
@@ -189,6 +190,28 @@ def test_get_available_models(self):
self.assertIn("gpt-4", models)
self.assertIn("gpt-4o", models)
+ def test_converter_preserves_reasoning_with_tool_calls(self):
+ messages = [
+ {
+ "role": "assistant",
+ "reasoning_content": "keep-me",
+ "content": [
+ {"type": "text", "text": "Thinking..."},
+ {
+ "type": "tool_use",
+ "id": "toolu_1",
+ "name": "TaskCreate",
+ "input": {"title": "todo"},
+ },
+ ],
+ }
+ ]
+ converted = _convert_anthropic_messages_to_openai(messages)
+ self.assertEqual(len(converted), 1)
+ self.assertEqual(converted[0]["role"], "assistant")
+ self.assertEqual(converted[0]["reasoning_content"], "keep-me")
+ self.assertTrue(converted[0]["tool_calls"])
+
@patch("src.providers.openai_provider.OpenAI")
def test_chat(self, mock_openai):
"""Test synchronous chat."""
diff --git a/tests/test_query_loop.py b/tests/test_query_loop.py
index 28afd20e1..3dad14f36 100644
--- a/tests/test_query_loop.py
+++ b/tests/test_query_loop.py
@@ -129,6 +129,62 @@ async def run():
]
self.assertGreaterEqual(len(tool_results), 1)
+ def test_multi_turn_replays_reasoning_content_for_followup(self):
+ provider = MagicMock()
+ provider.chat_stream_response.side_effect = NotImplementedError()
+
+ first = ChatResponse(
+ content="I'll handle this",
+ model="deepseek-v4-pro",
+ usage={"input_tokens": 10, "output_tokens": 20},
+ finish_reason="tool_use",
+ reasoning_content="thinking trace from provider",
+ tool_uses=[{
+ "id": "toolu_001",
+ "name": "Write",
+ "input": {
+ "file_path": str(self.workspace / "reasoning.txt"),
+ "content": "hello",
+ },
+ }],
+ )
+ second = ChatResponse(
+ content="Done",
+ model="deepseek-v4-pro",
+ usage={"input_tokens": 30, "output_tokens": 10},
+ finish_reason="end_turn",
+ tool_uses=None,
+ )
+ provider.chat.side_effect = [first, second]
+
+ params = QueryParams(
+ messages=[UserMessage(content="Create file")],
+ system_prompt="You are helpful.",
+ tools=self.registry.list_tools(),
+ tool_registry=self.registry,
+ tool_use_context=self.context,
+ provider=provider,
+ abort_controller=self.abort,
+ max_turns=10,
+ )
+
+ async def run():
+ async for _msg in query(params):
+ pass
+
+ _run(run())
+
+ self.assertEqual(provider.chat.call_count, 2)
+ second_call_messages = provider.chat.call_args_list[1].args[0]
+ assistant_with_tool_use = next(
+ msg for msg in second_call_messages
+ if msg.get("role") == "assistant" and isinstance(msg.get("content"), list)
+ )
+ self.assertEqual(
+ assistant_with_tool_use.get("reasoning_content"),
+ "thinking trace from provider",
+ )
+
def test_max_turns_limit(self):
provider = MagicMock()
provider.chat_stream_response.side_effect = NotImplementedError()
diff --git a/tests/test_repl.py b/tests/test_repl.py
index ca9f958e4..b20f4c6b4 100644
--- a/tests/test_repl.py
+++ b/tests/test_repl.py
@@ -7,6 +7,8 @@
from pathlib import Path
import tempfile
import json
+import threading
+import time
from rich.markdown import Markdown
from src.repl import ClawcodexREPL
@@ -429,6 +431,94 @@ def test_load_nonexistent_session(self):
# Session should not change
self.assertEqual(repl.session, original_session)
+ def test_permission_prompt_is_serialized(self):
+ """Concurrent permission checks should not open overlapping prompts."""
+ with patch('src.repl.core.get_provider_config', return_value={
+ "api_key": "test_api_key_12345678",
+ "base_url": "https://open.bigmodel.cn/api/paas/v4",
+ "default_model": "glm-4.5",
+ }), patch('src.repl.core.PromptSession') as mock_prompt_session:
+ mock_prompt_session.return_value = Mock(prompt=Mock(return_value=""))
+ with patch('src.repl.core.Session.create'):
+ with patch('src.repl.core.get_provider_class') as mock_provider_class:
+ mock_provider = Mock()
+ mock_provider.model = "glm-4.5"
+ mock_provider_class.return_value = mock_provider
+ repl = ClawcodexREPL(provider_name="glm")
+ repl.console.print = Mock()
+
+ in_prompt = 0
+ max_in_prompt = 0
+ counter_lock = threading.Lock()
+
+ def fake_input(_prompt: str) -> str:
+ nonlocal in_prompt, max_in_prompt
+ with counter_lock:
+ in_prompt += 1
+ if in_prompt > max_in_prompt:
+ max_in_prompt = in_prompt
+ time.sleep(0.03)
+ with counter_lock:
+ in_prompt -= 1
+ return "1"
+
+ repl._safe_input = fake_input # type: ignore[assignment]
+
+ t1 = threading.Thread(
+ target=repl._handle_permission_request,
+ args=("Grep", "Claude wants to use Grep. Allow?", None),
+ )
+ t2 = threading.Thread(
+ target=repl._handle_permission_request,
+ args=("Read", "Claude wants to use Read. Allow?", None),
+ )
+ t1.start()
+ t2.start()
+ t1.join()
+ t2.join()
+
+ self.assertEqual(max_in_prompt, 1)
+
+ def test_permission_prompt_cached_per_tool(self):
+ """After first decision, same tool should not prompt again."""
+ with patch('src.repl.core.get_provider_config', return_value={
+ "api_key": "test_api_key_12345678",
+ "base_url": "https://open.bigmodel.cn/api/paas/v4",
+ "default_model": "glm-4.5",
+ }), patch('src.repl.core.PromptSession') as mock_prompt_session:
+ mock_prompt_session.return_value = Mock(prompt=Mock(return_value=""))
+ with patch('src.repl.core.Session.create'):
+ with patch('src.repl.core.get_provider_class') as mock_provider_class:
+ mock_provider = Mock()
+ mock_provider.model = "glm-4.5"
+ mock_provider_class.return_value = mock_provider
+ repl = ClawcodexREPL(provider_name="glm")
+ repl.console.print = Mock()
+
+ prompt_calls = 0
+
+ def fake_input(_prompt: str) -> str:
+ nonlocal prompt_calls
+ prompt_calls += 1
+ return "1"
+
+ repl._safe_input = fake_input # type: ignore[assignment]
+
+ first = repl._handle_permission_request(
+ "Grep",
+ "Claude wants to use Grep. Allow?",
+ None,
+ )
+ second = repl._handle_permission_request(
+ "Grep",
+ "Claude wants to use Grep. Allow?",
+ None,
+ )
+
+ self.assertEqual(first, (True, False))
+ self.assertEqual(second, (True, False))
+ self.assertEqual(prompt_calls, 1)
+
class TestConversation(unittest.TestCase):
"""Test conversation management."""
From 34d8d6c554315bb49326df95dd65cb58dbd2d68c Mon Sep 17 00:00:00 2001
From: agentforce314
Date: Mon, 27 Apr 2026 16:42:15 -0700
Subject: [PATCH 010/251] Add Adopt Me-style pet game demo (React + Vite +
Vitest)
New demo under demos/adopt-me-app showcasing an Adopt Me-style pet
game generated end-to-end by ClawCodex. Features five pages
(Home, Nursery, MyPets, Shop, Trade), shared GameContext state,
hatch animation + stat bars, and a Vitest test suite covering the
core flows.
Stack: React 18, React Router 6, Vite 6, Vitest 2.
Co-Authored-By: Claude Opus 4.7
---
demos/adopt-me-app/index.html | 12 +
demos/adopt-me-app/package-lock.json | 4294 +++++++++++++++++
demos/adopt-me-app/package.json | 27 +
demos/adopt-me-app/src/App.jsx | 31 +
demos/adopt-me-app/src/__tests__/App.test.jsx | 31 +
.../src/__tests__/MyPets.test.jsx | 92 +
.../src/__tests__/Nursery.test.jsx | 73 +
.../adopt-me-app/src/__tests__/Shop.test.jsx | 66 +
.../adopt-me-app/src/__tests__/Trade.test.jsx | 68 +
.../src/components/HatchAnimation.jsx | 24 +
demos/adopt-me-app/src/components/Navbar.jsx | 25 +
demos/adopt-me-app/src/components/PetCard.jsx | 42 +
demos/adopt-me-app/src/components/StatBar.jsx | 19 +
.../adopt-me-app/src/context/GameContext.jsx | 182 +
demos/adopt-me-app/src/data/pets.js | 65 +
demos/adopt-me-app/src/index.css | 246 +
demos/adopt-me-app/src/main.jsx | 10 +
demos/adopt-me-app/src/pages/Home.jsx | 58 +
demos/adopt-me-app/src/pages/MyPets.jsx | 151 +
demos/adopt-me-app/src/pages/Nursery.jsx | 40 +
demos/adopt-me-app/src/pages/Shop.jsx | 53 +
demos/adopt-me-app/src/pages/Trade.jsx | 127 +
demos/adopt-me-app/src/setupTests.js | 1 +
demos/adopt-me-app/vite.config.js | 11 +
24 files changed, 5748 insertions(+)
create mode 100644 demos/adopt-me-app/index.html
create mode 100644 demos/adopt-me-app/package-lock.json
create mode 100644 demos/adopt-me-app/package.json
create mode 100644 demos/adopt-me-app/src/App.jsx
create mode 100644 demos/adopt-me-app/src/__tests__/App.test.jsx
create mode 100644 demos/adopt-me-app/src/__tests__/MyPets.test.jsx
create mode 100644 demos/adopt-me-app/src/__tests__/Nursery.test.jsx
create mode 100644 demos/adopt-me-app/src/__tests__/Shop.test.jsx
create mode 100644 demos/adopt-me-app/src/__tests__/Trade.test.jsx
create mode 100644 demos/adopt-me-app/src/components/HatchAnimation.jsx
create mode 100644 demos/adopt-me-app/src/components/Navbar.jsx
create mode 100644 demos/adopt-me-app/src/components/PetCard.jsx
create mode 100644 demos/adopt-me-app/src/components/StatBar.jsx
create mode 100644 demos/adopt-me-app/src/context/GameContext.jsx
create mode 100644 demos/adopt-me-app/src/data/pets.js
create mode 100644 demos/adopt-me-app/src/index.css
create mode 100644 demos/adopt-me-app/src/main.jsx
create mode 100644 demos/adopt-me-app/src/pages/Home.jsx
create mode 100644 demos/adopt-me-app/src/pages/MyPets.jsx
create mode 100644 demos/adopt-me-app/src/pages/Nursery.jsx
create mode 100644 demos/adopt-me-app/src/pages/Shop.jsx
create mode 100644 demos/adopt-me-app/src/pages/Trade.jsx
create mode 100644 demos/adopt-me-app/src/setupTests.js
create mode 100644 demos/adopt-me-app/vite.config.js
diff --git a/demos/adopt-me-app/index.html b/demos/adopt-me-app/index.html
new file mode 100644
index 000000000..8079b444d
--- /dev/null
+++ b/demos/adopt-me-app/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Adopt Me!
+
+
+
+
+
+
diff --git a/demos/adopt-me-app/package-lock.json b/demos/adopt-me-app/package-lock.json
new file mode 100644
index 000000000..02e7722a0
--- /dev/null
+++ b/demos/adopt-me-app/package-lock.json
@@ -0,0 +1,4294 @@
+{
+ "name": "adopt-me-app",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "adopt-me-app",
+ "version": "1.0.0",
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^6.28.0"
+ },
+ "devDependencies": {
+ "@testing-library/jest-dom": "^6.6.3",
+ "@testing-library/react": "^16.1.0",
+ "@testing-library/user-event": "^14.5.2",
+ "@vitejs/plugin-react": "^4.3.4",
+ "jsdom": "^25.0.1",
+ "vite": "^6.0.0",
+ "vitest": "^2.1.0"
+ }
+ },
+ "node_modules/@adobe/css-tools": {
+ "version": "4.4.4",
+ "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz",
+ "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
+ "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/css-calc": "^2.1.3",
+ "@csstools/css-color-parser": "^3.0.9",
+ "@csstools/css-parser-algorithms": "^3.0.4",
+ "@csstools/css-tokenizer": "^3.0.3",
+ "lru-cache": "^10.4.3"
+ }
+ },
+ "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
+ "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@csstools/color-helpers": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
+ "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
+ "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
+ "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/color-helpers": "^5.1.0",
+ "@csstools/css-calc": "^2.1.4"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
+ "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
+ "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@remix-run/router": {
+ "version": "1.23.2",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
+ "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz",
+ "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz",
+ "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz",
+ "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz",
+ "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz",
+ "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz",
+ "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz",
+ "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz",
+ "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz",
+ "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz",
+ "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz",
+ "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz",
+ "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz",
+ "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz",
+ "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz",
+ "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz",
+ "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz",
+ "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz",
+ "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz",
+ "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz",
+ "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz",
+ "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz",
+ "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz",
+ "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz",
+ "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz",
+ "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@testing-library/dom": {
+ "version": "10.4.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
+ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/runtime": "^7.12.5",
+ "@types/aria-query": "^5.0.1",
+ "aria-query": "5.3.0",
+ "dom-accessibility-api": "^0.5.9",
+ "lz-string": "^1.5.0",
+ "picocolors": "1.1.1",
+ "pretty-format": "^27.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@testing-library/jest-dom": {
+ "version": "6.9.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz",
+ "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@adobe/css-tools": "^4.4.0",
+ "aria-query": "^5.0.0",
+ "css.escape": "^1.5.1",
+ "dom-accessibility-api": "^0.6.3",
+ "picocolors": "^1.1.1",
+ "redent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ }
+ },
+ "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
+ "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@testing-library/react": {
+ "version": "16.3.2",
+ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz",
+ "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@testing-library/user-event": {
+ "version": "14.6.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz",
+ "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": ">=7.21.4"
+ }
+ },
+ "node_modules/@types/aria-query": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
+ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz",
+ "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "2.1.9",
+ "@vitest/utils": "2.1.9",
+ "chai": "^5.1.2",
+ "tinyrainbow": "^1.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz",
+ "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^1.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz",
+ "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "2.1.9",
+ "pathe": "^1.1.2"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz",
+ "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "2.1.9",
+ "magic-string": "^0.30.12",
+ "pathe": "^1.1.2"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz",
+ "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyspy": "^3.0.2"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz",
+ "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "2.1.9",
+ "loupe": "^3.1.2",
+ "tinyrainbow": "^1.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/aria-query": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
+ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.22",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.22.tgz",
+ "integrity": "sha512-6qruVrb5rse6WylFkU0FhBKKGuecWseqdpQfhkawn6ztyk2QlfwSRjsDxMCLJrkfmfN21qvhl9ABgaMeRkuwww==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001790",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz",
+ "integrity": "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chai": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
+ "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assertion-error": "^2.0.1",
+ "check-error": "^2.1.1",
+ "deep-eql": "^5.0.1",
+ "loupe": "^3.1.0",
+ "pathval": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/check-error": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
+ "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/css.escape": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
+ "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cssstyle": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
+ "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/css-color": "^3.2.0",
+ "rrweb-cssom": "^0.8.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/cssstyle/node_modules/rrweb-cssom": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
+ "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/data-urls": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
+ "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/deep-eql": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
+ "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/dom-accessibility-api": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
+ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.344",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz",
+ "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
+ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-encoding": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/jsdom": {
+ "version": "25.0.1",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz",
+ "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssstyle": "^4.1.0",
+ "data-urls": "^5.0.0",
+ "decimal.js": "^10.4.3",
+ "form-data": "^4.0.0",
+ "html-encoding-sniffer": "^4.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.5",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.12",
+ "parse5": "^7.1.2",
+ "rrweb-cssom": "^0.7.1",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^5.0.0",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^3.1.1",
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.0.0",
+ "ws": "^8.18.0",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "canvas": "^2.11.2"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/loupe": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
+ "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lz-string": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
+ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "lz-string": "bin/bin.js"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/min-indent": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
+ "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.38",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz",
+ "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nwsapi": {
+ "version": "2.2.23",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz",
+ "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/pathe": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
+ "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathval": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
+ "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.16"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.10",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
+ "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/pretty-format": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
+ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^17.0.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "6.30.3",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
+ "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.30.3",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
+ "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.2",
+ "react-router": "6.30.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/redent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
+ "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "indent-string": "^4.0.0",
+ "strip-indent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz",
+ "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.60.2",
+ "@rollup/rollup-android-arm64": "4.60.2",
+ "@rollup/rollup-darwin-arm64": "4.60.2",
+ "@rollup/rollup-darwin-x64": "4.60.2",
+ "@rollup/rollup-freebsd-arm64": "4.60.2",
+ "@rollup/rollup-freebsd-x64": "4.60.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.2",
+ "@rollup/rollup-linux-arm64-musl": "4.60.2",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.2",
+ "@rollup/rollup-linux-loong64-musl": "4.60.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.2",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.2",
+ "@rollup/rollup-linux-x64-gnu": "4.60.2",
+ "@rollup/rollup-linux-x64-musl": "4.60.2",
+ "@rollup/rollup-openbsd-x64": "4.60.2",
+ "@rollup/rollup-openharmony-arm64": "4.60.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.2",
+ "@rollup/rollup-win32-x64-gnu": "4.60.2",
+ "@rollup/rollup-win32-x64-msvc": "4.60.2",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/rrweb-cssom": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
+ "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/strip-indent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
+ "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "min-indent": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinypool": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
+ "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz",
+ "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tinyspy": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
+ "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tldts": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
+ "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^6.1.86"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
+ "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tough-cookie": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
+ "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^6.1.32"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.4.2",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
+ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "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
+ }
+ }
+ },
+ "node_modules/vite-node": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz",
+ "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cac": "^6.7.14",
+ "debug": "^4.3.7",
+ "es-module-lexer": "^1.5.4",
+ "pathe": "^1.1.2",
+ "vite": "^5.0.0"
+ },
+ "bin": {
+ "vite-node": "vite-node.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/vite-node/node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz",
+ "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "2.1.9",
+ "@vitest/mocker": "2.1.9",
+ "@vitest/pretty-format": "^2.1.9",
+ "@vitest/runner": "2.1.9",
+ "@vitest/snapshot": "2.1.9",
+ "@vitest/spy": "2.1.9",
+ "@vitest/utils": "2.1.9",
+ "chai": "^5.1.2",
+ "debug": "^4.3.7",
+ "expect-type": "^1.1.0",
+ "magic-string": "^0.30.12",
+ "pathe": "^1.1.2",
+ "std-env": "^3.8.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^0.3.1",
+ "tinypool": "^1.0.1",
+ "tinyrainbow": "^1.2.0",
+ "vite": "^5.0.0",
+ "vite-node": "2.1.9",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "@vitest/browser": "2.1.9",
+ "@vitest/ui": "2.1.9",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@vitest/mocker": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz",
+ "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "2.1.9",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.12"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest/node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/vitest/node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
+ "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^5.1.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
+ "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
+ "dev": true,
+ "license": "MIT",
+ "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
+ }
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ }
+ }
+}
diff --git a/demos/adopt-me-app/package.json b/demos/adopt-me-app/package.json
new file mode 100644
index 000000000..06c28f3f0
--- /dev/null
+++ b/demos/adopt-me-app/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "adopt-me-app",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview",
+ "test": "vitest run",
+ "test:watch": "vitest"
+ },
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^6.28.0"
+ },
+ "devDependencies": {
+ "@testing-library/jest-dom": "^6.6.3",
+ "@testing-library/react": "^16.1.0",
+ "@testing-library/user-event": "^14.5.2",
+ "@vitejs/plugin-react": "^4.3.4",
+ "jsdom": "^25.0.1",
+ "vite": "^6.0.0",
+ "vitest": "^2.1.0"
+ }
+}
diff --git a/demos/adopt-me-app/src/App.jsx b/demos/adopt-me-app/src/App.jsx
new file mode 100644
index 000000000..c3a684730
--- /dev/null
+++ b/demos/adopt-me-app/src/App.jsx
@@ -0,0 +1,31 @@
+import { BrowserRouter, Routes, Route } from 'react-router-dom';
+import { GameProvider } from './context/GameContext';
+import Navbar from './components/Navbar';
+import HatchAnimation from './components/HatchAnimation';
+import Home from './pages/Home';
+import Nursery from './pages/Nursery';
+import MyPets from './pages/MyPets';
+import Shop from './pages/Shop';
+import Trade from './pages/Trade';
+
+export default function App() {
+ return (
+
+
+
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+
+
+
+ );
+}
diff --git a/demos/adopt-me-app/src/__tests__/App.test.jsx b/demos/adopt-me-app/src/__tests__/App.test.jsx
new file mode 100644
index 000000000..b1f94433e
--- /dev/null
+++ b/demos/adopt-me-app/src/__tests__/App.test.jsx
@@ -0,0 +1,31 @@
+import { render, screen } from '@testing-library/react';
+import { describe, it, expect } from 'vitest';
+import App from '../App';
+
+describe('App', () => {
+ it('renders the navbar with brand name', () => {
+ render( );
+ expect(screen.getByText('Adopt Me!')).toBeInTheDocument();
+ });
+
+ it('renders navigation links', () => {
+ render( );
+ const nav = screen.getByRole('navigation');
+ expect(nav).toHaveTextContent('Home');
+ expect(nav).toHaveTextContent('Nursery');
+ expect(nav).toHaveTextContent(/My Pets/);
+ expect(nav).toHaveTextContent('Shop');
+ expect(nav).toHaveTextContent('Trade');
+ });
+
+ it('shows the home page by default', () => {
+ render( );
+ expect(screen.getByText('Welcome to Adopt Me!')).toBeInTheDocument();
+ });
+
+ it('displays initial coin amount', () => {
+ render( );
+ const coinElements = screen.getAllByText(/1,000/);
+ expect(coinElements.length).toBeGreaterThan(0);
+ });
+});
diff --git a/demos/adopt-me-app/src/__tests__/MyPets.test.jsx b/demos/adopt-me-app/src/__tests__/MyPets.test.jsx
new file mode 100644
index 000000000..56dc5f09a
--- /dev/null
+++ b/demos/adopt-me-app/src/__tests__/MyPets.test.jsx
@@ -0,0 +1,92 @@
+import { render, screen, fireEvent } from '@testing-library/react';
+import { describe, it, expect } from 'vitest';
+import { MemoryRouter } from 'react-router-dom';
+import { GameProvider } from '../context/GameContext';
+import MyPets from '../pages/MyPets';
+
+const petInstance = {
+ instanceId: 'pet_test_1',
+ id: 'dog',
+ name: 'Dog',
+ emoji: '🐶',
+ rarity: 'COMMON',
+ basePrice: 100,
+ nickname: 'Buddy',
+ ageIndex: 0,
+ xp: 0,
+ stats: { hunger: 50, happiness: 50, energy: 50, hygiene: 50, health: 50 },
+ adoptedAt: Date.now(),
+ taskCooldowns: {},
+};
+
+function renderMyPets(overrides = {}) {
+ const state = {
+ player: { name: 'Tester', coins: 500, level: 1 },
+ pets: [petInstance],
+ activePetId: 'pet_test_1',
+ inventory: [],
+ tradeOffers: [],
+ notification: null,
+ ...overrides,
+ };
+ return render(
+
+
+
+
+
+ );
+}
+
+describe('MyPets', () => {
+ it('shows empty state when no pets', () => {
+ renderMyPets({ pets: [], activePetId: null });
+ expect(screen.getByText('No pets yet!')).toBeInTheDocument();
+ });
+
+ it('shows pet collection with count', () => {
+ renderMyPets();
+ expect(screen.getByText('Your Collection (1)')).toBeInTheDocument();
+ });
+
+ it('shows active pet detail', () => {
+ renderMyPets();
+ const buddyElements = screen.getAllByText('Buddy');
+ expect(buddyElements.length).toBe(2);
+ expect(screen.getByText(/Newborn/)).toBeInTheDocument();
+ });
+
+ it('shows care task buttons', () => {
+ renderMyPets();
+ expect(screen.getByText('Feed')).toBeInTheDocument();
+ expect(screen.getByText('Play')).toBeInTheDocument();
+ expect(screen.getByText('Sleep')).toBeInTheDocument();
+ expect(screen.getByText('Clean')).toBeInTheDocument();
+ expect(screen.getByText('Heal')).toBeInTheDocument();
+ });
+
+ it('executes a task and shows XP reward', () => {
+ renderMyPets();
+ const feedBtn = screen.getByText('Feed').closest('button');
+ fireEvent.click(feedBtn);
+ const xpElements = screen.getAllByText(/XP/);
+ expect(xpElements.length).toBeGreaterThan(0);
+ });
+
+ it('shows release pet button', () => {
+ renderMyPets();
+ expect(screen.getByText('Release Pet')).toBeInTheDocument();
+ });
+
+ it('shows confirmation before releasing', () => {
+ renderMyPets();
+ fireEvent.click(screen.getByText('Release Pet'));
+ expect(screen.getByText(/Are you sure/)).toBeInTheDocument();
+ expect(screen.getByText('Yes, Release')).toBeInTheDocument();
+ });
+
+ it('shows rename button', () => {
+ renderMyPets();
+ expect(screen.getByLabelText('Rename pet')).toBeInTheDocument();
+ });
+});
diff --git a/demos/adopt-me-app/src/__tests__/Nursery.test.jsx b/demos/adopt-me-app/src/__tests__/Nursery.test.jsx
new file mode 100644
index 000000000..b1a314b36
--- /dev/null
+++ b/demos/adopt-me-app/src/__tests__/Nursery.test.jsx
@@ -0,0 +1,73 @@
+import { render, screen, fireEvent } from '@testing-library/react';
+import { describe, it, expect } from 'vitest';
+import { MemoryRouter } from 'react-router-dom';
+import { GameProvider } from '../context/GameContext';
+import Nursery from '../pages/Nursery';
+import HatchAnimation from '../components/HatchAnimation';
+
+function renderNursery(initialState) {
+ return render(
+
+
+
+
+
+
+ );
+}
+
+describe('Nursery', () => {
+ it('renders the nursery page', () => {
+ renderNursery();
+ expect(screen.getByText('Nursery')).toBeInTheDocument();
+ });
+
+ it('shows all egg types', () => {
+ renderNursery();
+ expect(screen.getByText('Starter Egg')).toBeInTheDocument();
+ expect(screen.getByText('Royal Egg')).toBeInTheDocument();
+ expect(screen.getByText('Legendary Egg')).toBeInTheDocument();
+ expect(screen.getByText('Mythic Egg')).toBeInTheDocument();
+ });
+
+ it('shows hatch button enabled when player can afford', () => {
+ renderNursery({
+ player: { name: 'Tester', coins: 5000, level: 1 },
+ pets: [],
+ activePetId: null,
+ inventory: [],
+ tradeOffers: [],
+ notification: null,
+ });
+ const hatchButtons = screen.getAllByText('Hatch!');
+ expect(hatchButtons.length).toBeGreaterThan(0);
+ });
+
+ it('shows hatch animation after hatching an egg', () => {
+ renderNursery({
+ player: { name: 'Tester', coins: 5000, level: 1 },
+ pets: [],
+ activePetId: null,
+ inventory: [],
+ tradeOffers: [],
+ notification: null,
+ });
+ const hatchButtons = screen.getAllByText('Hatch!');
+ fireEvent.click(hatchButtons[0]);
+ expect(screen.getByText('You hatched a pet!')).toBeInTheDocument();
+ expect(screen.getByText('Awesome!')).toBeInTheDocument();
+ });
+
+ it('disables hatch when not enough coins', () => {
+ renderNursery({
+ player: { name: 'Tester', coins: 0, level: 1 },
+ pets: [],
+ activePetId: null,
+ inventory: [],
+ tradeOffers: [],
+ notification: null,
+ });
+ const disabledButtons = screen.getAllByText('Not enough coins');
+ expect(disabledButtons[0]).toBeDisabled();
+ });
+});
diff --git a/demos/adopt-me-app/src/__tests__/Shop.test.jsx b/demos/adopt-me-app/src/__tests__/Shop.test.jsx
new file mode 100644
index 000000000..ff2d389bc
--- /dev/null
+++ b/demos/adopt-me-app/src/__tests__/Shop.test.jsx
@@ -0,0 +1,66 @@
+import { render, screen, fireEvent } from '@testing-library/react';
+import { describe, it, expect } from 'vitest';
+import { MemoryRouter } from 'react-router-dom';
+import { GameProvider } from '../context/GameContext';
+import Shop from '../pages/Shop';
+
+function renderShop(initialState) {
+ return render(
+
+
+
+
+
+ );
+}
+
+describe('Shop', () => {
+ it('renders the shop page', () => {
+ renderShop();
+ expect(screen.getByText('Pet Shop')).toBeInTheDocument();
+ });
+
+ it('shows pet names in shop', () => {
+ renderShop();
+ expect(screen.getByText('Dog')).toBeInTheDocument();
+ expect(screen.getByText('Cat')).toBeInTheDocument();
+ expect(screen.getByText('Lion')).toBeInTheDocument();
+ });
+
+ it('shows rarity sections', () => {
+ renderShop();
+ expect(screen.getByText('Common')).toBeInTheDocument();
+ expect(screen.getByText('Rare')).toBeInTheDocument();
+ expect(screen.getByText('Legendary')).toBeInTheDocument();
+ });
+
+ it('allows buying a pet when player has enough coins', () => {
+ renderShop({
+ player: { name: 'Tester', coins: 500, level: 1 },
+ pets: [],
+ activePetId: null,
+ inventory: [],
+ tradeOffers: [],
+ notification: null,
+ });
+ const adoptButtons = screen.getAllByText('Adopt!');
+ expect(adoptButtons.length).toBeGreaterThan(0);
+ fireEvent.click(adoptButtons[0]);
+ // After adopting, coins should decrease (check via subtitle text)
+ expect(screen.getByText(/Your coins/)).toBeInTheDocument();
+ });
+
+ it('disables buy button when not enough coins', () => {
+ renderShop({
+ player: { name: 'Tester', coins: 0, level: 1 },
+ pets: [],
+ activePetId: null,
+ inventory: [],
+ tradeOffers: [],
+ notification: null,
+ });
+ const disabledButtons = screen.getAllByText('Too expensive');
+ expect(disabledButtons.length).toBeGreaterThan(0);
+ expect(disabledButtons[0]).toBeDisabled();
+ });
+});
diff --git a/demos/adopt-me-app/src/__tests__/Trade.test.jsx b/demos/adopt-me-app/src/__tests__/Trade.test.jsx
new file mode 100644
index 000000000..3768f79a9
--- /dev/null
+++ b/demos/adopt-me-app/src/__tests__/Trade.test.jsx
@@ -0,0 +1,68 @@
+import { render, screen } from '@testing-library/react';
+import { describe, it, expect } from 'vitest';
+import { MemoryRouter } from 'react-router-dom';
+import { GameProvider } from '../context/GameContext';
+import Trade from '../pages/Trade';
+
+function renderTrade(initialState) {
+ return render(
+
+
+
+
+
+ );
+}
+
+describe('Trade', () => {
+ it('renders the trade page', () => {
+ renderTrade();
+ expect(screen.getByText('Trading Plaza')).toBeInTheDocument();
+ });
+
+ it('shows NPC traders', () => {
+ renderTrade();
+ expect(screen.getByText('Friendly Farmer')).toBeInTheDocument();
+ expect(screen.getByText('Explorer Emma')).toBeInTheDocument();
+ expect(screen.getByText('Royal Knight')).toBeInTheDocument();
+ expect(screen.getByText('Mystic Mage')).toBeInTheDocument();
+ });
+
+ it('shows empty state when no pets', () => {
+ renderTrade({
+ player: { name: 'Tester', coins: 500, level: 1 },
+ pets: [],
+ activePetId: null,
+ inventory: [],
+ tradeOffers: [],
+ notification: null,
+ });
+ expect(screen.getByText('No pets to trade')).toBeInTheDocument();
+ });
+
+ it('shows pet selection when pets exist', () => {
+ renderTrade({
+ player: { name: 'Tester', coins: 500, level: 1 },
+ pets: [{
+ instanceId: 'pet_1',
+ id: 'dog',
+ name: 'Dog',
+ emoji: '🐶',
+ rarity: 'COMMON',
+ basePrice: 100,
+ nickname: 'Rex',
+ ageIndex: 0,
+ xp: 0,
+ stats: { hunger: 50, happiness: 50, energy: 50, hygiene: 50, health: 50 },
+ adoptedAt: Date.now(),
+ taskCooldowns: {},
+ }],
+ activePetId: 'pet_1',
+ inventory: [],
+ tradeOffers: [],
+ notification: null,
+ });
+ expect(screen.getByText('Select a Pet to Trade')).toBeInTheDocument();
+ expect(screen.getByText('Rex')).toBeInTheDocument();
+ });
+});
diff --git a/demos/adopt-me-app/src/components/HatchAnimation.jsx b/demos/adopt-me-app/src/components/HatchAnimation.jsx
new file mode 100644
index 000000000..831c0fddc
--- /dev/null
+++ b/demos/adopt-me-app/src/components/HatchAnimation.jsx
@@ -0,0 +1,24 @@
+import { PET_RARITIES } from '../data/pets';
+import { useGame } from '../context/GameContext';
+
+export default function HatchAnimation() {
+ const { state, clearNotification } = useGame();
+ const notif = state.notification;
+ if (!notif || notif.type !== 'hatch') return null;
+
+ const rarity = PET_RARITIES[notif.pet.rarity];
+
+ return (
+
+
+
You hatched a pet!
+
{notif.egg.emoji}
+
⬇️
+
{notif.pet.emoji}
+
{notif.pet.name}
+
{rarity.name}
+
Awesome!
+
+
+ );
+}
diff --git a/demos/adopt-me-app/src/components/Navbar.jsx b/demos/adopt-me-app/src/components/Navbar.jsx
new file mode 100644
index 000000000..4743542ef
--- /dev/null
+++ b/demos/adopt-me-app/src/components/Navbar.jsx
@@ -0,0 +1,25 @@
+import { NavLink } from 'react-router-dom';
+import { useGame } from '../context/GameContext';
+
+export default function Navbar() {
+ const { state } = useGame();
+
+ return (
+
+
+ 🐾
+ Adopt Me!
+
+
+ Home
+ Nursery
+ My Pets ({state.pets.length})
+ Shop
+ Trade
+
+
+ 🪙 {state.player.coins.toLocaleString()}
+
+
+ );
+}
diff --git a/demos/adopt-me-app/src/components/PetCard.jsx b/demos/adopt-me-app/src/components/PetCard.jsx
new file mode 100644
index 000000000..2e9b0bd45
--- /dev/null
+++ b/demos/adopt-me-app/src/components/PetCard.jsx
@@ -0,0 +1,42 @@
+import { PET_RARITIES, PET_AGES, getXpForAge } from '../data/pets';
+
+export default function PetCard({ pet, onClick, selected, compact }) {
+ const rarity = PET_RARITIES[pet.rarity];
+ const age = PET_AGES[pet.ageIndex];
+ const xpNeeded = getXpForAge(pet.ageIndex);
+ const xpPercent = Math.round((pet.xp / xpNeeded) * 100);
+
+ const avgStats = pet.stats
+ ? Math.round(Object.values(pet.stats).reduce((a, b) => a + b, 0) / 5)
+ : 0;
+
+ return (
+ e.key === 'Enter' && onClick?.()}
+ >
+
{pet.emoji}
+
+
{pet.nickname}
+
{rarity.name}
+ {!compact && (
+ <>
+
Age: {age}
+
+
+
XP {pet.xp}/{xpNeeded}
+
+
+ Mood: {avgStats >= 70 ? '😊' : avgStats >= 40 ? '😐' : '😞'} {avgStats}%
+
+ >
+ )}
+
+
+ );
+}
diff --git a/demos/adopt-me-app/src/components/StatBar.jsx b/demos/adopt-me-app/src/components/StatBar.jsx
new file mode 100644
index 000000000..acc6d6edf
--- /dev/null
+++ b/demos/adopt-me-app/src/components/StatBar.jsx
@@ -0,0 +1,19 @@
+export default function StatBar({ label, value, color }) {
+ const barColor = color || (value >= 70 ? '#4caf50' : value >= 40 ? '#ff9800' : '#f44336');
+ return (
+
+ );
+}
diff --git a/demos/adopt-me-app/src/context/GameContext.jsx b/demos/adopt-me-app/src/context/GameContext.jsx
new file mode 100644
index 000000000..ef065dddb
--- /dev/null
+++ b/demos/adopt-me-app/src/context/GameContext.jsx
@@ -0,0 +1,182 @@
+import { createContext, useContext, useReducer, useCallback } from 'react';
+import { PET_AGES, getXpForAge, rollPetFromEgg, EGGS } from '../data/pets';
+
+const GameContext = createContext();
+
+const INITIAL_STATE = {
+ player: {
+ name: 'Player',
+ coins: 1000,
+ level: 1,
+ },
+ pets: [],
+ activePetId: null,
+ inventory: [],
+ tradeOffers: [],
+ notification: null,
+};
+
+let nextPetInstanceId = 1;
+
+function createPetInstance(petData) {
+ return {
+ instanceId: `pet_${nextPetInstanceId++}`,
+ ...petData,
+ nickname: petData.name,
+ ageIndex: 0,
+ xp: 0,
+ stats: { hunger: 50, happiness: 50, energy: 50, hygiene: 50, health: 50 },
+ adoptedAt: Date.now(),
+ taskCooldowns: {},
+ };
+}
+
+function gameReducer(state, action) {
+ switch (action.type) {
+ case 'SET_PLAYER_NAME':
+ return { ...state, player: { ...state.player, name: action.payload } };
+
+ case 'ADD_COINS':
+ return { ...state, player: { ...state.player, coins: state.player.coins + action.payload } };
+
+ case 'SPEND_COINS': {
+ if (state.player.coins < action.payload) return state;
+ return { ...state, player: { ...state.player, coins: state.player.coins - action.payload } };
+ }
+
+ case 'ADOPT_PET': {
+ const pet = createPetInstance(action.payload);
+ return {
+ ...state,
+ pets: [...state.pets, pet],
+ activePetId: state.activePetId || pet.instanceId,
+ };
+ }
+
+ case 'SET_ACTIVE_PET':
+ return { ...state, activePetId: action.payload };
+
+ case 'RENAME_PET':
+ return {
+ ...state,
+ pets: state.pets.map(p =>
+ p.instanceId === action.payload.instanceId
+ ? { ...p, nickname: action.payload.nickname }
+ : p
+ ),
+ };
+
+ case 'DO_TASK': {
+ const { instanceId, task } = action.payload;
+ const now = Date.now();
+ return {
+ ...state,
+ pets: state.pets.map(p => {
+ if (p.instanceId !== instanceId) return p;
+ const cooldownEnd = p.taskCooldowns[task.id] || 0;
+ if (now < cooldownEnd) return p;
+ const newStats = { ...p.stats, [task.stat]: Math.min(100, p.stats[task.stat] + task.gain) };
+ let newXp = p.xp + task.xp;
+ let newAgeIndex = p.ageIndex;
+ const xpNeeded = getXpForAge(p.ageIndex);
+ if (newXp >= xpNeeded && newAgeIndex < PET_AGES.length - 1) {
+ newXp -= xpNeeded;
+ newAgeIndex += 1;
+ }
+ return {
+ ...p,
+ stats: newStats,
+ xp: newXp,
+ ageIndex: newAgeIndex,
+ taskCooldowns: { ...p.taskCooldowns, [task.id]: now + task.cooldown },
+ };
+ }),
+ player: { ...state.player, coins: state.player.coins + 5 },
+ };
+ }
+
+ case 'RELEASE_PET': {
+ const newPets = state.pets.filter(p => p.instanceId !== action.payload);
+ return {
+ ...state,
+ pets: newPets,
+ activePetId: state.activePetId === action.payload
+ ? (newPets[0]?.instanceId || null)
+ : state.activePetId,
+ };
+ }
+
+ case 'HATCH_EGG': {
+ const egg = EGGS.find(e => e.id === action.payload);
+ if (!egg || state.player.coins < egg.price) return state;
+ const petData = rollPetFromEgg(egg);
+ const pet = createPetInstance(petData);
+ return {
+ ...state,
+ player: { ...state.player, coins: state.player.coins - egg.price },
+ pets: [...state.pets, pet],
+ activePetId: state.activePetId || pet.instanceId,
+ notification: { type: 'hatch', pet, egg },
+ };
+ }
+
+ case 'CLEAR_NOTIFICATION':
+ return { ...state, notification: null };
+
+ case 'COMPLETE_TRADE': {
+ const { give, receive } = action.payload;
+ const newPets = state.pets.filter(p => !give.includes(p.instanceId));
+ receive.forEach(petData => {
+ newPets.push(createPetInstance(petData));
+ });
+ return {
+ ...state,
+ pets: newPets,
+ activePetId: newPets[0]?.instanceId || null,
+ };
+ }
+
+ default:
+ return state;
+ }
+}
+
+export function GameProvider({ children, initialState }) {
+ const [state, dispatch] = useReducer(gameReducer, initialState || INITIAL_STATE);
+
+ const addCoins = useCallback((amount) => dispatch({ type: 'ADD_COINS', payload: amount }), []);
+ const spendCoins = useCallback((amount) => dispatch({ type: 'SPEND_COINS', payload: amount }), []);
+ const adoptPet = useCallback((petData) => dispatch({ type: 'ADOPT_PET', payload: petData }), []);
+ const setActivePet = useCallback((id) => dispatch({ type: 'SET_ACTIVE_PET', payload: id }), []);
+ const renamePet = useCallback((instanceId, nickname) =>
+ dispatch({ type: 'RENAME_PET', payload: { instanceId, nickname } }), []);
+ const doTask = useCallback((instanceId, task) =>
+ dispatch({ type: 'DO_TASK', payload: { instanceId, task } }), []);
+ const releasePet = useCallback((id) => dispatch({ type: 'RELEASE_PET', payload: id }), []);
+ const hatchEgg = useCallback((eggId) => dispatch({ type: 'HATCH_EGG', payload: eggId }), []);
+ const clearNotification = useCallback(() => dispatch({ type: 'CLEAR_NOTIFICATION' }), []);
+ const setPlayerName = useCallback((name) => dispatch({ type: 'SET_PLAYER_NAME', payload: name }), []);
+
+ const value = {
+ state,
+ dispatch,
+ addCoins,
+ spendCoins,
+ adoptPet,
+ setActivePet,
+ renamePet,
+ doTask,
+ releasePet,
+ hatchEgg,
+ clearNotification,
+ setPlayerName,
+ };
+
+ return {children} ;
+}
+
+export function useGame() {
+ const ctx = useContext(GameContext);
+ if (!ctx) throw new Error('useGame must be used within GameProvider');
+ return ctx;
+}
diff --git a/demos/adopt-me-app/src/data/pets.js b/demos/adopt-me-app/src/data/pets.js
new file mode 100644
index 000000000..fd5763ccf
--- /dev/null
+++ b/demos/adopt-me-app/src/data/pets.js
@@ -0,0 +1,65 @@
+export const PET_RARITIES = {
+ COMMON: { name: 'Common', color: '#8b8b8b', chance: 0.40 },
+ UNCOMMON: { name: 'Uncommon', color: '#4caf50', chance: 0.25 },
+ RARE: { name: 'Rare', color: '#2196f3', chance: 0.18 },
+ ULTRA_RARE: { name: 'Ultra Rare', color: '#9c27b0', chance: 0.10 },
+ LEGENDARY: { name: 'Legendary', color: '#ff9800', chance: 0.05 },
+ MYTHIC: { name: 'Mythic', color: '#f44336', chance: 0.02 },
+};
+
+export const PET_CATALOG = [
+ // Common
+ { id: 'dog', name: 'Dog', emoji: '🐶', rarity: 'COMMON', basePrice: 100 },
+ { id: 'cat', name: 'Cat', emoji: '🐱', rarity: 'COMMON', basePrice: 100 },
+ { id: 'rabbit', name: 'Rabbit', emoji: '🐰', rarity: 'COMMON', basePrice: 150 },
+ { id: 'hamster', name: 'Hamster', emoji: '🐹', rarity: 'COMMON', basePrice: 120 },
+ // Uncommon
+ { id: 'fox', name: 'Fox', emoji: '🦊', rarity: 'UNCOMMON', basePrice: 300 },
+ { id: 'panda', name: 'Panda', emoji: '🐼', rarity: 'UNCOMMON', basePrice: 350 },
+ { id: 'koala', name: 'Koala', emoji: '🐨', rarity: 'UNCOMMON', basePrice: 320 },
+ { id: 'penguin', name: 'Penguin', emoji: '🐧', rarity: 'UNCOMMON', basePrice: 300 },
+ // Rare
+ { id: 'lion', name: 'Lion', emoji: '🦁', rarity: 'RARE', basePrice: 800 },
+ { id: 'elephant', name: 'Elephant', emoji: '🐘', rarity: 'RARE', basePrice: 850 },
+ { id: 'giraffe', name: 'Giraffe', emoji: '🦒', rarity: 'RARE', basePrice: 900 },
+ // Ultra Rare
+ { id: 'unicorn', name: 'Unicorn', emoji: '🦄', rarity: 'ULTRA_RARE', basePrice: 2000 },
+ { id: 'dragon_baby', name: 'Baby Dragon', emoji: '🐉', rarity: 'ULTRA_RARE', basePrice: 2500 },
+ // Legendary
+ { id: 'phoenix', name: 'Phoenix', emoji: '🔥', rarity: 'LEGENDARY', basePrice: 5000 },
+ { id: 'frost_dragon', name: 'Frost Dragon', emoji: '🐲', rarity: 'LEGENDARY', basePrice: 6000 },
+ // Mythic
+ { id: 'shadow_dragon', name: 'Shadow Dragon', emoji: '✨', rarity: 'MYTHIC', basePrice: 15000 },
+];
+
+export const PET_AGES = ['Newborn', 'Junior', 'Pre-Teen', 'Teen', 'Post-Teen', 'Full Grown'];
+
+export const TASKS = [
+ { id: 'feed', name: 'Feed', emoji: '🍼', stat: 'hunger', gain: 25, xp: 10, cooldown: 30000 },
+ { id: 'play', name: 'Play', emoji: '⚽', stat: 'happiness', gain: 20, xp: 15, cooldown: 45000 },
+ { id: 'sleep', name: 'Sleep', emoji: '💤', stat: 'energy', gain: 30, xp: 10, cooldown: 60000 },
+ { id: 'clean', name: 'Clean', emoji: '🛁', stat: 'hygiene', gain: 25, xp: 10, cooldown: 30000 },
+ { id: 'heal', name: 'Heal', emoji: '💊', stat: 'health', gain: 35, xp: 20, cooldown: 90000 },
+];
+
+export const EGGS = [
+ { id: 'starter_egg', name: 'Starter Egg', emoji: '🥚', price: 350, rarities: ['COMMON', 'UNCOMMON'] },
+ { id: 'royal_egg', name: 'Royal Egg', emoji: '👑', price: 1450, rarities: ['UNCOMMON', 'RARE', 'ULTRA_RARE'] },
+ { id: 'legendary_egg', name: 'Legendary Egg', emoji: '⭐', price: 5000, rarities: ['RARE', 'ULTRA_RARE', 'LEGENDARY'] },
+ { id: 'mythic_egg', name: 'Mythic Egg', emoji: '🌌', price: 12000, rarities: ['ULTRA_RARE', 'LEGENDARY', 'MYTHIC'] },
+];
+
+export function getXpForAge(ageIndex) {
+ return (ageIndex + 1) * 50;
+}
+
+export function rollPetFromEgg(egg) {
+ const availablePets = PET_CATALOG.filter(p => egg.rarities.includes(p.rarity));
+ const totalChance = availablePets.reduce((sum, p) => sum + PET_RARITIES[p.rarity].chance, 0);
+ let roll = Math.random() * totalChance;
+ for (const pet of availablePets) {
+ roll -= PET_RARITIES[pet.rarity].chance;
+ if (roll <= 0) return pet;
+ }
+ return availablePets[availablePets.length - 1];
+}
diff --git a/demos/adopt-me-app/src/index.css b/demos/adopt-me-app/src/index.css
new file mode 100644
index 000000000..74c454cb4
--- /dev/null
+++ b/demos/adopt-me-app/src/index.css
@@ -0,0 +1,246 @@
+/* Reset & Base */
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+:root {
+ --bg: #1a1a2e;
+ --bg-card: #16213e;
+ --bg-card-hover: #1a2745;
+ --text: #e8e8e8;
+ --text-dim: #a0a0b0;
+ --primary: #e94560;
+ --primary-hover: #d63050;
+ --accent: #0f3460;
+ --success: #4caf50;
+ --warning: #ff9800;
+ --danger: #f44336;
+ --radius: 12px;
+ --radius-sm: 8px;
+}
+body {
+ font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
+ background: var(--bg);
+ color: var(--text);
+ line-height: 1.5;
+ min-height: 100vh;
+}
+a { color: var(--primary); text-decoration: none; }
+
+/* App layout */
+.app { display: flex; flex-direction: column; min-height: 100vh; }
+.main-content { flex: 1; padding: 24px; max-width: 1200px; margin: 0 auto; width: 100%; }
+
+/* Navbar */
+.navbar {
+ display: flex; align-items: center; gap: 20px;
+ padding: 12px 24px; background: var(--accent);
+ border-bottom: 2px solid var(--primary);
+ flex-wrap: wrap;
+}
+.navbar__brand { display: flex; align-items: center; gap: 8px; }
+.navbar__logo { font-size: 28px; }
+.navbar__title { font-size: 22px; font-weight: 800; color: var(--primary); }
+.navbar__links { display: flex; gap: 4px; flex: 1; }
+.navbar__links a {
+ padding: 8px 16px; border-radius: var(--radius-sm);
+ color: var(--text-dim); font-weight: 600; transition: all 0.2s;
+}
+.navbar__links a:hover { background: rgba(233,69,96,0.1); color: var(--text); }
+.navbar__links a.active { background: var(--primary); color: #fff; }
+.navbar__coins { font-size: 18px; font-weight: 700; white-space: nowrap; }
+
+/* Buttons */
+.btn {
+ padding: 10px 20px; border: none; border-radius: var(--radius-sm);
+ font-size: 15px; font-weight: 600; cursor: pointer; transition: all 0.2s;
+ display: inline-flex; align-items: center; gap: 6px;
+}
+.btn--primary { background: var(--primary); color: #fff; }
+.btn--primary:hover { background: var(--primary-hover); }
+.btn--disabled { background: #444; color: #888; cursor: not-allowed; }
+.btn--ghost { background: transparent; color: var(--text-dim); }
+.btn--ghost:hover { color: var(--text); }
+.btn--small { padding: 6px 12px; font-size: 13px; }
+.btn--icon { background: none; border: none; cursor: pointer; font-size: 16px; padding: 4px; }
+.btn--danger { background: var(--danger); color: #fff; }
+.btn--danger:hover { background: #d32f2f; }
+.btn--danger-outline { background: transparent; border: 1px solid var(--danger); color: var(--danger); }
+.btn--danger-outline:hover { background: var(--danger); color: #fff; }
+
+/* Page */
+.page { animation: fadeIn 0.3s ease; }
+@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
+.page__subtitle { color: var(--text-dim); margin-bottom: 24px; font-size: 16px; }
+.page h1 { font-size: 28px; margin-bottom: 8px; }
+
+/* Home page */
+.home-hero {
+ text-align: center; padding: 40px 20px;
+ background: linear-gradient(135deg, var(--accent) 0%, #0a1628 100%);
+ border-radius: var(--radius); margin-bottom: 32px;
+}
+.home-hero h1 { font-size: 36px; margin-bottom: 8px; }
+.home-hero__subtitle { color: var(--text-dim); font-size: 18px; margin-bottom: 20px; }
+.home-hero__player { display: flex; justify-content: center; gap: 24px; font-size: 16px; }
+.home-active-pet {
+ background: var(--bg-card); padding: 24px; border-radius: var(--radius);
+ margin-bottom: 32px; text-align: center;
+}
+.home-active-pet__display {
+ display: flex; align-items: center; justify-content: center; gap: 16px; margin: 16px 0;
+}
+.home-active-pet__emoji { font-size: 64px; }
+.home-actions { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; }
+.home-action-card {
+ background: var(--bg-card); padding: 24px; border-radius: var(--radius);
+ text-align: center; transition: all 0.2s; border: 2px solid transparent;
+ color: var(--text);
+}
+.home-action-card:hover { border-color: var(--primary); background: var(--bg-card-hover); transform: translateY(-2px); }
+.home-action-card__emoji { font-size: 40px; display: block; margin-bottom: 12px; }
+.home-action-card h3 { margin-bottom: 8px; }
+.home-action-card p { color: var(--text-dim); font-size: 14px; }
+
+/* Pet Card */
+.pet-card {
+ display: flex; align-items: center; gap: 12px;
+ background: var(--bg-card); padding: 16px; border-radius: var(--radius);
+ border: 2px solid #333; cursor: pointer; transition: all 0.2s;
+}
+.pet-card:hover { background: var(--bg-card-hover); transform: translateY(-1px); }
+.pet-card--selected { border-color: var(--primary) !important; background: var(--bg-card-hover); }
+.pet-card--compact { padding: 10px 14px; }
+.pet-card--compact .pet-card__emoji { font-size: 28px; }
+.pet-card__emoji { font-size: 40px; flex-shrink: 0; }
+.pet-card__info { flex: 1; min-width: 0; }
+.pet-card__name { font-size: 16px; font-weight: 700; margin: 0; }
+.pet-card__rarity { font-size: 12px; font-weight: 700; text-transform: uppercase; }
+.pet-card__age { font-size: 13px; color: var(--text-dim); margin-top: 4px; }
+.pet-card__xp-bar {
+ position: relative; height: 14px; background: #333; border-radius: 7px;
+ overflow: hidden; margin-top: 6px;
+}
+.pet-card__xp-fill { height: 100%; background: var(--primary); border-radius: 7px; transition: width 0.3s; }
+.pet-card__xp-text { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 700; }
+.pet-card__mood { font-size: 13px; margin-top: 4px; }
+
+/* Stat bar */
+.stat-bar { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
+.stat-bar__label { width: 120px; font-size: 14px; flex-shrink: 0; }
+.stat-bar__track { flex: 1; height: 16px; background: #333; border-radius: 8px; overflow: hidden; }
+.stat-bar__fill { height: 100%; border-radius: 8px; transition: width 0.3s; }
+.stat-bar__value { width: 40px; text-align: right; font-size: 13px; font-weight: 600; }
+
+/* My Pets layout */
+.my-pets-layout { display: grid; grid-template-columns: 280px 1fr; gap: 24px; }
+@media (max-width: 768px) { .my-pets-layout { grid-template-columns: 1fr; } }
+.pet-list { display: flex; flex-direction: column; gap: 8px; }
+.pet-list h2 { font-size: 18px; margin-bottom: 8px; }
+.pet-detail { background: var(--bg-card); padding: 24px; border-radius: var(--radius); }
+.pet-detail__header { display: flex; align-items: center; gap: 16px; margin-bottom: 20px; }
+.pet-detail__emoji { font-size: 64px; }
+.pet-detail__header h2 { display: flex; align-items: center; gap: 8px; }
+.pet-detail__xp { margin-bottom: 20px; }
+.xp-bar { height: 12px; background: #333; border-radius: 6px; overflow: hidden; margin-bottom: 4px; }
+.xp-bar__fill { height: 100%; background: linear-gradient(90deg, #e94560, #ff9800); border-radius: 6px; transition: width 0.3s; }
+.pet-detail__stats { margin-bottom: 24px; }
+.pet-detail__stats h3 { margin-bottom: 12px; }
+.pet-detail__tasks { margin-bottom: 24px; }
+.pet-detail__tasks h3 { margin-bottom: 12px; }
+.task-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 8px; }
+.task-btn {
+ display: flex; flex-direction: column; align-items: center; gap: 4px;
+ padding: 14px 8px; border: 2px solid #444; border-radius: var(--radius-sm);
+ background: var(--bg); color: var(--text); cursor: pointer; transition: all 0.2s;
+}
+.task-btn:hover:not(:disabled) { border-color: var(--primary); background: var(--bg-card-hover); }
+.task-btn--cooldown { opacity: 0.5; cursor: not-allowed; }
+.task-btn__emoji { font-size: 24px; }
+.task-btn__name { font-weight: 600; font-size: 13px; }
+.task-btn__timer { font-size: 11px; color: var(--warning); }
+.task-btn__reward { font-size: 11px; color: var(--success); }
+.rename-form { display: flex; align-items: center; gap: 6px; }
+.rename-form input {
+ padding: 6px 10px; border: 1px solid #555; border-radius: var(--radius-sm);
+ background: var(--bg); color: var(--text); font-size: 14px; width: 140px;
+}
+.confirm-release { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
+.pet-detail__actions { border-top: 1px solid #333; padding-top: 16px; margin-top: 16px; }
+
+/* Nursery */
+.egg-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 16px; }
+.egg-card {
+ background: var(--bg-card); padding: 24px; border-radius: var(--radius);
+ text-align: center; border: 2px solid #333; transition: all 0.2s;
+}
+.egg-card:hover { border-color: var(--primary); transform: translateY(-2px); }
+.egg-card__emoji { font-size: 56px; display: block; margin-bottom: 12px; }
+.egg-card h3 { margin-bottom: 12px; }
+.egg-card__rarities { display: flex; gap: 6px; justify-content: center; flex-wrap: wrap; margin-bottom: 12px; }
+.egg-card__rarity-tag {
+ padding: 2px 10px; border-radius: 12px; font-size: 11px;
+ font-weight: 700; color: #fff; text-transform: uppercase;
+}
+.egg-card__price { font-size: 20px; font-weight: 700; margin-bottom: 16px; }
+
+/* Shop */
+.shop-section { margin-bottom: 32px; }
+.shop-section h2 { margin-bottom: 12px; font-size: 20px; }
+.shop-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; }
+.shop-card {
+ background: var(--bg-card); padding: 20px; border-radius: var(--radius);
+ text-align: center; border: 2px solid #333; transition: all 0.2s;
+}
+.shop-card:hover { transform: translateY(-2px); }
+.shop-card__emoji { font-size: 40px; display: block; margin-bottom: 8px; }
+.shop-card h3 { margin-bottom: 4px; font-size: 15px; }
+.shop-card__price { font-size: 16px; font-weight: 700; margin-bottom: 12px; }
+
+/* Trade */
+.trade-select { margin-bottom: 24px; }
+.trade-select h2 { margin-bottom: 12px; }
+.trade-pet-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 8px; }
+.trader-list { margin-top: 32px; }
+.trader-list h2 { margin-bottom: 16px; }
+.trader-card {
+ background: var(--bg-card); padding: 20px; border-radius: var(--radius);
+ margin-bottom: 12px; border: 2px solid #333;
+}
+.trader-card__header { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
+.trader-card__emoji { font-size: 32px; }
+.trader-card__deal { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
+.trader-card__arrow { font-size: 20px; }
+.trade-result {
+ background: var(--bg-card); padding: 24px; border-radius: var(--radius);
+ text-align: center; margin-bottom: 24px; border: 2px solid var(--success);
+}
+.trade-result__details {
+ display: flex; align-items: center; justify-content: center; gap: 24px;
+ margin: 16px 0; font-size: 40px;
+}
+.trade-result__gave, .trade-result__received { text-align: center; }
+.trade-result__gave span, .trade-result__received span { font-size: 48px; display: block; }
+.trade-result__gave p, .trade-result__received p { font-size: 14px; margin-top: 4px; }
+.trade-result__arrow { font-size: 24px; }
+
+/* Hatch overlay */
+.hatch-overlay {
+ position: fixed; inset: 0; background: rgba(0,0,0,0.85);
+ display: flex; align-items: center; justify-content: center; z-index: 100;
+ animation: fadeIn 0.3s ease;
+}
+.hatch-modal {
+ background: var(--bg-card); padding: 40px; border-radius: var(--radius);
+ text-align: center; max-width: 400px; width: 90%;
+ border: 2px solid var(--primary);
+}
+.hatch-modal h2 { margin-bottom: 20px; font-size: 24px; }
+.hatch-modal__egg { font-size: 60px; margin-bottom: 8px; }
+.hatch-modal__arrow { font-size: 28px; margin-bottom: 8px; }
+.hatch-modal__pet { font-size: 80px; margin-bottom: 12px; }
+.hatch-modal h3 { font-size: 22px; margin-bottom: 4px; }
+.hatch-modal__rarity { font-size: 14px; font-weight: 700; text-transform: uppercase; display: block; margin-bottom: 20px; }
+
+/* Empty state */
+.empty-state { text-align: center; padding: 60px 20px; }
+.empty-state__emoji { font-size: 64px; display: block; margin-bottom: 16px; }
+.empty-state h2 { margin-bottom: 8px; }
+.empty-state p { color: var(--text-dim); }
diff --git a/demos/adopt-me-app/src/main.jsx b/demos/adopt-me-app/src/main.jsx
new file mode 100644
index 000000000..47742c1b1
--- /dev/null
+++ b/demos/adopt-me-app/src/main.jsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+import './index.css';
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+
+);
diff --git a/demos/adopt-me-app/src/pages/Home.jsx b/demos/adopt-me-app/src/pages/Home.jsx
new file mode 100644
index 000000000..2b27220fb
--- /dev/null
+++ b/demos/adopt-me-app/src/pages/Home.jsx
@@ -0,0 +1,58 @@
+import { useGame } from '../context/GameContext';
+import { Link } from 'react-router-dom';
+
+export default function Home() {
+ const { state } = useGame();
+ const activePet = state.pets.find(p => p.instanceId === state.activePetId);
+
+ return (
+
+
+
Welcome to Adopt Me!
+
Adopt, raise, and trade adorable pets!
+
+ 👤 {state.player.name}
+ 🪙 {state.player.coins.toLocaleString()} coins
+ 🐾 {state.pets.length} pets
+
+
+
+ {activePet && (
+
+
Your Active Pet
+
+
{activePet.emoji}
+
+
{activePet.nickname}
+
{activePet.name}
+
+
+
Care for {activePet.nickname}
+
+ )}
+
+
+
+
🥚
+
Nursery
+
Hatch eggs to discover new pets
+
+
+
🛒
+
Pet Shop
+
Buy pets directly from the shop
+
+
+
🤝
+
Trading Plaza
+
Trade pets with other players
+
+
+
❤️
+
My Pets
+
Feed, play, and grow your pets
+
+
+
+ );
+}
diff --git a/demos/adopt-me-app/src/pages/MyPets.jsx b/demos/adopt-me-app/src/pages/MyPets.jsx
new file mode 100644
index 000000000..ce64392fb
--- /dev/null
+++ b/demos/adopt-me-app/src/pages/MyPets.jsx
@@ -0,0 +1,151 @@
+import { useState } from 'react';
+import { useGame } from '../context/GameContext';
+import PetCard from '../components/PetCard';
+import StatBar from '../components/StatBar';
+import { TASKS, PET_AGES, getXpForAge } from '../data/pets';
+
+export default function MyPets() {
+ const { state, setActivePet, doTask, renamePet, releasePet } = useGame();
+ const [renaming, setRenaming] = useState(null);
+ const [newName, setNewName] = useState('');
+ const [confirmRelease, setConfirmRelease] = useState(null);
+
+ const activePet = state.pets.find(p => p.instanceId === state.activePetId);
+
+ if (state.pets.length === 0) {
+ return (
+
+
My Pets
+
+
🐾
+
No pets yet!
+
Visit the Nursery or Shop to adopt your first pet.
+
+
+ );
+ }
+
+ const now = Date.now();
+
+ function startRename(pet) {
+ setRenaming(pet.instanceId);
+ setNewName(pet.nickname);
+ }
+
+ function submitRename(instanceId) {
+ if (newName.trim()) {
+ renamePet(instanceId, newName.trim());
+ }
+ setRenaming(null);
+ }
+
+ function handleRelease(instanceId) {
+ releasePet(instanceId);
+ setConfirmRelease(null);
+ }
+
+ return (
+
+
My Pets
+
+
+
+
Your Collection ({state.pets.length})
+ {state.pets.map(pet => (
+
setActivePet(pet.instanceId)}
+ />
+ ))}
+
+
+ {activePet && (
+
+
+
{activePet.emoji}
+
+ {renaming === activePet.instanceId ? (
+
+ setNewName(e.target.value)}
+ onKeyDown={e => e.key === 'Enter' && submitRename(activePet.instanceId)}
+ maxLength={20}
+ autoFocus
+ aria-label="Pet nickname"
+ />
+ submitRename(activePet.instanceId)}>Save
+ setRenaming(null)}>Cancel
+
+ ) : (
+
+ {activePet.nickname}
+ startRename(activePet)} aria-label="Rename pet">✏️
+
+ )}
+
{activePet.name} · {PET_AGES[activePet.ageIndex]}
+
+
+
+
+
+
XP: {activePet.xp} / {getXpForAge(activePet.ageIndex)}
+
+
+
+
Stats
+
+
+
+
+
+
+
+
+
Care Tasks
+
+ {TASKS.map(task => {
+ const cooldownEnd = activePet.taskCooldowns[task.id] || 0;
+ const onCooldown = now < cooldownEnd;
+ const cooldownLeft = onCooldown ? Math.ceil((cooldownEnd - now) / 1000) : 0;
+ return (
+ doTask(activePet.instanceId, task)}
+ >
+ {task.emoji}
+ {task.name}
+ {onCooldown && {cooldownLeft}s }
+ {!onCooldown && +{task.xp} XP }
+
+ );
+ })}
+
+
+
+
+ {confirmRelease === activePet.instanceId ? (
+
+
Are you sure you want to release {activePet.nickname}?
+
handleRelease(activePet.instanceId)}>Yes, Release
+
setConfirmRelease(null)}>Cancel
+
+ ) : (
+
setConfirmRelease(activePet.instanceId)}>
+ Release Pet
+
+ )}
+
+
+ )}
+
+
+ );
+}
diff --git a/demos/adopt-me-app/src/pages/Nursery.jsx b/demos/adopt-me-app/src/pages/Nursery.jsx
new file mode 100644
index 000000000..322d5a5c5
--- /dev/null
+++ b/demos/adopt-me-app/src/pages/Nursery.jsx
@@ -0,0 +1,40 @@
+import { useGame } from '../context/GameContext';
+import { EGGS, PET_RARITIES } from '../data/pets';
+
+export default function Nursery() {
+ const { state, hatchEgg } = useGame();
+
+ return (
+
+
Nursery
+
Buy and hatch eggs to discover new pets!
+
+
+ {EGGS.map(egg => {
+ const canAfford = state.player.coins >= egg.price;
+ return (
+
+
{egg.emoji}
+
{egg.name}
+
+ {egg.rarities.map(r => (
+
+ {PET_RARITIES[r].name}
+
+ ))}
+
+
🪙 {egg.price.toLocaleString()}
+
hatchEgg(egg.id)}
+ >
+ {canAfford ? 'Hatch!' : 'Not enough coins'}
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/demos/adopt-me-app/src/pages/Shop.jsx b/demos/adopt-me-app/src/pages/Shop.jsx
new file mode 100644
index 000000000..2770ce937
--- /dev/null
+++ b/demos/adopt-me-app/src/pages/Shop.jsx
@@ -0,0 +1,53 @@
+import { useGame } from '../context/GameContext';
+import { PET_CATALOG, PET_RARITIES } from '../data/pets';
+
+export default function Shop() {
+ const { state, adoptPet, spendCoins } = useGame();
+
+ function handleBuy(pet) {
+ if (state.player.coins < pet.basePrice) return;
+ spendCoins(pet.basePrice);
+ adoptPet(pet);
+ }
+
+ const grouped = {};
+ PET_CATALOG.forEach(pet => {
+ if (!grouped[pet.rarity]) grouped[pet.rarity] = [];
+ grouped[pet.rarity].push(pet);
+ });
+
+ return (
+
+
Pet Shop
+
Browse and buy pets directly. Your coins: 🪙 {state.player.coins.toLocaleString()}
+
+ {Object.entries(grouped).map(([rarity, pets]) => {
+ const rarityInfo = PET_RARITIES[rarity];
+ return (
+
+
{rarityInfo.name}
+
+ {pets.map(pet => {
+ const canAfford = state.player.coins >= pet.basePrice;
+ return (
+
+
{pet.emoji}
+
{pet.name}
+
🪙 {pet.basePrice.toLocaleString()}
+
handleBuy(pet)}
+ >
+ {canAfford ? 'Adopt!' : 'Too expensive'}
+
+
+ );
+ })}
+
+
+ );
+ })}
+
+ );
+}
diff --git a/demos/adopt-me-app/src/pages/Trade.jsx b/demos/adopt-me-app/src/pages/Trade.jsx
new file mode 100644
index 000000000..fbf31720f
--- /dev/null
+++ b/demos/adopt-me-app/src/pages/Trade.jsx
@@ -0,0 +1,127 @@
+import { useState, useMemo } from 'react';
+import { useGame } from '../context/GameContext';
+import PetCard from '../components/PetCard';
+import { PET_CATALOG, PET_RARITIES } from '../data/pets';
+
+const NPC_TRADERS = [
+ { name: 'Friendly Farmer', emoji: '🧑\uD83C\uDF3E', wants: 'COMMON', offers: 'UNCOMMON' },
+ { name: 'Explorer Emma', emoji: '🧑\uD83D\uDE80', wants: 'UNCOMMON', offers: 'RARE' },
+ { name: 'Royal Knight', emoji: '🤴', wants: 'RARE', offers: 'ULTRA_RARE' },
+ { name: 'Mystic Mage', emoji: '🧙', wants: 'ULTRA_RARE', offers: 'LEGENDARY' },
+];
+
+export default function Trade() {
+ const { state, dispatch } = useGame();
+ const [selectedPetId, setSelectedPetId] = useState(null);
+ const [tradeResult, setTradeResult] = useState(null);
+
+ const selectedPet = state.pets.find(p => p.instanceId === selectedPetId);
+
+ const availableTraders = useMemo(() => {
+ return NPC_TRADERS.map(trader => {
+ const eligible = state.pets.filter(p => p.rarity === trader.wants);
+ const offerPets = PET_CATALOG.filter(p => p.rarity === trader.offers);
+ return { ...trader, eligible, offerPets };
+ });
+ }, [state.pets]);
+
+ function executeTrade(trader) {
+ if (!selectedPet || selectedPet.rarity !== trader.wants) return;
+ const offerPets = PET_CATALOG.filter(p => p.rarity === trader.offers);
+ const received = offerPets[Math.floor(Math.random() * offerPets.length)];
+
+ dispatch({
+ type: 'COMPLETE_TRADE',
+ payload: {
+ give: [selectedPet.instanceId],
+ receive: [received],
+ },
+ });
+
+ setTradeResult({ gave: selectedPet, received, trader });
+ setSelectedPetId(null);
+ }
+
+ return (
+
+
Trading Plaza
+
Trade your pets with NPC traders to get rarer ones!
+
+ {tradeResult && (
+
+
Trade Complete!
+
+
+
{tradeResult.gave.emoji}
+
{tradeResult.gave.nickname}
+
+
➡️
+
+
{tradeResult.received.emoji}
+
{tradeResult.received.name}
+
+
+
setTradeResult(null)}>Continue
+
+ )}
+
+ {state.pets.length > 0 && (
+
+
Select a Pet to Trade
+
+ {state.pets.map(pet => (
+
setSelectedPetId(pet.instanceId)}
+ />
+ ))}
+
+
+ )}
+
+
+
NPC Traders
+ {availableTraders.map(trader => {
+ const canTrade = selectedPet && selectedPet.rarity === trader.wants;
+ const wantsRarity = PET_RARITIES[trader.wants];
+ const offersRarity = PET_RARITIES[trader.offers];
+ return (
+
+
+ {trader.emoji}
+
{trader.name}
+
+
+
+ Wants: {wantsRarity.name} pet
+
+ ➡️
+
+ Offers: {offersRarity.name} pet
+
+
+
executeTrade(trader)}
+ >
+ {canTrade ? 'Trade!' : `Select a ${wantsRarity.name} pet first`}
+
+
+ );
+ })}
+
+
+ {state.pets.length === 0 && (
+
+
🤝
+
No pets to trade
+
Adopt some pets first, then come back to trade!
+
+ )}
+
+ );
+}
diff --git a/demos/adopt-me-app/src/setupTests.js b/demos/adopt-me-app/src/setupTests.js
new file mode 100644
index 000000000..7b0828bfa
--- /dev/null
+++ b/demos/adopt-me-app/src/setupTests.js
@@ -0,0 +1 @@
+import '@testing-library/jest-dom';
diff --git a/demos/adopt-me-app/vite.config.js b/demos/adopt-me-app/vite.config.js
new file mode 100644
index 000000000..3b1ab3c8d
--- /dev/null
+++ b/demos/adopt-me-app/vite.config.js
@@ -0,0 +1,11 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()],
+ test: {
+ globals: true,
+ environment: 'jsdom',
+ setupFiles: './src/setupTests.js',
+ },
+})
From 6f29564bafa3ef6adf8db108af7751c15ab5d143 Mon Sep 17 00:00:00 2001
From: agentforce314
Date: Wed, 29 Apr 2026 02:07:04 -0700
Subject: [PATCH 011/251] fix: Show elapsed time and token count in REPL
spinner row
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Mirrors typescript/src/components/Spinner/SpinnerAnimationRow.tsx so the
default Python REPL renders ``(esc to interrupt · 12s · ↓ 1.2k tokens)``
under the active spinner once the 30s threshold is crossed, instead of
the previous static ``(esc to cancel · enter to queue)`` hint.
Three coupled fixes were required to flow per-turn token usage end-to-end:
- src/utils/format.py: new module porting ``formatDuration`` /
``formatNumber`` from typescript/src/utils/format.ts, including the
JS-style round-half-up for the seconds field so output matches.
- src/repl/live_status.py: ``LiveStatus`` now tracks its own start time
and exposes ``set_tokens(n)``. The spinner suffix is rebuilt every
frame from the running elapsed and token total. ``paused()`` snapshots
and restores the timer so foreground prompts don't reset it.
- src/repl/core.py: the ``chat()`` engine loop accumulates per-turn
input+output tokens from each AssistantMessage.usage and pushes the
running total to the spinner. ``self._stats_*`` cumulative counters
are unchanged.
- src/query/query.py: ``AssistantMessage`` was being constructed without
the ``usage`` field, dropping per-turn token totals on the floor.
``/stats`` was silently broken by the same omission.
- src/providers/openai_compatible.py: streaming requests now pass
``stream_options={"include_usage": True}`` so the OpenAI SDK emits a
final usage chunk; without it ``chat_stream_response`` always
returned ``usage={}`` and downstream consumers saw zero tokens.
Tests: new tests/test_format.py covers ``format_duration`` /
``format_number`` parity with the TS reference (30 cases, all green).
Co-Authored-By: Claude Opus 4.7
---
src/providers/openai_compatible.py | 14 +++-
src/query/query.py | 1 +
src/repl/core.py | 18 +++--
src/repl/live_status.py | 67 ++++++++++++++++++-
src/utils/format.py | 102 +++++++++++++++++++++++++++++
tests/test_format.py | 79 ++++++++++++++++++++++
6 files changed, 273 insertions(+), 8 deletions(-)
create mode 100644 src/utils/format.py
create mode 100644 tests/test_format.py
diff --git a/src/providers/openai_compatible.py b/src/providers/openai_compatible.py
index 4aa279ac4..a25a0517f 100644
--- a/src/providers/openai_compatible.py
+++ b/src/providers/openai_compatible.py
@@ -336,12 +336,24 @@ def chat_stream_response(
converted = [_convert_to_openai_tool_schema(t) for t in tools]
extra_kwargs["tools"] = [t for t in converted if t is not None]
+ # ``stream_options.include_usage`` opts the OpenAI streaming API
+ # into emitting a final ``usage`` chunk; without it, ``chunk.usage``
+ # is always ``None`` and the rebuilt ChatResponse has empty token
+ # counts. The spinner row + ``/stats`` rely on this — see
+ # ``_build_usage_dict`` below and the consumer in
+ # ``src/query/query.py``.
+ stream_kwargs = {k: v for k, v in kwargs.items() if k not in ["model", "tools"]}
+ existing_stream_options = stream_kwargs.pop("stream_options", None) or {}
+ stream_kwargs["stream_options"] = {
+ **existing_stream_options,
+ "include_usage": True,
+ }
stream = self.client.chat.completions.create(
model=model,
messages=provider_messages,
stream=True,
**extra_kwargs,
- **{k: v for k, v in kwargs.items() if k not in ["model", "tools"]},
+ **stream_kwargs,
)
content_parts: list[str] = []
diff --git a/src/query/query.py b/src/query/query.py
index be8f29e6a..3809c52f5 100644
--- a/src/query/query.py
+++ b/src/query/query.py
@@ -269,6 +269,7 @@ async def _call_model_sync(
assistant_msg = AssistantMessage(
content=assistant_blocks if assistant_blocks else "",
stop_reason=stop_reason,
+ usage=response.usage,
)
if response.reasoning_content:
# Preserve provider thinking metadata for follow-up turns.
diff --git a/src/repl/core.py b/src/repl/core.py
index a6ddb5328..f6c82a368 100644
--- a/src/repl/core.py
+++ b/src/repl/core.py
@@ -2289,6 +2289,11 @@ async def _run_query() -> tuple[str, bool]:
last_text_was_printed = False
api_call_count = 0
tool_use_map: dict[str, tuple[str, dict]] = {}
+ # Per-turn token totals — surfaced to the spinner suffix
+ # via ``status.set_tokens(...)``. Local to the closure so
+ # they reset every turn; ``self._stats_*`` remain the
+ # session-cumulative counters for ``/stats``.
+ turn_tokens = 0
# Track whether a Task*/TodoWrite round is "in flight" so we
# can coalesce a run of task-management calls into a single
# TaskListV2-style snapshot instead of dumping one ``●`` bullet
@@ -2331,12 +2336,13 @@ def _flush_task_snapshot_if_any() -> None:
self.session.conversation.add_assistant_message(msg.content)
usage = getattr(msg, "usage", None)
if isinstance(usage, dict):
- self._stats_input_tokens += int(
- usage.get("input_tokens", 0) or 0
- )
- self._stats_output_tokens += int(
- usage.get("output_tokens", 0) or 0
- )
+ in_toks = int(usage.get("input_tokens", 0) or 0)
+ out_toks = int(usage.get("output_tokens", 0) or 0)
+ self._stats_input_tokens += in_toks
+ self._stats_output_tokens += out_toks
+ turn_tokens += in_toks + out_toks
+ if _engine_status_ref:
+ _engine_status_ref[0].set_tokens(turn_tokens)
content = msg.content
if isinstance(content, str):
if content:
diff --git a/src/repl/live_status.py b/src/repl/live_status.py
index d79928dd6..b02cf9874 100644
--- a/src/repl/live_status.py
+++ b/src/repl/live_status.py
@@ -33,9 +33,12 @@
import asyncio
import threading
+import time
from contextlib import contextmanager
from typing import Callable, Iterator
+from src.utils.format import format_duration, format_number
+
try:
from prompt_toolkit.application import Application
from prompt_toolkit.buffer import Buffer
@@ -68,6 +71,9 @@
"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏",
)
_FRAME_INTERVAL = 0.08
+# Mirrors ``SHOW_TOKENS_AFTER_MS`` in
+# ``typescript/src/components/Spinner/SpinnerAnimationRow.tsx``.
+_SHOW_TIMER_AFTER_MS = 30_000
class LiveStatus:
@@ -103,6 +109,7 @@ def __init__(
on_submit: Callable[[str], None] | None = None,
on_expand: Callable[[], None] | None = None,
completer=None,
+ verbose: bool = False,
) -> None:
if not _HAS_PROMPT_TOOLKIT:
raise RuntimeError(
@@ -125,6 +132,18 @@ def __init__(
self._ready = threading.Event()
self._lock = threading.Lock()
self._input_buffer: Buffer | None = None
+ # Tracks the wall-clock origin for the spinner's elapsed-time
+ # readout (mirrors ``loadingStartTimeRef`` in the TS spinner).
+ # Set in ``__enter__`` / ``paused.__exit__`` and cleared in
+ # ``_stop`` so a paused-and-resumed cycle restarts the timer.
+ self._started_at: float | None = None
+ # Latest token total surfaced via :meth:`set_tokens`. Mirrors
+ # ``responseLengthRef.current / 4`` + teammate sum from
+ # ``SpinnerAnimationRow.tsx``.
+ self._tokens: int = 0
+ # Force-show the elapsed/token suffix before the 30s threshold.
+ # Maps to the TS ``verbose`` prop in ``SpinnerWithVerb``.
+ self._verbose = verbose
# ---- public API ----
def update(self, message: str) -> None:
@@ -134,7 +153,24 @@ def update(self, message: str) -> None:
self._message = message
self._invalidate()
+ def set_tokens(self, n: int) -> None:
+ """Update the token count shown in the spinner suffix.
+
+ Safe to call from any thread. Pass the running per-turn total
+ (input + output tokens) — the spinner re-renders on the next
+ frame tick. Mirrors how the TS spinner reads
+ ``responseLengthRef.current / 4`` each frame.
+ """
+
+ with self._lock:
+ if n == self._tokens:
+ return
+ self._tokens = max(0, int(n))
+ self._invalidate()
+
def __enter__(self) -> "LiveStatus":
+ self._started_at = time.monotonic()
+ self._tokens = 0
self._thread = threading.Thread(
target=self._run_thread,
name="clawcodex-live-status",
@@ -168,6 +204,11 @@ def paused(self) -> Iterator[None]:
on_submit = self._on_submit
on_expand = self._on_expand
completer = self._completer
+ # Preserve the timer / token counter across the pause so the
+ # spinner picks up where it left off after the foreground prompt
+ # finishes. ``_stop`` clears ``_started_at``; capture first.
+ started_at = self._started_at
+ tokens = self._tokens
self._stop()
try:
yield
@@ -178,6 +219,8 @@ def paused(self) -> Iterator[None]:
self._on_expand = on_expand
self._completer = completer
self._frame_index = 0
+ self._started_at = started_at
+ self._tokens = tokens
self._ready = threading.Event()
self._thread = threading.Thread(
target=self._run_thread,
@@ -369,14 +412,31 @@ def _accept(buf: "Buffer") -> bool:
def _render_spinner_text(self) -> "FormattedText":
with self._lock:
message = self._message
+ started_at = self._started_at
+ tokens = self._tokens
+ verbose = self._verbose
frame = _SPINNER_FRAMES[self._frame_index % len(_SPINNER_FRAMES)]
self._frame_index += 1
+
+ # Match ``SpinnerAnimationRow.tsx``'s suffix:
+ # ``(esc to interrupt · 12s · ↓ 1.2k tokens)``.
+ # Timer + token suffix gated by 30s elapsed (or ``verbose``),
+ # tokens additionally require a non-zero count.
+ elapsed_ms = (time.monotonic() - started_at) * 1000 if started_at else 0.0
+ wants_timer = verbose or elapsed_ms > _SHOW_TIMER_AFTER_MS
+ suffix = " (esc to interrupt"
+ if wants_timer:
+ suffix += f" · {format_duration(elapsed_ms)}"
+ if tokens > 0:
+ suffix += f" · ↓ {format_number(tokens)} tokens"
+ suffix += ")"
+
return FormattedText(
[
("class:spinner", frame),
("", " "),
("class:status", message),
- ("class:hint", " (esc to cancel · enter to queue)"),
+ ("class:hint", suffix),
]
)
@@ -403,3 +463,8 @@ def _stop(self) -> None:
self._loop = None
self._thread = None
self._input_buffer = None
+ # Cleared so a fresh ``__enter__`` after a full teardown starts
+ # the elapsed timer from zero. ``paused()`` snapshots and
+ # restores this so its pause/resume cycle preserves the timer.
+ self._started_at = None
+ self._tokens = 0
diff --git a/src/utils/format.py b/src/utils/format.py
new file mode 100644
index 000000000..f509eb9a1
--- /dev/null
+++ b/src/utils/format.py
@@ -0,0 +1,102 @@
+"""Pure display formatters — Python port of ``typescript/src/utils/format.ts``.
+
+Used by the REPL spinner (and any future caller) to render durations and
+token counts in the same compact form as the TypeScript Ink reference UI:
+``12s`` / ``1m 30s`` for time, ``900`` / ``1.2k`` / ``1.0m`` for counts.
+
+Behaviour matches the TS reference closely enough that the two
+implementations produce byte-identical output for the spinner row.
+"""
+
+from __future__ import annotations
+
+import math
+
+
+def format_duration(
+ ms: float,
+ *,
+ hide_trailing_zeros: bool = False,
+ most_significant_only: bool = False,
+) -> str:
+ """Render a millisecond duration the same way ``formatDuration`` in
+ ``typescript/src/utils/format.ts`` does."""
+
+ if ms < 60_000:
+ if ms == 0:
+ return "0s"
+ if ms < 1:
+ return f"{ms / 1000:.1f}s"
+ return f"{int(math.floor(ms / 1000))}s"
+
+ days = int(math.floor(ms / 86_400_000))
+ hours = int(math.floor((ms % 86_400_000) / 3_600_000))
+ minutes = int(math.floor((ms % 3_600_000) / 60_000))
+ # Match JS ``Math.round`` (round-half-up) rather than Python's
+ # banker's rounding so 0.5s ticks land identically across the two
+ # implementations.
+ seconds = int(math.floor((ms % 60_000) / 1000 + 0.5))
+
+ # Carry rounded seconds (e.g. 59.5s → 60s → 1m).
+ if seconds == 60:
+ seconds = 0
+ minutes += 1
+ if minutes == 60:
+ minutes = 0
+ hours += 1
+ if hours == 24:
+ hours = 0
+ days += 1
+
+ if most_significant_only:
+ if days > 0:
+ return f"{days}d"
+ if hours > 0:
+ return f"{hours}h"
+ if minutes > 0:
+ return f"{minutes}m"
+ return f"{seconds}s"
+
+ hide = hide_trailing_zeros
+
+ if days > 0:
+ if hide and hours == 0 and minutes == 0:
+ return f"{days}d"
+ if hide and minutes == 0:
+ return f"{days}d {hours}h"
+ return f"{days}d {hours}h {minutes}m"
+ if hours > 0:
+ if hide and minutes == 0 and seconds == 0:
+ return f"{hours}h"
+ if hide and seconds == 0:
+ return f"{hours}h {minutes}m"
+ return f"{hours}h {minutes}m {seconds}s"
+ if minutes > 0:
+ if hide and seconds == 0:
+ return f"{minutes}m"
+ return f"{minutes}m {seconds}s"
+ return f"{seconds}s"
+
+
+def format_number(n: float) -> str:
+ """Compact-notation number formatter matching ``formatNumber``.
+
+ Examples: ``900`` → ``"900"``, ``1321`` → ``"1.3k"``, ``1_000`` →
+ ``"1.0k"``, ``1_500_000`` → ``"1.5m"``.
+ """
+
+ if n < 1000:
+ # TS path: Intl.NumberFormat compact with no minimum fraction
+ # digits collapses < 1000 values to bare integers.
+ return str(int(n))
+ if n < 1_000_000:
+ return f"{n / 1000:.1f}k"
+ if n < 1_000_000_000:
+ return f"{n / 1_000_000:.1f}m"
+ return f"{n / 1_000_000_000:.1f}b"
+
+
+def format_tokens(count: int) -> str:
+ """Mirror ``formatTokens`` in TS: drop the ``.0`` when present."""
+
+ return format_number(count).replace(".0", "")
diff --git a/tests/test_format.py b/tests/test_format.py
new file mode 100644
index 000000000..92e675c84
--- /dev/null
+++ b/tests/test_format.py
@@ -0,0 +1,79 @@
+"""Parity tests for ``src.utils.format``.
+
+These mirror the behaviour of ``typescript/src/utils/format.ts`` so the
+REPL spinner row produces byte-identical output across implementations.
+"""
+
+from __future__ import annotations
+
+import pytest
+
+from src.utils.format import format_duration, format_number, format_tokens
+
+
+@pytest.mark.parametrize(
+ "ms,expected",
+ [
+ (0, "0s"),
+ (500, "0s"),
+ (999, "0s"),
+ (1_000, "1s"),
+ (12_345, "12s"),
+ (59_999, "59s"),
+ (60_000, "1m 0s"),
+ (90_000, "1m 30s"),
+ (3_599_000, "59m 59s"),
+ (3_600_000, "1h 0m 0s"),
+ (3_660_500, "1h 1m 1s"),
+ (86_400_000, "1d 0h 0m"),
+ (90_061_000, "1d 1h 1m"),
+ ],
+)
+def test_format_duration(ms: int, expected: str) -> None:
+ assert format_duration(ms) == expected
+
+
+@pytest.mark.parametrize(
+ "ms,expected",
+ [
+ (90_000, "1m"),
+ (3_600_000, "1h"),
+ (86_400_000, "1d"),
+ (45_000, "45s"),
+ ],
+)
+def test_format_duration_most_significant_only(ms: int, expected: str) -> None:
+ assert format_duration(ms, most_significant_only=True) == expected
+
+
+def test_format_duration_hide_trailing_zeros() -> None:
+ assert format_duration(3_600_000, hide_trailing_zeros=True) == "1h"
+ assert format_duration(3_660_000, hide_trailing_zeros=True) == "1h 1m"
+ assert format_duration(86_400_000, hide_trailing_zeros=True) == "1d"
+ assert format_duration(90_000_000, hide_trailing_zeros=True) == "1d 1h"
+
+
+@pytest.mark.parametrize(
+ "n,expected",
+ [
+ (0, "0"),
+ (1, "1"),
+ (900, "900"),
+ (999, "999"),
+ (1_000, "1.0k"),
+ (1_321, "1.3k"),
+ (12_500, "12.5k"),
+ (999_999, "1000.0k"),
+ (1_000_000, "1.0m"),
+ (1_500_000, "1.5m"),
+ (2_000_000_000, "2.0b"),
+ ],
+)
+def test_format_number(n: int, expected: str) -> None:
+ assert format_number(n) == expected
+
+
+def test_format_tokens_drops_trailing_zero() -> None:
+ assert format_tokens(1_000) == "1k"
+ assert format_tokens(1_321) == "1.3k"
+ assert format_tokens(900) == "900"
From c5bbe4b9c8b1e0f2c71f73b500b3d7d7b1db103e Mon Sep 17 00:00:00 2001
From: agentforce314
Date: Thu, 30 Apr 2026 01:06:09 -0700
Subject: [PATCH 012/251] feat(skills): port skills subsystem to parity with
TypeScript
Closes the gap between the Python skills subsystem and the TypeScript
reference implementation by unifying the registry, porting the runtime
render pipeline, hardening the frontmatter parser, and shipping the
bundled-skills catalogue.
Highlights:
- Unified skill registry: SkillTool now sees nested category:skill
namespacing, env-overridden dirs, MCP, and bundled skills via a
single get_all_skills() entry point. Activated conditional skills
also flow through the unified path.
- Runtime render pipeline (runtime_substitution.py): TS-order
base-dir header -> arg sub -> CLAUDE_SKILL_DIR -> CLAUDE_SESSION_ID
-> embedded shell exec (both fenced and inline patterns). MCP
skills skip exec; failures render visibly inline.
- Frontmatter swapped to PyYAML; Skill gains hooks/shell fields;
validators ported for description, model, effort, shell,
allowed-tools, and hooks (degrade gracefully, never raise).
- Realpath-based dedup, bare mode, CLAUDE_CODE_DISABLE_POLICY_SKILLS,
pathspec-based gitignore semantics for paths:, git check-ignore
for dynamic discovery.
- Bundled-skills package with idempotent init_bundled_skills() and
five ported skills: simplify, loop, debug, stuck, verify-content.
Tests: 251 skill tests passing (49 baseline + 117 dev + 85 QA),
0 regressions in the broader skill+command suite. Coverage 79-88%
on core skill modules. Includes adversarial unit tests, end-to-end
tests over real on-disk fixtures, and a manual smoke runner at
scripts/run_skill_smoke.py.
Tier-B bundled skills (update-config, keybindings-help, batch,
claude-api, claude-in-chrome, schedule-remote-agents) are deferred
pending Python-side runtime infrastructure.
Co-Authored-By: Claude Opus 4.7
---
pyproject.toml | 8 +
requirements.txt | 2 +
scripts/run_skill_smoke.py | 173 ++++
src/skills/__init__.py | 28 +
src/skills/bundled/__init__.py | 73 ++
src/skills/bundled/debug.py | 187 ++++
src/skills/bundled/loop.py | 272 +++++
src/skills/bundled/simplify.py | 88 ++
src/skills/bundled/stuck.py | 49 +
src/skills/bundled/verify_content.py | 57 +
src/skills/bundled_skills.py | 58 ++
src/skills/frontmatter.py | 165 +--
src/skills/loader.py | 980 ++++++++++++++----
src/skills/model.py | 43 +-
src/skills/runtime_substitution.py | 260 +++++
src/tool_system/context.py | 6 +
src/tool_system/tools/skill.py | 108 +-
tests/fixtures/skills/commit-helper/SKILL.md | 11 +
.../skills/frontend/add-component/SKILL.md | 8 +
tests/fixtures/skills/lint-py/SKILL.md | 13 +
tests/test_skills_bundled.py | 18 +-
tests/test_skills_bundled_catalogue.py | 272 +++++
tests/test_skills_dedup_and_paths.py | 502 +++++++++
tests/test_skills_dedup_paths.py | 368 +++++++
tests/test_skills_e2e.py | 368 +++++++
tests/test_skills_frontmatter_validators.py | 387 +++++++
tests/test_skills_frontmatter_yaml.py | 315 ++++++
tests/test_skills_runtime_substitution.py | 450 ++++++++
tests/test_skills_shell_exec.py | 426 ++++++++
tests/test_skills_substitutions.py | 293 ++++++
tests/test_skills_unified.py | 256 +++++
uv.lock | 77 ++
32 files changed, 6041 insertions(+), 280 deletions(-)
create mode 100644 scripts/run_skill_smoke.py
create mode 100644 src/skills/bundled/__init__.py
create mode 100644 src/skills/bundled/debug.py
create mode 100644 src/skills/bundled/loop.py
create mode 100644 src/skills/bundled/simplify.py
create mode 100644 src/skills/bundled/stuck.py
create mode 100644 src/skills/bundled/verify_content.py
create mode 100644 src/skills/runtime_substitution.py
create mode 100644 tests/fixtures/skills/commit-helper/SKILL.md
create mode 100644 tests/fixtures/skills/frontend/add-component/SKILL.md
create mode 100644 tests/fixtures/skills/lint-py/SKILL.md
create mode 100644 tests/test_skills_bundled_catalogue.py
create mode 100644 tests/test_skills_dedup_and_paths.py
create mode 100644 tests/test_skills_dedup_paths.py
create mode 100644 tests/test_skills_e2e.py
create mode 100644 tests/test_skills_frontmatter_validators.py
create mode 100644 tests/test_skills_frontmatter_yaml.py
create mode 100644 tests/test_skills_runtime_substitution.py
create mode 100644 tests/test_skills_shell_exec.py
create mode 100644 tests/test_skills_substitutions.py
create mode 100644 tests/test_skills_unified.py
diff --git a/pyproject.toml b/pyproject.toml
index b0655a03a..2b33ac43c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -32,6 +32,14 @@ dependencies = [
"prompt-toolkit",
"tiktoken>=0.7.0",
"textual>=0.79",
+ # YAML frontmatter parser (SKILL.md, agent files, output styles).
+ # Required for nested structures like ``hooks:`` and ``shell:`` to
+ # round-trip through ``src.skills.frontmatter.parse_frontmatter``.
+ "PyYAML>=6.0",
+ # gitwildmatch path matching for conditional-skill ``paths:`` rules.
+ # The TS port uses the `ignore` library; ``pathspec`` is the Python
+ # equivalent (full ``**`` recursion + anchoring + negation).
+ "pathspec>=0.11",
]
[project.optional-dependencies]
diff --git a/requirements.txt b/requirements.txt
index 2ce8e09da..450c77d37 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,6 +6,8 @@ python-dotenv>=1.0.0
rich>=13.0.0
prompt-toolkit>=3.0.0
textual>=0.79
+PyYAML>=6.0
+pathspec>=0.11
# Development Dependencies
build>=1.0.0
diff --git a/scripts/run_skill_smoke.py b/scripts/run_skill_smoke.py
new file mode 100644
index 000000000..dc1711cea
--- /dev/null
+++ b/scripts/run_skill_smoke.py
@@ -0,0 +1,173 @@
+#!/usr/bin/env python3
+"""Manual smoke-test runner for the QA-2 example skills.
+
+Loads every skill under ``tests/fixtures/skills/`` (plus the bundled
+catalogue from DEV-5), prints a listing that mirrors what the model
+would see in its system-reminder, and then invokes each in turn so you
+can eyeball the rendered prompt.
+
+Useful as:
+
+ - A manual artifact for the user / team-lead ("here's what skills
+ look like once they hit the model").
+ - A quick smoke-check that the disk → registry → SkillTool pipeline
+ is wired correctly without spinning up a real REPL.
+
+Run from the repo root:
+
+ python scripts/run_skill_smoke.py
+
+No arguments. Output goes to stdout.
+"""
+
+from __future__ import annotations
+
+import os
+import shutil
+import sys
+import tempfile
+from pathlib import Path
+
+
+# Make the repo root importable when the script is launched from a
+# subdirectory.
+REPO_ROOT = Path(__file__).resolve().parent.parent
+if str(REPO_ROOT) not in sys.path:
+ sys.path.insert(0, str(REPO_ROOT))
+
+
+from src.skills.bundled import init_bundled_skills # noqa: E402
+from src.skills.bundled_skills import clear_bundled_skills # noqa: E402
+from src.skills.loader import ( # noqa: E402
+ activate_conditional_skills_for_paths,
+ clear_dynamic_skills,
+ clear_skill_caches,
+ clear_skill_registry,
+ get_all_skills,
+)
+from src.tool_system.context import ToolContext # noqa: E402
+from src.tool_system.tools import SkillTool # noqa: E402
+
+
+FIXTURES_ROOT = REPO_ROOT / "tests" / "fixtures" / "skills"
+FIXTURE_NAMES = ("commit-helper", "frontend", "lint-py")
+PREVIEW_CHARS = 200
+
+
+def _build_workspace(tmp: Path) -> Path:
+ """Construct a workspace whose ``.claude/skills/`` mirrors the
+ fixture catalogue. Returns the workspace root."""
+ project = tmp / "proj"
+ skills_root = project / ".claude" / "skills"
+ skills_root.mkdir(parents=True)
+ for name in FIXTURE_NAMES:
+ shutil.copytree(FIXTURES_ROOT / name, skills_root / name)
+ return project
+
+
+def _isolate_env(tmp: Path) -> None:
+ """Strip every env knob that would inject non-fixture skill dirs."""
+ fake_home = tmp / "home"
+ fake_home.mkdir()
+ os.environ["HOME"] = str(fake_home)
+ os.environ["CLAUDE_MANAGED_CONFIG_DIR"] = str(tmp / "managed")
+ for var in (
+ "CLAUDE_CONFIG_DIR",
+ "CLAWCODEX_SKILLS_DIR",
+ "CLAUDE_SKILLS_DIR",
+ "CLAWCODEX_MANAGED_SKILLS_DIR",
+ "CLAUDE_CODE_BARE_MODE",
+ "CLAUDE_CODE_DISABLE_POLICY_SKILLS",
+ "CLAUDE_CODE_ADDITIONAL_DIRECTORIES",
+ ):
+ os.environ.pop(var, None)
+
+
+def _print_section(title: str) -> None:
+ bar = "=" * 72
+ print(f"\n{bar}\n{title}\n{bar}")
+
+
+def _print_listing(project: Path) -> None:
+ skills = get_all_skills(project_root=project)
+ _print_section(f"Available skills ({len(skills)}, unconditional only)")
+ print(f"{'name':<30} {'source':<10} description")
+ print("-" * 72)
+ for s in sorted(skills, key=lambda x: x.name):
+ desc = s.description or ""
+ if len(desc) > 60:
+ desc = desc[:57] + "..."
+ print(f"{s.name:<30} {s.loaded_from:<10} {desc}")
+
+
+def _print_invocation(
+ skill_name: str, args: str, ctx: ToolContext
+) -> None:
+ result = SkillTool.call({"skill": skill_name, "args": args}, ctx)
+ out = result.output
+ head = f"--- /{skill_name}"
+ if args:
+ head += f' "{args}"'
+ head += " ---"
+ print(f"\n{head}")
+ if not out.get("success"):
+ print(f" ERROR: {out}")
+ return
+ prompt = out["prompt"]
+ preview = prompt if len(prompt) <= PREVIEW_CHARS else (
+ prompt[:PREVIEW_CHARS] + f"\n... [truncated, total {len(prompt)} chars]"
+ )
+ for line in preview.splitlines():
+ print(f" {line}")
+ if out.get("loadedFrom"):
+ print(f" [loadedFrom={out['loadedFrom']}, allowedTools={out.get('allowedTools')}]")
+
+
+def main() -> int:
+ with tempfile.TemporaryDirectory() as tmp_str:
+ tmp = Path(tmp_str)
+ _isolate_env(tmp)
+
+ # Reset every cache + register the bundled catalogue.
+ clear_skill_caches()
+ clear_dynamic_skills()
+ clear_skill_registry()
+ clear_bundled_skills()
+ init_bundled_skills()
+
+ project = _build_workspace(tmp)
+ ctx = ToolContext(workspace_root=project)
+ ctx.session_id = "S-smoke-001"
+
+ _print_listing(project)
+
+ _print_section("Invocations (first 200 chars of each rendered prompt)")
+ # commit-helper — substitutions + args
+ _print_invocation("commit-helper", "feat", ctx)
+
+ # frontend:add-component — namespaced + named arg
+ _print_invocation("frontend:add-component", "Button", ctx)
+
+ # simplify — bundled skill (no base-dir header)
+ _print_invocation("simplify", "focus on caching", ctx)
+
+ # lint-py — conditional skill: activate it by touching a
+ # matching path, then invoke. As of bug fix #14, activated
+ # conditional skills flow through `get_all_skills` →
+ # `_skill_registry` automatically, so SkillTool resolves them
+ # naturally on the next call (no manual registry promotion).
+ _print_section("Activating conditional `lint-py` for src/foo.py")
+ py_path = project / "src" / "foo.py"
+ py_path.parent.mkdir(parents=True, exist_ok=True)
+ py_path.write_text("# placeholder")
+ activated = activate_conditional_skills_for_paths(
+ [str(py_path)], str(project)
+ )
+ print(f" activated: {activated}")
+ _print_invocation("lint-py", "", ctx)
+
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/src/skills/__init__.py b/src/skills/__init__.py
index 40cdb24f4..50b94fa85 100644
--- a/src/skills/__init__.py
+++ b/src/skills/__init__.py
@@ -1,4 +1,5 @@
from .argument_substitution import parse_arguments, substitute_arguments
+from .bundled import init_bundled_skills
from .bundled_skills import (
BundledSkillDefinition,
SkillValidationError,
@@ -17,17 +18,31 @@
add_skill_directories,
clear_dynamic_skills,
clear_skill_caches,
+ clear_skill_registry,
create_skill_command,
discover_skill_dirs_for_paths,
+ get_all_skills,
get_conditional_skill_count,
get_dynamic_skills,
+ get_registered_skill,
get_skill_dir_commands,
get_skills_path,
+ load_skills_from_dir,
load_skills_from_skills_dir,
parse_skill_frontmatter_fields,
)
from .mcp_skill_builders import get_mcp_skill_builders, register_mcp_skill_builders
from .model import PromptSkill, Skill
+from .runtime_substitution import (
+ find_shell_blocks,
+ format_shell_error,
+ format_shell_output,
+ has_shell_blocks,
+ prepend_base_dir_header,
+ render_skill_prompt,
+ substitute_session_id,
+ substitute_skill_dir,
+)
__all__ = [
"Skill",
@@ -50,6 +65,7 @@
"get_skill_dir_commands",
"get_skills_path",
"load_skills_from_skills_dir",
+ "load_skills_from_dir",
"discover_skill_dirs_for_paths",
"add_skill_directories",
"activate_conditional_skills_for_paths",
@@ -57,6 +73,18 @@
"get_conditional_skill_count",
"clear_skill_caches",
"clear_dynamic_skills",
+ "clear_skill_registry",
+ "get_all_skills",
+ "get_registered_skill",
"register_mcp_skill_builders",
"get_mcp_skill_builders",
+ "render_skill_prompt",
+ "prepend_base_dir_header",
+ "substitute_skill_dir",
+ "substitute_session_id",
+ "find_shell_blocks",
+ "has_shell_blocks",
+ "format_shell_output",
+ "format_shell_error",
+ "init_bundled_skills",
]
diff --git a/src/skills/bundled/__init__.py b/src/skills/bundled/__init__.py
new file mode 100644
index 000000000..8296e7c16
--- /dev/null
+++ b/src/skills/bundled/__init__.py
@@ -0,0 +1,73 @@
+"""Bundled-skill catalogue + init orchestrator.
+
+Mirrors the TS pattern in ``typescript/src/skills/bundled/index.ts``:
+each individual skill module exposes a ``register_*_skill()`` function
+that calls ``register_bundled_skill(BundledSkillDefinition(...))``;
+``init_bundled_skills()`` calls them in order at startup.
+
+``init_bundled_skills`` is idempotent — calling it twice does not
+re-register skills (the second call is a no-op). The bundled-skill
+registry consults a sentinel set so a fresh ``clear_bundled_skills()``
+forces re-init on the next call.
+"""
+
+from __future__ import annotations
+
+import logging
+
+from .debug import register_debug_skill
+from .loop import register_loop_skill
+from .simplify import register_simplify_skill
+from .stuck import register_stuck_skill
+from .verify_content import register_verify_content_skill
+
+logger = logging.getLogger(__name__)
+
+
+# Tracks whether ``init_bundled_skills`` has already populated the
+# registry. Reset by ``clear_bundled_skills`` (via the hook below) so
+# tests that wipe state can re-init cleanly.
+_INITIALIZED: bool = False
+
+
+def init_bundled_skills() -> None:
+ """Register every always-on bundled skill exactly once.
+
+ Calls each ``register_*_skill()`` function in a fixed order. Skills
+ with feature gates check ``is_enabled`` lazily at lookup time
+ (matches TS) — they're registered unconditionally so they show up
+ in the catalogue when the gate flips.
+
+ Idempotent: subsequent calls are no-ops. Use the
+ ``clear_bundled_skills()`` hook in ``src.skills.bundled_skills`` to
+ reset state in tests.
+ """
+ global _INITIALIZED
+ if _INITIALIZED:
+ return
+ register_simplify_skill()
+ register_debug_skill()
+ register_loop_skill()
+ register_stuck_skill()
+ register_verify_content_skill()
+ _INITIALIZED = True
+ logger.debug("bundled skills initialized")
+
+
+def reset_bundled_skills_init_flag() -> None:
+ """Drop the idempotency flag so the next ``init_bundled_skills``
+ call re-runs. Wired to ``clear_bundled_skills`` so test fixtures
+ that reset the registry also reset the flag."""
+ global _INITIALIZED
+ _INITIALIZED = False
+
+
+__all__ = [
+ "init_bundled_skills",
+ "reset_bundled_skills_init_flag",
+ "register_simplify_skill",
+ "register_debug_skill",
+ "register_loop_skill",
+ "register_stuck_skill",
+ "register_verify_content_skill",
+]
diff --git a/src/skills/bundled/debug.py b/src/skills/bundled/debug.py
new file mode 100644
index 000000000..e16ae3663
--- /dev/null
+++ b/src/skills/bundled/debug.py
@@ -0,0 +1,187 @@
+"""Bundled ``/debug`` skill — minimal port of ``bundled/debug.ts``.
+
+Reads the tail of a session debug log (last ~64 KB / 20 lines), surfaces
+the log path, settings paths, and the user's issue description so the
+model can diagnose. Scoped to ``Read``/``Grep``/``Glob`` tools.
+
+The full TS skill calls ``enableDebugLogging()`` to opt-in to logging
+mid-session. Python has no equivalent helper today, so the "just
+enabled" call is a no-op (TODO when the Python runtime grows one). The
+prompt still surfaces the resolved log path so the user can tail it
+out-of-band.
+"""
+
+from __future__ import annotations
+
+import logging
+import os
+from pathlib import Path
+from typing import Optional
+
+from ..bundled_skills import BundledSkillDefinition, register_bundled_skill
+
+logger = logging.getLogger(__name__)
+
+
+_DEFAULT_DEBUG_LINES_READ = 20
+_TAIL_READ_BYTES = 64 * 1024
+
+
+def _get_debug_log_path() -> str:
+ """Resolve the conventional session debug log path.
+
+ Honors ``CLAUDE_CODE_DEBUG_LOG_PATH`` if set; otherwise falls back to
+ ``$CLAUDE_CONFIG_DIR/debug.log`` (or ``~/.claude/debug.log``). The
+ file may not exist — callers handle that case.
+ """
+ env = os.environ.get("CLAUDE_CODE_DEBUG_LOG_PATH")
+ if env:
+ return str(Path(env).expanduser())
+ config_dir = os.environ.get("CLAUDE_CONFIG_DIR")
+ base = Path(config_dir).expanduser() if config_dir else Path.home() / ".claude"
+ return str(base / "debug.log")
+
+
+def _enable_debug_logging() -> bool:
+ """Stub for TS ``enableDebugLogging``.
+
+ Returns True if logging was already on, False otherwise. The Python
+ runtime has no mid-session logging toggle yet — the function is a
+ no-op that always reports "was already on" (so the prompt skips the
+ "just enabled" hint). TODO: wire to a real logging toggle when one
+ lands.
+ """
+ return True
+
+
+def _format_file_size(num_bytes: int) -> str:
+ units = ["B", "KB", "MB", "GB"]
+ n = float(num_bytes)
+ for unit in units:
+ if n < 1024 or unit == units[-1]:
+ return f"{n:.1f} {unit}" if unit != "B" else f"{int(n)} B"
+ n /= 1024
+ return f"{num_bytes} B"
+
+
+def _read_log_tail(log_path: str) -> str:
+ """Return a markdown-rendered tail block, or a graceful "no log" hint.
+
+ Mirrors TS: stat → seek to ``size - TAIL_READ_BYTES`` → read → keep
+ last ``DEFAULT_DEBUG_LINES_READ`` lines. Returns a short hint when
+ the file is missing or unreadable.
+ """
+ try:
+ p = Path(log_path)
+ size = p.stat().st_size
+ read_size = min(size, _TAIL_READ_BYTES)
+ start_offset = max(0, size - read_size)
+ with p.open("rb") as fh:
+ fh.seek(start_offset)
+ data = fh.read(read_size)
+ text = data.decode("utf-8", errors="replace")
+ tail_lines = text.split("\n")[-_DEFAULT_DEBUG_LINES_READ:]
+ tail = "\n".join(tail_lines)
+ return (
+ f"Log size: {_format_file_size(size)}\n\n"
+ f"### Last {_DEFAULT_DEBUG_LINES_READ} lines\n\n"
+ f"```\n{tail}\n```"
+ )
+ except FileNotFoundError:
+ return "No debug log exists yet — logging was just enabled."
+ except OSError as exc:
+ return (
+ f"Failed to read last {_DEFAULT_DEBUG_LINES_READ} lines of "
+ f"debug log: {exc}"
+ )
+
+
+def _settings_path_hint(scope: str) -> str:
+ """Return a stable hint for the settings file at ``scope``.
+
+ The Python codebase doesn't yet expose a unified
+ ``getSettingsFilePathForSource`` equivalent; we render conventional
+ paths so the user can locate them. Mirrors TS' three scopes.
+ """
+ if scope == "userSettings":
+ config_dir = os.environ.get("CLAUDE_CONFIG_DIR")
+ base = Path(config_dir).expanduser() if config_dir else Path.home() / ".claude"
+ return str(base / "settings.json")
+ if scope == "projectSettings":
+ return ".claude/settings.json"
+ if scope == "localSettings":
+ return ".claude/settings.local.json"
+ return "(unknown scope)"
+
+
+def _build_debug_prompt(args: str) -> str:
+ was_already_logging = _enable_debug_logging()
+ debug_log_path = _get_debug_log_path()
+ log_info = _read_log_tail(debug_log_path)
+
+ just_enabled_section = (
+ ""
+ if was_already_logging
+ else (
+ "\n## Debug Logging Just Enabled\n\n"
+ "Debug logging was OFF for this session until now. Nothing prior to this /debug invocation was captured.\n\n"
+ f"Tell the user that debug logging is now active at `{debug_log_path}`, ask them to reproduce the issue, then re-read the log. If they can't reproduce, they can also restart with `claude --debug` to capture logs from startup.\n"
+ )
+ )
+
+ issue_description = args or (
+ "The user did not describe a specific issue. Read the debug log and "
+ "summarize any errors, warnings, or notable issues."
+ )
+
+ return (
+ "# Debug Skill\n"
+ "\n"
+ "Help the user debug an issue they're encountering in this current Claude Code session.\n"
+ f"{just_enabled_section}\n"
+ "## Session Debug Log\n"
+ "\n"
+ f"The debug log for the current session is at: `{debug_log_path}`\n"
+ "\n"
+ f"{log_info}\n"
+ "\n"
+ "For additional context, grep for [ERROR] and [WARN] lines across the full file.\n"
+ "\n"
+ "## Issue Description\n"
+ "\n"
+ f"{issue_description}\n"
+ "\n"
+ "## Settings\n"
+ "\n"
+ f"Remember that settings are in:\n"
+ f"* user - {_settings_path_hint('userSettings')}\n"
+ f"* project - {_settings_path_hint('projectSettings')}\n"
+ f"* local - {_settings_path_hint('localSettings')}\n"
+ "\n"
+ "## Instructions\n"
+ "\n"
+ "1. Review the user's issue description\n"
+ f"2. The last {_DEFAULT_DEBUG_LINES_READ} lines show the debug file format. Look for [ERROR] and [WARN] entries, stack traces, and failure patterns across the file\n"
+ "3. Consider launching the claude-code-guide subagent to understand the relevant Claude Code features\n"
+ "4. Explain what you found in plain language\n"
+ "5. Suggest concrete fixes or next steps\n"
+ )
+
+
+def register_debug_skill() -> None:
+ register_bundled_skill(
+ BundledSkillDefinition(
+ name="debug",
+ description=(
+ "Enable debug logging for this session and help diagnose issues"
+ ),
+ allowed_tools=["Read", "Grep", "Glob"],
+ argument_hint="[issue description]",
+ # disable_model_invocation: matches TS — user must opt in
+ # via `/debug`; otherwise the description doesn't burn
+ # context budget on every model turn.
+ disable_model_invocation=True,
+ user_invocable=True,
+ get_prompt_for_command=_build_debug_prompt,
+ )
+ )
diff --git a/src/skills/bundled/loop.py b/src/skills/bundled/loop.py
new file mode 100644
index 000000000..cf56dff28
--- /dev/null
+++ b/src/skills/bundled/loop.py
@@ -0,0 +1,272 @@
+"""Bundled ``/loop`` skill — port of ``bundled/loop.ts``.
+
+Pure argument-parsing logic + two prompt builders. Three modes:
+``fixed-prompt``, ``fixed-maintenance``, ``dynamic-prompt``,
+``dynamic-maintenance``. Routes to ``buildFixedPrompt`` or
+``buildDynamicPrompt`` based on whether the user supplied an interval.
+
+The TS feature gate (``isKairosCronEnabled``) has no Python equivalent
+yet; the ``is_enabled`` callback returns True so the skill is always
+visible. TODO: wire to a real Python feature gate when one lands.
+"""
+
+from __future__ import annotations
+
+import re
+from dataclasses import dataclass
+from typing import Optional
+
+from ..bundled_skills import BundledSkillDefinition, register_bundled_skill
+
+
+# Cron-tool name constants. The Python tool registry exposes
+# ``CronCreate`` / ``CronDelete`` (see ``src.tool_system.tools.cron``)
+# which is the same naming the prompt body references.
+_CRON_CREATE_TOOL_NAME = "CronCreate"
+_CRON_DELETE_TOOL_NAME = "CronDelete"
+
+# Recurring-job auto-expiration window (mirrors TS DEFAULT_MAX_AGE_DAYS).
+_DEFAULT_MAX_AGE_DAYS = 7
+
+_DYNAMIC_MIN_DELAY = "1 minute"
+_DYNAMIC_MAX_DELAY = "1 hour"
+
+
+_MAINTENANCE_PROMPT = """Scheduled maintenance loop iteration.
+
+If .claude/loop.md exists, read it and follow it.
+Otherwise, if ~/.claude/loop.md exists, read it and follow it.
+Otherwise:
+- continue any unfinished work from the conversation
+- tend to the current branch's pull request: review comments, failed CI runs, merge conflicts
+- run cleanup passes such as bug hunts or simplification when nothing else is pending
+
+Do not start new initiatives outside that scope.
+Irreversible actions such as pushing or deleting only proceed when they continue something the transcript already authorized."""
+
+
+@dataclass(frozen=True)
+class ParsedLoopArgs:
+ mode: str # "dynamic-prompt" | "dynamic-maintenance" | "fixed-prompt" | "fixed-maintenance"
+ interval: Optional[str] = None
+ prompt: Optional[str] = None
+
+
+# ----------------------------------------------------------------------
+# Argument parser (verbatim port of TS parseLoopArgs et al.)
+# ----------------------------------------------------------------------
+
+
+def _normalize_interval_unit(raw_unit: str) -> str | None:
+ unit = raw_unit.lower()
+ if unit in {"s", "sec", "secs", "second", "seconds"}:
+ return "s"
+ if unit in {"m", "min", "mins", "minute", "minutes"}:
+ return "m"
+ if unit in {"h", "hr", "hrs", "hour", "hours"}:
+ return "h"
+ if unit in {"d", "day", "days"}:
+ return "d"
+ return None
+
+
+def _parse_interval_token(token: str) -> str | None:
+ """Match ```` (e.g. ``5m``, ``2hours``); return canonical form."""
+ m = re.match(r"^(\d+)\s*([a-zA-Z]+)$", token.strip())
+ if not m:
+ return None
+ try:
+ value = int(m.group(1), 10)
+ except ValueError:
+ return None
+ if value < 1:
+ return None
+ unit = _normalize_interval_unit(m.group(2))
+ if unit is None:
+ return None
+ return f"{value}{unit}"
+
+
+def _parse_trailing_every_clause(text: str) -> tuple[str, str] | None:
+ """Match ``... every `` and return ``(prompt, interval)``."""
+ m = re.match(r"^(.*?)(?:\s+every\s+)(\d+)\s*([a-zA-Z]+)\s*$", text, re.IGNORECASE)
+ if not m:
+ return None
+ interval = _parse_interval_token(f"{m.group(2)}{m.group(3)}")
+ if not interval:
+ return None
+ return m.group(1).strip(), interval
+
+
+def parse_loop_args(args: str) -> ParsedLoopArgs:
+ """Port of TS ``parseLoopArgs``.
+
+ Routing:
+ - empty args → ``dynamic-maintenance``
+ - bare interval (``5m``) → ``fixed-maintenance``
+ - `` `` → ``fixed-prompt``
+ - `` every `` → ``fixed-prompt``
+ - anything else → ``dynamic-prompt``
+ """
+ trimmed = args.strip()
+ if not trimmed:
+ return ParsedLoopArgs(mode="dynamic-maintenance")
+
+ bare = _parse_interval_token(trimmed)
+ if bare:
+ return ParsedLoopArgs(mode="fixed-maintenance", interval=bare)
+
+ tokens = trimmed.split()
+ first_token, rest_tokens = tokens[0], tokens[1:]
+ leading = _parse_interval_token(first_token)
+ if leading:
+ prompt = " ".join(rest_tokens).strip()
+ if not prompt:
+ return ParsedLoopArgs(mode="fixed-maintenance", interval=leading)
+ return ParsedLoopArgs(mode="fixed-prompt", interval=leading, prompt=prompt)
+
+ trailing = _parse_trailing_every_clause(trimmed)
+ if trailing:
+ prompt, interval = trailing
+ if not prompt:
+ return ParsedLoopArgs(mode="fixed-maintenance", interval=interval)
+ return ParsedLoopArgs(mode="fixed-prompt", interval=interval, prompt=prompt)
+
+ return ParsedLoopArgs(mode="dynamic-prompt", prompt=trimmed)
+
+
+# ----------------------------------------------------------------------
+# Prompt builders (verbatim ports of buildFixedPrompt / buildDynamicPrompt)
+# ----------------------------------------------------------------------
+
+
+def _build_fixed_prompt(parsed: ParsedLoopArgs) -> str:
+ if parsed.prompt:
+ target_instructions = (
+ "Use this prompt verbatim for both the immediate run and the recurring scheduled task:\n"
+ "\n--- BEGIN PROMPT ---\n"
+ f"{parsed.prompt}\n"
+ "--- END PROMPT ---\n"
+ )
+ else:
+ target_instructions = (
+ "This is a maintenance loop with no explicit prompt.\n"
+ "\nFor the recurring scheduled task, use this exact maintenance prompt body:\n"
+ "\n--- BEGIN MAINTENANCE PROMPT ---\n"
+ f"{_MAINTENANCE_PROMPT}\n"
+ "--- END MAINTENANCE PROMPT ---\n"
+ )
+
+ return (
+ "# /loop — fixed recurring interval\n"
+ "\n"
+ "The user invoked /loop with a fixed interval.\n"
+ "\n"
+ f"Requested interval: {parsed.interval}\n"
+ "\n"
+ f"{target_instructions}\n"
+ "## Instructions\n"
+ "\n"
+ "1. Convert the requested interval to a recurring cron expression.\n"
+ " - Supported suffixes: s, m, h, d.\n"
+ " - Seconds must be rounded up to the nearest minute because cron has minute granularity.\n"
+ " - If the requested interval does not map cleanly to cron cadence, choose the nearest clean recurring interval and tell the user what you picked.\n"
+ f"2. Call {_CRON_CREATE_TOOL_NAME} with:\n"
+ " - the recurring cron expression\n"
+ " - the effective prompt body above\n"
+ " - recurring: true\n"
+ " - durable: false\n"
+ f"3. Briefly confirm what was scheduled, the cron expression, the human cadence, that recurring tasks auto-expire after {_DEFAULT_MAX_AGE_DAYS} days, and that the user can cancel sooner with {_CRON_DELETE_TOOL_NAME} using the returned job ID.\n"
+ "4. Immediately execute the effective prompt now — do not wait for the first cron fire.\n"
+ " - If the effective prompt starts with a slash command, invoke it via the Skill tool.\n"
+ " - Otherwise, act on it directly.\n"
+ )
+
+
+def _build_dynamic_prompt(parsed: ParsedLoopArgs) -> str:
+ if parsed.prompt:
+ effective_instructions = (
+ "Use this prompt verbatim as the effective prompt for this iteration:\n"
+ "\n--- BEGIN PROMPT ---\n"
+ f"{parsed.prompt}\n"
+ "--- END PROMPT ---\n"
+ )
+ else:
+ effective_instructions = (
+ "This is a maintenance loop with no explicit prompt.\n"
+ "\nDetermine the effective prompt in this order:\n"
+ "1. If .claude/loop.md exists, read it and use it.\n"
+ "2. Otherwise, if ~/.claude/loop.md exists, read it and use it.\n"
+ "3. Otherwise, use this built-in maintenance prompt:\n"
+ "\n--- BEGIN MAINTENANCE PROMPT ---\n"
+ f"{_MAINTENANCE_PROMPT}\n"
+ "--- END MAINTENANCE PROMPT ---\n"
+ )
+
+ reschedule_prompt = f"/loop {parsed.prompt}" if parsed.prompt else "/loop"
+
+ return (
+ "# /loop — dynamic rescheduling\n"
+ "\n"
+ "The user invoked /loop without a fixed interval.\n"
+ "\n"
+ f"{effective_instructions}\n"
+ "## Instructions\n"
+ "\n"
+ "1. Execute the effective prompt now.\n"
+ " - If it starts with a slash command, invoke it via the Skill tool.\n"
+ " - Otherwise, act on it directly.\n"
+ f"2. After the work finishes, choose the next delay dynamically between {_DYNAMIC_MIN_DELAY} and {_DYNAMIC_MAX_DELAY}.\n"
+ " - Use shorter delays while active work is progressing or likely to change soon.\n"
+ " - Use longer delays when the situation is quiet or stable.\n"
+ "3. Briefly tell the user the chosen delay and the reason.\n"
+ f"4. Schedule exactly one session-only follow-up run with {_CRON_CREATE_TOOL_NAME}.\n"
+ " - Use recurring: false.\n"
+ " - Use durable: false.\n"
+ " - Pin the cron expression to a specific future local-time minute that matches the chosen delay.\n"
+ " - Set the scheduled prompt to this exact text so the next iteration stays in dynamic mode:\n"
+ "\n--- BEGIN SCHEDULED PROMPT ---\n"
+ f"{reschedule_prompt}\n"
+ "--- END SCHEDULED PROMPT ---\n"
+ "\n"
+ "5. Confirm the next run time and the returned job ID.\n"
+ "6. Do not create a recurring cron for this mode.\n"
+ )
+
+
+def _build_loop_prompt(args: str) -> str:
+ parsed = parse_loop_args(args)
+ if parsed.mode in ("fixed-prompt", "fixed-maintenance"):
+ return _build_fixed_prompt(parsed)
+ return _build_dynamic_prompt(parsed)
+
+
+def _is_loop_enabled() -> bool:
+ """Stub for TS' ``isKairosCronEnabled``.
+
+ Returns True so /loop is always discoverable in the catalogue.
+ TODO: wire to a real Python feature gate (e.g., a settings flag or
+ env-var driven check) when the runtime gains one.
+ """
+ return True
+
+
+def register_loop_skill() -> None:
+ register_bundled_skill(
+ BundledSkillDefinition(
+ name="loop",
+ description=(
+ "Run a prompt on a fixed interval or dynamically reschedule "
+ "it, including bare maintenance-mode loops."
+ ),
+ when_to_use=(
+ "When the user wants to poll for status, babysit a workflow, "
+ "run recurring maintenance, or keep re-running a prompt "
+ "within the current session."
+ ),
+ argument_hint="[interval] [prompt]",
+ user_invocable=True,
+ is_enabled=_is_loop_enabled,
+ get_prompt_for_command=_build_loop_prompt,
+ )
+ )
diff --git a/src/skills/bundled/simplify.py b/src/skills/bundled/simplify.py
new file mode 100644
index 000000000..5c326149d
--- /dev/null
+++ b/src/skills/bundled/simplify.py
@@ -0,0 +1,88 @@
+"""Bundled ``/simplify`` skill — verbatim port of ``bundled/simplify.ts``.
+
+Three-agent code review (reuse / quality / efficiency) over recently
+changed files, then fix-and-summarize.
+"""
+
+from __future__ import annotations
+
+from ..bundled_skills import BundledSkillDefinition, register_bundled_skill
+
+
+# AGENT_TOOL_NAME in TS is "Agent". The Python tool registry uses the
+# same name (see ``src.tool_system.tools.agent``) so the literal here
+# matches what the model invokes.
+_AGENT_TOOL_NAME = "Agent"
+
+
+SIMPLIFY_PROMPT = f"""# Simplify: Code Review and Cleanup
+
+Review all changed files for reuse, quality, and efficiency. Fix any issues found.
+
+## Phase 1: Identify Changes
+
+Run `git diff` (or `git diff HEAD` if there are staged changes) to see what changed. If there are no git changes, review the most recently modified files that the user mentioned or that you edited earlier in this conversation.
+
+## Phase 2: Launch Three Review Agents in Parallel
+
+Use the {_AGENT_TOOL_NAME} tool to launch all three agents concurrently in a single message. Pass each agent the full diff so it has the complete context.
+
+### Agent 1: Code Reuse Review
+
+For each change:
+
+1. **Search for existing utilities and helpers** that could replace newly written code. Look for similar patterns elsewhere in the codebase — common locations are utility directories, shared modules, and files adjacent to the changed ones.
+2. **Flag any new function that duplicates existing functionality.** Suggest the existing function to use instead.
+3. **Flag any inline logic that could use an existing utility** — hand-rolled string manipulation, manual path handling, custom environment checks, ad-hoc type guards, and similar patterns are common candidates.
+
+### Agent 2: Code Quality Review
+
+Review the same changes for hacky patterns:
+
+1. **Redundant state**: state that duplicates existing state, cached values that could be derived, observers/effects that could be direct calls
+2. **Parameter sprawl**: adding new parameters to a function instead of generalizing or restructuring existing ones
+3. **Copy-paste with slight variation**: near-duplicate code blocks that should be unified with a shared abstraction
+4. **Leaky abstractions**: exposing internal details that should be encapsulated, or breaking existing abstraction boundaries
+5. **Stringly-typed code**: using raw strings where constants, enums (string unions), or branded types already exist in the codebase
+6. **Unnecessary JSX nesting**: wrapper Boxes/elements that add no layout value — check if inner component props (flexShrink, alignItems, etc.) already provide the needed behavior
+7. **Unnecessary comments**: comments explaining WHAT the code does (well-named identifiers already do that), narrating the change, or referencing the task/caller — delete; keep only non-obvious WHY (hidden constraints, subtle invariants, workarounds)
+
+### Agent 3: Efficiency Review
+
+Review the same changes for efficiency:
+
+1. **Unnecessary work**: redundant computations, repeated file reads, duplicate network/API calls, N+1 patterns
+2. **Missed concurrency**: independent operations run sequentially when they could run in parallel
+3. **Hot-path bloat**: new blocking work added to startup or per-request/per-render hot paths
+4. **Recurring no-op updates**: state/store updates inside polling loops, intervals, or event handlers that fire unconditionally — add a change-detection guard so downstream consumers aren't notified when nothing changed. Also: if a wrapper function takes an updater/reducer callback, verify it honors same-reference returns (or whatever the "no change" signal is) — otherwise callers' early-return no-ops are silently defeated
+5. **Unnecessary existence checks**: pre-checking file/resource existence before operating (TOCTOU anti-pattern) — operate directly and handle the error
+6. **Memory**: unbounded data structures, missing cleanup, event listener leaks
+7. **Overly broad operations**: reading entire files when only a portion is needed, loading all items when filtering for one
+
+## Phase 3: Fix Issues
+
+Wait for all three agents to complete. Aggregate their findings and fix each issue directly. If a finding is a false positive or not worth addressing, note it and move on — do not argue with the finding, just skip it.
+
+When done, briefly summarize what was fixed (or confirm the code was already clean).
+"""
+
+
+def _build_simplify_prompt(args: str) -> str:
+ prompt = SIMPLIFY_PROMPT
+ if args:
+ prompt += f"\n\n## Additional Focus\n\n{args}"
+ return prompt
+
+
+def register_simplify_skill() -> None:
+ register_bundled_skill(
+ BundledSkillDefinition(
+ name="simplify",
+ description=(
+ "Review changed code for reuse, quality, and efficiency, "
+ "then fix any issues found."
+ ),
+ user_invocable=True,
+ get_prompt_for_command=_build_simplify_prompt,
+ )
+ )
diff --git a/src/skills/bundled/stuck.py b/src/skills/bundled/stuck.py
new file mode 100644
index 000000000..dbc6a8330
--- /dev/null
+++ b/src/skills/bundled/stuck.py
@@ -0,0 +1,49 @@
+"""Bundled ``/stuck`` skill — minimal "you're stuck, change tactics" prompt.
+
+The TS module (``bundled/stuck.ts``) is currently a no-op stub. The
+Python port keeps a small but useful prompt so the skill is discoverable
+in the catalogue and gives the user something actionable when invoked.
+"""
+
+from __future__ import annotations
+
+from ..bundled_skills import BundledSkillDefinition, register_bundled_skill
+
+
+_STUCK_PROMPT = """# /stuck — Reset and Re-Approach
+
+You appear to be stuck. Pause the current line of work and reassess.
+
+## Step 1: Diagnose
+- Summarize what you were trying to do, what you tried, and what failed.
+- Identify the *first* assumption that turned out to be wrong.
+
+## Step 2: Pivot
+- Pick a different approach. Examples: read files you haven't read, run a different diagnostic command, ask the user a clarifying question, or search for prior art in the codebase.
+- Avoid retrying the same thing with minor variations.
+
+## Step 3: Report
+- Tell the user concisely: what was failing, what new approach you're taking, and why.
+
+If the user supplied additional context with /stuck, treat it as guidance for the pivot.
+"""
+
+
+def _build_stuck_prompt(args: str) -> str:
+ if not args:
+ return _STUCK_PROMPT
+ return f"{_STUCK_PROMPT}\n\n## User Context\n\n{args}\n"
+
+
+def register_stuck_skill() -> None:
+ register_bundled_skill(
+ BundledSkillDefinition(
+ name="stuck",
+ description=(
+ "Reset when stuck: diagnose what failed, pivot to a different "
+ "approach, and report back."
+ ),
+ user_invocable=True,
+ get_prompt_for_command=_build_stuck_prompt,
+ )
+ )
diff --git a/src/skills/bundled/verify_content.py b/src/skills/bundled/verify_content.py
new file mode 100644
index 000000000..928b52459
--- /dev/null
+++ b/src/skills/bundled/verify_content.py
@@ -0,0 +1,57 @@
+"""Bundled ``/verify-content`` skill — sanity-check the user's intent
+against the most recent edits.
+
+The TS module (``bundled/verifyContent.ts``) is build-time inlined .md
+content; the Python port keeps a concise actionable prompt here so the
+skill is discoverable in the catalogue without requiring extra .md
+asset shipping.
+"""
+
+from __future__ import annotations
+
+from ..bundled_skills import BundledSkillDefinition, register_bundled_skill
+
+
+_VERIFY_PROMPT = """# /verify-content — Verify Recent Edits Match Intent
+
+Review the most recent changes (or the files the user names) and verify
+they match what was actually requested.
+
+## Step 1: Recall the request
+- State, in your own words, what the user asked for.
+- If multiple iterations occurred, focus on the latest stated goal.
+
+## Step 2: Read the changes
+- Read each touched file fresh. Do not rely on what you remember writing.
+- For each diff hunk, ask: does this implement (or move toward) the goal?
+
+## Step 3: Surface mismatches
+- Note any change that doesn't serve the stated goal — accidental edits, half-completed transformations, leftover debug code, comments that no longer match the code.
+- Note any part of the request that is *not* yet implemented.
+
+## Step 4: Report
+- Lead with a one-line verdict: matches / partial / mismatch.
+- Then list the specific file/line locations of any mismatches and what should be done about each.
+
+If the user supplied additional context with /verify-content, treat it as the authoritative description of intent.
+"""
+
+
+def _build_verify_prompt(args: str) -> str:
+ if not args:
+ return _VERIFY_PROMPT
+ return f"{_VERIFY_PROMPT}\n\n## User-Supplied Intent\n\n{args}\n"
+
+
+def register_verify_content_skill() -> None:
+ register_bundled_skill(
+ BundledSkillDefinition(
+ name="verify-content",
+ description=(
+ "Verify the most recent edits actually match the user's "
+ "stated intent; surface mismatches and gaps."
+ ),
+ user_invocable=True,
+ get_prompt_for_command=_build_verify_prompt,
+ )
+ )
diff --git a/src/skills/bundled_skills.py b/src/skills/bundled_skills.py
index 160d73af0..fa43d2425 100644
--- a/src/skills/bundled_skills.py
+++ b/src/skills/bundled_skills.py
@@ -41,6 +41,35 @@ class BundledSkillDefinition:
_bundled_skills: list[Skill] = []
+# Lazy-init guard — populated on first read so callers don't need to
+# explicitly invoke ``init_bundled_skills`` at runtime startup. Tests
+# that wipe state via ``clear_bundled_skills`` also reset this flag,
+# leaving them in full control.
+_LAZY_INITIALIZED: bool = False
+
+
+def _lazy_init() -> None:
+ """Run ``init_bundled_skills`` once on first registry consumer.
+
+ Imported lazily to break the import cycle (``bundled.*`` modules
+ import from this file). The first call seeds the registry; later
+ calls are no-ops until ``clear_bundled_skills`` resets the flag.
+ """
+ global _LAZY_INITIALIZED
+ if _LAZY_INITIALIZED:
+ return
+ # Set BEFORE the import to avoid recursion if a register_*_skill
+ # callable somehow calls back into ``get_bundled_skills`` mid-init.
+ _LAZY_INITIALIZED = True
+ try:
+ from .bundled import init_bundled_skills
+ init_bundled_skills()
+ except Exception:
+ # Fail open: if a bundled-skill module is malformed at import
+ # time, surface the failure as "no bundled skills" rather than
+ # crashing every SkillTool.call.
+ logger.exception("failed to initialize bundled skills")
+
def validate_skill_definition(
definition: BundledSkillDefinition,
@@ -129,6 +158,14 @@ def get_prompt(args: str) -> str:
def register_bundled_skill(definition: BundledSkillDefinition) -> None:
+ """Append a bundled-skill definition to the in-process registry.
+
+ Suppresses the lazy-init seeding for the rest of this fixture
+ cycle: if a caller (test or app) is explicitly registering, they
+ own the catalogue contents. ``clear_bundled_skills`` re-arms the
+ lazy-init flag so the next caller-driven cycle starts fresh.
+ """
+ global _LAZY_INITIALIZED
skill = Skill(
name=definition.name,
description=definition.description,
@@ -148,13 +185,16 @@ def register_bundled_skill(definition: BundledSkillDefinition) -> None:
is_hidden=not definition.user_invocable,
)
_bundled_skills.append(skill)
+ _LAZY_INITIALIZED = True
def get_bundled_skills() -> list[Skill]:
+ _lazy_init()
return list(_bundled_skills)
def get_bundled_skill_by_name(name: str) -> Skill | None:
+ _lazy_init()
for skill in _bundled_skills:
if skill.name == name:
return skill
@@ -164,4 +204,22 @@ def get_bundled_skill_by_name(name: str) -> Skill | None:
def clear_bundled_skills() -> None:
+ """Wipe the registry and reset the lazy-init flag.
+
+ Resetting the flag ensures the next ``get_bundled_skills`` /
+ ``register_bundled_skill`` cycle re-seeds the catalogue, which is
+ what test fixtures want — otherwise a clear-then-read would return
+ an empty list and silently mask "did init_bundled_skills get
+ called?" bugs.
+ """
+ global _LAZY_INITIALIZED
_bundled_skills.clear()
+ _LAZY_INITIALIZED = False
+ try:
+ from .bundled import reset_bundled_skills_init_flag
+ reset_bundled_skills_init_flag()
+ except Exception:
+ # If the bundled package can't import, the lazy-init flag in
+ # this module is enough; the bundled-side flag is just a
+ # belt-and-braces idempotency check.
+ pass
diff --git a/src/skills/frontmatter.py b/src/skills/frontmatter.py
index 4b8ebb999..acded57d2 100644
--- a/src/skills/frontmatter.py
+++ b/src/skills/frontmatter.py
@@ -1,7 +1,48 @@
+"""YAML frontmatter parser for SKILL.md / agent / output-style files.
+
+Replaces the prior homegrown YAML-subset parser (top-level scalars + list
+forms only) with ``yaml.safe_load`` so nested structures used by the TS
+port — most importantly ``hooks:`` and ``shell:`` blocks — round-trip
+intact. The public ``parse_frontmatter`` API is preserved.
+
+Structure of an accepted file::
+
+ ---
+ description: Some skill
+ allowed-tools:
+ - Bash(git status:*)
+ - Read
+ hooks:
+ PostToolUse:
+ - matcher: Write
+ hooks:
+ - type: command
+ command: ./scripts/format.sh
+ ---
+
+
+Behavior:
+- A missing or malformed frontmatter block returns ``frontmatter={}``
+ with the original markdown intact as ``body``. We never raise; a bad
+ ``---`` fence should not block-load other skills.
+- Empty frontmatter (``---\\n---``) returns ``{}`` and the body that
+ follows.
+- ``yaml.safe_load`` is used (not ``load``) so arbitrary Python tag
+ resolution is disabled.
+"""
+
from __future__ import annotations
+import logging
from dataclasses import dataclass
-from typing import Any, Dict, List, Tuple
+from typing import Any, Dict
+
+try:
+ import yaml # type: ignore[import-untyped]
+except ImportError: # pragma: no cover — yaml is a hard runtime dep
+ yaml = None # type: ignore[assignment]
+
+logger = logging.getLogger(__name__)
@dataclass(frozen=True)
@@ -11,25 +52,19 @@ class FrontmatterParseResult:
def parse_frontmatter(markdown: str) -> FrontmatterParseResult:
+ """Split ``---``-fenced frontmatter from a markdown document.
+
+ Returns a ``FrontmatterParseResult`` with the parsed frontmatter
+ dict and the remaining body. Files without a leading ``---`` fence
+ return an empty dict and the original markdown as the body.
"""
- Minimal frontmatter parser supporting a safe subset of YAML-like syntax:
- - Top-level key: value pairs
- - Booleans: true/false (case-insensitive)
- - Numbers: integers
- - Lists via inline bracket form:
- key: [item1, item2]
- - Lists via hyphen form:
- key:
- - item1
- - item2
- - Lists via comma-separated shorthand:
- key: a, b, c
- Any unsupported structure falls back to a string.
- """
+ if not markdown:
+ return FrontmatterParseResult(frontmatter={}, body=markdown or "")
+
lines = markdown.splitlines()
if len(lines) < 3 or lines[0].strip() != "---":
return FrontmatterParseResult(frontmatter={}, body=markdown)
- # Find closing ---
+
end_idx = None
for i in range(1, len(lines)):
if lines[i].strip() == "---":
@@ -38,70 +73,36 @@ def parse_frontmatter(markdown: str) -> FrontmatterParseResult:
if end_idx is None:
return FrontmatterParseResult(frontmatter={}, body=markdown)
- fm_lines = lines[1:end_idx]
+ fm_raw = "\n".join(lines[1:end_idx])
body = "\n".join(lines[end_idx + 1 :])
- fm: Dict[str, Any] = {}
- i = 0
- while i < len(fm_lines):
- line = fm_lines[i]
- if not line.strip():
- i += 1
- continue
- if ":" not in line:
- i += 1
- continue
- key, value = _split_key_value(line)
- # List (hyphen form)
- if value == "" and i + 1 < len(fm_lines) and fm_lines[i + 1].lstrip().startswith("- "):
- items: List[str] = []
- i += 1
- while i < len(fm_lines):
- item_line = fm_lines[i]
- if item_line.lstrip().startswith("- "):
- items.append(item_line.lstrip()[2:].strip())
- i += 1
- else:
- break
- fm[key] = [_coerce_scalar(x) for x in items]
- continue
- inline_list = _parse_inline_list(value)
- if inline_list is not None:
- fm[key] = inline_list
- i += 1
- continue
- # Comma-separated list
- if "," in value:
- fm[key] = [_coerce_scalar(v.strip()) for v in value.split(",") if v.strip()]
- else:
- fm[key] = _coerce_scalar(value.strip())
- i += 1
- return FrontmatterParseResult(frontmatter=fm, body=body)
-
-
-def _split_key_value(line: str) -> Tuple[str, str]:
- idx = line.find(":")
- key = line[:idx].strip()
- value = line[idx + 1 :].strip()
- return key, value
-
-
-def _coerce_scalar(value: str) -> Any:
- low = value.lower()
- if low in ("true", "false"):
- return low == "true"
- if value.isdigit():
- try:
- return int(value)
- except Exception:
- pass
- return value
-
-
-def _parse_inline_list(value: str) -> List[Any] | None:
- stripped = value.strip()
- if len(stripped) < 2 or not stripped.startswith("[") or not stripped.endswith("]"):
- return None
- inner = stripped[1:-1].strip()
- if not inner:
- return []
- return [_coerce_scalar(part.strip()) for part in inner.split(",") if part.strip()]
+
+ if not fm_raw.strip():
+ return FrontmatterParseResult(frontmatter={}, body=body)
+
+ if yaml is None:
+ # Defensive — ``pyproject.toml`` declares PyYAML as a hard
+ # dependency, so this branch only runs in degraded installs.
+ logger.warning(
+ "PyYAML not available; frontmatter parser cannot read nested "
+ "structures (hooks / shell will be silently dropped). "
+ "Install PyYAML to enable full parsing."
+ )
+ return FrontmatterParseResult(frontmatter={}, body=body)
+
+ try:
+ parsed = yaml.safe_load(fm_raw)
+ except yaml.YAMLError as exc:
+ logger.debug("frontmatter YAML parse failed: %s", exc)
+ return FrontmatterParseResult(frontmatter={}, body=body)
+
+ if parsed is None:
+ return FrontmatterParseResult(frontmatter={}, body=body)
+ if not isinstance(parsed, dict):
+ # Top-level scalar / list — not a frontmatter shape we recognize.
+ logger.debug(
+ "frontmatter parsed as %s, expected dict; ignoring",
+ type(parsed).__name__,
+ )
+ return FrontmatterParseResult(frontmatter={}, body=body)
+
+ return FrontmatterParseResult(frontmatter=parsed, body=body)
diff --git a/src/skills/loader.py b/src/skills/loader.py
index d84ea8879..6815690e2 100644
--- a/src/skills/loader.py
+++ b/src/skills/loader.py
@@ -63,38 +63,276 @@ def _get_skill_command_name(file_path: str, base_dir: str) -> str:
return f"{namespace}:{command_base_name}" if namespace else command_base_name
+# ----------------------------------------------------------------------
+# Field validators (mirrored from TS)
+#
+# These ride alongside ``parse_skill_frontmatter_fields`` and degrade
+# gracefully — a bad value logs a debug warning and either drops the
+# field (``None``) or keeps a permissive coerced value, mirroring TS
+# behavior of "fall back to default; never block-load the skill".
+# ----------------------------------------------------------------------
+
+# Mirrors TS ``EFFORT_LEVELS`` (utils/effort.ts).
+EFFORT_LEVELS: frozenset[str] = frozenset({"low", "medium", "high", "max"})
+
+# Mirrors TS ``FRONTMATTER_SHELLS`` (utils/frontmatterParser.ts).
+FRONTMATTER_SHELLS: frozenset[str] = frozenset({"bash", "powershell"})
+
+
+def _extract_description_from_markdown(content: str, default: str) -> str:
+ """Port of TS ``extractDescriptionFromMarkdown``.
+
+ Pulls the first non-empty line, strips a leading ``#`` heading
+ prefix, and clamps to 100 chars (the TS limit) with a trailing
+ ``...`` ellipsis when truncated. Falls back to ``default`` if no
+ content line is found.
+ """
+ for line in (content or "").splitlines():
+ trimmed = line.strip()
+ if not trimmed:
+ continue
+ # Strip ``#``/``##``/etc. heading prefix
+ m = re.match(r"^#+\s+(.+)$", trimmed)
+ text = m.group(1) if m else trimmed
+ if len(text) > 100:
+ return text[:97] + "..."
+ return text
+ return default
+
+
+def _coerce_description(
+ raw: Any, markdown_content: str, resolved_name: str
+) -> tuple[str, bool]:
+ """Return ``(description, has_user_specified_description)``.
+
+ Precedence: explicit frontmatter ``description`` > first-line of
+ markdown body > generated ``Skill: `` placeholder.
+ """
+ if raw is None or raw == "":
+ body_desc = _extract_description_from_markdown(
+ markdown_content, default=f"Skill: {resolved_name}"
+ )
+ return body_desc, False
+
+ if isinstance(raw, list):
+ return " ".join(str(x) for x in raw), True
+ return str(raw), True
+
+
+def _coerce_bool(value: Any, *, default: bool) -> bool:
+ """Coerce a YAML scalar to bool; falls back to ``default`` for
+ non-recognized values."""
+ if isinstance(value, bool):
+ return value
+ if isinstance(value, str):
+ return value.lower() in ("true", "yes", "1")
+ return default
+
+
+def _coerce_allowed_tools(value: Any) -> list[str]:
+ """Parse the ``allowed-tools`` field.
+
+ Accepts a string (comma-separated) or a list. Each entry can be a
+ bare tool name (``Read``) or a tool with a parenthesized arg pattern
+ (``Bash(git status:*)``). Both forms are preserved verbatim so the
+ permission layer can apply its own matching semantics.
+ """
+ if value is None:
+ return []
+ if isinstance(value, str):
+ return [t.strip() for t in value.split(",") if t.strip()]
+ if isinstance(value, list):
+ return [str(t).strip() for t in value if str(t).strip()]
+ return []
+
+
+def _coerce_model(value: Any) -> str | None:
+ """Validate the ``model`` field.
+
+ ``inherit`` / falsy → ``None`` (use caller default). Otherwise
+ return as a string. Logs a debug warning when the value isn't in
+ ``MODEL_ALIASES`` AND doesn't look like a canonical provider model
+ (``claude-``, ``gpt-``, ``o1-``, etc.) — but always keeps the value
+ so the user can pin a brand-new model that the alias table hasn't
+ learned about yet.
+ """
+ if value is None or value == "" or value == "inherit":
+ return None
+ s = str(value).strip()
+ if not s:
+ return None
+
+ try:
+ from src.models.aliases import MODEL_ALIASES
+ known_aliases = set(MODEL_ALIASES.keys())
+ except Exception:
+ known_aliases = set()
+
+ lower = s.lower()
+ looks_canonical = any(
+ lower.startswith(prefix)
+ for prefix in (
+ "claude-", "gpt-", "o1-", "o3-", "o4-",
+ "grok-", "gemini-", "deepseek-", "glm-", "qwen-",
+ )
+ )
+ if lower not in known_aliases and not looks_canonical:
+ logger.warning(
+ "skill frontmatter model %r is not a recognized alias or "
+ "canonical model name; keeping as-is",
+ s,
+ )
+ return s
+
+
+def _coerce_effort(value: Any) -> str | None:
+ """Port of TS ``parseEffortValue``.
+
+ Accepts:
+ - One of ``EFFORT_LEVELS`` (case-insensitive) → returned lowercased.
+ - An integer (or numeric string) → returned as a string.
+ Anything else logs a warning and returns ``None`` (mirrors TS
+ "drop on invalid").
+ """
+ if value is None or value == "":
+ return None
+ if isinstance(value, bool):
+ # bool is a subclass of int in Python — exclude explicitly
+ logger.warning("skill frontmatter effort=%r is not a valid level", value)
+ return None
+ if isinstance(value, int):
+ return str(value)
+ s = str(value).strip().lower()
+ if not s:
+ return None
+ if s in EFFORT_LEVELS:
+ return s
+ try:
+ n = int(s, 10)
+ return str(n)
+ except (TypeError, ValueError):
+ pass
+ logger.warning(
+ "skill frontmatter effort=%r is not a valid level "
+ "(expected one of %s or an integer); dropping",
+ value,
+ sorted(EFFORT_LEVELS),
+ )
+ return None
+
+
+def _coerce_shell(value: Any) -> str | None:
+ """Port of TS ``parseShellFrontmatter`` — accepts ``bash`` /
+ ``powershell``; logs and drops anything else."""
+ if value is None or value == "":
+ return None
+ s = str(value).strip().lower()
+ if not s:
+ return None
+ if s in FRONTMATTER_SHELLS:
+ return s
+ logger.warning(
+ "skill frontmatter shell=%r is not recognized "
+ "(valid: %s); falling back to default",
+ value,
+ sorted(FRONTMATTER_SHELLS),
+ )
+ return None
+
+
+def _coerce_hooks(value: Any, *, skill_name: str) -> dict | None:
+ """Validate the ``hooks:`` frontmatter dict against the rough TS
+ schema (``Partial>``).
+
+ Schema (loose check, no Zod equivalent):
+ hooks:
+ : # one of ALL_HOOK_EVENTS
+ - matcher: # optional
+ hooks: # required, list
+ - type: # required ("command" / "agent" / ...)
+ ... # other shape-specific fields
+ # (passed through verbatim)
+
+ Returns the parsed dict on shape-match. Returns ``None`` (and logs
+ a debug message) on any structural mismatch — the SkillTool can
+ keep loading the skill, just without the hooks.
+ """
+ if value is None:
+ return None
+ if not isinstance(value, dict):
+ logger.debug(
+ "skill %r hooks: expected dict, got %s; dropping",
+ skill_name,
+ type(value).__name__,
+ )
+ return None
+
+ try:
+ from src.hooks.hook_types import ALL_HOOK_EVENTS
+ valid_events = set(ALL_HOOK_EVENTS)
+ except Exception:
+ valid_events = set()
+
+ for event_name, matchers in value.items():
+ if valid_events and event_name not in valid_events:
+ logger.debug(
+ "skill %r hooks: unknown event %r; dropping all hooks",
+ skill_name,
+ event_name,
+ )
+ return None
+ if not isinstance(matchers, list):
+ logger.debug(
+ "skill %r hooks.%s: expected list of matchers, got %s",
+ skill_name,
+ event_name,
+ type(matchers).__name__,
+ )
+ return None
+ for matcher in matchers:
+ if not isinstance(matcher, dict):
+ logger.debug(
+ "skill %r hooks.%s: matcher must be a dict",
+ skill_name,
+ event_name,
+ )
+ return None
+ inner = matcher.get("hooks")
+ if not isinstance(inner, list):
+ logger.debug(
+ "skill %r hooks.%s.hooks: required list missing or wrong type",
+ skill_name,
+ event_name,
+ )
+ return None
+ for cmd in inner:
+ if not isinstance(cmd, dict) or "type" not in cmd:
+ logger.debug(
+ "skill %r hooks.%s.hooks[]: each entry needs a `type` field",
+ skill_name,
+ event_name,
+ )
+ return None
+
+ return value
+
+
def parse_skill_frontmatter_fields(
frontmatter: dict[str, Any],
markdown_content: str,
resolved_name: str,
) -> dict[str, Any]:
- raw_desc = frontmatter.get("description", "")
- if isinstance(raw_desc, list):
- description = " ".join(str(x) for x in raw_desc)
- elif raw_desc:
- description = str(raw_desc)
- else:
- description = f"Skill: {resolved_name}"
-
- user_invocable = frontmatter.get("user-invocable", True)
- if isinstance(user_invocable, str):
- user_invocable = user_invocable.lower() in ("true", "yes", "1")
- elif not isinstance(user_invocable, bool):
- user_invocable = True
-
- disable_model = frontmatter.get("disable-model-invocation", False)
- if isinstance(disable_model, str):
- disable_model = disable_model.lower() in ("true", "yes", "1")
- elif not isinstance(disable_model, bool):
- disable_model = False
-
- allowed_tools_raw = frontmatter.get("allowed-tools", [])
- if isinstance(allowed_tools_raw, str):
- allowed_tools = [t.strip() for t in allowed_tools_raw.split(",") if t.strip()]
- elif isinstance(allowed_tools_raw, list):
- allowed_tools = [str(t) for t in allowed_tools_raw]
- else:
- allowed_tools = []
+ raw_desc = frontmatter.get("description")
+ description, has_user_specified_description = _coerce_description(
+ raw_desc, markdown_content, resolved_name
+ )
+
+ user_invocable = _coerce_bool(frontmatter.get("user-invocable", True), default=True)
+ disable_model = _coerce_bool(
+ frontmatter.get("disable-model-invocation", False), default=False
+ )
+
+ allowed_tools = _coerce_allowed_tools(frontmatter.get("allowed-tools"))
argument_names = parse_argument_names(frontmatter.get("arguments"))
@@ -106,10 +344,7 @@ def parse_skill_frontmatter_fields(
if version is not None:
version = str(version)
- model_raw = frontmatter.get("model")
- model = None
- if model_raw and model_raw != "inherit":
- model = str(model_raw)
+ model = _coerce_model(frontmatter.get("model"))
context = frontmatter.get("context", "inline")
execution_context = "fork" if context == "fork" else None
@@ -118,9 +353,11 @@ def parse_skill_frontmatter_fields(
if agent is not None:
agent = str(agent)
- effort = frontmatter.get("effort")
- if effort is not None:
- effort = str(effort)
+ effort = _coerce_effort(frontmatter.get("effort"))
+
+ shell = _coerce_shell(frontmatter.get("shell"))
+
+ hooks = _coerce_hooks(frontmatter.get("hooks"), skill_name=resolved_name)
argument_hint_raw = frontmatter.get("argument-hint")
argument_hint = str(argument_hint_raw) if argument_hint_raw is not None else None
@@ -147,7 +384,7 @@ def parse_skill_frontmatter_fields(
return {
"display_name": display_name,
"description": description,
- "has_user_specified_description": bool(raw_desc),
+ "has_user_specified_description": has_user_specified_description,
"allowed_tools": allowed_tools,
"argument_hint": argument_hint,
"argument_names": argument_names,
@@ -160,6 +397,8 @@ def parse_skill_frontmatter_fields(
"agent": agent,
"effort": effort,
"paths": paths,
+ "hooks": hooks,
+ "shell": shell,
}
@@ -185,6 +424,8 @@ def create_skill_command(
agent: str | None = None,
paths: list[str] | None = None,
effort: str | None = None,
+ hooks: dict | None = None,
+ shell: str | None = None,
) -> Skill:
return Skill(
name=skill_name,
@@ -212,6 +453,8 @@ def create_skill_command(
has_user_specified_description=has_user_specified_description,
base_dir=base_dir,
markdown_content=markdown_content,
+ hooks=hooks,
+ shell=shell,
)
@@ -277,10 +520,33 @@ def _walk(skill_dir_path: Path) -> None:
return skill_files
+_SOURCE_TO_LOADED_FROM: dict[str, str] = {
+ "policySettings": "managed",
+ "userSettings": "user",
+ "projectSettings": "project",
+ "plugin": "plugin",
+}
+
+
def load_skills_from_skills_dir(
base_path: str,
source: str,
+ *,
+ loaded_from: str | None = None,
) -> list[Skill]:
+ """Load every ``SKILL.md`` under ``base_path`` recursively.
+
+ ``loaded_from`` defaults to a friendly label derived from ``source``
+ (``policySettings`` -> ``managed``, ``userSettings`` -> ``user``,
+ ``projectSettings`` -> ``project``). Callers can pass an explicit
+ string to override (used by the legacy registry path).
+ """
+ resolved_loaded_from = (
+ loaded_from
+ if loaded_from is not None
+ else _SOURCE_TO_LOADED_FROM.get(source, "skills")
+ )
+
skill_files = _find_skill_markdown_files(base_path)
skills: list[Skill] = []
@@ -318,11 +584,13 @@ def load_skills_from_skills_dir(
user_invocable=parsed["user_invocable"],
source=source,
base_dir=str(Path(skill_file_path).parent),
- loaded_from="skills",
+ loaded_from=resolved_loaded_from,
execution_context=parsed["execution_context"],
agent=parsed["agent"],
paths=parsed["paths"],
effort=parsed["effort"],
+ hooks=parsed.get("hooks"),
+ shell=parsed.get("shell"),
)
skills.append(skill)
@@ -347,39 +615,208 @@ def _get_project_skills_dirs(cwd: str) -> list[str]:
_skill_dir_cache: dict[str, list[Skill]] = {}
+# ----------------------------------------------------------------------
+# Bare mode + policy plumbing
+#
+# Mirrors TS' `isBareMode()` / `CLAUDE_CODE_DISABLE_POLICY_SKILLS` /
+# `isRestrictedToPluginOnly('skills')` gates so the Python loader honors
+# the same env-driven safety flags. We reuse the existing
+# `_is_bare_mode` / `_get_additional_directories` semantics already
+# established by `src/context_system/claude_md.py` rather than inventing
+# parallel ones.
+# ----------------------------------------------------------------------
+
+
+def _is_bare_mode() -> bool:
+ """True when ``CLAUDE_CODE_BARE_MODE`` is set (matches CLAUDE.md path).
+
+ Bare mode skips autodiscovery entirely; only ``--add-dir`` paths
+ contribute disk skills. Bundled/MCP skills are unaffected (they go
+ through `get_all_skills`'s separate merge path).
+ """
+ return os.environ.get("CLAUDE_CODE_BARE_MODE", "").lower() in ("1", "true", "yes")
+
+
+def _is_skills_policy_disabled() -> bool:
+ """True when ``CLAUDE_CODE_DISABLE_POLICY_SKILLS`` is set.
+
+ Mirrors the TS check at `loadSkillsDir.ts:771`. When set, the
+ managed/policy skills directory (`/etc/claude/.claude/skills`) is
+ skipped — useful for opting out of admin-distributed skills on
+ multi-tenant machines.
+ """
+ return os.environ.get("CLAUDE_CODE_DISABLE_POLICY_SKILLS", "").lower() in (
+ "1", "true", "yes",
+ )
+
+
+def _is_restricted_to_plugin_only(scope: str) -> bool:
+ """Stub for the TS `isRestrictedToPluginOnly(scope)` policy gate.
+
+ The TS implementation is policy-driven (managed settings can lock
+ skills/agents/etc. to plugin-supplied entries only). The Python port
+ has no policy plumbing yet, so this returns False — equivalent to
+ "policy unset; allow normal discovery". Plugin-policy support can
+ flip this in a future task without touching call sites.
+ """
+ _ = scope # documented hook for future plugin policy
+ return False
+
+
+def _get_additional_skill_dirs() -> list[str]:
+ """Read ``--add-dir`` paths from ``CLAUDE_CODE_ADDITIONAL_DIRECTORIES``.
+
+ Each entry maps to ``/.claude/skills`` for skill loading
+ (matches TS `additionalSkillsNested` block).
+ """
+ val = os.environ.get("CLAUDE_CODE_ADDITIONAL_DIRECTORIES", "")
+ if not val:
+ return []
+ return [d.strip() for d in val.split(os.pathsep) if d.strip()]
+
+
+def _get_file_identity(path: str) -> str | None:
+ """Return a stable identity for ``path`` (resolved via realpath).
+
+ Mirrors TS `getFileIdentity` (`loadSkillsDir.ts:118`). Uses
+ realpath so symlinked-and-overlapping mounts collapse to the same
+ identity. Returns ``None`` on broken-symlink / OSError so the caller
+ can fail open (i.e., keep the skill rather than drop it on a stat
+ failure).
+ """
+ try:
+ return os.path.realpath(path)
+ except (OSError, ValueError):
+ return None
+
+
def get_skill_dir_commands(cwd: str) -> list[Skill]:
+ """Return the union of disk-loaded skills, deduped by realpath identity.
+
+ Behavior matches TS `loadSkillsFromSkillsDir` (line 720+):
+ - Bare mode: load only ``--add-dir`` paths; skip everything else.
+ - Policy disabled: skip the managed/`/etc/claude` dir.
+ - Plugin-only restriction: collapses user + project loads to empty
+ (the gate currently always returns False; future hook).
+ - Order: managed → user → project → additional. Realpath dedup is
+ first-wins so the same SKILL.md file accessed via overlapping
+ sources (symlinks, bind mounts) collapses to one entry.
+
+ The cwd-keyed cache is preserved; cache invalidation goes through
+ ``clear_skill_caches``.
+ """
if cwd in _skill_dir_cache:
return list(_skill_dir_cache[cwd])
- user_skills_dir = str(_get_global_config_dir() / "skills")
+ additional_dirs = _get_additional_skill_dirs()
+ plugin_only = _is_restricted_to_plugin_only("skills")
+
+ # --- Bare mode short-circuit --------------------------------------
+ if _is_bare_mode():
+ if not additional_dirs or plugin_only:
+ logger.debug(
+ "[skills] bare mode active; skipping discovery "
+ "(additional_dirs=%d plugin_only=%s)",
+ len(additional_dirs),
+ plugin_only,
+ )
+ _skill_dir_cache[cwd] = []
+ return []
+ bare_skills: list[Skill] = []
+ for d in additional_dirs:
+ skills_dir = str(Path(d) / ".claude" / "skills")
+ bare_skills.extend(
+ load_skills_from_skills_dir(skills_dir, "projectSettings")
+ )
+ unconditional = _split_conditional(bare_skills)
+ _skill_dir_cache[cwd] = unconditional
+ return list(unconditional)
+
+ # --- Standard discovery -------------------------------------------
managed_skills_dir = str(_get_managed_file_path() / ".claude" / "skills")
+ user_skills_dir = str(_get_global_config_dir() / "skills")
project_skills_dirs = _get_project_skills_dirs(cwd)
- managed_skills = load_skills_from_skills_dir(managed_skills_dir, "policySettings")
- user_skills = load_skills_from_skills_dir(user_skills_dir, "userSettings")
+ managed_skills: list[Skill] = []
+ if not _is_skills_policy_disabled():
+ managed_skills = load_skills_from_skills_dir(
+ managed_skills_dir, "policySettings"
+ )
+ user_skills: list[Skill] = []
project_skills: list[Skill] = []
- for d in project_skills_dirs:
- project_skills.extend(load_skills_from_skills_dir(d, "projectSettings"))
+ if not plugin_only:
+ user_skills = load_skills_from_skills_dir(user_skills_dir, "userSettings")
+ for d in project_skills_dirs:
+ project_skills.extend(
+ load_skills_from_skills_dir(d, "projectSettings")
+ )
+
+ additional_skills: list[Skill] = []
+ if not plugin_only:
+ for d in additional_dirs:
+ skills_dir = str(Path(d) / ".claude" / "skills")
+ additional_skills.extend(
+ load_skills_from_skills_dir(skills_dir, "projectSettings")
+ )
+
+ all_skills = managed_skills + user_skills + project_skills + additional_skills
+
+ deduped = _dedup_by_realpath(all_skills)
+ unconditional = _split_conditional(deduped)
- all_skills = managed_skills + user_skills + project_skills
+ _skill_dir_cache[cwd] = unconditional
+ return list(unconditional)
- seen_names: set[str] = set()
- deduped: list[Skill] = []
- for skill in all_skills:
- if skill.name not in seen_names:
- seen_names.add(skill.name)
- deduped.append(skill)
+def _dedup_by_realpath(skills: list[Skill]) -> list[Skill]:
+ """First-wins dedup keyed on each skill's resolved SKILL.md path.
+
+ Matches TS `loadSkillsDir.ts:813-848`. Skills whose `base_dir` can't
+ be resolved (broken symlink, OSError) fall through unchanged — fail
+ open, mirroring TS' `null` identity branch.
+ """
+ seen: dict[str, Skill] = {}
+ out: list[Skill] = []
+ for skill in skills:
+ skill_md = (
+ str(Path(skill.base_dir) / "SKILL.md")
+ if skill.base_dir
+ else None
+ )
+ identity = _get_file_identity(skill_md) if skill_md else None
+ if identity is None:
+ out.append(skill)
+ continue
+ existing = seen.get(identity)
+ if existing is not None:
+ logger.debug(
+ "[skills] dropping duplicate %r from %s "
+ "(same SKILL.md already loaded from %s)",
+ skill.name,
+ skill.source,
+ existing.source,
+ )
+ continue
+ seen[identity] = skill
+ out.append(skill)
+ return out
+
+
+def _split_conditional(skills: list[Skill]) -> list[Skill]:
+ """Move conditional (paths-gated) skills to ``_conditional_skills``.
+
+ Returns the list of unconditional skills. Conditional ones become
+ visible only after `activate_conditional_skills_for_paths` matches
+ a touched file against their `paths:` patterns.
+ """
unconditional: list[Skill] = []
- for skill in deduped:
+ for skill in skills:
if not skill.is_conditional:
unconditional.append(skill)
else:
_conditional_skills[skill.name] = skill
-
- _skill_dir_cache[cwd] = unconditional
- return list(unconditional)
+ return unconditional
_conditional_skills: dict[str, Skill] = {}
@@ -392,10 +829,41 @@ def get_dynamic_skills() -> list[Skill]:
return list(_dynamic_skills.values())
+def _is_path_gitignored(path: str, cwd: str) -> bool:
+ """Return True iff git considers ``path`` ignored.
+
+ Mirrors TS `isPathGitignored` (`utils/git/gitignore.ts`) — shells out
+ to ``git check-ignore `` and treats exit 0 as ignored, exit 1
+ as not-ignored, anything else (e.g. exit 128 outside a git repo) as
+ not-ignored. Fails open on subprocess errors so non-git workspaces
+ keep working.
+ """
+ import subprocess
+ try:
+ result = subprocess.run(
+ ["git", "check-ignore", path],
+ cwd=cwd,
+ capture_output=True,
+ text=True,
+ timeout=5,
+ )
+ except (FileNotFoundError, subprocess.SubprocessError, OSError):
+ return False
+ return result.returncode == 0
+
+
def discover_skill_dirs_for_paths(
file_paths: list[str],
cwd: str,
) -> list[str]:
+ """Walk parent dirs of each touched file looking for `.claude/skills`.
+
+ Mirrors TS `discoverSkillDirsForPaths` (`loadSkillsDir.ts:951+`).
+ Each newly-found skills dir whose containing folder is gitignored is
+ skipped (e.g. `node_modules/pkg/.claude/skills` won't load silently);
+ `git check-ignore` handles nested `.gitignore` and global rules with
+ correct precedence. Fails open outside a git repo.
+ """
resolved_cwd = cwd.rstrip(os.sep)
new_dirs: list[str] = []
@@ -407,7 +875,13 @@ def discover_skill_dirs_for_paths(
if skill_dir not in _dynamic_skill_dirs:
_dynamic_skill_dirs.add(skill_dir)
if Path(skill_dir).is_dir():
- new_dirs.append(skill_dir)
+ if _is_path_gitignored(current_dir, resolved_cwd):
+ logger.debug(
+ "[skills] Skipped gitignored skills dir: %s",
+ skill_dir,
+ )
+ else:
+ new_dirs.append(skill_dir)
parent = str(Path(current_dir).parent)
if parent == current_dir:
@@ -430,33 +904,93 @@ def activate_conditional_skills_for_paths(
file_paths: list[str],
cwd: str,
) -> list[str]:
+ """Promote conditional skills whose ``paths:`` patterns match.
+
+ Path matching uses gitignore semantics via ``pathspec`` (TS uses the
+ `ignore` library) — supports ``**`` recursion, anchoring, negation,
+ etc. Each skill's full ``paths:`` list compiles into a single
+ `PathSpec` that the touched files match against; skills where any
+ pattern matches any file get moved out of `_conditional_skills`
+ into `_dynamic_skills`.
+
+ Path-validity guards from TS:
+ - Skip empty rel-paths.
+ - Skip ``..``-prefixed paths (file escaped the workspace).
+ - Skip absolute paths (Windows cross-drive).
+ """
if not _conditional_skills:
return []
+ # Pre-resolve relative paths once per file_path so we don't repeat
+ # the work for each conditional skill. Filter invalid entries here.
+ rel_paths: list[str] = []
+ for file_path in file_paths:
+ rel_path = os.path.relpath(file_path, cwd)
+ if not rel_path or rel_path.startswith("..") or os.path.isabs(rel_path):
+ continue
+ rel_paths.append(rel_path)
+
+ if not rel_paths:
+ return []
+
activated: list[str] = []
for name, skill in list(_conditional_skills.items()):
if not skill.paths:
continue
- for file_path in file_paths:
- rel_path = os.path.relpath(file_path, cwd)
- if rel_path.startswith("..") or os.path.isabs(rel_path):
- continue
- for pattern in skill.paths:
- if _path_matches_pattern(rel_path, pattern):
- _dynamic_skills[name] = skill
- del _conditional_skills[name]
- _activated_conditional_names.add(name)
- activated.append(name)
- break
- if name in activated:
+ spec = _compile_path_spec(skill.paths)
+ if spec is None:
+ continue
+ for rel_path in rel_paths:
+ if spec.match_file(rel_path):
+ _dynamic_skills[name] = skill
+ del _conditional_skills[name]
+ _activated_conditional_names.add(name)
+ activated.append(name)
break
return activated
+def _compile_path_spec(patterns: list[str]):
+ """Compile a list of gitwildmatch patterns into a ``PathSpec``.
+
+ Returns ``None`` (and logs at debug) if pathspec isn't installed or
+ the patterns can't compile. The unconditional-skill path treats this
+ as a no-match — better than crashing the activation loop.
+ """
+ try:
+ import pathspec
+ except ImportError: # pragma: no cover — pathspec is a hard dep
+ logger.debug(
+ "pathspec not installed; conditional `paths:` matching disabled"
+ )
+ return None
+ # Prefer the modern ``gitignore`` pattern factory (pathspec >= 1.0)
+ # which subsumes the legacy ``gitwildmatch`` and emits no deprecation
+ # warning. Fall back to ``gitwildmatch`` for older pathspec versions.
+ for factory in ("gitignore", "gitwildmatch"):
+ try:
+ return pathspec.PathSpec.from_lines(factory, patterns)
+ except (LookupError, KeyError):
+ continue
+ except Exception as exc:
+ logger.debug("failed to compile path spec %r: %s", patterns, exc)
+ return None
+ return None
+
+
def _path_matches_pattern(path: str, pattern: str) -> bool:
- import fnmatch
- return fnmatch.fnmatch(path, pattern) or fnmatch.fnmatch(path, pattern + "/**")
+ """Legacy single-pattern matcher kept for backward compat.
+
+ Uses pathspec under the hood now (so ``src/**/*.py`` works correctly
+ against ``src/foo/bar.py``); the prior `fnmatch`-based impl missed
+ those cases. New code should prefer building a `PathSpec` once and
+ calling `.match_file(...)` directly.
+ """
+ spec = _compile_path_spec([pattern])
+ if spec is None:
+ return False
+ return spec.match_file(path)
def clear_skill_caches() -> None:
@@ -476,122 +1010,88 @@ def get_conditional_skill_count() -> int:
return len(_conditional_skills)
-from .model import PromptSkill # noqa: E402
+# ----------------------------------------------------------------------
+# Unified skill registry
+#
+# Historically this module had two parallel disk-loading paths: the
+# TS-port branch (above) that produced rich `Skill` objects with nested
+# namespace support, and a second branch that produced `PromptSkill`
+# objects via `load_skills_from_dir` and populated `_skill_registry`.
+# Only the second registry was wired into `SkillTool`, so everything
+# loaded by the TS-port path was invisible to the model.
+#
+# `Skill` and `PromptSkill` are now the same class (see model.py).
+# `get_all_skills` delegates to `get_skill_dir_commands` for the
+# managed/user/project disk layout, then merges in bundled skills, MCP
+# skills, and any legacy clawcodex-specific directories. The unified
+# result is cached in `_skill_registry` so `get_registered_skill`
+# (used by `SkillTool`) continues to work and now sees every skill.
+# ----------------------------------------------------------------------
-_skill_registry: dict[str, PromptSkill] = {}
+from .model import PromptSkill # noqa: E402 (re-exported for back-compat)
+from .bundled_skills import get_bundled_skills, get_bundled_skill_by_name # noqa: E402
+
+_skill_registry: dict[str, Skill] = {}
def clear_skill_registry() -> None:
_skill_registry.clear()
-def _candidate_user_skills_dirs() -> list[Path]:
+def _legacy_user_skill_dirs(
+ user_skills_dir: str | Path | None,
+) -> list[Path]:
+ """Resolve the legacy clawcodex user-skill locations.
+
+ The TS-port loader walks `~/.claude/skills` (handled inside
+ `get_skill_dir_commands`). This function returns the additional
+ clawcodex-specific dirs plus any env overrides so existing setups
+ that drop skills under `CLAWCODEX_SKILLS_DIR` or `~/.clawcodex/skills`
+ continue to work.
+ """
+ dirs: list[Path] = []
+
+ if user_skills_dir is not None:
+ dirs.append(Path(user_skills_dir).expanduser().resolve())
+ return dirs
+
env_primary = os.environ.get("CLAWCODEX_SKILLS_DIR")
env_ts = os.environ.get("CLAUDE_SKILLS_DIR")
- dirs: list[Path] = []
if env_primary:
dirs.append(Path(env_primary).expanduser().resolve())
if env_ts:
p = Path(env_ts).expanduser().resolve()
if p not in dirs:
dirs.append(p)
- for d in (Path.home() / ".clawcodex" / "skills", Path.home() / ".claude" / "skills"):
- p = d.expanduser().resolve()
- if p not in dirs:
- dirs.append(p)
- return dirs
+ clawcodex_dir = (Path.home() / ".clawcodex" / "skills").expanduser().resolve()
+ if clawcodex_dir not in dirs:
+ dirs.append(clawcodex_dir)
-def _as_str_list(val: Any) -> list[str]:
- if val is None:
- return []
- if isinstance(val, list):
- return [str(x) for x in val if str(x)]
- if isinstance(val, str):
- s = val.strip()
- if not s:
- return []
- if "," in s:
- return [x.strip() for x in s.split(",") if x.strip()]
- return [s]
- return [str(val)]
+ return dirs
-def _extract_description(body: str) -> str | None:
- for line in body.splitlines():
- stripped = line.strip()
- if not stripped:
- continue
- if stripped.startswith("#"):
- continue
- return stripped[:200]
- return None
-
+def _legacy_project_skill_dirs(project_root: str | Path) -> list[Path]:
+ """Resolve clawcodex-specific project skill dirs.
-def load_skills_from_dir(
- base_dir: str | Path, *, loaded_from: str = "skills"
-) -> list[PromptSkill]:
- base = Path(base_dir).expanduser().resolve()
- if not base.exists() or not base.is_dir():
- return []
+ `.claude/skills` is already handled by `get_skill_dir_commands`; here
+ we add `.clawcodex/skills` as a sibling so the legacy layout still
+ works.
+ """
+ pr = Path(project_root).expanduser().resolve()
+ return [pr / ".clawcodex" / "skills"]
- skills: list[PromptSkill] = []
- for entry in sorted(base.iterdir()):
- if not entry.is_dir():
- continue
- skill_name = entry.name
- md_path = entry / "SKILL.md"
- if not md_path.exists():
- continue
- content = md_path.read_text(encoding="utf-8")
- parsed_fm = parse_frontmatter(content)
- fm = parsed_fm.frontmatter
- body = parsed_fm.body
- description = str(
- fm.get("description") or _extract_description(body) or f"Skill: {skill_name}"
- )
- user_invocable = bool(fm.get("user-invocable", True))
- disable_model_invocation = bool(fm.get("disable-model-invocation", False))
- when_to_use = fm.get("when_to_use")
- when_to_use = str(when_to_use) if when_to_use is not None else None
- version = fm.get("version")
- version = str(version) if version is not None else None
- model = fm.get("model")
- model = str(model) if model is not None else None
-
- allowed_tools = _as_str_list(fm.get("allowed-tools"))
- arg_names = parse_argument_names(fm.get("arguments"))
- context = "fork" if str(fm.get("context", "")).lower() == "fork" else "inline"
- agent_val = fm.get("agent")
- agent_val = str(agent_val) if agent_val is not None else None
- effort_val = fm.get("effort")
- effort_val = str(effort_val) if effort_val is not None else None
- paths_val = _as_str_list(fm.get("paths"))
- if paths_val == []:
- paths_val = None
-
- skill = PromptSkill(
- name=skill_name,
- description=description,
- loaded_from=loaded_from,
- user_invocable=user_invocable,
- disable_model_invocation=disable_model_invocation,
- content_length=len(body),
- is_hidden=not user_invocable,
- skill_root=str(entry),
- when_to_use=when_to_use,
- version=version,
- model=model,
- allowed_tools=allowed_tools,
- arg_names=arg_names,
- context=context,
- agent=agent_val,
- effort=effort_val,
- paths=paths_val,
- markdown_content=body,
+def _load_dirs_as(
+ dirs: Sequence[Path | str],
+ source: str,
+ loaded_from: str,
+) -> list[Skill]:
+ skills: list[Skill] = []
+ for d in dirs:
+ skills.extend(
+ load_skills_from_skills_dir(str(d), source, loaded_from=loaded_from)
)
- skills.append(skill)
return skills
@@ -599,36 +1099,140 @@ def get_all_skills(
*,
project_root: str | Path | None = None,
user_skills_dir: str | Path | None = None,
-) -> Sequence[PromptSkill]:
+) -> Sequence[Skill]:
+ """Return the unified set of skills available to the model.
+
+ Sources, in priority order (first occurrence of a name wins):
+
+ 1. Managed/policy skills (``/etc/claude/.claude/skills`` by default)
+ 2. User skills (``~/.claude/skills`` plus any clawcodex/env-specified
+ user-skill dirs)
+ 3. Project skills (walking up from ``project_root`` to ``$HOME`` for
+ ``.claude/skills`` plus ``/.clawcodex/skills``)
+ 4. Managed override via ``CLAWCODEX_MANAGED_SKILLS_DIR``
+ 5. Bundled skills registered via ``register_bundled_skill``
+ 6. MCP-loaded skills returned by registered MCP skill builders
+
+ Disk skills support nested namespacing (``category/skill/SKILL.md``
+ becomes ``category:skill``) via ``get_skill_dir_commands``.
+
+ The returned set is also stored in ``_skill_registry`` so legacy
+ callers of ``get_registered_skill`` see every source.
+ """
clear_skill_registry()
- if user_skills_dir is not None:
- user_dirs = [Path(user_skills_dir).expanduser().resolve()]
- else:
- user_dirs = _candidate_user_skills_dirs()
- for user_dir in user_dirs:
- for s in load_skills_from_dir(user_dir, loaded_from="user"):
- _skill_registry[s.name] = s
+ cwd = (
+ str(Path(project_root).expanduser().resolve())
+ if project_root is not None
+ else os.getcwd()
+ )
+
+ # 1-3: Managed + user + project disk skills via the unified TS-port loader
+ disk_skills: list[Skill] = list(get_skill_dir_commands(cwd))
+
+ # 2b: Additional user-skill dirs (clawcodex-specific + env overrides)
+ extra_user_skills = _load_dirs_as(
+ _legacy_user_skill_dirs(user_skills_dir),
+ source="userSettings",
+ loaded_from="user",
+ )
+
+ # 3b: Additional project-skill dir for the clawcodex layout
+ extra_project_skills: list[Skill] = []
+ if project_root is not None:
+ extra_project_skills = _load_dirs_as(
+ _legacy_project_skill_dirs(project_root),
+ source="projectSettings",
+ loaded_from="project",
+ )
+
+ # 4: Managed override env (separate from /etc/claude policy dir)
+ extra_managed_skills: list[Skill] = []
managed_env = os.environ.get("CLAWCODEX_MANAGED_SKILLS_DIR")
if managed_env:
- managed_dir = Path(managed_env).expanduser().resolve()
- for s in load_skills_from_dir(managed_dir, loaded_from="managed"):
- _skill_registry[s.name] = s
+ extra_managed_skills = _load_dirs_as(
+ [managed_env],
+ source="policySettings",
+ loaded_from="managed",
+ )
- if project_root is not None:
- pr = Path(project_root).expanduser().resolve()
- proj_dirs = []
- main_path = pr / ".clawcodex" / "skills"
- compat_path = pr / ".claude" / "skills"
- proj_dirs.append(main_path)
- if compat_path != main_path:
- proj_dirs.append(compat_path)
- for pr_dir in proj_dirs:
- for s in load_skills_from_dir(pr_dir, loaded_from="project"):
- _skill_registry[s.name] = s
-
- return list(_skill_registry.values())
-
-
-def get_registered_skill(name: str) -> PromptSkill | None:
- return _skill_registry.get(name)
+ # 5: Bundled skills (always present)
+ bundled = get_bundled_skills()
+
+ # 6: MCP skills (if a builder is registered)
+ mcp_skills: list[Skill] = []
+ builders = None
+ try:
+ from .mcp_skill_builders import get_mcp_skill_builders
+ builders = get_mcp_skill_builders()
+ except Exception:
+ builders = None
+ if builders:
+ for builder in builders.values():
+ try:
+ produced = builder()
+ except Exception:
+ continue
+ if produced:
+ mcp_skills.extend(produced)
+
+ # Activated conditional skills + skills introduced by
+ # ``add_skill_directories`` live in ``_dynamic_skills``. They must be
+ # merged last so explicit user/project skills with the same name win
+ # — a conditional skill is by definition the "lowest-priority"
+ # entry. Without this, ``activate_conditional_skills_for_paths``
+ # would silently move a skill into ``_dynamic_skills`` while the
+ # canonical ``SkillTool`` lookup (which goes through
+ # ``get_registered_skill`` → ``_skill_registry``) still returns
+ # "Unknown skill". (QA bug #14.)
+ dynamic_skills = get_dynamic_skills()
+
+ # Merge with first-source-wins precedence. The order below mirrors
+ # the priority list above: managed env override first (so admins can
+ # force a version), then policy/user/project from the unified loader,
+ # then clawcodex extras, then bundled and MCP as fallbacks, with
+ # dynamic (activated conditional / runtime-added) entries last.
+ merge_order: list[Skill] = (
+ list(extra_managed_skills)
+ + list(disk_skills)
+ + list(extra_user_skills)
+ + list(extra_project_skills)
+ + list(bundled)
+ + list(mcp_skills)
+ + list(dynamic_skills)
+ )
+
+ deduped: dict[str, Skill] = {}
+ for skill in merge_order:
+ if skill.name not in deduped:
+ deduped[skill.name] = skill
+
+ _skill_registry.update(deduped)
+ return list(deduped.values())
+
+
+def get_registered_skill(name: str) -> Skill | None:
+ """Look up a skill in the unified registry.
+
+ Falls back to ``get_bundled_skill_by_name`` so callers that rely on
+ bundled-skill aliases keep working even before ``get_all_skills`` is
+ populated for the current cwd.
+ """
+ found = _skill_registry.get(name)
+ if found is not None:
+ return found
+ return get_bundled_skill_by_name(name)
+
+
+# Back-compat shim for the old PromptSkill-producing loader. Some callers
+# (and downstream tests) imported `load_skills_from_dir` directly. Keep
+# the surface the same but route through the unified
+# `load_skills_from_skills_dir` so behaviour matches the registry.
+def load_skills_from_dir(
+ base_dir: str | Path, *, loaded_from: str = "skills"
+) -> list[Skill]:
+ return load_skills_from_skills_dir(
+ str(Path(base_dir).expanduser().resolve()),
+ source="userSettings",
+ loaded_from=loaded_from,
+ )
diff --git a/src/skills/model.py b/src/skills/model.py
index 6a6db12e8..3f42032ca 100644
--- a/src/skills/model.py
+++ b/src/skills/model.py
@@ -33,6 +33,20 @@ class Skill:
base_dir: Optional[str] = None
markdown_content: str = ""
progress_message: str = "running"
+ # ------------------------------------------------------------------
+ # Frontmatter fields supported by the TS port that the prior Python
+ # parser silently dropped:
+ # - ``hooks``: nested dict shaped as
+ # ``{HookEvent: [{"matcher"?: str, "hooks": [HookCommand]}]}``.
+ # Validated by ``parse_skill_frontmatter_fields`` against
+ # ``ALL_HOOK_EVENTS``; invalid shapes are logged and dropped
+ # (set to ``None``) without raising.
+ # - ``shell``: ``"bash"`` | ``"powershell"``. Used by the runtime
+ # shell-execution-in-prompt path to pick which shell tool
+ # evaluates ``!`...``` blocks. ``None`` -> caller default (bash).
+ # ------------------------------------------------------------------
+ hooks: Optional[dict] = None
+ shell: Optional[str] = None
get_prompt_for_command: Optional[Callable[[str], str]] = None
is_enabled_fn: Optional[Callable[[], bool]] = None
@@ -69,7 +83,30 @@ def is_enabled(self) -> bool:
return self.is_enabled_fn()
return True
+ # ------------------------------------------------------------------
+ # Backward-compat alias
+ #
+ # Historically a separate `PromptSkill` subclass exposed an `arg_names`
+ # field while the TS-port `Skill` used `argument_names`. Now that the
+ # two have collapsed to a single canonical `Skill`, expose `arg_names`
+ # as a property that proxies to `argument_names` so older call sites
+ # (e.g., command_system/skills_integration.py, tools/skill.py) keep
+ # working without modification.
+ # ------------------------------------------------------------------
-@dataclass
-class PromptSkill(Skill):
- arg_names: list[str] = field(default_factory=list)
+ @property
+ def arg_names(self) -> list[str]:
+ return self.argument_names
+
+ @arg_names.setter
+ def arg_names(self, value: list[str] | None) -> None:
+ self.argument_names = list(value) if value else []
+
+
+# ----------------------------------------------------------------------
+# `PromptSkill` is preserved as an alias for backward compatibility.
+# All disk-loaded skills are now plain `Skill` instances; the prior
+# parallel hierarchy has been folded into one canonical type.
+# ----------------------------------------------------------------------
+
+PromptSkill = Skill
diff --git a/src/skills/runtime_substitution.py b/src/skills/runtime_substitution.py
new file mode 100644
index 000000000..905fe7b4e
--- /dev/null
+++ b/src/skills/runtime_substitution.py
@@ -0,0 +1,260 @@
+"""Per-invocation transforms applied to a skill's markdown body.
+
+These mirror the four transforms TS performs inside
+``createSkillCommand.getPromptForCommand`` (loadSkillsDir.ts:344-399):
+
+ 1. Prepend ``Base directory for this skill: \\n\\n`` if the
+ skill carries a ``skill_root``/``base_dir``.
+ 2. Apply ``substitute_arguments`` (named placeholders, ``$N``,
+ ``$ARGUMENTS`` shorthand, optional append-on-no-placeholder).
+ 3. Replace ``${CLAUDE_SKILL_DIR}`` with the resolved skill directory.
+ Backslashes get normalized to forward slashes (Windows compat with
+ embedded shell injections).
+ 4. Replace ``${CLAUDE_SESSION_ID}`` with the active session id.
+ 5. Execute embedded ``!`...`` and `````! ... ````` shell blocks
+ via the supplied ``shell_executor``. Skipped entirely for skills
+ loaded from MCP (security boundary mirroring TS' ``loadedFrom !==
+ 'mcp'`` guard).
+
+The transforms live in this file as pure functions (one per step) plus a
+single orchestrator, ``render_skill_prompt``, that wires them together.
+The orchestrator takes a pluggable ``shell_executor`` callable so the
+SkillTool can pass a BashTool-backed implementation while unit tests can
+inject a fake (or pass ``None`` to skip shell execution).
+"""
+
+from __future__ import annotations
+
+import logging
+import re
+from typing import Callable, Optional, Sequence
+
+from .argument_substitution import substitute_arguments
+
+logger = logging.getLogger(__name__)
+
+
+# ----------------------------------------------------------------------
+# Shell-block patterns (mirroring promptShellExecution.ts)
+# ----------------------------------------------------------------------
+
+# `````!\\n\\n````` — multi-line fenced form. Capture is the inner
+# command with surrounding whitespace stripped at use-site.
+_BLOCK_PATTERN = re.compile(r"```!\s*\n?([\s\S]*?)\n?```")
+
+# ``!```` — inline form. Requires whitespace or start-of-line
+# before the ``!`` to avoid false positives inside other inline-code spans
+# (e.g. ```foo`!`bar```) and shell variable references like ``$!``.
+_INLINE_PATTERN = re.compile(r"(?:^|(?<=\s))!`([^`]+)`", re.MULTILINE)
+
+
+# ----------------------------------------------------------------------
+# Pure transforms (each independently testable)
+# ----------------------------------------------------------------------
+
+
+def prepend_base_dir_header(content: str, base_dir: Optional[str]) -> str:
+ """Prepend the canonical ``Base directory ...`` header.
+
+ No-op when ``base_dir`` is empty/None — matches TS behavior where
+ bundled skills without a shipped ``files`` directory get no header.
+ """
+ if not base_dir:
+ return content
+ return f"Base directory for this skill: {base_dir}\n\n{content}"
+
+
+def substitute_skill_dir(content: str, base_dir: Optional[str]) -> str:
+ """Replace every ``${CLAUDE_SKILL_DIR}`` with the skill root.
+
+ Backslashes are flipped to forward slashes so embedded shell commands
+ (``!`cat ${CLAUDE_SKILL_DIR}/script.sh```) don't treat them as escape
+ sequences on Windows. Mirrors the TS ``process.platform === 'win32'``
+ branch but applies unconditionally — flipping ``\\`` to ``/`` is safe
+ on POSIX paths since absolute POSIX paths never contain ``\\``.
+ """
+ if not base_dir:
+ return content
+ skill_dir = base_dir.replace("\\", "/") if "\\" in base_dir else base_dir
+ return content.replace("${CLAUDE_SKILL_DIR}", skill_dir)
+
+
+def substitute_session_id(content: str, session_id: Optional[str]) -> str:
+ """Replace ``${CLAUDE_SESSION_ID}`` with the active session id.
+
+ Unknown / unset session id → empty string substitution (matches TS
+ behavior when ``getSessionId()`` returns falsy).
+ """
+ return content.replace("${CLAUDE_SESSION_ID}", session_id or "")
+
+
+# ----------------------------------------------------------------------
+# Shell-block detection + execution helpers
+# ----------------------------------------------------------------------
+
+
+def find_shell_blocks(content: str) -> list[tuple[str, str, bool]]:
+ """Return ``[(full_match, command, inline)]`` for both block forms.
+
+ ``inline`` is ``True`` for ``!`...```, ``False`` for fenced `````! ... `````.
+ Fenced blocks are scanned unconditionally; the inline scan is gated on
+ the cheap ``"!`" in content`` check (mirrors the TS micro-opt: 93% of
+ skills have no inline form, and the inline regex with lookbehind is
+ significantly slower than the fenced one).
+ """
+ out: list[tuple[str, str, bool]] = []
+ for m in _BLOCK_PATTERN.finditer(content):
+ cmd = (m.group(1) or "").strip()
+ if cmd:
+ out.append((m.group(0), cmd, False))
+ if "!`" in content:
+ for m in _INLINE_PATTERN.finditer(content):
+ cmd = (m.group(1) or "").strip()
+ if cmd:
+ out.append((m.group(0), cmd, True))
+ return out
+
+
+def has_shell_blocks(content: str) -> bool:
+ """Cheap check for whether ``content`` contains any shell-exec form."""
+ if "```!" in content:
+ return True
+ if "!`" in content and _INLINE_PATTERN.search(content) is not None:
+ return True
+ return False
+
+
+def format_shell_output(stdout: str, stderr: str, *, inline: bool) -> str:
+ """Format BashTool output the way TS' ``formatBashOutput`` does."""
+ parts: list[str] = []
+ if stdout and stdout.strip():
+ parts.append(stdout.strip())
+ if stderr and stderr.strip():
+ if inline:
+ parts.append(f"[stderr: {stderr.strip()}]")
+ else:
+ parts.append(f"[stderr]\n{stderr.strip()}")
+ return (" " if inline else "\n").join(parts)
+
+
+def format_shell_error(
+ error: BaseException | str,
+ pattern: str,
+ *,
+ inline: bool,
+) -> str:
+ """Format an exception/timeout the way TS' ``formatBashError`` does.
+
+ Unlike TS (which raises ``MalformedCommandError`` and lets the caller
+ re-throw), the Python port embeds the formatted error text inline in
+ the rendered prompt. This satisfies the DEV-2 requirement that shell
+ failures "surface as visible errors in the rendered prompt, not
+ silent drops" without crashing the SkillTool call.
+ """
+ msg = str(error) if not isinstance(error, str) else error
+ if inline:
+ return f"[Error: {msg}]"
+ return f"[Error]\n{msg}"
+
+
+# ----------------------------------------------------------------------
+# Orchestrator
+# ----------------------------------------------------------------------
+
+
+# Signature: (command:str, inline:bool) -> rendered text to splice in.
+# Returning a string lets the executor format errors uniformly without
+# the renderer needing to know about the underlying tool.
+ShellExecutor = Callable[[str, bool], str]
+
+
+def render_skill_prompt(
+ *,
+ body: str,
+ args: str | None,
+ base_dir: Optional[str],
+ argument_names: Sequence[str] = (),
+ session_id: Optional[str] = None,
+ loaded_from: Optional[str] = None,
+ slash_command_name: str = "",
+ shell_executor: ShellExecutor | None = None,
+) -> str:
+ """Apply every per-invocation transform in TS order.
+
+ Order is fixed (and matches TS):
+ 1. base-dir prepend
+ 2. argument substitution
+ 3. ``${CLAUDE_SKILL_DIR}``
+ 4. ``${CLAUDE_SESSION_ID}``
+ 5. embedded shell execution (skipped for ``loaded_from == 'mcp'``)
+
+ ``shell_executor`` is the only side-effecting hook. When ``None`` (or
+ when the skill is MCP-loaded), step 5 is a no-op and any embedded
+ shell blocks survive verbatim in the output. The SkillTool wires a
+ BashTool-backed executor; tests can inject fakes.
+ """
+ content = body or ""
+
+ # 1. Base-dir header
+ content = prepend_base_dir_header(content, base_dir)
+
+ # 2. Arguments. ``append_if_no_placeholder=True`` matches TS and the
+ # existing Python behavior — non-placeholder skills still receive the
+ # raw args appended as ``ARGUMENTS: ``.
+ content = substitute_arguments(
+ content,
+ args,
+ append_if_no_placeholder=True,
+ argument_names=list(argument_names),
+ )
+
+ # 3. ${CLAUDE_SKILL_DIR}
+ content = substitute_skill_dir(content, base_dir)
+
+ # 4. ${CLAUDE_SESSION_ID}
+ content = substitute_session_id(content, session_id)
+
+ # 5. Shell exec — guarded on MCP source (security boundary). MCP
+ # skills come from remote untrusted servers; their markdown bodies
+ # must never trigger local shell execution.
+ if loaded_from == "mcp":
+ if has_shell_blocks(content):
+ logger.debug(
+ "skill %r is MCP-loaded; skipping inline shell execution "
+ "for %d blocks",
+ slash_command_name,
+ len(find_shell_blocks(content)),
+ )
+ return content
+
+ if shell_executor is None:
+ if has_shell_blocks(content):
+ logger.debug(
+ "skill %r contains inline shell blocks but no executor "
+ "was provided; leaving them as-is",
+ slash_command_name,
+ )
+ return content
+
+ blocks = find_shell_blocks(content)
+ if not blocks:
+ return content
+
+ for full_match, command, inline in blocks:
+ try:
+ replacement = shell_executor(command, inline)
+ except Exception as exc: # noqa: BLE001 — we want to surface anything
+ logger.exception(
+ "shell executor crashed for skill %r command %r",
+ slash_command_name,
+ command,
+ )
+ replacement = format_shell_error(exc, full_match, inline=inline)
+ # Use a lambda for the replacement to bypass re-style $-expansion
+ # in str.replace? Python's ``str.replace`` doesn't interpret
+ # backreferences, so a plain string is safe — but we limit to one
+ # replacement per call so multiple identical blocks can each get
+ # their own (potentially different) executor result.
+ content = content.replace(full_match, replacement, 1)
+
+ return content
diff --git a/src/tool_system/context.py b/src/tool_system/context.py
index 515b111ce..d68008f87 100644
--- a/src/tool_system/context.py
+++ b/src/tool_system/context.py
@@ -103,6 +103,12 @@ class ToolContext:
agent_type: str | None = None
tool_use_id: str | None = None
user_modified: bool = False
+ # Identifier of the active query/session. Surfaced to skills (SKILL.md
+ # bodies may reference ``${CLAUDE_SESSION_ID}``) and any other tool
+ # that needs to correlate with persisted session state. ``None`` is
+ # interpreted as "unknown" by callers; substitutions yield an empty
+ # string in that case.
+ session_id: str | None = None
def __post_init__(self) -> None:
self.workspace_root = Path(self.workspace_root).resolve()
diff --git a/src/tool_system/tools/skill.py b/src/tool_system/tools/skill.py
index 2d149d1ae..93dfe68cb 100644
--- a/src/tool_system/tools/skill.py
+++ b/src/tool_system/tools/skill.py
@@ -76,7 +76,11 @@ def _validate_skill_input(tool_input: dict[str, Any], context: ToolContext) -> V
# Remove leading slash if present (for compatibility)
command_name = trimmed.lstrip("/")
- # Look up in the skill registry
+ # Populate the unified registry for the current cwd, then look up.
+ # The registry now includes managed/user/project disk skills (with
+ # nested namespacing like "git:commit"), bundled skills, and any
+ # MCP-provided skills. `get_registered_skill` falls back to bundled
+ # alias matching for back-compat.
from src.skills.loader import get_all_skills, get_registered_skill
get_all_skills(project_root=context.workspace_root)
@@ -168,15 +172,46 @@ def _skill_call(tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
def _run_markdown_skill(skill_name: str, args: str, context: ToolContext) -> ToolResult:
from src.skills.loader import get_all_skills, get_registered_skill
- from src.skills.argument_substitution import substitute_arguments
+ from src.skills.runtime_substitution import render_skill_prompt
+ # Populate / refresh the unified registry for the active cwd. This
+ # pulls in managed / user / project disk skills (with nested
+ # `category:skill` namespacing), bundled skills, and any registered
+ # MCP skills.
get_all_skills(project_root=context.workspace_root)
skill = get_registered_skill(skill_name)
if skill is None:
- return ToolResult(name="Skill", output={"error": f"skill not found: {skill_name}"}, is_error=True)
+ return ToolResult(
+ name="Skill",
+ output={"error": f"skill not found: {skill_name}"},
+ is_error=True,
+ )
- body = skill.markdown_content or ""
- prompt = substitute_arguments(body, args, argument_names=skill.arg_names or [])
+ # Bundled skills supply a callable prompt builder and define their
+ # own substitution semantics; we pass args through and trust the
+ # callable. Disk-loaded skills go through the canonical renderer
+ # which mirrors TS' getPromptForCommand transform pipeline:
+ # 1. base-dir header → 2. arg substitute → 3. ${CLAUDE_SKILL_DIR}
+ # → 4. ${CLAUDE_SESSION_ID} → 5. embedded shell exec (gated on
+ # non-MCP sources, scoped through skill.allowed_tools).
+ if getattr(skill, "get_prompt_for_command", None) is not None:
+ prompt = skill.get_prompt_for_command(args or "")
+ else:
+ body = skill.markdown_content or skill.content or ""
+ base_dir = skill.base_dir or skill.skill_root
+ executor = _make_shell_executor(
+ context, skill.allowed_tools, slash_command_name=f"/{skill_name}"
+ )
+ prompt = render_skill_prompt(
+ body=body,
+ args=args,
+ base_dir=base_dir,
+ argument_names=skill.argument_names,
+ session_id=context.session_id,
+ loaded_from=skill.loaded_from,
+ slash_command_name=f"/{skill_name}",
+ shell_executor=executor,
+ )
# Build context modifier if skill specifies allowed_tools, model, or effort
context_modifier = _build_context_modifier(skill)
@@ -196,6 +231,69 @@ def _run_markdown_skill(skill_name: str, args: str, context: ToolContext) -> Too
)
+def _make_shell_executor(
+ context: ToolContext,
+ allowed_tools: list[str] | None,
+ *,
+ slash_command_name: str,
+):
+ """Return a callable that runs a shell command via BashTool.
+
+ The returned executor matches the
+ ``runtime_substitution.ShellExecutor`` signature: ``(command, inline)
+ -> rendered text``. Errors and non-zero exits are formatted via
+ ``format_shell_error`` / ``format_shell_output`` so the renderer can
+ splice the result back into the prompt without raising.
+
+ The skill's ``allowed_tools`` list is documented for parity with TS
+ (which injects them as ``alwaysAllowRules.command`` for the duration
+ of the call), but the Python BashTool's ``call()`` path bypasses the
+ registry-level permission gate and runs the command directly under
+ the active ``ToolContext`` permission mode. Wiring the
+ ``alwaysAllowRules.command`` injection precisely is tracked as a
+ follow-up; in bypass-permissions sessions (the default for the
+ in-process SkillTool path) commands run unprompted.
+ """
+ from .bash import BashTool
+ from src.skills.runtime_substitution import (
+ format_shell_error,
+ format_shell_output,
+ )
+
+ _ = allowed_tools # acknowledged; precise injection deferred (see docstring)
+
+ def _exec(command: str, inline: bool) -> str:
+ try:
+ tr = BashTool.call({"command": command}, context)
+ except Exception as exc: # noqa: BLE001 — surface every failure
+ return format_shell_error(exc, command, inline=inline)
+
+ output = tr.output if isinstance(tr.output, dict) else {}
+ stdout = str(output.get("stdout", ""))
+ stderr = str(output.get("stderr", ""))
+ exit_code = output.get("exit_code")
+
+ # Treat non-zero exit codes the same way TS' ShellError surfaces
+ # — embed the failure text inline so the model sees what went
+ # wrong, but keep going so the rest of the prompt still renders.
+ if isinstance(exit_code, int) and exit_code != 0:
+ err_text = format_shell_output(stdout, stderr, inline=inline)
+ err_text = err_text or f"command failed (exit {exit_code})"
+ return format_shell_error(err_text, command, inline=inline)
+
+ if tr.is_error:
+ err_text = (
+ format_shell_output(stdout, stderr, inline=inline)
+ or output.get("error")
+ or "command failed"
+ )
+ return format_shell_error(str(err_text), command, inline=inline)
+
+ return format_shell_output(stdout, stderr, inline=inline)
+
+ return _exec
+
+
def _build_context_modifier(skill: Any) -> Any:
"""Build a context modifier closure from skill frontmatter fields.
diff --git a/tests/fixtures/skills/commit-helper/SKILL.md b/tests/fixtures/skills/commit-helper/SKILL.md
new file mode 100644
index 000000000..8fd8bd4aa
--- /dev/null
+++ b/tests/fixtures/skills/commit-helper/SKILL.md
@@ -0,0 +1,11 @@
+---
+description: Generate a conventional-commit message from staged changes.
+allowed-tools: [Bash, Read]
+arguments: [scope]
+argument-hint:
+---
+# Commit Helper
+
+Run `git diff --cached` and produce a conventional-commit message in `$scope` scope.
+Skill base: ${CLAUDE_SKILL_DIR}
+Session: ${CLAUDE_SESSION_ID}
diff --git a/tests/fixtures/skills/frontend/add-component/SKILL.md b/tests/fixtures/skills/frontend/add-component/SKILL.md
new file mode 100644
index 000000000..cce19e4a9
--- /dev/null
+++ b/tests/fixtures/skills/frontend/add-component/SKILL.md
@@ -0,0 +1,8 @@
+---
+description: Scaffold a new React component.
+arguments: [name]
+when_to_use: When the user asks to add a new React component.
+---
+# Add Component
+
+Create `src/components/$name.tsx` and a matching test.
diff --git a/tests/fixtures/skills/lint-py/SKILL.md b/tests/fixtures/skills/lint-py/SKILL.md
new file mode 100644
index 000000000..baaff91a2
--- /dev/null
+++ b/tests/fixtures/skills/lint-py/SKILL.md
@@ -0,0 +1,13 @@
+---
+description: Run lints on Python files.
+paths:
+ - "**/*.py"
+allowed-tools: [Bash]
+---
+# Lint Python
+
+Lint the Python files in this checkout and report findings.
+
+Working directory: !`pwd`
+
+Run ruff and report findings.
diff --git a/tests/test_skills_bundled.py b/tests/test_skills_bundled.py
index becff4ed4..4c3b980c0 100644
--- a/tests/test_skills_bundled.py
+++ b/tests/test_skills_bundled.py
@@ -44,6 +44,14 @@ def test_multiple_skills(self) -> None:
assert len(get_bundled_skills()) == 3
def test_clear(self) -> None:
+ # Test the in-process behavior of `clear_bundled_skills`:
+ # registering a one-off, then clearing, must wipe that one-off
+ # from the registry. We don't assert "len == 0" after clear
+ # because clearing also re-arms the lazy-init flag, so the next
+ # `get_bundled_skills()` call seeds the always-on bundled
+ # catalogue (simplify, debug, loop, stuck, verify-content).
+ # Instead we assert the temporary skill is gone.
+ from src.skills.bundled_skills import _bundled_skills
register_bundled_skill(
BundledSkillDefinition(
name="temp",
@@ -51,9 +59,15 @@ def test_clear(self) -> None:
get_prompt_for_command=lambda a: a,
)
)
- assert len(get_bundled_skills()) == 1
+ assert len(get_bundled_skills()) == 1 # lazy-init suppressed by register
clear_bundled_skills()
- assert len(get_bundled_skills()) == 0
+ # After clear, the in-memory list is empty until the next
+ # consumer triggers lazy-init.
+ assert _bundled_skills == []
+ seeded = get_bundled_skills()
+ names = {s.name for s in seeded}
+ assert "temp" not in names # one-off was wiped
+ assert "simplify" in names # bundled catalogue re-seeded
def test_get_returns_copy(self) -> None:
register_bundled_skill(
diff --git a/tests/test_skills_bundled_catalogue.py b/tests/test_skills_bundled_catalogue.py
new file mode 100644
index 000000000..c2368c590
--- /dev/null
+++ b/tests/test_skills_bundled_catalogue.py
@@ -0,0 +1,272 @@
+"""Tests for DEV-5: bundled-skill catalogue + init orchestrator.
+
+Acceptance criteria:
+1. After init, get_bundled_skills returns ≥ 5 named skills.
+2. SkillTool /simplify returns prompt containing "Phase 1: Identify Changes".
+3. SkillTool /loop "5m hello" → fixed-prompt branch with 5m + hello body.
+4. SkillTool /loop "" → dynamic-rescheduling branch.
+5. SkillTool /debug returns a prompt that contains the debug log path
+ and does not throw on a missing log file.
+6. init_bundled_skills is idempotent (no double-registration).
+"""
+
+from __future__ import annotations
+
+import os
+import tempfile
+import unittest
+from pathlib import Path
+from unittest.mock import patch
+
+from src.skills.bundled import (
+ init_bundled_skills,
+ register_debug_skill,
+ register_loop_skill,
+ register_simplify_skill,
+ register_stuck_skill,
+ register_verify_content_skill,
+ reset_bundled_skills_init_flag,
+)
+from src.skills.bundled.loop import (
+ ParsedLoopArgs,
+ parse_loop_args,
+)
+from src.skills.bundled_skills import (
+ clear_bundled_skills,
+ get_bundled_skills,
+)
+from src.skills.loader import (
+ clear_dynamic_skills,
+ clear_skill_caches,
+ clear_skill_registry,
+)
+from src.tool_system.context import ToolContext
+from src.tool_system.tools import SkillTool
+
+
+def _reset_all() -> None:
+ clear_bundled_skills()
+ reset_bundled_skills_init_flag()
+ clear_skill_caches()
+ clear_dynamic_skills()
+ clear_skill_registry()
+
+
+class TestInitOrchestrator(unittest.TestCase):
+ def setUp(self) -> None:
+ _reset_all()
+
+ def tearDown(self) -> None:
+ _reset_all()
+
+ def test_init_seeds_at_least_five(self) -> None:
+ # AC#1
+ init_bundled_skills()
+ names = {s.name for s in get_bundled_skills()}
+ self.assertGreaterEqual(len(names), 5)
+ for required in ("simplify", "debug", "loop", "stuck", "verify-content"):
+ self.assertIn(required, names)
+
+ def test_init_idempotent(self) -> None:
+ # AC#6
+ init_bundled_skills()
+ first = len(get_bundled_skills())
+ init_bundled_skills()
+ init_bundled_skills()
+ self.assertEqual(len(get_bundled_skills()), first)
+
+
+class TestSimplifySkill(unittest.TestCase):
+ def setUp(self) -> None:
+ _reset_all()
+ self._tmp = tempfile.TemporaryDirectory()
+ self.root = Path(self._tmp.name).resolve()
+
+ def tearDown(self) -> None:
+ _reset_all()
+ self._tmp.cleanup()
+
+ def test_simplify_returns_phase1_marker(self) -> None:
+ # AC#2
+ ctx = ToolContext(workspace_root=self.root)
+ out = SkillTool.call({"skill": "simplify"}, ctx).output
+ self.assertTrue(out["success"])
+ self.assertIn("Phase 1: Identify Changes", out["prompt"])
+
+ def test_simplify_appends_args(self) -> None:
+ ctx = ToolContext(workspace_root=self.root)
+ out = SkillTool.call({"skill": "simplify", "args": "look at /auth"}, ctx).output
+ self.assertIn("Additional Focus", out["prompt"])
+ self.assertIn("look at /auth", out["prompt"])
+
+
+class TestLoopSkillParser(unittest.TestCase):
+ def test_empty_args_dynamic_maintenance(self) -> None:
+ self.assertEqual(parse_loop_args(""), ParsedLoopArgs(mode="dynamic-maintenance"))
+
+ def test_bare_interval_fixed_maintenance(self) -> None:
+ out = parse_loop_args("5m")
+ self.assertEqual(out.mode, "fixed-maintenance")
+ self.assertEqual(out.interval, "5m")
+
+ def test_leading_interval_with_prompt(self) -> None:
+ out = parse_loop_args("5m hello world")
+ self.assertEqual(out.mode, "fixed-prompt")
+ self.assertEqual(out.interval, "5m")
+ self.assertEqual(out.prompt, "hello world")
+
+ def test_trailing_every_clause(self) -> None:
+ out = parse_loop_args("ping the build every 10 minutes")
+ self.assertEqual(out.mode, "fixed-prompt")
+ self.assertEqual(out.interval, "10m")
+ self.assertEqual(out.prompt, "ping the build")
+
+ def test_unit_aliases(self) -> None:
+ # Variants accepted by normalize_interval_unit.
+ for token, expect in (
+ ("3secs", "3s"),
+ ("2 hours", "2h"),
+ ("1day", "1d"),
+ ("10 minute", "10m"),
+ ):
+ out = parse_loop_args(token)
+ self.assertEqual(
+ out, ParsedLoopArgs(mode="fixed-maintenance", interval=expect),
+ f"failed for token={token!r}",
+ )
+
+ def test_invalid_interval_returns_dynamic_prompt(self) -> None:
+ out = parse_loop_args("just some prose with no interval")
+ self.assertEqual(out.mode, "dynamic-prompt")
+ self.assertEqual(out.prompt, "just some prose with no interval")
+
+
+class TestLoopSkillEndToEnd(unittest.TestCase):
+ def setUp(self) -> None:
+ _reset_all()
+ self._tmp = tempfile.TemporaryDirectory()
+ self.root = Path(self._tmp.name).resolve()
+
+ def tearDown(self) -> None:
+ _reset_all()
+ self._tmp.cleanup()
+
+ def test_loop_5m_hello_fixed_prompt_branch(self) -> None:
+ # AC#3
+ ctx = ToolContext(workspace_root=self.root)
+ out = SkillTool.call({"skill": "loop", "args": "5m hello"}, ctx).output
+ self.assertTrue(out["success"])
+ prompt = out["prompt"]
+ self.assertIn("fixed recurring interval", prompt)
+ self.assertIn("Requested interval: 5m", prompt)
+ self.assertIn("hello", prompt)
+ self.assertIn("--- BEGIN PROMPT ---", prompt)
+ self.assertIn("CronCreate", prompt)
+
+ def test_loop_empty_args_dynamic_branch(self) -> None:
+ # AC#4
+ ctx = ToolContext(workspace_root=self.root)
+ out = SkillTool.call({"skill": "loop"}, ctx).output
+ self.assertTrue(out["success"])
+ prompt = out["prompt"]
+ self.assertIn("dynamic rescheduling", prompt)
+ self.assertIn("--- BEGIN MAINTENANCE PROMPT ---", prompt)
+
+
+class TestDebugSkill(unittest.TestCase):
+ def setUp(self) -> None:
+ _reset_all()
+ self._tmp = tempfile.TemporaryDirectory()
+ self.root = Path(self._tmp.name).resolve()
+
+ def tearDown(self) -> None:
+ _reset_all()
+ self._tmp.cleanup()
+
+ def test_debug_prompt_includes_log_path_no_log_file(self) -> None:
+ # AC#5: surfaces log path; no exception on missing file.
+ nonexistent = self.root / "no-such-debug.log"
+ with patch.dict(
+ os.environ,
+ {"CLAUDE_CODE_DEBUG_LOG_PATH": str(nonexistent)},
+ ):
+ ctx = ToolContext(workspace_root=self.root)
+ out = SkillTool.call({"skill": "debug"}, ctx).output
+ self.assertTrue(out["success"])
+ self.assertIn(str(nonexistent), out["prompt"])
+ # The "no debug log exists yet" hint must be in the body.
+ self.assertIn("No debug log exists yet", out["prompt"])
+
+ def test_debug_prompt_with_existing_log(self) -> None:
+ log = self.root / "debug.log"
+ log.write_text("[INFO] hi\n[ERROR] crash\n")
+ with patch.dict(os.environ, {"CLAUDE_CODE_DEBUG_LOG_PATH": str(log)}):
+ ctx = ToolContext(workspace_root=self.root)
+ out = SkillTool.call({"skill": "debug"}, ctx).output
+ self.assertTrue(out["success"])
+ self.assertIn(str(log), out["prompt"])
+ self.assertIn("[ERROR] crash", out["prompt"])
+
+ def test_debug_prompt_includes_user_issue(self) -> None:
+ ctx = ToolContext(workspace_root=self.root)
+ out = SkillTool.call(
+ {"skill": "debug", "args": "auth keeps failing"}, ctx
+ ).output
+ self.assertIn("auth keeps failing", out["prompt"])
+
+
+class TestStuckAndVerifySkills(unittest.TestCase):
+ def setUp(self) -> None:
+ _reset_all()
+ self._tmp = tempfile.TemporaryDirectory()
+ self.root = Path(self._tmp.name).resolve()
+
+ def tearDown(self) -> None:
+ _reset_all()
+ self._tmp.cleanup()
+
+ def test_stuck_renders(self) -> None:
+ ctx = ToolContext(workspace_root=self.root)
+ out = SkillTool.call({"skill": "stuck"}, ctx).output
+ self.assertTrue(out["success"])
+ self.assertIn("Reset and Re-Approach", out["prompt"])
+
+ def test_stuck_with_args_appends_user_context(self) -> None:
+ ctx = ToolContext(workspace_root=self.root)
+ out = SkillTool.call({"skill": "stuck", "args": "tests fail"}, ctx).output
+ self.assertIn("tests fail", out["prompt"])
+ self.assertIn("User Context", out["prompt"])
+
+ def test_verify_content_renders(self) -> None:
+ ctx = ToolContext(workspace_root=self.root)
+ out = SkillTool.call({"skill": "verify-content"}, ctx).output
+ self.assertTrue(out["success"])
+ self.assertIn("Verify Recent Edits Match Intent", out["prompt"])
+
+
+class TestRegisterFunctionsIdempotent(unittest.TestCase):
+ """Each register_*_skill is allowed to be called explicitly; doing
+ so should not double-register if init has already run."""
+
+ def setUp(self) -> None:
+ _reset_all()
+
+ def tearDown(self) -> None:
+ _reset_all()
+
+ def test_individual_register_does_not_double(self) -> None:
+ # Register-by-hand path (used by tests + the init orchestrator).
+ # The bundled_skills.register_bundled_skill IS additive — calling
+ # the same register function twice WILL register twice. The
+ # orchestrator's idempotency comes from the _INITIALIZED flag in
+ # bundled/__init__.py.
+ register_simplify_skill()
+ first_count = len(get_bundled_skills())
+ register_simplify_skill()
+ # NOTE: register_bundled_skill is additive by design. The init
+ # orchestrator (init_bundled_skills) is the idempotent layer.
+ self.assertEqual(len(get_bundled_skills()), first_count + 1)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_skills_dedup_and_paths.py b/tests/test_skills_dedup_and_paths.py
new file mode 100644
index 000000000..c61a1a574
--- /dev/null
+++ b/tests/test_skills_dedup_and_paths.py
@@ -0,0 +1,502 @@
+"""Tests for DEV-4: realpath dedup, bare/policy gates, conditional paths.
+
+Acceptance criteria:
+1. Two same-file skills accessed via different sources collapse to one.
+2. Symlink → SKILL.md does not produce a duplicate (same realpath).
+3. ``CLAUDE_CODE_DISABLE_POLICY_SKILLS`` skips the managed dir.
+4. ``CLAUDE_CODE_BARE_MODE`` skips autodiscovery; only ``--add-dir``.
+5. Conditional ``paths: ["src/**/*.py"]`` activates for `src/foo/bar.py`.
+6. ``discover_skill_dirs_for_paths`` skips a `.claude/skills` dir whose
+ parent is gitignored.
+"""
+
+from __future__ import annotations
+
+import os
+import shutil
+import subprocess
+import tempfile
+import unittest
+from pathlib import Path
+from unittest.mock import patch
+
+from src.skills.bundled_skills import clear_bundled_skills
+from src.skills.create import create_skill
+from src.skills.loader import (
+ _compile_path_spec,
+ _dedup_by_realpath,
+ _get_additional_skill_dirs,
+ _get_file_identity,
+ _is_bare_mode,
+ _is_path_gitignored,
+ _is_restricted_to_plugin_only,
+ _is_skills_policy_disabled,
+ _path_matches_pattern,
+ activate_conditional_skills_for_paths,
+ clear_dynamic_skills,
+ clear_skill_caches,
+ clear_skill_registry,
+ discover_skill_dirs_for_paths,
+ get_skill_dir_commands,
+ load_skills_from_skills_dir,
+)
+from src.skills.model import Skill
+
+
+class _IsolatedHomeMixin:
+ """Provide an isolated $HOME / managed-dir per test so user/managed
+ skill discovery doesn't leak in real disk content."""
+
+ def _isolate_env(self) -> None:
+ self._env_patch = patch.dict(
+ os.environ,
+ {
+ "HOME": str(self._home),
+ "CLAUDE_CONFIG_DIR": str(self._home / ".claude"),
+ "CLAUDE_MANAGED_CONFIG_DIR": str(self._managed),
+ },
+ clear=False,
+ )
+ self._env_patch.start()
+ # Drop any contaminating envs:
+ for k in (
+ "CLAWCODEX_SKILLS_DIR",
+ "CLAUDE_SKILLS_DIR",
+ "CLAWCODEX_MANAGED_SKILLS_DIR",
+ "CLAUDE_CODE_BARE_MODE",
+ "CLAUDE_CODE_ADDITIONAL_DIRECTORIES",
+ "CLAUDE_CODE_DISABLE_POLICY_SKILLS",
+ ):
+ os.environ.pop(k, None)
+
+
+class TestFileIdentity(unittest.TestCase):
+ def test_returns_realpath(self) -> None:
+ with tempfile.TemporaryDirectory() as t:
+ p = Path(t) / "f.txt"
+ p.write_text("x")
+ link = Path(t) / "link.txt"
+ link.symlink_to(p)
+ self.assertEqual(_get_file_identity(str(link)), str(p.resolve()))
+
+ def test_broken_symlink_returns_path_or_none(self) -> None:
+ # `os.path.realpath` on a broken symlink returns the target path
+ # without resolving it; either result is acceptable as long as
+ # we never raise.
+ with tempfile.TemporaryDirectory() as t:
+ broken = Path(t) / "broken"
+ broken.symlink_to(Path(t) / "missing")
+ out = _get_file_identity(str(broken))
+ # Should not raise; may return the unresolved path string.
+ self.assertIsInstance(out, str)
+
+
+class TestDedupByRealpath(unittest.TestCase):
+ def test_same_realpath_drops_later_entries(self) -> None:
+ with tempfile.TemporaryDirectory() as t:
+ sk = Path(t) / "s"
+ sk.mkdir()
+ (sk / "SKILL.md").write_text("---\ndescription: x\n---\nbody")
+ link = Path(t) / "link"
+ link.symlink_to(sk)
+
+ a = Skill(
+ name="x", description="x", source="userSettings",
+ base_dir=str(sk),
+ )
+ b = Skill(
+ name="x", description="x", source="projectSettings",
+ base_dir=str(link),
+ )
+ out = _dedup_by_realpath([a, b])
+ self.assertEqual(len(out), 1)
+ self.assertEqual(out[0].source, "userSettings") # first wins
+
+ def test_different_files_both_kept(self) -> None:
+ with tempfile.TemporaryDirectory() as t:
+ d1, d2 = Path(t) / "a", Path(t) / "b"
+ d1.mkdir(); d2.mkdir()
+ (d1 / "SKILL.md").write_text("body1")
+ (d2 / "SKILL.md").write_text("body2")
+ a = Skill(name="x", description="x", base_dir=str(d1))
+ b = Skill(name="x", description="x", base_dir=str(d2))
+ out = _dedup_by_realpath([a, b])
+ self.assertEqual(len(out), 2)
+
+ def test_no_base_dir_fails_open(self) -> None:
+ a = Skill(name="x", description="x", base_dir=None)
+ b = Skill(name="y", description="y", base_dir=None)
+ out = _dedup_by_realpath([a, b])
+ self.assertEqual(len(out), 2)
+
+
+class TestPolicyAndBareGates(unittest.TestCase):
+ def test_bare_mode_env_signal(self) -> None:
+ with patch.dict(os.environ, {"CLAUDE_CODE_BARE_MODE": "1"}):
+ self.assertTrue(_is_bare_mode())
+ with patch.dict(os.environ, {"CLAUDE_CODE_BARE_MODE": "0"}):
+ self.assertFalse(_is_bare_mode())
+ os.environ.pop("CLAUDE_CODE_BARE_MODE", None)
+ self.assertFalse(_is_bare_mode())
+
+ def test_policy_disabled_env_signal(self) -> None:
+ with patch.dict(os.environ, {"CLAUDE_CODE_DISABLE_POLICY_SKILLS": "true"}):
+ self.assertTrue(_is_skills_policy_disabled())
+ os.environ.pop("CLAUDE_CODE_DISABLE_POLICY_SKILLS", None)
+ self.assertFalse(_is_skills_policy_disabled())
+
+ def test_plugin_only_stub_returns_false(self) -> None:
+ self.assertFalse(_is_restricted_to_plugin_only("skills"))
+
+ def test_additional_dirs_pathsep_split(self) -> None:
+ with patch.dict(
+ os.environ,
+ {"CLAUDE_CODE_ADDITIONAL_DIRECTORIES": f"/a{os.pathsep}/b{os.pathsep}"},
+ ):
+ self.assertEqual(_get_additional_skill_dirs(), ["/a", "/b"])
+ os.environ.pop("CLAUDE_CODE_ADDITIONAL_DIRECTORIES", None)
+ self.assertEqual(_get_additional_skill_dirs(), [])
+
+
+class TestGetSkillDirCommandsBareAndPolicy(_IsolatedHomeMixin, unittest.TestCase):
+ def setUp(self) -> None:
+ self._tmp = tempfile.TemporaryDirectory()
+ self._root = Path(self._tmp.name).resolve()
+ self._home = self._root / "_home"; self._home.mkdir()
+ self._managed = self._root / "_etc"; self._managed.mkdir()
+ self._project = self._root / "proj"; self._project.mkdir()
+ self._isolate_env()
+ clear_skill_caches(); clear_dynamic_skills(); clear_skill_registry()
+
+ def tearDown(self) -> None:
+ self._env_patch.stop()
+ self._tmp.cleanup()
+ clear_skill_caches(); clear_dynamic_skills(); clear_skill_registry()
+
+ def _make_skill(self, parent: Path, name: str, body: str = "body") -> Path:
+ skill_dir = parent / "skills" / name
+ skill_dir.mkdir(parents=True, exist_ok=True)
+ (skill_dir / "SKILL.md").write_text(f"---\ndescription: {name}\n---\n{body}")
+ return skill_dir
+
+ def test_policy_disabled_skips_managed_dir(self) -> None:
+ # AC#3
+ self._make_skill(self._managed / ".claude", "managed-only")
+ with patch.dict(os.environ, {"CLAUDE_CODE_DISABLE_POLICY_SKILLS": "1"}):
+ clear_skill_caches()
+ skills = get_skill_dir_commands(str(self._project))
+ names = {s.name for s in skills}
+ self.assertNotIn("managed-only", names)
+
+ def test_managed_loaded_when_policy_enabled(self) -> None:
+ self._make_skill(self._managed / ".claude", "managed-only")
+ clear_skill_caches()
+ skills = get_skill_dir_commands(str(self._project))
+ names = {s.name for s in skills}
+ self.assertIn("managed-only", names)
+
+ def test_bare_mode_skips_managed_user_project(self) -> None:
+ # AC#4
+ self._make_skill(self._managed / ".claude", "m1")
+ self._make_skill(self._home / ".claude", "u1")
+ self._make_skill(self._project / ".claude", "p1")
+ with patch.dict(os.environ, {"CLAUDE_CODE_BARE_MODE": "1"}):
+ clear_skill_caches()
+ skills = get_skill_dir_commands(str(self._project))
+ # No additional dirs → bare mode returns empty.
+ self.assertEqual(skills, [])
+
+ def test_bare_mode_with_add_dir_loads_additional_only(self) -> None:
+ # AC#4 — bare + --add-dir loads only the explicit dir.
+ self._make_skill(self._managed / ".claude", "m1")
+ self._make_skill(self._home / ".claude", "u1")
+ self._make_skill(self._project / ".claude", "p1")
+ extra = self._root / "extra"
+ self._make_skill(extra / ".claude", "e1")
+ with patch.dict(
+ os.environ,
+ {
+ "CLAUDE_CODE_BARE_MODE": "1",
+ "CLAUDE_CODE_ADDITIONAL_DIRECTORIES": str(extra),
+ },
+ ):
+ clear_skill_caches()
+ skills = get_skill_dir_commands(str(self._project))
+ names = {s.name for s in skills}
+ self.assertEqual(names, {"e1"})
+
+
+class TestSymlinkDedup(_IsolatedHomeMixin, unittest.TestCase):
+ def setUp(self) -> None:
+ self._tmp = tempfile.TemporaryDirectory()
+ self._root = Path(self._tmp.name).resolve()
+ self._home = self._root / "_home"; self._home.mkdir()
+ self._managed = self._root / "_etc"; self._managed.mkdir()
+ self._project = self._root / "proj"; self._project.mkdir()
+ self._isolate_env()
+ clear_skill_caches(); clear_dynamic_skills(); clear_skill_registry()
+
+ def tearDown(self) -> None:
+ self._env_patch.stop()
+ self._tmp.cleanup()
+ clear_skill_caches(); clear_dynamic_skills(); clear_skill_registry()
+
+ def test_symlink_to_project_skill_collapses_to_one(self) -> None:
+ # AC#2: ~/.claude/skills/foo → /.claude/skills/foo
+ proj_skill = self._project / ".claude" / "skills" / "foo"
+ proj_skill.mkdir(parents=True)
+ (proj_skill / "SKILL.md").write_text("---\ndescription: foo\n---\nbody")
+
+ user_skills_dir = self._home / ".claude" / "skills"
+ user_skills_dir.mkdir(parents=True)
+ (user_skills_dir / "foo").symlink_to(proj_skill)
+
+ skills = get_skill_dir_commands(str(self._project))
+ names = [s.name for s in skills]
+ self.assertEqual(names.count("foo"), 1, f"got {names}")
+
+
+class TestConditionalPathsGitignoreSemantics(unittest.TestCase):
+ def setUp(self) -> None:
+ clear_skill_caches(); clear_dynamic_skills()
+
+ def tearDown(self) -> None:
+ clear_skill_caches(); clear_dynamic_skills()
+
+ def test_compile_pathspec_handles_doublestar(self) -> None:
+ spec = _compile_path_spec(["src/**/*.py"])
+ self.assertIsNotNone(spec)
+ self.assertTrue(spec.match_file("src/foo/bar.py"))
+ self.assertTrue(spec.match_file("src/x.py"))
+ self.assertFalse(spec.match_file("docs/x.md"))
+
+ def test_path_matches_pattern_recursive(self) -> None:
+ # Regression for the prior fnmatch-based matcher that missed nested files.
+ self.assertTrue(_path_matches_pattern("src/foo/bar.py", "src/**/*.py"))
+ self.assertFalse(_path_matches_pattern("docs/x.md", "src/**/*.py"))
+
+ def test_activate_conditional_via_pathspec(self) -> None:
+ # AC#5: paths: ["src/**/*.py"] activates for src/foo/bar.py
+ with tempfile.TemporaryDirectory() as t:
+ base = Path(t)
+ sk = base / "skills" / "py-helper"
+ sk.mkdir(parents=True)
+ (sk / "SKILL.md").write_text(
+ "---\n"
+ "description: py helper\n"
+ "paths:\n"
+ " - src/**/*.py\n"
+ "---\n"
+ "body\n"
+ )
+ # Loading puts the skill into _conditional_skills (because
+ # paths is set), not into the returned list — that's the
+ # contract callers rely on.
+ loaded = load_skills_from_skills_dir(
+ str(sk.parent), "projectSettings"
+ )
+ from src.skills.loader import _conditional_skills, _dynamic_skills
+ # Manually move into conditional pool (simulates the
+ # get_skill_dir_commands path):
+ for s in loaded:
+ _conditional_skills[s.name] = s
+
+ cwd = str(base)
+ (base / "src" / "foo").mkdir(parents=True)
+ (base / "src" / "foo" / "bar.py").write_text("x = 1")
+
+ activated = activate_conditional_skills_for_paths(
+ [str(base / "src" / "foo" / "bar.py")], cwd
+ )
+ self.assertIn("py-helper", activated)
+ self.assertIn("py-helper", _dynamic_skills)
+
+ # Same skill should NOT activate for unrelated docs/x.md:
+ _dynamic_skills.clear()
+ _conditional_skills["py-helper"] = loaded[0]
+ (base / "docs").mkdir()
+ (base / "docs" / "x.md").write_text("doc")
+ activated = activate_conditional_skills_for_paths(
+ [str(base / "docs" / "x.md")], cwd
+ )
+ self.assertNotIn("py-helper", activated)
+
+
+class TestActivatedConditionalIsInvokableViaSkillTool(unittest.TestCase):
+ """Regression for QA bug #14.
+
+ Before the fix, ``get_all_skills`` did not merge ``_dynamic_skills``
+ into ``_skill_registry``, so a conditional skill that
+ ``activate_conditional_skills_for_paths`` had just promoted was
+ invisible to ``SkillTool`` (`Unknown skill: ...`, error_code=2).
+ """
+
+ def setUp(self) -> None:
+ clear_skill_caches(); clear_dynamic_skills(); clear_skill_registry()
+ self._tmp = tempfile.TemporaryDirectory()
+ self._project = Path(self._tmp.name).resolve()
+ # Isolate $HOME and managed-dir so user/managed discovery doesn't
+ # contaminate the project under test.
+ self._home = self._project / "_home"; self._home.mkdir()
+ self._managed = self._project / "_etc"; self._managed.mkdir()
+ self._env_patch = patch.dict(
+ os.environ,
+ {
+ "HOME": str(self._home),
+ "CLAUDE_CONFIG_DIR": str(self._home / ".claude"),
+ "CLAUDE_MANAGED_CONFIG_DIR": str(self._managed),
+ },
+ clear=False,
+ )
+ self._env_patch.start()
+ for k in (
+ "CLAWCODEX_SKILLS_DIR",
+ "CLAUDE_SKILLS_DIR",
+ "CLAWCODEX_MANAGED_SKILLS_DIR",
+ "CLAUDE_CODE_BARE_MODE",
+ "CLAUDE_CODE_ADDITIONAL_DIRECTORIES",
+ "CLAUDE_CODE_DISABLE_POLICY_SKILLS",
+ ):
+ os.environ.pop(k, None)
+
+ def tearDown(self) -> None:
+ self._env_patch.stop()
+ self._tmp.cleanup()
+ clear_skill_caches(); clear_dynamic_skills(); clear_skill_registry()
+
+ def test_skilltool_can_invoke_activated_conditional(self) -> None:
+ from src.skills.loader import get_all_skills, get_registered_skill
+ from src.tool_system.context import ToolContext
+ from src.tool_system.tools import SkillTool
+
+ # Place a paths-gated SKILL.md under .claude/skills/lint-py/
+ skill_dir = self._project / ".claude" / "skills" / "lint-py"
+ skill_dir.mkdir(parents=True)
+ (skill_dir / "SKILL.md").write_text(
+ "---\n"
+ "description: Lint Python files\n"
+ "paths:\n"
+ ' - "**/*.py"\n'
+ "---\n"
+ "Lint that file.\n"
+ )
+
+ # Pre-activation: the skill is held back (conditional).
+ names = {s.name for s in get_all_skills(project_root=self._project)}
+ self.assertNotIn("lint-py", names)
+ self.assertIsNone(get_registered_skill("lint-py"))
+
+ # Touching a .py file activates it.
+ (self._project / "src").mkdir()
+ target = self._project / "src" / "foo.py"
+ target.write_text("x = 1")
+ activated = activate_conditional_skills_for_paths(
+ [str(target)], str(self._project)
+ )
+ self.assertIn("lint-py", activated)
+
+ # The SkillTool path must now succeed for /lint-py — that is,
+ # `get_all_skills` must splice `_dynamic_skills` into the
+ # registry so the lookup hits.
+ ctx = ToolContext(workspace_root=self._project)
+ result = SkillTool.call({"skill": "lint-py"}, ctx)
+ self.assertFalse(
+ result.is_error,
+ f"SkillTool returned error after activation: {result.output}",
+ )
+ self.assertTrue(result.output["success"])
+ self.assertIn("Lint that file.", result.output["prompt"])
+
+
+class TestDiscoverSkillDirsGitignore(unittest.TestCase):
+ def setUp(self) -> None:
+ clear_skill_caches(); clear_dynamic_skills()
+ self._tmp = tempfile.TemporaryDirectory()
+ self._cwd = Path(self._tmp.name).resolve()
+ # Initialize a real git repo so `git check-ignore` works.
+ try:
+ subprocess.run(
+ ["git", "init", "-q"], cwd=self._cwd, check=True, timeout=10,
+ )
+ subprocess.run(
+ ["git", "config", "user.email", "t@t.test"],
+ cwd=self._cwd, check=True, timeout=5,
+ )
+ subprocess.run(
+ ["git", "config", "user.name", "t"],
+ cwd=self._cwd, check=True, timeout=5,
+ )
+ self._git_ok = True
+ except (FileNotFoundError, subprocess.SubprocessError):
+ self._git_ok = False
+
+ def tearDown(self) -> None:
+ self._tmp.cleanup()
+ clear_skill_caches(); clear_dynamic_skills()
+
+ def test_skips_gitignored_skills_dir(self) -> None:
+ if not self._git_ok:
+ self.skipTest("git unavailable")
+ # AC#6: node_modules-style gitignored dir's .claude/skills isn't loaded.
+ (self._cwd / ".gitignore").write_text("node_modules/\n")
+ ignored_pkg = self._cwd / "node_modules" / "pkg"
+ ignored_pkg.mkdir(parents=True)
+ skills_dir = ignored_pkg / ".claude" / "skills"
+ skills_dir.mkdir(parents=True)
+ (skills_dir / "evil").mkdir()
+ (skills_dir / "evil" / "SKILL.md").write_text(
+ "---\ndescription: evil\n---\n"
+ )
+ (ignored_pkg / "x.js").write_text("nop")
+
+ new_dirs = discover_skill_dirs_for_paths(
+ [str(ignored_pkg / "x.js")], str(self._cwd)
+ )
+ self.assertNotIn(str(skills_dir), new_dirs)
+
+ def test_loads_non_gitignored_skills_dir(self) -> None:
+ if not self._git_ok:
+ self.skipTest("git unavailable")
+ # Sanity: a non-ignored .claude/skills dir IS picked up.
+ nested = self._cwd / "feature"
+ nested.mkdir()
+ skills_dir = nested / ".claude" / "skills"
+ skills_dir.mkdir(parents=True)
+ (skills_dir / "good").mkdir()
+ (skills_dir / "good" / "SKILL.md").write_text(
+ "---\ndescription: good\n---\n"
+ )
+ (nested / "f.txt").write_text("x")
+ new_dirs = discover_skill_dirs_for_paths(
+ [str(nested / "f.txt")], str(self._cwd)
+ )
+ self.assertIn(str(skills_dir), new_dirs)
+
+
+class TestGitignoreCheckHelper(unittest.TestCase):
+ def test_returns_false_outside_git_repo(self) -> None:
+ with tempfile.TemporaryDirectory() as t:
+ p = Path(t) / "f.txt"
+ p.write_text("x")
+ # No git init → fail open.
+ self.assertFalse(_is_path_gitignored(str(p), t))
+
+ def test_returns_true_for_ignored_path(self) -> None:
+ if shutil.which("git") is None:
+ self.skipTest("git unavailable")
+ with tempfile.TemporaryDirectory() as t:
+ try:
+ subprocess.run(
+ ["git", "init", "-q"], cwd=t, check=True, timeout=10,
+ )
+ except subprocess.SubprocessError:
+ self.skipTest("git init failed")
+ (Path(t) / ".gitignore").write_text("ignored_file\n")
+ (Path(t) / "ignored_file").write_text("x")
+ self.assertTrue(_is_path_gitignored("ignored_file", t))
+ (Path(t) / "kept").write_text("x")
+ self.assertFalse(_is_path_gitignored("kept", t))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_skills_dedup_paths.py b/tests/test_skills_dedup_paths.py
new file mode 100644
index 000000000..6f7b433e0
--- /dev/null
+++ b/tests/test_skills_dedup_paths.py
@@ -0,0 +1,368 @@
+"""Groups D & E — Symlink dedup, bare/policy modes, conditional paths,
+and gitignore-aware dynamic discovery (covers DEV-4).
+"""
+
+from __future__ import annotations
+
+import os
+import subprocess
+import unittest.mock as mock
+from pathlib import Path
+from typing import Iterator
+
+import pytest
+
+from src.skills.bundled_skills import clear_bundled_skills
+from src.skills.loader import (
+ activate_conditional_skills_for_paths,
+ clear_dynamic_skills,
+ clear_skill_caches,
+ clear_skill_registry,
+ discover_skill_dirs_for_paths,
+ get_all_skills,
+ get_conditional_skill_count,
+ get_dynamic_skills,
+ get_skill_dir_commands,
+)
+
+
+@pytest.fixture
+def isolated_home(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[Path]:
+ home = tmp_path / "home"
+ home.mkdir()
+ monkeypatch.setenv("HOME", str(home))
+ for var in (
+ "CLAUDE_CONFIG_DIR",
+ "CLAUDE_MANAGED_CONFIG_DIR",
+ "CLAWCODEX_SKILLS_DIR",
+ "CLAUDE_SKILLS_DIR",
+ "CLAWCODEX_MANAGED_SKILLS_DIR",
+ "CLAUDE_CODE_BARE_MODE",
+ "CLAUDE_CODE_DISABLE_POLICY_SKILLS",
+ "CLAUDE_CODE_ADDITIONAL_DIRECTORIES",
+ ):
+ monkeypatch.delenv(var, raising=False)
+ monkeypatch.setenv("CLAUDE_MANAGED_CONFIG_DIR", str(tmp_path / "managed"))
+ yield home
+
+
+@pytest.fixture(autouse=True)
+def _clean_skill_state() -> Iterator[None]:
+ clear_skill_caches()
+ clear_dynamic_skills()
+ clear_skill_registry()
+ clear_bundled_skills()
+ yield
+ clear_skill_caches()
+ clear_dynamic_skills()
+ clear_skill_registry()
+ clear_bundled_skills()
+
+
+def _write_skill(path: Path, body: str) -> None:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(body, encoding="utf-8")
+
+
+# ======================================================================
+# D — Dedup + bare/policy modes
+# ======================================================================
+
+
+def test_symlinked_skill_collapses_to_single_entry(
+ tmp_path: Path, isolated_home: Path
+) -> None:
+ """Two paths pointing at the same SKILL.md (via symlink) must
+ collapse to one entry after realpath dedup."""
+ project = tmp_path / "proj"
+ real_dir = project / ".claude" / "skills" / "real"
+ _write_skill(real_dir / "SKILL.md", "---\ndescription: real\n---\nbody")
+
+ # Create a sibling symlinked dir that points at the same
+ # SKILL.md-containing folder. After realpath dedup they should
+ # collapse to one entry.
+ link_dir = project / ".claude" / "skills" / "linked"
+ link_dir.symlink_to(real_dir, target_is_directory=True)
+
+ skills = get_skill_dir_commands(str(project))
+ # Both names land in the walker, but the realpath dedup keeps the
+ # first-wins entry only. Total count of unique skills must be 1.
+ seen_files = {
+ os.path.realpath(str(Path(s.base_dir) / "SKILL.md"))
+ for s in skills
+ if s.base_dir
+ }
+ assert len(seen_files) == 1, (
+ f"realpath dedup should collapse symlinked skills, got: "
+ f"{[s.name for s in skills]}"
+ )
+
+
+def test_disable_policy_skills_excludes_managed_dir(
+ tmp_path: Path, isolated_home: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ # Plant a "policy" skill in the managed dir we set via the fixture.
+ managed_root = Path(os.environ["CLAUDE_MANAGED_CONFIG_DIR"])
+ _write_skill(
+ managed_root / ".claude" / "skills" / "policyskill" / "SKILL.md",
+ "---\ndescription: policy\n---\nbody",
+ )
+
+ project = tmp_path / "proj"
+ project.mkdir()
+
+ # Without the disable, the policy skill should appear.
+ skills = get_skill_dir_commands(str(project))
+ assert any(s.name == "policyskill" for s in skills), (
+ f"managed/policy skill should load by default; got: "
+ f"{[s.name for s in skills]}"
+ )
+
+ # With CLAUDE_CODE_DISABLE_POLICY_SKILLS=1, it must NOT.
+ clear_skill_caches()
+ monkeypatch.setenv("CLAUDE_CODE_DISABLE_POLICY_SKILLS", "1")
+ skills2 = get_skill_dir_commands(str(project))
+ assert all(s.name != "policyskill" for s in skills2), (
+ f"CLAUDE_CODE_DISABLE_POLICY_SKILLS=1 must skip the managed "
+ f"dir; got: {[s.name for s in skills2]}"
+ )
+
+
+def test_bare_mode_skips_autodiscovery(
+ tmp_path: Path, isolated_home: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ project = tmp_path / "proj"
+ _write_skill(
+ project / ".claude" / "skills" / "projskill" / "SKILL.md",
+ "---\ndescription: proj\n---\nbody",
+ )
+
+ # Sanity: without bare mode the project skill loads.
+ assert any(s.name == "projskill" for s in get_skill_dir_commands(str(project)))
+
+ # With bare mode and no --add-dir paths, discovery returns empty.
+ clear_skill_caches()
+ monkeypatch.setenv("CLAUDE_CODE_BARE_MODE", "1")
+ assert get_skill_dir_commands(str(project)) == []
+
+
+def test_bare_mode_with_add_dir_only_loads_those(
+ tmp_path: Path, isolated_home: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ project = tmp_path / "proj"
+ # Plant a project skill that should be IGNORED in bare mode.
+ _write_skill(
+ project / ".claude" / "skills" / "projskill" / "SKILL.md",
+ "---\ndescription: proj\n---\nbody",
+ )
+
+ # Plant an additional-dir skill that SHOULD load.
+ extra_dir = tmp_path / "extra"
+ _write_skill(
+ extra_dir / ".claude" / "skills" / "extraskill" / "SKILL.md",
+ "---\ndescription: extra\n---\nbody",
+ )
+
+ monkeypatch.setenv("CLAUDE_CODE_BARE_MODE", "1")
+ monkeypatch.setenv("CLAUDE_CODE_ADDITIONAL_DIRECTORIES", str(extra_dir))
+
+ skills = get_skill_dir_commands(str(project))
+ names = {s.name for s in skills}
+ assert "extraskill" in names, (
+ f"bare mode should still load --add-dir skills; got: {names}"
+ )
+ assert "projskill" not in names, (
+ f"bare mode must skip auto-discovery; got: {names}"
+ )
+
+
+def test_managed_user_project_precedence(
+ tmp_path: Path, isolated_home: Path
+) -> None:
+ """When the same skill name appears in managed + user + project
+ dirs, the unified ``get_all_skills`` merge keeps the highest-
+ priority occurrence (TS order: managed → user → project → bundled).
+ """
+ managed_root = Path(os.environ["CLAUDE_MANAGED_CONFIG_DIR"])
+ user_root = isolated_home
+
+ _write_skill(
+ managed_root / ".claude" / "skills" / "shared" / "SKILL.md",
+ "---\ndescription: from-managed\n---\nM",
+ )
+ _write_skill(
+ user_root / ".claude" / "skills" / "shared" / "SKILL.md",
+ "---\ndescription: from-user\n---\nU",
+ )
+ project = tmp_path / "proj"
+ _write_skill(
+ project / ".claude" / "skills" / "shared" / "SKILL.md",
+ "---\ndescription: from-project\n---\nP",
+ )
+
+ skills = get_all_skills(project_root=project)
+ by_name = {s.name: s for s in skills}
+ # Managed wins per `get_skill_dir_commands` ordering (managed loads
+ # first, dedup is first-wins).
+ assert by_name["shared"].description == "from-managed", (
+ f"precedence regression — expected managed to win, got: "
+ f"{by_name['shared'].description!r}"
+ )
+
+
+# ======================================================================
+# E — Conditional paths + dynamic discovery
+# ======================================================================
+
+
+def test_conditional_skill_held_until_path_matches(
+ tmp_path: Path, isolated_home: Path
+) -> None:
+ project = tmp_path / "proj"
+ _write_skill(
+ project / ".claude" / "skills" / "lintpy" / "SKILL.md",
+ "---\n"
+ "description: lint python\n"
+ "paths:\n"
+ ' - "src/**/*.py"\n'
+ "---\n"
+ "Run ruff",
+ )
+
+ # Initial load: the conditional skill is held back.
+ skills = get_skill_dir_commands(str(project))
+ assert all(s.name != "lintpy" for s in skills), (
+ "conditional skill must NOT appear in unconditional results"
+ )
+ assert get_conditional_skill_count() >= 1
+
+ # A non-matching activation must not flip it on.
+ activated = activate_conditional_skills_for_paths(
+ [str(project / "docs" / "x.md")], str(project)
+ )
+ assert activated == []
+ assert "lintpy" not in {s.name for s in get_dynamic_skills()}
+
+ # A matching activation flips it on.
+ activated = activate_conditional_skills_for_paths(
+ [str(project / "src" / "foo" / "bar.py")], str(project)
+ )
+ assert activated == ["lintpy"]
+ assert "lintpy" in {s.name for s in get_dynamic_skills()}
+
+
+def test_paths_double_glob_treated_as_unconditional(
+ tmp_path: Path, isolated_home: Path
+) -> None:
+ project = tmp_path / "proj"
+ _write_skill(
+ project / ".claude" / "skills" / "always" / "SKILL.md",
+ "---\ndescription: always\npaths:\n - \"**\"\n---\nbody",
+ )
+ skills = get_skill_dir_commands(str(project))
+ names = {s.name for s in skills}
+ assert "always" in names, (
+ "`paths: ['**']` cleans to no-filter (None) and must appear "
+ "in the unconditional list, not the conditional bucket"
+ )
+
+
+def test_path_validity_guards_reject_dotdot_and_absolute(
+ tmp_path: Path, isolated_home: Path
+) -> None:
+ project = tmp_path / "proj"
+ _write_skill(
+ project / ".claude" / "skills" / "guarded" / "SKILL.md",
+ "---\n"
+ "description: guarded\n"
+ "paths:\n"
+ ' - "src/**/*.py"\n'
+ "---\n"
+ "body",
+ )
+
+ # Prime the conditional bucket.
+ get_skill_dir_commands(str(project))
+ assert get_conditional_skill_count() >= 1
+
+ # `..`-escaping path: should be filtered out by the validity guard.
+ above = tmp_path.parent / "outside.py"
+ activated = activate_conditional_skills_for_paths(
+ [str(above)], str(project)
+ )
+ assert activated == [], (
+ "files outside the cwd (relpath starts with '..') must be "
+ "ignored by the activation guard"
+ )
+
+ # Absolute path that ISN'T under cwd similarly drops.
+ activated2 = activate_conditional_skills_for_paths(
+ ["/etc/passwd"], str(project)
+ )
+ assert activated2 == []
+
+ # The skill stays in the conditional bucket.
+ assert "guarded" not in {s.name for s in get_dynamic_skills()}
+
+
+def test_dynamic_discovery_skips_gitignored_dirs(
+ tmp_path: Path, isolated_home: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ """`discover_skill_dirs_for_paths` consults `git check-ignore` to
+ avoid loading skills out of gitignored trees (e.g. node_modules).
+ """
+ project = tmp_path / "proj"
+ project.mkdir()
+
+ # Plant a `.claude/skills` dir under a gitignored path.
+ ignored_skills_dir = project / "node_modules" / "pkg" / ".claude" / "skills"
+ ignored_skills_dir.mkdir(parents=True)
+ (ignored_skills_dir / "noisy" / "SKILL.md").parent.mkdir(parents=True)
+ (ignored_skills_dir / "noisy" / "SKILL.md").write_text(
+ "---\ndescription: noisy\n---\nbody"
+ )
+
+ # Mock `git check-ignore` to say "yes, this is ignored". We mock
+ # at the `subprocess.run` boundary used by `_is_path_gitignored`.
+ def fake_run(cmd, **kwargs): # type: ignore[no-untyped-def]
+ # cmd looks like ["git", "check-ignore", ""]
+ # Anything under node_modules/ is ignored.
+ path = cmd[-1] if cmd else ""
+ rc = 0 if "node_modules" in path else 1
+ return subprocess.CompletedProcess(cmd, rc, "", "")
+
+ file_under_ignored = ignored_skills_dir.parent / "lib.js"
+ file_under_ignored.write_text("//")
+
+ # Walk from a touched file inside the ignored dir.
+ with mock.patch("subprocess.run", side_effect=fake_run):
+ new_dirs = discover_skill_dirs_for_paths(
+ [str(file_under_ignored)], str(project)
+ )
+
+ # The gitignored skills dir must NOT be returned.
+ assert all(
+ "node_modules" not in d for d in new_dirs
+ ), f"gitignored skills dir leaked into discovery: {new_dirs}"
+
+
+def test_dynamic_discovery_includes_non_ignored_dirs(
+ tmp_path: Path, isolated_home: Path
+) -> None:
+ """Sanity counterpart: a non-ignored `.claude/skills` dir is
+ returned by the discovery walk."""
+ project = tmp_path / "proj"
+ project.mkdir()
+ nested_skills_dir = project / "pkg" / ".claude" / "skills"
+ nested_skills_dir.mkdir(parents=True)
+ file_path = project / "pkg" / "lib.js"
+ file_path.write_text("//")
+
+ # `git check-ignore` outside a git repo returns 128 — treated as
+ # not-ignored by `_is_path_gitignored`'s fail-open branch.
+ new_dirs = discover_skill_dirs_for_paths([str(file_path)], str(project))
+ # The nested skills dir is included.
+ assert any(str(nested_skills_dir) == d for d in new_dirs), (
+ f"non-ignored nested .claude/skills dir should appear in "
+ f"discovery; got: {new_dirs}"
+ )
diff --git a/tests/test_skills_e2e.py b/tests/test_skills_e2e.py
new file mode 100644
index 000000000..cb97f5179
--- /dev/null
+++ b/tests/test_skills_e2e.py
@@ -0,0 +1,368 @@
+"""QA-2 — End-to-end real-skill tests.
+
+Drops three example SKILL.md files (under ``tests/fixtures/skills/``)
+into a workspace's ``.claude/skills/`` tree (via tmp-path symlink) and
+exercises them through the full ``SkillTool`` pipeline:
+
+ fixture SKILL.md ──► load via get_all_skills ──► invoke via
+ SkillTool.call ──► rendered prompt with all transforms applied
+
+The fixtures (live on disk under ``tests/fixtures/skills/``):
+
+ - ``commit-helper`` simple skill with args, ``${CLAUDE_SKILL_DIR}``,
+ ``${CLAUDE_SESSION_ID}``, allowed-tools.
+ - ``frontend/add-component``
+ nested-namespace skill (resolves as
+ ``frontend:add-component``); has ``when_to_use``
+ and a named arg.
+ - ``lint-py`` conditional skill (paths-gated) that ALSO has a
+ ``!`pwd``` shell block — exercises both the
+ conditional-activation gate and the shell-exec-
+ in-prompt path end-to-end.
+
+Plus one bundled-skill invocation (``simplify`` from DEV-5) to cover
+the bundled-path wiring through SkillTool.
+
+Acceptance criteria covered:
+ 1. ``pytest tests/test_skills_e2e.py -v`` passes.
+ 2. ``commit-helper`` test asserts arg, ``${CLAUDE_SKILL_DIR}``,
+ ``${CLAUDE_SESSION_ID}``, base-dir header all present.
+ 3. ``frontend:add-component`` invocation by namespaced name succeeds.
+ 4. ``lint-py`` not invokable until
+ ``activate_conditional_skills_for_paths`` matches.
+ 5. ``simplify`` bundled skill exercised end-to-end through SkillTool.
+"""
+
+from __future__ import annotations
+
+import os
+import shutil
+from pathlib import Path
+from typing import Iterator
+
+import pytest
+
+from src.skills.bundled import init_bundled_skills
+from src.skills.bundled_skills import clear_bundled_skills
+from src.skills.loader import (
+ activate_conditional_skills_for_paths,
+ clear_dynamic_skills,
+ clear_skill_caches,
+ clear_skill_registry,
+ get_all_skills,
+ get_conditional_skill_count,
+ get_registered_skill,
+)
+from src.tool_system.context import ToolContext
+from src.tool_system.tools import SkillTool
+
+
+FIXTURES_ROOT = Path(__file__).parent / "fixtures" / "skills"
+FIXTURE_NAMES = ("commit-helper", "frontend", "lint-py")
+
+
+@pytest.fixture
+def isolated_home(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[Path]:
+ """Isolate every env knob that would inject a non-fixture skill dir."""
+ home = tmp_path / "home"
+ home.mkdir()
+ monkeypatch.setenv("HOME", str(home))
+ for var in (
+ "CLAUDE_CONFIG_DIR",
+ "CLAWCODEX_SKILLS_DIR",
+ "CLAUDE_SKILLS_DIR",
+ "CLAWCODEX_MANAGED_SKILLS_DIR",
+ "CLAUDE_CODE_BARE_MODE",
+ "CLAUDE_CODE_DISABLE_POLICY_SKILLS",
+ "CLAUDE_CODE_ADDITIONAL_DIRECTORIES",
+ ):
+ monkeypatch.delenv(var, raising=False)
+ monkeypatch.setenv("CLAUDE_MANAGED_CONFIG_DIR", str(tmp_path / "managed"))
+ yield home
+
+
+@pytest.fixture(autouse=True)
+def _clean_skill_state() -> Iterator[None]:
+ clear_skill_caches()
+ clear_dynamic_skills()
+ clear_skill_registry()
+ clear_bundled_skills()
+ yield
+ clear_skill_caches()
+ clear_dynamic_skills()
+ clear_skill_registry()
+ clear_bundled_skills()
+
+
+@pytest.fixture
+def project_with_fixtures(tmp_path: Path, isolated_home: Path) -> Path:
+ """Construct a workspace whose ``.claude/skills/`` mirrors the
+ fixture catalogue.
+
+ Each fixture under ``tests/fixtures/skills//`` is copied to
+ ``/.claude/skills//`` so the unified loader picks
+ them up via the project-skills walk. We copy (rather than symlink)
+ so the dedup-by-realpath logic doesn't collapse them with the
+ fixtures dir itself.
+ """
+ project = tmp_path / "proj"
+ skills_root = project / ".claude" / "skills"
+ skills_root.mkdir(parents=True)
+
+ for name in FIXTURE_NAMES:
+ src = FIXTURES_ROOT / name
+ dst = skills_root / name
+ shutil.copytree(src, dst)
+
+ return project
+
+
+# ======================================================================
+# 1. ``commit-helper`` — args + ${CLAUDE_SKILL_DIR} + ${CLAUDE_SESSION_ID}
+# + allowed-tools metadata + base-dir header.
+# ======================================================================
+
+
+def test_commit_helper_renders_all_substitutions(
+ project_with_fixtures: Path,
+) -> None:
+ ctx = ToolContext(workspace_root=project_with_fixtures)
+ ctx.session_id = "S-e2e-001"
+
+ result = SkillTool.call(
+ {"skill": "commit-helper", "args": "feat"},
+ ctx,
+ )
+ out = result.output
+ assert out["success"] is True, f"unexpected failure: {out}"
+ prompt = out["prompt"]
+
+ skill_dir = project_with_fixtures / ".claude" / "skills" / "commit-helper"
+ expected_dir_str = str(skill_dir.resolve())
+
+ # (a) base-dir header — exact format pinned by render_skill_prompt.
+ expected_header = f"Base directory for this skill: {expected_dir_str}\n\n"
+ assert prompt.startswith(expected_header), (
+ f"missing/mangled base-dir header.\n"
+ f" expected start: {expected_header!r}\n"
+ f" actual start: {prompt[:200]!r}"
+ )
+
+ # (b) $scope arg substitution — `feat` lands in the body.
+ assert "in `feat` scope" in prompt
+
+ # (c) ${CLAUDE_SKILL_DIR} resolved to the skill's actual abs path.
+ assert f"Skill base: {expected_dir_str}" in prompt
+ assert "${CLAUDE_SKILL_DIR}" not in prompt
+
+ # (d) ${CLAUDE_SESSION_ID} resolved.
+ assert "Session: S-e2e-001" in prompt
+ assert "${CLAUDE_SESSION_ID}" not in prompt
+
+ # (e) allowed-tools metadata propagated to the tool result.
+ assert out["allowedTools"] == ["Bash", "Read"], (
+ f"allowed-tools metadata didn't ride through: {out!r}"
+ )
+ assert out["loadedFrom"] == "project"
+
+
+# ======================================================================
+# 2. ``frontend:add-component`` — nested namespace + when_to_use +
+# named arg substitution.
+# ======================================================================
+
+
+def test_frontend_add_component_namespaced_invocation(
+ project_with_fixtures: Path,
+) -> None:
+ # Listing first — confirms the namespace-construction logic.
+ skills = get_all_skills(project_root=project_with_fixtures)
+ by_name = {s.name: s for s in skills}
+ assert "frontend:add-component" in by_name, (
+ f"expected nested namespace 'frontend:add-component' in {sorted(by_name)}"
+ )
+
+ # `when_to_use` field rides onto the loaded Skill.
+ assert by_name["frontend:add-component"].when_to_use == (
+ "When the user asks to add a new React component."
+ )
+
+ # Invoke through SkillTool by the namespaced name.
+ ctx = ToolContext(workspace_root=project_with_fixtures)
+ result = SkillTool.call(
+ {"skill": "frontend:add-component", "args": "Button"},
+ ctx,
+ )
+ out = result.output
+ assert out["success"] is True
+ assert out["commandName"] == "frontend:add-component"
+ # Named-arg substitution: `$name` → `Button`.
+ assert "src/components/Button.tsx" in out["prompt"]
+
+
+# ======================================================================
+# 3. ``lint-py`` — conditional skill: NOT invokable until
+# ``activate_conditional_skills_for_paths`` matches.
+# Bonus: once activated, the embedded ``!`pwd`` shell block runs
+# end-to-end through BashTool, demonstrating shell-exec coverage.
+# ======================================================================
+
+
+def test_lint_py_not_invokable_until_activated(
+ project_with_fixtures: Path,
+) -> None:
+ # Initial registry walk: ``lint-py`` is conditional, so it should
+ # be held back from the unconditional list.
+ skills = get_all_skills(project_root=project_with_fixtures)
+ names = {s.name for s in skills}
+ assert "lint-py" not in names, (
+ f"conditional skill must NOT appear before path activation; "
+ f"got: {sorted(names)}"
+ )
+ assert get_conditional_skill_count() >= 1, (
+ "conditional bucket should hold lint-py"
+ )
+
+ # Activation with a non-matching path keeps it dormant.
+ activated = activate_conditional_skills_for_paths(
+ [str(project_with_fixtures / "docs" / "x.md")],
+ str(project_with_fixtures),
+ )
+ assert "lint-py" not in activated
+
+ # Activation with a matching path flips it on.
+ py_path = project_with_fixtures / "src" / "foo.py"
+ py_path.parent.mkdir(parents=True, exist_ok=True)
+ py_path.write_text("# placeholder")
+ activated = activate_conditional_skills_for_paths(
+ [str(py_path)], str(project_with_fixtures)
+ )
+ assert "lint-py" in activated, (
+ "conditional skill should activate on matching path; "
+ f"activated={activated}"
+ )
+
+ # After activation, `get_all_skills` must return lint-py — the
+ # unified loader splices `_dynamic_skills` (where activated
+ # conditionals live) into `_skill_registry` so the canonical
+ # SkillTool lookup path sees them.
+ skills_after = get_all_skills(project_root=project_with_fixtures)
+ names_after = {s.name for s in skills_after}
+ assert "lint-py" in names_after, (
+ f"after activation, lint-py should be reachable via "
+ f"get_all_skills; got: {sorted(names_after)}"
+ )
+
+
+def test_lint_py_after_activation_runs_shell_block_through_skilltool(
+ project_with_fixtures: Path,
+) -> None:
+ """Once activated, lint-py's `!`pwd`` shell block must execute
+ end-to-end via BashTool when invoked through SkillTool.
+
+ The fix for QA bug #14 makes `get_all_skills` splice
+ `_dynamic_skills` into `_skill_registry`, so the canonical SkillTool
+ lookup path resolves activated conditional skills without any
+ test-side promotion.
+ """
+ # Prime the registry + activate.
+ get_all_skills(project_root=project_with_fixtures)
+ py_path = project_with_fixtures / "src" / "foo.py"
+ py_path.parent.mkdir(parents=True, exist_ok=True)
+ py_path.write_text("# placeholder")
+ activate_conditional_skills_for_paths(
+ [str(py_path)], str(project_with_fixtures)
+ )
+
+ ctx = ToolContext(workspace_root=project_with_fixtures)
+ result = SkillTool.call({"skill": "lint-py"}, ctx)
+
+ out = result.output
+ assert out["success"] is True, f"lint-py invocation failed: {out}"
+ prompt = out["prompt"]
+
+ # Shell block ran: `!`pwd`` is replaced by the actual cwd path.
+ assert "Working directory: " in prompt
+ # The literal block is gone (substituted).
+ assert "!`pwd`" not in prompt
+ # The substituted output looks like a real path (starts with `/`).
+ # We don't pin the exact path because the test runs in whatever
+ # cwd pytest invoked from; what matters is that *something* path-
+ # shaped landed there.
+ after_marker = prompt.split("Working directory: ", 1)[1].split("\n", 1)[0]
+ assert after_marker.startswith("/"), (
+ f"shell exec should have produced a path; got: {after_marker!r}"
+ )
+
+
+# ======================================================================
+# 4. Bundled skill — exercises DEV-5's init orchestrator wiring through
+# SkillTool. Picks ``simplify`` per the spec example.
+# ======================================================================
+
+
+def test_simplify_bundled_skill_invokable_through_skilltool(
+ project_with_fixtures: Path,
+) -> None:
+ # DEV-5: init orchestrator registers the bundled-skill catalogue.
+ init_bundled_skills()
+
+ # Sanity: simplify is in the registry after init.
+ skills = get_all_skills(project_root=project_with_fixtures)
+ by_name = {s.name: s for s in skills}
+ assert "simplify" in by_name, (
+ f"bundled `simplify` missing after init; got: {sorted(by_name)}"
+ )
+ assert by_name["simplify"].loaded_from == "bundled"
+
+ # Invoke through SkillTool — exercises the bundled `get_prompt_for_command`
+ # branch in `_run_markdown_skill`.
+ ctx = ToolContext(workspace_root=project_with_fixtures)
+ result = SkillTool.call(
+ {"skill": "simplify", "args": "focus on caching"},
+ ctx,
+ )
+ out = result.output
+ assert out["success"] is True, f"bundled simplify failed: {out}"
+ prompt = out["prompt"]
+
+ # Pin the bundled prompt's contract (matches DEV-5 simplify.py):
+ assert "Phase 1: Identify Changes" in prompt
+ assert "Phase 2: Launch Three Review Agents in Parallel" in prompt
+ # User-supplied args land in the "Additional Focus" block.
+ assert "focus on caching" in prompt
+ # Bundled skills don't get a base-dir header (they have no skill_root).
+ assert "Base directory for this skill" not in prompt
+ assert out["loadedFrom"] == "bundled"
+
+
+# ======================================================================
+# 5. Catalogue listing — what the model sees in its system reminder.
+# All three disk fixtures + at least one bundled skill, all reachable
+# from the unified registry.
+# ======================================================================
+
+
+def test_catalogue_lists_disk_and_bundled_skills_together(
+ project_with_fixtures: Path,
+) -> None:
+ init_bundled_skills()
+ skills = get_all_skills(project_root=project_with_fixtures)
+ names = {s.name for s in skills}
+
+ # Disk fixtures (lint-py is conditional, so excluded from the
+ # unconditional listing — that's by design).
+ assert "commit-helper" in names
+ assert "frontend:add-component" in names
+ assert "lint-py" not in names, (
+ "lint-py is conditional and must not appear until activated"
+ )
+
+ # Bundled (DEV-5).
+ assert "simplify" in names
+
+ # Source labels: disk skills report `project`, bundled report `bundled`.
+ by_name = {s.name: s for s in skills}
+ assert by_name["commit-helper"].loaded_from == "project"
+ assert by_name["frontend:add-component"].loaded_from == "project"
+ assert by_name["simplify"].loaded_from == "bundled"
diff --git a/tests/test_skills_frontmatter_validators.py b/tests/test_skills_frontmatter_validators.py
new file mode 100644
index 000000000..7c4a7c2e6
--- /dev/null
+++ b/tests/test_skills_frontmatter_validators.py
@@ -0,0 +1,387 @@
+"""Tests for the YAML frontmatter parser + field validators.
+
+Covers DEV-3 acceptance criteria:
+1. Nested ``hooks:`` parses to a dict on Skill.
+2. Multi-line ``description: |`` joins.
+3. ``model: inherit`` -> None; valid → kept; bogus → warning logged.
+4. ``effort:`` accepts ``low/medium/high/max`` and integers; rejects garbage.
+5. ``allowed-tools: [Bash(git diff:*), Read]`` preserves arg-pattern intact.
+6. Invalid hook shape → ``Skill.hooks is None`` (no exception).
+7. Existing tests in test_skills_loader_ws8.py / test_skills_system.py
+ keep passing (covered separately).
+"""
+
+from __future__ import annotations
+
+import logging
+import unittest
+from pathlib import Path
+
+from src.skills.frontmatter import parse_frontmatter
+from src.skills.loader import (
+ EFFORT_LEVELS,
+ FRONTMATTER_SHELLS,
+ _coerce_allowed_tools,
+ _coerce_description,
+ _coerce_effort,
+ _coerce_hooks,
+ _coerce_model,
+ _coerce_shell,
+ _extract_description_from_markdown,
+ load_skills_from_skills_dir,
+ parse_skill_frontmatter_fields,
+)
+
+
+# ----------------------------------------------------------------------
+# Frontmatter parser (PyYAML-backed)
+# ----------------------------------------------------------------------
+
+
+class TestParseFrontmatterYaml(unittest.TestCase):
+ def test_no_frontmatter_returns_body(self) -> None:
+ out = parse_frontmatter("just a markdown body\nwith lines")
+ self.assertEqual(out.frontmatter, {})
+ self.assertEqual(out.body, "just a markdown body\nwith lines")
+
+ def test_basic_scalars(self) -> None:
+ out = parse_frontmatter("---\ndescription: hi\nversion: 1\n---\nbody")
+ self.assertEqual(out.frontmatter["description"], "hi")
+ # PyYAML coerces numeric-looking values to int
+ self.assertEqual(out.frontmatter["version"], 1)
+ self.assertEqual(out.body, "body")
+
+ def test_inline_list(self) -> None:
+ out = parse_frontmatter(
+ "---\nallowed-tools: [Bash, Read]\n---\nbody"
+ )
+ self.assertEqual(out.frontmatter["allowed-tools"], ["Bash", "Read"])
+
+ def test_hyphen_list(self) -> None:
+ out = parse_frontmatter(
+ "---\nallowed-tools:\n - Bash\n - Read\n---\nbody"
+ )
+ self.assertEqual(out.frontmatter["allowed-tools"], ["Bash", "Read"])
+
+ def test_nested_dict_hooks(self) -> None:
+ text = (
+ "---\n"
+ "hooks:\n"
+ " PostToolUse:\n"
+ " - matcher: Write\n"
+ " hooks:\n"
+ " - type: command\n"
+ " command: ./fmt.sh\n"
+ "---\n"
+ "body\n"
+ )
+ out = parse_frontmatter(text)
+ self.assertIn("hooks", out.frontmatter)
+ self.assertIsInstance(out.frontmatter["hooks"], dict)
+ post = out.frontmatter["hooks"]["PostToolUse"]
+ self.assertEqual(post[0]["matcher"], "Write")
+ self.assertEqual(post[0]["hooks"][0]["type"], "command")
+
+ def test_multiline_string_block(self) -> None:
+ text = (
+ "---\n"
+ "description: |\n"
+ " multi\n"
+ " line\n"
+ "---\n"
+ "body"
+ )
+ out = parse_frontmatter(text)
+ # YAML's ``|`` keeps interior newlines; the trailing newline is
+ # stripped because ``parse_frontmatter`` joins with ``\n`` and the
+ # frontmatter ends right at ``---``.
+ self.assertEqual(out.frontmatter["description"], "multi\nline")
+
+ def test_quoted_string(self) -> None:
+ out = parse_frontmatter(
+ '---\ndescription: "value with: colon"\n---\nbody'
+ )
+ self.assertEqual(out.frontmatter["description"], "value with: colon")
+
+ def test_empty_frontmatter(self) -> None:
+ out = parse_frontmatter("---\n---\nbody")
+ self.assertEqual(out.frontmatter, {})
+ self.assertEqual(out.body, "body")
+
+ def test_unclosed_frontmatter_keeps_body(self) -> None:
+ out = parse_frontmatter("---\ndescription: hi\nbody-line")
+ # No closing ``---`` → treat as no frontmatter, return raw text.
+ self.assertEqual(out.frontmatter, {})
+
+ def test_malformed_yaml_does_not_crash(self) -> None:
+ # Should not raise; should return empty frontmatter.
+ out = parse_frontmatter("---\nfoo: : bad\n---\nbody")
+ self.assertEqual(out.frontmatter, {})
+ self.assertEqual(out.body, "body")
+
+
+# ----------------------------------------------------------------------
+# Description extractor + coercer
+# ----------------------------------------------------------------------
+
+
+class TestDescriptionFallbacks(unittest.TestCase):
+ def test_first_non_empty_line_used(self) -> None:
+ out = _extract_description_from_markdown(
+ "\n\nFirst line text\nSecond\n", "default"
+ )
+ self.assertEqual(out, "First line text")
+
+ def test_heading_stripped(self) -> None:
+ out = _extract_description_from_markdown("# A Heading\nbody", "default")
+ self.assertEqual(out, "A Heading")
+
+ def test_truncates_long(self) -> None:
+ long = "x" * 200
+ out = _extract_description_from_markdown(long, "default")
+ self.assertEqual(len(out), 100)
+ self.assertTrue(out.endswith("..."))
+
+ def test_default_when_blank(self) -> None:
+ self.assertEqual(_extract_description_from_markdown("", "fallback"), "fallback")
+ self.assertEqual(
+ _extract_description_from_markdown("\n\n \n", "fallback"), "fallback"
+ )
+
+ def test_coerce_description_uses_frontmatter(self) -> None:
+ desc, has = _coerce_description("explicit", "ignored", "name")
+ self.assertEqual(desc, "explicit")
+ self.assertTrue(has)
+
+ def test_coerce_description_falls_back(self) -> None:
+ desc, has = _coerce_description(None, "First body line\nrest", "skill-x")
+ self.assertEqual(desc, "First body line")
+ self.assertFalse(has)
+
+ def test_coerce_description_default_fallback(self) -> None:
+ desc, has = _coerce_description(None, "", "skill-x")
+ self.assertEqual(desc, "Skill: skill-x")
+ self.assertFalse(has)
+
+
+# ----------------------------------------------------------------------
+# Field validators
+# ----------------------------------------------------------------------
+
+
+class TestModelCoercion(unittest.TestCase):
+ def test_inherit_returns_none(self) -> None:
+ self.assertIsNone(_coerce_model("inherit"))
+
+ def test_alias_kept(self) -> None:
+ self.assertEqual(_coerce_model("sonnet"), "sonnet")
+
+ def test_canonical_name_kept_silently(self) -> None:
+ with self.assertLogs("src.skills.loader", level="WARNING") as cm:
+ # Have to log _something_ for the assertLogs context to pass; emit
+ # a sentinel so an unrelated absence of warnings doesn't fail.
+ logging.getLogger("src.skills.loader").warning("sentinel")
+ self.assertEqual(_coerce_model("claude-opus-4-5"), "claude-opus-4-5")
+ # only the sentinel; the valid-canonical model should not warn.
+ warnings = [r for r in cm.records if "sentinel" not in r.message]
+ self.assertEqual(warnings, [])
+
+ def test_unknown_model_warns(self) -> None:
+ with self.assertLogs("src.skills.loader", level="WARNING") as cm:
+ out = _coerce_model("totally-bogus")
+ self.assertEqual(out, "totally-bogus") # kept; user might know best
+ self.assertTrue(any("not a recognized" in r.message for r in cm.records))
+
+ def test_empty_returns_none(self) -> None:
+ self.assertIsNone(_coerce_model(""))
+ self.assertIsNone(_coerce_model(None))
+
+
+class TestEffortCoercion(unittest.TestCase):
+ def test_levels_lowercased(self) -> None:
+ for v in EFFORT_LEVELS:
+ self.assertEqual(_coerce_effort(v.upper()), v)
+
+ def test_integer_kept_as_string(self) -> None:
+ self.assertEqual(_coerce_effort(5), "5")
+ self.assertEqual(_coerce_effort("7"), "7")
+
+ def test_invalid_warns_and_drops(self) -> None:
+ with self.assertLogs("src.skills.loader", level="WARNING") as cm:
+ out = _coerce_effort("insane")
+ self.assertIsNone(out)
+ self.assertTrue(any("not a valid level" in r.message for r in cm.records))
+
+ def test_empty_returns_none(self) -> None:
+ self.assertIsNone(_coerce_effort(None))
+ self.assertIsNone(_coerce_effort(""))
+
+
+class TestShellCoercion(unittest.TestCase):
+ def test_bash_accepted(self) -> None:
+ self.assertEqual(_coerce_shell("bash"), "bash")
+ self.assertEqual(_coerce_shell("BASH"), "bash")
+
+ def test_powershell_accepted(self) -> None:
+ self.assertEqual(_coerce_shell("powershell"), "powershell")
+
+ def test_invalid_warns_and_returns_none(self) -> None:
+ with self.assertLogs("src.skills.loader", level="WARNING") as cm:
+ out = _coerce_shell("zsh")
+ self.assertIsNone(out)
+ self.assertTrue(any("not recognized" in r.message for r in cm.records))
+
+ def test_empty_returns_none(self) -> None:
+ self.assertIsNone(_coerce_shell(None))
+ self.assertIsNone(_coerce_shell(""))
+
+
+class TestAllowedToolsCoercion(unittest.TestCase):
+ def test_arg_patterns_preserved(self) -> None:
+ out = _coerce_allowed_tools(["Bash(git diff:*)", "Read"])
+ self.assertEqual(out, ["Bash(git diff:*)", "Read"])
+
+ def test_string_form(self) -> None:
+ out = _coerce_allowed_tools("Bash, Read, Grep")
+ self.assertEqual(out, ["Bash", "Read", "Grep"])
+
+ def test_empty_inputs(self) -> None:
+ self.assertEqual(_coerce_allowed_tools(None), [])
+ self.assertEqual(_coerce_allowed_tools(""), [])
+ self.assertEqual(_coerce_allowed_tools([]), [])
+
+
+class TestHooksCoercion(unittest.TestCase):
+ def test_valid_shape(self) -> None:
+ hooks = {
+ "PostToolUse": [
+ {
+ "matcher": "Write",
+ "hooks": [{"type": "command", "command": "./fmt.sh"}],
+ }
+ ]
+ }
+ out = _coerce_hooks(hooks, skill_name="x")
+ self.assertEqual(out, hooks)
+
+ def test_unknown_event_drops(self) -> None:
+ hooks = {"NotAnEvent": [{"hooks": [{"type": "command"}]}]}
+ out = _coerce_hooks(hooks, skill_name="x")
+ self.assertIsNone(out)
+
+ def test_missing_inner_hooks_list(self) -> None:
+ hooks = {"PostToolUse": [{"matcher": "Write"}]} # no `hooks` key
+ out = _coerce_hooks(hooks, skill_name="x")
+ self.assertIsNone(out)
+
+ def test_inner_hook_missing_type(self) -> None:
+ hooks = {
+ "PostToolUse": [{"hooks": [{"command": "./x.sh"}]}] # no `type`
+ }
+ out = _coerce_hooks(hooks, skill_name="x")
+ self.assertIsNone(out)
+
+ def test_non_dict_input(self) -> None:
+ self.assertIsNone(_coerce_hooks("not a dict", skill_name="x"))
+ self.assertIsNone(_coerce_hooks([], skill_name="x"))
+
+ def test_none_input(self) -> None:
+ self.assertIsNone(_coerce_hooks(None, skill_name="x"))
+
+
+# ----------------------------------------------------------------------
+# Integration: parse_skill_frontmatter_fields dispatches to validators
+# ----------------------------------------------------------------------
+
+
+class TestParseSkillFrontmatterFields(unittest.TestCase):
+ def test_hooks_round_trip(self) -> None:
+ fm = {
+ "description": "hi",
+ "hooks": {
+ "PostToolUse": [
+ {"hooks": [{"type": "command", "command": "./x.sh"}]}
+ ]
+ },
+ }
+ parsed = parse_skill_frontmatter_fields(fm, "body", "x")
+ self.assertIsNotNone(parsed["hooks"])
+ self.assertIn("PostToolUse", parsed["hooks"])
+
+ def test_invalid_hooks_dropped_silently(self) -> None:
+ # AC#6: invalid shape → hooks is None, no exception
+ fm = {"description": "hi", "hooks": {"PostToolUse": [{"matcher": "X"}]}}
+ parsed = parse_skill_frontmatter_fields(fm, "body", "x")
+ self.assertIsNone(parsed["hooks"])
+
+ def test_shell_passes_through(self) -> None:
+ fm = {"description": "hi", "shell": "powershell"}
+ parsed = parse_skill_frontmatter_fields(fm, "body", "x")
+ self.assertEqual(parsed["shell"], "powershell")
+
+ def test_model_inherit_to_none(self) -> None:
+ fm = {"description": "hi", "model": "inherit"}
+ parsed = parse_skill_frontmatter_fields(fm, "body", "x")
+ self.assertIsNone(parsed["model"])
+
+ def test_effort_int_preserved(self) -> None:
+ fm = {"description": "hi", "effort": 5}
+ parsed = parse_skill_frontmatter_fields(fm, "body", "x")
+ self.assertEqual(parsed["effort"], "5")
+
+ def test_effort_invalid_dropped(self) -> None:
+ fm = {"description": "hi", "effort": "insane"}
+ parsed = parse_skill_frontmatter_fields(fm, "body", "x")
+ self.assertIsNone(parsed["effort"])
+
+ def test_description_falls_back_to_first_line(self) -> None:
+ fm = {} # no description
+ body = "First line of the body\nsecond"
+ parsed = parse_skill_frontmatter_fields(fm, body, "x")
+ self.assertEqual(parsed["description"], "First line of the body")
+ self.assertFalse(parsed["has_user_specified_description"])
+
+ def test_allowed_tools_with_arg_pattern(self) -> None:
+ fm = {"allowed-tools": ["Bash(git diff:*)", "Read"]}
+ parsed = parse_skill_frontmatter_fields(fm, "", "x")
+ self.assertEqual(parsed["allowed_tools"], ["Bash(git diff:*)", "Read"])
+
+
+# ----------------------------------------------------------------------
+# End-to-end via load_skills_from_skills_dir — disk SKILL.md path
+# ----------------------------------------------------------------------
+
+
+class TestLoadSkillsWithFrontmatter(unittest.TestCase):
+ def test_skill_carries_hooks_and_shell(self, *, tmp_path=None) -> None:
+ import tempfile
+ with tempfile.TemporaryDirectory() as t:
+ base = Path(t)
+ sk = base / "demo"
+ sk.mkdir()
+ (sk / "SKILL.md").write_text(
+ "---\n"
+ "description: demo\n"
+ "shell: powershell\n"
+ "hooks:\n"
+ " PostToolUse:\n"
+ " - matcher: Write\n"
+ " hooks:\n"
+ " - type: command\n"
+ " command: ./fmt.sh\n"
+ "---\n"
+ "body\n"
+ )
+ skills = load_skills_from_skills_dir(str(base), "projectSettings")
+ self.assertEqual(len(skills), 1)
+ s = skills[0]
+ self.assertEqual(s.shell, "powershell")
+ self.assertIsNotNone(s.hooks)
+ self.assertIn("PostToolUse", s.hooks)
+ self.assertEqual(
+ s.hooks["PostToolUse"][0]["hooks"][0]["command"], "./fmt.sh"
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_skills_frontmatter_yaml.py b/tests/test_skills_frontmatter_yaml.py
new file mode 100644
index 000000000..5d8170e2d
--- /dev/null
+++ b/tests/test_skills_frontmatter_yaml.py
@@ -0,0 +1,315 @@
+"""Group C — Frontmatter parser & field validators (covers DEV-3).
+
+DEV-3 swapped the homegrown frontmatter parser for ``yaml.safe_load`` so
+nested structures (`hooks:`, `shell:`) round-trip, and added per-field
+validators in ``loader.parse_skill_frontmatter_fields`` that drop bad
+values rather than raising. These tests pin the parser/validator
+contracts so a regression that silently crashes-on-bad-input or starts
+swallowing valid input fails loudly.
+"""
+
+from __future__ import annotations
+
+import logging
+from pathlib import Path
+from typing import Iterator
+
+import pytest
+
+from src.skills.bundled_skills import clear_bundled_skills
+from src.skills.frontmatter import parse_frontmatter
+from src.skills.loader import (
+ clear_dynamic_skills,
+ clear_skill_caches,
+ clear_skill_registry,
+ load_skills_from_skills_dir,
+ parse_skill_frontmatter_fields,
+)
+
+
+@pytest.fixture(autouse=True)
+def _clean_skill_state() -> Iterator[None]:
+ clear_skill_caches()
+ clear_dynamic_skills()
+ clear_skill_registry()
+ clear_bundled_skills()
+ yield
+ clear_skill_caches()
+ clear_dynamic_skills()
+ clear_skill_registry()
+ clear_bundled_skills()
+
+
+def _write_skill(path: Path, body: str) -> None:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(body, encoding="utf-8")
+
+
+# ======================================================================
+# Frontmatter parser (the YAML layer that produces the raw dict)
+# ======================================================================
+
+
+class TestFrontmatterParser:
+ def test_nested_hooks_round_trip(self) -> None:
+ md = (
+ "---\n"
+ "description: H\n"
+ "hooks:\n"
+ " PostToolUse:\n"
+ " - matcher: Write\n"
+ " hooks:\n"
+ " - type: command\n"
+ " command: ./scripts/format.sh\n"
+ "---\n"
+ "body\n"
+ )
+ result = parse_frontmatter(md)
+ hooks = result.frontmatter["hooks"]
+ # Round-trips into a real nested dict, not a stringified blob.
+ assert isinstance(hooks, dict)
+ assert "PostToolUse" in hooks
+ post = hooks["PostToolUse"]
+ assert isinstance(post, list)
+ assert post[0]["matcher"] == "Write"
+ assert post[0]["hooks"][0]["type"] == "command"
+ assert post[0]["hooks"][0]["command"] == "./scripts/format.sh"
+
+ def test_multiline_pipe_description_joins(self) -> None:
+ md = (
+ "---\n"
+ "description: |\n"
+ " Line one\n"
+ " Line two\n"
+ "---\n"
+ "body\n"
+ )
+ result = parse_frontmatter(md)
+ # YAML `|` preserves newlines as a single string.
+ desc = result.frontmatter["description"]
+ assert isinstance(desc, str)
+ assert "Line one" in desc
+ assert "Line two" in desc
+
+ def test_allowed_tools_preserves_paren_arg_pattern(self) -> None:
+ # The TS `allowed-tools` syntax allows `Tool(arg-pattern)`. The
+ # YAML parser must keep these as single list entries, not split
+ # them on the `:` inside the parens.
+ md = (
+ "---\n"
+ 'allowed-tools: ["Bash(git diff:*)", "Read"]\n'
+ "---\n"
+ "body\n"
+ )
+ result = parse_frontmatter(md)
+ tools = result.frontmatter["allowed-tools"]
+ assert tools == ["Bash(git diff:*)", "Read"], (
+ f"paren-arg pattern was split or mangled: {tools!r}"
+ )
+
+ def test_malformed_yaml_does_not_raise(self, caplog: pytest.LogCaptureFixture) -> None:
+ # A bad YAML body must not crash the loader — it should return
+ # an empty frontmatter and the body intact. This is the
+ # "never block-load other skills" contract.
+ md = (
+ "---\n"
+ "description: ok\n"
+ " bad: indent: here\n" # malformed
+ "---\n"
+ "body content\n"
+ )
+ with caplog.at_level(logging.DEBUG, logger="src.skills.frontmatter"):
+ result = parse_frontmatter(md)
+ # Must not raise; frontmatter is empty, body is preserved.
+ assert result.frontmatter == {}
+ assert "body content" in result.body
+
+ def test_no_frontmatter_returns_empty_dict_and_full_body(self) -> None:
+ md = "no fence here\nstill no fence\n"
+ result = parse_frontmatter(md)
+ assert result.frontmatter == {}
+ assert result.body == md
+
+
+# ======================================================================
+# parse_skill_frontmatter_fields — coercions / validators
+# ======================================================================
+
+
+class TestFieldValidators:
+ def test_model_inherit_returns_none(self) -> None:
+ result = parse_skill_frontmatter_fields(
+ {"model": "inherit"}, "", "s"
+ )
+ assert result["model"] is None, (
+ "`model: inherit` must yield None so callers fall back to "
+ "the active session model (TS parity)."
+ )
+
+ def test_model_unset_returns_none(self) -> None:
+ assert parse_skill_frontmatter_fields({}, "", "s")["model"] is None
+
+ def test_model_known_value_kept(self) -> None:
+ # A canonical-looking model name is preserved as-is.
+ result = parse_skill_frontmatter_fields(
+ {"model": "claude-sonnet-4-5"}, "", "s"
+ )
+ assert result["model"] == "claude-sonnet-4-5"
+
+ def test_effort_low_kept(self) -> None:
+ result = parse_skill_frontmatter_fields({"effort": "low"}, "", "s")
+ assert result["effort"] == "low"
+
+ def test_effort_medium_high_max_kept(self) -> None:
+ for level in ("medium", "high", "max"):
+ result = parse_skill_frontmatter_fields({"effort": level}, "", "s")
+ assert result["effort"] == level
+
+ def test_effort_invalid_drops_with_warning(
+ self, caplog: pytest.LogCaptureFixture
+ ) -> None:
+ with caplog.at_level(logging.WARNING, logger="src.skills.loader"):
+ result = parse_skill_frontmatter_fields(
+ {"effort": "garbage"}, "", "s"
+ )
+ assert result["effort"] is None, (
+ "invalid effort levels must be dropped (None), not preserved"
+ )
+ # Confirm we logged the rejection — the spec asks us to capture
+ # the warning explicitly with caplog.
+ assert any(
+ "effort" in rec.getMessage().lower() for rec in caplog.records
+ ), "expected a warning log for invalid effort"
+
+ def test_effort_integer_kept_as_string(self) -> None:
+ # TS' parseEffortValue accepts numeric strings too.
+ result = parse_skill_frontmatter_fields({"effort": 3}, "", "s")
+ assert result["effort"] == "3"
+
+ def test_allowed_tools_preserves_paren_arg_pattern(self) -> None:
+ result = parse_skill_frontmatter_fields(
+ {"allowed-tools": ["Bash(git diff:*)"]}, "", "s"
+ )
+ assert result["allowed_tools"] == ["Bash(git diff:*)"], (
+ "paren-arg pattern must be preserved as one entry; "
+ "splitting on `:` would break Bash command rules"
+ )
+
+ def test_allowed_tools_string_form_split_on_comma(self) -> None:
+ result = parse_skill_frontmatter_fields(
+ {"allowed-tools": "Bash, Read"}, "", "s"
+ )
+ # String form is split on commas.
+ assert result["allowed_tools"] == ["Bash", "Read"]
+
+ def test_hooks_valid_event_round_trips_to_dict(self) -> None:
+ hooks_in = {
+ "PostToolUse": [
+ {"matcher": "Write", "hooks": [{"type": "command", "command": "x"}]}
+ ]
+ }
+ result = parse_skill_frontmatter_fields({"hooks": hooks_in}, "", "s")
+ assert result["hooks"] == hooks_in
+
+ def test_hooks_unknown_event_drops_silently(
+ self, caplog: pytest.LogCaptureFixture
+ ) -> None:
+ with caplog.at_level(logging.DEBUG, logger="src.skills.loader"):
+ result = parse_skill_frontmatter_fields(
+ {"hooks": {"BogusEvent": [{"hooks": [{"type": "command"}]}]}},
+ "",
+ "s",
+ )
+ assert result["hooks"] is None
+
+ def test_hooks_malformed_inner_drops(self) -> None:
+ # Inner hooks list missing `type` must drop the whole hooks dict.
+ result = parse_skill_frontmatter_fields(
+ {
+ "hooks": {
+ "PostToolUse": [{"hooks": [{"command": "no-type-field"}]}]
+ }
+ },
+ "",
+ "s",
+ )
+ assert result["hooks"] is None
+
+ def test_shell_bash_kept(self) -> None:
+ result = parse_skill_frontmatter_fields({"shell": "bash"}, "", "s")
+ assert result["shell"] == "bash"
+
+ def test_shell_powershell_kept(self) -> None:
+ result = parse_skill_frontmatter_fields({"shell": "powershell"}, "", "s")
+ assert result["shell"] == "powershell"
+
+ def test_shell_invalid_drops_with_warning(
+ self, caplog: pytest.LogCaptureFixture
+ ) -> None:
+ with caplog.at_level(logging.WARNING, logger="src.skills.loader"):
+ result = parse_skill_frontmatter_fields(
+ {"shell": "fish"}, "", "s"
+ )
+ assert result["shell"] is None
+
+
+# ======================================================================
+# Round-trip via load_skills_from_skills_dir — confirms the new fields
+# (hooks, shell) ride all the way onto the Skill dataclass when a real
+# SKILL.md is loaded from disk.
+# ======================================================================
+
+
+class TestSkillRoundTrip:
+ def test_hooks_field_lands_on_skill(self, tmp_path: Path) -> None:
+ skills_dir = tmp_path / "skills"
+ body = (
+ "---\n"
+ "description: H\n"
+ "hooks:\n"
+ " PostToolUse:\n"
+ " - matcher: Write\n"
+ " hooks:\n"
+ " - type: command\n"
+ " command: ./fmt.sh\n"
+ "---\n"
+ "body\n"
+ )
+ _write_skill(skills_dir / "withhooks" / "SKILL.md", body)
+
+ skills = load_skills_from_skills_dir(str(skills_dir), "projectSettings")
+ assert len(skills) == 1
+ skill = skills[0]
+ assert skill.hooks is not None
+ assert "PostToolUse" in skill.hooks
+ assert skill.hooks["PostToolUse"][0]["matcher"] == "Write"
+
+ def test_shell_field_lands_on_skill(self, tmp_path: Path) -> None:
+ skills_dir = tmp_path / "skills"
+ _write_skill(
+ skills_dir / "shsk" / "SKILL.md",
+ "---\ndescription: S\nshell: bash\n---\nbody\n",
+ )
+ skills = load_skills_from_skills_dir(str(skills_dir), "projectSettings")
+ assert skills[0].shell == "bash"
+
+ def test_invalid_yaml_skill_is_skipped_quietly_others_still_load(
+ self, tmp_path: Path
+ ) -> None:
+ skills_dir = tmp_path / "skills"
+ # Bad skill.
+ _write_skill(
+ skills_dir / "broken" / "SKILL.md",
+ "---\ndescription: ok\n bad: yaml\n---\nbody",
+ )
+ # Good skill alongside it.
+ _write_skill(
+ skills_dir / "good" / "SKILL.md",
+ "---\ndescription: ok\n---\nbody",
+ )
+ skills = load_skills_from_skills_dir(str(skills_dir), "projectSettings")
+ names = {s.name for s in skills}
+ # The "good" skill must still load. The broken one falls back
+ # to an empty frontmatter dict but still loads; what matters
+ # is that one bad skill doesn't kill the whole batch.
+ assert "good" in names
diff --git a/tests/test_skills_runtime_substitution.py b/tests/test_skills_runtime_substitution.py
new file mode 100644
index 000000000..983347fe6
--- /dev/null
+++ b/tests/test_skills_runtime_substitution.py
@@ -0,0 +1,450 @@
+"""Tests for the per-invocation skill prompt-rendering pipeline.
+
+Covers DEV-2: each transform in isolation, the orchestrator combining
+them in TS order, the MCP shell-exec security guard, and shell-error
+formatting (failures embed inline rather than crash the SkillTool).
+"""
+
+from __future__ import annotations
+
+import os
+import tempfile
+import unittest
+from pathlib import Path
+from unittest.mock import patch
+
+from src.skills.bundled_skills import (
+ BundledSkillDefinition,
+ clear_bundled_skills,
+ register_bundled_skill,
+)
+from src.skills.create import create_skill
+from src.skills.loader import (
+ clear_dynamic_skills,
+ clear_skill_caches,
+ clear_skill_registry,
+)
+from src.skills.runtime_substitution import (
+ find_shell_blocks,
+ format_shell_error,
+ format_shell_output,
+ has_shell_blocks,
+ prepend_base_dir_header,
+ render_skill_prompt,
+ substitute_session_id,
+ substitute_skill_dir,
+)
+from src.tool_system.context import ToolContext
+from src.tool_system.tools import SkillTool
+
+
+# ----------------------------------------------------------------------
+# Pure transforms in isolation
+# ----------------------------------------------------------------------
+
+
+class TestPrependBaseDirHeader(unittest.TestCase):
+ def test_prepends_when_base_dir_set(self) -> None:
+ out = prepend_base_dir_header("body", "/skills/foo")
+ self.assertEqual(out, "Base directory for this skill: /skills/foo\n\nbody")
+
+ def test_no_op_when_base_dir_none(self) -> None:
+ self.assertEqual(prepend_base_dir_header("body", None), "body")
+
+ def test_no_op_when_base_dir_empty(self) -> None:
+ self.assertEqual(prepend_base_dir_header("body", ""), "body")
+
+
+class TestSubstituteSkillDir(unittest.TestCase):
+ def test_replaces_placeholder(self) -> None:
+ out = substitute_skill_dir(
+ "cd ${CLAUDE_SKILL_DIR}/scripts", "/abs/skill"
+ )
+ self.assertEqual(out, "cd /abs/skill/scripts")
+
+ def test_replaces_multiple(self) -> None:
+ out = substitute_skill_dir(
+ "${CLAUDE_SKILL_DIR}/a ${CLAUDE_SKILL_DIR}/b", "/x"
+ )
+ self.assertEqual(out, "/x/a /x/b")
+
+ def test_normalizes_backslashes(self) -> None:
+ out = substitute_skill_dir(
+ "${CLAUDE_SKILL_DIR}/script.ps1", r"C:\users\me\skill"
+ )
+ self.assertEqual(out, "C:/users/me/skill/script.ps1")
+
+ def test_no_op_when_base_dir_missing(self) -> None:
+ self.assertEqual(
+ substitute_skill_dir("${CLAUDE_SKILL_DIR}", None),
+ "${CLAUDE_SKILL_DIR}",
+ )
+
+
+class TestSubstituteSessionId(unittest.TestCase):
+ def test_replaces_placeholder(self) -> None:
+ out = substitute_session_id("session=${CLAUDE_SESSION_ID}", "abc-123")
+ self.assertEqual(out, "session=abc-123")
+
+ def test_unset_id_substitutes_empty(self) -> None:
+ # Matches TS' getSessionId() -> '' fallback.
+ out = substitute_session_id("session=${CLAUDE_SESSION_ID}!", None)
+ self.assertEqual(out, "session=!")
+
+
+class TestFindShellBlocks(unittest.TestCase):
+ def test_finds_fenced_block(self) -> None:
+ text = "before\n```!\nls -la\n```\nafter"
+ blocks = find_shell_blocks(text)
+ self.assertEqual(len(blocks), 1)
+ full, cmd, inline = blocks[0]
+ self.assertIn("```!", full)
+ self.assertEqual(cmd, "ls -la")
+ self.assertFalse(inline)
+
+ def test_finds_inline_block(self) -> None:
+ blocks = find_shell_blocks("status: !`git status -sb`")
+ self.assertEqual(len(blocks), 1)
+ _, cmd, inline = blocks[0]
+ self.assertEqual(cmd, "git status -sb")
+ self.assertTrue(inline)
+
+ def test_inline_requires_whitespace_before_bang(self) -> None:
+ # TS' lookbehind blocks `foo!`bar` where ! has no preceding ws.
+ # We replicate by anchoring the inline pattern at start-or-ws.
+ blocks = find_shell_blocks("foo!`nope`")
+ self.assertEqual(blocks, [])
+
+ def test_inline_at_line_start(self) -> None:
+ blocks = find_shell_blocks("!`pwd`")
+ self.assertEqual(len(blocks), 1)
+ self.assertEqual(blocks[0][1], "pwd")
+
+ def test_no_blocks(self) -> None:
+ self.assertEqual(find_shell_blocks("plain text"), [])
+
+ def test_has_shell_blocks_cheap_check(self) -> None:
+ self.assertTrue(has_shell_blocks("```!\nls\n```"))
+ self.assertTrue(has_shell_blocks(" !`ls`"))
+ self.assertFalse(has_shell_blocks("nothing here"))
+ self.assertFalse(has_shell_blocks("foo!`bar`"))
+
+
+class TestFormatShellOutput(unittest.TestCase):
+ def test_stdout_only(self) -> None:
+ self.assertEqual(format_shell_output("hello\n", "", inline=False), "hello")
+
+ def test_stderr_block_form(self) -> None:
+ out = format_shell_output("ok", "warn!", inline=False)
+ self.assertEqual(out, "ok\n[stderr]\nwarn!")
+
+ def test_stderr_inline_form(self) -> None:
+ out = format_shell_output("ok", "warn!", inline=True)
+ self.assertEqual(out, "ok [stderr: warn!]")
+
+ def test_empty(self) -> None:
+ self.assertEqual(format_shell_output("", "", inline=False), "")
+
+
+class TestFormatShellError(unittest.TestCase):
+ def test_block_form(self) -> None:
+ self.assertEqual(
+ format_shell_error("oops", "```!cmd```", inline=False),
+ "[Error]\noops",
+ )
+
+ def test_inline_form(self) -> None:
+ self.assertEqual(
+ format_shell_error("oops", "!`cmd`", inline=True),
+ "[Error: oops]",
+ )
+
+ def test_accepts_exception(self) -> None:
+ out = format_shell_error(RuntimeError("boom"), "x", inline=True)
+ self.assertEqual(out, "[Error: boom]")
+
+
+# ----------------------------------------------------------------------
+# Renderer orchestration
+# ----------------------------------------------------------------------
+
+
+class TestRenderSkillPrompt(unittest.TestCase):
+ def test_combined_substitutions(self) -> None:
+ out = render_skill_prompt(
+ body="Base: ${CLAUDE_SKILL_DIR}\nSession: ${CLAUDE_SESSION_ID}",
+ args="",
+ base_dir="/skills/demo",
+ argument_names=[],
+ session_id="sess-xyz",
+ loaded_from="user",
+ )
+ # Header prepended
+ self.assertTrue(out.startswith("Base directory for this skill: /skills/demo"))
+ # ${CLAUDE_SKILL_DIR} substituted
+ self.assertIn("Base: /skills/demo", out)
+ # ${CLAUDE_SESSION_ID} substituted
+ self.assertIn("Session: sess-xyz", out)
+
+ def test_argument_substitution_runs_after_header(self) -> None:
+ out = render_skill_prompt(
+ body="Hello $name from $1",
+ args="alice somewhere",
+ base_dir="/s/d",
+ argument_names=["name"],
+ session_id=None,
+ loaded_from="user",
+ )
+ self.assertIn("Hello alice from somewhere", out)
+ # Header is still first
+ self.assertTrue(out.startswith("Base directory for this skill: /s/d"))
+
+ def test_arguments_appended_when_no_placeholder(self) -> None:
+ out = render_skill_prompt(
+ body="Static body",
+ args="extra",
+ base_dir=None,
+ argument_names=[],
+ session_id=None,
+ loaded_from="user",
+ )
+ self.assertIn("Static body", out)
+ self.assertIn("ARGUMENTS: extra", out)
+
+ def test_no_base_dir_no_header(self) -> None:
+ out = render_skill_prompt(
+ body="bare body",
+ args="",
+ base_dir=None,
+ argument_names=[],
+ session_id=None,
+ loaded_from="bundled",
+ )
+ self.assertNotIn("Base directory for this skill", out)
+ self.assertEqual(out, "bare body")
+
+ def test_session_id_unset_substitutes_empty(self) -> None:
+ out = render_skill_prompt(
+ body="ID=${CLAUDE_SESSION_ID}",
+ args="",
+ base_dir=None,
+ argument_names=[],
+ session_id=None,
+ loaded_from="user",
+ )
+ self.assertEqual(out, "ID=")
+
+
+class TestRenderSkillPromptShellExecution(unittest.TestCase):
+ def test_shell_executor_called_for_inline(self) -> None:
+ seen: list[tuple[str, bool]] = []
+
+ def fake_exec(cmd: str, inline: bool) -> str:
+ seen.append((cmd, inline))
+ return "fake-output"
+
+ out = render_skill_prompt(
+ body="status: !`git status`",
+ args="",
+ base_dir=None,
+ argument_names=[],
+ session_id=None,
+ loaded_from="user",
+ shell_executor=fake_exec,
+ )
+ self.assertEqual(seen, [("git status", True)])
+ self.assertIn("fake-output", out)
+ self.assertNotIn("!`git status`", out)
+
+ def test_shell_executor_called_for_block(self) -> None:
+ seen: list[tuple[str, bool]] = []
+
+ def fake_exec(cmd: str, inline: bool) -> str:
+ seen.append((cmd, inline))
+ return f"<<{cmd}>>"
+
+ out = render_skill_prompt(
+ body="```!\nls -la\n```",
+ args="",
+ base_dir=None,
+ argument_names=[],
+ session_id=None,
+ loaded_from="user",
+ shell_executor=fake_exec,
+ )
+ self.assertEqual(seen, [("ls -la", False)])
+ self.assertIn("<>", out)
+
+ def test_mcp_skill_skips_shell_execution(self) -> None:
+ # Security boundary — MCP skills come from remote untrusted servers
+ # and must never trigger local shell execution.
+ called: list[str] = []
+
+ def fake_exec(cmd: str, inline: bool) -> str:
+ called.append(cmd)
+ return "WOULD-RUN"
+
+ out = render_skill_prompt(
+ body="hi !`evil` bye",
+ args="",
+ base_dir=None,
+ argument_names=[],
+ session_id=None,
+ loaded_from="mcp",
+ shell_executor=fake_exec,
+ )
+ self.assertEqual(called, [])
+ self.assertIn("!`evil`", out) # left intact
+
+ def test_no_executor_leaves_blocks_intact(self) -> None:
+ out = render_skill_prompt(
+ body="run !`whoami` here",
+ args="",
+ base_dir=None,
+ argument_names=[],
+ session_id=None,
+ loaded_from="user",
+ shell_executor=None,
+ )
+ self.assertIn("!`whoami`", out)
+
+ def test_executor_exception_renders_inline_error(self) -> None:
+ def boom(cmd: str, inline: bool) -> str:
+ raise RuntimeError("oops")
+
+ out = render_skill_prompt(
+ body="x !`bad` y",
+ args="",
+ base_dir=None,
+ argument_names=[],
+ session_id=None,
+ loaded_from="user",
+ shell_executor=boom,
+ )
+ # Failures must SURFACE in the rendered prompt, not be silently
+ # dropped. They should also not crash the renderer.
+ self.assertIn("[Error: oops]", out)
+ self.assertNotIn("!`bad`", out)
+
+ def test_transforms_run_in_correct_order(self) -> None:
+ # Shell command should see the post-substitution dir, since ${CLAUDE_SKILL_DIR}
+ # is replaced before shell exec.
+ captured: list[str] = []
+
+ def fake_exec(cmd: str, inline: bool) -> str:
+ captured.append(cmd)
+ return "ran"
+
+ render_skill_prompt(
+ body="check !`stat ${CLAUDE_SKILL_DIR}/x`",
+ args="",
+ base_dir="/abs/skill",
+ argument_names=[],
+ session_id=None,
+ loaded_from="user",
+ shell_executor=fake_exec,
+ )
+ self.assertEqual(captured, ["stat /abs/skill/x"])
+
+
+# ----------------------------------------------------------------------
+# Acceptance criteria — end-to-end via SkillTool
+# ----------------------------------------------------------------------
+
+
+class TestSkillToolRuntimeIntegration(unittest.TestCase):
+ def setUp(self) -> None:
+ self.tmp = tempfile.TemporaryDirectory()
+ self.root = Path(self.tmp.name).resolve()
+
+ def tearDown(self) -> None:
+ clear_skill_caches()
+ clear_dynamic_skills()
+ clear_skill_registry()
+ clear_bundled_skills()
+ self.tmp.cleanup()
+
+ def test_disk_skill_gets_base_dir_header(self) -> None:
+ """AC#2: disk-loaded skill prepends the canonical header."""
+ skills_dir = self.root / ".claude" / "skills"
+ create_skill(
+ directory=skills_dir, name="hello", description="say hi", body="Hi!",
+ )
+ ctx = ToolContext(workspace_root=self.root)
+ out = SkillTool.call({"skill": "hello"}, ctx).output
+ self.assertTrue(out["success"])
+ self.assertTrue(
+ out["prompt"].startswith("Base directory for this skill:"),
+ f"prompt does not start with header: {out['prompt']!r}",
+ )
+ self.assertIn(str(skills_dir / "hello"), out["prompt"])
+ self.assertIn("Hi!", out["prompt"])
+
+ def test_skill_dir_and_session_substitution(self) -> None:
+ """AC#1: ${CLAUDE_SKILL_DIR} and ${CLAUDE_SESSION_ID} both
+ substitute in a real disk-loaded skill via SkillTool."""
+ skills_dir = self.root / ".claude" / "skills"
+ create_skill(
+ directory=skills_dir, name="echo",
+ description="echo placeholders",
+ body="DIR=${CLAUDE_SKILL_DIR}\nSID=${CLAUDE_SESSION_ID}",
+ )
+ ctx = ToolContext(workspace_root=self.root, session_id="my-session-42")
+ out = SkillTool.call({"skill": "echo"}, ctx).output
+ self.assertTrue(out["success"])
+ self.assertIn(f"DIR={skills_dir / 'echo'}", out["prompt"])
+ self.assertIn("SID=my-session-42", out["prompt"])
+
+ def test_bundled_skill_no_header(self) -> None:
+ """AC#3: bundled skill without skill_root should NOT get header."""
+ register_bundled_skill(
+ BundledSkillDefinition(
+ name="b1",
+ description="bundled",
+ get_prompt_for_command=lambda a: f"BUNDLED-PROMPT:{a}",
+ )
+ )
+ ctx = ToolContext(workspace_root=self.root)
+ out = SkillTool.call({"skill": "b1", "args": "x"}, ctx).output
+ self.assertTrue(out["success"])
+ self.assertNotIn("Base directory for this skill", out["prompt"])
+ self.assertIn("BUNDLED-PROMPT:x", out["prompt"])
+
+ def test_argument_substitution_with_header(self) -> None:
+ """AC#4: arg substitution still works after header prepend."""
+ skills_dir = self.root / ".claude" / "skills"
+ create_skill(
+ directory=skills_dir, name="greet", description="greet",
+ arguments=["name"], body="Hello $name from $1",
+ )
+ ctx = ToolContext(workspace_root=self.root)
+ out = SkillTool.call({"skill": "greet", "args": "alice town"}, ctx).output
+ self.assertTrue(out["success"])
+ self.assertTrue(out["prompt"].startswith("Base directory for this skill"))
+ self.assertIn("Hello alice from town", out["prompt"])
+
+ def test_shell_executor_logs_when_no_executor_for_mcp(self) -> None:
+ """AC#5: MCP-loaded skills never trigger inline shell execution."""
+ # Simulate by directly calling the renderer with loaded_from="mcp"
+ # and a tracker executor. The MCP guard runs before any executor
+ # call.
+ called: list[str] = []
+ def trk(cmd: str, inline: bool) -> str:
+ called.append(cmd)
+ return "x"
+
+ out = render_skill_prompt(
+ body="!`whoami`",
+ args="",
+ base_dir=None,
+ argument_names=[],
+ session_id=None,
+ loaded_from="mcp",
+ shell_executor=trk,
+ )
+ self.assertEqual(called, [])
+ self.assertIn("!`whoami`", out)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_skills_shell_exec.py b/tests/test_skills_shell_exec.py
new file mode 100644
index 000000000..cdbdc65b3
--- /dev/null
+++ b/tests/test_skills_shell_exec.py
@@ -0,0 +1,426 @@
+"""Group B' — Shell-execution-in-prompt (covers DEV-2).
+
+Required cases (per QA-1 acceptance criteria #5 and #6):
+
+ - Success (inline ``!`...``` and fenced `````! ... ````` forms).
+ - Failure (non-zero exit) — visible error marker, prompt still renders.
+ - Timeout — visible timeout marker, doesn't block the test.
+ - **MCP-skip** — security boundary. A skill loaded from MCP whose
+ body contains shell blocks must NOT spawn a subprocess. We assert
+ via mock that the executor entrypoint was never called.
+ - Combined transform-order — single body exercising every transform
+ (header + arg sub + var sub + shell exec) and verifying that ``$1``
+ inside `` !`echo $1` `` sees the substituted arg, not the literal
+ ``$1`` (validates DEV-2's transform order matches TS).
+
+Strategy: most cases test ``render_skill_prompt`` directly with a fake
+``shell_executor`` callable — pure, fast, no subprocess. The MCP-skip
+case is double-layered: a unit assertion against the renderer's
+executor mock, and an integration assertion against ``BashTool.call``
+(through ``SkillTool``) so a regression at either layer fails loudly.
+"""
+
+from __future__ import annotations
+
+import unittest.mock as mock
+from pathlib import Path
+from typing import Iterator
+
+import pytest
+
+from src.skills.bundled_skills import clear_bundled_skills
+from src.skills.loader import (
+ clear_dynamic_skills,
+ clear_skill_caches,
+ clear_skill_registry,
+)
+from src.skills.runtime_substitution import (
+ find_shell_blocks,
+ has_shell_blocks,
+ render_skill_prompt,
+)
+from src.tool_system.context import ToolContext
+from src.tool_system.tools import SkillTool
+
+
+@pytest.fixture
+def isolated_home(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[Path]:
+ home = tmp_path / "home"
+ home.mkdir()
+ monkeypatch.setenv("HOME", str(home))
+ for var in (
+ "CLAUDE_CONFIG_DIR",
+ "CLAUDE_MANAGED_CONFIG_DIR",
+ "CLAWCODEX_SKILLS_DIR",
+ "CLAUDE_SKILLS_DIR",
+ "CLAWCODEX_MANAGED_SKILLS_DIR",
+ "CLAUDE_CODE_BARE_MODE",
+ "CLAUDE_CODE_DISABLE_POLICY_SKILLS",
+ "CLAUDE_CODE_ADDITIONAL_DIRECTORIES",
+ ):
+ monkeypatch.delenv(var, raising=False)
+ monkeypatch.setenv("CLAUDE_MANAGED_CONFIG_DIR", str(tmp_path / "managed"))
+ yield home
+
+
+@pytest.fixture(autouse=True)
+def _clean_skill_state() -> Iterator[None]:
+ clear_skill_caches()
+ clear_dynamic_skills()
+ clear_skill_registry()
+ clear_bundled_skills()
+ yield
+ clear_skill_caches()
+ clear_dynamic_skills()
+ clear_skill_registry()
+ clear_bundled_skills()
+
+
+def _write_skill(path: Path, body: str) -> None:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(body, encoding="utf-8")
+
+
+# ======================================================================
+# Block-detection sanity checks (cheap; protect the regex from drift).
+# ======================================================================
+
+
+class TestBlockDetection:
+ def test_inline_block_detected(self) -> None:
+ body = "echo: !`echo hello`"
+ assert has_shell_blocks(body) is True
+ blocks = find_shell_blocks(body)
+ assert len(blocks) == 1
+ full, cmd, inline = blocks[0]
+ assert cmd == "echo hello"
+ assert inline is True
+
+ def test_fenced_block_detected(self) -> None:
+ body = "```!\necho world\n```"
+ assert has_shell_blocks(body) is True
+ blocks = find_shell_blocks(body)
+ assert len(blocks) == 1
+ full, cmd, inline = blocks[0]
+ assert cmd == "echo world"
+ assert inline is False
+
+ def test_inline_requires_leading_whitespace(self) -> None:
+ # The lookbehind protects against false positives like
+ # ``foo!`bar``` — the ``!`` must be at start-of-line or
+ # preceded by whitespace.
+ body = "abc!`echo nope`"
+ # `has_shell_blocks` does a cheap "!`" check then runs the regex
+ # again; for a clean negative we look at the canonical scan:
+ assert find_shell_blocks(body) == []
+
+
+# ======================================================================
+# B'-1. Success — both inline and fenced forms run and substitute.
+# ======================================================================
+
+
+def test_inline_shell_block_success_substitutes_stdout() -> None:
+ calls: list[tuple[str, bool]] = []
+
+ def fake_exec(command: str, inline: bool) -> str:
+ calls.append((command, inline))
+ # Mirror what the real executor returns via format_shell_output:
+ # plain stdout for a clean success.
+ return "hello"
+
+ out = render_skill_prompt(
+ body="echo: !`echo hello`",
+ args=None,
+ base_dir=None,
+ loaded_from="project",
+ shell_executor=fake_exec,
+ )
+ assert calls == [("echo hello", True)]
+ assert out == "echo: hello"
+ # Original literal block is gone.
+ assert "!`echo hello`" not in out
+
+
+def test_fenced_shell_block_success_substitutes_stdout() -> None:
+ calls: list[tuple[str, bool]] = []
+
+ def fake_exec(command: str, inline: bool) -> str:
+ calls.append((command, inline))
+ return "world"
+
+ body = "before\n```!\necho world\n```\nafter"
+ out = render_skill_prompt(
+ body=body,
+ args=None,
+ base_dir=None,
+ loaded_from="project",
+ shell_executor=fake_exec,
+ )
+ assert calls == [("echo world", False)]
+ assert "before" in out and "after" in out
+ assert "world" in out
+ assert "```!" not in out
+
+
+def test_multiple_blocks_each_executed_once() -> None:
+ counter = {"n": 0}
+
+ def fake_exec(command: str, inline: bool) -> str:
+ counter["n"] += 1
+ return f"[{counter['n']}]"
+
+ # The inline regex requires whitespace (or start-of-line) BEFORE
+ # the `!` to avoid matching inside other inline-code spans. So
+ # each block needs leading whitespace; we use a leading space.
+ out = render_skill_prompt(
+ body="A= !`a` B= !`b` C= !`c`",
+ args=None,
+ base_dir=None,
+ loaded_from="project",
+ shell_executor=fake_exec,
+ )
+ # Each unique block replaced separately (replace called with count=1).
+ assert counter["n"] == 3, f"expected 3 exec calls, got {counter['n']} (out={out!r})"
+ # Distinct outputs land in order.
+ assert "A= [1]" in out
+ assert "B= [2]" in out
+ assert "C= [3]" in out
+
+
+# ======================================================================
+# B'-2. Failure (non-zero exit / executor exception) — visible marker.
+# ======================================================================
+
+
+def test_failure_executor_exception_renders_visible_marker() -> None:
+ def crashing_exec(command: str, inline: bool) -> str:
+ raise RuntimeError("simulated non-zero exit (exit 7)")
+
+ body = "before !`exit 7` after"
+ out = render_skill_prompt(
+ body=body,
+ args=None,
+ base_dir=None,
+ loaded_from="project",
+ shell_executor=crashing_exec,
+ )
+ # `format_shell_error(...)` is the marker DEV-2 picked: inline form
+ # produces `[Error: ]`. Capturing the format here pins it so a
+ # silent change to the marker shape will fail this test.
+ assert "[Error:" in out, f"expected visible error marker in: {out!r}"
+ assert "exit 7" in out
+ # Rest of the prompt still renders.
+ assert "before" in out
+ assert "after" in out
+
+
+def test_failure_executor_returns_formatted_error_string() -> None:
+ # The executor (e.g. _make_shell_executor in skill.py) is allowed
+ # to format errors itself and return them as the substitution.
+ # render_skill_prompt should pass that string through unmodified.
+ def errfmt_exec(command: str, inline: bool) -> str:
+ return "[Error: command failed (exit 7)]"
+
+ # `!` requires whitespace (or BOL) in front per _INLINE_PATTERN.
+ body = "x= !`exit 7`"
+ out = render_skill_prompt(
+ body=body,
+ args=None,
+ base_dir=None,
+ loaded_from="project",
+ shell_executor=errfmt_exec,
+ )
+ assert out == "x= [Error: command failed (exit 7)]"
+
+
+# ======================================================================
+# B'-3. Timeout — visible marker, doesn't block.
+# DEV-2 doesn't expose a timeout-override hook on render_skill_prompt;
+# instead the executor itself is responsible for raising on timeout.
+# We simulate by having the fake executor raise TimeoutError, which
+# render_skill_prompt's broad `except Exception` catches and formats.
+# ======================================================================
+
+
+def test_timeout_renders_visible_marker_and_continues() -> None:
+ def timeout_exec(command: str, inline: bool) -> str:
+ raise TimeoutError("command timed out after 1s")
+
+ body = "result: !`sleep 30`"
+ out = render_skill_prompt(
+ body=body,
+ args=None,
+ base_dir=None,
+ loaded_from="project",
+ shell_executor=timeout_exec,
+ )
+ assert "[Error:" in out, f"expected error marker in: {out!r}"
+ assert "timed out" in out
+ # Doesn't block; the surrounding text still renders.
+ assert "result:" in out
+
+
+# ======================================================================
+# B'-4. MCP-skip — the security boundary. DOUBLE-LAYERED:
+# (a) render_skill_prompt with loaded_from="mcp" must NOT call the
+# executor mock, and the literal block must survive.
+# (b) SkillTool.call against an MCP-loaded skill must NOT invoke
+# BashTool.call. Asserted via patch on the BashTool.call entry.
+# A regression at either layer should fail loudly — this is exactly
+# the "fail loudly on regression" requirement from the QA-1 spec.
+# ======================================================================
+
+
+def test_mcp_skill_renderer_does_not_call_shell_executor() -> None:
+ executor = mock.Mock(return_value="should-not-appear")
+ body = "danger: !`whoami`"
+
+ out = render_skill_prompt(
+ body=body,
+ args=None,
+ base_dir=None,
+ loaded_from="mcp", # the security gate
+ shell_executor=executor,
+ )
+
+ # Hard security assertion: zero subprocess invocations.
+ assert executor.call_count == 0, (
+ "SECURITY REGRESSION: render_skill_prompt invoked the shell "
+ "executor for an MCP-loaded skill. The TS port and DEV-2 spec "
+ "require MCP skills to skip shell execution entirely."
+ )
+ # Literal block survives unchanged in the rendered prompt.
+ assert "!`whoami`" in out
+
+
+def test_mcp_skill_through_skilltool_never_calls_bash(
+ tmp_path: Path, isolated_home: Path
+) -> None:
+ """End-to-end version of the MCP-skip security boundary.
+
+ Registers an MCP-loaded skill containing a shell block, invokes
+ through SkillTool, and asserts BashTool.call was never invoked.
+ """
+ from src.skills.bundled_skills import skill_from_mcp_tool
+ from src.skills.loader import _skill_registry
+
+ # MCP-loaded skills go through `get_prompt_for_command` callable
+ # rather than the markdown renderer, so for the security-boundary
+ # test we construct a synthetic disk-style MCP skill directly. We
+ # bypass `register_bundled_skill` because that hard-codes
+ # `loaded_from="bundled"`. This mirrors how a real MCP skill would
+ # arrive in the registry via `get_all_skills` step 6.
+ from src.skills.model import Skill
+ mcp_skill = Skill(
+ name="evil-mcp",
+ description="MCP skill with a shell block",
+ markdown_content="user is !`whoami`",
+ source="mcp:evil",
+ loaded_from="mcp",
+ )
+ _skill_registry["evil-mcp"] = mcp_skill
+
+ project = tmp_path / "proj"
+ project.mkdir()
+ ctx = ToolContext(workspace_root=project)
+
+ # NOTE: SkillTool re-populates the registry on each call via
+ # `get_all_skills`, which would clobber our manual registration.
+ # We patch the source (`src.skills.loader.get_all_skills`) since
+ # it's imported inside the function rather than at module scope.
+ with mock.patch(
+ "src.skills.loader.get_all_skills", lambda **_: None
+ ), mock.patch("src.tool_system.tools.bash.BashTool.call") as bash_call:
+ result = SkillTool.call({"skill": "evil-mcp"}, ctx)
+
+ assert bash_call.call_count == 0, (
+ "SECURITY REGRESSION: SkillTool invoked BashTool for an "
+ "MCP-loaded skill. MCP-sourced skills must never trigger "
+ "local shell execution (TS-port `loadedFrom !== 'mcp'` guard)."
+ )
+ out = result.output
+ assert out["success"] is True
+ # The literal block survives.
+ assert "!`whoami`" in out["prompt"]
+ assert out["loadedFrom"] == "mcp"
+
+
+# ======================================================================
+# B'-5. Combined transform order — every transform in one body.
+# Validates DEV-2's order matches TS:
+# header → arg sub → ${CLAUDE_SKILL_DIR} → ${CLAUDE_SESSION_ID}
+# → shell exec
+# Critical assertion: `$1` inside `!`echo $1`` must be substituted to
+# `world` BEFORE the shell block runs, so the executor receives
+# `echo world` (not the literal `echo $1`).
+# ======================================================================
+
+
+def test_combined_transform_order_arg_sub_runs_before_shell_exec() -> None:
+ received: list[str] = []
+
+ def fake_exec(command: str, inline: bool) -> str:
+ received.append(command)
+ # Echo back the command's last token, simulating real `echo`
+ # behavior closely enough to confirm the substitution worked.
+ return command.split()[-1]
+
+ # `$0` is the 0-indexed shorthand for the first parsed arg (this
+ # impl is 0-indexed: see argument_substitution._repl_shorthand).
+ # Note: `!` needs whitespace before it for the inline regex.
+ body = (
+ "Base: ${CLAUDE_SKILL_DIR} | "
+ "Session: ${CLAUDE_SESSION_ID} | "
+ "Out: !`echo $0`"
+ )
+ out = render_skill_prompt(
+ body=body,
+ args="world",
+ base_dir="/abs/skill",
+ argument_names=[],
+ session_id="sess-42",
+ loaded_from="project",
+ shell_executor=fake_exec,
+ )
+
+ # 1. base-dir header is FIRST
+ assert out.startswith("Base directory for this skill: /abs/skill\n\n")
+
+ # 2. argument substitution applied — the executor must have been
+ # called with `echo world`, not `echo $0`. This is the critical
+ # transform-order assertion (DEV-2 docstring step order:
+ # base-dir prepend → arg sub → ${CLAUDE_SKILL_DIR} →
+ # ${CLAUDE_SESSION_ID} → shell exec).
+ assert received == ["echo world"], (
+ f"transform order regression — shell exec saw {received!r}; "
+ "DEV-2's order is supposed to substitute $0 BEFORE running "
+ "embedded shell blocks (matches TS getPromptForCommand)."
+ )
+
+ # 3 + 4. var subs applied
+ assert "Base: /abs/skill" in out
+ assert "Session: sess-42" in out
+
+ # 5. shell exec output spliced in
+ assert "Out: world" in out
+
+ # No literal placeholders survived.
+ assert "${CLAUDE_SKILL_DIR}" not in out
+ assert "${CLAUDE_SESSION_ID}" not in out
+ assert "!`echo" not in out
+
+
+def test_render_no_executor_leaves_blocks_in_place() -> None:
+ # If no shell_executor is supplied (e.g. a test setup or a
+ # SkillTool wired without bash), shell blocks survive verbatim
+ # rather than crashing the render.
+ body = "before !`echo hi` after"
+ out = render_skill_prompt(
+ body=body,
+ args=None,
+ base_dir=None,
+ loaded_from="project",
+ shell_executor=None,
+ )
+ assert out == body
diff --git a/tests/test_skills_substitutions.py b/tests/test_skills_substitutions.py
new file mode 100644
index 000000000..8d0f931f0
--- /dev/null
+++ b/tests/test_skills_substitutions.py
@@ -0,0 +1,293 @@
+"""Group B — Runtime substitutions (covers DEV-2, var-sub portion).
+
+Tests every transform in ``runtime_substitution.render_skill_prompt`` in
+isolation:
+
+ - base-dir header is prepended for disk skills, omitted for bundled
+ - ``${CLAUDE_SKILL_DIR}`` resolves to the skill's base dir
+ - ``${CLAUDE_SESSION_ID}`` resolves to the active session id (and to
+ the empty string when unknown)
+ - argument substitution still composes correctly with the prepended
+ header (i.e., the order ``header → arg sub`` doesn't break either)
+
+Shell-execution-in-prompt is a separate file (test_skills_shell_exec.py).
+"""
+
+from __future__ import annotations
+
+from pathlib import Path
+from typing import Iterator
+
+import pytest
+
+from src.skills.bundled_skills import (
+ BundledSkillDefinition,
+ clear_bundled_skills,
+ register_bundled_skill,
+)
+from src.skills.loader import (
+ clear_dynamic_skills,
+ clear_skill_caches,
+ clear_skill_registry,
+)
+from src.skills.runtime_substitution import (
+ prepend_base_dir_header,
+ render_skill_prompt,
+ substitute_session_id,
+ substitute_skill_dir,
+)
+from src.tool_system.context import ToolContext
+from src.tool_system.tools import SkillTool
+
+
+@pytest.fixture
+def isolated_home(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[Path]:
+ home = tmp_path / "home"
+ home.mkdir()
+ monkeypatch.setenv("HOME", str(home))
+ for var in (
+ "CLAUDE_CONFIG_DIR",
+ "CLAUDE_MANAGED_CONFIG_DIR",
+ "CLAWCODEX_SKILLS_DIR",
+ "CLAUDE_SKILLS_DIR",
+ "CLAWCODEX_MANAGED_SKILLS_DIR",
+ "CLAUDE_CODE_BARE_MODE",
+ "CLAUDE_CODE_DISABLE_POLICY_SKILLS",
+ "CLAUDE_CODE_ADDITIONAL_DIRECTORIES",
+ ):
+ monkeypatch.delenv(var, raising=False)
+ monkeypatch.setenv("CLAUDE_MANAGED_CONFIG_DIR", str(tmp_path / "managed"))
+ yield home
+
+
+@pytest.fixture(autouse=True)
+def _clean_skill_state() -> Iterator[None]:
+ clear_skill_caches()
+ clear_dynamic_skills()
+ clear_skill_registry()
+ clear_bundled_skills()
+ yield
+ clear_skill_caches()
+ clear_dynamic_skills()
+ clear_skill_registry()
+ clear_bundled_skills()
+
+
+def _write_skill(path: Path, body: str) -> None:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(body, encoding="utf-8")
+
+
+# ======================================================================
+# Pure-function tests (each transform in isolation)
+# ======================================================================
+
+
+class TestPrependBaseDirHeader:
+ def test_with_base_dir_prepends_header(self) -> None:
+ result = prepend_base_dir_header("body", "/path/to/skill")
+ assert result == "Base directory for this skill: /path/to/skill\n\nbody"
+
+ def test_without_base_dir_is_noop(self) -> None:
+ assert prepend_base_dir_header("body", None) == "body"
+ assert prepend_base_dir_header("body", "") == "body"
+
+
+class TestSubstituteSkillDir:
+ def test_replaces_placeholder(self) -> None:
+ result = substitute_skill_dir(
+ "Path: ${CLAUDE_SKILL_DIR}/script.sh", "/abs/skill"
+ )
+ assert result == "Path: /abs/skill/script.sh"
+
+ def test_replaces_all_occurrences(self) -> None:
+ result = substitute_skill_dir(
+ "${CLAUDE_SKILL_DIR} | ${CLAUDE_SKILL_DIR}", "/x"
+ )
+ assert result == "/x | /x"
+
+ def test_no_base_dir_leaves_placeholder(self) -> None:
+ # Bundled skills (no base_dir) keep the literal placeholder
+ # rather than emitting a stray empty-string substitution.
+ assert (
+ substitute_skill_dir("Path: ${CLAUDE_SKILL_DIR}", None)
+ == "Path: ${CLAUDE_SKILL_DIR}"
+ )
+
+ def test_normalizes_backslashes(self) -> None:
+ # Windows compat: backslashes get flipped so embedded shell
+ # commands don't see them as escape sequences.
+ result = substitute_skill_dir("X=${CLAUDE_SKILL_DIR}", r"C:\Users\me\skill")
+ assert "\\" not in result
+ assert "C:/Users/me/skill" in result
+
+
+class TestSubstituteSessionId:
+ def test_replaces_placeholder(self) -> None:
+ assert (
+ substitute_session_id("S=${CLAUDE_SESSION_ID}", "abc-123")
+ == "S=abc-123"
+ )
+
+ def test_unknown_session_substitutes_empty(self) -> None:
+ # Matches TS' falsy-getSessionId() behavior.
+ assert substitute_session_id("S=${CLAUDE_SESSION_ID}", None) == "S="
+ assert substitute_session_id("S=${CLAUDE_SESSION_ID}", "") == "S="
+
+
+# ======================================================================
+# Combined renderer tests (disk skill — full transform chain)
+# ======================================================================
+
+
+def test_render_disk_skill_prepends_base_dir_header() -> None:
+ out = render_skill_prompt(
+ body="hello",
+ args=None,
+ base_dir="/abs/skill",
+ argument_names=[],
+ session_id="sess-1",
+ loaded_from="project",
+ )
+ assert out.startswith("Base directory for this skill: /abs/skill\n\n")
+ assert out.endswith("hello")
+
+
+def test_render_bundled_skill_skips_base_dir_header() -> None:
+ # Bundled skills don't ship a `base_dir` — the header must NOT
+ # appear, mirroring the TS behavior that gates the prepend on the
+ # presence of a `files` directory.
+ out = render_skill_prompt(
+ body="bundled body",
+ args=None,
+ base_dir=None,
+ argument_names=[],
+ session_id="sess-1",
+ loaded_from="bundled",
+ )
+ assert "Base directory for this skill" not in out
+ assert out == "bundled body"
+
+
+def test_render_substitutes_skill_dir_and_session_id_together() -> None:
+ out = render_skill_prompt(
+ body="dir=${CLAUDE_SKILL_DIR} sess=${CLAUDE_SESSION_ID}",
+ args=None,
+ base_dir="/abs/skill",
+ argument_names=[],
+ session_id="sess-xyz",
+ loaded_from="project",
+ )
+ # Header + body with both placeholders resolved.
+ assert "dir=/abs/skill" in out
+ assert "sess=sess-xyz" in out
+ assert out.startswith("Base directory for this skill: /abs/skill")
+
+
+def test_render_argument_substitution_works_after_prepend() -> None:
+ # Body has `$0` (0-indexed shorthand for the first parsed arg);
+ # args provided. After base-dir prepend the body should still see
+ # argument substitution apply correctly.
+ out = render_skill_prompt(
+ body="Hello $0",
+ args="world",
+ base_dir="/abs/skill",
+ argument_names=[],
+ session_id=None,
+ loaded_from="project",
+ )
+ assert "Hello world" in out
+ assert out.startswith("Base directory for this skill:")
+
+
+def test_render_named_argument_substitution_works_after_prepend() -> None:
+ out = render_skill_prompt(
+ body="Hello $name",
+ args="alice",
+ base_dir="/abs/skill",
+ argument_names=["name"],
+ session_id=None,
+ loaded_from="project",
+ )
+ assert "Hello alice" in out
+
+
+# ======================================================================
+# End-to-end through SkillTool — verifies the wiring (not just the
+# helper functions) substitutes everything correctly when invoked the
+# way the model would.
+# ======================================================================
+
+
+def test_skilltool_invocation_substitutes_all_placeholders(
+ tmp_path: Path, isolated_home: Path
+) -> None:
+ project = tmp_path / "proj"
+ skill_dir = project / ".claude" / "skills" / "showctx"
+ _write_skill(
+ skill_dir / "SKILL.md",
+ "---\n"
+ "description: Show context placeholders\n"
+ "arguments: [name]\n"
+ "---\n"
+ "Hi $name from ${CLAUDE_SKILL_DIR} (session ${CLAUDE_SESSION_ID})",
+ )
+
+ ctx = ToolContext(workspace_root=project)
+ ctx.session_id = "S-12345"
+
+ result = SkillTool.call({"skill": "showctx", "args": "ada"}, ctx)
+ out = result.output
+ assert out["success"] is True
+ prompt = out["prompt"]
+
+ # All four expected substitutions:
+ assert prompt.startswith(
+ "Base directory for this skill:"
+ ), f"missing base-dir header in: {prompt!r}"
+ assert str(skill_dir.resolve()) in prompt or str(skill_dir) in prompt
+ assert "Hi ada from" in prompt
+ assert "(session S-12345)" in prompt
+ # Placeholders fully resolved (no literal ${...} survivors).
+ assert "${CLAUDE_SKILL_DIR}" not in prompt
+ assert "${CLAUDE_SESSION_ID}" not in prompt
+
+
+def test_skilltool_unknown_session_id_renders_empty(
+ tmp_path: Path, isolated_home: Path
+) -> None:
+ project = tmp_path / "proj"
+ _write_skill(
+ project / ".claude" / "skills" / "sess" / "SKILL.md",
+ "---\ndescription: shows session\n---\nsession=[${CLAUDE_SESSION_ID}]",
+ )
+
+ ctx = ToolContext(workspace_root=project)
+ # Default ToolContext.session_id is None.
+ assert ctx.session_id is None
+
+ result = SkillTool.call({"skill": "sess"}, ctx)
+ prompt = result.output["prompt"]
+ assert "session=[]" in prompt
+
+
+def test_bundled_skill_invocation_does_not_get_base_dir_header(
+ tmp_path: Path, isolated_home: Path
+) -> None:
+ register_bundled_skill(
+ BundledSkillDefinition(
+ name="bcheck",
+ description="bundled prompt-builder",
+ get_prompt_for_command=lambda args: f"bundled body :: args={args}",
+ )
+ )
+ project = tmp_path / "proj"
+ project.mkdir()
+ ctx = ToolContext(workspace_root=project)
+ result = SkillTool.call({"skill": "bcheck", "args": "x"}, ctx)
+ prompt = result.output["prompt"]
+ # Bundled skills route through their own get_prompt_for_command
+ # callable; the header (which is render_skill_prompt's job) must
+ # not appear.
+ assert "Base directory for this skill" not in prompt
+ assert "bundled body :: args=x" in prompt
diff --git a/tests/test_skills_unified.py b/tests/test_skills_unified.py
new file mode 100644
index 000000000..20c08e929
--- /dev/null
+++ b/tests/test_skills_unified.py
@@ -0,0 +1,256 @@
+"""Group A — Unified registry (covers DEV-1).
+
+Verifies that disk-loaded skills (including nested-namespace skills) and
+bundled skills are reachable through the single ``get_all_skills`` /
+``SkillTool`` entry point. Before DEV-1 the SkillTool only saw the
+narrower ``PromptSkill`` registry; these tests pin the unified path so a
+regression on the wiring fails loudly.
+"""
+
+from __future__ import annotations
+
+import os
+from pathlib import Path
+from typing import Iterator
+
+import pytest
+
+from src.skills.bundled_skills import (
+ BundledSkillDefinition,
+ clear_bundled_skills,
+ register_bundled_skill,
+)
+from src.skills.loader import (
+ clear_dynamic_skills,
+ clear_skill_caches,
+ clear_skill_registry,
+ get_all_skills,
+ get_registered_skill,
+)
+from src.tool_system.context import ToolContext
+from src.tool_system.tools import SkillTool
+
+
+# ----------------------------------------------------------------------
+# Test isolation: every test gets a clean ``HOME`` (so ``~/.claude`` /
+# ``~/.clawcodex`` lookups don't leak from the developer's machine) and
+# every skill cache is cleared before AND after each test.
+# ----------------------------------------------------------------------
+
+
+@pytest.fixture
+def isolated_home(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[Path]:
+ home = tmp_path / "home"
+ home.mkdir()
+ monkeypatch.setenv("HOME", str(home))
+ # Strip every env knob that would inject extra skill dirs.
+ for var in (
+ "CLAUDE_CONFIG_DIR",
+ "CLAUDE_MANAGED_CONFIG_DIR",
+ "CLAWCODEX_SKILLS_DIR",
+ "CLAUDE_SKILLS_DIR",
+ "CLAWCODEX_MANAGED_SKILLS_DIR",
+ "CLAUDE_CODE_BARE_MODE",
+ "CLAUDE_CODE_DISABLE_POLICY_SKILLS",
+ "CLAUDE_CODE_ADDITIONAL_DIRECTORIES",
+ ):
+ monkeypatch.delenv(var, raising=False)
+ # Send the managed-policy lookup to an empty tmp dir so /etc/claude
+ # never affects results.
+ monkeypatch.setenv("CLAUDE_MANAGED_CONFIG_DIR", str(tmp_path / "managed"))
+ yield home
+
+
+@pytest.fixture(autouse=True)
+def _clean_skill_state() -> Iterator[None]:
+ clear_skill_caches()
+ clear_dynamic_skills()
+ clear_skill_registry()
+ clear_bundled_skills()
+ yield
+ clear_skill_caches()
+ clear_dynamic_skills()
+ clear_skill_registry()
+ clear_bundled_skills()
+
+
+def _write_skill(path: Path, body: str) -> None:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(body, encoding="utf-8")
+
+
+# ======================================================================
+# A1. Flat project skill at ``.claude/skills/foo/SKILL.md`` is
+# invokable through SkillTool with skill="foo".
+# ======================================================================
+
+
+def test_flat_project_skill_invokable_via_skilltool(
+ tmp_path: Path, isolated_home: Path
+) -> None:
+ project = tmp_path / "proj"
+ _write_skill(
+ project / ".claude" / "skills" / "foo" / "SKILL.md",
+ "---\ndescription: foo skill\n---\nfoo body",
+ )
+
+ ctx = ToolContext(workspace_root=project)
+ result = SkillTool.call({"skill": "foo"}, ctx)
+ out = result.output
+ assert out["success"] is True, f"expected success, got: {out}"
+ assert out["commandName"] == "foo"
+ assert "foo body" in out["prompt"]
+
+
+# ======================================================================
+# A2. Nested namespace lookup — the previously-broken case from DEV-1.
+# ``.claude/skills/git/commit/SKILL.md`` must be invokable as
+# ``skill: "git:commit"`` through SkillTool.
+# ======================================================================
+
+
+def test_nested_namespace_skill_invokable_as_colon_form(
+ tmp_path: Path, isolated_home: Path
+) -> None:
+ project = tmp_path / "proj"
+ _write_skill(
+ project / ".claude" / "skills" / "git" / "commit" / "SKILL.md",
+ "---\ndescription: git commit skill\n---\nWrite a commit message",
+ )
+
+ ctx = ToolContext(workspace_root=project)
+ result = SkillTool.call({"skill": "git:commit"}, ctx)
+ out = result.output
+ assert out["success"] is True, (
+ "nested-namespace lookup is the previously-broken case from "
+ f"DEV-1; SkillTool must resolve 'git:commit' through the "
+ f"unified registry. Got: {out}"
+ )
+ assert out["commandName"] == "git:commit"
+ assert "Write a commit message" in out["prompt"]
+
+
+def test_nested_namespace_appears_in_get_all_skills(
+ tmp_path: Path, isolated_home: Path
+) -> None:
+ project = tmp_path / "proj"
+ _write_skill(
+ project / ".claude" / "skills" / "deep" / "nested" / "thing" / "SKILL.md",
+ "---\ndescription: deep skill\n---\nbody",
+ )
+
+ skills = get_all_skills(project_root=project)
+ by_name = {s.name: s for s in skills}
+ # `_get_skill_command_name` joins ALL relative-path segments with
+ # `:` (every dir between the skills root and the skill folder
+ # becomes a namespace component).
+ assert "deep:nested:thing" in by_name, (
+ f"expected nested namespace 'deep:nested:thing' in {sorted(by_name)}"
+ )
+
+
+# ======================================================================
+# A3. Bundled skills registered via ``register_bundled_skill`` are
+# discoverable through SkillTool.
+# ======================================================================
+
+
+def test_bundled_skill_discoverable_through_skilltool(
+ tmp_path: Path, isolated_home: Path
+) -> None:
+ register_bundled_skill(
+ BundledSkillDefinition(
+ name="bundled-thing",
+ description="A bundled skill",
+ get_prompt_for_command=lambda args: f"bundled prompt: {args}",
+ )
+ )
+
+ project = tmp_path / "proj"
+ project.mkdir()
+ ctx = ToolContext(workspace_root=project)
+ result = SkillTool.call({"skill": "bundled-thing", "args": "hello"}, ctx)
+ out = result.output
+ assert out["success"] is True
+ assert out["commandName"] == "bundled-thing"
+ assert "bundled prompt: hello" in out["prompt"]
+ assert out["loadedFrom"] == "bundled"
+
+
+# ======================================================================
+# A4. Listing via ``get_all_skills(project_root=tmp)`` returns the union
+# of bundled + disk-loaded skills.
+# ======================================================================
+
+
+def test_get_all_skills_returns_union_of_bundled_and_disk(
+ tmp_path: Path, isolated_home: Path
+) -> None:
+ project = tmp_path / "proj"
+ _write_skill(
+ project / ".claude" / "skills" / "diskskill" / "SKILL.md",
+ "---\ndescription: disk skill\n---\nbody",
+ )
+ register_bundled_skill(
+ BundledSkillDefinition(
+ name="bundledskill",
+ description="bundled",
+ get_prompt_for_command=lambda a: a,
+ )
+ )
+
+ skills = get_all_skills(project_root=project)
+ names = {s.name for s in skills}
+ assert "diskskill" in names, f"missing disk skill in {names}"
+ assert "bundledskill" in names, f"missing bundled skill in {names}"
+
+
+# ======================================================================
+# A5. ``CLAWCODEX_SKILLS_DIR`` + the user-skills bucket are merged into
+# the unified registry — env-var dirs are reachable via SkillTool.
+# ======================================================================
+
+
+def test_env_var_user_skill_dir_is_reachable_via_skilltool(
+ tmp_path: Path, isolated_home: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ user_skills = tmp_path / "extra_user_skills"
+ _write_skill(
+ user_skills / "envskill" / "SKILL.md",
+ "---\ndescription: env skill\n---\nbody-from-env",
+ )
+ monkeypatch.setenv("CLAWCODEX_SKILLS_DIR", str(user_skills))
+
+ project = tmp_path / "proj"
+ project.mkdir()
+ ctx = ToolContext(workspace_root=project)
+ result = SkillTool.call({"skill": "envskill"}, ctx)
+ out = result.output
+ assert out["success"] is True
+ assert "body-from-env" in out["prompt"]
+ assert out["loadedFrom"] == "user"
+
+
+# ======================================================================
+# A6. ``get_registered_skill`` is the public lookup used by SkillTool's
+# validate path. Calling ``get_all_skills`` first must populate it.
+# ======================================================================
+
+
+def test_get_registered_skill_populated_after_get_all_skills(
+ tmp_path: Path, isolated_home: Path
+) -> None:
+ project = tmp_path / "proj"
+ _write_skill(
+ project / ".claude" / "skills" / "lookup-me" / "SKILL.md",
+ "---\ndescription: lookup\n---\nbody",
+ )
+
+ # Before populating, the registry should not have it.
+ assert get_registered_skill("lookup-me") is None
+
+ get_all_skills(project_root=project)
+ found = get_registered_skill("lookup-me")
+ assert found is not None
+ assert found.name == "lookup-me"
+ assert found.markdown_content == "body"
diff --git a/uv.lock b/uv.lock
index a85e345d9..fd638611e 100644
--- a/uv.lock
+++ b/uv.lock
@@ -240,8 +240,10 @@ source = { editable = "." }
dependencies = [
{ name = "anthropic" },
{ name = "openai" },
+ { name = "pathspec" },
{ name = "prompt-toolkit" },
{ name = "python-dotenv" },
+ { name = "pyyaml" },
{ name = "rich" },
{ name = "textual" },
{ name = "tiktoken" },
@@ -260,9 +262,11 @@ requires-dist = [
{ name = "anthropic" },
{ name = "build", marker = "extra == 'dev'", specifier = ">=1.0.0" },
{ name = "openai" },
+ { name = "pathspec", specifier = ">=0.11" },
{ name = "prompt-toolkit" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },
{ name = "python-dotenv" },
+ { name = "pyyaml", specifier = ">=6.0" },
{ name = "rich" },
{ name = "textual", specifier = ">=0.79" },
{ name = "tiktoken", specifier = ">=0.7.0" },
@@ -722,6 +726,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
]
+[[package]]
+name = "pathspec"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" },
+]
+
[[package]]
name = "platformdirs"
version = "4.9.6"
@@ -957,6 +970,70 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" },
]
+[[package]]
+name = "pyyaml"
+version = "6.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" },
+ { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" },
+ { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
+ { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
+ { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
+ { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
+ { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
+ { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
+ { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
+ { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
+ { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
+ { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
+ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
+]
+
[[package]]
name = "readme-renderer"
version = "44.0"
From 1eff4f4a7d4b95856d2d100ee30bb5cc3b7a12d7 Mon Sep 17 00:00:00 2001
From: agentforce314
Date: Fri, 1 May 2026 20:47:27 -0700
Subject: [PATCH 013/251] docs(readme): add running session screenshot to
header
Co-Authored-By: Claude Opus 4.7
---
README.md | 2 ++
assets/clawcodex-screenshot-1.png | Bin 0 -> 341050 bytes
2 files changed, 2 insertions(+)
create mode 100644 assets/clawcodex-screenshot-1.png
diff --git a/README.md b/README.md
index 25b858c4f..8452b8f81 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,8 @@
**🔥 Active Development • New Features Weekly 🔥**
+
+
***
diff --git a/assets/clawcodex-screenshot-1.png b/assets/clawcodex-screenshot-1.png
new file mode 100644
index 0000000000000000000000000000000000000000..e357df93c609eec1659f7d1ee0fd1a3a4227c9ee
GIT binary patch
literal 341050
zcmeEucQ{<#-Znyn2+