diff --git a/.github/workflows/desktop.yml b/.github/workflows/desktop.yml
new file mode 100644
index 0000000..7ab6521
--- /dev/null
+++ b/.github/workflows/desktop.yml
@@ -0,0 +1,44 @@
+name: REI Reverse Engineer desktop
+on:
+ pull_request:
+ paths: ['desktop/**', 'src/rei_rea/**', 'scripts/*desktop*', '.github/workflows/desktop.yml']
+ workflow_dispatch:
+permissions:
+ contents: read
+jobs:
+ windows-desktop:
+ runs-on: windows-latest
+ timeout-minutes: 90
+ env:
+ CARGO_PROFILE_DEV_DEBUG: '0'
+ CARGO_PROFILE_TEST_DEBUG: '0'
+ PYTHONPATH: ${{ github.workspace }}/src
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.13'
+ - uses: actions/cache@v4
+ with:
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/git
+ desktop/target
+ key: rei-windows-rust-1.97-${{ hashFiles('desktop/Cargo.lock') }}
+ - name: Install pinned compiler
+ run: rustup toolchain install 1.97.0 --profile minimal
+ - name: Build native app and bridge diagnostic
+ working-directory: desktop
+ run: cargo +1.97.0 build --locked -p craft-anything --bins
+ - name: Desktop and UI tests
+ working-directory: desktop
+ run: cargo +1.97.0 test --locked -p craft-anything -p craft-anything-ui-egui
+ - name: Real Python MCP bridge
+ shell: pwsh
+ run: |
+ python -m rei_rea.desktop_bridge --workspace . --config .rei-desktop
+ $env:REI_CONFIG_DIR = Join-Path $PWD '.rei-desktop'
+ $fixture = Join-Path $env:RUNNER_TEMP 'rei-test.wasm'
+ [IO.File]::WriteAllBytes($fixture, [byte[]](0,97,115,109,1,0,0,0))
+ & ./desktop/target/debug/rei-bridge-check.exe $fixture
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
diff --git a/.gitignore b/.gitignore
index a57b60c..7a84e53 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,9 @@ dist/
*.log
*.sqlite3
.DS_Store
+
+.tools/
+desktop/target/
+
+.rei-desktop/
+Projects/
diff --git a/.gitmodules b/.gitmodules
index a2b6371..b640b49 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,6 @@
[submodule "upstream/rea"]
path = upstream/rea
url = https://github.com/morluto/rea.git
+[submodule "upstream/craft-anything"]
+ path = upstream/craft-anything
+ url = https://github.com/storytold/craft-anything.git
diff --git a/DISCLAIMER.md b/DISCLAIMER.md
new file mode 100644
index 0000000..d8dee66
--- /dev/null
+++ b/DISCLAIMER.md
@@ -0,0 +1,39 @@
+# Responsible Use & Legal Disclaimer
+
+REI Reverse Engineer is an experimental research and engineering tool maintained under the Rootcastle Engineering & Innovation identity. This notice supplements the applicable open-source licenses; it does not replace them or remove permissions they grant.
+
+## Authorized use and operator responsibility
+
+Use this project only for lawful, authorized activities. You are solely responsible for establishing and documenting permission to inspect software, firmware, devices, accounts, communications and other targets. Possessing a file or being able to reach a service does not by itself establish authorization.
+
+You are responsible for complying with applicable laws, contracts, intellectual property rights, privacy and data protection obligations, export restrictions and third-party license and service terms. The project grants no right to bypass access controls, circumvent licensing, obtain unauthorized access or distribute material belonging to others.
+
+Read-only inspection is the default. Dynamic analysis, live probes, instrumentation, execution of untrusted code and firmware changes require separate, explicit scope and suitable isolation. You are responsible for backups, containment and the consequences of operations you authorize.
+
+## No warranties
+
+To the maximum extent permitted by applicable law, this software, documentation, integrations and outputs are provided **“AS IS” and “AS AVAILABLE,” without warranties of any kind, express or implied**, including merchantability, fitness for a particular purpose, noninfringement, accuracy, security or availability. There is no promise that the project is error-free, suitable for production or compatible with every target, provider or environment.
+
+## Limitation of liability
+
+To the maximum extent permitted by applicable law, **Rootcastle Engineering & Innovation, the authors, copyright holders, maintainers and contributors disclaim liability for claims, damages, losses or other liability arising from or relating to the software or its use, misuse or inability to be used**, whether in contract, tort or otherwise.
+
+This includes unauthorized or unlawful activity; inaccurate analysis or AI-generated output; data loss or disclosure; security incidents; interruption of services; damage to software, hardware or devices; loss of profits; and subscription, API, compute or other third-party costs.
+
+**Nothing in this notice excludes or limits liability, obligations or rights that applicable law does not permit to be excluded or limited.** Component licenses retain their own warranty and liability provisions.
+
+## Analysis, evidence and AI outputs
+
+Findings are technical observations or inferences, not legal, forensic or security certifications. Hashes and integrity checks do not establish ownership, authorization, complete coverage or the truth of a conclusion. Independently verify consequential results. Reconstructed or decompiled material is not necessarily the original source code.
+
+## External services, data and charges
+
+Third-party tools, models and account connections are governed by their own licenses, policies, quotas and availability. You choose and authorize their use and bear associated charges. Account-based access does not imply free or unlimited usage.
+
+Prompts, context and tool results submitted to external providers may leave your device. Review and redact sensitive data, obtain necessary consent and assess the provider's data handling before use. Do not commit credentials or proprietary artifacts to the public repository.
+
+## Attribution and reporting
+
+Third-party names identify interoperability and provenance; no endorsement or affiliation is implied. Preserve required licenses and notices when redistributing. Report vulnerabilities through [SECURITY.md](SECURITY.md), without publishing credentials, customer data or proprietary samples.
+
+Project: [REI Reverse Engineer](https://github.com/rootcastleco/Rei-Reverse-engineer) · Maintainer identity: [Rootcastle Engineering & Innovation](https://rootcastle.com).
diff --git a/README.md b/README.md
index faa8920..5127433 100644
--- a/README.md
+++ b/README.md
@@ -1,190 +1,153 @@
-# REI REA
+
+
+
-> **Upstream updates:** The original [morluto/rea](https://github.com/morluto/rea) revision is tracked separately in [upstream/rea](upstream/rea) and receives scheduled review PRs. [How synchronization works](docs/UPSTREAM-SYNC.md).
+REI Reverse Engineer
-### Evidence-first reverse engineering. One workbench. Verifiable findings.
+Inspect the artifact. Trace the evidence. Understand the system.
+A native research workbench connecting software analysis, AI assistance and reproducible evidence. Built by Rootcastle Engineering & Innovation .
-[](https://github.com/rootcastleco/Rei-Reverse-engineer/actions/workflows/ci.yml)
-[](https://www.python.org/)
-[](#connect-an-ai-coding-agent)
-[](LICENSE)
-[](CHANGELOG.md)
+
+
+
+
+
+
-**REI REA (Rootcastle Reverse Engineering Workbench)** gives developers and security researchers a consistent starting point for inspecting software artifacts, finding suitable analysis tools, and recording what the evidence actually supports.
+
+ Desktop setup · AI connections · CLI & MCP · Verification · Legal notice
+
-It brings together a **124-entry reverse-engineering tool catalog**, **read-only artifact triage**, **SHA-256 evidence cases**, a **JSON command-line interface**, a local **Model Context Protocol (MCP) server**, a **loopback dashboard**, and **12 specialist AI-agent skills**.
+---
-The objective is not to produce confident-looking pseudocode. It is to make each conclusion **traceable, testable, and reproducible**.
+## From an unknown artifact to a defensible finding
-> [!IMPORTANT]
-> **Alpha scope, stated precisely.** REI REA is an original Rootcastle implementation, not an official fork or feature-parity replacement of [morluto/rea](https://github.com/morluto/rea). A catalog entry does **not** mean a tool is installed, integrated, or validated. Eleven fixed static CLI adapters are implemented. External decompilers, debuggers, mobile instrumentation systems, and the upstream REA server are **not included**. See [Capabilities and limitations](#capabilities-and-limitations).
+Reverse engineering starts with questions: What is this file? What changed? Which tool can answer the next question? What evidence supports the conclusion?
-**[Quick start](#quick-start)** · **[Capabilities](#capabilities-and-limitations)** · **[Architecture](#architecture)** · **[MCP integration](#connect-an-ai-coding-agent)** · **[Agent skills](#twelve-specialist-agent-skills)** · **[Security](#safety-and-trust-boundaries)** · **[Roadmap](docs/ROADMAP.md)**
+REI Reverse Engineer brings those steps into one native workspace. The Rust application connects the Rootcastle Python workbench and the separately versioned REA server through local MCP. AI providers help navigate the work; artifact identity, coverage limits and source references keep the investigation grounded.
----
+| Identify | Investigate | Preserve | Connect |
+| :--- | :--- | :--- | :--- |
+| Static signatures, full-file SHA-256 and bounded metadata. | PE, ELF and WASM structures, version differences and format-aware plans. | Local case records, confidence labels and integrity-checked exports. | Native Rust tools, Python MCP, upstream REA and configurable AI backends. |
-## What makes REI REA different?
+**124 catalog entries · 14 Python MCP tools · 234 upstream MCP tools · 12 specialist skills**
-A reverse-engineering session usually involves multiple disconnected tools. Findings end up scattered across terminals, screenshots, notes, and decompiler projects. REI REA provides a small, explicit control plane around that work.
+These numbers describe different things. Catalog entries are metadata; discovered routes are callable interfaces. Neither establishes that every tool is installed, authorized or verified on your machine.
-| Problem | REI REA approach | What is available today |
-| --- | --- | --- |
-| Which tool should I use? | Query one registry by artifact type and capability. | 124 catalog entries with tags, homepages, and PATH checks. |
-| What exactly did I inspect? | Identify the target before discussing behavior. | SHA-256 hashes, bounded format inspection, and archive metadata. |
-| Can I reproduce this finding? | Keep observations linked to one artifact identity. | Local hash-linked case events and integrity verification. |
-| Can an AI agent use the same evidence? | Expose structured operations over a single interface. | JSON CLI plus fourteen local stdio MCP tools. |
-| Can I add more analysis engines? | Use isolated provider contracts instead of rewriting the core. | Eleven opt-in static adapters; advanced providers are planned. |
-| Is a model's explanation proof? | Distinguish what was seen from what was inferred. | `observed`, `inferred`, and `unknown` evidence states. |
+## Inside the native application
-**Three distinct facts must never be conflated:**
+
-1. **Cataloged:** a tool appears in the registry.
-2. **Detected:** its executable name exists on the local `PATH`.
-3. **Supported:** REI REA has a reviewed adapter for that operation.
+*Actual, unaltered Windows application capture. The image shows the project request screen, not a completed AI build or an analysis result.*
-Even a supported operation is **not end-to-end verified** until its output is checked against the specific artifact and environment.
+The desktop provides a shared interface for projects, AI connections and tool results. The launcher registers the Python and upstream MCP servers with the native engine. Their results reach the same desktop evidence interface, while each server retains its own storage.
-## Quick start
+The application is a licensed source fork of [Craft Anything](https://github.com/storytold/craft-anything), adapted with Rootcastle identity, isolated settings and the REI integration. The [original REA project](https://github.com/morluto/rea) remains pinned separately. See [source provenance](#open-source-provenance).
-**Requirements:** Python 3.10 or later, Git, and a local file that you own or are permitted to inspect. The core has no third-party Python runtime dependencies.
+> [!IMPORTANT]
+> **Development preview.** The Windows application has been built and opened locally. Real static-analysis calls have passed through both MCP bridges. This is an unsigned source-build preview; all 248 external routes, third-party analyzers and AI-assisted workflows have not been individually validated.
-```bash
+## Start the desktop
+
+**Windows x64 prerequisites:** Git, Python 3.10+, Rust 1.97.0, Visual Studio C++ build tools and the Windows SDK. Python 3.11+ is recommended for merging newly available upstream settings into an existing profile.
+
+```powershell
git clone https://github.com/rootcastleco/Rei-Reverse-engineer.git
cd Rei-Reverse-engineer
python -m pip install -e .
+git submodule update --init
-rei-rea --version
-rei-rea tools doctor
-rei-rea tools list --tag android
+./scripts/setup_upstream.ps1
+./scripts/build_desktop.ps1
+./scripts/run_desktop.ps1 -Python (Get-Command python).Source
```
-Inspect one artifact **without executing it**:
-
-```bash
-rei-rea inspect /path/to/your/sample.bin
-rei-rea signals /path/to/your/sample.bin
-rei-rea plan /path/to/your/sample.bin
-rei-rea deep /path/to/your/sample.exe
-rei-rea investigate /path/to/your/sample.wasm
-```
-
-Compare two versions:
-
-```bash
-rei-rea compare /path/to/old.bin /path/to/new.bin
-rei-rea diff /path/to/old.wasm /path/to/new.wasm
-```
+Setup downloads a pinned project-local Node runtime, locked upstream dependencies and the version-matched REA native addon. The build script compiles the desktop from source. The launcher prepares local MCP settings and refreshes tool discovery before opening the application.
-These operations return structured JSON. Inspection uses signatures and bounded metadata reads; it does not decompile, unpack, or run the target. `signals --include-strings` is optional because extracted strings can contain confidential values.
+After the first build, double-click **`REI Reverse Engineer.cmd`** to start the combined workbench. Use the launcher to supply the Python runtime and MCP profile. Existing user settings are retained; `REI_CONFIG_DIR` selects an alternative profile.
-### Record a reproducible investigation
+[Desktop guide](desktop/README.md) · [Rootcastle fork notes](desktop/ROOTCASTLE.md) · [Integration evidence](docs/DESKTOP-VERIFICATION.md)
-```bash
-rei-rea case init /path/to/your/sample.bin --workspace .rei-cases
-```
+## Connect your AI
-Use the case directory returned by the command:
+Open **Settings → AI** and choose the connection that matches your account and environment.
-```bash
-rei-rea case note .rei-cases/CASE-ID \
- --type analyst-note \
- --text 'Header at offset 0x20 requires verification' \
- --source 'artifact:0x20' \
- --confidence inferred
+| Connection | How it works | What you need |
+| :--- | :--- | :--- |
+| **Experiential Labs** | OpenAI-compatible gateway, streaming responses and live model listing. | Your provider API key in the masked field or `EXPLABS_API_KEY`. |
+| **ChatGPT account / Codex** | The installed official Codex CLI opens its browser sign-in flow. | Codex CLI and an eligible ChatGPT account. |
+| **Google account / Antigravity** | The installed official Antigravity CLI handles provider sign-in. | Antigravity CLI and the required account access. |
+| **Codex Cloud** | Opens the official cloud workspace in your browser. | A supported account; local REI operations use the CLI connection. |
+| **Other backends** | Configure supported agent, hosted or compatible server connections. | The selected backend's runtime, credentials and terms. |
-rei-rea case verify .rei-cases/CASE-ID --recheck-target
-rei-rea case report .rei-cases/CASE-ID --output finding.md
-rei-rea case index .rei-cases
-rei-rea case search .rei-cases --kind pe
-rei-rea case bundle .rei-cases/CASE-ID case-evidence.zip
-rei-rea case bundle-verify case-evidence.zip
-```
+Account sign-in is handled by the official CLI. REI does not scrape browser cookies, read provider credential caches or retain login output. Missing CLI installations show setup links. Antigravity's dedicated login command is version-dependent; terminal onboarding and Detect/Add are the documented fallback.
-Every case has a target identity and a hash-linked observation sequence. The verifier detects changes to that local record. **This is tamper-evident bookkeeping, not a signed chain of custody or a trusted timestamp service.** Keep source artifacts and generated reports in an access-controlled workspace.
+**No API key does not mean free or unlimited access.** Provider plans, quotas and charges still apply. Codex Cloud is a separate web workspace; opening it does not turn its browser session into a local chat API.
-## New in v0.2.0 — real structural analysis
+When you select a hosted model or cloud agent, submitted prompts, tool results and context may leave your machine. Review sensitive material and the provider's policies before sharing. See [AI setup, official references and verification limits](docs/AI-CONNECTIONS.md).
-The dependency-free core can now parse **PE headers and imported APIs**, **ELF program/section tables and shared-library dependencies**, and **WebAssembly version 1 imports/exports** using bounded local reads. `rei-rea diff` compares parsed structures across exact artifact hashes; `rei-rea investigate` combines identification, bounded analysis, signal scanning and an evidence-aware plan, cross-checking the hash before and after each step.
+## What you can do today
-A new **SQLite casebook** explicitly indexes local case metadata for search by name, hash or format. Case exports include the manifest, hash-chain events, report and checksum manifest—**never original binaries**. See [v0.2 upgrade contract](docs/UPGRADE-V0.2.md).
+| Surface | Available capability | Boundary |
+| :--- | :--- | :--- |
+| **Native desktop** | Projects, AI configuration and native/MCP tool dispatch. | Windows source-build preview; unsigned. |
+| **Python analysis** | Identification; bounded PE/ELF/WASM parsing; signals and structural comparison. | Partial format coverage; no decompilation or target execution. |
+| **Evidence cases** | SHA-256 identity, hash-linked notes, SQLite metadata search and portable bundles. | Integrity checks do not establish semantic truth or a signed chain of custody. |
+| **Tool catalog** | 124 searchable records with capabilities and PATH discovery. | A record does not install or integrate its tool. |
+| **External adapters** | Eleven fixed static CLI adapters and twelve provider profiles. | Explicit opt-in; only reviewed operations execute. Many profiles are discovery-only. |
+| **Python MCP** | Fourteen structured local operations. | Case creation writes metadata; other tools are read-only. No arbitrary shell runner. |
+| **Upstream REA bridge** | 234 discovered tools at the pinned revision. | Static entropy dispatch verified; other operations retain upstream requirements and consent boundaries. |
+| **Agent skills** | Twelve specialist investigation guides. | Guidance is distinct from an installed analysis engine. |
-> This is **structural inspection, not decompilation or code execution**. PE/ELF/WASM parsers are intentionally partial implementations of large specifications. A complete parser status means the bounded parser finished without error, not that every security property has been verified.
+Keep four states separate: **cataloged → PATH-detected → adapter-supported → end-to-end-verified**. A later state requires its own evidence.
-## Capabilities and limitations
+Read the [provider contracts](docs/PROVIDERS.md), [core specification](docs/SPEC.md) and [desktop specification](docs/DESKTOP-SPEC.md) for exact scope.
-| Area | Status in v0.2.0 | Implementation boundary |
-| --- | --- | --- |
-| Tool discovery | **Implemented** | 124 metadata records; filtered searches and PATH availability. |
-| Artifact identification | **Implemented** | Static header checks for PE, ELF, Mach-O, WASM, DEX, SQLite, PCAP, and archive indicators. |
-| PE/ELF/WASM structure | **Implemented** | Bounded sections, imports/exports, ELF needed libraries, WASM section inventory; no full semantic validation. |
-| Local casebook | **Implemented** | Explicit SQLite metadata index/search, portable hash-checked evidence bundles. |
-| Single-call investigation | **Implemented** | Static pipeline with SHA cross-check; file size limited to 64 MiB. |
-| Bounded signals | **Implemented** | Entropy/signature probes and optional strings; scanning is capped. |
-| Version comparison | **Implemented** | File hashes and bounded byte differences. |
-| Local evidence cases | **Implemented** | SHA-256 identity, append-only hash-linked event records, verification, and Markdown reports. |
-| CLI and agent transport | **Implemented** | JSON CLI and fourteen stdio MCP tools. |
-| Dashboard | **Implemented** | Loopback-only catalog view; no target execution. |
-| External static adapters | **Opt-in** | Eleven fixed commands; only executable tools present on the machine can run. |
-| Ghidra / IDA / Rizin session control | **Planned** | Names may be cataloged; no live session provider is included. |
-| JADX / Frida / emulator automation | **Planned** | Skills document procedures, not active runtime integration. |
-| Upstream REA MCP compatibility | **Planned** | No API-parity claim or bundled upstream server. |
-| Firmware flashing or live network testing | **Not supported** | Excluded from this alpha gateway. |
+## Use the Python workbench
-The fixed external adapters are `file`, `readelf`, `objdump`, `llvm-objdump`, `llvm-readobj`, `nm`, `exiftool`, `otool`, `aapt2` (catalog ID `android-aapt2`), `rabin2` and `rz-bin`. Each requires explicit opt-in and runs without a shell or user-supplied arbitrary flags.
+The dependency-free Python core also works independently of the desktop on Windows, Linux and macOS with Python 3.10+.
```bash
-# Only if readelf is separately installed and the file is authorized:
-rei-rea run readelf /path/to/your/example.elf --allow-external --timeout 12
-```
-
-A failed or missing external parser does not disable the built-in inspection path.
+python -m pip install -e .
+rei-rea --version
+rei-rea tools doctor
+rei-rea tools list --tag android
-## Architecture
+# Read-only inspection of an artifact you are authorized to analyze.
+rei-rea inspect /path/to/sample.bin
+rei-rea deep /path/to/sample.exe
+rei-rea investigate /path/to/sample.wasm
-```mermaid
-flowchart TD
- U["Analyst / coding agent"] --> CLI["JSON CLI"]
- U --> MCP["Local stdio MCP · 14 tools"]
- U --> UI["Loopback dashboard"]
- CLI --> Core["REI REA control plane"]
- MCP --> Core
- UI --> Core
- Core --> Registry["124-tool capability catalog"]
- Core --> Inspector["Static artifact inspector"]
- Core --> Planner["Format-aware investigation planner"]
- Core --> Evidence["SHA-256 evidence cases"]
- Core --> Adapter["Eleven fixed static adapters · explicit opt-in"]
- Adapter -. "PATH-installed executable" .-> Local["External analysis tool"]
- Future["Future provider API: REA / Ghidra / JADX / Frida"] -. "not implemented" .-> Core
+# Compare exact artifacts and their parsed structures.
+rei-rea compare /path/to/old.bin /path/to/new.bin
+rei-rea diff /path/to/old.wasm /path/to/new.wasm
```
-The code is deliberately separated into focused components:
-
-```text
-src/rei_rea/
-├── cli.py # Command-line entry point and JSON output
-├── mcp.py # Model Context Protocol, stdio JSON-RPC
-├── registry.py # Canonical tool lookup and approved static adapters
-├── inspector.py # File identity, signatures, bounded archive inspection
-├── signals.py # Bounded signal analysis and binary comparisons
-├── planner.py # Artifact-aware routes into specialist skills
-├── evidence.py # Hash-linked local cases and reports
-├── web.py # Loopback-only HTTP server
-├── data/tools.json # Generated tool catalog
-└── webui/ # Local dashboard
-```
+Commands return structured JSON. Built-in inspection does not execute or unpack the target. `signals --include-strings` is optional because extracted strings can expose confidential data. The combined investigation pipeline accepts files up to 64 MiB.
-See [architecture and contracts](docs/ARCHITECTURE.md), [threat model](docs/THREAT_MODEL.md), and [system requirements](docs/SPEC.md). Integration points are designed to preserve the stable core when future backends are introduced.
+### Keep a reproducible case
-## Connect an AI coding agent
+```bash
+rei-rea case init /path/to/sample.bin --workspace .rei-cases
-REI REA provides a **local stdio MCP server**. Start it directly:
+# Replace CASE-ID with the directory returned above.
+rei-rea case note .rei-cases/CASE-ID --type analyst-note \
+ --text 'Header at offset 0x20 requires verification' \
+ --source 'artifact:0x20' --confidence inferred
-```bash
-rei-rea mcp
+rei-rea case verify .rei-cases/CASE-ID --recheck-target
+rei-rea case report .rei-cases/CASE-ID --output finding.md
+rei-rea case index .rei-cases
+rei-rea case search .rei-cases --kind pe
+rei-rea case bundle .rei-cases/CASE-ID case-evidence.zip
+rei-rea case bundle-verify case-evidence.zip
```
-For MCP clients that use the `mcpServers` JSON format, a typical entry is:
+Findings distinguish `observed`, `inferred` and `unknown`. Case bundles contain metadata, notes and integrity manifests, **never the original binary**. Review notes and paths before sharing. A valid hash chain detects changes to a record; it does not prove that its conclusions are correct.
+
+### Connect an MCP client
+
+Start the server with `rei-rea mcp`, or configure a client that accepts `mcpServers`:
```json
{
@@ -197,133 +160,108 @@ For MCP clients that use the `mcpServers` JSON format, a typical entry is:
}
```
-The `rei-rea` executable must be available in the environment used by your MCP client. Some clients require a full executable path and a restart after configuration.
+Use an absolute executable path if your client cannot resolve `rei-rea`. Client permissions and OS access controls still apply. The Python server exposes fourteen tools; the desktop upstream bridge is configured separately by the launcher.
-**Fourteen exposed MCP tools:**
+
+View the Python MCP tool reference
| Tool | Purpose |
-| --- | --- |
-| `rei_rea_providers` | Discover provider profiles without execution. |
+| :--- | :--- |
+| `rei_rea_providers` | Discover provider profiles. |
| `rei_rea_tools` | Filter catalog entries. |
-| `rei_rea_doctor` | Inspect local PATH availability. |
+| `rei_rea_doctor` | Inspect PATH availability. |
| `rei_rea_inspect` | Identify and hash an artifact. |
-| `rei_rea_plan` | Produce an evidence-based investigation plan. |
+| `rei_rea_plan` | Plan an investigation. |
| `rei_rea_signals` | Read bounded file signals. |
-| `rei_rea_compare` | Compare two local files. |
-| `rei_rea_case_create` | Start an evidence case. |
-| `rei_rea_case_verify` | Validate a case record. |
-| `rei_rea_investigate` | Read-only cross-hash-checked static analysis pipeline. |
+| `rei_rea_compare` | Compare local files. |
+| `rei_rea_case_create` | Create a local evidence case. |
+| `rei_rea_case_verify` | Verify a case record. |
+| `rei_rea_investigate` | Run the static pipeline with hash cross-checks. |
| `rei_rea_deep` | Parse bounded PE/ELF/WASM structures. |
-| `rei_rea_diff` | Compare binary structures across versions. |
-| `rei_rea_case_search` | Search an existing local casebook index. |
-| `rei_rea_bundle_verify` | Check checksums of an exported evidence bundle. |
+| `rei_rea_diff` | Compare parsed structures. |
+| `rei_rea_case_search` | Search an existing casebook index. |
+| `rei_rea_bundle_verify` | Verify an exported evidence bundle. |
-MCP registration gives access to **these fourteen operations only**. It does not install third-party engines, grant new filesystem permissions, or create a remote analysis service. Client-specific permission settings still apply.
+
-## Twelve specialist agent skills
+The catalog dashboard runs with `rei-rea serve --port 8765` at **http://127.0.0.1:8765**. It is a loopback catalog viewer, with no target uploads or executable launch interface.
-Each skill is a separate directory with `SKILL.md`, agent metadata, and reference material. Skills route the investigation. They are **not executable analysis engines**.
+## How the parts work together
-| Skill | Investigation domain |
-| --- | --- |
-| [`rei-rea-orchestrator`](skills/rei-rea-orchestrator/) | Intake, evidence routing, tool selection, and verification. |
-| [`rei-binary-intelligence`](skills/rei-binary-intelligence/) | Built-in bounded PE/ELF/WASM structural and version-diff analysis. |
-| [`rei-casebook-forensics`](skills/rei-casebook-forensics/) | SQLite evidence casebook and metadata-only bundle handoff. |
-| [`rei-native-binaries`](skills/rei-native-binaries/) | PE, ELF, Mach-O, symbols, imports, and ABI. |
-| [`rei-electron-and-js`](skills/rei-electron-and-js/) | Electron, ASAR, Node.js, preload scripts, and IPC. |
-| [`rei-android-apks`](skills/rei-android-apks/) | Android APK/AAB, DEX, manifest, and JNI. |
-| [`rei-firmware-and-iot`](skills/rei-firmware-and-iot/) | Embedded firmware, MCU, UEFI, GPS, and IoT. |
-| [`rei-mobile-runtime`](skills/rei-mobile-runtime/) | Authorized mobile observation and instrumentation planning. |
-| [`rei-evidence-ledger`](skills/rei-evidence-ledger/) | Artifact provenance, confidence, and audit. |
-| [`rei-reconstruction-parity`](skills/rei-reconstruction-parity/) | Behavioral specifications and golden-test comparisons. |
-| [`rei-web-and-protocols`](skills/rei-web-and-protocols/) | HAR, PCAP, APIs, telemetry frames, and protocol reconstruction. |
-| [`rei-wasm-and-managed`](skills/rei-wasm-and-managed/) | WebAssembly, JVM bytecode, and .NET assemblies. |
+```mermaid
+flowchart LR
+ Analyst["Analyst"] --> Desktop["REI native desktop · Rust"]
+ Desktop <--> AI["Configured AI backend / official agent CLI"]
+ Desktop --> Native["Native engine tools"]
+ Desktop --> Bridge["Local MCP routing"]
+ Bridge --> Python["Rootcastle Python · 14 tools"]
+ Bridge --> Upstream["Pinned upstream REA · 234 tools"]
+ CLI["JSON CLI / external MCP client"] --> Python
+ Python --> Registry["124-entry tool catalog"]
+ Python --> Cases["Python case records"]
+ Upstream --> Store["Upstream evidence store"]
+```
-Read [skills installation and packaging](docs/SKILLS.md). Install the Python workbench separately; importing a skill does not register MCP or provide a local tool binary.
+Dispatch and evidence presentation are shared in the desktop. Storage remains owned by each engine. External tools and artifact-reading operations retain the desktop's consent and mode controls.
-## Local dashboard
+## Specialist investigation skills
-```bash
-rei-rea serve --port 8765
-```
+Twelve packages cover orchestration, bounded binary analysis, native binaries, Electron/JavaScript, Android, firmware/IoT, authorized mobile runtime work, evidence ledgers, casebook forensics, reconstruction parity, web/protocols, and WASM/managed code.
-Visit **http://127.0.0.1:8765**. The dashboard displays the tool registry and local availability indicators. It does not accept uploads, inspect arbitrary remote hosts, or run third-party executables. It binds to loopback; do not expose it through a public reverse proxy without a separate security review.
+Browse [the skill packages](skills/) and [installation guide](docs/SKILLS.md). Installing a skill supplies instructions, not a binary analyzer, MCP registration or permission to test a third-party system.
-## Safety and trust boundaries
+## What is verified
-REI REA is intended for **owned artifacts, authorized security research, interoperability, software preservation, and controlled laboratory analysis**.
+Local evidence was recorded on **Windows 11 x64 · Rust 1.97.0 · Python 3.14.7 · Node 24.22.1**:
-- **Read-only first.** `inspect`, `signals`, `plan`, `compare`, and registry lookup do not execute target code.
-- **External tools are never launched implicitly.** Fixed adapters require `--allow-external`. Use disposable, low-privilege environments for untrusted files.
-- **Evidence is not certainty.** File names, strings, and reconstructed pseudocode are not proof of actual runtime behavior.
-- **Artifact data remains local by design.** Case records hold metadata and analyst notes; review sensitive strings before sharing reports.
-- **No general remote scanner, shell runner, firmware flasher, or license bypass mechanism** is included in this alpha.
+- Native executable built; its real window was opened and inspected.
+- MCP discovery registered **248 external routes**. Python artifact identification and upstream byte-entropy analysis both returned through the desktop engine for the same synthetic WASM fixture and SHA-256.
+- The AI/UI/CLI Rust suite passed **352 tests**. The Python suite ran **65 tests: 64 passed, one optional-tool skip**.
+- All **12 root skills** validated. Formatting, Clippy with warnings denied, layer checks, lock checks and the source secret scan passed.
+- Experiential streaming and model listing passed loopback integration tests. **Live paid API calls and user OAuth sign-in were not performed.**
-Read [SECURITY.md](SECURITY.md) for reporting and [the threat model](docs/THREAT_MODEL.md) for the relevant trust boundaries.
+These checks apply to the recorded environments and operations. They do not verify all upstream tools, decompiler accuracy, device behavior, AI-generated rebuild quality or production readiness. The pinned upstream npm installation reported **11 moderate advisories**; release hardening remains open. Remote results are available in [GitHub Actions](https://github.com/rootcastleco/Rei-Reverse-engineer/actions).
-## Development and verification
+[Desktop verification evidence](docs/DESKTOP-VERIFICATION.md) · [AI verification boundaries](docs/AI-CONNECTIONS.md) · [Core stabilization](docs/STABILIZATION.md)
+
+## Develop and contribute
```bash
python -m pip install -e .
-python -m unittest discover -s tests -v
-python -m rei_rea --version
+PYTHONPATH=src python -m unittest discover -s tests -v
+python scripts/generate_catalog.py
```
-Verification is recorded per commit and environment in [the stabilization report](docs/STABILIZATION.md). GitHub Actions tests Python 3.10 and 3.13 on Linux, Windows and macOS. A local test pass does not establish remote CI or external-provider compatibility.
-
-Hardware behavior, mobile instrumentation, third-party engine accuracy, native decompilation parity, cloud deployment, and production performance are **not yet verified**.
+On PowerShell, set `$env:PYTHONPATH = 'src'` before the unittest command. Python CI covers Python 3.10 and 3.13 on Linux, Windows and macOS. The desktop workflow builds and tests Windows separately.
-## Roadmap
+Start with [AGENTS.md](AGENTS.md), [the architecture](docs/ARCHITECTURE.md), [the threat model](docs/THREAT_MODEL.md) and existing tests. Add capabilities through reviewed adapters with contracts and regression coverage. Keep customer binaries, secrets and proprietary samples out of this public repository.
-The next technical milestones are:
+Next priorities include provider-specific verification, parser fuzzing, dependency remediation and signed desktop releases. See [the roadmap](docs/ROADMAP.md), [learning path](docs/LEARNING_PATH.md) and [upstream synchronization policy](docs/UPSTREAM-SYNC.md).
-1. Formalize a versioned provider interface with capability and authorization checks.
-2. Integrate upstream REA and selected native tools without changing the evidence contract.
-3. Add reproducible fixtures for ELF/PE, Electron IPC, APKs, firmware images, and protocol captures.
-4. Add indexed case search, provenance export, and more rigorous differential verification.
-5. Run cross-platform CI, dependency reviews, and release hardening before a stable version.
+## Responsible use and legal disclaimer
-Track the scope and acceptance gates in [docs/ROADMAP.md](docs/ROADMAP.md). For a practical self-study path, read [the reverse-engineering learning roadmap](docs/LEARNING_PATH.md).
+> [!WARNING]
+> **Authorized use only. Use at your own risk.** Analyze only software, devices, recordings and systems you own or have explicit permission to inspect. You are responsible for your actions, legal compliance, permissions, data handling and third-party service charges.
-## Frequently asked questions
+**To the maximum extent permitted by applicable law, Rootcastle Engineering & Innovation, the authors, maintainers and contributors provide this software “AS IS” and “AS AVAILABLE,” without warranties, and disclaim liability for claims, losses or damages arising from its use, misuse or inability to use it.** This includes unauthorized activity, inaccurate analysis or AI output, data loss, security incidents, service interruption, device damage and third-party costs.
-
-Does REI REA contain 124 working analysis integrations?
-
-No. It has 124 **catalog records**. Eleven fixed static tool adapters are implemented, and each adapter needs its executable installed locally. The other records are discovery-only.
-
+No tool output or reconstruction guarantees correctness, safety, ownership or legal permission. Reconstructed or decompiled output must not be represented as original source. Dynamic analysis, live probing and firmware modification require separate scoped authorization and isolation. This project does not grant permission to bypass licenses, access controls or other parties' rights.
-
-Is this the same project as morluto/rea?
+**These exclusions apply only where legally enforceable. Nothing excludes liability or rights that applicable law does not allow to be excluded.** Read the complete [Responsible Use & Legal Disclaimer](DISCLAIMER.md), [MIT License](LICENSE) and [security reporting policy](SECURITY.md).
-No. [morluto/rea](https://github.com/morluto/rea) is an independent project. REI REA is an original Rootcastle control plane with its own implementation and evidence contract. Upstream integration is a future adapter, not a current feature.
-
+## Open-source provenance
-
-Can it decompile an APK or control Ghidra automatically?
+| Component | Origin and license | Maintenance |
+| :--- | :--- | :--- |
+| **Rootcastle Python workbench and integration** | [MIT](LICENSE), © 2026 Batuhan AYRIBAS. | Original implementation in `src/rei_rea/` and integration scripts. |
+| **Native desktop foundation** | [Craft Anything](https://github.com/storytold/craft-anything), MIT OR Apache-2.0. | Licensed fork in `desktop/`; exact revision in [ROOTCASTLE-FORK.json](desktop/ROOTCASTLE-FORK.json). |
+| **Upstream REA** | [morluto/rea](https://github.com/morluto/rea), under its retained license. | Separately pinned submodule; connected through local MCP. |
+| **Fonts, icons and dependencies** | Their respective licenses. | Retained [NOTICE](desktop/NOTICE), [ATTRIBUTION](desktop/ATTRIBUTION.md) and license documentation. |
-Not in v0.2.0. The registry helps you discover relevant tools, and the skills document investigation workflows. Automated Ghidra, JADX, and Frida providers remain future work.
-
+Keep required notices with redistributions. Code licenses do not grant third-party trademark rights. Rootcastle branding identifies this adaptation; no upstream or provider endorsement or affiliation is implied.
-
-What does a verified finding mean?
-
-The local hash chain and artifact identity have been checked. It does **not** automatically establish that the analyst's conclusion is correct. Semantic accuracy still requires independent evidence and suitable tests.
-
-
-## Project, credit, and license
-
-**Built by [Rootcastle Engineering & Innovation](https://rootcastle.com).** Distributed under the [MIT License](LICENSE), copyright © 2026 Batuhan Ayrıbaş (license file uses `Batuhan AYRIBAS`). Third-party tools remain under their own licenses; the catalog does not redistribute their code or binaries.
-
-**Upstream credit:** [morluto/rea](https://github.com/morluto/rea) provides a separate reverse-engineering agent project and an important reference for the skill/MCP ecosystem. REI REA is not endorsed by its maintainers. Additional technical sources and publications are listed in [docs/SOURCES.md](docs/SOURCES.md).
-
-**Discoverability metadata:** The canonical repository description and GitHub Topics are maintained in [`.github/repository-metadata.json`](.github/repository-metadata.json). See [the publication checklist](docs/DISCOVERABILITY.md) to apply them to the GitHub repository's About section.
+The header uses the official [Rootcastle logo](https://rootcastle.com/brand/rootcastle-rei.svg) in an original SVG composition. See [artwork provenance](docs/images/README.md). Discovery metadata is maintained in [repository-metadata.json](.github/repository-metadata.json).
---
-REI REA · Rootcastle Engineering & Innovation · Measure first. Reconstruct second. Verify always.
-
-
-## External analysis providers (v0.3 preview)
-
-Twelve additional provider profiles are available through `rei-rea providers list`, with opt-in read-only inspection for radare2/rabin2, Rizin/rz-bin and LLVM readobj. The remaining providers are *discovery-only*. No third-party tools are automatically installed or executed, and the MCP server offers inventory without external execution. See [reviewed provider contracts](docs/PROVIDERS.md).
+ROOTCASTLE · ENGINEERING & INNOVATION rootcastle.com · Open-source engineering Measure first. Reconstruct second. Verify always.
diff --git a/REI Reverse Engineer.cmd b/REI Reverse Engineer.cmd
new file mode 100644
index 0000000..c2963ec
--- /dev/null
+++ b/REI Reverse Engineer.cmd
@@ -0,0 +1,3 @@
+@echo off
+powershell.exe -NoProfile -File "%~dp0scripts\run_desktop.ps1"
+if errorlevel 1 pause
diff --git a/desktop/.cargo/config.toml b/desktop/.cargo/config.toml
new file mode 100644
index 0000000..9741f11
--- /dev/null
+++ b/desktop/.cargo/config.toml
@@ -0,0 +1,2 @@
+[alias]
+xtask = "run --quiet --package xtask --"
diff --git a/desktop/.coderabbit.yaml b/desktop/.coderabbit.yaml
new file mode 100644
index 0000000..2408084
--- /dev/null
+++ b/desktop/.coderabbit.yaml
@@ -0,0 +1,6 @@
+# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
+reviews:
+ auto_review:
+ # Dependabot bumps are checked by CI; skip CodeRabbit reviews on them.
+ ignore_usernames:
+ - "dependabot[bot]"
diff --git a/desktop/.gitattributes b/desktop/.gitattributes
new file mode 100644
index 0000000..a843de2
--- /dev/null
+++ b/desktop/.gitattributes
@@ -0,0 +1,7 @@
+# LF everywhere: files are embedded with include_str! and must not change with the checkout platform.
+* text=auto eol=lf
+*.png binary
+*.jpg binary
+*.ico binary
+*.icns binary
+*.ttf binary
diff --git a/desktop/.githooks/pre-commit b/desktop/.githooks/pre-commit
new file mode 100644
index 0000000..85ed81c
--- /dev/null
+++ b/desktop/.githooks/pre-commit
@@ -0,0 +1,4 @@
+#!/bin/sh
+# Refuse commits that carry API keys, private keys, secret env values or a home folder path.
+# Enable once per clone: git config core.hooksPath .githooks
+exec cargo xtask secrets
diff --git a/desktop/.github/ISSUE_TEMPLATE/bug_report.md b/desktop/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..b697dd7
--- /dev/null
+++ b/desktop/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,16 @@
+---
+name: Something doesn't work
+about: A crash, a hang, a wrong result, or a build that goes wrong
+labels: bug
+---
+
+
+
+**What you did:**
+
+**What happened:**
+
+**Version and system:**
+
+**Output:**
diff --git a/desktop/.github/ISSUE_TEMPLATE/config.yml b/desktop/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..fe2e654
--- /dev/null
+++ b/desktop/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: true
+contact_links:
+ - name: Report a security problem
+ url: https://github.com/storytold/craft-anything/security/advisories/new
+ about: Please report vulnerabilities privately (SECURITY.md), not as an issue.
diff --git a/desktop/.github/ISSUE_TEMPLATE/idea.md b/desktop/.github/ISSUE_TEMPLATE/idea.md
new file mode 100644
index 0000000..1a0ff06
--- /dev/null
+++ b/desktop/.github/ISSUE_TEMPLATE/idea.md
@@ -0,0 +1,11 @@
+---
+name: An idea
+about: Something Craft Anything should do, study or make
+labels: idea
+---
+
+**What you'd like to do:**
+
+**Why:**
+
+
diff --git a/desktop/.github/actionlint.yaml b/desktop/.github/actionlint.yaml
new file mode 100644
index 0000000..08ccd93
--- /dev/null
+++ b/desktop/.github/actionlint.yaml
@@ -0,0 +1,4 @@
+# Custom runner labels actionlint can't know about.
+self-hosted-runner:
+ labels:
+ - github-ubuntu-16core # org 16-core larger runner, used by the FreeBSD VM jobs (release.yml, freebsd.yml)
diff --git a/desktop/.github/pull_request_template.md b/desktop/.github/pull_request_template.md
new file mode 100644
index 0000000..695b2fa
--- /dev/null
+++ b/desktop/.github/pull_request_template.md
@@ -0,0 +1,16 @@
+
+
+**Roadmap item:**
+
+**What changed:**
+
+**Verified:**
+
+**Not verified:**
+
+- [ ] `cargo fmt --all --check`, `cargo clippy -p --all-targets -- -D warnings`,
+ `cargo test -p `, `cargo xtask layers`, `cargo xtask secrets`
+- [ ] Docs in this PR: the roadmap line ticked (and new `- [ ]` lines for what's left), STATUS if
+ what works changed, `cargo xtask tools` if tools were added
+- [ ] No other tool's code, scripts or wording copied (behaviour only; AGENTS.md)
diff --git a/desktop/.github/workflows/ci.yml b/desktop/.github/workflows/ci.yml
new file mode 100644
index 0000000..ae7a699
--- /dev/null
+++ b/desktop/.github/workflows/ci.yml
@@ -0,0 +1,28 @@
+name: CI
+
+# Quality gates for every push to main and every pull request (`cargo xtask ci`), through the
+# suite's shared gate workflow in craft-common-actions.
+on:
+ push:
+ branches: [main]
+ pull_request:
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+# A newer push to a PR branch cancels its older run; every push to main runs to the end.
+concurrency:
+ group: ci-${{ github.event_name == 'pull_request' && github.ref || github.run_id }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+jobs:
+ gate:
+ # Tracks @main so suite-wide CI fixes reach every app without a PR in each repository.
+ uses: storytold/craft-common-actions/.github/workflows/gate.yml@main
+ with:
+ # Same craft-fonts commit as release.yml.
+ craft-fonts-ref: b076417b5880bd4154bd22f00695b1d28c6bea83
+ packages: >-
+ libxkbcommon-dev libwayland-dev libx11-dev libxrandr-dev libxi-dev libgl1-mesa-dev
+ libgtk-3-dev
diff --git a/desktop/.github/workflows/freebsd.yml b/desktop/.github/workflows/freebsd.yml
new file mode 100644
index 0000000..537aafa
--- /dev/null
+++ b/desktop/.github/workflows/freebsd.yml
@@ -0,0 +1,36 @@
+# Builds and tests the workspace on FreeBSD, inside a FreeBSD VM on a Linux runner
+# (GitHub has no FreeBSD runners). GPU tests skip themselves when the VM has no adapter.
+# It then runs packaging/freebsd/package.sh, the script the release job runs, and checks the
+# tarball, so a broken FreeBSD package fails here before it can fail a release. Nothing is
+# uploaded and no secrets are used.
+name: FreeBSD
+
+# The VM build takes one to several hours, so it runs nightly on main rather than on every push
+# or dependency change: a drifted dependency still shows up within a day. PRs that change the
+# FreeBSD packaging itself still run it.
+on:
+ push:
+ branches: [release]
+ schedule:
+ - cron: "17 2 * * *"
+ pull_request:
+ paths:
+ - .github/workflows/freebsd.yml
+ - packaging/freebsd/**
+ - packaging/env.sh
+ workflow_dispatch:
+
+concurrency:
+ group: freebsd-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ freebsd:
+ # Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos.
+ uses: storytold/craft-common-actions/.github/workflows/freebsd.yml@main
+ with:
+ craft-fonts-ref: b076417b5880bd4154bd22f00695b1d28c6bea83
+ run-package-check: true
diff --git a/desktop/.github/workflows/packaging-lint.yml b/desktop/.github/workflows/packaging-lint.yml
new file mode 100644
index 0000000..f54b053
--- /dev/null
+++ b/desktop/.github/workflows/packaging-lint.yml
@@ -0,0 +1,23 @@
+# Quick static checks for the release pipeline (seconds, no Rust build): workflow syntax,
+# packaging shell/PowerShell scripts, and the Linux desktop/AppStream/Flatpak metadata.
+name: Packaging lint
+
+on:
+ push:
+ branches: [main, release]
+ paths: [".github/workflows/**", "packaging/**", "assets/app-icon/**"]
+ pull_request:
+ paths: [".github/workflows/**", "packaging/**", "assets/app-icon/**"]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: packaging-lint-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ lint:
+ # Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos.
+ uses: storytold/craft-common-actions/.github/workflows/packaging-lint.yml@main
diff --git a/desktop/.github/workflows/release.yml b/desktop/.github/workflows/release.yml
new file mode 100644
index 0000000..01346b6
--- /dev/null
+++ b/desktop/.github/workflows/release.yml
@@ -0,0 +1,44 @@
+name: Release
+
+on:
+ push:
+ branches: [release]
+ workflow_dispatch:
+ inputs:
+ version:
+ description: "Version override for a test run, e.g. 0.2.0-rc.1 (empty: Cargo.toml)"
+ type: string
+ default: ""
+
+permissions:
+ contents: write
+ packages: write
+
+concurrency:
+ group: release-${{ github.ref }}
+ cancel-in-progress: false
+
+jobs:
+ release:
+ # Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos.
+ uses: storytold/craft-common-actions/.github/workflows/release.yml@main
+ with:
+ app-name: craft-anything
+ app-title: "Craft Anything"
+ craft-fonts-ref: b076417b5880bd4154bd22f00695b1d28c6bea83
+ version: ${{ inputs.version }}
+ secrets:
+ APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
+ APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
+ KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
+ APPLE_ID: ${{ secrets.APPLE_ID }}
+ APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+ WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
+ WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
+ AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
+ AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
+ AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
+ AZURE_SIGNING_ENDPOINT: ${{ secrets.AZURE_SIGNING_ENDPOINT }}
+ AZURE_SIGNING_ACCOUNT: ${{ secrets.AZURE_SIGNING_ACCOUNT }}
+ AZURE_CERT_PROFILE: ${{ secrets.AZURE_CERT_PROFILE }}
diff --git a/desktop/.github/workflows/windows-arm64.yml b/desktop/.github/workflows/windows-arm64.yml
new file mode 100644
index 0000000..7c11db1
--- /dev/null
+++ b/desktop/.github/workflows/windows-arm64.yml
@@ -0,0 +1,31 @@
+# Windows on ARM: package the ARM64 release the way release.yml does (cross-compiled on an x64
+# runner, unsigned here), then install that MSI on a Windows 11 ARM64 runner, check the installed
+# binaries are ARM64, run the CLI natively, and uninstall. (A native ARM64 `cargo test` job is left
+# out for now: PdfCraft saw an intermittent access violation in egui UI tests there.)
+name: Windows ARM64
+
+# Nightly on main (dependency and runner-image drift shows up within a day), and on PRs that
+# change the Windows packaging itself.
+on:
+ pull_request:
+ paths:
+ - .github/workflows/windows-arm64.yml
+ - packaging/windows/**
+ schedule:
+ - cron: "41 4 * * *"
+ workflow_dispatch:
+
+concurrency:
+ group: windows-arm64-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ windows-arm64:
+ # Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos.
+ uses: storytold/craft-common-actions/.github/workflows/windows-arm64.yml@main
+ with:
+ app-name: craft-anything
+ app-dir: "Craft Anything"
diff --git a/desktop/.gitignore b/desktop/.gitignore
new file mode 100644
index 0000000..f779ef9
--- /dev/null
+++ b/desktop/.gitignore
@@ -0,0 +1,18 @@
+*~
+screenshots/
+plan/
+plans/
+/target
+/log/
+corpus/
+/dist/
+.DS_Store
+.claude/worktrees/
+# Secrets and local settings (`cargo xtask secrets` refuses them too)
+.env
+.env.*
+*.pem
+*.p12
+*.pfx
+*.key
+.claude/settings.local.json
diff --git a/desktop/AGENTS.md b/desktop/AGENTS.md
new file mode 100644
index 0000000..8de0696
--- /dev/null
+++ b/desktop/AGENTS.md
@@ -0,0 +1,169 @@
+# AGENTS.md: guide for AI agents and contributors
+
+Craft Anything is a native, pure-Rust app that shows anyone how a piece of software works (programs, apps, files,
+websites, phones) with evidence-backed answers, then helps them rebuild it. It ships a desktop app (egui), a CLI and
+an MCP server, all driven by one engine. Display name **Craft Anything**; machine names `craft-anything-*`; bundle id
+`ai.storyteller.craft-anything`. Standards shared across the Crafting Apps live in `../craftrules` (read its
+`AGENTS.md`); follow them. Repos never share code.
+
+**Original work only:** never copy code, scripts, schemas, names, assets or wording from another project, and
+never name other reverse-engineering products in source, docs, UI or commit messages.
+
+## Workspace map
+
+```text
+crates/
+ evidence L0 Evidence records, canonical ids, confidence, unknowns, ToolError (the error contract)
+ formats L0 Standalone parsers (no workspace deps, no I/O): ELF/PE/Mach-O/WASM/DEX/APK/ASAR/plist/.NET/Go/…
+ disasm L1 Our own native engine (Clean room): decode, functions, CFG, xrefs, call graph, pseudo-C
+ js L1 JavaScript/Electron/web bundles: parse, module graph, IPC, source maps, unbundling
+ analysis L2 Analyzers producing evidence (triage, dossiers, trace, compare, app graph, rebuild ledger)
+ platform L2 Native services: owned processes, PTY capture, sandbox, engine store, atomic files
+ engines L3 Optional external engines: Ghidra bridge (Deep mode), adb, CDP, Frida, JADX, Apktool…
+ engine L4 Session, projects (.cany), workspaces (what the person is making), the inbox
+ (questions/approvals), ToolSpec registry, settings, consent, the tool list
+ ai L5 AI backends (Anthropic, OpenAI, OpenRouter, local, agent CLIs, SSH) + agent loop + crews,
+ and the rebuild workflow (study → build → check → refine)
+ mcp L5 MCP server: progressive toolsets, resources, prompts
+ setup L5 Connect to coding-agent apps; doctor; uninstall
+ ui-egui L6 The egui interface (Beginner + Experienced). The only crate that may use egui/eframe/winit/rfd.
+ testkit Test helpers and synthetic sample builders
+apps/ craft-anything (desktop) · craft-anything-cli (CLI + `mcp`) · craft-anything-web (browser viewer)
+xtask/ cargo xtask layers | tools [--check] | wasm | secrets [--history] | ci | version [set X.Y.Z]
+```
+
+Layering is enforced by `cargo xtask layers`. Register new crates in `xtask/src/main.rs`.
+
+## Golden rules
+
+1. **Never crash** (`../craftrules/standards/never-crash.md`). Hostile input is our main input. No
+ `unwrap`/`expect`/`panic!`/`todo!`/`unimplemented!`/`unreachable!`/indexing on input-derived values in non-test
+ code; every production crate root carries the deny attribute. Checked arithmetic, bounded recursion, capped
+ allocations. Every crash fix gets a regression test.
+2. **Everything is a tool.** User-visible behaviour = a `ToolSpec` in `crates/engine/src/tools/ .rs` with id,
+ title, summary, beginner text, toolset, effects, requires, a schemars params struct, and `run`. Register the
+ module with one line in `tools/mod.rs`. UI, CLI, MCP and the AI agent all dispatch by id. Programmatic calls
+ never open dialogs.
+3. **Evidence is the result.** Analysis tools return `ToolOutput::Evidence` built with `EvidenceBuilder` (honest
+ confidence, limitations, coverage, locations). Large results: summary first, paged views (`offset`/`limit`).
+4. **Clean room vs Deep.** `session.clean_room()` is true unless Deep mode is on (`deep_mode`, or `--deep` for one
+ CLI run). Clean room never looks inside the original: only the tools in `engine/src/clean_room.rs` run, so a new
+ tool is Deep-only until it's listed there, and it may be listed only if it touches no target or watches from
+ outside, as a visitor or user would. Deep mode uses every tool; built-in engines must work fully without Ghidra,
+ which only adds. Evidence records the engine and the flag.
+5. **Effects are honest.** Declare every side effect. `EXECUTES_FOREIGN_CODE` and `MUTATES_DEVICE` trigger consent.
+6. **Never break wasm** for L0–L2 (`cargo xtask wasm`).
+7. **Thin UI.** UI state is serde data; colours/radii come from theme tokens.
+8. **No secrets, no personal data** in requests, logs, tests or the repo (it is public): no API keys, tokens,
+ private keys, `.env` files or local paths, only obvious fakes like `sk-test`. `cargo xtask secrets` checks what
+ git would commit (`--history` checks every commit); enable the hook once per clone with
+ `git config core.hooksPath .githooks`. Generic `CraftAnything-dev` User-Agent.
+9. **Safe Rust only.** `unsafe_code = "forbid"` everywhere, no exceptions. Prefer pure-Rust crates; any dependency
+ must be safe to use from safe code.
+10. **Never crash, never lock up, around other programs.** Every external program (Ghidra, Java, adb, a browser,
+ an agent CLI, ssh, git…) runs through `craft_anything_platform::process` only, never `std::process::Command`
+ directly:
+ - stdin is closed (or a pipe we write to), never inherited, so nothing can sit waiting at a prompt; ssh gets
+ `BatchMode=yes`; tools get their non-interactive flags;
+ - every run has a deadline (wall clock and idle), output is capped and drained on threads so pipes never fill,
+ and the child (and its process group / Job Object) is killed on timeout, cancel or drop;
+ - a missing program, non-zero exit, bad output or timeout is a `ToolError` with a plain-English remediation,
+ never a panic or a hang;
+ - nothing slow ever runs on the UI thread: the UI sends work to background threads and polls results.
+11. **Never lock up on a lock.** Use `craft_anything_platform::sync::Mutex`, never `std::sync::Mutex` (clippy
+ refuses it). It stops a real deadlock as soon as it forms, the waiter's work unwinding to its `catch_unwind` as
+ an error; the watchdog reports anything held too long. Long jobs are fine: label the guard (`guard.label(id)`,
+ as `Engine::call` does for tools) and they're waited for, not treated as stuck. A guard that isn't bound to a
+ name lives until the statement ends, so read the session once: `cargo xtask locks` refuses a second
+ `.session()` in reach of the first.
+
+## Working rules (mandatory)
+
+These apply to every change by every agent and contributor, whether or not anyone mentions them.
+
+### Roles and ownership
+- `docs/ROADMAP.md` is the plan: each item has an id (`R.`), an **owner**, a status and pending
+ changes (`- [ ]` lines). Owners are roles, never people's names: **maintainers** (the project's maintainers
+ steer it; agents may take its unclaimed pending changes, but anything marked **decision** waits for a
+ maintainer) or **open** (anyone may take it).
+- **Claim before you start:** put `— claimed: , branch ` on the pending change and push your branch
+ early (or open a draft PR) so others see it. Never take a change someone else has claimed.
+- Work in the order the roadmap's "Order and dependencies" table gives; an item starts only when its
+ dependencies are done.
+- A person's explicit instruction for a task overrides these rules for that task only; otherwise they apply to
+ every change, without being asked.
+
+### Before you write code
+- Pick work from `docs/ROADMAP.md` only. Don't start an item that is claimed, `blocked`, or waiting on a
+ **decision**, and don't start one whose dependencies aren't done.
+- Claim the pending change first (name + branch, in its own small commit), so no two agents do the same work.
+- Read the code you'll touch and check STATUS.md and the roadmap for gaps it depends on. If you find new gaps or
+ dependencies, add them to the roadmap rather than widening your change.
+
+### Git workflow
+- Never commit to `main`. Branch from an up-to-date `main` with a standard prefix, then the roadmap id if
+ there is one: `feature/`, `bugfix/`, `hotfix/` (urgent fix to released code), `refactor/`, `docs/`, `test/`
+ or `chore/` (tooling, deps, CI). Example: `feature/r1.1-target-kind`.
+- One roadmap item (or pending change) per PR. Keep PRs small and reviewable. No unrelated refactors or drive-by
+ formatting.
+- Open a PR into `main`. It is **squash-merged**, so the PR title is the commit message: plain English, present
+ tense, what changed for the person or developer.
+- Rebase on `main` before asking for review. Never force-push someone else's branch.
+- Parallel agents work in their own git worktree and their own `CARGO_TARGET_DIR` (see "Parallel agents" below).
+- Before every commit and push, check `git branch --show-current`; push by name (`git push origin `).
+
+### Architecture
+- **Headless first.** Every capability works without the GUI: engine → tools → CLI/MCP, with the GUI as one
+ thin client. Orchestration, persistence and process control never live in `ui-egui` or `apps/*`. If the UI
+ needs something, it calls the same engine API (or tool) the CLI does.
+- **Extend through seams.** New target kinds, checks, builders, asset providers and engines are trait
+ implementations registered in one place. Don't add another arm to a `match` over a closed enum. Prefer traits
+ and generics with sensible built-in implementations, so forks can swap parts without editing core code.
+- **Data over code.** Catalogues (stacks, engine recipes, model ids, prices, install pins) are typed tables, not
+ string matches or prompt text.
+- **Structured results.** Decide on typed fields, never on English text.
+- **Versioned files.** Anything we persist has a version field, a schema and a migration path.
+- Respect the layers (`cargo xtask layers`); put code at the lowest layer that can own it.
+
+### Done means
+- The checks in "Before you finish" pass for the crates you touched. New behaviour has tests; crash fixes have
+ regression tests.
+- Docs updated in the same PR: the roadmap item ticked, STATUS.md if what works changed, `docs/tools.md` if you
+ added tools.
+- Report what you verified and what you didn't. Never claim a live run you didn't do.
+- Made a mistake worth not repeating? Add it to `docs/pitfalls.md` in the same PR.
+- Found work that's missing or left for later? Add it as a `- [ ]` line under its roadmap item in the same PR
+ (`cargo xtask pending` lists all open work).
+
+## Extending
+
+`docs/extending.md` says where each kind of addition goes (tools, target kinds, checks, stacks, coding agents,
+asset providers, engines, plugins).
+
+## Pitfalls
+
+`docs/pitfalls.md` lists mistakes already made here (git, tests, builds, design) and the rule for each.
+It is part of these rules: read it before your first change (Claude Code loads it automatically through
+`CLAUDE.md`).
+
+## Before you finish
+
+```sh
+cargo fmt --all
+cargo test -p
+cargo clippy -p --all-targets -- -D warnings
+cargo xtask layers
+cargo xtask locks
+cargo xtask secrets # no keys, tokens or local paths in what you commit
+cargo xtask wasm # if you touched L0–L2
+cargo xtask tools # if you added tools (commit docs/tools.md)
+```
+
+Then append a terse entry to `log/devlog.md` (what landed, numbers, what's still open). `log/` is gitignored:
+the devlog is a local scratch record; anything others need goes in the PR, `docs/STATUS.md` or `docs/ROADMAP.md`.
+
+## Parallel agents
+
+Own target dir (`CARGO_TARGET_DIR=target/agent-`), own files. Shared files (`engine/src/tools/mod.rs`,
+`engine/src/lib.rs`, workspace `Cargo.toml`) get small surgical edits; re-read right before editing. Write manifests
+atomically (temp file outside `crates/`, then `mv`). If someone else's edit breaks the build, wait and retry.
diff --git a/desktop/ATTRIBUTION.md b/desktop/ATTRIBUTION.md
new file mode 100644
index 0000000..3a5f203
--- /dev/null
+++ b/desktop/ATTRIBUTION.md
@@ -0,0 +1,51 @@
+# Attribution
+
+Third-party material bundled with Craft Anything, file by file. Rust crates are pulled from
+crates.io under their own licences (see `Cargo.lock`) and are not listed here.
+
+## Fonts (`assets/fonts/`)
+
+All fonts are under the SIL Open Font License 1.1. The fonts are embedded unmodified; the
+licence requires that the font software is not sold by itself and that reserved font names are
+not used for modified versions.
+
+| File | Font | Copyright | Source | Licence |
+|---|---|---|---|---|
+| `DMSans-Regular.ttf`, `DMSans-Medium.ttf`, `DMSans-Bold.ttf` | DM Sans 1.200 (body text, labels, headlines) | © 2014-2017 Indian Type Foundry, © 2019 Google LLC | https://github.com/googlefonts/dm-fonts (`Sans/Exports/`, commit 23aafb3) | `OFL-DMSans.txt` |
+| `JetBrainsMono-Regular.ttf` | JetBrains Mono (code views) | © 2020 The JetBrains Mono Project Authors | https://github.com/JetBrains/JetBrainsMono | `OFL-JetBrainsMono.txt` |
+| `GeistMono-Regular.ttf`, `GeistMono-Medium.ttf` | Geist Mono (labels and buttons) | © 2024 The Geist Project Authors | https://github.com/vercel/geist-font (`fonts/GeistMono/ttf/`) | `OFL-GeistMono.txt` |
+
+## egui default fonts (`epaint_default_fonts` crate)
+
+egui embeds these fonts through the `epaint_default_fonts` crate (crate licence:
+`(MIT OR Apache-2.0) AND OFL-1.1 AND Ubuntu-font-1.0`). They are embedded unmodified; the licence
+texts are in the crate's `fonts/` folder.
+
+| File | Font | Copyright | Licence |
+|---|---|---|---|
+| `Ubuntu-Light.ttf` | Ubuntu Light (egui's default proportional font) | © 2011 Canonical Ltd. | Ubuntu Font Licence 1.0, `UFL.txt` |
+| `NotoEmoji-Regular.ttf` | Noto Emoji | © 2013 Google Inc. | SIL OFL 1.1, `OFL.txt` |
+| `Hack-Regular.ttf` | Hack (egui's default monospace font) | © 2018 Source Foundry Authors; Bitstream Vera © 2003 Bitstream, Inc.; DejaVu changes public domain | MIT and Bitstream Vera License, `Hack-Regular.txt` |
+| `emoji-icon-font.ttf` | emoji-icon-font | © 2014 John Slegers | MIT, `emoji-icon-font-mit-license.txt` |
+
+## Icons (`assets/icons/`)
+
+| Files | Set | Copyright | Source | Licence |
+|---|---|---|---|---|
+| `*.svg` | Lucide | © Lucide Icons and Contributors | https://lucide.dev | ISC, `LICENSE-lucide.txt` |
+
+## App icon (`assets/app-icon/`)
+
+The raven, the Crafting Apps' shared icon, made by the ArtCraft team; not open source, used on the
+brand terms below. See `assets/app-icon/LICENSE.txt`.
+
+## Brand (`docs/brand/`)
+
+The ArtCraft name, wordmark and logos are trademarks of the ArtCraft Team and are not covered by
+the open-source licence; see `docs/brand/LICENSE-brand.txt`.
+
+## Optional build-time fonts
+
+Builds made with `CRAFT_FONTS_DIR` (craft-fonts), releases among them, also embed the fonts in
+that folder (all OFL-1.1). Their packages ship each one's licence as `OFL-.txt`; craft-fonts'
+own `ATTRIBUTION.md` names their authors and pinned sources.
diff --git a/desktop/CLAUDE.md b/desktop/CLAUDE.md
new file mode 100644
index 0000000..b96698b
--- /dev/null
+++ b/desktop/CLAUDE.md
@@ -0,0 +1,11 @@
+# Craft Anything
+
+The rules below are loaded into every session (imports). They are mandatory for every change: follow them
+without being asked, unless the person explicitly says otherwise for a specific task.
+
+@AGENTS.md
+@docs/pitfalls.md
+
+The plan and assignments are `docs/ROADMAP.md` (open work: `cargo xtask pending`); what works today is
+`docs/STATUS.md` (with `docs/tool-status.md`); where additions go is `docs/extending.md`. Private plans may
+live in `plan/` (gitignored) when present.
diff --git a/desktop/CONTRIBUTING.md b/desktop/CONTRIBUTING.md
new file mode 100644
index 0000000..12881f1
--- /dev/null
+++ b/desktop/CONTRIBUTING.md
@@ -0,0 +1,31 @@
+# Contributing
+
+Thanks for helping. The rules every change follows (people and AI agents alike) are in
+[AGENTS.md](AGENTS.md); the mistakes they come from are in [docs/pitfalls.md](docs/pitfalls.md).
+In short:
+
+- **Pick work from [docs/ROADMAP.md](docs/ROADMAP.md)** (`cargo xtask pending` lists what's open).
+ Claim it first: add `— claimed: , branch ` to its line and open a draft PR,
+ so nobody else does the same work.
+- **One change per PR**, on a branch named `feature/`, `bugfix/`, `docs/`, `test/`, `refactor/` or
+ `chore/` plus the roadmap id (`feature/r1.4-provider-key-ui`). PRs are squash-merged, so the
+ title is the commit message: plain English, what changed for the person or developer.
+- **Where things go:** [docs/extending.md](docs/extending.md) (tools, target kinds, checks, coding
+ agents, engines, plugins).
+- **Never crash, safe Rust only, every external program through `craft_anything_platform::process`,
+ no secrets or personal data anywhere.** Everything is our own work: no code, names or wording copied from other projects.
+
+Before you ask for review:
+
+```sh
+cargo fmt --all
+cargo test -p
+cargo clippy -p --all-targets -- -D warnings
+cargo xtask layers
+cargo xtask secrets
+```
+
+Enable the secret-scan hook once per clone: `git config core.hooksPath .githooks`.
+
+Code is dual-licensed under MIT or Apache-2.0 ([LICENSE-MIT](LICENSE-MIT),
+[LICENSE-APACHE](LICENSE-APACHE)); by contributing you agree your work is licensed the same way.
diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock
new file mode 100644
index 0000000..3f74780
--- /dev/null
+++ b/desktop/Cargo.lock
@@ -0,0 +1,8402 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "accesskit"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320"
+dependencies = [
+ "enumn",
+ "serde",
+ "uuid",
+]
+
+[[package]]
+name = "accesskit_atspi_common"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e8c61bee90b42a772d39d06a740207dc71a4e780004ace1db8d99fb1baaa954"
+dependencies = [
+ "accesskit",
+ "accesskit_consumer 0.36.0",
+ "atspi-common",
+ "phf 0.13.1",
+ "serde",
+ "zvariant 5.15.0",
+]
+
+[[package]]
+name = "accesskit_consumer"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53cf47daed85312e763fbf85ceca136e0d7abc68e0a7e12abe11f48172bc3b10"
+dependencies = [
+ "accesskit",
+ "hashbrown 0.16.1",
+]
+
+[[package]]
+name = "accesskit_consumer"
+version = "0.36.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25e0d7e25d06f4dc21d1774d67146e9e80d6789216cbd4d1e88185b0095dba60"
+dependencies = [
+ "accesskit",
+ "hashbrown 0.16.1",
+]
+
+[[package]]
+name = "accesskit_consumer"
+version = "0.38.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d10a236f96f87d70732e44520046785431ef01d5bcd6b041317bfadd2f88245"
+dependencies = [
+ "accesskit",
+ "hashbrown 0.16.1",
+]
+
+[[package]]
+name = "accesskit_macos"
+version = "0.26.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce02dc63b43f0c9296af9ac946312a2dc8814427d7a64d2d600971dac55b6076"
+dependencies = [
+ "accesskit",
+ "accesskit_consumer 0.38.0",
+ "hashbrown 0.16.1",
+ "objc2 0.5.3",
+ "objc2-app-kit 0.2.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "accesskit_unix"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b016ca8db0ea0ea2ceff29a9d6240391492d960716aa471967c00e8cc8cb197c"
+dependencies = [
+ "accesskit",
+ "accesskit_atspi_common",
+ "async-channel",
+ "async-executor",
+ "async-task",
+ "atspi",
+ "futures-lite",
+ "futures-util",
+ "serde",
+ "zbus 5.19.0",
+]
+
+[[package]]
+name = "accesskit_windows"
+version = "0.32.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eff7009f1a532e917d66970a1e80c965140c6cfbbabbdde3d64e5431e6c78e21"
+dependencies = [
+ "accesskit",
+ "accesskit_consumer 0.35.0",
+ "hashbrown 0.16.1",
+ "static_assertions",
+ "windows",
+ "windows-core",
+]
+
+[[package]]
+name = "accesskit_winit"
+version = "0.32.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fe9a94394896352cc4660ca2288bd4ef883d83238853c038b44070c8f134313"
+dependencies = [
+ "accesskit",
+ "accesskit_macos",
+ "accesskit_unix",
+ "accesskit_windows",
+ "raw-window-handle",
+ "winit",
+]
+
+[[package]]
+name = "addr2line"
+version = "0.26.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "aes"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures 0.2.17",
+]
+
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "getrandom 0.3.4",
+ "once_cell",
+ "serde",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "android-activity"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd"
+dependencies = [
+ "android-properties",
+ "bitflags 2.13.2",
+ "cc",
+ "jni",
+ "libc",
+ "log",
+ "ndk",
+ "ndk-context",
+ "ndk-sys",
+ "num_enum",
+ "thiserror 2.0.21",
+]
+
+[[package]]
+name = "android-properties"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "annotate-snippets"
+version = "0.11.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4"
+dependencies = [
+ "anstyle",
+ "unicode-width",
+]
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
+
+[[package]]
+name = "arbitrary"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed"
+
+[[package]]
+name = "arboard"
+version = "3.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf"
+dependencies = [
+ "clipboard-win",
+ "image",
+ "log",
+ "objc2 0.6.5",
+ "objc2-app-kit 0.3.2",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-foundation 0.3.2",
+ "parking_lot",
+ "percent-encoding",
+ "windows-sys 0.60.2",
+ "x11rb",
+]
+
+[[package]]
+name = "arraydeque"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
+
+[[package]]
+name = "arrayref"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
+
+[[package]]
+name = "arrayvec"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
+
+[[package]]
+name = "as-raw-xcb-connection"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b"
+
+[[package]]
+name = "ash"
+version = "0.38.0+1.3.281"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f"
+dependencies = [
+ "libloading",
+]
+
+[[package]]
+name = "async-broadcast"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-channel"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
+dependencies = [
+ "concurrent-queue",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-fs"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5"
+dependencies = [
+ "async-lock",
+ "blocking",
+ "futures-lite",
+]
+
+[[package]]
+name = "async-io"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite",
+ "parking",
+ "polling",
+ "rustix 1.1.5",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-process"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
+dependencies = [
+ "async-channel",
+ "async-io",
+ "async-lock",
+ "async-signal",
+ "async-task",
+ "blocking",
+ "cfg-if",
+ "event-listener",
+ "futures-lite",
+ "rustix 1.1.5",
+]
+
+[[package]]
+name = "async-recursion"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f8abc12baad266b1c8cec146854c195b5864b4221d4b2ca7296a7ae82d9e451"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "async-signal"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "atomic-waker",
+ "cfg-if",
+ "futures-core",
+ "futures-io",
+ "rustix 1.1.5",
+ "signal-hook-registry",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-task"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
+
+[[package]]
+name = "async-trait"
+version = "0.1.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "atspi"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c77886257be21c9cd89a4ae7e64860c6f0eefca799bb79127913052bd0eefb3d"
+dependencies = [
+ "atspi-common",
+ "atspi-proxies",
+]
+
+[[package]]
+name = "atspi-common"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20c5617155740c98003016429ad13fe43ce7a77b007479350a9f8bf95a29f63d"
+dependencies = [
+ "enumflags2",
+ "serde",
+ "static_assertions",
+ "zbus 5.19.0",
+ "zbus-lockstep",
+ "zbus-lockstep-macros",
+ "zbus_names 4.3.4",
+ "zvariant 5.15.0",
+]
+
+[[package]]
+name = "atspi-proxies"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc"
+dependencies = [
+ "atspi-common",
+ "serde",
+ "zbus 5.19.0",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "base16ct"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "base64"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
+
+[[package]]
+name = "base64-simd"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195"
+dependencies = [
+ "outref",
+ "vsimd",
+]
+
+[[package]]
+name = "base64ct"
+version = "1.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
+
+[[package]]
+name = "bindgen"
+version = "0.72.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
+dependencies = [
+ "annotate-snippets",
+ "bitflags 2.13.2",
+ "cexpr",
+ "clang-sys",
+ "itertools 0.13.0",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash 2.1.3",
+ "shlex 1.3.0",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "bit-set"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "bitvec"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block-padding"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block2"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f"
+dependencies = [
+ "objc2 0.5.3",
+]
+
+[[package]]
+name = "block2"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
+dependencies = [
+ "objc2 0.6.5",
+]
+
+[[package]]
+name = "blocking"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa"
+dependencies = [
+ "async-channel",
+ "async-task",
+ "futures-io",
+ "futures-lite",
+ "piper",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+dependencies = [
+ "allocator-api2",
+]
+
+[[package]]
+name = "bytecount"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
+
+[[package]]
+name = "bytemuck"
+version = "1.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
+dependencies = [
+ "bytemuck_derive",
+]
+
+[[package]]
+name = "bytemuck_derive"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "byteorder-lite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
+
+[[package]]
+name = "bytes"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
+
+[[package]]
+name = "bzip2-rs"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "beeb59e7e4c811ab37cc73680c798c7a5da77fc9989c62b09138e31ee740f735"
+dependencies = [
+ "crc32fast",
+ "tinyvec",
+]
+
+[[package]]
+name = "calloop"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec"
+dependencies = [
+ "bitflags 2.13.2",
+ "log",
+ "polling",
+ "rustix 0.38.44",
+ "slab",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "calloop"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43b32313e937c8721c9d17ee9bf60699a6402b4b7c2cb6ac1bee0ffcaa4b4e5e"
+dependencies = [
+ "bitflags 2.13.2",
+ "polling",
+ "rustix 1.1.5",
+ "tracing",
+]
+
+[[package]]
+name = "calloop-wayland-source"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20"
+dependencies = [
+ "calloop 0.13.0",
+ "rustix 0.38.44",
+ "wayland-backend",
+ "wayland-client",
+]
+
+[[package]]
+name = "calloop-wayland-source"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa"
+dependencies = [
+ "calloop 0.14.5",
+ "rustix 1.1.5",
+ "wayland-backend",
+ "wayland-client",
+]
+
+[[package]]
+name = "castaway"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "cbc"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
+name = "cc"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838"
+dependencies = [
+ "find-msvc-tools",
+ "jobserver",
+ "libc",
+ "shlex 2.0.1",
+]
+
+[[package]]
+name = "cexpr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+dependencies = [
+ "nom 7.1.3",
+]
+
+[[package]]
+name = "cfg-expr"
+version = "0.20.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ba9e9ec16c447027685b1f897b720e18e9a8afd00bd7332c483537e38086c9f"
+dependencies = [
+ "smallvec",
+ "target-lexicon",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
+
+[[package]]
+name = "cgl"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "chacha20"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.1",
+ "rand_core 0.10.1",
+]
+
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
+[[package]]
+name = "clang-sys"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading",
+]
+
+[[package]]
+name = "clap"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
+
+[[package]]
+name = "clipboard-win"
+version = "5.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4"
+dependencies = [
+ "error-code",
+]
+
+[[package]]
+name = "cobs"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
+dependencies = [
+ "thiserror 2.0.21",
+]
+
+[[package]]
+name = "codespan-reporting"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681"
+dependencies = [
+ "serde",
+ "termcolor",
+ "unicode-width",
+]
+
+[[package]]
+name = "color"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ec7c5eb7a16992b1904d76c517d170ab353b0e0b3d5a0c81a8a0cd1037893cf"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "color_quant"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "colored"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
+dependencies = [
+ "lazy_static",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "combine"
+version = "4.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
+[[package]]
+name = "compact_str"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79fcda08c33bb58b97008b2cdada6622500e949e060f5913361763121abd2416"
+dependencies = [
+ "castaway",
+ "cfg-if",
+ "itoa",
+ "static_assertions",
+ "zmij",
+]
+
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
+[[package]]
+name = "cookie-factory"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2"
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "core-graphics"
+version = "0.23.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation 0.9.4",
+ "core-graphics-types",
+ "foreign-types",
+ "libc",
+]
+
+[[package]]
+name = "core-graphics-types"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation 0.9.4",
+ "libc",
+]
+
+[[package]]
+name = "core_detect"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48"
+
+[[package]]
+name = "cow-utils"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79"
+
+[[package]]
+name = "cpp_demangle"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0667304c32ea56cb4cd6d2d7c0cfe9a2f8041229db8c033af7f8d69492429def"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "craft-anything"
+version = "0.3.0"
+dependencies = [
+ "craft-anything-ai",
+ "craft-anything-engine",
+ "craft-anything-platform",
+ "craft-anything-setup",
+ "craft-anything-ui-egui",
+ "eframe",
+ "log",
+ "rfd",
+ "serde_json",
+ "winresource",
+]
+
+[[package]]
+name = "craft-anything-ai"
+version = "0.3.0"
+dependencies = [
+ "craft-anything-engine",
+ "craft-anything-engines",
+ "craft-anything-evidence",
+ "craft-anything-formats",
+ "craft-anything-platform",
+ "craft-anything-testkit",
+ "dirs",
+ "keyring",
+ "log",
+ "schemars",
+ "serde",
+ "serde_json",
+ "tempfile",
+ "toml 0.9.12+spec-1.1.0",
+ "ureq",
+]
+
+[[package]]
+name = "craft-anything-analysis"
+version = "0.3.0"
+dependencies = [
+ "craft-anything-disasm",
+ "craft-anything-evidence",
+ "craft-anything-formats",
+ "craft-anything-js",
+ "craft-anything-testkit",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "craft-anything-cli"
+version = "0.3.0"
+dependencies = [
+ "clap",
+ "craft-anything-ai",
+ "craft-anything-engine",
+ "craft-anything-engines",
+ "craft-anything-evidence",
+ "craft-anything-mcp",
+ "craft-anything-platform",
+ "craft-anything-setup",
+ "ctrlc",
+ "log",
+ "serde",
+ "serde_json",
+ "tempfile",
+ "winresource",
+]
+
+[[package]]
+name = "craft-anything-disasm"
+version = "0.3.0"
+dependencies = [
+ "craft-anything-formats",
+ "craft-anything-testkit",
+ "iced-x86",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.21",
+ "yaxpeax-arch",
+ "yaxpeax-arm",
+]
+
+[[package]]
+name = "craft-anything-engine"
+version = "0.3.0"
+dependencies = [
+ "base64 0.22.1",
+ "bitflags 2.13.2",
+ "craft-anything-analysis",
+ "craft-anything-disasm",
+ "craft-anything-engines",
+ "craft-anything-evidence",
+ "craft-anything-formats",
+ "craft-anything-js",
+ "craft-anything-platform",
+ "craft-anything-testkit",
+ "dirs",
+ "hex",
+ "image",
+ "log",
+ "png 0.17.16",
+ "regex",
+ "schemars",
+ "serde",
+ "serde_json",
+ "sha1",
+ "sha2",
+ "tempfile",
+ "thiserror 2.0.21",
+ "toml 0.9.12+spec-1.1.0",
+]
+
+[[package]]
+name = "craft-anything-engines"
+version = "0.3.0"
+dependencies = [
+ "base64 0.22.1",
+ "craft-anything-evidence",
+ "craft-anything-formats",
+ "craft-anything-platform",
+ "craft-anything-testkit",
+ "hex",
+ "log",
+ "serde",
+ "serde_json",
+ "sha1",
+ "sha2",
+ "tempfile",
+ "thiserror 2.0.21",
+ "toml 0.9.12+spec-1.1.0",
+ "ureq",
+ "wasmtime",
+]
+
+[[package]]
+name = "craft-anything-evidence"
+version = "0.3.0"
+dependencies = [
+ "hex",
+ "serde",
+ "serde_json",
+ "sha2",
+ "thiserror 2.0.21",
+]
+
+[[package]]
+name = "craft-anything-formats"
+version = "0.3.0"
+dependencies = [
+ "bzip2-rs",
+ "craft-anything-testkit",
+ "lzma-rs",
+ "miniz_oxide 0.8.9",
+ "object 0.36.7",
+ "p256",
+ "p384",
+ "rsa",
+ "rustc-demangle",
+ "ruzstd",
+ "serde",
+ "serde_json",
+ "sha1",
+ "sha2",
+ "thiserror 2.0.21",
+ "wasmparser 0.261.0",
+ "wasmprinter 0.261.0",
+]
+
+[[package]]
+name = "craft-anything-js"
+version = "0.3.0"
+dependencies = [
+ "craft-anything-testkit",
+ "hex",
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_ast_visit",
+ "oxc_codegen",
+ "oxc_parser",
+ "oxc_semantic",
+ "oxc_span",
+ "oxc_syntax",
+ "serde",
+ "serde_json",
+ "sha2",
+ "thiserror 2.0.21",
+]
+
+[[package]]
+name = "craft-anything-mcp"
+version = "0.3.0"
+dependencies = [
+ "craft-anything-engine",
+ "craft-anything-evidence",
+ "craft-anything-platform",
+ "log",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "craft-anything-platform"
+version = "0.3.0"
+dependencies = [
+ "craft-anything-evidence",
+ "hex",
+ "log",
+ "nix 0.31.3",
+ "portable-pty",
+ "process-wrap",
+ "serde",
+ "serde_json",
+ "sha2",
+ "tempfile",
+ "vt100",
+ "windows",
+ "xcap",
+]
+
+[[package]]
+name = "craft-anything-setup"
+version = "0.3.0"
+dependencies = [
+ "craft-anything-engine",
+ "craft-anything-engines",
+ "craft-anything-evidence",
+ "craft-anything-formats",
+ "craft-anything-platform",
+ "log",
+ "schemars",
+ "serde",
+ "serde_json",
+ "tempfile",
+ "toml 0.9.12+spec-1.1.0",
+ "toml_edit",
+ "yaml-rust2",
+]
+
+[[package]]
+name = "craft-anything-testkit"
+version = "0.3.0"
+dependencies = [
+ "miniz_oxide 0.8.9",
+ "p256",
+ "rand 0.8.8",
+ "rsa",
+ "serde_json",
+ "sha1",
+ "sha2",
+]
+
+[[package]]
+name = "craft-anything-ui-egui"
+version = "0.3.0"
+dependencies = [
+ "craft-anything-ai",
+ "craft-anything-engine",
+ "craft-anything-engines",
+ "craft-anything-platform",
+ "craft-anything-setup",
+ "craft-anything-testkit",
+ "eframe",
+ "egui",
+ "egui_extras",
+ "egui_kittest",
+ "log",
+ "pollster 0.4.0",
+ "rfd",
+ "serde",
+ "serde_json",
+ "sys-locale",
+ "tempfile",
+]
+
+[[package]]
+name = "craft-anything-web"
+version = "0.3.0"
+dependencies = [
+ "craft-anything-analysis",
+ "craft-anything-evidence",
+ "craft-anything-formats",
+ "eframe",
+ "egui",
+ "log",
+ "rfd",
+ "serde_json",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "cranelift-assembler-x64"
+version = "0.136.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9c587da2696fbaa58e7a2fa2e5bfdd365d0928219f36992304ac1f15e5bc254"
+dependencies = [
+ "cranelift-assembler-x64-meta",
+]
+
+[[package]]
+name = "cranelift-assembler-x64-meta"
+version = "0.136.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f28a60027b7556b680b051651291053ec381edf4ffbc01784997ae51a64269bb"
+dependencies = [
+ "cranelift-srcgen",
+]
+
+[[package]]
+name = "cranelift-bforest"
+version = "0.136.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62bfdb846acebfd591d601994ea283c98d525ac0bc9a84ca3f2c36bcf5b69ff1"
+dependencies = [
+ "cranelift-entity",
+ "wasmtime-internal-core",
+]
+
+[[package]]
+name = "cranelift-bitset"
+version = "0.136.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa9f8cc1080739d5f72b4ee7e00aff7d589f1284beaf0a19472c07878be7f299"
+dependencies = [
+ "serde",
+ "serde_derive",
+ "wasmtime-internal-core",
+]
+
+[[package]]
+name = "cranelift-codegen"
+version = "0.136.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d85417d493ecec833b5134546b4a252c78cc0bd5be11729a35d25dcff9e4e70b"
+dependencies = [
+ "bumpalo",
+ "cranelift-assembler-x64",
+ "cranelift-bforest",
+ "cranelift-bitset",
+ "cranelift-codegen-meta",
+ "cranelift-codegen-shared",
+ "cranelift-control",
+ "cranelift-entity",
+ "cranelift-isle",
+ "gimli",
+ "hashbrown 0.17.1",
+ "libm",
+ "log",
+ "postcard",
+ "pulley-interpreter",
+ "regalloc2",
+ "rustc-hash 2.1.3",
+ "serde",
+ "serde_derive",
+ "sha2",
+ "smallvec",
+ "target-lexicon",
+ "wasmtime-internal-core",
+]
+
+[[package]]
+name = "cranelift-codegen-meta"
+version = "0.136.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d01c8ed0098360bae69ad6b82952d4247516799ce8050d45ad168d6689f4eae6"
+dependencies = [
+ "cranelift-assembler-x64-meta",
+ "cranelift-codegen-shared",
+ "cranelift-srcgen",
+ "heck",
+ "pulley-interpreter",
+]
+
+[[package]]
+name = "cranelift-codegen-shared"
+version = "0.136.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04f9b86d809cec9c4e88b201f1b3ed3b0347674c9ccfbb8c355228669e34be81"
+
+[[package]]
+name = "cranelift-control"
+version = "0.136.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce182038fb10b719841e65fdf5deeee258c684f63439f5be680c2be059274b58"
+dependencies = [
+ "arbitrary",
+]
+
+[[package]]
+name = "cranelift-entity"
+version = "0.136.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bca812b03a7b942796391f25bc02aa70a1a007fee3ce2756490c788429e9c39"
+dependencies = [
+ "cranelift-bitset",
+ "serde",
+ "serde_derive",
+ "wasmtime-internal-core",
+]
+
+[[package]]
+name = "cranelift-frontend"
+version = "0.136.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0c30563d2aef95e8d1a00a920199c99117b350339599f0e9487a7a1cf5d59de"
+dependencies = [
+ "cranelift-codegen",
+ "hashbrown 0.17.1",
+ "log",
+ "smallvec",
+ "target-lexicon",
+]
+
+[[package]]
+name = "cranelift-isle"
+version = "0.136.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c54184333a275698edb1d3bdff226131a25d47133d792aa716e47e32b8b7f05"
+
+[[package]]
+name = "cranelift-native"
+version = "0.136.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb67a4c8a3180b5f1d77dffcd6317c05386ced42e5a31c7004523a9d99dc85c1"
+dependencies = [
+ "cranelift-codegen",
+ "libc",
+ "target-lexicon",
+]
+
+[[package]]
+name = "cranelift-srcgen"
+version = "0.136.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b533db1679b4b728568bd490da6533cf50974c48a4d7534e78f1e0e02bc39769"
+
+[[package]]
+name = "crc"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
+
+[[package]]
+name = "crc32fast"
+version = "1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
+
+[[package]]
+name = "crunchy"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+
+[[package]]
+name = "crypto-bigint"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
+dependencies = [
+ "generic-array",
+ "rand_core 0.6.4",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "ctrlc"
+version = "3.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162"
+dependencies = [
+ "dispatch2",
+ "nix 0.31.3",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "cursor-icon"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
+
+[[package]]
+name = "data-url"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
+
+[[package]]
+name = "dbus"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e"
+dependencies = [
+ "libc",
+ "libdbus-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "dbus-secret-service"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6"
+dependencies = [
+ "aes",
+ "block-padding",
+ "cbc",
+ "dbus",
+ "fastrand",
+ "hkdf",
+ "num",
+ "once_cell",
+ "sha2",
+ "zeroize",
+]
+
+[[package]]
+name = "der"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
+dependencies = [
+ "const-oid",
+ "pem-rfc7468",
+ "zeroize",
+]
+
+[[package]]
+name = "dify"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90ce0fb972943b4e88cd03b8f92953df0c71bb05e0bde8e5b684895d808013cc"
+dependencies = [
+ "anyhow",
+ "colored",
+ "getopts",
+ "image",
+ "rayon",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "const-oid",
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "dirs"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "dispatch"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
+
+[[package]]
+name = "dispatch2"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
+dependencies = [
+ "bitflags 2.13.2",
+ "block2 0.6.2",
+ "libc",
+ "objc2 0.6.5",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "dlib"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a"
+dependencies = [
+ "libloading",
+]
+
+[[package]]
+name = "document-features"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
+dependencies = [
+ "litrs",
+]
+
+[[package]]
+name = "downcast-rs"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
+
+[[package]]
+name = "dpi"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
+
+[[package]]
+name = "dragonbox_ecma"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd8e701084c37e7ef62d3f9e453b618130cbc0ef3573847785952a3ac3f746bf"
+
+[[package]]
+name = "drm"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80bc8c5c6c2941f70a55c15f8d9f00f9710ebda3ffda98075f996a0e6c92756f"
+dependencies = [
+ "bitflags 2.13.2",
+ "bytemuck",
+ "drm-ffi",
+ "drm-fourcc",
+ "libc",
+ "rustix 0.38.44",
+]
+
+[[package]]
+name = "drm"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a41816e58f47f49acfd956651055ddcf137c4882c2098c30c448817af21183a"
+dependencies = [
+ "bitflags 2.13.2",
+ "bytemuck",
+ "bytemuck_derive",
+ "drm-ffi",
+ "drm-fourcc",
+ "libc",
+ "rustix 1.1.5",
+]
+
+[[package]]
+name = "drm-ffi"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51a91c9b32ac4e8105dec255e849e0d66e27d7c34d184364fb93e469db08f690"
+dependencies = [
+ "drm-sys",
+ "rustix 1.1.5",
+]
+
+[[package]]
+name = "drm-fourcc"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4"
+
+[[package]]
+name = "drm-sys"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc8e1361066d91f5ffccff060a3c3be9c3ecde15be2959c1937595f7a82a9f8"
+dependencies = [
+ "libc",
+ "linux-raw-sys 0.9.4",
+]
+
+[[package]]
+name = "dyn-clone"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
+
+[[package]]
+name = "ecdsa"
+version = "0.16.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
+dependencies = [
+ "der",
+ "digest",
+ "elliptic-curve",
+ "rfc6979",
+ "signature",
+ "spki",
+]
+
+[[package]]
+name = "ecolor"
+version = "0.36.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0656a7530b9e2841ee7a5f983965bdd9840c32f4012f759cfd44b912cced6b34"
+dependencies = [
+ "bytemuck",
+ "emath",
+ "serde",
+]
+
+[[package]]
+name = "eframe"
+version = "0.36.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55ad41fac6e3149abb4866e93df37dbd33baa95db525c3368c944584e95a5dc2"
+dependencies = [
+ "ahash",
+ "bytemuck",
+ "document-features",
+ "egui",
+ "egui-wgpu",
+ "egui-winit",
+ "egui_glow",
+ "glutin",
+ "glutin-winit",
+ "image",
+ "js-sys",
+ "log",
+ "objc2 0.6.5",
+ "objc2-app-kit 0.3.2",
+ "objc2-foundation 0.3.2",
+ "parking_lot",
+ "percent-encoding",
+ "pollster 1.0.1",
+ "profiling",
+ "raw-window-handle",
+ "ron",
+ "serde",
+ "static_assertions",
+ "wasm-bindgen",
+ "web-sys",
+ "web-time",
+ "wgpu",
+ "windows-sys 0.61.2",
+ "winit",
+]
+
+[[package]]
+name = "egui"
+version = "0.36.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc938cc27cd911415e1e4d151bc56ff9df063d8db081374e87aeb805ea74b2ba"
+dependencies = [
+ "accesskit",
+ "ahash",
+ "bitflags 2.13.2",
+ "emath",
+ "epaint",
+ "itertools 0.15.0",
+ "log",
+ "nohash-hasher",
+ "profiling",
+ "ron",
+ "serde",
+ "smallvec",
+ "unicode-segmentation",
+ "web-sys",
+]
+
+[[package]]
+name = "egui-wgpu"
+version = "0.36.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cef1f6d1bba40b09a4d0d62cb822b81030922cfbf65c5373398c3a3caafc5bd"
+dependencies = [
+ "ahash",
+ "bytemuck",
+ "document-features",
+ "egui",
+ "epaint",
+ "log",
+ "profiling",
+ "thiserror 2.0.21",
+ "type-map",
+ "web-time",
+ "wgpu",
+ "winit",
+]
+
+[[package]]
+name = "egui-winit"
+version = "0.36.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98466000559d66ba4db786a30448b01483946add4188e7f10f522a27b4fb89cc"
+dependencies = [
+ "accesskit_winit",
+ "arboard",
+ "bytemuck",
+ "egui",
+ "log",
+ "objc2 0.6.5",
+ "objc2-foundation 0.3.2",
+ "objc2-ui-kit 0.3.2",
+ "profiling",
+ "raw-window-handle",
+ "serde",
+ "smithay-clipboard",
+ "web-time",
+ "webbrowser",
+ "winit",
+]
+
+[[package]]
+name = "egui_extras"
+version = "0.36.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05cbdc785020861553f230f23d330ba1fb72613b9d8e0e457f3bdc8ba6db6dcf"
+dependencies = [
+ "ahash",
+ "egui",
+ "enum-map",
+ "itertools 0.15.0",
+ "log",
+ "profiling",
+ "resvg",
+]
+
+[[package]]
+name = "egui_glow"
+version = "0.36.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "606ed4167daf341e5753d703285c4c1d562324b2c07aa8f405cd7d066fe58f3b"
+dependencies = [
+ "bytemuck",
+ "egui",
+ "glow",
+ "log",
+ "profiling",
+ "winit",
+]
+
+[[package]]
+name = "egui_kittest"
+version = "0.36.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eb3f3effb2726fee23449293e67acbf6cc6ce35525b1722bd5743bb348114d5"
+dependencies = [
+ "dify",
+ "eframe",
+ "egui",
+ "egui-wgpu",
+ "image",
+ "kittest",
+ "log",
+ "open",
+ "pollster 1.0.1",
+ "serde",
+ "tempfile",
+ "toml 1.1.8+spec-1.1.0",
+ "wgpu",
+]
+
+[[package]]
+name = "either"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be"
+
+[[package]]
+name = "elliptic-curve"
+version = "0.13.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
+dependencies = [
+ "base16ct",
+ "crypto-bigint",
+ "digest",
+ "ff",
+ "generic-array",
+ "group",
+ "pem-rfc7468",
+ "pkcs8",
+ "rand_core 0.6.4",
+ "sec1",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "emath"
+version = "0.36.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52c8d4141cf0f60fb2f7aefaf352d3ffa5171ef0ab09731c64391801e95e48f1"
+dependencies = [
+ "bytemuck",
+ "serde",
+]
+
+[[package]]
+name = "embedded-io"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
+
+[[package]]
+name = "embedded-io"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679"
+dependencies = [
+ "cfg-if",
+ "core_detect",
+ "multiversion_no_op",
+ "rustversion",
+ "scopeguard",
+ "simdutf8",
+]
+
+[[package]]
+name = "endi"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
+
+[[package]]
+name = "enum-map"
+version = "2.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9"
+dependencies = [
+ "enum-map-derive",
+]
+
+[[package]]
+name = "enum-map-derive"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "enumflags2"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
+dependencies = [
+ "enumflags2_derive",
+ "serde",
+]
+
+[[package]]
+name = "enumflags2_derive"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "enumn"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "epaint"
+version = "0.36.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0ec0308dc23130fc1a7d4623a8534c2ed069c6dc84e258ee4f3de99615aa1a3"
+dependencies = [
+ "ahash",
+ "bytemuck",
+ "ecolor",
+ "emath",
+ "epaint_default_fonts",
+ "font-types",
+ "harfrust",
+ "log",
+ "nohash-hasher",
+ "parking_lot",
+ "profiling",
+ "self_cell",
+ "serde",
+ "skrifa",
+ "smallvec",
+ "unicode-general-category",
+ "unicode-segmentation",
+ "vello_cpu",
+]
+
+[[package]]
+name = "epaint_default_fonts"
+version = "0.36.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773fa9c96dd0dbef887e39d0ed6177f141cce4d68e6041df77570aa3702dfa13"
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "error-code"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7"
+
+[[package]]
+name = "escape-simd"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "120339709b24832c07464b0838828361b735d793c409b8ca3147db0dae601542"
+
+[[package]]
+name = "euclid"
+version = "0.22.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "event-listener"
+version = "5.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
+dependencies = [
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
+
+[[package]]
+name = "fax"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a"
+
+[[package]]
+name = "fdeflate"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
+dependencies = [
+ "simd-adler32",
+]
+
+[[package]]
+name = "fearless_simd"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97b65636e5b9ef369943878ac74335ba1c55c1cb6adbf1e2c293c624248d693"
+
+[[package]]
+name = "ff"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
+dependencies = [
+ "rand_core 0.6.4",
+ "subtle",
+]
+
+[[package]]
+name = "filedescriptor"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d"
+dependencies = [
+ "libc",
+ "thiserror 1.0.69",
+ "winapi",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
+
+[[package]]
+name = "flate2"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide 0.9.1",
+ "zlib-rs",
+]
+
+[[package]]
+name = "float-cmp"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
+[[package]]
+name = "font-types"
+version = "0.12.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c92d9081c7ab3bd83ef1eecdfad090f3d966e3ce74106779664114cfe1a6274"
+dependencies = [
+ "bytemuck",
+ "serde",
+]
+
+[[package]]
+name = "foreign-types"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
+dependencies = [
+ "foreign-types-macros",
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-macros"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "funty"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
+
+[[package]]
+name = "futures"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
+
+[[package]]
+name = "futures-io"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
+
+[[package]]
+name = "futures-lite"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "futures-macro"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
+
+[[package]]
+name = "futures-task"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
+
+[[package]]
+name = "futures-util"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "gbm"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce852e998d3ca5e4a97014fb31c940dc5ef344ec7d364984525fd11e8a547e6a"
+dependencies = [
+ "bitflags 2.13.2",
+ "drm 0.14.1",
+ "drm-fourcc",
+ "gbm-sys",
+ "libc",
+ "wayland-backend",
+ "wayland-server",
+]
+
+[[package]]
+name = "gbm-sys"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c13a5f2acc785d8fb6bf6b7ab6bfb0ef5dad4f4d97e8e70bb8e470722312f76f"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+ "zeroize",
+]
+
+[[package]]
+name = "gethostname"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
+dependencies = [
+ "rustix 1.1.5",
+ "windows-link",
+]
+
+[[package]]
+name = "getopts"
+version = "0.2.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 5.3.0",
+ "wasip2",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 6.0.0",
+ "rand_core 0.10.1",
+]
+
+[[package]]
+name = "gif"
+version = "0.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159"
+dependencies = [
+ "color_quant",
+ "weezl",
+]
+
+[[package]]
+name = "gimli"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c"
+dependencies = [
+ "fnv",
+ "hashbrown 0.16.1",
+ "indexmap",
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "gl"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a94edab108827d67608095e269cf862e60d920f144a5026d3dbcfd8b877fb404"
+dependencies = [
+ "gl_generator",
+]
+
+[[package]]
+name = "gl_generator"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d"
+dependencies = [
+ "khronos_api",
+ "log",
+ "xml-rs",
+]
+
+[[package]]
+name = "glifo"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed4a1bb24121291d27230c1b1b44e07d6a9b28cefdb32fe1581dfb84e14f940a"
+dependencies = [
+ "bytemuck",
+ "foldhash 0.2.0",
+ "hashbrown 0.17.1",
+ "log",
+ "peniko",
+ "skrifa",
+ "smallvec",
+ "vello_common",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
+
+[[package]]
+name = "glow"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5"
+dependencies = [
+ "js-sys",
+ "slotmap",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "glutin"
+version = "0.32.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325"
+dependencies = [
+ "bitflags 2.13.2",
+ "cfg_aliases 0.2.2",
+ "cgl",
+ "dispatch2",
+ "glutin_egl_sys",
+ "glutin_glx_sys",
+ "glutin_wgl_sys",
+ "libloading",
+ "objc2 0.6.5",
+ "objc2-app-kit 0.3.2",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.2",
+ "once_cell",
+ "raw-window-handle",
+ "wayland-sys",
+ "windows-sys 0.52.0",
+ "x11-dl",
+]
+
+[[package]]
+name = "glutin-winit"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f"
+dependencies = [
+ "cfg_aliases 0.2.2",
+ "glutin",
+ "raw-window-handle",
+ "winit",
+]
+
+[[package]]
+name = "glutin_egl_sys"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2"
+dependencies = [
+ "gl_generator",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "glutin_glx_sys"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185"
+dependencies = [
+ "gl_generator",
+ "x11-dl",
+]
+
+[[package]]
+name = "glutin_wgl_sys"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e"
+dependencies = [
+ "gl_generator",
+]
+
+[[package]]
+name = "gpu-allocator"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795"
+dependencies = [
+ "ash",
+ "hashbrown 0.16.1",
+ "log",
+ "presser",
+ "thiserror 2.0.21",
+ "windows",
+]
+
+[[package]]
+name = "group"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
+dependencies = [
+ "ff",
+ "rand_core 0.6.4",
+ "subtle",
+]
+
+[[package]]
+name = "guillotiere"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b17e70c989c36bad147b27a58d148c0741c51448aa5653436547323e524d0ab"
+dependencies = [
+ "euclid",
+]
+
+[[package]]
+name = "half"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
+dependencies = [
+ "cfg-if",
+ "crunchy",
+ "num-traits",
+ "zerocopy",
+]
+
+[[package]]
+name = "harfrust"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9"
+dependencies = [
+ "bitflags 2.13.2",
+ "bytemuck",
+ "read-fonts",
+ "smallvec",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash 0.1.5",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash 0.2.0",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+dependencies = [
+ "allocator-api2",
+ "foldhash 0.2.0",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "hashlink"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
+dependencies = [
+ "hashbrown 0.15.5",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hkdf"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
+dependencies = [
+ "hmac",
+]
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "http"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "iced-x86"
+version = "1.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c447cff8c7f384a7d4f741cfcff32f75f3ad02b406432e8d6c878d56b1edf6b"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "serde",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_fallback"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "251af8e57c9400e3eb58242fe5b8b1152b2a64fdf4cf632f923c38ccee6f2fa9"
+dependencies = [
+ "icu_locale_core",
+ "icu_locale_fallback_data",
+ "icu_provider",
+ "potential_utf",
+ "tinystr",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_fallback_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "decf2a22ec8fa68f1a0c1129a3f8583f8f8bc24e8b9ccbe98ead99f62a4dc3a8"
+
+[[package]]
+name = "icu_normalizer"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
+
+[[package]]
+name = "icu_properties"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
+
+[[package]]
+name = "icu_provider"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "serde",
+ "stable_deref_trait",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_segmenter"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82d07aafccd67af15d02512a6adf5896fbc5ed00f2e99b471d2efa14016db3db"
+dependencies = [
+ "icu_collections",
+ "icu_locale_fallback",
+ "icu_provider",
+ "icu_segmenter_data",
+ "potential_utf",
+ "smallvec",
+ "utf8_iter",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_segmenter_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae293c039020f9ec10710af98d29ce6aa2051486638b49c9a6409f3b4a9e98ad"
+
+[[package]]
+name = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "image"
+version = "0.25.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
+dependencies = [
+ "bytemuck",
+ "byteorder-lite",
+ "color_quant",
+ "gif",
+ "image-webp",
+ "moxcms",
+ "num-traits",
+ "png 0.18.1",
+ "tiff",
+ "zune-core",
+ "zune-jpeg",
+]
+
+[[package]]
+name = "image-webp"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
+dependencies = [
+ "byteorder-lite",
+ "quick-error",
+]
+
+[[package]]
+name = "imagesize"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285"
+
+[[package]]
+name = "indexmap"
+version = "2.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.17.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "inout"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
+dependencies = [
+ "block-padding",
+ "generic-array",
+]
+
+[[package]]
+name = "is-docker"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "is-wsl"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
+dependencies = [
+ "is-docker",
+ "once_cell",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itertools"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itertools"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "jni"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
+dependencies = [
+ "cfg-if",
+ "combine",
+ "jni-macros",
+ "jni-sys 0.4.1",
+ "log",
+ "simd_cesu8",
+ "thiserror 2.0.21",
+ "walkdir",
+ "windows-link",
+]
+
+[[package]]
+name = "jni-macros"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "simd_cesu8",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258"
+dependencies = [
+ "jni-sys 0.4.1",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
+dependencies = [
+ "jni-sys-macros",
+]
+
+[[package]]
+name = "jni-sys-macros"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
+dependencies = [
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "jobserver"
+version = "0.1.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
+dependencies = [
+ "getrandom 0.4.3",
+ "libc",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "json-escape-simd"
+version = "3.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6520172708e1b08b95f31bbf76cec064db4203a615cf2fb4e1128d6dd142b033"
+dependencies = [
+ "escape-simd",
+]
+
+[[package]]
+name = "keyring"
+version = "3.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c"
+dependencies = [
+ "byteorder",
+ "dbus-secret-service",
+ "log",
+ "secret-service",
+ "security-framework 2.11.1",
+ "security-framework 3.7.0",
+ "windows-sys 0.60.2",
+ "zbus 4.4.0",
+ "zeroize",
+]
+
+[[package]]
+name = "khronos-egl"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76"
+dependencies = [
+ "libc",
+ "libloading",
+ "pkg-config",
+]
+
+[[package]]
+name = "khronos_api"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
+
+[[package]]
+name = "kittest"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90ceaa75eb0036a32b6b9833962eb18137449e9817e2e586006471925b727fd5"
+dependencies = [
+ "accesskit",
+ "accesskit_consumer 0.35.0",
+]
+
+[[package]]
+name = "kurbo"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62"
+dependencies = [
+ "arrayvec",
+ "euclid",
+ "smallvec",
+]
+
+[[package]]
+name = "kurbo"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2"
+dependencies = [
+ "arrayvec",
+ "euclid",
+ "polycool",
+ "smallvec",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
+dependencies = [
+ "spin",
+]
+
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[package]]
+name = "libc"
+version = "0.2.190"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
+
+[[package]]
+name = "libdbus-sys"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043"
+dependencies = [
+ "pkg-config",
+]
+
+[[package]]
+name = "libloading"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
+dependencies = [
+ "cfg-if",
+ "windows-link",
+]
+
+[[package]]
+name = "libm"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
+
+[[package]]
+name = "libredox"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d"
+dependencies = [
+ "bitflags 2.13.2",
+ "libc",
+ "plain",
+ "redox_syscall 0.9.4",
+]
+
+[[package]]
+name = "libspa"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "882f7427e7989dcc9d388b7f05c4630390a1d7696f9ffa469cd4a7a48f0b4c40"
+dependencies = [
+ "bitflags 2.13.2",
+ "cc",
+ "cookie-factory",
+ "libc",
+ "libspa-sys",
+ "nom 8.0.0",
+ "rustix 1.1.5",
+ "system-deps",
+]
+
+[[package]]
+name = "libspa-sys"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b6e17bdaf63ed0d5e4144022624032b41fd9733112e8c74ac26fc9bf1291924"
+dependencies = [
+ "bindgen",
+ "cc",
+ "system-deps",
+]
+
+[[package]]
+name = "libwayshot-xcap"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea8a46e4d016ef464e386e4970f5415fe0939c2d63abe0c2f539c05ef88c5fe5"
+dependencies = [
+ "drm 0.15.0",
+ "gbm",
+ "gl",
+ "image",
+ "khronos-egl",
+ "memmap2",
+ "rustix 1.1.5",
+ "thiserror 2.0.21",
+ "tracing",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "wayland-protocols-wlr",
+]
+
+[[package]]
+name = "linebender_resource_handle"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "litemap"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
+
+[[package]]
+name = "litrs"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4744e383959f0db86ede514b809b1c53251889093803c05267acc7d4e7030d70"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
+
+[[package]]
+name = "lzma-rs"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e"
+dependencies = [
+ "byteorder",
+ "crc",
+]
+
+[[package]]
+name = "mach2"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b"
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "memfd"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57804b2c9b69967f1536a56f86297e367a33b19e98852ed624b84551cdbc0d90"
+dependencies = [
+ "rustix 1.1.5",
+]
+
+[[package]]
+name = "memmap2"
+version = "0.9.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "moxcms"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
+dependencies = [
+ "num-traits",
+ "pxfm",
+]
+
+[[package]]
+name = "multiversion_no_op"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d"
+
+[[package]]
+name = "naga"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a616d2fb8c89516ac2723a581f69d6c18576046bed761bd6b305e5618e6ae130"
+dependencies = [
+ "arrayvec",
+ "bit-set",
+ "bitflags 2.13.2",
+ "cfg-if",
+ "cfg_aliases 0.2.2",
+ "codespan-reporting",
+ "half",
+ "hashbrown 0.17.1",
+ "indexmap",
+ "libm",
+ "log",
+ "naga-types",
+ "num-traits",
+ "once_cell",
+ "rustc-hash 1.1.0",
+ "spirv",
+ "thiserror 2.0.21",
+ "unicode-ident",
+]
+
+[[package]]
+name = "naga-types"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "590afbf58a6f4f62873cd5cff4468061844bafa1cdf399cc954537c22d768d49"
+dependencies = [
+ "hashbrown 0.17.1",
+ "indexmap",
+ "rustc-hash 1.1.0",
+ "thiserror 2.0.21",
+]
+
+[[package]]
+name = "ndk"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
+dependencies = [
+ "bitflags 2.13.2",
+ "jni-sys 0.3.1",
+ "log",
+ "ndk-sys",
+ "num_enum",
+ "raw-window-handle",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "ndk-context"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
+
+[[package]]
+name = "ndk-sys"
+version = "0.6.0+11769913"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
+dependencies = [
+ "jni-sys 0.3.1",
+]
+
+[[package]]
+name = "nix"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
+dependencies = [
+ "bitflags 2.13.2",
+ "cfg-if",
+ "cfg_aliases 0.1.1",
+ "libc",
+]
+
+[[package]]
+name = "nix"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
+dependencies = [
+ "bitflags 2.13.2",
+ "cfg-if",
+ "cfg_aliases 0.2.2",
+ "libc",
+ "memoffset",
+]
+
+[[package]]
+name = "nix"
+version = "0.31.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
+dependencies = [
+ "bitflags 2.13.2",
+ "cfg-if",
+ "cfg_aliases 0.2.2",
+ "libc",
+]
+
+[[package]]
+name = "nohash-hasher"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "nom"
+version = "8.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "nonmax"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51"
+
+[[package]]
+name = "num"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
+dependencies = [
+ "num-bigint 0.4.8",
+ "num-complex",
+ "num-integer",
+ "num-iter",
+ "num-rational",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint-dig"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
+dependencies = [
+ "lazy_static",
+ "libm",
+ "num-integer",
+ "num-iter",
+ "num-traits",
+ "rand 0.8.8",
+ "smallvec",
+ "zeroize",
+]
+
+[[package]]
+name = "num-complex"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
+dependencies = [
+ "num-bigint 0.4.8",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+ "libm",
+]
+
+[[package]]
+name = "num_enum"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
+dependencies = [
+ "num_enum_derive",
+ "rustversion",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "objc-sys"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310"
+
+[[package]]
+name = "objc2"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19d5490aaf8f1d7cf7688dfa9b0ce07900e168852c45cd2c03f534dfd27cfd0b"
+dependencies = [
+ "objc-sys",
+ "objc2-encode",
+]
+
+[[package]]
+name = "objc2"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8"
+dependencies = [
+ "objc2-encode",
+]
+
+[[package]]
+name = "objc2-app-kit"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
+dependencies = [
+ "bitflags 2.13.2",
+ "block2 0.5.1",
+ "libc",
+ "objc2 0.5.3",
+ "objc2-core-data 0.2.2",
+ "objc2-core-image 0.2.2",
+ "objc2-foundation 0.2.2",
+ "objc2-quartz-core 0.2.2",
+]
+
+[[package]]
+name = "objc2-app-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
+dependencies = [
+ "bitflags 2.13.2",
+ "block2 0.6.2",
+ "libc",
+ "objc2 0.6.5",
+ "objc2-cloud-kit 0.3.2",
+ "objc2-core-data 0.3.2",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-core-image 0.3.2",
+ "objc2-core-text",
+ "objc2-core-video",
+ "objc2-foundation 0.3.2",
+ "objc2-quartz-core 0.3.2",
+]
+
+[[package]]
+name = "objc2-av-foundation"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "478ae33fcac9df0a18db8302387c666b8ef08a3e2d62b510ca4fc278a384b6c0"
+dependencies = [
+ "bitflags 2.13.2",
+ "block2 0.6.2",
+ "dispatch2",
+ "objc2 0.6.5",
+ "objc2-avf-audio",
+ "objc2-core-audio-types",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-core-image 0.3.2",
+ "objc2-core-video",
+ "objc2-foundation 0.3.2",
+ "objc2-image-io",
+ "objc2-media-toolbox",
+ "objc2-quartz-core 0.3.2",
+]
+
+[[package]]
+name = "objc2-avf-audio"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be"
+dependencies = [
+ "objc2 0.6.5",
+ "objc2-foundation 0.3.2",
+]
+
+[[package]]
+name = "objc2-cloud-kit"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
+dependencies = [
+ "bitflags 2.13.2",
+ "block2 0.5.1",
+ "objc2 0.5.3",
+ "objc2-core-location",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-cloud-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c"
+dependencies = [
+ "bitflags 2.13.2",
+ "objc2 0.6.5",
+ "objc2-foundation 0.3.2",
+]
+
+[[package]]
+name = "objc2-contacts"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
+dependencies = [
+ "block2 0.5.1",
+ "objc2 0.5.3",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-core-audio"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2"
+dependencies = [
+ "dispatch2",
+ "objc2 0.6.5",
+ "objc2-core-audio-types",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-core-audio-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c"
+dependencies = [
+ "bitflags 2.13.2",
+ "objc2 0.6.5",
+]
+
+[[package]]
+name = "objc2-core-data"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
+dependencies = [
+ "bitflags 2.13.2",
+ "block2 0.5.1",
+ "objc2 0.5.3",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-core-data"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
+dependencies = [
+ "bitflags 2.13.2",
+ "objc2 0.6.5",
+ "objc2-foundation 0.3.2",
+]
+
+[[package]]
+name = "objc2-core-foundation"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
+dependencies = [
+ "bitflags 2.13.2",
+ "block2 0.6.2",
+ "dispatch2",
+ "libc",
+ "objc2 0.6.5",
+]
+
+[[package]]
+name = "objc2-core-graphics"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
+dependencies = [
+ "bitflags 2.13.2",
+ "block2 0.6.2",
+ "dispatch2",
+ "libc",
+ "objc2 0.6.5",
+ "objc2-core-foundation",
+ "objc2-io-surface",
+ "objc2-metal 0.3.2",
+]
+
+[[package]]
+name = "objc2-core-image"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
+dependencies = [
+ "block2 0.5.1",
+ "objc2 0.5.3",
+ "objc2-foundation 0.2.2",
+ "objc2-metal 0.2.2",
+]
+
+[[package]]
+name = "objc2-core-image"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006"
+dependencies = [
+ "objc2 0.6.5",
+ "objc2-foundation 0.3.2",
+]
+
+[[package]]
+name = "objc2-core-location"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
+dependencies = [
+ "block2 0.5.1",
+ "objc2 0.5.3",
+ "objc2-contacts",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-core-media"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05ec576860167a15dd9fce7fbee7512beb4e31f532159d3482d1f9c6caedf31d"
+dependencies = [
+ "bitflags 2.13.2",
+ "block2 0.6.2",
+ "dispatch2",
+ "objc2 0.6.5",
+ "objc2-core-audio",
+ "objc2-core-audio-types",
+ "objc2-core-foundation",
+ "objc2-core-video",
+]
+
+[[package]]
+name = "objc2-core-text"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"
+dependencies = [
+ "bitflags 2.13.2",
+ "objc2 0.6.5",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+]
+
+[[package]]
+name = "objc2-core-video"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6"
+dependencies = [
+ "bitflags 2.13.2",
+ "block2 0.6.2",
+ "objc2 0.6.5",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-io-surface",
+ "objc2-metal 0.3.2",
+]
+
+[[package]]
+name = "objc2-encode"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
+
+[[package]]
+name = "objc2-foundation"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
+dependencies = [
+ "bitflags 2.13.2",
+ "block2 0.5.1",
+ "dispatch",
+ "libc",
+ "objc2 0.5.3",
+]
+
+[[package]]
+name = "objc2-foundation"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
+dependencies = [
+ "bitflags 2.13.2",
+ "block2 0.6.2",
+ "libc",
+ "objc2 0.6.5",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-image-io"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32b0446e98cf4a784cc7a0177715ff317eeaa8463841c616cfc78aa4f953c4ea"
+dependencies = [
+ "objc2 0.6.5",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+]
+
+[[package]]
+name = "objc2-io-surface"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
+dependencies = [
+ "bitflags 2.13.2",
+ "objc2 0.6.5",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-link-presentation"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
+dependencies = [
+ "block2 0.5.1",
+ "objc2 0.5.3",
+ "objc2-app-kit 0.2.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-media-toolbox"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edd9fdde720df3da7046bb9097811000c1e7ab5cd579fa89d96b27d56781fb30"
+dependencies = [
+ "objc2 0.6.5",
+ "objc2-core-audio-types",
+ "objc2-core-foundation",
+ "objc2-core-media",
+]
+
+[[package]]
+name = "objc2-metal"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
+dependencies = [
+ "bitflags 2.13.2",
+ "block2 0.5.1",
+ "objc2 0.5.3",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-metal"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794"
+dependencies = [
+ "bitflags 2.13.2",
+ "block2 0.6.2",
+ "objc2 0.6.5",
+ "objc2-foundation 0.3.2",
+]
+
+[[package]]
+name = "objc2-quartz-core"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
+dependencies = [
+ "bitflags 2.13.2",
+ "block2 0.5.1",
+ "objc2 0.5.3",
+ "objc2-foundation 0.2.2",
+ "objc2-metal 0.2.2",
+]
+
+[[package]]
+name = "objc2-quartz-core"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
+dependencies = [
+ "bitflags 2.13.2",
+ "objc2 0.6.5",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-foundation 0.3.2",
+ "objc2-metal 0.3.2",
+]
+
+[[package]]
+name = "objc2-symbols"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc"
+dependencies = [
+ "objc2 0.5.3",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-ui-kit"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
+dependencies = [
+ "bitflags 2.13.2",
+ "block2 0.5.1",
+ "objc2 0.5.3",
+ "objc2-cloud-kit 0.2.2",
+ "objc2-core-data 0.2.2",
+ "objc2-core-image 0.2.2",
+ "objc2-core-location",
+ "objc2-foundation 0.2.2",
+ "objc2-link-presentation",
+ "objc2-quartz-core 0.2.2",
+ "objc2-symbols",
+ "objc2-uniform-type-identifiers",
+ "objc2-user-notifications",
+]
+
+[[package]]
+name = "objc2-ui-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
+dependencies = [
+ "bitflags 2.13.2",
+ "objc2 0.6.5",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.2",
+]
+
+[[package]]
+name = "objc2-uniform-type-identifiers"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
+dependencies = [
+ "block2 0.5.1",
+ "objc2 0.5.3",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-user-notifications"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
+dependencies = [
+ "bitflags 2.13.2",
+ "block2 0.5.1",
+ "objc2 0.5.3",
+ "objc2-core-location",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "object"
+version = "0.36.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "object"
+version = "0.40.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd229a0361b9d0d4396176e02d65897f487eebeab7caa6d443855ee152ca0b9c"
+dependencies = [
+ "crc32fast",
+ "hashbrown 0.17.1",
+ "indexmap",
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "open"
+version = "5.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa576c76302b7b808eecc68061e67336c47833ef9d22caa74dda10fa9675eebc"
+dependencies = [
+ "is-wsl",
+ "libc",
+]
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "orbclient"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747"
+dependencies = [
+ "libc",
+ "libredox",
+]
+
+[[package]]
+name = "ordered-float"
+version = "5.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c7c9e0d9b23589f26070720bac724174bfec1083e82f7854cdd0267518343c0"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "ordered-stream"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "outref"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
+
+[[package]]
+name = "oxc_allocator"
+version = "0.153.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9222664e7018ba33c0781da9b8b97007afdf86e4702123dde331c0f32168550"
+dependencies = [
+ "allocator-api2",
+ "hashbrown 0.17.1",
+ "oxc_data_structures",
+ "rustc-hash 2.1.3",
+]
+
+[[package]]
+name = "oxc_ast"
+version = "0.153.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b02c8089e522e19f85f03e384042b9acd2e7013e125bc11e17ff052160b5ce8"
+dependencies = [
+ "bitflags 2.13.2",
+ "oxc_allocator",
+ "oxc_ast_macros",
+ "oxc_data_structures",
+ "oxc_diagnostics",
+ "oxc_estree",
+ "oxc_regular_expression",
+ "oxc_span",
+ "oxc_str",
+ "oxc_syntax",
+]
+
+[[package]]
+name = "oxc_ast_macros"
+version = "0.153.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b795bfdf0b88722a9f0e9a61a9ef6d2ecd115a728f5f0290fcc66da3ec5151ea"
+dependencies = [
+ "phf 0.14.0",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "oxc_ast_visit"
+version = "0.153.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62ee65572c656f232524b4c55c0fe8fc9b54a0f444a40c74727f623aecb2dd54"
+dependencies = [
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_span",
+ "oxc_syntax",
+]
+
+[[package]]
+name = "oxc_codegen"
+version = "0.153.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5534ee12503faae4e633c0dff6b873450116e5c97bd75adf6fb3d6274a9dc317"
+dependencies = [
+ "bitflags 2.13.2",
+ "cow-utils",
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_data_structures",
+ "oxc_ecmascript",
+ "oxc_index",
+ "oxc_semantic",
+ "oxc_sourcemap",
+ "oxc_span",
+ "oxc_str",
+ "oxc_syntax",
+ "rustc-hash 2.1.3",
+ "smallvec",
+]
+
+[[package]]
+name = "oxc_data_structures"
+version = "0.153.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fbea321d618c6b87789cfc19aee4e332525b25420c8c5644ea71f1ffae5e68"
+
+[[package]]
+name = "oxc_diagnostics"
+version = "0.153.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a93ce3ad9aacc728d625990815e41a844e3285c518b8a4508c4f1a423f5251cd"
+dependencies = [
+ "bytecount",
+ "cow-utils",
+ "itoa",
+ "memchr",
+ "oxc_span",
+ "percent-encoding",
+ "smallvec",
+ "textwrap",
+ "unicode-segmentation",
+ "unicode-width",
+]
+
+[[package]]
+name = "oxc_ecmascript"
+version = "0.153.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52605e06c9ef037d346a45e89db78f60962e39f26f648b41d930854354ba1e93"
+dependencies = [
+ "dragonbox_ecma",
+ "itoa",
+ "num-bigint 0.5.1",
+ "num-traits",
+ "oxc_ast",
+ "oxc_data_structures",
+ "oxc_span",
+ "oxc_syntax",
+]
+
+[[package]]
+name = "oxc_estree"
+version = "0.153.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58d091d1d56046eca8741fc89d7e205a9c4c560ff80313be931a6be57edbcc3e"
+
+[[package]]
+name = "oxc_index"
+version = "5.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "191884bee6c3744909a51acc7d78d4ae370d817b25875b10642f632327b6296e"
+dependencies = [
+ "nonmax",
+ "serde",
+]
+
+[[package]]
+name = "oxc_parser"
+version = "0.153.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71ce9e6d646d84b30f8eb4e1a1b50c69e66f4ef08066c721a3b82a451326bad4"
+dependencies = [
+ "bitflags 2.13.2",
+ "cow-utils",
+ "memchr",
+ "num-bigint 0.5.1",
+ "num-traits",
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_data_structures",
+ "oxc_diagnostics",
+ "oxc_ecmascript",
+ "oxc_regular_expression",
+ "oxc_span",
+ "oxc_str",
+ "oxc_syntax",
+ "rustc-hash 2.1.3",
+ "seq-macro",
+]
+
+[[package]]
+name = "oxc_regular_expression"
+version = "0.153.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "369f33abce8a9a6a49c97a16579d8bb42331f1dd0ea5826412729ab94085c155"
+dependencies = [
+ "bitflags 2.13.2",
+ "oxc_allocator",
+ "oxc_ast_macros",
+ "oxc_diagnostics",
+ "oxc_span",
+ "oxc_str",
+ "phf 0.14.0",
+ "rustc-hash 2.1.3",
+ "unicode-id-start",
+]
+
+[[package]]
+name = "oxc_semantic"
+version = "0.153.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2819478f4352d619c21b14ace45c08c78b9eacdcb2d5cd4c90072e0ef58119ab"
+dependencies = [
+ "itertools 0.15.0",
+ "memchr",
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_ast_visit",
+ "oxc_data_structures",
+ "oxc_diagnostics",
+ "oxc_ecmascript",
+ "oxc_index",
+ "oxc_span",
+ "oxc_str",
+ "oxc_syntax",
+ "rustc-hash 2.1.3",
+ "self_cell",
+ "smallvec",
+]
+
+[[package]]
+name = "oxc_sourcemap"
+version = "9.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1133371a5fadc14b9f66a8fec14be3564a77647c7ab21b1ef0ec973bd82613ce"
+dependencies = [
+ "base64-simd",
+ "json-escape-simd",
+ "rustc-hash 2.1.3",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "oxc_span"
+version = "0.153.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "152232e21d62e0ddd2a6c23867121c3252e663f05cdc432578d9e1efc52a253b"
+dependencies = [
+ "compact_str",
+ "oxc_allocator",
+ "oxc_ast_macros",
+ "oxc_estree",
+ "oxc_str",
+]
+
+[[package]]
+name = "oxc_str"
+version = "0.153.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37716fe1827bf766fa68e9f1011408ca0149b90903ae171289c48401229e2f34"
+dependencies = [
+ "compact_str",
+ "hashbrown 0.17.1",
+ "oxc_allocator",
+ "oxc_estree",
+]
+
+[[package]]
+name = "oxc_syntax"
+version = "0.153.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2214f0546f6df736d976b611782df7072fbe8010e4f3bbd3b98bee37e814b6a"
+dependencies = [
+ "bitflags 2.13.2",
+ "cow-utils",
+ "dragonbox_ecma",
+ "nonmax",
+ "oxc_allocator",
+ "oxc_ast_macros",
+ "oxc_estree",
+ "oxc_index",
+ "oxc_span",
+ "oxc_str",
+ "phf 0.14.0",
+ "unicode-id-start",
+]
+
+[[package]]
+name = "p256"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
+dependencies = [
+ "ecdsa",
+ "elliptic-curve",
+ "primeorder",
+ "sha2",
+]
+
+[[package]]
+name = "p384"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6"
+dependencies = [
+ "ecdsa",
+ "elliptic-curve",
+ "primeorder",
+ "sha2",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall 0.5.18",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "pem-rfc7468"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
+dependencies = [
+ "base64ct",
+]
+
+[[package]]
+name = "peniko"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "839c8299360d2e998bdb106dc0a6cd71dcc5f4df51df1b620361bf50e283cca6"
+dependencies = [
+ "bytemuck",
+ "color",
+ "kurbo 0.13.1",
+ "linebender_resource_handle",
+ "smallvec",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "phf"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
+dependencies = [
+ "phf_macros 0.13.1",
+ "phf_shared 0.13.1",
+ "serde",
+]
+
+[[package]]
+name = "phf"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6"
+dependencies = [
+ "phf_macros 0.14.0",
+ "phf_shared 0.14.0",
+ "serde",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
+dependencies = [
+ "fastrand",
+ "phf_shared 0.13.1",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100"
+dependencies = [
+ "fastrand",
+ "phf_shared 0.14.0",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
+dependencies = [
+ "phf_generator 0.13.1",
+ "phf_shared 0.13.1",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fa8d0ca26d424d27630da600c6624696e7dec8bf7b3b492b383c5dc49e5e085"
+dependencies = [
+ "phf_generator 0.14.0",
+ "phf_shared 0.14.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "pico-args"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
+
+[[package]]
+name = "pin-project"
+version = "1.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "piper"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
+dependencies = [
+ "atomic-waker",
+ "fastrand",
+ "futures-io",
+]
+
+[[package]]
+name = "pipewire"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bde71084c4e25959d68f1ea54daa75e5ecdb338e5caf0b5510143b79baa32d5c"
+dependencies = [
+ "bitflags 2.13.2",
+ "libc",
+ "libspa",
+ "libspa-sys",
+ "pipewire-sys",
+ "rustix 1.1.5",
+]
+
+[[package]]
+name = "pipewire-sys"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce653f53e63e5b93853218092ee9a8906a5d082c92f3f1db26316955dd63ce0"
+dependencies = [
+ "bindgen",
+ "libspa-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "pkcs1"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
+dependencies = [
+ "der",
+ "pkcs8",
+ "spki",
+]
+
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
+
+[[package]]
+name = "plain"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
+
+[[package]]
+name = "png"
+version = "0.17.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
+dependencies = [
+ "bitflags 1.3.2",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide 0.8.9",
+]
+
+[[package]]
+name = "png"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
+dependencies = [
+ "bitflags 2.13.2",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide 0.8.9",
+]
+
+[[package]]
+name = "polling"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix 1.1.5",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "pollster"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
+
+[[package]]
+name = "pollster"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc6355899e1c9462875b6757c79f3caa011a1fdae12bbb1a2e72dd1f234f8336"
+
+[[package]]
+name = "polycool"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6"
+dependencies = [
+ "arrayvec",
+]
+
+[[package]]
+name = "portable-atomic"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715"
+dependencies = [
+ "portable-atomic",
+]
+
+[[package]]
+name = "portable-pty"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e"
+dependencies = [
+ "anyhow",
+ "bitflags 1.3.2",
+ "downcast-rs",
+ "filedescriptor",
+ "lazy_static",
+ "libc",
+ "log",
+ "nix 0.28.0",
+ "serial2",
+ "shared_library",
+ "shell-words",
+ "winapi",
+ "winreg",
+]
+
+[[package]]
+name = "postcard"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
+dependencies = [
+ "cobs",
+ "embedded-io 0.4.0",
+ "embedded-io 0.6.1",
+ "serde",
+]
+
+[[package]]
+name = "potential_utf"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
+dependencies = [
+ "serde_core",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "presser"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa"
+
+[[package]]
+name = "primeorder"
+version = "0.13.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
+dependencies = [
+ "elliptic-curve",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
+dependencies = [
+ "toml_edit",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "process-wrap"
+version = "10.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f21b97672d2dc848e7b25701ab4535618b92f4861c13cc3f7f7bed52ad3c8da"
+dependencies = [
+ "indexmap",
+ "nix 0.31.3",
+ "windows",
+]
+
+[[package]]
+name = "profiling"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5"
+
+[[package]]
+name = "pulley-interpreter"
+version = "49.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cd3de43f4f4cff536de8902097edbdf749321b97dda548f36a37918eb85bba3"
+dependencies = [
+ "cranelift-bitset",
+ "log",
+ "pulley-macros",
+ "wasmtime-internal-core",
+]
+
+[[package]]
+name = "pulley-macros"
+version = "49.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8319a8fa5269b4895957f8795861c42822bd26218e495f70060a963323d94847"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "pxfm"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
+
+[[package]]
+name = "quick-error"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
+
+[[package]]
+name = "quick-xml"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "radium"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
+
+[[package]]
+name = "rand"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af"
+dependencies = [
+ "chacha20",
+ "getrandom 0.4.3",
+ "rand_core 0.10.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.17",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
+
+[[package]]
+name = "range-alloc"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08"
+
+[[package]]
+name = "raw-window-handle"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
+
+[[package]]
+name = "raw-window-metal"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135"
+dependencies = [
+ "objc2 0.6.5",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.2",
+ "objc2-quartz-core 0.3.2",
+]
+
+[[package]]
+name = "rayon"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "read-fonts"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709"
+dependencies = [
+ "bytemuck",
+ "font-types",
+ "once_cell",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags 2.13.2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e"
+dependencies = [
+ "bitflags 2.13.2",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0"
+dependencies = [
+ "libredox",
+ "thiserror 2.0.21",
+]
+
+[[package]]
+name = "ref-cast"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3"
+dependencies = [
+ "ref-cast-impl",
+]
+
+[[package]]
+name = "ref-cast-impl"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "regalloc2"
+version = "0.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "757712e8e61590d6d4f5d563483755538b5aa13467837a3b41cd9832509a7f85"
+dependencies = [
+ "allocator-api2",
+ "bumpalo",
+ "hashbrown 0.17.1",
+ "log",
+ "rustc-hash 2.1.3",
+ "serde",
+ "smallvec",
+]
+
+[[package]]
+name = "regex"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
+[[package]]
+name = "renderdoc-sys"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
+
+[[package]]
+name = "resvg"
+version = "0.45.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43"
+dependencies = [
+ "log",
+ "pico-args",
+ "rgb",
+ "svgtypes",
+ "tiny-skia",
+ "usvg",
+]
+
+[[package]]
+name = "rfc6979"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
+dependencies = [
+ "hmac",
+ "subtle",
+]
+
+[[package]]
+name = "rfd"
+version = "0.17.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220"
+dependencies = [
+ "block2 0.6.2",
+ "dispatch2",
+ "js-sys",
+ "libc",
+ "log",
+ "objc2 0.6.5",
+ "objc2-app-kit 0.3.2",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.2",
+ "percent-encoding",
+ "pollster 0.4.0",
+ "raw-window-handle",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "web-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rgb"
+version = "0.8.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "ron"
+version = "0.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81116b9531d61eabc41aeb228e4b6b2435bcca3233b98cf3b3077d4e6e9debb3"
+dependencies = [
+ "bitflags 2.13.2",
+ "once_cell",
+ "serde",
+ "serde_derive",
+ "typeid",
+ "unicode-ident",
+]
+
+[[package]]
+name = "roxmltree"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
+
+[[package]]
+name = "rsa"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
+dependencies = [
+ "const-oid",
+ "digest",
+ "num-bigint-dig",
+ "num-integer",
+ "num-traits",
+ "pkcs1",
+ "pkcs8",
+ "rand_core 0.6.4",
+ "sha2",
+ "signature",
+ "spki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb"
+
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "0.38.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
+dependencies = [
+ "bitflags 2.13.2",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.4.15",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
+dependencies = [
+ "bitflags 2.13.2",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.12.1",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf"
+dependencies = [
+ "log",
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
+
+[[package]]
+name = "ruzstd"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69e4ce4a039f42bf25920b95667e4d789e98749ab11bbb8d2c2936f788421bb5"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schemars"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "schemars_derive",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars_derive"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde_derive_internals",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "scoped-tls"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "sec1"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
+dependencies = [
+ "base16ct",
+ "der",
+ "generic-array",
+ "pkcs8",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "secret-service"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4d35ad99a181be0a60ffcbe85d680d98f87bdc4d7644ade319b87076b9dbfd4"
+dependencies = [
+ "aes",
+ "cbc",
+ "futures-util",
+ "generic-array",
+ "hkdf",
+ "num",
+ "once_cell",
+ "rand 0.8.8",
+ "serde",
+ "sha2",
+ "zbus 4.4.0",
+]
+
+[[package]]
+name = "security-framework"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
+dependencies = [
+ "bitflags 2.13.2",
+ "core-foundation 0.9.4",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework"
+version = "3.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
+dependencies = [
+ "bitflags 2.13.2",
+ "core-foundation 0.10.1",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "self_cell"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813"
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+dependencies = [
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "seq-macro"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc"
+
+[[package]]
+name = "serde"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "serde_derive_internals"
+version = "0.30.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.154"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_repr"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "serial2"
+version = "0.2.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b16809bc35793b19ce4e0c53924bc0dce3937f15487997cfdaed936004180730"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "digest",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "digest",
+]
+
+[[package]]
+name = "shared_library"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11"
+dependencies = [
+ "lazy_static",
+ "libc",
+]
+
+[[package]]
+name = "shell-words"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc",
+]
+
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "digest",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
+
+[[package]]
+name = "simd_cesu8"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"
+dependencies = [
+ "rustc_version",
+ "simdutf8",
+]
+
+[[package]]
+name = "simdutf8"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+
+[[package]]
+name = "simplecss"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c"
+dependencies = [
+ "log",
+]
+
+[[package]]
+name = "siphasher"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256"
+
+[[package]]
+name = "skrifa"
+version = "0.44.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68"
+dependencies = [
+ "bytemuck",
+ "read-fonts",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "slotmap"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.16.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "smawk"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100"
+
+[[package]]
+name = "smithay-client-toolkit"
+version = "0.19.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016"
+dependencies = [
+ "bitflags 2.13.2",
+ "calloop 0.13.0",
+ "calloop-wayland-source 0.3.0",
+ "cursor-icon",
+ "libc",
+ "log",
+ "memmap2",
+ "rustix 0.38.44",
+ "thiserror 1.0.69",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-csd-frame",
+ "wayland-cursor",
+ "wayland-protocols",
+ "wayland-protocols-wlr",
+ "wayland-scanner",
+ "xkeysym",
+]
+
+[[package]]
+name = "smithay-client-toolkit"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0"
+dependencies = [
+ "bitflags 2.13.2",
+ "calloop 0.14.5",
+ "calloop-wayland-source 0.4.1",
+ "cursor-icon",
+ "libc",
+ "log",
+ "memmap2",
+ "rustix 1.1.5",
+ "thiserror 2.0.21",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-csd-frame",
+ "wayland-cursor",
+ "wayland-protocols",
+ "wayland-protocols-experimental",
+ "wayland-protocols-misc",
+ "wayland-protocols-wlr",
+ "wayland-scanner",
+ "xkeysym",
+]
+
+[[package]]
+name = "smithay-clipboard"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226"
+dependencies = [
+ "libc",
+ "smithay-client-toolkit 0.20.0",
+ "wayland-backend",
+]
+
+[[package]]
+name = "smol_str"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "spin"
+version = "0.9.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e"
+
+[[package]]
+name = "spirv"
+version = "0.4.0+sdk-1.4.341.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f"
+dependencies = [
+ "bitflags 2.13.2",
+]
+
+[[package]]
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "strict-num"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
+dependencies = [
+ "float-cmp",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "svgtypes"
+version = "0.15.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc"
+dependencies = [
+ "kurbo 0.11.3",
+ "siphasher",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "sys-locale"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "system-deps"
+version = "7.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7"
+dependencies = [
+ "cfg-expr",
+ "heck",
+ "pkg-config",
+ "toml 1.1.8+spec-1.1.0",
+ "version-compare",
+]
+
+[[package]]
+name = "tap"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+
+[[package]]
+name = "target-lexicon"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
+
+[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom 0.4.3",
+ "once_cell",
+ "rustix 1.1.5",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.16.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ecfad6c3abc80a577f2b91c1e412ee57e7a060d430b553c1b0c940974ebcd49"
+dependencies = [
+ "icu_segmenter",
+ "smawk",
+ "unicode-width",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
+dependencies = [
+ "thiserror-impl 2.0.21",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "tiff"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52"
+dependencies = [
+ "fax",
+ "flate2",
+ "half",
+ "quick-error",
+ "weezl",
+ "zune-jpeg",
+]
+
+[[package]]
+name = "tiny-skia"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab"
+dependencies = [
+ "arrayref",
+ "arrayvec",
+ "bytemuck",
+ "cfg-if",
+ "log",
+ "png 0.17.16",
+ "tiny-skia-path",
+]
+
+[[package]]
+name = "tiny-skia-path"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93"
+dependencies = [
+ "arrayref",
+ "bytemuck",
+ "strict-num",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
+dependencies = [
+ "displaydoc",
+ "serde_core",
+ "zerovec",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
+
+[[package]]
+name = "tokio"
+version = "1.53.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044"
+dependencies = [
+ "pin-project-lite",
+]
+
+[[package]]
+name = "toml"
+version = "0.9.12+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
+dependencies = [
+ "indexmap",
+ "serde_core",
+ "serde_spanned",
+ "toml_datetime 0.7.5+spec-1.1.0",
+ "toml_parser",
+ "toml_writer",
+ "winnow 0.7.15",
+]
+
+[[package]]
+name = "toml"
+version = "1.1.8+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d"
+dependencies = [
+ "indexmap",
+ "serde_core",
+ "serde_spanned",
+ "toml_datetime 1.1.2+spec-1.1.0",
+ "toml_parser",
+ "toml_writer",
+ "winnow 1.0.4",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.7.5+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.25.17+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c"
+dependencies = [
+ "indexmap",
+ "toml_datetime 1.1.2+spec-1.1.0",
+ "toml_parser",
+ "toml_writer",
+ "winnow 1.0.4",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.1.5+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c"
+dependencies = [
+ "winnow 1.0.4",
+]
+
+[[package]]
+name = "toml_writer"
+version = "1.1.3+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "type-map"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90"
+dependencies = [
+ "rustc-hash 2.1.3",
+]
+
+[[package]]
+name = "typeid"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
+
+[[package]]
+name = "typenum"
+version = "1.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+
+[[package]]
+name = "uds_windows"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
+dependencies = [
+ "memoffset",
+ "tempfile",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "unicode-general-category"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
+
+[[package]]
+name = "unicode-id-start"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0825d2e09dcae814a4273c1e1435979d414ede25b2ee235b1545da6682483765"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
+
+[[package]]
+name = "unicode-width"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "ureq"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc"
+dependencies = [
+ "base64 0.23.1",
+ "flate2",
+ "log",
+ "percent-encoding",
+ "rustls",
+ "rustls-pki-types",
+ "ureq-proto",
+ "utf8-zero",
+ "webpki-roots",
+]
+
+[[package]]
+name = "ureq-proto"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f86fd172ccca569e458f61b6bdd6220965a9ef36e672a6852953b51a0e1583be"
+dependencies = [
+ "base64 0.23.1",
+ "http",
+ "httparse",
+ "log",
+]
+
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "usvg"
+version = "0.45.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef"
+dependencies = [
+ "base64 0.22.1",
+ "data-url",
+ "flate2",
+ "imagesize",
+ "kurbo 0.11.3",
+ "log",
+ "pico-args",
+ "roxmltree",
+ "simplecss",
+ "siphasher",
+ "strict-num",
+ "svgtypes",
+ "tiny-skia-path",
+ "xmlwriter",
+]
+
+[[package]]
+name = "utf8-zero"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "uuid"
+version = "1.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606"
+dependencies = [
+ "js-sys",
+ "serde_core",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "vello_common"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2e9aed918117e8152c9eddfd8362d73c465c23f26c44786aa331707b8a64fa2"
+dependencies = [
+ "bytemuck",
+ "fearless_simd",
+ "guillotiere",
+ "log",
+ "peniko",
+ "smallvec",
+ "thiserror 2.0.21",
+]
+
+[[package]]
+name = "vello_cpu"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac7349e1f55f6b801c7c277958df4ea53e7f20f21e8014910ad888b2ecda93ea"
+dependencies = [
+ "bytemuck",
+ "glifo",
+ "hashbrown 0.17.1",
+ "vello_common",
+]
+
+[[package]]
+name = "version-compare"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "vsimd"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
+
+[[package]]
+name = "vt100"
+version = "0.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "054ff75fb8fa83e609e685106df4faeffdf3a735d3c74ebce97ec557d5d36fd9"
+dependencies = [
+ "itoa",
+ "unicode-width",
+ "vte",
+]
+
+[[package]]
+name = "vte"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5924018406ce0063cd67f8e008104968b74b563ee1b85dde3ed1f7cb87d3dbd"
+dependencies = [
+ "arrayvec",
+ "memchr",
+]
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.4+wasi-0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.129"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e"
+dependencies = [
+ "js-sys",
+ "tokio",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.129"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.129"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.129"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-encoder"
+version = "0.258.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f64765e43c000fae008a48db8b288e175e237fc9ce20a26586836878c1bee89f"
+dependencies = [
+ "leb128fmt",
+ "wasmparser 0.258.3",
+]
+
+[[package]]
+name = "wasm-encoder"
+version = "0.262.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4495a6f85dd94c411c749303efcbc5defade0b0cbdbc0541495241ab917a9b5a"
+dependencies = [
+ "leb128fmt",
+ "wasmparser 0.262.0",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.258.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47e10808eb27efde692f1b1706089fe19296728bf872291f5b4e0d50fba88f2f"
+dependencies = [
+ "anyhow",
+ "indexmap",
+ "wasm-encoder 0.258.3",
+ "wasmparser 0.258.3",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.258.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe9269dd817e0e7021bf534ccd9761bc2d64778b6708e41228b8439d75eaa0b1"
+dependencies = [
+ "bitflags 2.13.2",
+ "hashbrown 0.17.1",
+ "indexmap",
+ "semver",
+ "serde",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.261.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f20f20e44f7e8aeb6744823ea9d869ede51e51be4fdaedede2852282e54d2d8"
+dependencies = [
+ "bitflags 2.13.2",
+ "indexmap",
+ "semver",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.262.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "974d643b125b272607e2fff8012ea14a8328acd7621e4bafa7afb32500052b1b"
+dependencies = [
+ "bitflags 2.13.2",
+ "indexmap",
+ "semver",
+]
+
+[[package]]
+name = "wasmprinter"
+version = "0.258.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14a457ba8670a3db3d357c1aeac3ed4fe378a76054a221789f0df0ea507e292b"
+dependencies = [
+ "anyhow",
+ "termcolor",
+ "wasmparser 0.258.3",
+]
+
+[[package]]
+name = "wasmprinter"
+version = "0.261.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86a21c9e3325655ba02de3e645bc8357a8b3651a2e3ada25b6dbc37560deb9b7"
+dependencies = [
+ "anyhow",
+ "termcolor",
+ "wasmparser 0.261.0",
+]
+
+[[package]]
+name = "wasmtime"
+version = "49.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36abb25de5518707e5ba96b586366b09db168d2d4976cb9dac57265a317497ea"
+dependencies = [
+ "addr2line",
+ "async-trait",
+ "bitflags 2.13.2",
+ "bumpalo",
+ "cc",
+ "encoding_rs",
+ "futures",
+ "libc",
+ "log",
+ "mach2",
+ "memfd",
+ "object 0.40.0",
+ "once_cell",
+ "postcard",
+ "pulley-interpreter",
+ "rustix 1.1.5",
+ "semver",
+ "serde",
+ "serde_derive",
+ "smallvec",
+ "target-lexicon",
+ "wasmparser 0.258.3",
+ "wasmtime-environ",
+ "wasmtime-internal-component-macro",
+ "wasmtime-internal-component-util",
+ "wasmtime-internal-core",
+ "wasmtime-internal-cranelift",
+ "wasmtime-internal-fiber",
+ "wasmtime-internal-jit-debug",
+ "wasmtime-internal-jit-icache-coherence",
+ "wasmtime-internal-unwinder",
+ "wasmtime-internal-versioned-export-macros",
+ "wat",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "wasmtime-environ"
+version = "49.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92f16a4d09fb1fbf2dcfeddb9818e1517a258e13b505b3bfdadcf47a19bf31ca"
+dependencies = [
+ "anyhow",
+ "cpp_demangle",
+ "cranelift-bforest",
+ "cranelift-bitset",
+ "cranelift-entity",
+ "gimli",
+ "hashbrown 0.17.1",
+ "indexmap",
+ "log",
+ "object 0.40.0",
+ "postcard",
+ "rustc-demangle",
+ "semver",
+ "serde",
+ "serde_derive",
+ "sha2",
+ "smallvec",
+ "target-lexicon",
+ "wasm-encoder 0.258.3",
+ "wasmparser 0.258.3",
+ "wasmprinter 0.258.3",
+ "wasmtime-internal-component-util",
+ "wasmtime-internal-core",
+]
+
+[[package]]
+name = "wasmtime-internal-component-macro"
+version = "49.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fca03bd4425856574c0807d1dee16fad43f947feaf4e9ae1d2129596ee4de4"
+dependencies = [
+ "anyhow",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "wasmtime-internal-component-util",
+ "wasmtime-internal-wit-bindgen",
+ "wit-parser",
+]
+
+[[package]]
+name = "wasmtime-internal-component-util"
+version = "49.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4006009fc3598af4e895ab18c6c33d2c6b7c657b1bd0a755ca7ee1abef6f0c16"
+
+[[package]]
+name = "wasmtime-internal-core"
+version = "49.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "947a732ad659f42c10ec639706fc8cb1f068dd9a55eebde8a1f24dfd06dfa525"
+dependencies = [
+ "hashbrown 0.17.1",
+ "libm",
+ "serde",
+]
+
+[[package]]
+name = "wasmtime-internal-cranelift"
+version = "49.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbfb8b455d524939e03df42e3e749f8e26e26ba2acd8b08fa00b3bda55686750"
+dependencies = [
+ "cranelift-codegen",
+ "cranelift-control",
+ "cranelift-entity",
+ "cranelift-frontend",
+ "cranelift-native",
+ "gimli",
+ "itertools 0.14.0",
+ "log",
+ "object 0.40.0",
+ "pulley-interpreter",
+ "smallvec",
+ "target-lexicon",
+ "thiserror 2.0.21",
+ "wasmparser 0.258.3",
+ "wasmtime-environ",
+ "wasmtime-internal-core",
+ "wasmtime-internal-unwinder",
+ "wasmtime-internal-versioned-export-macros",
+]
+
+[[package]]
+name = "wasmtime-internal-fiber"
+version = "49.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd8aa8711f68b2cdd94c2b85ba5c8317edaa1ac4ee0a0534b957123680239307"
+dependencies = [
+ "cc",
+ "libc",
+ "rustix 1.1.5",
+ "wasmtime-environ",
+ "wasmtime-internal-versioned-export-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "wasmtime-internal-jit-debug"
+version = "49.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e873e4504faf50aa8ce255d6ea7210d87b39d5f379c2486782e2c5f179c70ca8"
+dependencies = [
+ "cc",
+ "wasmtime-internal-versioned-export-macros",
+]
+
+[[package]]
+name = "wasmtime-internal-jit-icache-coherence"
+version = "49.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "197ab1fda5da958a5c0b8a97eb241beafc48ac568b82ef4f12f38d6866c74173"
+dependencies = [
+ "libc",
+ "wasmtime-internal-core",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "wasmtime-internal-unwinder"
+version = "49.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5577f3a5a979a7c3b1b23c1e9f13e70a8583acc8828471ec56f282117fc4952b"
+dependencies = [
+ "cranelift-codegen",
+ "log",
+ "object 0.40.0",
+ "wasmtime-environ",
+]
+
+[[package]]
+name = "wasmtime-internal-versioned-export-macros"
+version = "49.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6399c9c4050ed431cf66e21c05fadd6f9731db44476f452f8c7142b4597b883"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "wasmtime-internal-wit-bindgen"
+version = "49.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4eaf45d4da7681d234bb6efd846b4a528b08a75384a7fc23f125d0145364b06"
+dependencies = [
+ "anyhow",
+ "bitflags 2.13.2",
+ "heck",
+ "indexmap",
+ "wit-component",
+ "wit-parser",
+]
+
+[[package]]
+name = "wast"
+version = "262.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30612a0c29249b5f185888a107e8a519d920226b7d07f9d351fd094e9f75eb23"
+dependencies = [
+ "bumpalo",
+ "leb128fmt",
+ "memchr",
+ "unicode-width",
+ "wasm-encoder 0.262.0",
+]
+
+[[package]]
+name = "wat"
+version = "1.262.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e228027f2b07bbaf946980ebbe2437fca73be6f54c40c2a8d4f42f277075f8d1"
+dependencies = [
+ "wast",
+]
+
+[[package]]
+name = "wayland-backend"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078"
+dependencies = [
+ "cc",
+ "downcast-rs",
+ "rustix 1.1.5",
+ "scoped-tls",
+ "smallvec",
+ "wayland-sys",
+]
+
+[[package]]
+name = "wayland-client"
+version = "0.31.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073"
+dependencies = [
+ "bitflags 2.13.2",
+ "rustix 1.1.5",
+ "wayland-backend",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-csd-frame"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e"
+dependencies = [
+ "bitflags 2.13.2",
+ "cursor-icon",
+ "wayland-backend",
+]
+
+[[package]]
+name = "wayland-cursor"
+version = "0.31.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d"
+dependencies = [
+ "rustix 1.1.5",
+ "wayland-client",
+ "xcursor",
+]
+
+[[package]]
+name = "wayland-protocols"
+version = "0.32.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6"
+dependencies = [
+ "bitflags 2.13.2",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-protocols-experimental"
+version = "20250721.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1"
+dependencies = [
+ "bitflags 2.13.2",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-protocols-misc"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8"
+dependencies = [
+ "bitflags 2.13.2",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-protocols-plasma"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91"
+dependencies = [
+ "bitflags 2.13.2",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-protocols-wlr"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234"
+dependencies = [
+ "bitflags 2.13.2",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-scanner"
+version = "0.31.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0"
+dependencies = [
+ "proc-macro2",
+ "quick-xml",
+ "quote",
+]
+
+[[package]]
+name = "wayland-server"
+version = "0.31.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dde9c29be0f723a573977de51ee455bf3dfa03652730a74f9dd3b337e374d75"
+dependencies = [
+ "bitflags 2.13.2",
+ "downcast-rs",
+ "rustix 1.1.5",
+ "wayland-backend",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-sys"
+version = "0.31.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be"
+dependencies = [
+ "dlib",
+ "libc",
+ "log",
+ "memoffset",
+ "once_cell",
+ "pkg-config",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webbrowser"
+version = "1.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62c35be770821a214dbc362fc26908c853e776c0004294d0b10b8a6bad582f94"
+dependencies = [
+ "jni",
+ "log",
+ "ndk-context",
+ "objc2 0.6.5",
+ "objc2-app-kit 0.3.2",
+ "objc2-foundation 0.3.2",
+ "url",
+ "web-sys",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "weezl"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
+
+[[package]]
+name = "wgpu"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "527ccdf43dd5b2e8676eed9984ce00e2bbb0a1b85b70c1969dcb6cd2eb55ab9e"
+dependencies = [
+ "arrayvec",
+ "bitflags 2.13.2",
+ "bytemuck",
+ "cfg-if",
+ "cfg_aliases 0.2.2",
+ "document-features",
+ "hashbrown 0.17.1",
+ "js-sys",
+ "log",
+ "naga",
+ "parking_lot",
+ "portable-atomic",
+ "profiling",
+ "raw-window-handle",
+ "smallvec",
+ "static_assertions",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "wgpu-core",
+ "wgpu-hal",
+ "wgpu-types",
+]
+
+[[package]]
+name = "wgpu-core"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c018fce9b6270aa203c2fdd56f3cce996713534bd757e4ea58c8560b121f14"
+dependencies = [
+ "arrayvec",
+ "bit-set",
+ "bit-vec",
+ "bitflags 2.13.2",
+ "bytemuck",
+ "cfg_aliases 0.2.2",
+ "document-features",
+ "hashbrown 0.17.1",
+ "indexmap",
+ "log",
+ "naga",
+ "naga-types",
+ "once_cell",
+ "parking_lot",
+ "portable-atomic",
+ "profiling",
+ "raw-window-handle",
+ "rustc-hash 1.1.0",
+ "smallvec",
+ "thiserror 2.0.21",
+ "wgpu-core-deps-apple",
+ "wgpu-core-deps-emscripten",
+ "wgpu-core-deps-wasm",
+ "wgpu-core-deps-windows-linux-android",
+ "wgpu-hal",
+ "wgpu-naga-bridge",
+ "wgpu-types",
+]
+
+[[package]]
+name = "wgpu-core-deps-apple"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "061f3d319a40d39d00b1ecc2c33b89fe21d4e6fe01859df3500a3a8ecccd6b68"
+dependencies = [
+ "wgpu-hal",
+]
+
+[[package]]
+name = "wgpu-core-deps-emscripten"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98b86cf4abf524a902dd35f18ca6a3f08fc2ae9847c8f10b48e30491b1f0b86"
+dependencies = [
+ "wgpu-hal",
+]
+
+[[package]]
+name = "wgpu-core-deps-wasm"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d260ca2ca8adc654f513a869bf3cac0b47f5b8d3f87803d6942015f8de597aae"
+dependencies = [
+ "wgpu-hal",
+]
+
+[[package]]
+name = "wgpu-core-deps-windows-linux-android"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7586165fd5f6d881cb9ce4bb71f40d6caab2c0f1837e3fc1d9788a197fb6004f"
+dependencies = [
+ "wgpu-hal",
+]
+
+[[package]]
+name = "wgpu-hal"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6b7fb58561a792bc237628ba0792e332de418fefe145f13b5ed8201e6d52f58"
+dependencies = [
+ "android_system_properties",
+ "arrayvec",
+ "ash",
+ "bit-set",
+ "bitflags 2.13.2",
+ "block2 0.6.2",
+ "bytemuck",
+ "cfg-if",
+ "cfg_aliases 0.2.2",
+ "glow",
+ "glutin_wgl_sys",
+ "gpu-allocator",
+ "hashbrown 0.17.1",
+ "js-sys",
+ "khronos-egl",
+ "libc",
+ "libloading",
+ "log",
+ "naga",
+ "naga-types",
+ "ndk-sys",
+ "objc2 0.6.5",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-foundation 0.3.2",
+ "objc2-metal 0.3.2",
+ "objc2-quartz-core 0.3.2",
+ "once_cell",
+ "ordered-float",
+ "parking_lot",
+ "portable-atomic",
+ "portable-atomic-util",
+ "profiling",
+ "range-alloc",
+ "raw-window-handle",
+ "raw-window-metal",
+ "renderdoc-sys",
+ "smallvec",
+ "static_assertions",
+ "thiserror 2.0.21",
+ "wasm-bindgen",
+ "wayland-sys",
+ "web-sys",
+ "wgpu-naga-bridge",
+ "wgpu-types",
+ "windows",
+ "windows-core",
+ "windows-result",
+]
+
+[[package]]
+name = "wgpu-naga-bridge"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2f62e73117bb7a62bfd9c5a5841438a823f6566c6442a808ee269d2d055c081"
+dependencies = [
+ "naga",
+ "wgpu-types",
+]
+
+[[package]]
+name = "wgpu-types"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99dad6f1fbdbbdb4c278a6508b059d44688f5cebddf78d005a46a31340269286"
+dependencies = [
+ "bitflags 2.13.2",
+ "bytemuck",
+ "js-sys",
+ "log",
+ "naga-types",
+ "raw-window-handle",
+ "static_assertions",
+ "web-sys",
+]
+
+[[package]]
+name = "widestring"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
+dependencies = [
+ "windows-collections",
+ "windows-core",
+ "windows-future",
+ "windows-numerics",
+]
+
+[[package]]
+name = "windows-collections"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
+dependencies = [
+ "windows-core",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-future"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
+dependencies = [
+ "windows-core",
+ "windows-link",
+ "windows-threading",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-numerics"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
+dependencies = [
+ "windows-core",
+ "windows-link",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
+dependencies = [
+ "windows-link",
+ "windows_aarch64_gnullvm 0.53.1",
+ "windows_aarch64_msvc 0.53.1",
+ "windows_i686_gnu 0.53.1",
+ "windows_i686_gnullvm 0.53.1",
+ "windows_i686_msvc 0.53.1",
+ "windows_x86_64_gnu 0.53.1",
+ "windows_x86_64_gnullvm 0.53.1",
+ "windows_x86_64_msvc 0.53.1",
+]
+
+[[package]]
+name = "windows-threading"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+
+[[package]]
+name = "winit"
+version = "0.30.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d"
+dependencies = [
+ "ahash",
+ "android-activity",
+ "atomic-waker",
+ "bitflags 2.13.2",
+ "block2 0.5.1",
+ "bytemuck",
+ "calloop 0.13.0",
+ "cfg_aliases 0.2.2",
+ "concurrent-queue",
+ "core-foundation 0.9.4",
+ "core-graphics",
+ "cursor-icon",
+ "dpi",
+ "js-sys",
+ "libc",
+ "memmap2",
+ "ndk",
+ "objc2 0.5.3",
+ "objc2-app-kit 0.2.2",
+ "objc2-foundation 0.2.2",
+ "objc2-ui-kit 0.2.2",
+ "orbclient",
+ "percent-encoding",
+ "pin-project",
+ "raw-window-handle",
+ "redox_syscall 0.4.1",
+ "rustix 0.38.44",
+ "smithay-client-toolkit 0.19.2",
+ "smol_str",
+ "tracing",
+ "unicode-segmentation",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "wayland-protocols-plasma",
+ "web-sys",
+ "web-time",
+ "windows-sys 0.52.0",
+ "x11-dl",
+ "x11rb",
+ "xkbcommon-dl",
+]
+
+[[package]]
+name = "winnow"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
+
+[[package]]
+name = "winnow"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winreg"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winresource"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2bc6d36480b829272a5bdabf08e6dca8004761c1da7e35efef800540c66221b"
+dependencies = [
+ "toml 1.1.8+spec-1.1.0",
+ "version_check",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "wit-component"
+version = "0.258.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0fca88950af0c1f42bc77e17315e48798b8d68340171b6d204f62bc19a878a6"
+dependencies = [
+ "anyhow",
+ "bitflags 2.13.2",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder 0.258.3",
+ "wasm-metadata",
+ "wasmparser 0.258.3",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.258.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72a2127663dcf3ee16614fb0e3c33d5c6c66fec9dad317a8b0fdd3869487a533"
+dependencies = [
+ "anyhow",
+ "hashbrown 0.17.1",
+ "id-arena",
+ "indexmap",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-ident",
+ "wasmparser 0.258.3",
+]
+
+[[package]]
+name = "writeable"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
+
+[[package]]
+name = "wyz"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
+dependencies = [
+ "tap",
+]
+
+[[package]]
+name = "x11-dl"
+version = "2.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f"
+dependencies = [
+ "libc",
+ "once_cell",
+ "pkg-config",
+]
+
+[[package]]
+name = "x11rb"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
+dependencies = [
+ "as-raw-xcb-connection",
+ "gethostname",
+ "libc",
+ "libloading",
+ "once_cell",
+ "rustix 1.1.5",
+ "x11rb-protocol",
+]
+
+[[package]]
+name = "x11rb-protocol"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
+
+[[package]]
+name = "xcap"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da270fd7c8581d43d731cb690bcc791fc0a7b3bc96e131a4b6552d0379640a9e"
+dependencies = [
+ "dispatch2",
+ "image",
+ "libwayshot-xcap",
+ "log",
+ "objc2 0.6.5",
+ "objc2-app-kit 0.3.2",
+ "objc2-av-foundation",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-core-media",
+ "objc2-core-video",
+ "objc2-foundation 0.3.2",
+ "percent-encoding",
+ "pipewire",
+ "rand 0.10.3",
+ "scopeguard",
+ "serde",
+ "thiserror 2.0.21",
+ "url",
+ "widestring",
+ "windows",
+ "xcb",
+ "zbus 5.19.0",
+]
+
+[[package]]
+name = "xcb"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6c2ad15e0e922856ee89afe862b8992334bbe7953adad56cd1199358cb30566"
+dependencies = [
+ "bitflags 2.13.2",
+ "libc",
+ "quick-xml",
+]
+
+[[package]]
+name = "xcursor"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23"
+
+[[package]]
+name = "xdg-home"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6"
+dependencies = [
+ "libc",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "xkbcommon-dl"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5"
+dependencies = [
+ "bitflags 2.13.2",
+ "dlib",
+ "log",
+ "once_cell",
+ "xkeysym",
+]
+
+[[package]]
+name = "xkeysym"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
+
+[[package]]
+name = "xml-rs"
+version = "0.8.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
+
+[[package]]
+name = "xmlwriter"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
+
+[[package]]
+name = "xtask"
+version = "0.3.0"
+dependencies = [
+ "craft-anything-engine",
+ "proc-macro2",
+ "syn 2.0.119",
+ "toml 0.9.12+spec-1.1.0",
+]
+
+[[package]]
+name = "yaml-rust2"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9"
+dependencies = [
+ "arraydeque",
+ "encoding_rs",
+ "hashlink",
+]
+
+[[package]]
+name = "yaxpeax-arch"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36274fcc5403da2a7636ffda4d02eca12a1b2b8267b9d2e04447bd2ccfc72082"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "yaxpeax-arm"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79f8d9808ad0b7e434882c66e50704138e7c0bdf3408a9f3159c50603dc3168c"
+dependencies = [
+ "bitvec",
+ "yaxpeax-arch",
+]
+
+[[package]]
+name = "yoke"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+ "synstructure",
+]
+
+[[package]]
+name = "zbus"
+version = "4.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725"
+dependencies = [
+ "async-broadcast",
+ "async-executor",
+ "async-fs",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "async-recursion",
+ "async-task",
+ "async-trait",
+ "blocking",
+ "enumflags2",
+ "event-listener",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "hex",
+ "nix 0.29.0",
+ "ordered-stream",
+ "rand 0.8.8",
+ "serde",
+ "serde_repr",
+ "sha1",
+ "static_assertions",
+ "tracing",
+ "uds_windows",
+ "windows-sys 0.52.0",
+ "xdg-home",
+ "zbus_macros 4.4.0",
+ "zbus_names 3.0.0",
+ "zvariant 4.2.0",
+]
+
+[[package]]
+name = "zbus"
+version = "5.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907"
+dependencies = [
+ "async-broadcast",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "async-recursion",
+ "async-task",
+ "async-trait",
+ "blocking",
+ "enumflags2",
+ "event-listener",
+ "futures-core",
+ "futures-lite",
+ "hex",
+ "libc",
+ "ordered-stream",
+ "rustix 1.1.5",
+ "serde",
+ "serde_repr",
+ "tracing",
+ "uds_windows",
+ "uuid",
+ "windows-sys 0.61.2",
+ "winnow 1.0.4",
+ "zbus_macros 5.19.0",
+ "zbus_names 4.3.4",
+ "zvariant 5.15.0",
+]
+
+[[package]]
+name = "zbus-lockstep"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863"
+dependencies = [
+ "zbus_xml",
+ "zvariant 5.15.0",
+]
+
+[[package]]
+name = "zbus-lockstep-macros"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "zbus-lockstep",
+ "zbus_xml",
+ "zvariant 5.15.0",
+]
+
+[[package]]
+name = "zbus_macros"
+version = "4.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "zvariant_utils 2.1.0",
+]
+
+[[package]]
+name = "zbus_macros"
+version = "5.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+ "zbus_names 4.3.4",
+ "zvariant 5.15.0",
+ "zvariant_utils 4.2.0",
+]
+
+[[package]]
+name = "zbus_names"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c"
+dependencies = [
+ "serde",
+ "static_assertions",
+ "zvariant 4.2.0",
+]
+
+[[package]]
+name = "zbus_names"
+version = "4.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e"
+dependencies = [
+ "serde",
+ "winnow 1.0.4",
+ "zvariant 5.15.0",
+]
+
+[[package]]
+name = "zbus_xml"
+version = "5.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1586c021a01ca0a9216dcd874e546382e156a5cbab5fab6cb5f10087e22682a"
+dependencies = [
+ "serde",
+ "winnow 1.0.4",
+ "zbus_names 4.3.4",
+ "zvariant 5.15.0",
+]
+
+[[package]]
+name = "zcheapstr"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.62"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86502bf56ac7c77571a32e2647bb2a15894565e981fb2a48d7bde2d91c965a9d"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.62"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5457206954b06561e2608c7e19cf58b1926586d999c246eebe4502f7e2039d1a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879"
+dependencies = [
+ "zeroize_derive",
+]
+
+[[package]]
+name = "zeroize_derive"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "zerotrie"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
+dependencies = [
+ "serde",
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "zlib-rs"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"
+
+[[package]]
+name = "zmij"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+
+[[package]]
+name = "zune-core"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b"
+
+[[package]]
+name = "zune-jpeg"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
+dependencies = [
+ "zune-core",
+]
+
+[[package]]
+name = "zvariant"
+version = "4.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe"
+dependencies = [
+ "endi",
+ "enumflags2",
+ "serde",
+ "static_assertions",
+ "zvariant_derive 4.2.0",
+]
+
+[[package]]
+name = "zvariant"
+version = "5.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147"
+dependencies = [
+ "endi",
+ "enumflags2",
+ "serde",
+ "winnow 1.0.4",
+ "zcheapstr",
+ "zvariant_derive 5.15.0",
+ "zvariant_utils 4.2.0",
+]
+
+[[package]]
+name = "zvariant_derive"
+version = "4.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "zvariant_utils 2.1.0",
+]
+
+[[package]]
+name = "zvariant_derive"
+version = "5.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+ "zvariant_utils 4.2.0",
+]
+
+[[package]]
+name = "zvariant_utils"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "zvariant_utils"
+version = "4.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde",
+ "syn 3.0.9",
+ "winnow 1.0.4",
+]
diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml
new file mode 100644
index 0000000..5775126
--- /dev/null
+++ b/desktop/Cargo.toml
@@ -0,0 +1,91 @@
+[workspace]
+resolver = "3"
+members = ["crates/*", "apps/*", "xtask"]
+
+[workspace.package]
+version = "0.3.0"
+edition = "2024"
+license = "MIT OR Apache-2.0"
+rust-version = "1.97"
+repository = "https://github.com/storytold/craft-anything"
+
+[workspace.dependencies]
+# internal
+craft-anything-evidence = { path = "crates/evidence" }
+craft-anything-formats = { path = "crates/formats" }
+craft-anything-disasm = { path = "crates/disasm" }
+craft-anything-js = { path = "crates/js" }
+craft-anything-analysis = { path = "crates/analysis" }
+craft-anything-platform = { path = "crates/platform" }
+craft-anything-engines = { path = "crates/engines" }
+craft-anything-engine = { path = "crates/engine" }
+craft-anything-ai = { path = "crates/ai" }
+craft-anything-mcp = { path = "crates/mcp" }
+craft-anything-setup = { path = "crates/setup" }
+craft-anything-ui-egui = { path = "crates/ui-egui" }
+craft-anything-testkit = { path = "crates/testkit" }
+
+# external
+thiserror = "2"
+serde = { version = "1", features = ["derive", "rc"] }
+serde_json = "1"
+sha2 = "0.10"
+hex = "0.4"
+bitflags = { version = "2", features = ["serde"] }
+schemars = "1"
+log = "0.4"
+toml = "0.9"
+eframe = { version = "0.36", default-features = false }
+egui = "0.36"
+egui_extras = { version = "0.36", default-features = false }
+egui_kittest = "0.36"
+rfd = "0.17"
+rayon = "1"
+proptest = "1"
+# JavaScript (pure Rust, wasm-safe)
+oxc_allocator = "0.153"
+oxc_ast = "0.153"
+oxc_ast_visit = "0.153"
+oxc_parser = "0.153"
+oxc_span = "0.153"
+oxc_syntax = "0.153"
+oxc_codegen = "0.153"
+oxc_semantic = "0.153"
+miniz_oxide = "0.8"
+
+# native engine (pure Rust)
+object = { version = "0.36", default-features = false, features = ["read", "std"] }
+iced-x86 = { version = "1.21", default-features = false, features = ["std", "decoder", "intel", "instr_info"] }
+yaxpeax-arm = { version = "0.5", default-features = false, features = ["std", "alloc", "fmt"] }
+yaxpeax-arch = { version = "0.3", default-features = false, features = ["std", "alloc"] }
+rustc-demangle = "0.1"
+wasmparser = { version = "0.261", default-features = false, features = ["std", "validate", "features", "component-model", "simd"] }
+wasmprinter = { version = "0.261", default-features = false, features = ["component-model"] }
+regex = "1"
+
+[workspace.lints.rust]
+unsafe_code = "forbid"
+
+[workspace.lints.clippy]
+dbg_macro = "warn"
+todo = "warn"
+
+[profile.dev.package."*"]
+opt-level = 2
+
+[profile.dev]
+opt-level = 1
+
+[profile.release]
+lto = "thin"
+codegen-units = 1
+
+# The browser build (apps/craft-anything-web, `trunk build --release`): small and fast to load.
+[profile.web-release]
+inherits = "release"
+opt-level = "s"
+lto = "fat"
+codegen-units = 1
+debug = false
+strip = true
+panic = "abort"
diff --git a/desktop/LICENSE-APACHE b/desktop/LICENSE-APACHE
new file mode 100644
index 0000000..f433b1a
--- /dev/null
+++ b/desktop/LICENSE-APACHE
@@ -0,0 +1,177 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
diff --git a/desktop/LICENSE-MIT b/desktop/LICENSE-MIT
new file mode 100644
index 0000000..8e63994
--- /dev/null
+++ b/desktop/LICENSE-MIT
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 ArtCraft Team and the Craft Anything contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/desktop/NOTICE b/desktop/NOTICE
new file mode 100644
index 0000000..e3f21dc
--- /dev/null
+++ b/desktop/NOTICE
@@ -0,0 +1,31 @@
+Craft Anything
+Copyright (c) 2026 ArtCraft Team and the Craft Anything contributors.
+Licensed under the MIT License or the Apache License, Version 2.0, at your option
+(LICENSE-MIT, LICENSE-APACHE).
+
+The ArtCraft name, wordmark and logos in docs/brand/ are trademarks of the ArtCraft Team. They are
+not open source and are not covered by the license above; their terms are in
+docs/brand/LICENSE-brand.txt.
+
+Third-party material
+
+This product bundles third-party material under its own licenses, listed below. The per-file
+list (author, source, license) is in ATTRIBUTION.md.
+
+- Inter (assets/fonts/) — SIL Open Font License 1.1. See assets/fonts/OFL-Inter.txt.
+- JetBrains Mono (assets/fonts/) — SIL Open Font License 1.1. See assets/fonts/OFL-JetBrainsMono.txt.
+- Archivo (assets/fonts/) — SIL Open Font License 1.1. See assets/fonts/OFL-Archivo.txt.
+- Geist Mono (assets/fonts/) — SIL Open Font License 1.1. See assets/fonts/OFL-GeistMono.txt.
+- Lucide icons (assets/icons/) — ISC License. See assets/icons/LICENSE-lucide.txt.
+- egui default fonts (embedded by the epaint_default_fonts crate):
+ - Ubuntu Light — Ubuntu Font Licence 1.0.
+ - Noto Emoji — SIL Open Font License 1.1.
+ - Hack — MIT License; contains Bitstream Vera glyphs under the Bitstream Vera License.
+ - emoji-icon-font (Copyright (c) 2014 John Slegers) — MIT License.
+ The licence texts ship in that crate's fonts/ folder (UFL.txt, OFL.txt, Hack-Regular.txt,
+ emoji-icon-font-mit-license.txt) and in the third-party licence bundle (docs/licences.md).
+
+Rust crates are listed with their licences in the third-party licence bundle shipped with each
+release (see docs/licences.md).
+
+Builds made with craft-fonts (CRAFT_FONTS_DIR) also embed the fonts listed in ATTRIBUTION.md.
diff --git a/desktop/README.ja.md b/desktop/README.ja.md
new file mode 100644
index 0000000..45a87bc
--- /dev/null
+++ b/desktop/README.ja.md
@@ -0,0 +1,354 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+Craft Anything
+
+
+ English · 日本語 · 简体中文 · 한국어
+
+
+
+ あらゆる Web サイト、アプリ、ゲームがどう動いているかを見て、あなた自身の版を AI に作らせる。純粋な Rust で書かれた、オープンソースのクリーンルームエンジン。
+ 対象を指定して、欲しいものを伝えるだけ。実際の調査ツールで元のソフトを調べ、
+ コーディングエージェントに新しいプロジェクトを作らせ、結果を元のソフトと比較して磨き上げます。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Craft Anything をダウンロード ·
+ ArtCraft ·
+ すべての Crafting Apps
+
+
+
+
+
+
+
+ Web サイトプロジェクト: 各チェックは、ビルドが元のソフトにどれくらい似ているかを示し、ルールから外れるものはあなたが承認し、プランは残りの作業を示します。
+
+
+> [!NOTE]
+> **ArtCraft は、あらゆる分野の作り手が集まるコミュニティです。** 画家、写真家、映像作家、
+> イラストレーター、デザイナー、アニメーター、趣味で作っている人、先週ペンを持ったばかりの人。
+> 何かを作っているなら、あなたも仲間です。**[Discord で気軽に声をかけてください](https://discord.gg/artcraft)。**
+
+
+ 仕組み ·
+ 2 つの使い方 ·
+ AI ·
+ エージェント向け ·
+ 内部の仕組み ·
+ はじめかた ·
+ ステータス ·
+ ダウンロード ·
+ Discord
+
+
+
+
+
+
+
+ 🔍 なんでも調べる
+ Web サイト、デスクトップアプリ、スマホアプリ、ゲーム、ファイル。本物の調査ツールが、それらがどう作られ、何をするのかを突き止め、答えの裏付けとなるエビデンスも添えます。
+
+
+ 🛠️ 自分の版を作る
+ 「この Web サイトを再現して」「このゲームのゲームプレイを Rust で再現して」と伝えれば、コーディングエージェントが指定した技術スタックで新しいプロジェクトを作ります。
+
+
+ ✅ 推測ではなく検証
+ すべてのビルドは元のソフトと比較され (3 つの画面幅で表示したページ、ゲーム自身のセルフテスト、アプリの実行結果)、十分近づくまで磨かれます。
+
+
+ 🤖 エージェント対応
+ 314 個のツールは、それぞれが CLI コマンドであり、MCP ツールでもあります。どのエージェントでも、アプリが使うのと同じエンジンを動かせます。調査に AI は要りません。
+
+
+
+
+
+
+
+
+## 仕組み
+
+Craft Anything で対象を指定し、欲しいものを伝えて「Start」を押すだけ。4 つのステップで進み、判断が必要なときは、わかりやすい言葉であなたに尋ねます:
+
+1. **調べる (Study):** 元のソフトを調査します。あらゆる幅で表示したページ、コード、ファイル、動作のしかたを調べます。
+2. **作る (Build):** コーディングエージェントが新しいプロジェクトを作ります。言語、フレームワーク、エンジンはリクエストで指定したもの (指定がなければ妥当な既定値)を使います。
+3. **確認する (Check):** ビルドを元のソフトと比較し、まだ一致していない点を一覧にします。
+4. **完成 (Ready):** 結果を開いたり、遊んだり、プレビューしたりできます。変更を頼めば磨き直します。
+
+
+
+
+
+ Make: 何を再現するかを選び、欲しいものを伝え、Start を押します。
+
+
+
+ Ready: ゲームで遊ぶ。変更は下の欄から頼めます。
+
+
+
+
+
+
+## 2 つの使い方
+
+- **Simple** (既定): 「What do you want to make?」と表示されるので、Web サイト、アプリ、ゲームから選び、説明して Start を押します。プロジェクトページには 4 つのステップ (Study → Build → Check → Ready)、エージェントが今何をしているかの平易な説明、質問と承認のカード、結果がどれくらい似ているか、結果やそのフォルダを開くボタンが並びます。変更は下の入力欄から頼めます。「Just look inside」は、何も作らずに対象へ質問できます (ディープモード時)。
+- **Advanced**: フル機能のワークベンチ。すべてのツール (Ctrl/Cmd+K)、関数、文字列、疑似 C、逆アセンブル、16 進数、コールグラフ、エビデンス、不明点、ジョブ、ログ、エージェントコンソール。
+
+どちらのモードも同じエンジンを動かします。
+
+## クリーンルーム(Clean room)かディープモード(Deep)
+
+- **クリーンルーム(Clean room)** (既定): 元のソフトの中身は一切読みません。コードも、ファイルも、ページのソースも読みません。Craft Anything は、あなたに見えるものだけから作業します。追加した画像、訪問者から見た Web サイト (スクリーンショット、見えているテキスト、計測したレイアウト、色、フォント、余白、開いたメニュー)、そして許可した場合はアプリのウィンドウの画像です。中を覗こうとするツールは拒否され、ディープモードの有効化方法を案内します。
+- **ディープモード(Deep)**: すべてのツールを使います。私的利用のためのモードです。プログラムのコードとファイルは内蔵エンジン (インストールされていれば Ghidra も)で読み、Web サイトの JavaScript を読み、そのスタートページを観察します。ビルダーもそれらすべてを使えます。
+
+タイトルバーまたは Settings で切り替えられます。`settings_set deep_mode true` で設定するか、CLI の 1 回限りなら `--deep` (または `--clean-room`)を使ってください。
+
+
+
+
+ Advanced: 関数、疑似 C、エージェント、そしてあらゆる答えの裏にあるエビデンス。
+
+
+
+
+## AI
+
+ビルドには、お使いのコンピュータ上のコーディングエージェントが必要です。まず **Claude Code** に対応しています。Craft Anything は、あなた自身の改変されていない `claude` プログラムを実行します。これはあなた自身のサインイン情報を使います。Craft Anything がアカウントを見たり扱ったりすることはありません。利用は Anthropic とのあなた自身の契約に従います。方法と理由は [docs/claude-code.md](docs/claude-code.md) を参照してください。Codex アダプターもありますが、実際のインストール環境ではまだ検証されていません。質問と分析には、Anthropic、OpenAI、OpenRouter、ローカルの OpenAI 互換サーバーが使えます (Settings → AI)。
+
+
+
+## エージェント向け
+
+すべてのツールは CLI サブコマンドでも MCP ツールでもあるので、エージェントはアプリなしで対象を調べたり、再構築を丸ごと実行したりできます:
+
+```sh
+craft-anything-cli commands # every tool, by toolset
+craft-anything-cli mcp # serve them to any MCP client
+craft-anything-cli rebuild https://example.com "Recreate this website"
+```
+
+結果は、信頼度・カバレッジ・制限事項を備えたエビデンス記録です。大きな結果はまず要約として返り、`core_evidence_browse` でページ送りできます。`craft-anything-cli setup` は Craft Anything をコーディングエージェントのアプリにつなぎます。
+
+
+
+## 内部の仕組み
+
+純粋な Rust で書かれたリバースエンジニアリングエンジンです。ネイティブプログラム、.NET、Java、Android、Apple、Electron/JavaScript、Web サイト、ファームウェア、パケットキャプチャ、データベース、ゲームファイル向けに 314 個のツール (うち 298 個がエンジン本体、残りは AI とセットアップの層)を備えています。独自のデコーダ、逆アセンブラ、逆コンパイラは外部プログラムなしで動きます。ディープモードではそれらすべてを使い、インストールされていれば Ghidra も使います。これがデスクトップアプリ (egui)、CLI、MCP サーバーを支え、一部は WebAssembly としてブラウザでも動きます。全リストは [docs/tools.md](docs/tools.md) にあります。
+
+
+
+## はじめかた
+
+Rust **1.97 以降**が必要です (依存関係が要求します。`rustup update`)。Debian か Ubuntu では、デスクトップアプリには次のものも必要です:
+
+```sh
+sudo apt-get install libxkbcommon-dev libwayland-dev libx11-dev libxrandr-dev libxi-dev libgl1-mesa-dev libgtk-3-dev
+```
+
+続いて:
+
+```sh
+git clone https://github.com/storytold/craft-anything && cd craft-anything
+cargo run --release -p craft-anything # the desktop app
+cargo install --path apps/craft-anything-cli # the CLI, as `craft-anything-cli`
+craft-anything-cli doctor # checks the install, coding agents and engines
+```
+
+ビルドには、コンピュータ上でサインイン済みのコーディングエージェントが必要です ([AI](#ai) を参照)。足りないものは `doctor` が示します。コマンドラインからの再構築 (ターミナルで質問に答えます):
+
+```sh
+craft-anything-cli rebuild https://example.com "Recreate this website"
+craft-anything-cli rebuild - "A snake game in Rust" --rounds 1
+craft-anything-cli rebuild https://example.com "Make the header blue" --continue-in ""
+craft-anything-cli rebuild - "Add a contact form" --project-folder "" --existing edit
+```
+
+新しいプロジェクトは `Documents/Craft Anything` に作られます (Settings → General で変更できます)。各プロジェクトフォルダには `craft.json` (リクエスト、対象、状態、履歴)、`capture/` (対象から記録したもの)、`app/` (生成されたプロジェクト)、`checks/` (元のソフトとの比較ごとの結果)、`investigation.cany/` (エビデンス)、`inbox/` (未回答の質問)が入ります。
+
+プロジェクトは、選んだフォルダから始めることもできます (Start の下にある「Choose a folder…」、または `--project-folder`)。すでにファイルがある場合は、それが何なのかを答えます。アプリ本体 (ビルダーがその場で編集します)か、そこから始める素材 (アプリはその隣の `app/` に作られます)のどちらかです (Settings → General で答えを記憶できます)。Settings → General では、ほかのフォルダにあるプロジェクトも一覧できます。一覧から外すと、その一覧から消えるだけです。
+
+
+
+## ステータス
+
+> [!IMPORTANT]
+> **ステータス:** Craft Anything はアルファ版の初期段階です。いま動くもの、一部だけのもの、この先の予定:
+>
+> - **いま動くもの:** デスクトップアプリ (Simple と Advanced)、CLI、MCP サーバー。Claude Code
+> をビルダーとして、Web サイト・アプリ・ゲームを「調べる → 作る → 確認する → 磨く」の順に進める
+> ワークフロー。3 つの画面幅での Web サイト確認。Unity、Unreal、Godot でのゲーム再構築。Ghidra を
+> 使うディープモード。生成画像と 3D モデル (オプトイン、自分の fal.ai キー)。プラグイン (MCP
+> サーバー、および `wasm-plugins` オプション付きビルドでの WASM コンポーネント)。
+> - **一部だけ:** API キーや他のコーディングエージェントでのビルド (実装済み、実環境では未実行)、
+> アプリとゲームの挙動チェック、実物ではなくダミーの対象でしかテストしていない一部のエンジンツール。
+> - **次:** 最初の公開リリース (Windows、macOS、Linux、FreeBSD 向けインストーラとブラウザ
+> ビューアは CI に用意済み)、その後マネージドプランを提供予定。
+>
+> [docs/STATUS.md](docs/STATUS.md) に詳細と、各部分をどう確かめたか、
+> [docs/tool-status.md](docs/tool-status.md) にツールごとのチェックリスト、
+> [docs/ROADMAP.md](docs/ROADMAP.md) に計画の項目別一覧があります。issue を立てるか、
+> [Discord](https://discord.gg/artcraft) で壊れたところを教えてください。
+
+**責任ある使い方。** Craft Anything は、所有しているか、調べる許可があるソフトにだけ使ってください。DRM、ログイン、
+アンチチートを回避することは決してありません。再構築は、エビデンスに基づいてレイアウト・スタイル・挙動を再現します。エージェントには、ロゴ・写真・テキストを、あなたが権利を持つか使用許可があると確認できるまでコピーしないよう指示しています。
+
+
+
+## ダウンロード
+
+GitHub の [最新リリース](https://github.com/storytold/craft-anything/releases/latest) には以下に挙げるすべてのビルドがあり、[すべてのリリース](https://github.com/storytold/craft-anything/releases) には過去のバージョンとそのリリースノートがあります。ファイル名の `` はバージョン番号で、`SHA256SUMS.txt` に各ファイルのチェックサムが載っています。
+
+### Windows
+
+| ビルド | インストーラ | ポータブル |
+|---|---|---|
+| x64 (64 ビット Intel/AMD) | `craft-anything--windows-x64.msi` | `craft-anything--windows-x64-portable.zip` |
+| arm64 (Snapdragon などの ARM PC) | `craft-anything--windows-arm64.msi` | `craft-anything--windows-arm64-portable.zip` |
+| x86 (32 ビット) | `craft-anything--windows-x86.msi` | `craft-anything--windows-x86-portable.zip` |
+
+インストーラと実行ファイルはコード署名済みです。ポータブルの zip は設定をアプリの隣に保存します。
+
+### macOS
+
+| ビルド | ファイル | 備考 |
+|---|---|---|
+| アプリ、ユニバーサル (Apple silicon + Intel) | `craft-anything--macos-universal.dmg` | 署名・公証済み |
+| コマンドラインツール、ユニバーサル | `craft-anything-cli--macos-universal.zip` | 署名・公証済み。macOS がブロックする場合は下記を参照 |
+
+0.2.0 以降のバージョンではコマンドラインツールがアプリに同梱されるため、ビルドに別の zip は
+不要です。必要なのは、アプリなしでコマンドラインだけを使う場合です。
+
+ブラウザから zip をダウンロードすると「Apple could not verify craft-anything-cli」で止まることが
+あります(単体のプログラムは公証を自分に添付できないため、macOS がオンラインで確認します)。
+ターミナルからのダウンロードはこの検査を受けないので、代わりに次を使ってください:
+
+```sh
+ver=0.2.0 # change this to the version you want
+curl -fLO "https://github.com/storytold/craft-anything/releases/download/v$ver/craft-anything-cli-$ver-macos-universal.zip"
+ditto -x -k "craft-anything-cli-$ver-macos-universal.zip" .
+mkdir -p ~/.local/bin
+mv "craft-anything-cli-$ver-macos-universal/craft-anything-cli" ~/.local/bin/
+~/.local/bin/craft-anything-cli --version
+```
+
+`craft-anything-cli` だけで実行したい場合は、`~/.local/bin` を `PATH` に追加してください。
+
+すでにブロックされたコピーがある場合は、Done(「ゴミ箱に入れる」ではなく)を押してから
+`xattr -d com.apple.quarantine ` を実行するか、システム設定 →
+プライバシーとセキュリティ → このまま開く で許可してください。
+
+### Linux
+
+| 形式 | x86_64 | aarch64 (ARM64) | 備考 |
+|---|---|---|---|
+| AppImage | `craft-anything--linux-x86_64.AppImage` | `craft-anything--linux-aarch64.AppImage` | どこでも動きます。[AppImageUpdate](https://github.com/AppImageCommunity/AppImageUpdate) で自己更新します (`.zsync` ファイル) |
+| Flatpak | `craft-anything--linux-x86_64.flatpak` | `craft-anything--linux-aarch64.flatpak` | サンドボックス化。`flatpak install --user ` |
+| Debian/Ubuntu | `craft-anything--linux-x86_64.deb` | `craft-anything--linux-aarch64.deb` | |
+| Fedora/RHEL/openSUSE | `craft-anything--linux-x86_64.rpm` | `craft-anything--linux-aarch64.rpm` | |
+| Tarball | `craft-anything--linux-x86_64.tar.gz` | `craft-anything--linux-aarch64.tar.gz` | どこにでも展開できます |
+
+RISC-V (riscv64): `craft-anything--linux-riscv64.tar.gz`、tarball のみです。
+
+### FreeBSD
+
+| ビルド | ファイル |
+|---|---|
+| x86_64 | `craft-anything--freebsd-x86_64.tar.gz` |
+
+### Web (WebAssembly)
+
+| ビルド | ファイル | 備考 |
+|---|---|---|
+| ブラウザビューア | `craft-anything-web-.zip` | ファイルが何であるかをブラウザ上で示します (ファイルはブラウザから出ません)。フォルダはどんな静的 Web サーバーでも公開できます ([packaging/web/README.md](packaging/web/README.md)) |
+
+
+
+
+
+
+
+一緒にものづくりを
+
+
+ 私たちの Discord は、あらゆる種類の作り手が集まる場所です。絵を描く人、写真を撮る人、
+ デッサンする人、映像を編集する人、文字を組む人、そしてまだ自分が何を作りたいのか
+ 模索している人。いま取り組んでいることを共有したり、助けを求めたり、不具合を
+ 教えたり、これらのツールでこうだったらいいのにと思うことを話したりしてください。
+ どんな表現手段を使っていても、経験の長さにかかわらず、ここでは誰でも歓迎します。
+
+
+
+ discord.gg/artcraft ·
+ getartcraft.com ·
+ すべての Crafting Apps ·
+ Craft Anything
+
+
+---
+
+## The Craft App Suite
+
+| | アプリ | 用途 | コード | 詳細 |
+|:-:|---|---|---|---|
+| | **PhotoCraft** | 画像編集:レイヤー、マスク、文字、本物の PSD ファイル | [GitHub](https://github.com/storytold/photocraft) | [ウェブサイト](https://getartcraft.com/apps/photocraft) |
+| | **VectorCraft** | ベクターイラスト | [GitHub](https://github.com/storytold/vectorcraft) | [ウェブサイト](https://getartcraft.com/apps/vectorcraft) |
+| | **FilmCraft** | 動画編集、カラー、サウンド | [GitHub](https://github.com/storytold/filmcraft) | [ウェブサイト](https://getartcraft.com/apps/filmcraft) |
+| | **LightCraft** | 写真ライブラリと RAW 現像 | [GitHub](https://github.com/storytold/lightcraft) | [ウェブサイト](https://getartcraft.com/apps/lightcraft) |
+| | **PdfCraft** | PDF の閲覧、整理、保護 | [GitHub](https://github.com/storytold/pdfcraft) | [ウェブサイト](https://getartcraft.com/apps/pdfcraft) |
+| | **EffectCraft** | モーショングラフィックスと視覚効果 | [GitHub](https://github.com/storytold/effectcraft) | [ウェブサイト](https://getartcraft.com/apps/effectcraft) |
+| | **DesignCraft** | ページレイアウトと出版 | [GitHub](https://github.com/storytold/designcraft) | [ウェブサイト](https://getartcraft.com/apps/designcraft) |
+| | **FigCraft** | インターフェースデザインとプロトタイピング | [GitHub](https://github.com/storytold/figcraft) | [ウェブサイト](https://getartcraft.com/apps/figcraft) |
+| | **WordCraft** | ワープロと文書 | [GitHub](https://github.com/storytold/wordcraft) | [ウェブサイト](https://getartcraft.com/apps/wordcraft) |
+| | **SoundCraft** | 音声の録音と編集 | [GitHub](https://github.com/storytold/soundcraft) | [ウェブサイト](https://getartcraft.com/apps/soundcraft) |
+| | **DeckCraft** | プレゼンテーションとスライド | [GitHub](https://github.com/storytold/deckcraft) | [ウェブサイト](https://getartcraft.com/apps/deckcraft) |
+| | **CADCraft** | コンピューター支援設計 | [GitHub](https://github.com/storytold/cadcraft) | [ウェブサイト](https://getartcraft.com/apps/cadcraft) |
+| | **GridCraft** | 表計算とデータ | [GitHub](https://github.com/storytold/gridcraft) | [ウェブサイト](https://getartcraft.com/apps/gridcraft) |
+| | **TextureCraft** | 3D テクスチャ描画 | [GitHub](https://github.com/storytold/texturecraft) | [ウェブサイト](https://getartcraft.com/apps/texturecraft) |
+| | **Craft Anything** | あらゆるソフトの仕組みを知り、自分の版を作る | [GitHub](https://github.com/storytold/craft-anything) | [ウェブサイト](https://getartcraft.com/apps/craft-anything) |
+| | **SolidCraft** | Tangency Labs によるパラメトリック 3D CAD(コミュニティ製アプリ) | [GitHub](https://github.com/tangency-labs/solidcraft) | |
+
+## ライセンスとクレジット
+
+Craft Anything は [MIT](LICENSE-MIT) または [Apache-2.0](LICENSE-APACHE) のデュアルライセンスです (どちらかお選びください)。Copyright (c) 2026 ArtCraft Team and the Craft Anything contributors. 必要な通知は [NOTICE](NOTICE) にあります。
+
+同梱のフォント、アイコン、画像、その他のアセットは、それぞれ独自のオープンライセンスが適用されます。各アセットの作者、出典、ライセンスは [ATTRIBUTION.md](ATTRIBUTION.md) に記載しています。
+
+スクリーンショットに写っているのは、そのために作られたサンプルプロジェクトです: example.com、スネークゲーム、小さな電卓プログラム。
+
+[`docs/brand/`](docs/brand/) にある ArtCraft の名称、ワードマーク、ロゴは ArtCraft Team の商標で、このライセンスの対象外です。無改変のまま、このリポジトリと Craft Anything の一部としてのみ、[`docs/brand/LICENSE-brand.txt`](docs/brand/LICENSE-brand.txt) に従って使用できます。フォークや改変版では、これらを削除しなければなりません。
+
+
+
+ ArtCraft チームとコミュニティによる制作。
+
+
+---
+
+このページは [README.md](README.md) の日本語訳です。内容に食い違いがある場合は英語版が優先されます。
diff --git a/desktop/README.ko.md b/desktop/README.ko.md
new file mode 100644
index 0000000..dbc6736
--- /dev/null
+++ b/desktop/README.ko.md
@@ -0,0 +1,329 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+Craft Anything
+
+
+ English · 日本語 · 简体中文 · 한국어
+
+
+
+ 어떤 웹사이트, 앱, 게임이든 어떻게 동작하는지 파악하고, AI로 나만의 버전을 만들어 보세요. 순수 Rust로 만든 오픈소스 clean-room 엔진입니다.
+ 대상을 지정하고 원하는 것을 말하면, 실제 검사 도구로 원본을 분석한 뒤
+ 코딩 에이전트가 새 프로젝트를 빌드하고, 원본과 비교하며 결과를 다듬습니다.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Craft Anything 다운로드 ·
+ ArtCraft ·
+ 모든 Crafting Apps
+
+
+
+
+
+
+
+ 웹사이트 프로젝트: 각 검사는 빌드가 원본과 얼마나 비슷한지 보여 주고, 규칙을 벗어나는 작업은 직접 승인하며, 계획에서는 남은 일을 확인할 수 있습니다.
+
+
+> [!NOTE]
+> **ArtCraft는 다양한 분야의 예술가들이 모인 커뮤니티입니다.** 화가, 사진가,
+> 영화 제작자, 일러스트레이터, 디자이너, 애니메이터, 취미 창작자, 그리고 지난주에
+> 연필을 처음 든 사람까지. 무언가를 만든다면 여러분도 우리 일원입니다. **[디스코드에 인사하러 오세요](https://discord.gg/artcraft).**
+
+
+ 작동 방식 ·
+ 두 가지 사용 방법 ·
+ AI ·
+ 에이전트 ·
+ 내부 구조 ·
+ 시작하기 ·
+ 현재 상태 ·
+ 다운로드 ·
+ 디스코드
+
+
+
+
+
+
+
+ 🔍 무엇이든 분석
+ 웹사이트, 데스크톱 앱, 모바일 앱, 게임, 파일: 실제 검사 도구가 어떻게 만들어졌고 무엇을 하는지 알아내며, 모든 답의 근거를 함께 보여 줍니다.
+
+
+ 🛠️ 나만의 버전 만들기
+ "이 웹사이트를 재현해 줘", "이 게임의 게임플레이를 Rust로 재현해 줘"처럼 원하는 것을 말하면, 코딩 에이전트가 요청한 스택으로 새 프로젝트를 만듭니다.
+
+
+ ✅ 추측이 아닌 검증
+ 모든 빌드는 원본과 비교하며(세 가지 화면 너비의 페이지, 게임 자체 테스트, 앱 실행 결과), 충분히 비슷해질 때까지 다듬습니다.
+
+
+ 🤖 에이전트 연동 지원
+ 314개 도구는 각각 CLI 명령이자 MCP 도구이므로, 어떤 에이전트든 앱이 쓰는 같은 엔진을 조작할 수 있습니다. 검사하는 데 AI는 필요하지 않습니다.
+
+
+
+
+
+
+## 작동 방식
+
+Craft Anything에서 대상을 지정하고 원하는 것을 말한 뒤 Start를 누르세요. 네 단계로 진행하며,
+결정이 필요할 때는 알기 쉬운 말로 직접 물어봅니다:
+
+1. **Study:** 원본을 살펴봅니다. 화면 너비별 페이지, 코드, 파일, 실행 방식을 확인합니다.
+2. **Build:** 코딩 에이전트가 새 프로젝트를 만듭니다. 요청에 적힌 언어, 프레임워크 또는 엔진을 쓰고, 적힌 것이 없으면 기본값을 씁니다.
+3. **Check:** 빌드를 원본과 비교하고, 맞지 않는 부분을 나열합니다.
+4. **Ready:** 결과를 열거나, 플레이하거나, 미리 볼 수 있습니다. 바꿀 부분을 요청하면 다듬어 줍니다.
+
+
+
+
+
+ Make: 재현할 대상을 고르고, 원하는 것을 말한 뒤 Start를 누르세요.
+
+
+
+ Ready: 게임을 플레이하거나, 아래에서 변경을 요청하세요.
+
+
+
+
+## 두 가지 사용 방법
+
+- **Simple**(기본값): "What do you want to make?" 웹사이트, 앱 또는 게임을 고르고 설명한 뒤 Start를 누릅니다.
+ 프로젝트 페이지에는 네 단계(Study → Build → Check → Ready), 에이전트가 하는 일을 쉬운 말로 적은 내용,
+ 질문과 승인 카드, 결과가 원본과 얼마나 비슷한지, 결과나 폴더를 여는 버튼이 표시됩니다.
+ 아래쪽 입력창에서 변경을 요청하세요. "Just look inside"는 결과물을 만들지 않고, 대상을 열어 질문만 할 수 있습니다(Deep 모드에서).
+- **Advanced**: 전체 기능을 갖춘 워크벤치입니다. 모든 도구(Ctrl/Cmd+K), 함수, 문자열, 의사 C 코드, 디스어셈블리,
+ 16진수(hex), 호출 그래프, 근거, 미확인 사항, 작업, 로그, 에이전트 콘솔을 제공합니다.
+
+두 모드 모두 같은 엔진을 사용합니다.
+
+## Clean room 또는 Deep
+
+- **Clean room**(기본값): 코드, 파일, 페이지 소스 등 원본 내부는 일절 읽지 않습니다. Craft Anything은
+ 오직 눈에 보이는 것만으로 작업합니다. 사용자가 추가한 이미지, 방문자가 보는 웹사이트(스크린샷, 보이는 텍스트,
+ 측정한 레이아웃, 색상, 글꼴, 간격, 열어 본 메뉴), 그리고 허용한 경우 캡처한 앱 화면이 여기에 해당합니다.
+ 내부를 들여다보는 도구는 거부되며, Deep 모드를 켜는 방법을 알려 줍니다.
+- **Deep**: 개인 용도로 모든 도구를 사용합니다. 프로그램의 코드와 파일은 내장 엔진(설치되어 있으면 Ghidra도)으로 읽고,
+ 웹사이트의 JavaScript를 읽고 시작 페이지를 관찰하며, 빌더는 이 모두를 쓸 수 있습니다.
+
+제목 표시줄이나 Settings에서 전환하거나, `settings_set deep_mode true`로 바꾸거나, CLI를 한 번 실행할 때
+`--deep`(또는 `--clean-room`)을 붙여 전환합니다.
+
+
+
+
+ Advanced: 함수, 의사 C 코드, 에이전트, 그리고 모든 답 뒤의 근거.
+
+
+## AI
+
+빌드하려면 컴퓨터에 코딩 에이전트가 있어야 합니다. **Claude Code**를 먼저 지원합니다. Craft Anything은 설치된 순정 `claude` 프로그램을 변조 없이 그대로 실행하며, 사용자의 기존 로그인을 사용합니다. Craft Anything은 계정을 열람하거나 다루지 않습니다. 이 사용에는 본인이 Anthropic과 맺은 약관이 적용됩니다. 방법과 이유는 [docs/claude-code.md](docs/claude-code.md)를 보세요. Codex 어댑터도 있지만 실제 설치 환경에서는 아직 검증되지 않았습니다. 질문과 분석에는 Anthropic, OpenAI, OpenRouter, 그리고 로컬 OpenAI 호환 서버를 사용할 수 있습니다(Settings → AI).
+
+## 에이전트를 위한 설계
+
+모든 도구는 CLI 하위 명령이자 MCP 도구이므로, 에이전트는 앱 없이도 대상을 조사하거나 재구현 전체를 실행할 수 있습니다:
+
+```sh
+craft-anything-cli commands # every tool, by toolset
+craft-anything-cli mcp # serve them to any MCP client
+craft-anything-cli rebuild https://example.com "Recreate this website"
+```
+
+결과는 신뢰도, 범위, 한계가 적힌 근거 기록입니다. 큰 결과는 요약을 먼저 보여 주고, `core_evidence_browse`로 나눠 볼 수 있습니다. `craft-anything-cli setup`은 Craft Anything을 코딩 에이전트 앱에 연결합니다.
+
+## 내부 구조
+
+네이티브 프로그램, .NET, Java, Android, Apple, Electron/JavaScript, 웹사이트, 펌웨어, 패킷 캡처, 데이터베이스, 게임 파일을 위한 314개 도구(엔진에 298개, 나머지는 AI 및 설정 계층)를 갖춘 순수 Rust 리버스 엔지니어링 엔진입니다. 자체 디코더, 디스어셈블러, 디컴파일러는 외부 프로그램 없이 동작합니다. Deep 모드는 이 모두를 쓰며, 설치되어 있으면 Ghidra도 사용합니다. 데스크톱 앱(egui), CLI, MCP 서버를 구동하며, 일부는 WebAssembly로 브라우저에서도 실행됩니다. 전체 목록은 [docs/tools.md](docs/tools.md)에 있습니다.
+
+## 시작하기
+
+**Rust 1.97 이상**이 필요합니다(하위 의존성에 필요하며, `rustup update`로 업데이트합니다). Debian 또는 Ubuntu에서는 데스크톱 앱에 다음 패키지도 필요합니다:
+
+```sh
+sudo apt-get install libxkbcommon-dev libwayland-dev libx11-dev libxrandr-dev libxi-dev libgl1-mesa-dev libgtk-3-dev
+```
+
+그다음:
+
+```sh
+git clone https://github.com/storytold/craft-anything && cd craft-anything
+cargo run --release -p craft-anything # the desktop app
+cargo install --path apps/craft-anything-cli # the CLI, as `craft-anything-cli`
+craft-anything-cli doctor # checks the install, coding agents and engines
+```
+
+빌드하려면 컴퓨터에 로그인된 코딩 에이전트가 있어야 합니다([AI](#ai) 참고). `doctor`가 빠진 것을 알려 줍니다. 명령줄에서 재구현하세요(질문은 터미널에 나옵니다):
+
+```sh
+craft-anything-cli rebuild https://example.com "Recreate this website"
+craft-anything-cli rebuild - "A snake game in Rust" --rounds 1
+craft-anything-cli rebuild https://example.com "Make the header blue" --continue-in ""
+craft-anything-cli rebuild - "Add a contact form" --project-folder "" --existing edit
+```
+
+새 프로젝트는 `Documents/Craft Anything`에 저장됩니다(Settings → General에서 바꿀 수 있습니다). 각 프로젝트 폴더에는 `craft.json`(요청, 대상, 상태, 기록), `capture/`(대상에서 기록한 것), `app/`(생성된 프로젝트), `checks/`(원본과의 각 비교), `investigation.cany/`(근거), `inbox/`(미답변 질문)가 들어 있습니다.
+
+직접 고른 폴더에서 프로젝트를 시작할 수도 있습니다(Start 아래의 "Choose a folder…" 또는 `--project-folder`). 폴더에 이미 파일이 있다면 그 파일이 무엇인지 지정하면 됩니다. 빌더가 그 자리에서 바로 수정할 앱 자체인지, 아니면 시작점으로 삼을 참고 자료(이 경우 앱은 그 옆 `app/`에 빌드됨)인지 고를 수 있습니다(Settings → General에서 선택을 기억하게 할 수 있습니다). Settings → General에는 다른 폴더의 프로젝트 목록도 함께 표시되며, 목록에서 제거해도 목록에서만 빠질 뿐 실제 파일은 지워지지 않습니다.
+
+## 현재 상태
+
+> [!IMPORTANT]
+> **현재 상태:** Craft Anything은 Early Alpha 단계입니다. 지금 되는 것, 일부만 되는 것, 다음 계획은 다음과 같습니다:
+>
+> - **지금 되는 것:** 데스크톱 앱(Simple 및 Advanced), CLI, MCP 서버. Claude Code를 빌더로 쓰는 웹사이트, 앱, 게임의 Study → Build → Check → Refine 워크플로. 세 가지 화면 너비의 웹사이트 검사. Unity, Unreal 또는 Godot 게임 재구현. Ghidra를 쓰는 Deep 모드. 이미지 및 3D 모델 생성(선택 사항, 본인의 fal.ai 키 사용). 플러그인(MCP 서버, 그리고 `wasm-plugins` 옵션으로 빌드할 때 WASM 컴포넌트).
+> - **부분적으로 됨:** API 키와 다른 코딩 에이전트로 빌드(구현은 되었지만 실제 실행은 아직 안 함), 앱과 게임의 동작 검사, 모의 대상(stand-in)으로만 테스트된 일부 엔진 도구.
+> - **다음:** 첫 공개 릴리스(Windows, macOS, Linux, FreeBSD용 설치 프로그램과 브라우저 뷰어는 CI에 준비되어 있음), 그다음 관리형 요금제.
+>
+> [docs/STATUS.md](docs/STATUS.md)에 자세한 내용과 각 부분을 확인한 방법이, [docs/tool-status.md](docs/tool-status.md)에 도구별 점검표가, [docs/ROADMAP.md](docs/ROADMAP.md)에 항목별 계획이 있습니다. 이슈를 남기거나, [디스코드](https://discord.gg/artcraft)에서 고장 난 부분을 알려 주세요.
+
+**책임 있는 사용.** Craft Anything은 본인이 소유하거나 살펴볼 권한이 있는 소프트웨어에만 사용하세요. DRM, 로그인, 안티치트를 절대 우회하지 않습니다. 재구현은 근거를 바탕으로 레이아웃, 스타일, 동작을 재현하며, 에이전트는 본인이 소유하거나 허가받았음을 확인하지 않는 한 로고, 사진, 텍스트를 복사하지 않도록 지시받습니다.
+
+## 다운로드
+
+GitHub의 [최신 릴리스](https://github.com/storytold/craft-anything/releases/latest)에 아래 모든 빌드가 있고, [전체 릴리스](https://github.com/storytold/craft-anything/releases)에는 이전 버전과 릴리스 노트가 있습니다. 파일 이름의 ``는 버전 번호이며, `SHA256SUMS.txt`에 모든 파일의 체크섬이 있습니다.
+
+### Windows
+
+| 빌드 | 설치 프로그램 | 포터블 |
+| --------------------------------- | ---------------------------------------- | ------------------------------------------------- |
+| x64 (64비트 Intel/AMD) | `craft-anything--windows-x64.msi` | `craft-anything--windows-x64-portable.zip` |
+| arm64 (Snapdragon 및 기타 ARM PC) | `craft-anything--windows-arm64.msi` | `craft-anything--windows-arm64-portable.zip` |
+| x86 (32비트) | `craft-anything--windows-x86.msi` | `craft-anything--windows-x86-portable.zip` |
+
+설치 프로그램과 실행 파일은 코드 서명되어 있습니다. 포터블 zip은 설정을 앱 옆에 보관합니다.
+
+### macOS
+
+| 빌드 | 파일 | 참고 |
+| ---------------------------------- | ---------------------------------------------- | ------------------------------------------------------ |
+| 앱, 유니버설(Apple 실리콘 + Intel) | `craft-anything--macos-universal.dmg` | 서명 및 공증됨 |
+| 명령줄 도구, 유니버설 | `craft-anything-cli--macos-universal.zip` | 서명 및 공증됨. macOS가 막으면 아래를 참고하세요 |
+
+0.2.0 이후 버전은 앱 안에 명령줄 도구가 들어 있어서, 빌드할 때 별도 zip이 필요 없습니다. 앱 없이 명령줄만 쓰려는 경우에만 필요합니다.
+
+브라우저로 zip을 내려받으면 "Apple could not verify craft-anything-cli"라며 멈출 수 있습니다(단독 프로그램은 공증 정보를 함께 담을 수 없어서, macOS가 온라인으로 확인해야 하기 때문입니다). 터미널로 내려받으면 이 확인을 거치지 않으므로, 대신 다음을 사용하세요:
+
+```sh
+ver=0.2.0 # change this to the version you want
+curl -fLO "https://github.com/storytold/craft-anything/releases/download/v$ver/craft-anything-cli-$ver-macos-universal.zip"
+ditto -x -k "craft-anything-cli-$ver-macos-universal.zip" .
+mkdir -p ~/.local/bin
+mv "craft-anything-cli-$ver-macos-universal/craft-anything-cli" ~/.local/bin/
+~/.local/bin/craft-anything-cli --version
+```
+
+`craft-anything-cli`만으로 실행하려면 `~/.local/bin`을 `PATH`에 추가하세요.
+
+이미 막힌 파일이 있다면 휴지통으로 이동(Move to Bin) 대신 완료(Done)를 누른 뒤 `xattr -d com.apple.quarantine `를 실행하거나, 시스템 설정 → 개인정보 보호 및 보안 → 그래도 열기(Open Anyway)에서 허용하세요.
+
+### Linux
+
+| 형식 | x86_64 | aarch64 (ARM64) | 참고 |
+| -------------------- | -------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
+| AppImage | `craft-anything--linux-x86_64.AppImage` | `craft-anything--linux-aarch64.AppImage` | 어디서나 실행. [AppImageUpdate](https://github.com/AppImageCommunity/AppImageUpdate)(`.zsync` 파일)로 자체 업데이트 |
+| Flatpak | `craft-anything--linux-x86_64.flatpak` | `craft-anything--linux-aarch64.flatpak` | 샌드박스. `flatpak install --user ` |
+| Debian/Ubuntu | `craft-anything--linux-x86_64.deb` | `craft-anything--linux-aarch64.deb` | |
+| Fedora/RHEL/openSUSE | `craft-anything--linux-x86_64.rpm` | `craft-anything--linux-aarch64.rpm` | |
+| Tarball | `craft-anything--linux-x86_64.tar.gz` | `craft-anything--linux-aarch64.tar.gz` | 원하는 곳에 압축 해제 |
+
+RISC-V(riscv64): `craft-anything--linux-riscv64.tar.gz`, tarball만 제공합니다.
+
+### FreeBSD
+
+| 빌드 | 파일 |
+| ------ | -------------------------------------------- |
+| x86_64 | `craft-anything--freebsd-x86_64.tar.gz` |
+
+### Web (WebAssembly)
+
+| 빌드 | 파일 | 참고 |
+| ------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| 브라우저 뷰어 | `craft-anything-web-.zip` | 브라우저 안에서 파일이 무엇인지 분석해 알려 줌(파일은 브라우저를 벗어나지 않습니다). 폴더를 아무 정적 웹 서버에나 올리세요([packaging/web/README.md](packaging/web/README.md)) |
+
+
+
+
+
+
+
+ 우리 함께 만들어요
+
+
+ 우리 디스코드에는 모든 분야의 창작자가 모입니다. 그림, 사진, 스케치, 영상 편집,
+ 서체 디자인을 하는 분들부터 아직 무엇을 좋아하는지 알아가는 중인 사람들까지요. 작업 중인 것을 공유하고,
+ 도움을 요청하고, 고장 난 부분을 알려 주고, 무엇을 할 수 있었으면 하는지 말해 주세요.
+ 여러분이 어떤 작업을 하든, 작업을 시작한 지 얼마나 되었든, 이곳에서는 모두 환영합니다.
+
+
+
+ discord.gg/artcraft ·
+ getartcraft.com ·
+ The Crafting Apps ·
+ Craft Anything
+
+
+---
+
+## The Craft App Suite
+
+| | 앱 | 용도 | 코드 | 자세히 보기 |
+|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:| ------------------ | -------------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------- |
+| | **PhotoCraft** | 이미지 편집: 레이어, 마스크, 텍스트, 실제 PSD 파일 | [GitHub](https://github.com/storytold/photocraft) | [웹사이트](https://getartcraft.com/apps/photocraft) |
+| | **VectorCraft** | 벡터 일러스트레이션 | [GitHub](https://github.com/storytold/vectorcraft) | [웹사이트](https://getartcraft.com/apps/vectorcraft) |
+| | **FilmCraft** | 영상 편집, 색보정, 사운드 | [GitHub](https://github.com/storytold/filmcraft) | [웹사이트](https://getartcraft.com/apps/filmcraft) |
+| | **LightCraft** | 사진 보관함과 RAW 현상 | [GitHub](https://github.com/storytold/lightcraft) | [웹사이트](https://getartcraft.com/apps/lightcraft) |
+| | **PdfCraft** | PDF 읽기, 정리, 보호 | [GitHub](https://github.com/storytold/pdfcraft) | [웹사이트](https://getartcraft.com/apps/pdfcraft) |
+| | **EffectCraft** | 모션 그래픽과 시각 효과 | [GitHub](https://github.com/storytold/effectcraft) | [웹사이트](https://getartcraft.com/apps/effectcraft) |
+| | **DesignCraft** | 페이지 레이아웃과 출판 | [GitHub](https://github.com/storytold/designcraft) | [웹사이트](https://getartcraft.com/apps/designcraft) |
+| | **FigCraft** | 인터페이스 디자인과 프로토타이핑 | [GitHub](https://github.com/storytold/figcraft) | [웹사이트](https://getartcraft.com/apps/figcraft) |
+| | **WordCraft** | 워드 프로세싱과 문서 | [GitHub](https://github.com/storytold/wordcraft) | [웹사이트](https://getartcraft.com/apps/wordcraft) |
+| | **SoundCraft** | 오디오 녹음과 편집 | [GitHub](https://github.com/storytold/soundcraft) | [웹사이트](https://getartcraft.com/apps/soundcraft) |
+| | **DeckCraft** | 프레젠테이션과 슬라이드 덱 | [GitHub](https://github.com/storytold/deckcraft) | [웹사이트](https://getartcraft.com/apps/deckcraft) |
+| | **CADCraft** | 컴퓨터 지원 설계(CAD) | [GitHub](https://github.com/storytold/cadcraft) | [웹사이트](https://getartcraft.com/apps/cadcraft) |
+| | **GridCraft** | 스프레드시트와 데이터 | [GitHub](https://github.com/storytold/gridcraft) | [웹사이트](https://getartcraft.com/apps/gridcraft) |
+| | **TextureCraft** | 3D 텍스처 페인팅 | [GitHub](https://github.com/storytold/texturecraft) | [웹사이트](https://getartcraft.com/apps/texturecraft) |
+| | **Craft Anything** | 원하는 소프트웨어를 분석하고, 나만의 작품을 만들어 보세요 | [GitHub](https://github.com/storytold/craft-anything) | [웹사이트](https://getartcraft.com/apps/craft-anything) |
+| | **SolidCraft** | Tangency Labs의 파라메트릭 3D CAD(커뮤니티 앱) | [GitHub](https://github.com/tangency-labs/solidcraft) | |
+
+## 라이선스와 크레딧
+
+Craft Anything은 [MIT](LICENSE-MIT) 또는 [Apache-2.0](LICENSE-APACHE) 듀얼 라이선스이며, 원하는 쪽을 선택할 수 있습니다.
+Copyright (c) 2026 ArtCraft Team and the Craft Anything contributors. 필수 고지는 [NOTICE](NOTICE)에 있습니다.
+
+동봉된 폰트, 아이콘, 이미지 및 기타 에셋은 각자의 오픈 라이선스를 유지합니다. 각 항목의 작성자, 출처, 라이선스는 [ATTRIBUTION.md](ATTRIBUTION.md)에 나와 있습니다.
+
+스크린샷에 보이는 것은 그 용도로 만든 예시 프로젝트입니다: example.com, 뱀 게임, 작은 계산기 프로그램.
+
+[`docs/brand/`](docs/brand/)에 있는 ArtCraft 이름, 워드마크, 로고는 ArtCraft Team의 상표이며 이 라이선스의 적용을 받지 않습니다. 수정하지 않은 상태에서만, 이 저장소와 Craft Anything의 일부로서만 사용할 수 있으며, 이때 [`docs/brand/LICENSE-brand.txt`](docs/brand/LICENSE-brand.txt)를 따라야 합니다. 포크와 수정본은 이를 제거해야 합니다.
+
+
+
+ ArtCraft 팀과 커뮤니티가 만들었습니다.
+
diff --git a/desktop/README.md b/desktop/README.md
new file mode 100644
index 0000000..30616fe
--- /dev/null
+++ b/desktop/README.md
@@ -0,0 +1,35 @@
+# REI Reverse Engineer Desktop
+
+Native Windows desktop fork of Craft Anything, based on upstream commit
+`ecbd519c6a4bac713bcd0ad1efa6b042c3c3e7da` (workspace version 0.3.0).
+Original repository: https://github.com/storytold/craft-anything.
+
+Rootcastle changes include the product title and application ID
+`com.rootcastle.rei`, an original geometric icon, Rootcastle blue/black colors,
+an isolated `rootcastle-rei` settings folder and `Documents/REI Reverse Engineer`
+project directory. `REI_CONFIG_DIR` overrides the settings location. Internal
+crate names and evidence/project formats retain upstream names for compatibility.
+
+Build from the parent repository with `./scripts/build_desktop.ps1`; add
+`-Release` for an optimized build. Requires Rust 1.97.0 and Visual Studio C++
+build tools / Windows SDK. Run with `./scripts/run_desktop.ps1`.
+
+The native Rust application registers the Python workbench and original REA
+as local MCP plugins. The Windows smoke test discovered 248 external tools
+and executed Python identification and upstream static entropy analysis through
+the desktop engine. Each server retains its own storage; this is a unified tool
+interface rather than a merged evidence database. Start with
+`../scripts/run_desktop.ps1 -Python ` after running upstream setup.
+AI builder features require a separately configured coding agent or model.
+
+Keep LICENSE-MIT, LICENSE-APACHE, NOTICE and ATTRIBUTION.md with distributions.
+Original copyright and third-party notices remain applicable. Upstream brand
+artwork in docs/brand is archival material, not Rootcastle branding; see its
+separate license. No upstream endorsement is claimed. ROOTCASTLE-FORK.json
+records the exact source revision and modifications.
+
+Windows compilation and native window startup passed. Local Python and Rust
+results, upstream pins and dependency advisory limitations are recorded in
+[the verification report](../docs/DESKTOP-VERIFICATION.md). Optimized packaging,
+signing and release CI remain pending. A local build does not prove every
+upstream analyzer's accuracy.
diff --git a/desktop/README.zh.md b/desktop/README.zh.md
new file mode 100644
index 0000000..c3b395d
--- /dev/null
+++ b/desktop/README.zh.md
@@ -0,0 +1,333 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Craft Anything
+
+
+ English · 日本語 · 简体中文 · 한국어
+
+
+
+ 看清任意网站、应用或游戏是怎么做的,再让 AI 给你造一个自己的版本;一个纯 Rust 写的开源净室引擎。
+ 把目标指给它,说清你想要什么,它就用真实的检查工具研究原作,
+ 让编码 agent 建一个新项目,把结果与原件比对,再反复收敛。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 下载 Craft Anything ·
+ ArtCraft ·
+ 全部 Crafting 应用
+
+
+
+
+
+
+
+ 一个网站项目:每项检查都说明构建成果有多像,规则之外的任何动作都要你批准,计划显示还剩什么没做。
+
+
+> [!NOTE]
+> **ArtCraft 是一个来自各行各业的创作者社区。** 画家、摄影师、导演、插画师、设计师、动画师、
+> 爱好者,以及上周才拿起笔的人。只要你动手做东西,你就是我们的一员。**[来 Discord 打个招呼](https://discord.gg/artcraft)。**
+
+
+ 工作原理 ·
+ 两种用法 ·
+ AI ·
+ 面向 agent ·
+ 内部实现 ·
+ 开始使用 ·
+ 状态 ·
+ 下载 ·
+ Discord
+
+
+
+
+
+
+
+ 🔍 研究一切
+ 网站、桌面程序、手机应用、游戏和文件:真实的检查工具弄清它们是怎么搭起来的、做了什么,每个结论都带着证据。
+
+
+ 🛠️ 造一个自己的
+ 说出你要什么(“复刻这个网站”、“用 Rust 复刻这个游戏的玩法”),编码 agent 就按你指定的技术栈建一个新项目。
+
+
+ ✅ 比对过,不是猜的
+ 每个构建都会与原件比对(网页按三种屏宽、游戏跑它自己的自检、应用看它的运行),并反复收敛到足够接近。
+
+
+ 🤖 为 agent 准备好
+ 314 个工具,每个既是 CLI 子命令、又是 MCP 工具,任何 agent 都能驱动跟应用同一个引擎。纯研究不需要 AI。
+
+
+
+
+
+
+
+
+## 工作原理
+
+把 Craft Anything 指向一个目标,说清你要什么,按下 Start。它分四步走,遇到该你拍板的地方就用大白话问你:
+
+1. **研究:** 检查原件:它在各种宽度下的页面、它的代码、它的文件、它怎么运行。
+2. **构建:** 编码 agent 建一个新项目,用你的请求指定的语言、框架或引擎(没指定就用合理默认)。
+3. **检查:** 把构建成果与原件比对,列出还不一致的地方。
+4. **就绪:** 打开、试玩或预览结果,或者继续提要求让它接着收敛。
+
+
+
+
+
+ Make: 选择要复刻什么,说出你要什么,按下 Start。
+
+
+
+ 就绪: 试玩游戏,或者在下面提改动要求。
+
+
+
+
+
+
+## 两种用法
+
+- **简单模式**(默认):“你想做什么?” 选一个网站、应用或游戏,描述它,按下 Start。项目页显示四个步骤(研究 → 构建 → 检查 → 就绪)、agent 正在做什么(用大白话)、提问卡和审批卡、结果有多像,以及打开结果或它所在文件夹的按钮。在底部的输入框里提改动要求。“只看里面”会打开一个目标供你提问,但不构建任何东西(需深度模式)。
+- **进阶模式**:完整的工作台:所有工具(Ctrl/Cmd+K)、函数、字符串、伪 C、反汇编、十六进制、调用图、证据、未知项、任务、日志和 agent 控制台。
+
+两种模式驱动同一个引擎。
+
+
+
+## 净室还是深度
+
+- **净室**(默认):绝不读取原件内部的任何东西——不读它的代码、文件或页面源码。Craft Anything 只依据你所能看到的来工作:你添加的图片、网站以访客身份呈现的样子(截图、可见文本、测量出的布局、颜色、字体与间距、展开的菜单),以及在你允许时截取的应用窗口画面。所有会往内部看的工具都会被拒绝,并告诉你如何开启深度模式。
+- **深度**:使用全部工具,供你自己私下使用。程序的代码和文件由内置引擎读取(装了 Ghidra 时也会用它),网站的 JavaScript 会被读取、起始页面会被持续观察,构建方也可以使用全部这些能力。
+
+在标题栏或设置里切换,或执行 `settings_set deep_mode true`,或单次 CLI 运行时加 `--deep`(或 `--clean-room`)。
+
+
+
+
+ 进阶: 函数、伪 C、agent,以及每个答案背后的证据。
+
+
+
+
+## AI
+
+构建需要你电脑上有一个已登录的编码 agent。**Claude Code** 是首选支持对象:Craft Anything 运行你自己那份未经改动的 `claude` 程序,它用你自己的登录;Craft Anything 从不查看也不接触你的账号,你对它的使用适用你与 Anthropic 自己的协议。原因与做法见 [docs/claude-code.md](docs/claude-code.md)。Codex 适配器已存在,但还没在真实安装上验证过。提问和分析可选用 Anthropic、OpenAI、OpenRouter 以及本地 OpenAI 兼容服务(设置 → AI)。
+
+
+
+## 面向 agent
+
+每个工具同时是一个 CLI 子命令和一个 MCP 工具,所以 agent 不用打开应用也能研究目标或跑完整个重建:
+
+```sh
+craft-anything-cli commands # every tool, by toolset
+craft-anything-cli mcp # serve them to any MCP client
+craft-anything-cli rebuild https://example.com "Recreate this website"
+```
+
+结果是带置信度、覆盖范围和局限性的证据记录;大结果先给摘要,再用 `core_evidence_browse` 分页读取。`craft-anything-cli setup` 把 Craft Anything 接到编码 agent 应用上。
+
+
+
+## 内部实现
+
+一个纯 Rust 的逆向工程引擎,314 个工具(引擎里 298 个,其余来自 AI 与设置层),覆盖原生程序、.NET、Java、Android、Apple、Electron/JavaScript、网站、固件、抓包、数据库和游戏文件。它自带的解码器、反汇编器和反编译器无需任何外部程序即可工作;深度模式会全部用上,装了 Ghidra 时也会使用它。它支撑着桌面应用(egui)、CLI 和 MCP 服务器,其中一部分还能以 WebAssembly 跑在浏览器里。完整清单在 [docs/tools.md](docs/tools.md)。
+
+
+
+## 开始使用
+
+需要 Rust **1.97 或更新**(有依赖要求它;`rustup update`)。在 Debian 或 Ubuntu 上,桌面应用还需要:
+
+```sh
+sudo apt-get install libxkbcommon-dev libwayland-dev libx11-dev libxrandr-dev libxi-dev libgl1-mesa-dev libgtk-3-dev
+```
+
+然后:
+
+```sh
+git clone https://github.com/storytold/craft-anything && cd craft-anything
+cargo run --release -p craft-anything # the desktop app
+cargo install --path apps/craft-anything-cli # the CLI, as `craft-anything-cli`
+craft-anything-cli doctor # checks the install, coding agents and engines
+```
+
+构建需要你电脑上已登录一个编码 agent(见 [AI](#ai));`doctor` 会告诉你缺什么。从命令行重建(问题在终端里问):
+
+```sh
+craft-anything-cli rebuild https://example.com "Recreate this website"
+craft-anything-cli rebuild - "A snake game in Rust" --rounds 1
+craft-anything-cli rebuild https://example.com "Make the header blue" --continue-in ""
+craft-anything-cli rebuild - "Add a contact form" --project-folder "" --existing edit
+```
+
+新项目放在 `Documents/Craft Anything`(可在设置 → 通用里改)。每个项目文件夹里有 `craft.json`(请求、目标、状态和历史)、`capture/`(从目标录到的内容)、`app/`(生成的项目)、`checks/`(每次与原件比对的结果)、`investigation.cany/`(证据)和 `inbox/`(待回答问题)。
+
+项目也可以从你指定的文件夹开始(Start 下的“选择文件夹…”,或 `--project-folder`)。如果那里已经有文件,你要说明它们是什么:就是应用本身(构建方会就地修改),还是作为起点用的素材(应用会建在它们旁边的 `app/` 里);设置 → 通用可以记住你的选择。设置 → 通用还会列出其他文件夹里的项目;移除一个只是把它从列表里拿掉。
+
+
+
+## 状态
+
+> [!IMPORTANT]
+> **状态:** Craft Anything 处于早期 alpha。今天能用的、部分可用的,以及接下来要做的:
+>
+> - **今天可用:** 桌面应用(简单与进阶模式)、CLI 和 MCP 服务器;以 Claude Code 作为构建方、面向网站/应用/游戏的
+> 研究 → 构建 → 检查 → 收敛 流程;三种屏宽下的网站检查;Unity、Unreal 或 Godot 的游戏重建;带 Ghidra 的深度
+> 模式;生成图片和 3D 模型(需自行选择开启,用你自己的 fal.ai 密钥);插件(MCP 服务器,以及开启 `wasm-plugins`
+> 选项的构建中的 WASM 组件)。
+> - **部分可用:** 用 API 密钥和其他编码 agent 构建(已实现,尚未真机跑通),应用和游戏的行为检查,少数引擎工具只用替身测过。
+> - **接下来:** 第一次公开发布(Windows、macOS、Linux 和 FreeBSD 的安装包,以及浏览器查看器都已在 CI 里配好),然后是托管版。
+>
+> [docs/STATUS.md](docs/STATUS.md) 有细节和每部分的验证方式,[docs/tool-status.md](docs/tool-status.md) 是逐个工具的清单,
+> [docs/ROADMAP.md](docs/ROADMAP.md) 是逐项的计划。欢迎提 issue,或者到 [Discord](https://discord.gg/artcraft) 说哪里坏了。
+
+**负责地使用。** 请在你拥有或获准检查的软件上使用 Craft Anything。它绝不绕过 DRM、登录或反作弊。重建是依据证据复现布局、风格与行为;agent 被要求不得复制 logo、照片或文本,除非你确认你拥有它们或已获授权。
+
+
+
+## 下载
+
+在 GitHub 上,[最新发布](https://github.com/storytold/craft-anything/releases/latest) 列出了下面所有构建,[全部发布](https://github.com/storytold/craft-anything/releases) 有更早的版本和它们的说明。文件名里的 `` 是版本号,`SHA256SUMS.txt` 列出每个文件的校验和。
+
+### Windows
+
+| 构建 | 安装包 | 便携版 |
+|---|---|---|
+| x64(64 位 Intel/AMD) | `craft-anything--windows-x64.msi` | `craft-anything--windows-x64-portable.zip` |
+| arm64(骁龙及其他 ARM PC) | `craft-anything--windows-arm64.msi` | `craft-anything--windows-arm64-portable.zip` |
+| x86(32 位) | `craft-anything--windows-x86.msi` | `craft-anything--windows-x86-portable.zip` |
+
+安装包和可执行文件都有代码签名。便携版把设置放在应用旁边。
+
+### macOS
+
+| 构建 | 文件 | 说明 |
+|---|---|---|
+| 应用,通用版(Apple silicon + Intel) | `craft-anything--macos-universal.dmg` | 已签名并公证 |
+| 命令行工具,通用版 | `craft-anything-cli--macos-universal.zip` | 已签名并公证 |
+
+### Linux
+
+| 格式 | x86_64 | aarch64(ARM64) | 说明 |
+|---|---|---|---|
+| AppImage | `craft-anything--linux-x86_64.AppImage` | `craft-anything--linux-aarch64.AppImage` | 随处可跑;用 [AppImageUpdate](https://github.com/AppImageCommunity/AppImageUpdate) 自我更新(`.zsync` 文件) |
+| Flatpak | `craft-anything--linux-x86_64.flatpak` | `craft-anything--linux-aarch64.flatpak` | 沙箱运行;`flatpak install --user ` |
+| Debian/Ubuntu | `craft-anything--linux-x86_64.deb` | `craft-anything--linux-aarch64.deb` | |
+| Fedora/RHEL/openSUSE | `craft-anything--linux-x86_64.rpm` | `craft-anything--linux-aarch64.rpm` | |
+| Tarball | `craft-anything--linux-x86_64.tar.gz` | `craft-anything--linux-aarch64.tar.gz` | 解压到任意位置 |
+
+RISC-V(riscv64):`craft-anything--linux-riscv64.tar.gz`,只有 tarball。
+
+### FreeBSD
+
+| 构建 | 文件 |
+|---|---|
+| x86_64 | `craft-anything--freebsd-x86_64.tar.gz` |
+
+### Web(WebAssembly)
+
+| 构建 | 文件 | 说明 |
+|---|---|---|
+| 浏览器查看器 | `craft-anything-web-.zip` | 在浏览器里说明一个文件是什么(文件本身不离开浏览器);把这个文件夹放到任意静态 Web 服务器上即可([packaging/web/README.md](packaging/web/README.md)) |
+
+
+
+
+
+
+
+来和我们一起做东西
+
+
+ 我们的 Discord 是各类创作者待的地方:画画的人、拍摄的人、绘图的人、剪片子的人、排版的人,
+ 以及还在摸索自己喜欢做什么的人。分享你正在做的东西、求助、告诉我们哪里坏了,或者说说你希望
+ 这些工具能做什么。不论你用什么媒介、入行多久,这里都欢迎你。
+
+
+
+ discord.gg/artcraft ·
+ getartcraft.com ·
+ The Crafting Apps ·
+ Craft Anything
+
+
+---
+
+## The Craft App Suite
+
+| | 应用 | 用来做什么 | 代码 | 了解更多 |
+|:-:|---|---|---|---|
+| | **PhotoCraft** | 图像编辑:图层、蒙版、文字,以及真正的 PSD 文件 | [GitHub](https://github.com/storytold/photocraft) | [网站](https://getartcraft.com/apps/photocraft) |
+| | **VectorCraft** | 矢量插画 | [GitHub](https://github.com/storytold/vectorcraft) | [网站](https://getartcraft.com/apps/vectorcraft) |
+| | **FilmCraft** | 视频剪辑、调色与声音 | [GitHub](https://github.com/storytold/filmcraft) | [网站](https://getartcraft.com/apps/filmcraft) |
+| | **LightCraft** | 照片库与 raw 冲图 | [GitHub](https://github.com/storytold/lightcraft) | [网站](https://getartcraft.com/apps/lightcraft) |
+| | **PdfCraft** | 阅读、整理与保护 PDF | [GitHub](https://github.com/storytold/pdfcraft) | [网站](https://getartcraft.com/apps/pdfcraft) |
+| | **EffectCraft** | 动态图形与视觉特效 | [GitHub](https://github.com/storytold/effectcraft) | [网站](https://getartcraft.com/apps/effectcraft) |
+| | **DesignCraft** | 页面排版与出版 | [GitHub](https://github.com/storytold/designcraft) | [网站](https://getartcraft.com/apps/designcraft) |
+| | **FigCraft** | 界面设计与原型 | [GitHub](https://github.com/storytold/figcraft) | [网站](https://getartcraft.com/apps/figcraft) |
+| | **WordCraft** | 文字处理与文档 | [GitHub](https://github.com/storytold/wordcraft) | [网站](https://getartcraft.com/apps/wordcraft) |
+| | **SoundCraft** | 音频录制与编辑 | [GitHub](https://github.com/storytold/soundcraft) | [网站](https://getartcraft.com/apps/soundcraft) |
+| | **DeckCraft** | 演示文稿与幻灯片 | [GitHub](https://github.com/storytold/deckcraft) | [网站](https://getartcraft.com/apps/deckcraft) |
+| | **CADCraft** | 计算机辅助设计 | [GitHub](https://github.com/storytold/cadcraft) | [网站](https://getartcraft.com/apps/cadcraft) |
+| | **GridCraft** | 电子表格与数据 | [GitHub](https://github.com/storytold/gridcraft) | [网站](https://getartcraft.com/apps/gridcraft) |
+| | **TextureCraft** | 3D 贴图绘制 | [GitHub](https://github.com/storytold/texturecraft) | [网站](https://getartcraft.com/apps/texturecraft) |
+| | **Craft Anything** | 看清任意软件是怎么做的,然后造一个自己的 | [GitHub](https://github.com/storytold/craft-anything) | [网站](https://getartcraft.com/apps/craft-anything) |
+| | **SolidCraft** | Tangency Labs 的参数化 3D CAD(社区应用) | [GitHub](https://github.com/tangency-labs/solidcraft) | |
+
+
+
+
+## 许可与致谢
+
+Craft Anything 采用 [MIT](LICENSE-MIT) 或 [Apache-2.0](LICENSE-APACHE) 双许可,任你选择。
+Copyright (c) 2026 ArtCraft Team and the Craft Anything contributors。必需的声明在 [NOTICE](NOTICE)。
+
+随附的字体、图标、图片和其他资源各自保留其开源许可;每一项都在 [ATTRIBUTION.md](ATTRIBUTION.md) 里列出了作者、来源和许可。
+
+截图里展示的是为它们制作的示例项目:example.com、一个贪吃蛇游戏和一个小计算器程序。
+
+[`docs/brand/`](docs/brand/) 里的 ArtCraft 名称、标志图形和 logo 是 ArtCraft Team 的商标,不在本许可范围内。
+它们只能在未作修改的前提下,作为本仓库和 Craft Anything 的一部分使用,并须遵守
+[`docs/brand/LICENSE-brand.txt`](docs/brand/LICENSE-brand.txt)。分支和修改版必须移除它们。
+
+
+
+ 由 ArtCraft 团队与社区制作。
+
+
+---
+
+本页是 [README.md](README.md) 的简体中文翻译。如有出入,以英文版为准。
diff --git a/desktop/ROOTCASTLE-FORK.json b/desktop/ROOTCASTLE-FORK.json
new file mode 100644
index 0000000..eb111c4
--- /dev/null
+++ b/desktop/ROOTCASTLE-FORK.json
@@ -0,0 +1,16 @@
+{
+ "product": "REI Reverse Engineer Desktop",
+ "upstream": "https://github.com/storytold/craft-anything",
+ "revision": "ecbd519c6a4bac713bcd0ad1efa6b042c3c3e7da",
+ "upstream_version": "0.3.0",
+ "license": "MIT OR Apache-2.0",
+ "fork_changes": [
+ "product identity",
+ "Rootcastle palette and original monogram",
+ "isolated settings directory",
+ "rootcastle-rei executable",
+ "Python REI MCP integration",
+ "upstream REA MCP integration",
+ "unified evidence routing and bridge diagnostic"
+ ]
+}
diff --git a/desktop/ROOTCASTLE.md b/desktop/ROOTCASTLE.md
new file mode 100644
index 0000000..30616fe
--- /dev/null
+++ b/desktop/ROOTCASTLE.md
@@ -0,0 +1,35 @@
+# REI Reverse Engineer Desktop
+
+Native Windows desktop fork of Craft Anything, based on upstream commit
+`ecbd519c6a4bac713bcd0ad1efa6b042c3c3e7da` (workspace version 0.3.0).
+Original repository: https://github.com/storytold/craft-anything.
+
+Rootcastle changes include the product title and application ID
+`com.rootcastle.rei`, an original geometric icon, Rootcastle blue/black colors,
+an isolated `rootcastle-rei` settings folder and `Documents/REI Reverse Engineer`
+project directory. `REI_CONFIG_DIR` overrides the settings location. Internal
+crate names and evidence/project formats retain upstream names for compatibility.
+
+Build from the parent repository with `./scripts/build_desktop.ps1`; add
+`-Release` for an optimized build. Requires Rust 1.97.0 and Visual Studio C++
+build tools / Windows SDK. Run with `./scripts/run_desktop.ps1`.
+
+The native Rust application registers the Python workbench and original REA
+as local MCP plugins. The Windows smoke test discovered 248 external tools
+and executed Python identification and upstream static entropy analysis through
+the desktop engine. Each server retains its own storage; this is a unified tool
+interface rather than a merged evidence database. Start with
+`../scripts/run_desktop.ps1 -Python ` after running upstream setup.
+AI builder features require a separately configured coding agent or model.
+
+Keep LICENSE-MIT, LICENSE-APACHE, NOTICE and ATTRIBUTION.md with distributions.
+Original copyright and third-party notices remain applicable. Upstream brand
+artwork in docs/brand is archival material, not Rootcastle branding; see its
+separate license. No upstream endorsement is claimed. ROOTCASTLE-FORK.json
+records the exact source revision and modifications.
+
+Windows compilation and native window startup passed. Local Python and Rust
+results, upstream pins and dependency advisory limitations are recorded in
+[the verification report](../docs/DESKTOP-VERIFICATION.md). Optimized packaging,
+signing and release CI remain pending. A local build does not prove every
+upstream analyzer's accuracy.
diff --git a/desktop/SECURITY.md b/desktop/SECURITY.md
new file mode 100644
index 0000000..7683ea4
--- /dev/null
+++ b/desktop/SECURITY.md
@@ -0,0 +1,23 @@
+# Security
+
+Craft Anything reads hostile files and runs other programs, so we take reports seriously.
+
+## Reporting a vulnerability
+
+Please report it privately: on GitHub, open the repository's **Security** tab and choose
+**Report a vulnerability**. Don't open a public issue for it. Say what's affected, how to reproduce
+it, and what an attacker gains. We'll reply within a week and tell you what we'll do.
+
+## What counts
+
+- A crash, hang or runaway memory use on any input file or website (never-crash is a hard rule:
+ [AGENTS.md](AGENTS.md)).
+- Code from a target running without the person's consent, or a tool doing more than its declared
+ effects.
+- A path that lets a coding agent act outside its project without approval.
+- A key, token or other secret reaching a log, a tool's output, evidence or the repository.
+- A plugin escaping its sandbox (WASM plugins import nothing; MCP plugins ask before each call).
+
+## Supported versions
+
+Fixes go into the latest release.
diff --git a/desktop/UPSTREAM-README.md b/desktop/UPSTREAM-README.md
new file mode 100644
index 0000000..0d87bdd
--- /dev/null
+++ b/desktop/UPSTREAM-README.md
@@ -0,0 +1,380 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+Craft Anything
+
+
+ English · 日本語 · 简体中文 · 한국어
+
+
+
+ See how any website, app or game works, then have AI build your own version; an open-source, clean-room engine in pure Rust.
+ Point it at a target, say what you want, and it studies the original with real inspection tools,
+ has a coding agent build a new project, checks the result against the original and refines it.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Download Craft Anything ·
+ ArtCraft ·
+ All Crafting Apps
+
+
+
+
+
+
+
+ A website project: each check says how alike the build is, you approve anything outside the rules, and the plan shows what's left.
+
+
+> [!NOTE]
+> **ArtCraft is a community of artists from all walks of life.** Painters, photographers,
+> filmmakers, illustrators, designers, animators, hobbyists, and people who picked up a pencil
+> last week. If you make things, you're one of us. **[Come say hi on Discord](https://discord.gg/artcraft).**
+
+
+ How it works ·
+ Two ways to use it ·
+ AI ·
+ Agents ·
+ Under the hood ·
+ Get started ·
+ Status ·
+ Downloads ·
+ Discord
+
+
+
+
+
+
+
+ 🔍 Study anything
+ Websites, desktop apps, phone apps, games and files: real inspection tools find out how they're built and what they do, with the evidence behind every answer.
+
+
+ 🛠️ Build your own
+ Say what you want ("Recreate this website", "Recreate this game's gameplay in Rust") and a coding agent builds a new project in the stack you ask for.
+
+
+ ✅ Checked, not guessed
+ Every build is compared with the original (pages at three screen widths, a game's own self-test, an app's runs) and refined until it's close.
+
+
+ 🤖 Agent-ready
+ 314 tools, each one a CLI command and an MCP tool, so any agent can drive the same engine the app uses. No AI is needed to inspect.
+
+
+
+
+
+
+## How it works
+
+Point Craft Anything at a target, say what you want, and press Start. It works in four steps,
+asking you in plain words when a decision is yours:
+
+1. **Study:** inspect the original: its pages at every width, its code, its files, how it runs.
+2. **Build:** a coding agent builds a new project, in the language, framework or engine your
+ request names (or a sensible default).
+3. **Check:** compare the build with the original and list what doesn't match yet.
+4. **Ready:** open, play or preview the result, or ask for changes and it refines it.
+
+
+
+
+
+ Make: pick what to recreate, say what you want, press Start.
+
+
+
+ Ready: play the game, or ask for changes below.
+
+
+
+
+## Two ways to use it
+
+- **Simple** (default): "What do you want to make?" Pick a website, app or game, describe it,
+ press Start. A project page shows the four steps (Study → Build → Check → Ready), what the
+ agent is doing in plain words, question and approval cards, how alike the result is, and
+ buttons to open the result or its folder. Ask for changes in the box at the bottom.
+ "Just look inside" opens a target for questions without building anything (in Deep mode).
+- **Advanced**: the full workbench: every tool (Ctrl/Cmd+K), functions, strings, pseudo-C,
+ disassembly, hex, call graphs, evidence, unknowns, jobs, logs and the agent console.
+
+Both modes drive the same engine.
+
+## Clean room or Deep
+
+- **Clean room** (the default): nothing inside the original is read, not its code, files or page
+ source. Craft Anything works from what you can see: pictures you add, the website as a visitor
+ sees it (screenshots, visible text, measured layout, colours, fonts and spacing, menus opened)
+ and, when you allow it, a picture of the app's window. Tools that would look inside are refused
+ and say how to turn on Deep mode.
+- **Deep**: every tool is used, for your private use. A program's code and files are read with the
+ built-in engines (and Ghidra when it's installed), a website's JavaScript is read and its start
+ page watched, and the builder may use all of them.
+
+Switch in the title bar or Settings, with `settings_set deep_mode true`, or for one CLI run with
+`--deep` (or `--clean-room`).
+
+
+
+
+ Advanced: functions, pseudo-C, the agent and the evidence behind every answer.
+
+
+## AI
+
+Building needs a coding agent on your computer. **Claude Code** is supported first: Craft
+Anything runs your own, unmodified `claude` program, which uses your own sign-in; Craft Anything
+never sees or handles your account. Your use of it falls under your own agreement with
+Anthropic. See [docs/claude-code.md](docs/claude-code.md) for how and why. A Codex adapter
+exists but hasn't been verified on a real install yet. Other agents that speak ACP (Gemini CLI,
+Qwen Code, opencode, goose, Hermes, Maki) can be switched to it per backend; of those, only Maki has
+been run live. Anthropic, OpenAI, OpenRouter and local
+OpenAI-compatible servers are available for questions and analysis (Settings → AI).
+
+## Built for agents
+
+Every tool is also a CLI subcommand and an MCP tool, so an agent can study a target or run a
+whole rebuild without the app:
+
+```sh
+craft-anything-cli commands # every tool, by toolset
+craft-anything-cli mcp # serve them to any MCP client
+craft-anything-cli rebuild https://example.com "Recreate this website"
+```
+
+Results are evidence records with their confidence, coverage and limitations; big ones come back
+as a summary first and are paged with `core_evidence_browse`. `craft-anything-cli setup`
+connects Craft Anything to coding-agent apps.
+
+## Under the hood
+
+A pure-Rust reverse-engineering engine with 314 tools (298 in the engine, the rest from the AI and
+setup layers) for native programs, .NET, Java, Android, Apple, Electron/JavaScript, websites,
+firmware, packet captures, databases and game files. Its own decoders, disassembler and
+decompiler work without any outside program; Deep mode uses them all, and Ghidra when it's
+installed. It powers the desktop app (egui), the CLI and the MCP server, and parts of it run in
+the browser as WebAssembly. The full list is in [docs/tools.md](docs/tools.md).
+
+## Get started
+
+Requires Rust **1.97 or newer** (a dependency needs it; `rustup update`). On Debian or Ubuntu,
+the desktop app also needs:
+
+```sh
+sudo apt-get install libxkbcommon-dev libwayland-dev libx11-dev libxrandr-dev libxi-dev libgl1-mesa-dev libgtk-3-dev
+```
+
+Then:
+
+```sh
+git clone https://github.com/storytold/craft-anything && cd craft-anything
+cargo run --release -p craft-anything # the desktop app
+cargo install --path apps/craft-anything-cli # the CLI, as `craft-anything-cli`
+craft-anything-cli doctor # checks the install, coding agents and engines
+```
+
+Building needs a coding agent signed in on your computer (see [AI](#ai)); `doctor` says what's
+missing. Rebuild from the command line (questions are asked in the terminal):
+
+```sh
+craft-anything-cli rebuild https://example.com "Recreate this website"
+craft-anything-cli rebuild - "A snake game in Rust" --rounds 1
+craft-anything-cli rebuild https://example.com "Make the header blue" --continue-in ""
+craft-anything-cli rebuild - "Add a contact form" --project-folder "" --existing edit
+```
+
+New projects go in `Documents/Craft Anything` (change it in Settings → General). Each project
+folder holds `craft.json` (the request, target, status and history), `capture/` (what was
+recorded from the target), `app/` (the generated project), `checks/` (each comparison against
+the original), `investigation.cany/` (evidence) and `inbox/` (open questions).
+
+A project can also start in a folder you pick (Choose a folder… under Start, or `--project-folder`).
+If it already has files, you say what they are: the app itself, which the builder edits in place,
+or material to start from, with the app built in `app/` beside them (Settings → General can
+remember the answer). Settings → General also lists projects from more folders; removing one
+only takes it off the list.
+
+## Status
+
+> [!IMPORTANT]
+> **Status:** Craft Anything is in early alpha. What works today, what is partial and what's next:
+>
+> - **Works today:** the desktop app (Simple and Advanced), the CLI and the MCP server; the
+> Study → Build → Check → Refine workflow for websites, apps and games with Claude Code as the
+> builder; website checks at three screen widths; game rebuilds in Unity, Unreal or Godot; Deep
+> mode with Ghidra; generated pictures and 3D models (opt-in, your own fal.ai key); plugins (MCP
+> servers, and WASM components in builds with the `wasm-plugins` option).
+> - **Partly there:** building with API keys and other coding agents (built, not yet run live),
+> behaviour checks for apps and games, a few engine tools tested only against stand-ins.
+> - **Next:** the first public release (installers for Windows, macOS, Linux and FreeBSD, and the
+> browser viewer are set up in CI), then the managed tier.
+>
+> [docs/STATUS.md](docs/STATUS.md) has the detail and how each part was checked,
+> [docs/tool-status.md](docs/tool-status.md) the tool-by-tool checklist, and
+> [docs/ROADMAP.md](docs/ROADMAP.md) the plan, item by item. Please file issues, or tell us what
+> broke on [Discord](https://discord.gg/artcraft).
+
+**Responsible use.** Use Craft Anything on software you own or are allowed to examine. It never
+bypasses DRM, logins or anti-cheat. Rebuilds recreate layout, style and behaviour from evidence;
+the agent is told not to copy logos, photos or text unless you confirm you own them or have
+permission.
+
+## Downloads
+
+On GitHub, the [latest release](https://github.com/storytold/craft-anything/releases/latest) has every build listed below, and [all releases](https://github.com/storytold/craft-anything/releases) has earlier versions and their notes. `` in the file names is the version number, and `SHA256SUMS.txt` lists a checksum for every file.
+
+### Windows
+
+| Build | Installer | Portable |
+|---|---|---|
+| x64 (64-bit Intel/AMD) | `craft-anything--windows-x64.msi` | `craft-anything--windows-x64-portable.zip` |
+| arm64 (Snapdragon and other ARM PCs) | `craft-anything--windows-arm64.msi` | `craft-anything--windows-arm64-portable.zip` |
+| x86 (32-bit) | `craft-anything--windows-x86.msi` | `craft-anything--windows-x86-portable.zip` |
+
+Installers and executables are code-signed. The portable zip keeps its settings next to the app.
+
+### macOS
+
+| Build | File | Notes |
+|---|---|---|
+| App, universal (Apple silicon + Intel) | `craft-anything--macos-universal.dmg` | Signed and notarized |
+| Command-line tool, universal | `craft-anything-cli--macos-universal.zip` | Signed and notarized; see below if macOS blocks it |
+
+Versions after 0.2.0 include the command-line tool in the app, so builds don't need the separate
+zip. You only need it to use the command line without the app.
+
+A browser download of the zip can stop with "Apple could not verify craft-anything-cli" (a lone
+program can't carry its notarization with it, so macOS has to look it up online). A download from
+the terminal isn't checked this way, so use this instead:
+
+```sh
+ver=0.2.0 # change this to the version you want
+curl -fLO "https://github.com/storytold/craft-anything/releases/download/v$ver/craft-anything-cli-$ver-macos-universal.zip"
+ditto -x -k "craft-anything-cli-$ver-macos-universal.zip" .
+mkdir -p ~/.local/bin
+mv "craft-anything-cli-$ver-macos-universal/craft-anything-cli" ~/.local/bin/
+~/.local/bin/craft-anything-cli --version
+```
+
+To run it as just `craft-anything-cli`, add `~/.local/bin` to your `PATH`.
+
+If you already have the blocked copy, press Done (not Move to Bin), then run
+`xattr -d com.apple.quarantine `, or allow it in System Settings →
+Privacy & Security → Open Anyway.
+
+### Linux
+
+| Format | x86_64 | aarch64 (ARM64) | Notes |
+|---|---|---|---|
+| AppImage | `craft-anything--linux-x86_64.AppImage` | `craft-anything--linux-aarch64.AppImage` | Runs anywhere; updates itself with [AppImageUpdate](https://github.com/AppImageCommunity/AppImageUpdate) (`.zsync` files) |
+| Flatpak | `craft-anything--linux-x86_64.flatpak` | `craft-anything--linux-aarch64.flatpak` | Sandboxed; `flatpak install --user ` |
+| Debian/Ubuntu | `craft-anything--linux-x86_64.deb` | `craft-anything--linux-aarch64.deb` | |
+| Fedora/RHEL/openSUSE | `craft-anything--linux-x86_64.rpm` | `craft-anything--linux-aarch64.rpm` | |
+| Tarball | `craft-anything--linux-x86_64.tar.gz` | `craft-anything--linux-aarch64.tar.gz` | Unpack anywhere |
+
+RISC-V (riscv64): `craft-anything--linux-riscv64.tar.gz`, a tarball only.
+
+### FreeBSD
+
+| Build | File |
+|---|---|
+| x86_64 | `craft-anything--freebsd-x86_64.tar.gz` |
+
+### Web (WebAssembly)
+
+| Build | File | Notes |
+|---|---|---|
+| Browser viewer | `craft-anything-web-.zip` | Says what a file is, in the browser (the file never leaves it); host the folder on any static web server ([packaging/web/README.md](packaging/web/README.md)) |
+
+
+
+
+
+
+
+Come make things with us
+
+
+ Our Discord is where artists of every kind hang out: people who paint, shoot, draw, cut film,
+ set type, and people still figuring out what they like to make. Share what you're working on,
+ ask for help, tell us what's broken, or tell us what you wish these tools could do.
+ Whatever your medium and however long you've been at it, you're welcome here.
+
+
+
+ discord.gg/artcraft ·
+ getartcraft.com ·
+ The Crafting Apps ·
+ Craft Anything
+
+
+---
+
+## The Craft App Suite
+
+| | App | What it's for | Code | Learn more |
+|:-:|---|---|---|---|
+| | **PhotoCraft** | Image editing: layers, masks, type and real PSD files | [GitHub](https://github.com/storytold/photocraft) | [Website](https://getartcraft.com/apps/photocraft) |
+| | **VectorCraft** | Vector illustration | [GitHub](https://github.com/storytold/vectorcraft) | [Website](https://getartcraft.com/apps/vectorcraft) |
+| | **FilmCraft** | Video editing, color and sound | [GitHub](https://github.com/storytold/filmcraft) | [Website](https://getartcraft.com/apps/filmcraft) |
+| | **LightCraft** | Photo library and raw development | [GitHub](https://github.com/storytold/lightcraft) | [Website](https://getartcraft.com/apps/lightcraft) |
+| | **PdfCraft** | Reading, organizing and protecting PDFs | [GitHub](https://github.com/storytold/pdfcraft) | [Website](https://getartcraft.com/apps/pdfcraft) |
+| | **EffectCraft** | Motion graphics and visual effects | [GitHub](https://github.com/storytold/effectcraft) | [Website](https://getartcraft.com/apps/effectcraft) |
+| | **DesignCraft** | Page layout and publishing | [GitHub](https://github.com/storytold/designcraft) | [Website](https://getartcraft.com/apps/designcraft) |
+| | **FigCraft** | Interface design and prototyping | [GitHub](https://github.com/storytold/figcraft) | [Website](https://getartcraft.com/apps/figcraft) |
+| | **WordCraft** | Word processing and documents | [GitHub](https://github.com/storytold/wordcraft) | [Website](https://getartcraft.com/apps/wordcraft) |
+| | **SoundCraft** | Audio recording and editing | [GitHub](https://github.com/storytold/soundcraft) | [Website](https://getartcraft.com/apps/soundcraft) |
+| | **DeckCraft** | Presentations and slide decks | [GitHub](https://github.com/storytold/deckcraft) | [Website](https://getartcraft.com/apps/deckcraft) |
+| | **CADCraft** | Computer-aided design | [GitHub](https://github.com/storytold/cadcraft) | [Website](https://getartcraft.com/apps/cadcraft) |
+| | **GridCraft** | Spreadsheets and data | [GitHub](https://github.com/storytold/gridcraft) | [Website](https://getartcraft.com/apps/gridcraft) |
+| | **TextureCraft** | 3D texture painting | [GitHub](https://github.com/storytold/texturecraft) | [Website](https://getartcraft.com/apps/texturecraft) |
+| | **Craft Anything** | See how any software works, then build your own | [GitHub](https://github.com/storytold/craft-anything) | [Website](https://getartcraft.com/apps/craft-anything) |
+| | **SolidCraft** | Parametric 3D CAD by Tangency Labs (community app) | [GitHub](https://github.com/tangency-labs/solidcraft) | |
+
+
+## License and credits
+
+Craft Anything is dual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE), at your option.
+Copyright (c) 2026 ArtCraft Team and the Craft Anything contributors. Required notices are in [NOTICE](NOTICE).
+
+Bundled fonts, icons, images and other assets keep their own open licenses; each one is listed
+with its author, source and license in [ATTRIBUTION.md](ATTRIBUTION.md).
+
+The screenshots show sample projects made for them: example.com, a snake game and a small
+calculator program.
+
+The ArtCraft name, wordmark and logos in [`docs/brand/`](docs/brand/) are trademarks of the
+ArtCraft Team and are not covered by this license. They may be used only unmodified, and only as
+part of this repository and Craft Anything, under [`docs/brand/LICENSE-brand.txt`](docs/brand/LICENSE-brand.txt).
+Forks and modified versions must remove them.
+
+
+
+ Made by the ArtCraft team and community.
+
diff --git a/desktop/about.hbs b/desktop/about.hbs
new file mode 100644
index 0000000..c09c465
--- /dev/null
+++ b/desktop/about.hbs
@@ -0,0 +1,35 @@
+
+
+
+
+Craft Anything: third-party licences
+
+
+
+Third-party licences
+Craft Anything includes the following Rust crates. Fonts and icons bundled from
+assets/ are listed in NOTICE and ATTRIBUTION.md.
+Summary
+
+{{#each overview}}
+{{name}} ({{count}})
+{{/each}}
+
+Licences
+{{#each licenses}}
+
+{{name}}
+Used by:
+
+{{text}}
+
+{{/each}}
+
+
diff --git a/desktop/about.toml b/desktop/about.toml
new file mode 100644
index 0000000..2a8b24a
--- /dev/null
+++ b/desktop/about.toml
@@ -0,0 +1,23 @@
+# cargo-about config: builds the third-party licence bundle shipped with releases.
+# cargo about generate about.hbs -o THIRD-PARTY-LICENSES.html
+# Keep `accepted` in step with deny.toml (policy: docs/licences.md).
+accepted = [
+ "MIT",
+ "Apache-2.0",
+ "Apache-2.0 WITH LLVM-exception",
+ "BSD-2-Clause",
+ "BSD-3-Clause",
+ "ISC",
+ "Zlib",
+ "0BSD",
+ "Unicode-3.0",
+ "BSL-1.0",
+ "CDLA-Permissive-2.0",
+ "OFL-1.1",
+ "Ubuntu-font-1.0",
+ "MPL-2.0", # only option-ext; deny.toml enforces that
+]
+# Bundle only what ships: skip dev-dependencies and build tooling crates.
+ignore-dev-dependencies = true
+ignore-build-dependencies = false
+workarounds = ["ring"]
diff --git a/desktop/apps/craft-anything-cli/Cargo.toml b/desktop/apps/craft-anything-cli/Cargo.toml
new file mode 100644
index 0000000..e4c8140
--- /dev/null
+++ b/desktop/apps/craft-anything-cli/Cargo.toml
@@ -0,0 +1,34 @@
+[package]
+name = "craft-anything-cli"
+description = "Craft Anything on the command line: every tool, the MCP server, setup and doctor"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+publish = false
+
+[lints]
+workspace = true
+
+[dependencies]
+craft-anything-engine = { workspace = true }
+craft-anything-engines = { workspace = true }
+craft-anything-evidence = { workspace = true }
+craft-anything-platform = { workspace = true }
+craft-anything-mcp = { workspace = true }
+craft-anything-ai = { workspace = true }
+craft-anything-setup = { workspace = true }
+serde = { workspace = true }
+serde_json = { workspace = true }
+log = { workspace = true }
+clap = { version = "4", features = ["derive"] }
+ctrlc = { version = "3", features = ["termination"] }
+
+[features]
+wasm-plugins = ["craft-anything-engine/wasm-plugins"]
+
+[dev-dependencies]
+tempfile = "3"
+
+[build-dependencies]
+# The DPI-aware manifest for craft-anything-cli.exe; build.rs only uses it when targeting Windows.
+winresource = "0.1"
diff --git a/desktop/apps/craft-anything-cli/build.rs b/desktop/apps/craft-anything-cli/build.rs
new file mode 100644
index 0000000..613f617
--- /dev/null
+++ b/desktop/apps/craft-anything-cli/build.rs
@@ -0,0 +1,30 @@
+//! Windows only: mark `craft-anything-cli.exe` DPI-aware (an application manifest), so window
+//! pictures (`native_window_capture`) and screen coordinates are in real pixels at any display
+//! scaling. On every other target this does nothing. A missing resource compiler is a warning,
+//! unless `CRAFT_ANYTHING_REQUIRE_WINRES=1` (the release workflow) makes it an error.
+
+const MANIFEST: &str = r#"
+
+
+ true/pm
+ PerMonitorV2
+
+
+ "#;
+
+fn main() {
+ println!("cargo:rerun-if-changed=build.rs");
+ println!("cargo:rerun-if-env-changed=CRAFT_ANYTHING_REQUIRE_WINRES");
+ if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows") {
+ return;
+ }
+ let mut res = winresource::WindowsResource::new();
+ res.set_manifest(MANIFEST).set("OriginalFilename", "craft-anything-cli.exe").set("InternalName", "craft-anything-cli");
+ if let Err(e) = res.compile() {
+ if std::env::var_os("CRAFT_ANYTHING_REQUIRE_WINRES").is_some() {
+ println!("cargo::error=embedding Windows resources failed: {e}");
+ return;
+ }
+ println!("cargo:warning=craft-anything-cli.exe built without its DPI-aware manifest: {e}");
+ }
+}
diff --git a/desktop/apps/craft-anything-cli/src/ai_cmd.rs b/desktop/apps/craft-anything-cli/src/ai_cmd.rs
new file mode 100644
index 0000000..9e419e8
--- /dev/null
+++ b/desktop/apps/craft-anything-cli/src/ai_cmd.rs
@@ -0,0 +1,386 @@
+//! `chat` and `ai list|detect|add|test`.
+
+use crate::output::Format;
+use crate::{Cli, INTERRUPTED, emit, fail, make_engine, out};
+use clap::{Args, Subcommand};
+use craft_anything_ai::{AgentEvent, AiHub, BackendConfig, BackendKind, Effort};
+use craft_anything_evidence::ToolError;
+use serde_json::{Value, json};
+use std::io::{Read as _, Write as _};
+use std::sync::atomic::Ordering;
+use std::time::Duration;
+
+#[derive(Debug, Args)]
+pub struct ChatArgs {
+ /// The question.
+ pub question: String,
+ /// File, app or folder to open first.
+ #[arg(long)]
+ pub target: Option,
+ /// Backend name (default: the lead backend).
+ #[arg(long)]
+ pub backend: Option,
+ /// Plain-words answers.
+ #[arg(long)]
+ pub beginner: bool,
+}
+
+#[derive(Debug, Subcommand)]
+pub enum AiCommand {
+ /// Complete browser sign-in using the official installed CLI (codex or antigravity).
+ Login { agent: String },
+ /// Configured backends.
+ List,
+ /// Look for keys, local servers, agent CLIs and SSH hosts.
+ Detect,
+ /// Add a backend.
+ Add {
+ name: String,
+ /// anthropic, openai, openrouter, openai_compatible, agent_cli
+ #[arg(long)]
+ kind: String,
+ #[arg(long)]
+ model: Option,
+ #[arg(long)]
+ base_url: Option,
+ #[arg(long)]
+ ssh_host: Option,
+ /// Read the API key from this environment variable (keys are never taken on the command line).
+ #[arg(long)]
+ key_env: Option,
+ /// Read the API key from stdin.
+ #[arg(long)]
+ key_stdin: bool,
+ },
+ /// Remove a backend.
+ Remove { name: String },
+ /// Turn a backend on again.
+ On { name: String },
+ /// Turn a backend off: nothing uses it, and it keeps its settings and key.
+ Off { name: String },
+ /// Set the program an agent app runs, when it's installed where it isn't found (leave the
+ /// path out to find it by its usual name again).
+ Program { name: String, path: Option },
+ /// Set the model an AI uses (Claude Code: the model it's asked for); leave it out to list the
+ /// ones it can use.
+ Model { name: String, model: Option },
+ /// Set how hard a Claude backend thinks (the Claude API or Claude Code): low, medium, high,
+ /// xhigh or max, or `default` for the usual level; leave it out to see the level and choices.
+ Effort { name: String, level: Option },
+ /// Send a tiny request and report latency.
+ Test { name: String },
+ /// Choose the backend that builds (leave the name out to go back to automatic).
+ Builder { name: Option },
+ /// Choose the backend that takes a game's own files out after the study, in Deep mode (an API
+ /// backend such as Kimi K3 through OpenRouter; leave the name out to turn it off).
+ Extractor { name: Option },
+}
+
+fn hub(cli: &Cli) -> Result {
+ Ok(AiHub::new(make_engine(cli.project.as_deref(), cli.deep_for_run())?))
+}
+
+fn write_raw(s: &str) {
+ let mut o = std::io::stdout().lock();
+ let _ = o.write_all(s.as_bytes()).and_then(|_| o.flush());
+}
+
+/// Headless agent run: stream the answer to stdout, tool activity to stderr.
+pub fn chat(cli: &Cli, a: &ChatArgs) -> u8 {
+ let hub = match hub(cli) {
+ Ok(h) => h,
+ Err(e) => return fail(&e, cli.format()),
+ };
+ if let Some(t) = &a.target {
+ let abs = match std::path::absolute(t) {
+ Ok(p) => p,
+ Err(e) => return fail(&ToolError::io(t, e), cli.format()),
+ };
+ if let Err(e) = hub.engine.call("core_target_open", json!({"path": abs}), crate::call_options(cli, &Default::default())) {
+ return fail(&e, cli.format());
+ }
+ }
+ let mut conv = match hub.start(a.backend.as_deref(), a.beginner) {
+ Ok(c) => c,
+ Err(e) => return fail(&e, cli.format()),
+ };
+ conv.send(&a.question);
+ let jsonl = cli.format() == Format::Jsonl;
+ let mut code = 0u8;
+ let mut stopped = false;
+ loop {
+ if INTERRUPTED.load(Ordering::SeqCst) && !stopped {
+ conv.stop();
+ stopped = true;
+ }
+ let events = conv.poll();
+ if events.is_empty() {
+ if stopped && !conv.is_busy() {
+ break;
+ }
+ std::thread::sleep(Duration::from_millis(15));
+ continue;
+ }
+ let mut done = false;
+ for ev in events {
+ if jsonl {
+ out(&serde_json::to_string(&ev).unwrap_or_default());
+ }
+ match &ev {
+ AgentEvent::TextDelta { text } if !jsonl => write_raw(text),
+ AgentEvent::Status { text } if !jsonl => eprintln!("· {text}"),
+ AgentEvent::ToolStarted { tool, .. } if !jsonl => eprintln!("→ {tool}"),
+ AgentEvent::ToolFinished { tool, ok, summary, evidence_id, .. } if !jsonl => {
+ let ev = evidence_id.as_deref().map(|e| format!(" [{e}]")).unwrap_or_default();
+ eprintln!("{} {tool}: {summary}{ev}", if *ok { "✓" } else { "✗" });
+ }
+ AgentEvent::ConsentNeeded { call_id, title, explanation, .. } => {
+ if !jsonl {
+ eprintln!(
+ "! {title} needs your OK: {explanation} ({})",
+ if cli.allow_run { "allowed by --allow-run" } else { "declined; re-run with --allow-run to allow" }
+ );
+ }
+ conv.answer_consent(call_id, cli.allow_run);
+ }
+ AgentEvent::Error { message, remediation, log } => {
+ code = 1;
+ if !jsonl {
+ eprintln!("error: {message}");
+ if let Some(r) = remediation {
+ eprintln!("fix: {r}");
+ }
+ if let Some(l) = log {
+ eprintln!("log: {}", l.display());
+ }
+ }
+ }
+ AgentEvent::TurnDone { backend, input_tokens, output_tokens, cost_usd, cost_is_estimate } => {
+ if !jsonl {
+ let cost = cost_usd
+ .map(|c| if *cost_is_estimate { format!(", about ${c:.4} at API prices (an estimate, not a bill)") } else { format!(", ${c:.4}") })
+ .unwrap_or_default();
+ write_raw("\n");
+ eprintln!("[{backend}: {input_tokens} in / {output_tokens} out{cost}]");
+ }
+ done = true;
+ }
+ _ => {}
+ }
+ }
+ if done {
+ break;
+ }
+ }
+ if stopped { 130 } else { code }
+}
+
+fn read_key(key_env: &Option, key_stdin: bool) -> Result, String> {
+ if let Some(var) = key_env {
+ return std::env::var(var).map(Some).map_err(|_| format!("environment variable {var} is not set"));
+ }
+ if key_stdin {
+ let mut s = String::new();
+ std::io::stdin().take(64 * 1024).read_to_string(&mut s).map_err(|e| format!("reading the key from stdin: {e}"))?;
+ let k = s.trim().to_string();
+ return if k.is_empty() { Err("no key on stdin".into()) } else { Ok(Some(k)) };
+ }
+ Ok(None)
+}
+
+pub fn ai(cli: &Cli, c: &AiCommand, f: Format) -> u8 {
+ let hub = match hub(cli) {
+ Ok(h) => h,
+ Err(e) => return fail(&e, f),
+ };
+ match c {
+ AiCommand::Login { agent } => {
+ let cancel = craft_anything_engine::CancelToken::default();
+ let done = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
+ let watch_done = done.clone();
+ let watch_cancel = cancel.clone();
+ let watcher = std::thread::spawn(move || {
+ while !watch_done.load(Ordering::Relaxed) {
+ if INTERRUPTED.load(Ordering::Relaxed) {
+ watch_cancel.cancel();
+ break;
+ }
+ std::thread::sleep(Duration::from_millis(50));
+ }
+ });
+ let result = craft_anything_ai::connections::login(agent, cancel);
+ done.store(true, Ordering::Relaxed);
+ let _ = watcher.join();
+ match result {
+ Ok(()) => {
+ emit(&json!({"agent": agent, "login_completed": true, "credentials": "owned by official CLI"}), f);
+ 0
+ }
+ Err(e) => fail(&e, f),
+ }
+ }
+ AiCommand::List => {
+ let v: Vec = hub
+ .backends()
+ .into_iter()
+ .map(|b| {
+ let has_key = hub.has_key(&b.name);
+ let mut v = serde_json::to_value(&b).unwrap_or(Value::Null);
+ if let Some(m) = v.as_object_mut() {
+ m.insert("key_stored".into(), json!(has_key));
+ }
+ v
+ })
+ .collect();
+ emit(&Value::Array(v), f);
+ 0
+ }
+ AiCommand::Detect => {
+ emit(&serde_json::to_value(hub.detect()).unwrap_or(Value::Null), f);
+ 0
+ }
+ AiCommand::Add { name, kind, model, base_url, ssh_host, key_env, key_stdin } => {
+ let kind: BackendKind = match serde_json::from_value(json!(kind)) {
+ Ok(k) => k,
+ Err(_) => return crate::usage_error("--kind must be one of anthropic, openai, openrouter, openai_compatible, agent_cli"),
+ };
+ let key = match read_key(key_env, *key_stdin) {
+ Ok(k) => k,
+ Err(m) => return crate::usage_error(&m),
+ };
+ // A server by its address with no model named: asked for its models, and one that can chat is used.
+ if let (BackendKind::OpenaiCompatible, Some(address), None) = (kind, base_url, model) {
+ return match hub.add_server(name, address, None, key) {
+ Ok(model) => {
+ emit(&json!({"added": {"name": name, "kind": kind, "model": model, "base_url": address}}), f);
+ 0
+ }
+ Err(e) => fail(&e, f),
+ };
+ }
+ let model = model.clone().or_else(|| kind.suggested_models().first().map(|m| m.to_string())).unwrap_or_default();
+ let cfg = BackendConfig { name: name.clone(), kind, model, base_url: base_url.clone(), ssh_host: ssh_host.clone(), enabled: true };
+ match hub.add_backend(cfg.clone(), key) {
+ Ok(()) => {
+ emit(&json!({"added": cfg}), f);
+ 0
+ }
+ Err(e) => fail(&e, f),
+ }
+ }
+ AiCommand::Remove { name } => match hub.remove_backend(name) {
+ Ok(removed) => {
+ emit(&json!({"removed": removed, "name": name}), f);
+ if removed { 0 } else { 1 }
+ }
+ Err(e) => fail(&e, f),
+ },
+ AiCommand::On { name } | AiCommand::Off { name } => {
+ let on = matches!(c, AiCommand::On { .. });
+ match hub.set_enabled(name, on) {
+ Ok(found) => {
+ emit(&json!({"name": name, "enabled": on, "found": found}), f);
+ if found { 0 } else { 1 }
+ }
+ Err(e) => fail(&e, f),
+ }
+ }
+ AiCommand::Model { name, model: None } => match hub.model_choices(name) {
+ Ok(models) => {
+ emit(&json!({"name": name, "models": models}), f);
+ 0
+ }
+ Err(e) => fail(&e, f),
+ },
+ AiCommand::Model { name, model: Some(model) } => match hub.set_model(name, model) {
+ Ok(found) => {
+ emit(&json!({"name": name, "model": model.trim(), "found": found}), f);
+ if found { 0 } else { 1 }
+ }
+ Err(e) => fail(&e, f),
+ },
+ AiCommand::Effort { name, level: None } => {
+ let Some(b) = hub.backends().into_iter().find(|b| b.name == *name) else {
+ return fail(&ToolError::invalid(format!("No AI backend named {name:?}")).with_remediation("See `craft-anything-cli ai list`."), f);
+ };
+ let options = hub.options();
+ match craft_anything_ai::effort_in_use(&b, options.get(name)) {
+ Some(effort) => {
+ let levels: Vec<&str> = Effort::ALL.iter().map(|e| e.as_str()).collect();
+ emit(&json!({"name": name, "effort": effort.as_str(), "levels": levels}), f);
+ 0
+ }
+ None => fail(
+ &ToolError::invalid(format!("{name} doesn't take an effort level.")).with_remediation("Effort is for the Claude API and Claude Code."),
+ f,
+ ),
+ }
+ }
+ AiCommand::Effort { name, level: Some(level) } => {
+ let effort = match level.trim() {
+ "default" => None,
+ l => match Effort::parse(l) {
+ Some(e) => Some(e),
+ None => {
+ let e = ToolError::invalid(format!("{l:?} isn't an effort level.")).with_remediation("Use low, medium, high, xhigh, max or default.");
+ return fail(&e, f);
+ }
+ },
+ };
+ match hub.set_effort(name, effort) {
+ Ok(found) => {
+ emit(&json!({"name": name, "effort": effort.map(Effort::as_str), "found": found}), f);
+ if found { 0 } else { 1 }
+ }
+ Err(e) => fail(&e, f),
+ }
+ }
+ AiCommand::Program { name, path } => match hub.set_command(name, path.as_deref()) {
+ Ok(found) => {
+ emit(&json!({"name": name, "program": path, "found": found}), f);
+ if found { 0 } else { 1 }
+ }
+ Err(e) => fail(&e, f),
+ },
+ AiCommand::Builder { name } => match hub.set_role(craft_anything_ai::Role::Builder, name.as_deref()) {
+ Ok(()) => {
+ emit(&json!({"builder": name}), f);
+ 0
+ }
+ Err(e) => fail(&e, f),
+ },
+ AiCommand::Extractor { name } => match hub.set_role(craft_anything_ai::Role::Extractor, name.as_deref()) {
+ Ok(()) => {
+ emit(&json!({"extractor": name}), f);
+ 0
+ }
+ Err(e) => fail(&e, f),
+ },
+ AiCommand::Test { name } => match hub.test_backend(name) {
+ Ok(r) => {
+ emit(&serde_json::to_value(r).unwrap_or(Value::Null), f);
+ 0
+ }
+ Err(e) => fail(&e, f),
+ },
+ }
+}
+
+/// AI reachability checks for `doctor` (bounded; each configured backend once).
+pub fn doctor_checks() -> Vec {
+ use craft_anything_setup::{Check, CheckStatus};
+ let Ok(engine) = make_engine(None, None) else { return Vec::new() };
+ let hub = AiHub::new(engine);
+ let results = hub.check_reachability();
+ if results.is_empty() {
+ return vec![Check::new("ai", CheckStatus::Info, "No AI backend is set up (optional).").fix("Run `craft-anything-cli ai detect`, then `ai add`.")];
+ }
+ results
+ .into_iter()
+ .map(|(name, r)| match r {
+ Ok(ms) => Check::new(format!("ai:{name}"), CheckStatus::Ok, format!("{name} answered in {ms} ms.")),
+ Err(why) => Check::new(format!("ai:{name}"), CheckStatus::Warn, format!("{name} isn't reachable: {why}"))
+ .fix(format!("Run `craft-anything-cli ai test {name}` for details.")),
+ })
+ .collect()
+}
diff --git a/desktop/apps/craft-anything-cli/src/main.rs b/desktop/apps/craft-anything-cli/src/main.rs
new file mode 100644
index 0000000..90fc278
--- /dev/null
+++ b/desktop/apps/craft-anything-cli/src/main.rs
@@ -0,0 +1,527 @@
+//! `craft-anything-cli`: every tool as a subcommand, the MCP server, setup, doctor and headless chat.
+//!
+//! Exit codes: 0 success, 1 the operation failed (a structured error is printed), 2 usage error,
+//! 130 interrupted.
+#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
+
+mod ai_cmd;
+mod output;
+mod rebuild_cmd;
+mod setup_cmd;
+mod tool_args;
+
+use clap::{Parser, Subcommand};
+use craft_anything_engine::project::{Project, Purpose};
+use craft_anything_engine::{CallOptions, Caller, CancelToken, Consent, Engine, Requires, Session, Settings, ToolOutput};
+use craft_anything_evidence::ToolError;
+use output::Format;
+use serde_json::{Value, json};
+use std::path::{Path, PathBuf};
+use std::process::ExitCode;
+use std::sync::Arc;
+use std::sync::atomic::{AtomicBool, Ordering};
+
+pub static INTERRUPTED: AtomicBool = AtomicBool::new(false);
+
+#[derive(Debug, Parser)]
+#[command(name = "craft-anything-cli", version, about = "REI Reverse Engineer on the command line: every tool, the MCP server, setup and doctor.")]
+#[command(after_help = "Every tool is also a subcommand: `craft-anything-cli commands` lists them, `craft-anything-cli --help` shows its options.\n\
+When a tool has its own --format option (core-target-open, ...), --format after the tool name goes to the tool; pick the output with --output-format (or --format before the tool name). `report` has no format option of its own: use `report --html` for HTML.")]
+pub struct Cli {
+ /// Output format. `--output-format` always means this, even after a tool that has its own `--format`.
+ #[arg(long, global = true, value_enum, default_value_t = Format::Json, alias = "output-format")]
+ pub format: Format,
+ /// Shorthand for --format json.
+ #[arg(long, global = true)]
+ pub json: bool,
+ /// A project folder (`*.cany`): evidence, memory and the journal are kept there. Created if missing.
+ #[arg(long, global = true)]
+ pub project: Option,
+ /// Allow tools that run code or change a device without asking.
+ #[arg(long, global = true)]
+ pub allow_run: bool,
+ /// Deep mode for this run only: tools may read a target's code, files and page source.
+ #[arg(long, global = true, conflicts_with = "clean_room")]
+ pub deep: bool,
+ /// Clean room for this run only, whatever Settings say: nothing inside a target is read.
+ #[arg(long, global = true)]
+ pub clean_room: bool,
+ #[command(subcommand)]
+ pub command: Command,
+}
+
+#[derive(Debug, Subcommand)]
+pub enum Command {
+ /// Run the MCP server on stdin/stdout.
+ Mcp {
+ /// List every tool instead of the Essentials plus enabled toolsets.
+ #[arg(long)]
+ all_tools: bool,
+ /// A folder where questions and approvals for the person are posted (`core_person_ask`,
+ /// `core_action_approve`); the REI Reverse Engineer app answers them.
+ #[arg(long)]
+ inbox: Option,
+ },
+ /// Connect REI Reverse Engineer to your coding-agent apps.
+ Setup(setup_cmd::SetupArgs),
+ /// Check the install, registrations, engines, AI backends and the MCP server.
+ Doctor(setup_cmd::DoctorArgs),
+ /// Remove REI Reverse Engineer's registrations and skill from agent apps.
+ Uninstall(setup_cmd::UninstallArgs),
+ /// List every tool.
+ Commands,
+ /// What this install can do: toolsets, tool availability, engines.
+ Capabilities,
+ /// Open a file and run a first look at it.
+ Triage { path: String },
+ /// Write a readable investigation report (Markdown or HTML) from findings and evidence.
+ Report {
+ /// Optional target to open first (its sha256 ties evidence to a file). Omit to report on
+ /// the open project's memory and ledger.
+ path: Option,
+ /// Write a self-contained HTML page instead of Markdown.
+ #[arg(long)]
+ html: bool,
+ /// Write to this file instead of printing to stdout.
+ #[arg(long)]
+ out: Option,
+ /// Leave out the evidence-ledger appendix.
+ #[arg(long)]
+ no_evidence: bool,
+ },
+ /// Ask the AI agent a question about a target (streams to stdout).
+ Chat(ai_cmd::ChatArgs),
+ /// Study a website, app or game and have a coding agent rebuild it in a new project folder.
+ Rebuild(rebuild_cmd::RebuildArgs),
+ /// Manage AI backends.
+ Ai {
+ #[command(subcommand)]
+ command: ai_cmd::AiCommand,
+ },
+ /// For a coding agent's hook, run after each of its tool calls: hand over the notes the person
+ /// added while it works (printed as the hook's JSON; nothing when none wait).
+ #[command(hide = true)]
+ BuilderNotes { dir: PathBuf },
+ /// Run a tool by name: `craft-anything-cli [path] [--key value] [--json '{..}' | @file.json | -]`.
+ #[command(external_subcommand)]
+ Tool(Vec),
+}
+
+impl Cli {
+ pub fn format(&self) -> Format {
+ if self.json { Format::Json } else { self.format }
+ }
+
+ /// The mode `--deep` or `--clean-room` asks for, if either.
+ pub fn deep_for_run(&self) -> Option {
+ (self.deep || self.clean_room).then_some(self.deep)
+ }
+}
+
+fn main() -> ExitCode {
+ init_logging();
+ let _ = ctrlc::set_handler(|| {
+ if INTERRUPTED.swap(true, Ordering::SeqCst) {
+ // second Ctrl-C: leave now
+ std::process::exit(130);
+ }
+ });
+ let cli = match Cli::try_parse() {
+ Ok(c) => c,
+ Err(e) => {
+ let code = if e.use_stderr() { 2 } else { 0 };
+ let _ = e.print();
+ return ExitCode::from(code);
+ }
+ };
+ let code = run(cli);
+ if INTERRUPTED.load(Ordering::SeqCst) { ExitCode::from(130) } else { ExitCode::from(code) }
+}
+
+fn init_logging() {
+ struct Stderr(log::LevelFilter);
+ impl log::Log for Stderr {
+ fn enabled(&self, m: &log::Metadata<'_>) -> bool {
+ m.level() <= self.0
+ }
+ fn log(&self, r: &log::Record<'_>) {
+ if self.enabled(r.metadata()) {
+ eprintln!("[{}] {}: {}", r.level(), r.target(), r.args());
+ }
+ }
+ fn flush(&self) {}
+ }
+ let level = match std::env::var("CRAFT_ANYTHING_LOG").unwrap_or_default().to_ascii_lowercase().as_str() {
+ "trace" => log::LevelFilter::Trace,
+ "debug" => log::LevelFilter::Debug,
+ "info" => log::LevelFilter::Info,
+ "warn" => log::LevelFilter::Warn,
+ "error" => log::LevelFilter::Error,
+ _ => log::LevelFilter::Off,
+ };
+ if level != log::LevelFilter::Off && log::set_logger(Box::leak(Box::new(Stderr(level)))).is_ok() {
+ log::set_max_level(level);
+ }
+}
+
+/// Print a line to stdout; a closed pipe (`| head`) is not an error worth crashing over.
+pub fn out(s: &str) {
+ use std::io::Write as _;
+ let mut o = std::io::stdout().lock();
+ let _ = o.write_all(s.as_bytes()).and_then(|_| o.write_all(b"\n")).and_then(|_| o.flush());
+}
+
+/// Print a value in the chosen format.
+pub fn emit(v: &Value, f: Format) {
+ out(&output::render(v, f));
+}
+
+/// Print an error (to stdout as structured output, so scripts can parse it) and return exit code 1.
+pub fn fail(e: &ToolError, f: Format) -> u8 {
+ emit(&json!({"error": e}), f);
+ 1
+}
+
+pub fn usage_error(msg: &str) -> u8 {
+ usage(msg)
+}
+
+fn usage(msg: &str) -> u8 {
+ eprintln!("error: {msg}");
+ 2
+}
+
+/// The engine with settings loaded, the run's mode, and the project (if any) opened.
+pub fn make_engine(project: Option<&Path>, deep_for_run: Option) -> Result, ToolError> {
+ craft_anything_platform::sync::start_watchdog(craft_anything_engine::settings::config_dir().join("logs"));
+ let mut session = Session::new(Settings::load());
+ session.deep_for_run = deep_for_run;
+ if let Some(dir) = project {
+ session.open_project(open_or_create_project(dir)?)?;
+ }
+ let plugins = craft_anything_engine::plugins::install(&mut session);
+ let mut layer_tools = craft_anything_ai::tools();
+ layer_tools.extend(craft_anything_setup::tools::specs());
+ Ok(Engine::shared_with(session, layer_tools, plugins))
+}
+
+fn open_or_create_project(dir: &Path) -> Result {
+ let dir = std::path::absolute(dir).map_err(|e| ToolError::io(dir.display(), e))?;
+ if dir.join("project.toml").is_file() {
+ return Project::open(&dir);
+ }
+ let name = dir.file_stem().map(|s| s.to_string_lossy().into_owned()).unwrap_or_else(|| "project".into());
+ Project::create(&dir, &name, Purpose::default())
+}
+
+pub fn call_options(cli: &Cli, cancel: &CancelToken) -> CallOptions<'static> {
+ let consent = if cli.allow_run { Consent::Granted } else { Consent::NotAsked };
+ CallOptions::new(Caller::Cli).with_consent(consent).with_cancel(cancel.clone())
+}
+
+/// A cancel token that trips on Ctrl-C.
+pub fn interrupt_token() -> CancelToken {
+ let token = CancelToken::default();
+ let t = token.clone();
+ let _ = std::thread::Builder::new().name("ctrl-c-watch".into()).spawn(move || {
+ while !INTERRUPTED.load(Ordering::SeqCst) {
+ std::thread::sleep(std::time::Duration::from_millis(50));
+ }
+ t.cancel();
+ });
+ token
+}
+
+fn run(cli: Cli) -> u8 {
+ let f = cli.format();
+ match &cli.command {
+ // Runs after every tool call of a builder: no engine, nothing but the hook's JSON on stdout.
+ Command::BuilderNotes { dir } => {
+ use craft_anything_ai::rebuild::notes;
+ if let Some(out) = notes::hook_output(¬es::claim(dir)) {
+ println!("{out}");
+ }
+ 0
+ }
+ Command::Mcp { all_tools, inbox } => {
+ let engine = match make_engine(cli.project.as_deref(), cli.deep_for_run()) {
+ Ok(e) => e,
+ Err(e) => {
+ eprintln!("craft-anything mcp: {}", e.message);
+ return 1;
+ }
+ };
+ if let Some(dir) = inbox {
+ engine.session().ext.insert(Arc::new(craft_anything_engine::inbox::InboxDir(dir.clone())));
+ // A builder can generate game assets with the person's keys (one per asset
+ // provider), read here from the keychain so none appears on a command line or in
+ // a log.
+ let providers = engine.session().ext.get::().map(|p| p.as_ref().clone()).unwrap_or_default();
+ let keys = craft_anything_ai::keys::asset_keys(&craft_anything_ai::KeyStore::system(), &providers);
+ engine.session().ext.insert(Arc::new(keys));
+ }
+ let mut opts = craft_anything_mcp::ServerOptions::from_env();
+ opts.all_tools |= *all_tools;
+ opts.allow_run = cli.allow_run;
+ match craft_anything_mcp::serve_stdio(engine, opts) {
+ Ok(()) => 0,
+ Err(e) => {
+ eprintln!("craft-anything mcp: {e}");
+ 1
+ }
+ }
+ }
+ Command::Setup(a) => setup_cmd::setup(a, f),
+ Command::Doctor(a) => setup_cmd::doctor(a, f),
+ Command::Uninstall(a) => setup_cmd::uninstall(a, f),
+ Command::Commands => commands(f),
+ Command::Capabilities => capabilities(&cli, f),
+ Command::Triage { path } => triage(&cli, path, f),
+ Command::Report { path, html, out, no_evidence } => {
+ report(&cli, path.as_deref(), if *html { "html" } else { "markdown" }, out.as_deref(), *no_evidence, f)
+ }
+ Command::Chat(a) => ai_cmd::chat(&cli, a),
+ Command::Rebuild(a) => rebuild_cmd::rebuild(&cli, a),
+ Command::Ai { command } => ai_cmd::ai(&cli, command, f),
+ Command::Tool(args) => tool(&cli, args, f),
+ }
+}
+
+fn commands(f: Format) -> u8 {
+ // The same engine every command gets: settings, the AI layer's tools and plugins.
+ let engine = match make_engine(None, None) {
+ Ok(e) => e,
+ Err(e) => return fail(&e, f),
+ };
+ if f == Format::Json || f == Format::Jsonl {
+ let v: Vec = engine
+ .registry()
+ .all()
+ .iter()
+ .map(|s| json!({"command": s.cli_name(), "tool": s.id, "toolset": s.toolset.id(), "title": s.title, "summary": s.summary, "needs_target": s.requires == Requires::Target}))
+ .collect();
+ emit(&Value::Array(v), f);
+ } else {
+ for t in engine.registry().toolsets() {
+ let specs: Vec<_> = engine.registry().in_toolset(t).collect();
+ if specs.is_empty() {
+ continue;
+ }
+ out(&format!("{}:", t.title()));
+ for s in specs {
+ out(&format!(" {:<34} {}", s.cli_name(), s.title));
+ }
+ }
+ }
+ 0
+}
+
+fn capabilities(cli: &Cli, f: Format) -> u8 {
+ let engine = match make_engine(cli.project.as_deref(), cli.deep_for_run()) {
+ Ok(e) => e,
+ Err(e) => return fail(&e, f),
+ };
+ let session = engine.session();
+ let toolsets: Vec = engine
+ .registry()
+ .toolsets()
+ .iter()
+ .map(|t| {
+ let tools: Vec = engine
+ .registry()
+ .in_toolset(*t)
+ .map(|s| {
+ let a = engine.availability(s, &session);
+ // Before anything is open a target tool is still offered (needs_target says what
+ // it waits for), but not one the mode refuses (#249).
+ let refused = session.clean_room() && !craft_anything_engine::clean_room::allows(s.id);
+ let waived = s.requires == Requires::Target && !refused;
+ json!({"tool": s.id, "available": a.available || waived, "needs_target": s.requires == Requires::Target, "reason": if waived { None } else { a.reason }})
+ })
+ .collect();
+ json!({"id": t.id(), "title": t.title(), "tools": tools})
+ })
+ .collect();
+ let engines: Vec = craft_anything_engines::ENGINES
+ .iter()
+ .map(|e| {
+ let found = session.settings.engine_path(e.id);
+ json!({"id": e.id, "name": e.name, "needed_for": e.needed_for, "found": found.is_some(), "path": found, "installable_here": e.host_pin().is_some()})
+ })
+ .collect();
+ emit(
+ &json!({
+ "version": env!("CARGO_PKG_VERSION"),
+ "analysis_mode": if session.clean_room() { "clean-room" } else { "deep" },
+ "tools": engine.registry().len(),
+ "toolsets": toolsets,
+ "engines": engines,
+ "host": craft_anything_evidence::host_triple(),
+ }),
+ f,
+ );
+ 0
+}
+
+/// open → op → close for one target. `open` carries extra `core_target_open` params (raw-binary
+/// architecture and entry point, a forced format, a Deep language pin).
+fn with_target(
+ engine: &Engine,
+ cli: &Cli,
+ cancel: &CancelToken,
+ path: &str,
+ open: &serde_json::Map,
+ op: impl FnOnce() -> Result,
+) -> Result {
+ let abs = std::path::absolute(path).map_err(|e| ToolError::io(path, e))?;
+ let mut params = open.clone();
+ params.insert("path".into(), json!(abs));
+ engine.call("core_target_open", Value::Object(params), call_options(cli, cancel))?;
+ let out = op();
+ let _ = engine.call("core_target_close", json!({}), call_options(cli, cancel));
+ out
+}
+
+fn triage(cli: &Cli, path: &str, f: Format) -> u8 {
+ let engine = match make_engine(cli.project.as_deref(), cli.deep_for_run()) {
+ Ok(e) => e,
+ Err(e) => return fail(&e, f),
+ };
+ let cancel = interrupt_token();
+ let tool = ["core_target_triage", "core_overview_show"].into_iter().find(|t| engine.registry().get(t).is_some());
+ let r = with_target(&engine, cli, &cancel, path, &Default::default(), || match tool {
+ Some(t) => engine.call(t, json!({}), call_options(cli, &cancel)),
+ None => {
+ let mut v = engine.call("core_session_show", json!({}), call_options(cli, &cancel))?.to_json();
+ if let Some(m) = v.as_object_mut() {
+ m.insert("note".into(), json!("No triage tool is registered in this build yet; showing what was opened."));
+ }
+ Ok(ToolOutput::Value(v))
+ }
+ });
+ match r {
+ Ok(out) => {
+ emit(&out.to_json(), f);
+ 0
+ }
+ Err(e) => fail(&e, f),
+ }
+}
+
+fn report(cli: &Cli, path: Option<&str>, format: &str, out_file: Option<&Path>, no_evidence: bool, f: Format) -> u8 {
+ let format = format.to_ascii_lowercase();
+ if format != "markdown" && format != "html" {
+ return usage("--format must be markdown or html");
+ }
+ let engine = match make_engine(cli.project.as_deref(), cli.deep_for_run()) {
+ Ok(e) => e,
+ Err(e) => return fail(&e, f),
+ };
+ let cancel = interrupt_token();
+ // The report tool writes the file itself when given an absolute path; otherwise it returns the
+ // text and we print it (raw, so the Markdown/HTML is usable directly).
+ let out_abs = match out_file {
+ Some(p) => match std::path::absolute(p) {
+ Ok(a) => Some(a),
+ Err(e) => return fail(&ToolError::io(p.display(), e), f),
+ },
+ None => None,
+ };
+ let mut params = json!({"format": format, "include_evidence": !no_evidence});
+ if let (Some(a), Some(m)) = (&out_abs, params.as_object_mut()) {
+ m.insert("path".into(), json!(a));
+ }
+ let run = || engine.call("core_report_write", params.clone(), call_options(cli, &cancel));
+ let result = match path {
+ Some(p) => with_target(&engine, cli, &cancel, p, &Default::default(), run),
+ None => run(),
+ };
+ match result {
+ Ok(output) => {
+ let v = output.to_json();
+ match (out_abs, v["result"]["report"].as_str()) {
+ // Printed report: emit the raw text so it can be piped to a file or viewer.
+ (None, Some(text)) => out(text),
+ // Wrote a file (or an unexpected shape): show the structured result.
+ _ => emit(&v, f),
+ }
+ 0
+ }
+ Err(e) => fail(&e, f),
+ }
+}
+
+fn tool(cli: &Cli, args: &[String], f: Format) -> u8 {
+ let Some((name, rest)) = args.split_first() else { return usage("missing command") };
+ // Global flags written after the tool name land here; pick them out.
+ let mut f = f;
+ let (mut allow_run, mut deep, mut clean_room) = (cli.allow_run, cli.deep, cli.clean_room);
+ let mut rest_args = Vec::new();
+ let mut it = rest.iter().peekable();
+ // `--format` and `--project` are left to tool_args when the tool has an option of that name
+ // (`project-show --project `); otherwise they're the global flags.
+ while let Some(a) = it.next() {
+ match a.as_str() {
+ "--allow-run" => allow_run = true,
+ "--deep" => deep = true,
+ "--clean-room" => clean_room = true,
+ "--json" if it.peek().is_none_or(|n| !(n.starts_with('{') || n.starts_with('@') || n.as_str() == "-")) => f = Format::Json,
+ _ => rest_args.push(a.clone()),
+ }
+ }
+ // The engine first (tools from higher layers are only known through it), with the project
+ // given before the tool name.
+ if deep && clean_room {
+ return usage("--deep and --clean-room can't be used together");
+ }
+ let mut cli = Cli { format: f, json: false, project: cli.project.clone(), allow_run, deep, clean_room, command: Command::Commands };
+ let mut engine = match make_engine(cli.project.as_deref(), cli.deep_for_run()) {
+ Ok(e) => e,
+ Err(e) => return fail(&e, f),
+ };
+ let Some(spec) = engine.registry().get(name).copied() else {
+ return usage(&format!("unknown command {name:?}. Run `craft-anything-cli commands` to list tools, or --help for built-in commands."));
+ };
+ let schema = engine.registry().input_schema(&spec);
+ let tool_has_project = schema.pointer("/properties/project").is_some();
+ if !tool_has_project && let Some(i) = rest_args.iter().position(|a| a == "--project") {
+ let Some(p) = rest_args.get(i + 1).cloned() else { return usage("--project needs a folder") };
+ rest_args.drain(i..i + 2);
+ cli.project = Some(PathBuf::from(p));
+ engine = match make_engine(cli.project.as_deref(), cli.deep_for_run()) {
+ Ok(e) => e,
+ Err(e) => return fail(&e, f),
+ };
+ }
+ let cli = &cli;
+ let rest = rest_args.as_slice();
+ let inv = match tool_args::parse(&spec, &schema, rest) {
+ Ok(i) => i,
+ Err(m) => return usage(&m),
+ };
+ if let Some(v) = &inv.output_format {
+ match ::from_str(v, true) {
+ Ok(v) => f = v,
+ Err(_) => return usage("--output-format needs one of json, jsonl, md, text"),
+ }
+ }
+ if inv.help {
+ out(&tool_args::help(&spec, &schema));
+ return 0;
+ }
+ let cancel = interrupt_token();
+ let result = match (&inv.open_path, spec.requires) {
+ (Some(path), _) => with_target(&engine, cli, &cancel, path, &inv.open_params, || engine.call(spec.id, inv.params.clone(), call_options(cli, &cancel))),
+ (None, Requires::Target) => {
+ return usage(&format!("{} needs a target: craft-anything-cli {} …", spec.cli_name(), spec.cli_name()));
+ }
+ (None, _) => engine.call(spec.id, inv.params.clone(), call_options(cli, &cancel)),
+ };
+ match result {
+ Ok(out) => {
+ emit(&out.to_json(), f);
+ 0
+ }
+ Err(e) => fail(&e, f),
+ }
+}
diff --git a/desktop/apps/craft-anything-cli/src/output.rs b/desktop/apps/craft-anything-cli/src/output.rs
new file mode 100644
index 0000000..3974725
--- /dev/null
+++ b/desktop/apps/craft-anything-cli/src/output.rs
@@ -0,0 +1,236 @@
+//! Output formats: json (pretty on a TTY), jsonl, md, text, csv and tsv (a result's rows, for a spreadsheet).
+
+use clap::ValueEnum;
+use serde_json::{Map, Value};
+use std::io::IsTerminal as _;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
+pub enum Format {
+ #[default]
+ Json,
+ Jsonl,
+ Md,
+ Text,
+ Csv,
+ Tsv,
+}
+
+pub fn render(v: &Value, f: Format) -> String {
+ match f {
+ Format::Json => {
+ if std::io::stdout().is_terminal() {
+ serde_json::to_string_pretty(v).unwrap_or_default()
+ } else {
+ serde_json::to_string(v).unwrap_or_default()
+ }
+ }
+ Format::Jsonl => match v {
+ Value::Array(items) => items.iter().map(|i| serde_json::to_string(i).unwrap_or_default()).collect::>().join("\n"),
+ _ => serde_json::to_string(v).unwrap_or_default(),
+ },
+ Format::Md => markdown(v),
+ Format::Text => text(v, 0),
+ Format::Csv => table(v, ','),
+ Format::Tsv => table(v, '\t'),
+ }
+}
+
+/// The rows a table shows: an array of objects itself, or the largest one in a result (evidence's
+/// `result`, where a tool's list sits); otherwise the object's own fields as one row.
+fn rows(v: &Value) -> Vec> {
+ // A tool's plain value comes wrapped as `{"result": …}`, like evidence's result.
+ let wrapped = v.as_object().is_some_and(|m| m.len() == 1 && m.contains_key("result"));
+ let body = if is_evidence(v) || wrapped { &v["result"] } else { v };
+ let objects = |a: &[Value]| a.iter().filter_map(|i| i.as_object().cloned()).collect::>();
+ let found = match body {
+ Value::Array(a) => objects(a),
+ Value::Object(m) => m.values().filter_map(Value::as_array).map(|a| objects(a)).max_by_key(Vec::len).unwrap_or_default(),
+ _ => Vec::new(),
+ };
+ if !found.is_empty() {
+ return found;
+ }
+ match body {
+ Value::Object(m) => vec![m.clone()],
+ _ => Vec::new(),
+ }
+}
+
+fn table(v: &Value, sep: char) -> String {
+ if let Some(e) = v.get("error") {
+ return text(&serde_json::json!({"error": e}), 0);
+ }
+ let rows = rows(v);
+ let mut columns: Vec<&String> = Vec::new();
+ for key in rows.iter().flat_map(|r| r.keys()) {
+ if !columns.contains(&key) {
+ columns.push(key);
+ }
+ }
+ let line = |cells: Vec| cells.join(&sep.to_string());
+ let mut out = vec![line(columns.iter().map(|c| cell(&Value::String((*c).clone()), sep)).collect())];
+ for r in &rows {
+ out.push(line(columns.iter().map(|c| r.get(*c).map_or_else(String::new, |x| cell(x, sep))).collect()));
+ }
+ out.join("\n")
+}
+
+/// One table cell. What's in a result comes from the file being studied, so text a spreadsheet
+/// would run as a formula is kept as text.
+fn cell(v: &Value, sep: char) -> String {
+ let raw = match v {
+ Value::Null => String::new(),
+ Value::String(s) => s.clone(),
+ other => other.to_string(),
+ };
+ let safe = if matches!(v, Value::String(_)) && raw.starts_with(['=', '+', '-', '@', '\t', '\r']) { format!("'{raw}") } else { raw };
+ if sep == '\t' {
+ return safe.replace(['\t', '\n', '\r'], " ");
+ }
+ if safe.contains([sep, '"', '\n', '\r']) { format!("\"{}\"", safe.replace('"', "\"\"")) } else { safe }
+}
+
+fn is_evidence(v: &Value) -> bool {
+ v.get("id").and_then(Value::as_str).is_some_and(|i| i.starts_with("ev_")) && v.get("operation").is_some()
+}
+
+fn markdown(v: &Value) -> String {
+ if let Some(e) = v.get("error") {
+ let mut s = format!("**Error ({})**: {}\n", e["code"].as_str().unwrap_or("error"), e["message"].as_str().unwrap_or_default());
+ if let Some(r) = e.get("remediation").and_then(Value::as_str) {
+ s.push_str(&format!("\n**What to do:** {r}\n"));
+ }
+ return s;
+ }
+ if is_evidence(v) {
+ let mut s = format!("## {}\n\n", v["operation"].as_str().unwrap_or_default());
+ if let Some(sum) = v.get("summary").and_then(Value::as_str).filter(|s| !s.is_empty()) {
+ s.push_str(&format!("{sum}\n\n"));
+ }
+ s.push_str(&format!("- Evidence: `{}`\n- Confidence: {}\n", v["id"].as_str().unwrap_or_default(), v["confidence"].as_str().unwrap_or_default()));
+ if let Some(ls) = v.get("limitations").and_then(Value::as_array).filter(|l| !l.is_empty()) {
+ s.push_str("- Limitations:\n");
+ for l in ls {
+ s.push_str(&format!(" - {}\n", l["message"].as_str().unwrap_or_default()));
+ }
+ }
+ s.push_str(&format!("\n```json\n{}\n```\n", serde_json::to_string_pretty(&v["result"]).unwrap_or_default()));
+ return s;
+ }
+ format!("```json\n{}\n```\n", serde_json::to_string_pretty(v).unwrap_or_default())
+}
+
+fn text(v: &Value, depth: usize) -> String {
+ if depth == 0 {
+ if let Some(e) = v.get("error") {
+ let mut s = format!("error: {}", e["message"].as_str().unwrap_or_default());
+ if let Some(r) = e.get("remediation").and_then(Value::as_str) {
+ s.push_str(&format!("\nfix: {r}"));
+ }
+ return s;
+ }
+ if is_evidence(v) {
+ let sum = v.get("summary").and_then(Value::as_str).unwrap_or_default();
+ return format!("{sum}\n[{}; {}]\n{}", v["id"].as_str().unwrap_or_default(), v["confidence"].as_str().unwrap_or_default(), text(&v["result"], 1));
+ }
+ }
+ let pad = " ".repeat(depth.saturating_sub(1));
+ match v {
+ Value::Object(m) => m
+ .iter()
+ .map(|(k, val)| match val {
+ Value::Object(_) | Value::Array(_) => format!("{pad}{k}:\n{}", text(val, depth + 1)),
+ _ => format!("{pad}{k}: {}", scalar(val)),
+ })
+ .collect::>()
+ .join("\n"),
+ Value::Array(a) => a
+ .iter()
+ .map(|val| match val {
+ Value::Object(_) | Value::Array(_) => format!("{pad}-\n{}", text(val, depth + 1)),
+ _ => format!("{pad}- {}", scalar(val)),
+ })
+ .collect::>()
+ .join("\n"),
+ _ => format!("{pad}{}", scalar(v)),
+ }
+}
+
+fn scalar(v: &Value) -> String {
+ match v {
+ Value::String(s) => s.clone(),
+ other => other.to_string(),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use serde_json::json;
+
+ #[test]
+ fn formats() {
+ let v = json!({"a": 1, "b": {"c": [1, "x"]}});
+ assert_eq!(render(&v, Format::Jsonl), r#"{"a":1,"b":{"c":[1,"x"]}}"#);
+ let t = render(&v, Format::Text);
+ assert!(t.contains("a: 1") && t.contains("- x"), "{t}");
+ assert!(render(&v, Format::Md).starts_with("```json"));
+ let e = json!({"error": {"code": "bad_input", "message": "bad", "remediation": "fix it"}});
+ assert!(render(&e, Format::Text).contains("fix: fix it"));
+ assert!(render(&e, Format::Md).contains("What to do"));
+ assert_eq!(render(&json!([1, 2]), Format::Jsonl), "1\n2");
+ }
+
+ fn evidence(result: Value) -> Value {
+ json!({"id": "ev_1", "operation": "op", "summary": "s", "confidence": "observed", "result": result})
+ }
+
+ #[test]
+ fn tables_show_the_largest_list_of_rows() {
+ let v = evidence(json!({"total": 2, "notes": [{"a": 1}], "entries": [{"path": "a,b", "size": 1}, {"path": "c\"d", "kind": "x"}]}));
+ assert_eq!(render(&v, Format::Csv), "path,size,kind\n\"a,b\",1,\n\"c\"\"d\",,x");
+ assert_eq!(render(&v, Format::Tsv), "path\tsize\tkind\na,b\t1\t\nc\"d\t\tx");
+ }
+
+ #[test]
+ fn a_bare_array_and_a_plain_object_are_tables_too() {
+ assert_eq!(render(&json!([{"a": 1}, {"a": 2}]), Format::Csv), "a\n1\n2");
+ assert_eq!(render(&json!({"a": 1, "b": "x"}), Format::Csv), "a,b\n1,x");
+ assert_eq!(render(&json!([1, 2]), Format::Csv), "");
+ }
+
+ #[test]
+ fn a_plain_tools_wrapped_value_is_unwrapped() {
+ assert_eq!(render(&json!({"result": {"count": 0, "projects": [{"id": "a"}]}}), Format::Csv), "id\na");
+ assert_eq!(render(&json!({"result": {"count": 0}}), Format::Csv), "count\n0");
+ }
+
+ #[test]
+ fn nested_values_stay_json_in_one_cell() {
+ assert_eq!(render(&json!([{"a": {"b": [1, 2]}}]), Format::Csv), "a\n\"{\"\"b\"\":[1,2]}\"");
+ }
+
+ #[test]
+ fn text_from_a_hostile_file_cannot_become_a_spreadsheet_formula() {
+ for evil in ["=cmd|' /C calc'!A0", "+1+1", "-2", "@SUM(A1)", "\t=1"] {
+ let csv = render(&json!([{"name": evil}]), Format::Csv);
+ let cell = csv.lines().nth(1).unwrap().trim_start_matches('"');
+ assert!(cell.starts_with('\''), "{csv}");
+ let tsv = render(&json!([{"name": evil}]), Format::Tsv);
+ assert!(tsv.lines().nth(1).unwrap().starts_with('\''), "{tsv}");
+ }
+ assert_eq!(render(&json!([{"n": -2}]), Format::Csv), "n\n-2", "real numbers stay numbers");
+ }
+
+ #[test]
+ fn line_breaks_and_tabs_cannot_add_rows_or_columns() {
+ assert_eq!(render(&json!([{"a": "x\ny"}]), Format::Csv), "a\n\"x\ny\"");
+ assert_eq!(render(&json!([{"a": "x\n\ty"}]), Format::Tsv), "a\nx y");
+ }
+
+ #[test]
+ fn errors_stay_readable_in_table_formats() {
+ let e = json!({"error": {"code": "x", "message": "boom"}});
+ assert!(render(&e, Format::Csv).contains("boom"));
+ }
+}
diff --git a/desktop/apps/craft-anything-cli/src/rebuild_cmd.rs b/desktop/apps/craft-anything-cli/src/rebuild_cmd.rs
new file mode 100644
index 0000000..8375176
--- /dev/null
+++ b/desktop/apps/craft-anything-cli/src/rebuild_cmd.rs
@@ -0,0 +1,260 @@
+//! `rebuild`: the whole study → build → check workflow, headless. Questions and approvals are
+//! answered on the terminal, or automatically with `--unattended` (first choice; approvals are
+//! refused, so nothing outside the project happens without a person).
+
+use crate::{Cli, INTERRUPTED, make_engine};
+use clap::Args;
+use craft_anything_ai::rebuild::{self, RebuildRun, RunEvent, RunOptions};
+use craft_anything_ai::{AgentEvent, AiHub, BackendKind};
+use craft_anything_engine::inbox::{Answer, Request, RequestKind};
+use craft_anything_engine::workspace::Workspace;
+use craft_anything_evidence::ToolError;
+use std::io::{BufRead as _, Write as _};
+use std::path::PathBuf;
+use std::sync::atomic::Ordering;
+use std::time::Duration;
+
+#[derive(Debug, Args)]
+pub struct RebuildArgs {
+ /// A web address, an absolute path to an app, game or file, or `-` for none.
+ pub target: String,
+ /// What to make, in your words.
+ pub request: String,
+ /// Where the new project folder goes (default: the projects folder from settings).
+ #[arg(long, conflicts_with = "project_folder")]
+ pub projects_dir: Option,
+ /// Start the new project in this folder itself (it's added to the project list). If it
+ /// already has files, --existing says what they are.
+ #[arg(long, conflicts_with = "continue_in")]
+ pub project_folder: Option,
+ /// With --project-folder that has files: `edit` (they're the app, edited in place) or
+ /// `start-from` (material to start from; the app is built in app/ beside them).
+ #[arg(long, requires = "project_folder", value_parser = ["edit", "start-from"])]
+ pub existing: Option,
+ /// Continue an existing project folder; the request becomes a follow-up message.
+ #[arg(long)]
+ pub continue_in: Option,
+ /// With --continue-in: carry on where the project's last run stopped (paused, or the app or
+ /// the computer stopped); the request is ignored.
+ #[arg(long, requires = "continue_in")]
+ pub resume: bool,
+ /// The coding agent to use (a backend name from `ai list`).
+ #[arg(long)]
+ pub backend: Option,
+ /// Refinement rounds after the first build.
+ #[arg(long, default_value_t = 2)]
+ pub rounds: u32,
+ /// Answer questions with the first choice and refuse approvals, without asking.
+ #[arg(long)]
+ pub unattended: bool,
+ /// What to build with: an engine (unity, unreal, godot, rust) or a stack's full name.
+ /// New projects only; otherwise it's chosen from the request and target.
+ #[arg(long)]
+ pub build_with: Option,
+ /// Let the builder generate pictures and 3D models for a new game (needs a fal.ai key; paid
+ /// within the project's asset budget). Change it later with `project-assets-allow`.
+ #[arg(long)]
+ pub generate_assets: bool,
+}
+
+pub fn rebuild(cli: &Cli, a: &RebuildArgs) -> u8 {
+ match run(cli, a) {
+ Ok(true) => 0,
+ Ok(false) => 1,
+ Err(e) => {
+ eprintln!("rebuild: {}", e.message);
+ if let Some(r) = e.remediation {
+ eprintln!(" {r}");
+ }
+ 1
+ }
+ }
+}
+
+/// Use the configured agent app, or add the first one found on this computer.
+fn ensure_agent(hub: &AiHub) -> Result<(), ToolError> {
+ if hub.backends().iter().any(|b| b.kind == BackendKind::AgentCli && b.enabled) {
+ return Ok(());
+ }
+ if let Some(d) = hub.detect().into_iter().find(|d| d.suggestion.kind == BackendKind::AgentCli) {
+ eprintln!("Using {} ({}).", d.suggestion.name, d.reason);
+ hub.add_backend(d.suggestion, None)?;
+ }
+ Ok(())
+}
+
+fn run(cli: &Cli, a: &RebuildArgs) -> Result {
+ let engine = make_engine(None, cli.deep_for_run())?;
+ let hub = AiHub::new(engine.clone());
+ ensure_agent(&hub)?;
+ if a.build_with.is_some() && a.continue_in.is_some() {
+ return Err(ToolError::invalid("--build-with is for new projects; a project keeps the stack it was built with.")
+ .with_remediation("Leave out --build-with, or start a new project."));
+ }
+ if a.generate_assets && a.continue_in.is_some() {
+ return Err(ToolError::invalid("--generate-assets is for new projects.").with_remediation("Use project-assets-allow for an existing project."));
+ }
+ let (ws, message) = match &a.continue_in {
+ Some(dir) => (Workspace::open(&std::path::absolute(dir).map_err(|e| ToolError::io(dir.display(), e))?)?, (!a.resume).then(|| a.request.clone())),
+ None => {
+ use craft_anything_ai::rebuild::project::Place;
+ use craft_anything_engine::workspace::ExistingFiles;
+ let settings = engine.session().settings.clone();
+ let existing = a.existing.as_deref().map(|e| if e == "edit" { ExistingFiles::Edit } else { ExistingFiles::StartFrom });
+ let folder = a.project_folder.as_deref().map(std::path::absolute).transpose().map_err(|e| ToolError::io("--project-folder", e))?;
+ let place = match (&folder, &a.projects_dir) {
+ (Some(f), _) => Place::At(f, existing),
+ (None, Some(d)) => Place::In(d),
+ (None, None) => Place::Projects,
+ };
+ let ws = rebuild::project::new_project(&settings, &a.target, &a.request, place, a.build_with.as_deref(), a.generate_assets, a.backend.as_deref())?;
+ if folder.is_some() {
+ engine.session().remember_project(&ws.root)?;
+ }
+ (ws, None)
+ }
+ };
+ println!("Project: {}", ws.root.display());
+ // A follow-up shows in the project's feed as the person typed it.
+ let shown = message.clone();
+ let good_enough = engine.session().settings.build.good_enough;
+ let opts = RunOptions {
+ backend: a.backend.clone(),
+ message,
+ shown,
+ continuing: a.resume,
+ max_rounds: a.rounds,
+ good_enough,
+ beginner: !cli.json,
+ ..RunOptions::default()
+ };
+ let mut run = RebuildRun::start(&hub, ws, opts)?;
+ let mut ok = false;
+ let mut line_open = false;
+ loop {
+ if INTERRUPTED.load(Ordering::SeqCst) {
+ run.stop();
+ }
+ for e in run.poll() {
+ match e {
+ RunEvent::Step { step, text } => {
+ end_line(&mut line_open);
+ println!("[{}] {text}", step.label());
+ }
+ RunEvent::Agent(AgentEvent::TextDelta { text }) => {
+ print!("{text}");
+ line_open = !text.ends_with('\n');
+ }
+ RunEvent::Agent(AgentEvent::ToolStarted { tool, input, .. }) => {
+ end_line(&mut line_open);
+ println!(" · {}", rebuild::activity::describe(&tool, &input, &[]).words());
+ }
+ RunEvent::Agent(AgentEvent::Error { message, remediation, log }) => {
+ end_line(&mut line_open);
+ println!(" ! {message}");
+ if let Some(r) = remediation {
+ println!("{r}");
+ }
+ if let Some(l) = log {
+ println!(" log: {}", l.display());
+ }
+ }
+ RunEvent::Agent(_) => {}
+ RunEvent::Note(text) => {
+ end_line(&mut line_open);
+ println!(" + {text}");
+ }
+ RunEvent::Ask(r) => {
+ end_line(&mut line_open);
+ let answer = if a.unattended { auto_answer(&r) } else { prompt_answer(&r) };
+ run.answer(&r.id, &answer)?;
+ }
+ RunEvent::Checked(c) => {
+ end_line(&mut line_open);
+ match c.similarity {
+ Some(s) => println!("[Check {}] {s:.0}% alike — {}", c.round, c.note),
+ None => println!("[Check {}] {}", c.round, c.note),
+ }
+ }
+ RunEvent::Preview(url) => println!("Preview: {url}"),
+ RunEvent::Finished { ok: o, message, paused } => {
+ end_line(&mut line_open);
+ println!(
+ "{}",
+ if o {
+ format!("Done. {message}")
+ } else if paused {
+ message
+ } else {
+ format!("Stopped: {message}")
+ }
+ );
+ ok = o;
+ }
+ }
+ }
+ if !run.is_running() && run.poll().is_empty() {
+ break;
+ }
+ std::thread::sleep(Duration::from_millis(100));
+ }
+ let _ = std::io::stdout().flush();
+ Ok(ok)
+}
+
+fn end_line(open: &mut bool) {
+ if *open {
+ println!();
+ *open = false;
+ }
+}
+
+fn auto_answer(r: &Request) -> Answer {
+ match r.kind {
+ RequestKind::Question => {
+ println!("? {} → {} (unattended)", r.title, r.choices.first().map(String::as_str).unwrap_or("no preference"));
+ Answer {
+ choice: r.choices.first().cloned(),
+ text: r.choices.is_empty().then(|| "No preference; use your best judgement.".to_string()),
+ ..Default::default()
+ }
+ }
+ RequestKind::Approval => {
+ println!("? {} → refused (unattended)", r.title);
+ Answer { allow: Some(false), text: Some("Unattended run: actions outside the project aren't allowed.".into()), ..Default::default() }
+ }
+ }
+}
+
+fn read_line() -> String {
+ let mut s = String::new();
+ let _ = std::io::stdin().lock().read_line(&mut s);
+ s.trim().to_string()
+}
+
+fn prompt_answer(r: &Request) -> Answer {
+ println!("\n? {}", r.title);
+ if !r.detail.is_empty() {
+ println!(" {}", r.detail.lines().take(12).collect::>().join("\n "));
+ }
+ match r.kind {
+ RequestKind::Question => {
+ for (i, c) in r.choices.iter().enumerate() {
+ println!(" {}) {c}", i + 1);
+ }
+ print!("Your answer (number or text): ");
+ let _ = std::io::stdout().flush();
+ let line = read_line();
+ match line.parse::().ok().and_then(|n| n.checked_sub(1)).and_then(|i| r.choices.get(i)) {
+ Some(c) => Answer { choice: Some(c.clone()), ..Default::default() },
+ None => Answer { text: Some(line), ..Default::default() },
+ }
+ }
+ RequestKind::Approval => {
+ print!("Allow? [y]es / [a]lways for this project / [n]o: ");
+ let _ = std::io::stdout().flush();
+ let line = read_line().to_lowercase();
+ Answer { allow: Some(line.starts_with('y') || line.starts_with('a')), always: line.starts_with('a'), ..Default::default() }
+ }
+ }
+}
diff --git a/desktop/apps/craft-anything-cli/src/setup_cmd.rs b/desktop/apps/craft-anything-cli/src/setup_cmd.rs
new file mode 100644
index 0000000..3d66130
--- /dev/null
+++ b/desktop/apps/craft-anything-cli/src/setup_cmd.rs
@@ -0,0 +1,185 @@
+//! `setup`, `doctor`, `uninstall`.
+
+use crate::output::Format;
+use crate::{emit, fail, out};
+use clap::Args;
+use craft_anything_setup::{ActionKind, CheckStatus, DoctorRequest, Host, PlanRequest};
+use serde_json::{Value, json};
+use std::io::IsTerminal as _;
+
+#[derive(Debug, Args)]
+pub struct SetupArgs {
+ /// Show what would change without writing anything.
+ #[arg(long)]
+ pub dry_run: bool,
+ /// Client to configure (repeatable): claude_code, codex, cursor, …
+ #[arg(long = "client")]
+ pub clients: Vec,
+ /// Configure every detected client.
+ #[arg(long)]
+ pub all_detected: bool,
+ /// Apply without asking.
+ #[arg(long, short = 'y')]
+ pub yes: bool,
+ /// Don't install the skill.
+ #[arg(long)]
+ pub no_skill: bool,
+}
+
+#[derive(Debug, Args)]
+pub struct DoctorArgs {
+ /// Only check these clients (repeatable).
+ #[arg(long = "client")]
+ pub clients: Vec,
+}
+
+#[derive(Debug, Args)]
+pub struct UninstallArgs {}
+
+fn human(f: Format) -> bool {
+ matches!(f, Format::Text | Format::Md)
+}
+
+fn confirm(question: &str) -> bool {
+ if !(std::io::stdin().is_terminal() && std::io::stderr().is_terminal()) {
+ return false;
+ }
+ eprint!("{question} [y/N] ");
+ let mut line = String::new();
+ std::io::stdin().read_line(&mut line).is_ok() && matches!(line.trim(), "y" | "Y" | "yes")
+}
+
+fn print_plan(plan: &craft_anything_setup::Plan) {
+ out(&format!("Register: {}", plan.command.join(" ")));
+ for a in &plan.actions {
+ let v = serde_json::to_value(a).unwrap_or(Value::Null);
+ out(&format!("[{}] {} — {}", v["action"].as_str().unwrap_or_default(), a.display, v["path"].as_str().unwrap_or_default()));
+ if let Some(r) = v["reason"].as_str() {
+ out(&format!(" {r}"));
+ }
+ if let Some(r) = v["remediation"].as_str() {
+ out(&format!(" → {r}"));
+ }
+ if matches!(a.action, ActionKind::Create | ActionKind::Update) {
+ for l in v["preview"].as_str().unwrap_or_default().lines() {
+ out(&format!(" {l}"));
+ }
+ }
+ }
+ for s in &plan.skills {
+ let v = serde_json::to_value(s).unwrap_or(Value::Null);
+ out(&format!("[skill] {}", v["dir"].as_str().unwrap_or_default()));
+ }
+ for n in &plan.notes {
+ out(&format!("note: {n}"));
+ }
+}
+
+pub fn setup(a: &SetupArgs, f: Format) -> u8 {
+ let host = Host::current();
+ let command = match craft_anything_setup::default_command() {
+ Ok(c) => c,
+ Err(e) => return fail(&e, f),
+ };
+ let req = PlanRequest { clients: a.clients.clone(), all_detected: a.all_detected, command, include_skill: !a.no_skill };
+ let plan = match craft_anything_setup::plan(&host, &req) {
+ Ok(p) => p,
+ Err(e) => return fail(&e, f),
+ };
+ let status = if plan.blocked {
+ "blocked"
+ } else if a.dry_run {
+ "planned"
+ } else if plan.change_count() == 0 {
+ "unchanged"
+ } else {
+ ""
+ };
+ if !status.is_empty() {
+ if human(f) {
+ print_plan(&plan);
+ out(&format!("status: {status}"));
+ } else {
+ emit(&json!({"status": status, "plan": plan}), f);
+ }
+ return if plan.blocked { 1 } else { 0 };
+ }
+ let approved = a.yes || {
+ if human(f) {
+ print_plan(&plan);
+ }
+ confirm(&format!("Apply these {} changes?", plan.change_count()))
+ };
+ if !approved {
+ if human(f) {
+ out("status: needs_confirmation (re-run with --yes to apply)");
+ } else {
+ emit(&json!({"status": "needs_confirmation", "plan": plan}), f);
+ }
+ return 1;
+ }
+ let report = craft_anything_setup::apply(&plan);
+ if human(f) {
+ for r in &report.results {
+ let v = serde_json::to_value(r).unwrap_or(Value::Null);
+ out(&format!(
+ "[{}] {} {}",
+ v["outcome"].as_str().unwrap_or_default(),
+ v["client"].as_str().unwrap_or_default(),
+ v["error"].as_str().unwrap_or_default()
+ ));
+ }
+ out(&format!("status: {}", report.status));
+ if !report.restart.is_empty() {
+ out(&format!("Restart {} to load REI Reverse Engineer.", report.restart.join(", ")));
+ }
+ } else {
+ emit(&serde_json::to_value(&report).unwrap_or(Value::Null), f);
+ }
+ if matches!(report.status, "configured" | "unchanged") { 0 } else { 1 }
+}
+
+pub fn doctor(a: &DoctorArgs, f: Format) -> u8 {
+ let host = Host::current();
+ let req = DoctorRequest { clients: a.clients.clone(), extra: crate::ai_cmd::doctor_checks(), ..DoctorRequest::default() };
+ let report = craft_anything_setup::doctor(&host, &req);
+ if human(f) {
+ for c in &report.checks {
+ let st = match c.status {
+ CheckStatus::Ok => "ok",
+ CheckStatus::Warn => "warn",
+ CheckStatus::Fail => "FAIL",
+ CheckStatus::Info => "info",
+ };
+ out(&format!("[{st}] {}: {}", c.name, c.message));
+ if let Some(r) = &c.remediation {
+ out(&format!(" → {r}"));
+ }
+ }
+ out(if report.healthy { "healthy" } else { "problems found" });
+ } else {
+ emit(&serde_json::to_value(&report).unwrap_or(Value::Null), f);
+ }
+ if report.healthy { 0 } else { 1 }
+}
+
+pub fn uninstall(_a: &UninstallArgs, f: Format) -> u8 {
+ let report = craft_anything_setup::uninstall(&Host::current());
+ let v = serde_json::to_value(&report).unwrap_or(Value::Null);
+ if human(f) {
+ for r in &report.removed_entries {
+ let e = serde_json::to_value(r).unwrap_or(Value::Null);
+ out(&format!("removed {} ({})", e["client"].as_str().unwrap_or_default(), e["path"].as_str().unwrap_or_default()));
+ }
+ for p in &report.removed_skill_files {
+ out(&format!("removed {}", p.display()));
+ }
+ for b in &report.blocking {
+ out(&format!("blocked by {} — fix or move that file first", b.display));
+ }
+ out(&format!("status: {}", report.status));
+ } else {
+ emit(&v, f);
+ }
+ if matches!(report.status, "removed" | "nothing_to_remove") { 0 } else { 1 }
+}
diff --git a/desktop/apps/craft-anything-cli/src/tool_args.rs b/desktop/apps/craft-anything-cli/src/tool_args.rs
new file mode 100644
index 0000000..20cf730
--- /dev/null
+++ b/desktop/apps/craft-anything-cli/src/tool_args.rs
@@ -0,0 +1,408 @@
+//! Turn `craft-anything-cli [path] [--json '{..}' | @file.json] [--key value…]` into tool params,
+//! using the tool's JSON schema to type each flag.
+
+use craft_anything_engine::{Requires, ToolSpec};
+use serde_json::{Map, Value, json};
+
+/// A parsed tool invocation.
+#[derive(Debug, Default, PartialEq)]
+pub struct ToolInvocation {
+ pub params: Value,
+ /// Path to open first (for tools that need an open target).
+ pub open_path: Option,
+ /// Extra `core_target_open` params for that path (`format`, `architecture`, `entry_point`, `language_id`).
+ pub open_params: Map,
+ /// The output format asked for after the tool name (`--output-format`, or `--format` when the
+ /// tool has no `format` option of its own).
+ pub output_format: Option,
+ pub help: bool,
+}
+
+/// `core_target_open` options a target tool accepts for its `` (raw binaries, Deep pins).
+const OPEN_OPTIONS: [&str; 3] = ["architecture", "entry_point", "language_id"];
+/// `--format` values that pick how the target is opened rather than how output is printed.
+const OPEN_FORMATS: [&str; 2] = ["raw-binary", "dos-com"];
+
+fn schema_props(schema: &Value) -> Map {
+ schema.get("properties").and_then(Value::as_object).cloned().unwrap_or_default()
+}
+
+fn required(schema: &Value) -> Vec {
+ schema.get("required").and_then(Value::as_array).map(|a| a.iter().filter_map(|v| v.as_str().map(str::to_string)).collect()).unwrap_or_default()
+}
+
+/// The JSON type of a property ("string", "integer", "number", "boolean", "array", "object"), skipping "null".
+fn prop_type(schema: &Value, prop: &Value) -> String {
+ let resolved = resolve_ref(schema, prop);
+ match resolved.get("type") {
+ Some(Value::String(s)) => s.clone(),
+ Some(Value::Array(a)) => a.iter().filter_map(Value::as_str).find(|t| *t != "null").unwrap_or("string").to_string(),
+ _ => {
+ // anyOf/oneOf of [X, null]
+ for key in ["anyOf", "oneOf"] {
+ if let Some(arr) = resolved.get(key).and_then(Value::as_array) {
+ for alt in arr {
+ let t = prop_type(schema, alt);
+ if t != "null" {
+ return t;
+ }
+ }
+ }
+ }
+ if resolved.get("enum").is_some() { "string".into() } else { "any".into() }
+ }
+ }
+}
+
+fn resolve_ref<'a>(schema: &'a Value, prop: &'a Value) -> &'a Value {
+ if let Some(r) = prop.get("$ref").and_then(Value::as_str) {
+ let name = r.rsplit('/').next().unwrap_or_default();
+ for defs in ["$defs", "definitions"] {
+ if let Some(d) = schema.get(defs).and_then(|d| d.get(name)) {
+ return d;
+ }
+ }
+ }
+ prop
+}
+
+fn convert(ty: &str, key: &str, raw: &str) -> Result {
+ match ty {
+ "integer" => {
+ let t = raw.trim();
+ if let Some(h) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
+ u64::from_str_radix(h, 16).map(|n| json!(n)).map_err(|_| format!("--{key} expects a number, got {raw:?}"))
+ } else {
+ t.parse::().map(|n| json!(n)).map_err(|_| format!("--{key} expects a whole number, got {raw:?}"))
+ }
+ }
+ "number" => raw.trim().parse::().map(|n| json!(n)).map_err(|_| format!("--{key} expects a number, got {raw:?}")),
+ "boolean" => match raw.trim() {
+ "true" | "yes" | "1" => Ok(json!(true)),
+ "false" | "no" | "0" => Ok(json!(false)),
+ _ => Err(format!("--{key} expects true or false, got {raw:?}")),
+ },
+ "object" => serde_json::from_str::(raw).map_err(|e| format!("--{key} expects JSON: {e}")),
+ "array" => Ok(serde_json::from_str::(raw).ok().filter(Value::is_array).unwrap_or_else(|| json!([raw]))),
+ "string" => Ok(json!(raw)),
+ _ => Ok(serde_json::from_str::(raw).unwrap_or_else(|_| json!(raw))),
+ }
+}
+
+/// Read inline JSON or `@file.json`.
+pub fn read_json_arg(raw: &str) -> Result {
+ let text = if raw == "-" {
+ read_stdin_json(std::io::stdin())?
+ } else {
+ match raw.strip_prefix('@') {
+ Some(path) => std::fs::read_to_string(path).map_err(|e| format!("can't read {path}: {e}"))?,
+ None => raw.to_string(),
+ }
+ };
+ serde_json::from_str(&text).map_err(|e| format!("params must be a JSON object: {e}"))
+}
+
+/// Most bytes of parameters taken from standard input.
+const MAX_STDIN_JSON: u64 = 64 << 20;
+
+/// Parameters piped in (`--json -`). Never waits at a terminal: nobody is going to type JSON there.
+fn read_stdin_json(input: R) -> Result {
+ if input.is_terminal() {
+ return Err("--json - reads parameters from a pipe, but standard input is a terminal; pipe the JSON in, or pass it inline or as @file.json".into());
+ }
+ read_bounded_json(input)
+}
+
+fn read_bounded_json(input: R) -> Result {
+ use std::io::Read as _;
+ let mut text = String::new();
+ let read = input.take(MAX_STDIN_JSON + 1).read_to_string(&mut text).map_err(|e| format!("can't read parameters from standard input: {e}"))?;
+ if read as u64 > MAX_STDIN_JSON {
+ return Err(format!("parameters on standard input are over the limit of {MAX_STDIN_JSON} bytes; use @file.json"));
+ }
+ Ok(text)
+}
+
+pub fn parse(spec: &ToolSpec, schema: &Value, args: &[String]) -> Result {
+ let props = schema_props(schema);
+ let mut params = Map::new();
+ let mut positionals = Vec::new();
+ let mut inv = ToolInvocation::default();
+ let mut i = 0usize;
+ while let Some(a) = args.get(i) {
+ i += 1;
+ if a == "--help" || a == "-h" {
+ inv.help = true;
+ continue;
+ }
+ if a == "--json" || a == "--params" {
+ let raw = args.get(i).ok_or("--json needs a value")?;
+ i += 1;
+ merge(&mut params, read_json_arg(raw)?)?;
+ continue;
+ }
+ if let Some(rest) = a.strip_prefix("--") {
+ let (key, inline) = match rest.split_once('=') {
+ Some((k, v)) => (k.to_string(), Some(v.to_string())),
+ None => (rest.to_string(), None),
+ };
+ let snake = key.replace('-', "_");
+ let is_target = spec.requires == Requires::Target;
+ // Output format and target-open options, unless the tool has an option of the same name.
+ if !props.contains_key(&snake) && (snake == "output_format" || snake == "format" || (is_target && OPEN_OPTIONS.contains(&snake.as_str()))) {
+ let raw = match inline {
+ Some(v) => v,
+ None => {
+ let v = args.get(i).ok_or_else(|| format!("--{key} needs a value"))?.clone();
+ i += 1;
+ v
+ }
+ };
+ if snake == "output_format" || (snake == "format" && !(is_target && OPEN_FORMATS.contains(&raw.as_str()))) {
+ inv.output_format = Some(raw);
+ } else {
+ inv.open_params.insert(snake, json!(raw));
+ }
+ continue;
+ }
+ let prop = props.get(&snake).ok_or_else(|| {
+ let known: Vec = props.keys().map(|k| format!("--{}", k.replace('_', "-"))).collect();
+ format!("{} has no option --{key}. Options: {}", spec.cli_name(), if known.is_empty() { "(none)".into() } else { known.join(", ") })
+ })?;
+ let ty = prop_type(schema, prop);
+ let raw = match inline {
+ Some(v) => v,
+ None if ty == "boolean" && args.get(i).is_none_or(|n| n.starts_with("--")) => "true".to_string(),
+ None => {
+ let v = args.get(i).ok_or_else(|| format!("--{key} needs a value"))?.clone();
+ i += 1;
+ v
+ }
+ };
+ let value = convert(&ty, &key, &raw)?;
+ if ty == "array"
+ && let Some(Value::Array(existing)) = params.get_mut(&snake)
+ && let Value::Array(more) = value
+ {
+ existing.extend(more);
+ continue;
+ }
+ params.insert(snake, value);
+ continue;
+ }
+ if let Some(path) = a.strip_prefix('@')
+ && path.ends_with(".json")
+ {
+ merge(&mut params, read_json_arg(a)?)?;
+ continue;
+ }
+ positionals.push(a.clone());
+ }
+ // An architecture or entry point only means something for a raw binary.
+ if (inv.open_params.contains_key("architecture") || inv.open_params.contains_key("entry_point")) && !inv.open_params.contains_key("format") {
+ inv.open_params.insert("format".into(), json!("raw-binary"));
+ }
+ let mut positionals = positionals.into_iter();
+ if spec.requires == Requires::Target
+ && let Some(p) = positionals.next()
+ {
+ inv.open_path = Some(p);
+ }
+ // Remaining positionals fill required string params in order, then an optional path (what a bare
+ // argument almost always is, whatever other options sort before it), then other string params.
+ let mut slots: Vec = required(schema).into_iter().filter(|k| !params.contains_key(k)).collect();
+ if !slots.iter().any(|k| k == "path") && !params.contains_key("path") && props.get("path").is_some_and(|v| prop_type(schema, v) == "string") {
+ slots.push("path".into());
+ }
+ for k in props.keys() {
+ if !slots.contains(k) && !params.contains_key(k) && prop_type(schema, props.get(k).unwrap_or(&Value::Null)) == "string" {
+ slots.push(k.clone());
+ }
+ }
+ let mut slots = slots.into_iter();
+ for p in positionals {
+ let slot = slots.next().ok_or_else(|| format!("{} doesn't take another argument ({p:?})", spec.cli_name()))?;
+ let ty = prop_type(schema, props.get(&slot).unwrap_or(&Value::Null));
+ params.insert(slot.clone(), convert(&ty, &slot.replace('_', "-"), &p)?);
+ }
+ inv.params = Value::Object(params);
+ Ok(inv)
+}
+
+fn merge(into: &mut Map, v: Value) -> Result<(), String> {
+ match v {
+ Value::Object(m) => {
+ into.extend(m);
+ Ok(())
+ }
+ _ => Err("params must be a JSON object".into()),
+ }
+}
+
+/// Help text for one tool, generated from its schema.
+/// The fields inside an object option (or an array of objects), listed under it as
+/// `option.field`, two levels deep and at most 30 a level.
+fn nested_fields(schema: &Value, prop: &Value, path: &str, depth: usize, out: &mut String) {
+ if depth > 2 {
+ return;
+ }
+ let mut obj = resolve_ref(schema, prop);
+ // Option arrives as anyOf/oneOf [T, null].
+ if let Some(alt) = ["anyOf", "oneOf"]
+ .iter()
+ .find_map(|k| obj.get(*k).and_then(Value::as_array))
+ .and_then(|a| a.iter().find(|x| x.get("type").and_then(Value::as_str) != Some("null")))
+ {
+ obj = resolve_ref(schema, alt);
+ }
+ if obj.get("type").and_then(Value::as_str) == Some("array")
+ && let Some(items) = obj.get("items")
+ {
+ obj = resolve_ref(schema, items);
+ }
+ let Some(fields) = obj.get("properties").and_then(Value::as_object) else { return };
+ let req = required(obj);
+ for (name, f) in fields.iter().take(30) {
+ let ty = prop_type(schema, f);
+ let desc = f.get("description").and_then(Value::as_str).unwrap_or_default();
+ let star = if req.contains(name) { " (required)" } else { "" };
+ let field = format!("{path}.{name}");
+ out.push_str(&format!(" {field:<26} {ty}{star} {desc}\n"));
+ nested_fields(schema, f, &field, depth + 1, out);
+ }
+}
+
+pub fn help(spec: &ToolSpec, schema: &Value) -> String {
+ let props = schema_props(schema);
+ let req = required(schema);
+ let mut s = format!("{} — {}\n\n{}\n\nUsage: craft-anything-cli {}", spec.cli_name(), spec.title, spec.summary, spec.cli_name());
+ if spec.requires == Requires::Target {
+ s.push_str(" ");
+ }
+ s.push_str(" [options] [--json '{...}' | @params.json | -]\n");
+ if !props.is_empty() {
+ s.push_str("\nOptions:\n");
+ for (k, p) in &props {
+ let ty = prop_type(schema, p);
+ let desc = p.get("description").and_then(Value::as_str).unwrap_or_default();
+ let star = if req.contains(k) { " (required)" } else { "" };
+ s.push_str(&format!(" --{:<28} {ty}{star} {desc}\n", k.replace('_', "-")));
+ nested_fields(schema, p, k, 1, &mut s);
+ }
+ }
+ if spec.requires == Requires::Target {
+ s.push_str("\nTarget options (how is opened):\n");
+ s.push_str(" --architecture x86, x86_64, arm or arm64 for a raw binary (implies --format raw-binary)\n");
+ s.push_str(" --entry-point Load/entry address (0x-hex) for a raw binary\n");
+ s.push_str(" --format raw-binary or dos-com to force how the file is read\n");
+ s.push_str(" --language-id Deep mode only: pin a Ghidra processor language\n");
+ }
+ let own_format = props.contains_key("format");
+ s.push_str(&format!(
+ "\nOutput:\n --output-format json, jsonl, md or text{}\n",
+ if own_format { " (--format after the tool name is this tool's own option)" } else { " (--format works too)" }
+ ));
+ s
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use craft_anything_engine::Registry;
+
+ // The tests use built-in tools, whose schema is their own type's.
+ fn parse(spec: &ToolSpec, args: &[String]) -> Result {
+ super::parse(spec, &(spec.input_schema)(), args)
+ }
+ fn help(spec: &ToolSpec) -> String {
+ super::help(spec, &(spec.input_schema)())
+ }
+
+ #[test]
+ fn help_lists_fields_inside_object_options() {
+ let r = Registry::with_all_tools();
+ let h = help(r.get("app_rebuild_claim_check").unwrap());
+ assert!(h.contains("specification.goal"), "{h}");
+ assert!(h.contains("specification.expectations.evidence_ids"), "array items are listed too: {h}");
+ let plan = help(r.get("core_plan_update").unwrap());
+ assert!(plan.contains(" steps."), "{plan}");
+ let flat = help(r.get("core_target_open").unwrap());
+ assert!(!flat.contains(" path."), "plain options have no nested lines");
+ }
+
+ #[test]
+ fn flags_positionals_and_json() {
+ let r = Registry::with_all_tools();
+ let open = r.get("core_target_open").unwrap();
+ let inv = parse(open, &["/bin/ls".into()]).unwrap();
+ assert_eq!(inv.params["path"], "/bin/ls");
+ assert!(inv.open_path.is_none());
+
+ let upd = r.get("core_plan_update").unwrap();
+ let inv = parse(upd, &["--json".into(), r#"{"steps":[]}"#.into(), "--based-on-revision".into(), "3".into()]).unwrap();
+ assert_eq!(inv.params, json!({"steps": [], "based_on_revision": 3}));
+ assert!(parse(upd, &["--nope".into(), "1".into()]).unwrap_err().contains("--steps"));
+ assert!(parse(upd, &["--based-on-revision".into(), "x".into()]).is_err());
+
+ let close = r.get("core_target_close").unwrap();
+ let inv = parse(close, &["/tmp/a.bin".into()]).unwrap();
+ assert_eq!(inv.open_path.as_deref(), Some("/tmp/a.bin"));
+
+ let rj = r.get("core_journal_read").unwrap();
+ let inv = parse(rj, &["--limit=5".into(), "--kind".into(), "tool_call".into()]).unwrap();
+ assert_eq!(inv.params, json!({"limit": 5, "kind": "tool_call"}));
+ assert!(parse(rj, &["--help".into()]).unwrap().help);
+ assert!(help(rj).contains("--limit"));
+ }
+
+ #[test]
+ fn format_flag_goes_to_the_tool_its_target_or_the_output() {
+ let r = Registry::with_all_tools();
+ // A tool with its own `format` option keeps it; the output format has its own flag.
+ let open = r.get("core_target_open").unwrap();
+ let inv = parse(open, &["--format".into(), "raw-binary".into(), "--output-format".into(), "text".into()]).unwrap();
+ assert_eq!(inv.params["format"], "raw-binary");
+ assert_eq!(inv.output_format.as_deref(), Some("text"));
+ assert!(help(open).contains("this tool's own option"));
+ // Without one, --format is the output format...
+ let rj = r.get("core_journal_read").unwrap();
+ let inv = parse(rj, &["--format=md".into()]).unwrap();
+ assert_eq!(inv.output_format.as_deref(), Some("md"));
+ assert_eq!(inv.params, json!({}));
+ // ...and on a target tool, raw-binary/dos-com and the raw options describe how to open the path.
+ let pc = r.get("native_function_pseudocode").unwrap();
+ let args: Vec = ["/tmp/raw.bin", "--architecture", "x86_64", "--entry-point", "0x1000", "--function", "0x1000", "--format", "text"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect();
+ let inv = parse(pc, &args).unwrap();
+ assert_eq!(inv.open_path.as_deref(), Some("/tmp/raw.bin"));
+ assert_eq!(Value::Object(inv.open_params.clone()), json!({"architecture": "x86_64", "entry_point": "0x1000", "format": "raw-binary"}));
+ assert_eq!(inv.output_format.as_deref(), Some("text"));
+ let inv = parse(pc, &["/tmp/a.com".into(), "--format".into(), "dos-com".into()]).unwrap();
+ assert_eq!(inv.open_params["format"], "dos-com");
+ assert!(inv.output_format.is_none());
+ assert!(help(pc).contains("--entry-point"));
+ }
+
+ #[test]
+ fn at_file_input() {
+ let dir = std::env::temp_dir().join(format!("cany-cli-test-{}", std::process::id()));
+ std::fs::create_dir_all(&dir).unwrap();
+ let f = dir.join("p.json");
+ std::fs::write(&f, r#"{"question":"q?"}"#).unwrap();
+ let r = Registry::with_all_tools();
+ let inv = parse(r.get("core_brief_write").unwrap(), &[format!("@{}", f.display())]).unwrap();
+ assert_eq!(inv.params["question"], "q?");
+ let _ = std::fs::remove_dir_all(&dir);
+ }
+
+ #[test]
+ fn parameters_from_a_pipe_are_bounded() {
+ assert_eq!(read_bounded_json(&b"{\"a\":1}"[..]).unwrap(), "{\"a\":1}");
+ let big = std::io::repeat(b' ').take(MAX_STDIN_JSON + 10);
+ use std::io::Read as _;
+ assert!(read_bounded_json(big).unwrap_err().contains("over the limit"));
+ assert!(read_bounded_json(&[0xffu8, 0xfe][..]).is_err(), "bad text is an error");
+ }
+}
diff --git a/desktop/apps/craft-anything-cli/tests/build_scripts.rs b/desktop/apps/craft-anything-cli/tests/build_scripts.rs
new file mode 100644
index 0000000..b202b7e
--- /dev/null
+++ b/desktop/apps/craft-anything-cli/tests/build_scripts.rs
@@ -0,0 +1,19 @@
+//! The app build scripts must fail the build (Cargo's `cargo::error` instruction) when Windows
+//! resources are required. The single-colon `cargo:error=` form is ignored by Cargo as metadata.
+
+use std::path::Path;
+
+fn read(rel: &str) -> String {
+ let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(rel);
+ std::fs::read_to_string(&path).unwrap_or_default()
+}
+
+#[test]
+fn required_windows_resource_failure_uses_cargo_error_directive() {
+ for rel in ["build.rs", "../craft-anything/build.rs"] {
+ let src = read(rel);
+ assert!(!src.is_empty(), "{rel} should be readable");
+ assert!(!src.contains("cargo:error="), "{rel} emits the ignored single-colon error directive");
+ assert!(src.contains("cargo::error="), "{rel} must emit cargo::error= when resources are required");
+ }
+}
diff --git a/desktop/apps/craft-anything-cli/tests/cli.rs b/desktop/apps/craft-anything-cli/tests/cli.rs
new file mode 100644
index 0000000..1e9949f
--- /dev/null
+++ b/desktop/apps/craft-anything-cli/tests/cli.rs
@@ -0,0 +1,217 @@
+//! End-to-end tests of the built binary (run through the process runner).
+
+use craft_anything_platform::process::{Output, Process};
+use serde_json::{Value, json};
+use std::time::Duration;
+
+fn cli(args: &[&str], config: &std::path::Path) -> Output {
+ Process::new(env!("CARGO_BIN_EXE_craft-anything-cli")).args(args).env("REI_CONFIG_DIR", config).timeout(Duration::from_secs(60)).run().unwrap()
+}
+
+#[test]
+fn commands_and_exit_codes() {
+ let cfg = tempfile::tempdir().unwrap();
+ let out = cli(&["commands", "--format", "json"], cfg.path());
+ assert!(out.success);
+ let v: Value = serde_json::from_slice(&out.stdout).unwrap();
+ assert!(v.as_array().unwrap().iter().any(|c| c["command"] == "core-session-show"));
+ assert_eq!(cli(&["no-such-tool"], cfg.path()).code, Some(2));
+ assert_eq!(cli(&["--bogus-flag"], cfg.path()).code, Some(2));
+ let fail = cli(&["core-finding-record", "--statement", "x", "--evidence-ids", "ev_nope"], cfg.path());
+ assert_eq!(fail.code, Some(1));
+ let e: Value = serde_json::from_slice(&fail.stdout).unwrap();
+ assert_eq!(e["error"]["code"], "bad_input");
+}
+
+/// #249: in Clean room a target tool the mode refuses was listed as available, because nothing
+/// being open yet was waived for every target tool.
+#[test]
+fn capabilities_keep_the_clean_room_refusal() {
+ let cfg = tempfile::tempdir().unwrap();
+ let tool = |args: &[&str], id: &str| {
+ let out = cli(args, cfg.path());
+ let v: Value = serde_json::from_slice(&out.stdout).unwrap();
+ let tools = v["toolsets"].as_array().unwrap().iter().flat_map(|t| t["tools"].as_array().unwrap().clone());
+ tools.into_iter().find(|t| t["tool"] == id).unwrap_or_else(|| panic!("{id} not in {v}"))
+ };
+ let clean = tool(&["--format", "json", "capabilities"], "native_function_pseudocode");
+ assert_eq!((clean["available"].as_bool(), clean["needs_target"].as_bool()), (Some(false), Some(true)), "{clean}");
+ assert!(clean["reason"].as_str().is_some_and(|r| !r.is_empty()), "{clean}");
+ let deep = tool(&["--deep", "--format", "json", "capabilities"], "native_function_pseudocode");
+ assert_eq!((deep["available"].as_bool(), deep["reason"].is_null()), (Some(true), true), "{deep}");
+}
+
+/// After the tool name, `--project` is the tool's own option when it has one (it used to be taken
+/// as the global investigation folder, which was then created there).
+#[test]
+fn a_tools_own_project_option_reaches_the_tool() {
+ let cfg = tempfile::tempdir().unwrap();
+ let folder = tempfile::tempdir().unwrap();
+ let out = cli(&["project-show", "--project", folder.path().to_str().unwrap()], cfg.path());
+ let v: Value = serde_json::from_slice(&out.stdout).unwrap();
+ // project_show read the folder (it isn't a project, so it says so) instead of missing its option.
+ let msg = v["error"]["message"].as_str().unwrap_or_default();
+ assert!(!msg.contains("missing field") && !msg.contains("`project`"), "{v}");
+ assert_eq!(std::fs::read_dir(folder.path()).unwrap().count(), 0, "no investigation was created in the folder");
+}
+
+#[test]
+fn target_tools_open_then_close_and_project_journal() {
+ let cfg = tempfile::tempdir().unwrap();
+ let dir = tempfile::tempdir().unwrap();
+ let file = dir.path().join("sample.bin");
+ std::fs::write(&file, b"\x7fELF not really").unwrap();
+ let proj = dir.path().join("p.cany");
+ let out = cli(&["--deep", "native-cursor-show", file.to_str().unwrap(), "--format", "json", "--project", proj.to_str().unwrap()], cfg.path());
+ assert!(out.success, "{}", out.stdout_text());
+ let v: Value = serde_json::from_slice(&out.stdout).unwrap();
+ assert_eq!(v["result"]["document"], "sample.bin");
+ let days: Vec<_> = std::fs::read_dir(proj.join("journal")).unwrap().collect();
+ assert_eq!(days.len(), 1);
+}
+
+#[test]
+fn clean_room_refuses_to_look_inside_and_says_how_to_turn_on_deep_mode() {
+ let cfg = tempfile::tempdir().unwrap();
+ let dir = tempfile::tempdir().unwrap();
+ let file = dir.path().join("sample.bin");
+ std::fs::write(&file, b"\x7fELF not really").unwrap();
+ let out = cli(&["core-target-open", file.to_str().unwrap()], cfg.path());
+ assert_eq!(out.code, Some(1), "{}", out.stdout_text());
+ let v: Value = serde_json::from_slice(&out.stdout).unwrap();
+ assert_eq!(v["error"]["details"]["reason"], "clean_room", "{v}");
+ assert!(v["error"]["remediation"].as_str().unwrap_or_default().contains("--deep"), "{v}");
+ // --deep is for this run only: nothing is saved.
+ assert!(cli(&["--deep", "core-target-open", file.to_str().unwrap()], cfg.path()).success);
+ assert!(!cfg.path().join("config.toml").exists(), "the run's mode isn't saved");
+ assert_eq!(cli(&["--deep", "--clean-room", "core-session-show"], cfg.path()).code, Some(2));
+ assert_eq!(cli(&["core-session-show", "--deep", "--clean-room"], cfg.path()).code, Some(2));
+}
+
+#[test]
+fn mcp_over_stdio() {
+ let cfg = tempfile::tempdir().unwrap();
+ let input = [
+ json!({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "t", "version": "0"}}}),
+ json!({"jsonrpc": "2.0", "method": "notifications/initialized"}),
+ json!({"jsonrpc": "2.0", "id": 2, "method": "tools/list"}),
+ json!({"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "core_session_show", "arguments": {}}}),
+ ]
+ .iter()
+ .map(|v| format!("{v}\n"))
+ .collect::();
+ let out = Process::new(env!("CARGO_BIN_EXE_craft-anything-cli"))
+ .arg("mcp")
+ .env("REI_CONFIG_DIR", cfg.path())
+ .stdin_bytes(input)
+ .timeout(Duration::from_secs(30))
+ .run()
+ .unwrap();
+ assert!(out.success);
+ let msgs: Vec = out.stdout_text().lines().map(|l| serde_json::from_str(l).unwrap()).collect();
+ let by_id = |id: u64| msgs.iter().find(|m| m["id"] == json!(id)).unwrap().clone();
+ assert_eq!(by_id(1)["result"]["serverInfo"]["name"], "craft-anything");
+ assert!(by_id(2)["result"]["tools"].as_array().unwrap().iter().any(|t| t["name"] == "core_session_show"));
+ assert_eq!(by_id(3)["result"]["isError"], false);
+}
+
+#[test]
+fn raw_binary_open_params_tool_format_and_project_evidence_across_processes() {
+ let cfg = tempfile::tempdir().unwrap();
+ let dir = tempfile::tempdir().unwrap();
+ let raw = dir.path().join("raw.bin");
+ // push rbp; mov rbp, rsp; xor eax, eax; pop rbp; ret
+ std::fs::write(&raw, [0x55, 0x48, 0x89, 0xe5, 0x31, 0xc0, 0x5d, 0xc3]).unwrap();
+ let raw = raw.to_str().unwrap();
+ let proj = dir.path().join("p.cany");
+ let proj = proj.to_str().unwrap();
+
+ // A tool's own --format reaches the tool; the output format has --output-format.
+ let out = cli(
+ &["core-target-open", raw, "--format", "raw-binary", "--architecture", "x86_64", "--entry-point", "0x1000", "--output-format", "json", "--deep"],
+ cfg.path(),
+ );
+ assert!(out.success, "{}", out.stdout_text());
+ let v: Value = serde_json::from_slice(&out.stdout).unwrap();
+ assert_eq!(v["result"]["kind"], "raw-binary", "{v}");
+
+ // A target tool reopens the path with the raw-binary options.
+ let out = cli(
+ &["--deep", "native-function-pseudocode", raw, "--architecture", "x86_64", "--entry-point", "0x1000", "--function", "0x1000", "--project", proj],
+ cfg.path(),
+ );
+ assert!(out.success, "{}", out.stdout_text());
+
+ // A new process on the same project sees the stored evidence.
+ let out = cli(&["core-evidence-show", "--detail", "summary", "--project", proj], cfg.path());
+ assert!(out.success, "{}", out.stdout_text());
+ let v: Value = serde_json::from_slice(&out.stdout).unwrap();
+ assert!(v["result"]["total"].as_u64().unwrap() >= 1, "{v}");
+ let out = cli(&["core-session-show", "--project", proj], cfg.path());
+ let v: Value = serde_json::from_slice(&out.stdout).unwrap();
+ assert!(v["result"]["evidence_records"].as_u64().unwrap() >= 1, "{v}");
+}
+
+fn cli_with_stdin(args: &[&str], config: &std::path::Path, input: &[u8]) -> Output {
+ Process::new(env!("CARGO_BIN_EXE_craft-anything-cli"))
+ .args(args)
+ .env("REI_CONFIG_DIR", config)
+ .stdin_bytes(input.to_vec())
+ .timeout(Duration::from_secs(60))
+ .run()
+ .unwrap()
+}
+
+/// `--json -` takes a tool's parameters from a pipe, and `-` isn't read as the shorthand for JSON output.
+#[test]
+fn parameters_can_come_from_a_pipe() {
+ let cfg = tempfile::tempdir().unwrap();
+ let dir = tempfile::tempdir().unwrap();
+ let file = dir.path().join("plain.txt");
+ std::fs::write(&file, b"nothing packed here").unwrap();
+ let params = json!({"path": file}).to_string();
+ let out = cli_with_stdin(&["core-packed-files-list", "--deep", "--json", "-"], cfg.path(), params.as_bytes());
+ let v: Value = serde_json::from_slice(&out.stdout).unwrap();
+ // The file reached the tool: it says the file isn't packed, not that the path is missing.
+ assert_eq!(v["error"]["code"], "not_supported", "{v}");
+ let bad = cli_with_stdin(&["core-packed-files-list", "--deep", "--json", "-"], cfg.path(), b"not json");
+ assert_eq!(bad.code, Some(2), "{}", String::from_utf8_lossy(&bad.stderr));
+ let empty = cli_with_stdin(&["core-packed-files-list", "--deep", "--json", "-"], cfg.path(), b"");
+ assert_eq!(empty.code, Some(2), "an empty pipe is an error, not a wait");
+}
+
+#[test]
+fn csv_and_tsv_output_show_a_tools_rows() {
+ let cfg = tempfile::tempdir().unwrap();
+ let out = cli(&["core-session-show", "--output-format", "csv"], cfg.path());
+ assert!(out.success);
+ let text = String::from_utf8_lossy(&out.stdout);
+ let mut lines = text.lines();
+ assert!(lines.next().unwrap().split(',').any(|c| c == "clean_room"), "{text}");
+ let tsv = cli(&["core-session-show", "--output-format", "tsv"], cfg.path());
+ assert!(String::from_utf8_lossy(&tsv.stdout).lines().next().unwrap().split('\t').any(|c| c == "clean_room"));
+}
+
+/// A bare argument is the file, not whichever string option sorts first (`contains`, `detail`).
+#[test]
+fn a_bare_argument_is_the_path_even_when_other_options_sort_first() {
+ let cfg = tempfile::tempdir().unwrap();
+ let dir = tempfile::tempdir().unwrap();
+ let file = dir.path().join("plain.txt");
+ std::fs::write(&file, b"nothing packed here").unwrap();
+ let path = file.to_str().unwrap();
+ let packed: Value = serde_json::from_slice(&cli(&["core-packed-files-list", "--deep", path], cfg.path()).stdout).unwrap();
+ assert_eq!(packed["error"]["code"], "not_supported", "the file was read: {packed}");
+ let model: Value = serde_json::from_slice(&cli(&["formats-ai-weights-show", "--deep", path], cfg.path()).stdout).unwrap();
+ assert!(!model["error"]["message"].as_str().unwrap_or_default().contains("unknown variant"), "{model}");
+}
+
+/// #254: the top-level help must not advise `report --format`, which the parser sends to the global output format.
+#[test]
+fn help_does_not_advise_a_report_format_option() {
+ let cfg = tempfile::tempdir().unwrap();
+ let help = cli(&["--help"], cfg.path());
+ assert!(help.success);
+ assert!(!help.stdout_text().contains("(core-target-open, report"));
+ assert_eq!(cli(&["report", "--format", "html"], cfg.path()).code, Some(2));
+}
diff --git a/desktop/apps/craft-anything-cli/tests/live_rebuild.rs b/desktop/apps/craft-anything-cli/tests/live_rebuild.rs
new file mode 100644
index 0000000..7dc9f55
--- /dev/null
+++ b/desktop/apps/craft-anything-cli/tests/live_rebuild.rs
@@ -0,0 +1,47 @@
+//! Live end-to-end rebuild with a real coding agent. Opt-in: it needs network access, a
+//! signed-in Claude Code (`claude auth login`) and spends the person's usage, so it only runs
+//! with `CRAFT_E2E=1`:
+//!
+//! ```sh
+//! CRAFT_E2E=1 cargo test -p craft-anything-cli --test live_rebuild -- --nocapture
+//! ```
+
+use craft_anything_platform::process::Process;
+use serde_json::Value;
+use std::time::Duration;
+
+fn enabled() -> bool {
+ let on = std::env::var("CRAFT_E2E").is_ok_and(|v| v == "1");
+ if !on {
+ eprintln!("skipped: set CRAFT_E2E=1 to run the live rebuild");
+ }
+ on
+}
+
+#[test]
+fn website_clone_reaches_ready_and_looks_alike() {
+ if !enabled() {
+ return;
+ }
+ let projects = tempfile::tempdir().unwrap();
+ let out = Process::new(env!("CARGO_BIN_EXE_craft-anything-cli"))
+ .args(["rebuild", "https://example.com", "Clone this website", "--rounds", "1", "--unattended", "--format", "text"])
+ .arg("--projects-dir")
+ .arg(projects.path())
+ .timeout(Duration::from_secs(45 * 60))
+ .idle_timeout(Duration::from_secs(15 * 60))
+ .run()
+ .unwrap();
+ let log = out.stdout_text();
+ assert!(out.success, "rebuild failed:\n{log}");
+
+ let dirs: Vec<_> = std::fs::read_dir(projects.path()).unwrap().filter_map(Result::ok).filter(|e| e.path().is_dir()).collect();
+ assert_eq!(dirs.len(), 1, "one project folder");
+ let root = dirs[0].path();
+ let manifest: Value = serde_json::from_slice(&std::fs::read(root.join("craft.json")).unwrap()).unwrap();
+ assert_eq!(manifest["status"], "ready", "{log}");
+ assert_eq!(manifest["target"]["kind"], "website");
+ let best = manifest["checks"].as_array().unwrap().iter().filter_map(|c| c["similarity"].as_f64()).fold(0.0, f64::max);
+ assert!(best >= 80.0, "best likeness {best:.0}%:\n{log}");
+ assert!(["dist", "out", "build"].iter().any(|d| root.join("app").join(d).join("index.html").is_file()), "a built site in app/dist, app/out or app/build");
+}
diff --git a/desktop/apps/craft-anything-cli/tests/plugins.rs b/desktop/apps/craft-anything-cli/tests/plugins.rs
new file mode 100644
index 0000000..1f16cb9
--- /dev/null
+++ b/desktop/apps/craft-anything-cli/tests/plugins.rs
@@ -0,0 +1,56 @@
+//! Plugins end to end: REI Reverse Engineer's own MCP server (`craft-anything-cli mcp`) configured as a
+//! plugin, refreshed, listed and called through the CLI. Everything stays in temp folders.
+
+use craft_anything_platform::process::{Output, Process};
+use serde_json::Value;
+use std::time::Duration;
+
+fn cli(args: &[&str], config: &std::path::Path) -> Output {
+ Process::new(env!("CARGO_BIN_EXE_craft-anything-cli")).args(args).env("REI_CONFIG_DIR", config).timeout(Duration::from_secs(120)).run().unwrap()
+}
+
+#[test]
+fn an_mcp_server_becomes_a_toolset_of_proxied_tools() {
+ let cfg = tempfile::tempdir().unwrap();
+ let projects = tempfile::tempdir().unwrap();
+ let exe = env!("CARGO_BIN_EXE_craft-anything-cli");
+ std::fs::write(
+ cfg.path().join("config.toml"),
+ format!("version = 1\n\n[[plugins.mcp]]\nid = \"selfmcp\"\nname = \"Itself\"\ncommand = '{exe}'\nargs = [\"mcp\", \"--all-tools\"]\nconsent = false\n"),
+ )
+ .unwrap();
+
+ // Before a refresh nothing is cached, so no proxied tools.
+ let list = cli(&["plugins-list", "--format", "json"], cfg.path());
+ assert!(list.success, "{}", list.stdout_text());
+ let v: Value = serde_json::from_slice(&list.stdout).unwrap();
+ assert_eq!(v["result"]["plugins"][0]["id"], "selfmcp");
+ assert_eq!(v["result"]["plugins"][0]["cached_tools"].as_array().map(Vec::len), Some(0));
+
+ // Refreshing starts the server (it's a program, so it needs --allow-run) and caches its tools.
+ assert!(!cli(&["plugins-refresh", "--format", "json"], cfg.path()).success, "refused without consent");
+ let r = cli(&["plugins-refresh", "--allow-run", "--format", "json"], cfg.path());
+ assert!(r.success, "{}", r.stdout_text());
+ let v: Value = serde_json::from_slice(&r.stdout).unwrap();
+ let tools = v["result"]["refreshed"][0]["tools"].as_array().cloned().unwrap_or_default();
+ assert!(tools.iter().any(|t| t == "project_list"), "{v}");
+
+ // The next start offers them as a toolset of its own.
+ let c = cli(&["commands", "--format", "json"], cfg.path());
+ let v: Value = serde_json::from_slice(&c.stdout).unwrap();
+ let ours = v.as_array().unwrap().iter().find(|c| c["command"] == "selfmcp-project-list").cloned();
+ assert_eq!(ours.as_ref().map(|c| c["toolset"].clone()), Some(Value::from("selfmcp")), "{v}");
+
+ // A plugin's tool may look at anything, so Clean room doesn't run it.
+ let dir = projects.path().to_str().unwrap();
+ let refused = cli(&["selfmcp-project-list", "--projects-dir", dir, "--format", "json"], cfg.path());
+ let v: Value = serde_json::from_slice(&refused.stdout).unwrap();
+ assert_eq!(v["error"]["details"]["reason"], "clean_room", "{v}");
+
+ // In Deep mode a call goes through the server and comes back as evidence from an external service.
+ let call = cli(&["--deep", "selfmcp-project-list", "--projects-dir", dir, "--format", "json"], cfg.path());
+ assert!(call.success, "{}", call.stdout_text());
+ let v: Value = serde_json::from_slice(&call.stdout).unwrap();
+ assert_eq!(v["authority"], "outside-service", "{v}");
+ assert!(v.to_string().contains("projects_dir"), "{v}");
+}
diff --git a/desktop/apps/craft-anything-web/Cargo.toml b/desktop/apps/craft-anything-web/Cargo.toml
new file mode 100644
index 0000000..96bcb1d
--- /dev/null
+++ b/desktop/apps/craft-anything-web/Cargo.toml
@@ -0,0 +1,32 @@
+[package]
+name = "craft-anything-web"
+description = "Craft Anything in the browser: drop a file to see what it is (eframe web runner, WebGPU with WebGL2 fallback)"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+publish = false
+
+[lints]
+workspace = true
+
+[[bin]]
+name = "craft-anything-web"
+path = "src/main.rs"
+
+[dependencies]
+# Only the wasm-safe crates (`cargo xtask wasm`): no engine, platform, AI or desktop UI.
+craft-anything-evidence = { workspace = true }
+craft-anything-formats = { workspace = true }
+craft-anything-analysis = { workspace = true }
+egui = { workspace = true }
+serde_json = { workspace = true }
+log = { workspace = true }
+
+[target.'cfg(target_arch = "wasm32")'.dependencies]
+# `wgpu` pulls egui-wgpu's defaults: WebGPU plus the WebGL2 fallback.
+eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit"] }
+# Open button: the browser's file picker (an , read in memory).
+rfd = { workspace = true }
+wasm-bindgen = "0.2"
+wasm-bindgen-futures = "0.4"
+web-sys = { version = "0.3", features = ["Window", "Document", "Element", "HtmlCanvasElement", "Location"] }
diff --git a/desktop/apps/craft-anything-web/Trunk.toml b/desktop/apps/craft-anything-web/Trunk.toml
new file mode 100644
index 0000000..638a404
--- /dev/null
+++ b/desktop/apps/craft-anything-web/Trunk.toml
@@ -0,0 +1,10 @@
+# Build with: trunk build --release (from apps/craft-anything-web)
+[build]
+target = "index.html"
+dist = "../../dist/web"
+public_url = "./"
+filehash = true
+
+[serve]
+addresses = ["127.0.0.1"]
+port = 8771
diff --git a/desktop/apps/craft-anything-web/index.html b/desktop/apps/craft-anything-web/index.html
new file mode 100644
index 0000000..69dcf96
--- /dev/null
+++ b/desktop/apps/craft-anything-web/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+ Craft Anything
+
+
+
+
+
+ Loading Craft Anything…
+
+
diff --git a/desktop/apps/craft-anything-web/src/main.rs b/desktop/apps/craft-anything-web/src/main.rs
new file mode 100644
index 0000000..5a98ac1
--- /dev/null
+++ b/desktop/apps/craft-anything-web/src/main.rs
@@ -0,0 +1,27 @@
+//! REI Reverse Engineer in the browser: a small viewer that tells you what a file is.
+//!
+//! It runs only the wasm-safe crates (formats, analysis, evidence), entirely in the page: the file
+//! never leaves the browser. Native programs (ELF, PE, Mach-O, DOS) get the same plain-English
+//! triage as the desktop app's `triage_binary`; other files are identified by their signature. The
+//! desktop app does everything else (disassembly, JavaScript apps, Android, devices, AI).
+//!
+//! Build with `trunk build --release` from this directory; the site lands in `dist/web`.
+//! URL query flag: `?webgl` forces the WebGL2 backend instead of WebGPU.
+#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
+// The viewer and triage only run in the browser; natively they're compiled (and tested) but unused.
+#![cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
+
+mod triage;
+mod viewer;
+#[cfg(target_arch = "wasm32")]
+mod web;
+
+#[cfg(target_arch = "wasm32")]
+fn main() {
+ web::start();
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+fn main() {
+ eprintln!("craft-anything-web only runs in the browser: build it with `trunk build --release` in apps/craft-anything-web");
+}
diff --git a/desktop/apps/craft-anything-web/src/triage.rs b/desktop/apps/craft-anything-web/src/triage.rs
new file mode 100644
index 0000000..c3bceb5
--- /dev/null
+++ b/desktop/apps/craft-anything-web/src/triage.rs
@@ -0,0 +1,145 @@
+//! What a dropped file is: native triage for programs, a signature check for everything else.
+//!
+//! Pure and synchronous (no clock, threads or file system), so it is safe in the browser.
+
+use craft_anything_analysis::js_inventory::sniff_kind;
+use craft_anything_analysis::native;
+use craft_anything_evidence::{Confidence, sha256_hex};
+use craft_anything_formats::{entropy, parse_binary};
+use serde_json::Value;
+
+/// Largest file the page reads (browser memory is the limit; the desktop app has none).
+pub const MAX_BYTES: usize = 512 * 1024 * 1024;
+
+/// The answer for one file.
+pub struct Look {
+ pub file_name: String,
+ /// One or two plain sentences.
+ pub summary: String,
+ pub confidence: Confidence,
+ /// Label/value pairs shown as a table.
+ pub facts: Vec<(String, String)>,
+ pub limitations: Vec,
+ /// The full result, pretty-printed (empty when there is none beyond the facts).
+ pub details: String,
+}
+
+pub fn look(file_name: &str, bytes: &[u8]) -> Look {
+ let mut facts = vec![("Size".to_string(), size_text(bytes.len())), ("SHA-256".to_string(), sha256_hex(bytes))];
+ let sample = bytes.get(..bytes.len().min(4 * 1024 * 1024)).unwrap_or_default();
+ facts.push(("Entropy (first 4 MB)".to_string(), format!("{:.2} bits per byte", entropy(sample))));
+ let kind = sniff_kind(bytes, file_name);
+ if let Ok(bin) = parse_binary(bytes) {
+ let report = native::triage(&bin, bytes, file_name);
+ let field = |k: &str| report.result.get(k).and_then(Value::as_str).map(str::to_string);
+ let mut native_facts = Vec::new();
+ for (label, key) in
+ [("Format", "format_name"), ("Runs on", "operating_system"), ("Kind", "kind"), ("Architecture", "architecture"), ("Entry point", "entry_point")]
+ {
+ if let Some(v) = field(key) {
+ native_facts.push((label.to_string(), v));
+ }
+ }
+ if let Some(s) = report.result.get("stripped").and_then(Value::as_bool) {
+ native_facts.push(("Names removed (stripped)".to_string(), if s { "yes" } else { "no" }.to_string()));
+ }
+ native_facts.append(&mut facts);
+ return Look {
+ file_name: file_name.to_string(),
+ summary: report.summary,
+ confidence: report.confidence,
+ facts: native_facts,
+ limitations: report.limitations.into_iter().map(|l| l.message).collect(),
+ details: serde_json::to_string_pretty(&report.result).unwrap_or_default(),
+ };
+ }
+ let (what, next) = describe(kind);
+ facts.insert(0, ("Detected as".to_string(), kind.to_string()));
+ Look {
+ file_name: file_name.to_string(),
+ summary: format!("{file_name} looks like {what}. {next}"),
+ confidence: if matches!(kind, "text" | "binary") { Confidence::Inferred } else { Confidence::Observed },
+ facts,
+ limitations: vec!["identified from its first bytes and name only; the web viewer doesn't open containers or code".to_string()],
+ details: String::new(),
+ }
+}
+
+/// A plain description of a sniffed kind, and where to look next.
+fn describe(kind: &str) -> (&'static str, &'static str) {
+ const DESKTOP: &str = "Open it in the REI Reverse Engineer desktop app to look inside.";
+ match kind {
+ "wasm" => ("a WebAssembly module (code that runs in browsers and other hosts)", DESKTOP),
+ "dex" => ("Android app code (a DEX file)", DESKTOP),
+ "apk" => ("an Android app package", DESKTOP),
+ "ipa" => ("an iPhone/iPad app package", DESKTOP),
+ "jar" => ("a Java archive", DESKTOP),
+ "java-class" => ("a compiled Java class", DESKTOP),
+ "msix" => ("a Windows app package (MSIX/AppX)", DESKTOP),
+ "hap" => ("a HarmonyOS app package", DESKTOP),
+ "zip" => ("a ZIP archive", DESKTOP),
+ "asar" => ("an Electron app archive (app.asar): the app's JavaScript lives here", DESKTOP),
+ "native-addon" => ("a Node.js native add-on", DESKTOP),
+ "javascript" | "typescript" => ("JavaScript or TypeScript source", DESKTOP),
+ "source-map" => ("a source map (links bundled JavaScript back to its sources)", DESKTOP),
+ "binary-plist" | "xml-plist" => ("an Apple property list (settings or app metadata)", DESKTOP),
+ "gzip" => ("gzip-compressed data", DESKTOP),
+ "xar" => ("a XAR archive (macOS installer packages use it)", DESKTOP),
+ "sqlite" => ("an SQLite database", DESKTOP),
+ "png" | "jpeg" | "gif" | "svg" => ("an image", "There's no code to look at in an image."),
+ "pdf" => ("a PDF document", "There's no program to look at here."),
+ "json" | "html" | "css" | "xml" | "text" => ("text", DESKTOP),
+ _ => ("data we don't recognise", "The desktop app has many more ways to identify files."),
+ }
+}
+
+fn size_text(n: usize) -> String {
+ const UNITS: [&str; 4] = ["KB", "MB", "GB", "TB"];
+ if n < 1024 {
+ return format!("{n} bytes");
+ }
+ let mut v = n as f64 / 1024.0;
+ let mut unit = UNITS[0];
+ for u in UNITS.iter().skip(1) {
+ if v < 1024.0 {
+ break;
+ }
+ v /= 1024.0;
+ unit = u;
+ }
+ format!("{v:.1} {unit} ({n} bytes)")
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn empty_and_garbage_never_crash() {
+ for bytes in [&b""[..], b"\x7fELF", b"MZ\0\0\0\0", &[0xcf, 0xfa, 0xed, 0xfe, 1, 2], &[0xca, 0xfe, 0xba, 0xbe, 0, 0, 0, 9]] {
+ let l = look("x", bytes);
+ assert!(!l.summary.is_empty());
+ }
+ }
+
+ #[test]
+ fn signatures() {
+ assert!(look("a.png", b"\x89PNG\r\n\x1a\n").summary.contains("image"));
+ assert!(look("m.wasm", b"\0asm\x01\0\0\0").summary.contains("WebAssembly"));
+ }
+
+ #[test]
+ fn host_program() {
+ let Ok(bytes) = std::fs::read("/bin/ls") else { return };
+ let l = look("ls", &bytes);
+ assert!(l.summary.starts_with("ls is a program"), "{}", l.summary);
+ assert!(l.facts.iter().any(|(k, _)| k == "Architecture"));
+ assert!(!l.details.is_empty());
+ }
+
+ #[test]
+ fn sizes() {
+ assert_eq!(size_text(10), "10 bytes");
+ assert_eq!(size_text(2048), "2.0 KB (2048 bytes)");
+ }
+}
diff --git a/desktop/apps/craft-anything-web/src/viewer.rs b/desktop/apps/craft-anything-web/src/viewer.rs
new file mode 100644
index 0000000..9f36e91
--- /dev/null
+++ b/desktop/apps/craft-anything-web/src/viewer.rs
@@ -0,0 +1,165 @@
+//! The page: a welcome screen, drag and drop (and an Open button), then the answer.
+
+use crate::triage::{Look, MAX_BYTES, look};
+use craft_anything_evidence::Confidence;
+use egui::{Color32, RichText};
+#[allow(clippy::disallowed_types)] // The browser has one thread: nothing can wait here.
+use std::sync::{Arc, Mutex, PoisonError};
+
+// A few tokens copied from the desktop app's Studio theme (crates/ui-egui/src/theme.rs).
+const CANVAS: Color32 = Color32::from_rgb(0x0e, 0x0e, 0x0f);
+const CARD: Color32 = Color32::from_rgb(0x1a, 0x1a, 0x1c);
+const CARD_BORDER: Color32 = Color32::from_rgb(0x2a, 0x2a, 0x2e);
+const TEXT: Color32 = Color32::from_rgb(0xec, 0xec, 0xf0);
+const TEXT_DIM: Color32 = Color32::from_rgb(0xa9, 0xa9, 0xb2);
+/// The app colour (getartcraft.com's deep accent), as in the desktop app.
+const BRAND: Color32 = Color32::from_rgb(0x16, 0x59, 0xc4);
+/// The app colour as text on the dark background (getartcraft.com's light accent).
+const BRAND_INK: Color32 = Color32::from_rgb(0x74, 0xaa, 0xff);
+const SEEN_IT: Color32 = Color32::from_rgb(0x3f, 0xb2, 0x7f);
+const WORKED_OUT: Color32 = Color32::from_rgb(0x5b, 0x8d, 0xef);
+const BEST_GUESS: Color32 = Color32::from_rgb(0xe0, 0xa2, 0x3a);
+
+/// A file handed over from outside a frame (the browser's file picker).
+/// A file (name and contents), or a plain message saying why it couldn't be read.
+#[allow(clippy::disallowed_types)]
+pub type Inbox = Arc), String>>>>;
+
+pub struct Viewer {
+ look: Option,
+ error: Option,
+ inbox: Inbox,
+ /// Opens the browser's file picker; its result arrives in `inbox`.
+ open: Box,
+ /// Reads a dropped file (asynchronously in the browser); its contents arrive in `inbox`.
+ read_drop: Box,
+}
+
+impl Viewer {
+ pub fn new(inbox: Inbox, open: Box, read_drop: Box) -> Self {
+ Viewer { look: None, error: None, inbox, open, read_drop }
+ }
+
+ pub fn setup(ctx: &egui::Context) {
+ let mut visuals = egui::Visuals::dark();
+ visuals.panel_fill = CANVAS;
+ visuals.window_fill = CARD;
+ visuals.override_text_color = Some(TEXT);
+ visuals.selection.bg_fill = BRAND;
+ visuals.hyperlink_color = BRAND_INK;
+ // The desktop app's Studio theme is dark; don't follow the system's light preference.
+ ctx.set_theme(egui::ThemePreference::Dark);
+ ctx.set_visuals_of(egui::Theme::Dark, visuals);
+ }
+
+ fn take(&mut self, name: String, bytes: &[u8]) {
+ if bytes.len() > MAX_BYTES {
+ self.error = Some(format!("{name} is larger than {} MB, which is more than the web viewer reads.", MAX_BYTES / (1024 * 1024)));
+ return;
+ }
+ self.error = None;
+ self.look = Some(look(&name, bytes));
+ }
+
+ pub fn ui(&mut self, ui: &mut egui::Ui) {
+ let ctx = ui.ctx().clone();
+ let dropped = ctx.input_mut(|i| std::mem::take(&mut i.raw.dropped_files));
+ if let Some(f) = dropped.into_iter().next() {
+ (self.read_drop)(f);
+ }
+ let picked = self.inbox.lock().unwrap_or_else(PoisonError::into_inner).take();
+ match picked {
+ Some(Ok((name, bytes))) => self.take(name, &bytes),
+ Some(Err(e)) => self.error = Some(e),
+ None => {}
+ }
+ let hovering = ctx.input(|i| !i.raw.hovered_files.is_empty());
+
+ egui::CentralPanel::default().show(ui, |ui| {
+ egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| {
+ ui.set_max_width(ui.available_width().min(860.0));
+ ui.add_space(24.0);
+ ui.horizontal(|ui| {
+ ui.label(RichText::new("REI Reverse Engineer").size(26.0).strong());
+ ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
+ if ui.button(RichText::new("Open a file…").size(15.0)).clicked() {
+ (self.open)();
+ }
+ });
+ });
+ ui.label(RichText::new("Drop a file to see what it is.").size(17.0).color(TEXT_DIM));
+ ui.add_space(16.0);
+ if hovering {
+ card(ui, |ui| {
+ ui.label(RichText::new("Let go to look at it").size(18.0).color(BRAND_INK));
+ });
+ } else if let Some(e) = &self.error {
+ card(ui, |ui| {
+ ui.label(RichText::new(e).color(BEST_GUESS));
+ });
+ } else if let Some(l) = &self.look {
+ show_look(ui, l);
+ } else {
+ card(ui, |ui| {
+ ui.label(
+ "Drop a program, app, library or any file here. REI Reverse Engineer tells you what it is, what it runs on and what it was made with.",
+ );
+ ui.add_space(6.0);
+ ui.label(RichText::new("Everything happens in this page: your file is never uploaded.").color(TEXT_DIM));
+ });
+ }
+ ui.add_space(16.0);
+ ui.label(
+ RichText::new(
+ "This is the small web version. The desktop app does much more: disassembly, functions and call graphs, \
+ JavaScript and Electron apps, Android, websites, devices, and an AI that explains what it finds.",
+ )
+ .color(TEXT_DIM),
+ );
+ ui.hyperlink_to("Get the desktop app", "https://github.com/storytold/craft-anything/releases/latest");
+ });
+ });
+ }
+}
+
+fn card(ui: &mut egui::Ui, add: impl FnOnce(&mut egui::Ui)) {
+ egui::Frame::new().fill(CARD).stroke(egui::Stroke::new(1.0, CARD_BORDER)).corner_radius(12.0).inner_margin(18.0).show(ui, |ui| {
+ ui.set_width(ui.available_width());
+ add(ui);
+ });
+}
+
+fn show_look(ui: &mut egui::Ui, l: &Look) {
+ card(ui, |ui| {
+ ui.label(RichText::new(&l.file_name).size(18.0).strong());
+ let (color, label) = match l.confidence {
+ Confidence::Observed => (SEEN_IT, "Seen it"),
+ Confidence::Derived => (WORKED_OUT, "Worked it out"),
+ Confidence::Inferred => (BEST_GUESS, "Best guess"),
+ };
+ ui.label(RichText::new(label).color(color).small());
+ ui.add_space(6.0);
+ ui.label(RichText::new(&l.summary).size(15.0));
+ ui.add_space(10.0);
+ egui::Grid::new("facts").num_columns(2).spacing([16.0, 6.0]).striped(true).show(ui, |ui| {
+ for (k, v) in &l.facts {
+ ui.label(RichText::new(k).color(TEXT_DIM));
+ ui.add(egui::Label::new(RichText::new(v).monospace()).wrap());
+ ui.end_row();
+ }
+ });
+ if !l.limitations.is_empty() {
+ ui.add_space(10.0);
+ ui.label(RichText::new("What this can't tell you").color(TEXT_DIM));
+ for m in &l.limitations {
+ ui.label(format!("• {m}"));
+ }
+ }
+ if !l.details.is_empty() {
+ ui.add_space(10.0);
+ egui::CollapsingHeader::new("All details (JSON)").show(ui, |ui| {
+ ui.add(egui::Label::new(RichText::new(&l.details).monospace().size(12.0)).wrap());
+ });
+ }
+ });
+}
diff --git a/desktop/apps/craft-anything-web/src/web.rs b/desktop/apps/craft-anything-web/src/web.rs
new file mode 100644
index 0000000..4d23a6c
--- /dev/null
+++ b/desktop/apps/craft-anything-web/src/web.rs
@@ -0,0 +1,98 @@
+//! The browser shell: the eframe web runner and the file picker.
+
+use crate::viewer::{Inbox, Viewer};
+use wasm_bindgen::JsCast as _;
+
+const CANVAS_ID: &str = "craft_anything_canvas";
+const LOADING_ID: &str = "craft_anything_loading";
+
+pub fn start() {
+ eframe::WebLogger::init(log::LevelFilter::Info).ok();
+ wasm_bindgen_futures::spawn_local(async {
+ let Some(document) = web_sys::window().and_then(|w| w.document()) else {
+ log::error!("no document");
+ return;
+ };
+ let Some(canvas) = document.get_element_by_id(CANVAS_ID).and_then(|e| e.dyn_into::().ok()) else {
+ log::error!("missing ");
+ return;
+ };
+ let mut options = eframe::WebOptions::default();
+ // `?webgl`: force the WebGL2 backend (troubleshooting, and headless browsers whose WebGPU
+ // adapter request returns nothing).
+ if query().contains("webgl")
+ && let eframe::egui_wgpu::WgpuSetup::CreateNew(create) = &mut options.wgpu_options.wgpu_setup
+ {
+ create.instance_descriptor.backends = eframe::wgpu::Backends::GL;
+ }
+ let result = eframe::WebRunner::new()
+ .start(
+ canvas,
+ options,
+ Box::new(|cc| {
+ if let Some(rs) = &cc.wgpu_render_state {
+ log::info!("craft-anything-web: wgpu backend {:?}", rs.adapter.get_info().backend);
+ }
+ Viewer::setup(&cc.egui_ctx);
+ let inbox = Inbox::default();
+ let open = open_picker(inbox.clone(), cc.egui_ctx.clone());
+ let read_drop = read_dropped(inbox.clone(), cc.egui_ctx.clone());
+ Ok(Box::new(Shell { viewer: Viewer::new(inbox, open, read_drop) }))
+ }),
+ )
+ .await;
+ if let Some(el) = document.get_element_by_id(LOADING_ID) {
+ match result {
+ Ok(()) => el.remove(),
+ Err(e) => el.set_inner_html(&format!("REI Reverse Engineer failed to start: {e:?}
A browser with WebGPU or WebGL2 is required.
")),
+ }
+ }
+ });
+}
+
+/// The Open button: the browser's file picker, read into memory, then handed to the next frame.
+fn open_picker(inbox: Inbox, ctx: egui::Context) -> Box {
+ Box::new(move || {
+ let inbox = inbox.clone();
+ let ctx = ctx.clone();
+ wasm_bindgen_futures::spawn_local(async move {
+ let Some(file) = rfd::AsyncFileDialog::new().pick_file().await else { return };
+ let bytes = file.read().await;
+ deliver(&inbox, &ctx, Ok((file.file_name(), bytes)));
+ });
+ })
+}
+
+/// A dropped file: browsers only read file contents asynchronously.
+fn read_dropped(inbox: Inbox, ctx: egui::Context) -> Box {
+ Box::new(move |file| {
+ let inbox = inbox.clone();
+ let ctx = ctx.clone();
+ wasm_bindgen_futures::spawn_local(async move {
+ let name = file.path().file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_else(|| "the file".to_string());
+ let got = file.bytes_async().await.map(|b| (name.clone(), b)).map_err(|e| format!("The browser couldn't read {name}: {e}"));
+ deliver(&inbox, &ctx, got);
+ });
+ })
+}
+
+fn deliver(inbox: &Inbox, ctx: &egui::Context, got: Result<(String, Vec), String>) {
+ if let Ok(mut slot) = inbox.lock() {
+ *slot = Some(got);
+ }
+ ctx.request_repaint();
+}
+
+fn query() -> String {
+ web_sys::window().and_then(|w| w.location().search().ok()).unwrap_or_default()
+}
+
+struct Shell {
+ viewer: Viewer,
+}
+
+impl eframe::App for Shell {
+ fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
+ self.viewer.ui(ui);
+ }
+}
diff --git a/desktop/apps/craft-anything/Cargo.toml b/desktop/apps/craft-anything/Cargo.toml
new file mode 100644
index 0000000..8bff64a
--- /dev/null
+++ b/desktop/apps/craft-anything/Cargo.toml
@@ -0,0 +1,39 @@
+[package]
+name = "craft-anything"
+description = "REI Reverse Engineer: native evidence-driven reverse engineering workbench"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+publish = false
+build = "build.rs"
+
+[lints]
+workspace = true
+
+[dependencies]
+craft-anything-engine = { workspace = true }
+craft-anything-platform = { workspace = true }
+craft-anything-ai = { workspace = true }
+craft-anything-setup = { workspace = true }
+craft-anything-ui-egui = { workspace = true }
+serde_json = { workspace = true }
+eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "wayland", "x11", "persistence"] }
+log = { workspace = true }
+# The one dialog the app shows itself: when its window can't open, so a start that shows nothing
+# still says why (#166).
+rfd = { workspace = true }
+
+[features]
+wasm-plugins = ["craft-anything-engine/wasm-plugins"]
+
+[build-dependencies]
+# Icon + VERSIONINFO for craft-anything.exe; build.rs only uses it when targeting Windows.
+winresource = "0.1"
+
+[[bin]]
+name = "rei-reverse-engineer"
+path = "src/main.rs"
+
+[[bin]]
+name = "rei-bridge-check"
+path = "src/bin/bridge_check.rs"
diff --git a/desktop/apps/craft-anything/build.rs b/desktop/apps/craft-anything/build.rs
new file mode 100644
index 0000000..3ea4099
--- /dev/null
+++ b/desktop/apps/craft-anything/build.rs
@@ -0,0 +1,31 @@
+//! Windows only: embed the app icon and version info (VERSIONINFO) into `craft-anything.exe`.
+//!
+//! On every other target this does nothing. A missing resource compiler is a warning, so a
+//! cross-compile from macOS or Linux still links, unless `CRAFT_ANYTHING_REQUIRE_WINRES=1` (set by
+//! the release workflow) turns it into an error.
+
+fn main() {
+ println!("cargo:rerun-if-changed=build.rs");
+ println!("cargo:rerun-if-changed=../../assets/app-icon/craft-anything.ico");
+ println!("cargo:rerun-if-env-changed=CRAFT_ANYTHING_REQUIRE_WINRES");
+ if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows") {
+ return;
+ }
+ let mut res = winresource::WindowsResource::new();
+ res.set_icon("../../assets/app-icon/craft-anything.ico")
+ .set("ProductName", "REI Reverse Engineer")
+ .set("FileDescription", "REI Reverse Engineer: find out how any software works")
+ .set(
+ "LegalCopyright",
+ "Original work: ArtCraft Team and Craft Anything contributors. Rootcastle modifications: Rootcastle Engineering & Innovation. MIT OR Apache-2.0.",
+ )
+ .set("OriginalFilename", "rei-reverse-engineer.exe")
+ .set("InternalName", "rootcastle-rei");
+ if let Err(e) = res.compile() {
+ if std::env::var_os("CRAFT_ANYTHING_REQUIRE_WINRES").is_some() {
+ println!("cargo::error=embedding Windows resources failed: {e}");
+ return;
+ }
+ println!("cargo:warning=craft-anything.exe built without icon/version resources: {e}");
+ }
+}
diff --git a/desktop/apps/craft-anything/src/bin/bridge_check.rs b/desktop/apps/craft-anything/src/bin/bridge_check.rs
new file mode 100644
index 0000000..9217d80
--- /dev/null
+++ b/desktop/apps/craft-anything/src/bin/bridge_check.rs
@@ -0,0 +1,40 @@
+//! Verify the same MCP plugin routes used by the desktop, without target execution.
+use craft_anything_engine::{CallOptions, Caller, CancelToken, Consent, Engine, Session, Settings, plugins};
+use serde_json::json;
+
+fn main() -> Result<(), Box> {
+ let target = std::env::args().nth(1);
+ let mut settings = Settings::load();
+ let upstream = settings.plugins.mcp.iter().any(|p| p.id == "upstream_rea" && p.enabled);
+ // This diagnostic's optional call inspects only the explicitly supplied file.
+ // Normal desktop sessions retain the user's Clean room / Deep selection.
+ if target.is_some() {
+ settings.deep_mode = true;
+ }
+ let mut session = Session::new(settings);
+ let config = session.config_dir();
+ for plugin in session.settings.plugins.mcp.iter().filter(|p| p.enabled) {
+ let tools = plugins::refresh(plugin, &config)?;
+ println!("{}: {} tools discovered over real MCP", plugin.id, tools.len());
+ }
+ let routes = plugins::install(&mut session);
+ println!("{} external routes registered in desktop engine", routes.len());
+ let engine = Engine::shared_with(session, vec![], routes);
+ if let Some(path) = target {
+ let result = engine.call(
+ "rei_rei_rea_inspect",
+ json!({"target": &path}),
+ CallOptions { caller: Caller::Cli, consent: Consent::Granted, cancel: CancelToken::default(), progress: &|_| {} },
+ )?;
+ println!("{}", serde_json::to_string_pretty(&result)?);
+ if upstream {
+ let result = engine.call(
+ "upstream_rea_inspect_byte_entropy",
+ json!({"path": path}),
+ CallOptions { caller: Caller::Cli, consent: Consent::Granted, cancel: CancelToken::default(), progress: &|_| {} },
+ )?;
+ println!("Upstream static entropy call completed:\n{}", serde_json::to_string_pretty(&result)?);
+ }
+ }
+ Ok(())
+}
diff --git a/desktop/apps/craft-anything/src/main.rs b/desktop/apps/craft-anything/src/main.rs
new file mode 100644
index 0000000..794abe0
--- /dev/null
+++ b/desktop/apps/craft-anything/src/main.rs
@@ -0,0 +1,212 @@
+//! The REI Reverse Engineer desktop app.
+#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
+#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
+
+use craft_anything_ai::AiHub;
+use craft_anything_engine::{Engine, Session, Settings};
+use craft_anything_ui_egui::CraftApp;
+use std::path::PathBuf;
+use std::sync::Arc;
+
+fn main() -> eframe::Result {
+ let args: Vec = std::env::args().skip(1).collect();
+ if args.iter().any(|a| a == "--version" || a == "-V") {
+ println!("rei-reverse-engineer {}", env!("CARGO_PKG_VERSION"));
+ return Ok(());
+ }
+ // A path on the command line (or from "Open with…") opens as the first target.
+ let open: Option = args.iter().find(|a| !a.starts_with('-')).map(PathBuf::from);
+ // The window app has no console: what goes wrong before (or instead of) a window is kept in
+ // desktop.log beside the settings, so a start that shows nothing leaves a trace.
+ log_to_file();
+ log::info!("starting {} (pid {})", env!("CARGO_PKG_VERSION"), std::process::id());
+
+ let settings = Settings::load();
+ let mut session = Session::new(settings);
+ let plugins = craft_anything_engine::plugins::install(&mut session);
+ let mut layer_tools = craft_anything_ai::tools();
+ layer_tools.extend(craft_anything_setup::tools::specs());
+ let engine = Engine::shared_with(session, layer_tools, plugins);
+ let hub = Arc::new(AiHub::new(engine.clone()));
+ log::info!("engine and AI ready; opening the window");
+
+ let viewport = eframe::egui::ViewportBuilder::default()
+ .with_title("REI Reverse Engineer")
+ .with_app_id("com.rootcastle.rei")
+ .with_inner_size([1360.0, 860.0])
+ .with_min_inner_size([760.0, 520.0])
+ // Our own title bar draws the window controls, moves and resizes the window.
+ .with_decorations(false)
+ .with_resizable(true)
+ .with_drag_and_drop(true);
+ // On macOS a window icon replaces the Dock icon, and the bundle's own icon is the one padded to
+ // Apple's icon grid (the full tile looked bigger than other apps', #102). An empty icon keeps
+ // the bundle's; no icon at all would bring eframe's own.
+ let icon = if cfg!(target_os = "macos") { None } else { eframe::icon_data::from_png_bytes(craft_anything_ui_egui::APP_ICON_PNG).ok() };
+ let viewport = viewport.with_icon(Arc::new(icon.unwrap_or_default()));
+ #[cfg_attr(not(windows), expect(unused_mut, reason = "only Windows picks its own graphics adapter"))]
+ let mut options = eframe::NativeOptions { viewport, ..Default::default() };
+ #[cfg(windows)]
+ if let eframe::egui_wgpu::WgpuSetup::CreateNew(setup) = &mut options.wgpu_options.wgpu_setup {
+ // Starting GL loads the system's OpenGL driver before any adapter is picked, and some
+ // Intel drivers crash in it (ig9icd64.dll, #196), so the window never opens. Similarly,
+ // starting Vulkan loads Intel's Vulkan driver (igvk64.dll, #394) and crashes before any
+ // window. DX12 (with WARP) covers all Windows systems; `WGPU_BACKEND` still asks for others.
+ setup.instance_descriptor.backends = windows_backends(eframe::wgpu::Backends::from_env());
+ setup.native_adapter_selector = Some(Arc::new(pick_adapter));
+ }
+ let run = eframe::run_native(
+ "REI Reverse Engineer",
+ options,
+ Box::new(move |cc| {
+ log::info!("window open");
+ CraftApp::setup_context(&cc.egui_ctx);
+ let mut app = CraftApp::new(engine, hub);
+ app.load_prefs(cc.storage);
+ if let Some(p) = open {
+ app.open_path(p);
+ }
+ Ok(Box::new(app))
+ }),
+ );
+ match &run {
+ Ok(()) => log::info!("closed"),
+ Err(e) => {
+ log::error!("the window couldn't start: {e}");
+ // Without a window the app would just vanish (#166): say why, and where the log is.
+ let log = craft_anything_engine::settings::config_dir().join("desktop.log");
+ let _ = rfd::MessageDialog::new()
+ .set_level(rfd::MessageLevel::Error)
+ .set_title("REI Reverse Engineer couldn't open its window")
+ .set_description(format!(
+ "{e}\n\nUpdating your graphics driver often fixes this. What happened is written to {}; please attach it if you report this.",
+ log.display()
+ ))
+ .set_buttons(rfd::MessageButtons::Ok)
+ .show();
+ }
+ }
+ run
+}
+
+/// Windows: the adapter that draws the window. GL is the last resort, Windows' own software
+/// renderer (WARP) included: in a VMware VM, GL is the way to the virtual GPU and drew a black
+/// window (#27). The fastest device wins, and between same-class devices DX12 comes before Vulkan
+/// (#33). `WGPU_BACKEND` still narrows the choice.
+#[cfg(windows)]
+fn pick_adapter(adapters: &[eframe::wgpu::Adapter], surface: Option<&eframe::wgpu::Surface<'_>>) -> Result {
+ let picked = adapters.iter().filter(|a| surface.is_none_or(|s| a.is_surface_supported(s))).min_by_key(|a| {
+ let info = a.get_info();
+ adapter_rank(info.backend, info.device_type)
+ });
+ match picked {
+ Some(a) => {
+ log::info!("drawing with {:?}", a.get_info());
+ Ok(a.clone())
+ }
+ None => Err(format!("None of the {} graphics adapters found can draw this window.", adapters.len())),
+ }
+}
+
+/// Windows: the graphics backends to start. `WGPU_BACKEND` decides when set; otherwise DX12
+/// only (#196, #394). Vulkan and GL load third-party driver DLLs (igvk64.dll, ig9icd64.dll) before
+/// an adapter is chosen, which crash on some Intel systems. DX12 (with WARP) covers all Windows
+/// installations; `WGPU_BACKEND=vulkan` or `gl` still asks for them explicitly.
+#[cfg(windows)]
+fn windows_backends(from_env: Option) -> eframe::wgpu::Backends {
+ from_env.unwrap_or(eframe::wgpu::Backends::DX12)
+}
+
+/// Lower is better: GL last, fastest device first, DX12 before Vulkan only between same-class devices
+/// (#33).
+#[cfg(windows)]
+fn adapter_rank(backend: eframe::wgpu::Backend, device: eframe::wgpu::DeviceType) -> (bool, u8, u8) {
+ use eframe::wgpu::DeviceType;
+ let is_gl = backend == eframe::wgpu::Backend::Gl;
+ let speed = match device {
+ DeviceType::DiscreteGpu => 0,
+ DeviceType::IntegratedGpu => 1,
+ DeviceType::VirtualGpu => 2,
+ DeviceType::Cpu => 3,
+ DeviceType::Other => 4,
+ };
+ let dx12_before_vulkan = match backend {
+ eframe::wgpu::Backend::Dx12 => 0,
+ eframe::wgpu::Backend::Vulkan => 1,
+ _ => 2,
+ };
+ (is_gl, speed, dx12_before_vulkan)
+}
+
+#[cfg(all(test, windows))]
+mod tests {
+ use super::*;
+ use eframe::wgpu::{Backend as B, DeviceType as T};
+
+ /// #27: a VMware VM offers its virtual GPU over GL and WARP over DX12; GL drew a black window.
+ /// #33: a start that shows nothing is looked at through `desktop.log`; between same-class
+ /// devices DX12 comes before Vulkan, and GL stays the last resort.
+ #[test]
+ fn dx12_comes_before_vulkan_and_gl_is_the_last_resort() {
+ let best = |c: &[(B, T)]| c.iter().enumerate().min_by_key(|(_, (b, t))| adapter_rank(*b, *t)).map(|(i, _)| i);
+ assert_eq!(best(&[(B::Gl, T::Other), (B::Dx12, T::Cpu)]), Some(1), "VMware: WARP, not GL");
+ assert_eq!(best(&[(B::Gl, T::IntegratedGpu), (B::Dx12, T::Cpu)]), Some(1));
+ assert_eq!(best(&[(B::Vulkan, T::DiscreteGpu), (B::Dx12, T::IntegratedGpu)]), Some(0), "fastest GPU first");
+ assert_eq!(best(&[(B::Dx12, T::IntegratedGpu), (B::Vulkan, T::IntegratedGpu)]), Some(0), "DX12 before Vulkan between same-class devices");
+ assert_eq!(best(&[(B::Dx12, T::Cpu), (B::Vulkan, T::DiscreteGpu)]), Some(1), "WARP does not beat a real Vulkan device");
+ assert_eq!(best(&[(B::Dx12, T::IntegratedGpu), (B::Vulkan, T::DiscreteGpu), (B::Dx12, T::Cpu)]), Some(1), "fastest GPU first");
+ assert_eq!(best(&[(B::Gl, T::Other)]), Some(0), "GL when it's all there is");
+ }
+
+ /// #196: an Intel OpenGL driver crashed the app as wgpu started GL, before any window.
+ /// #394: an Intel Vulkan driver (igvk64.dll) crashed the app as wgpu started Vulkan.
+ #[test]
+ fn gl_and_vulkan_are_only_started_when_asked_for() {
+ use eframe::wgpu::Backends;
+ let started = windows_backends(None);
+ assert_eq!(started, Backends::DX12);
+ assert_eq!(windows_backends(Some(Backends::GL)), Backends::GL, "WGPU_BACKEND=gl still works");
+ assert_eq!(windows_backends(Some(Backends::VULKAN)), Backends::VULKAN, "WGPU_BACKEND=vulkan still works");
+ }
+}
+
+/// Kept small: started afresh once it passes this size.
+const LOG_MAX: u64 = 512 * 1024;
+
+/// Warnings and errors from everything, plus our own progress, appended to `desktop.log`; and
+/// panics, which would otherwise vanish with the window.
+fn log_to_file() {
+ use std::io::Write as _;
+ struct FileLog(craft_anything_platform::sync::Mutex>);
+ impl log::Log for FileLog {
+ fn enabled(&self, m: &log::Metadata<'_>) -> bool {
+ m.level() <= log::Level::Warn || (m.level() <= log::Level::Info && m.target().starts_with("craft_anything"))
+ }
+ fn log(&self, r: &log::Record<'_>) {
+ if !self.enabled(r.metadata()) {
+ return;
+ }
+ let at = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
+ if let Ok(mut f) = self.0.lock()
+ && let Some(f) = f.as_mut()
+ {
+ let _ = writeln!(f, "{at} [{}] {}: {}", r.level(), r.target(), r.args());
+ }
+ }
+ fn flush(&self) {}
+ }
+ let dir = craft_anything_engine::settings::config_dir();
+ let path = dir.join("desktop.log");
+ let _ = std::fs::create_dir_all(&dir);
+ let fresh = std::fs::metadata(&path).is_ok_and(|m| m.len() > LOG_MAX);
+ let file = std::fs::OpenOptions::new().create(true).append(!fresh).write(true).truncate(fresh).open(&path).ok();
+ if log::set_logger(Box::leak(Box::new(FileLog(craft_anything_platform::sync::Mutex::new(file))))).is_ok() {
+ log::set_max_level(log::LevelFilter::Info);
+ }
+ let default_hook = std::panic::take_hook();
+ std::panic::set_hook(Box::new(move |info| {
+ log::error!("panic: {info}");
+ default_hook(info);
+ }));
+ craft_anything_platform::sync::start_watchdog(craft_anything_engine::settings::config_dir().join("logs"));
+}
diff --git a/desktop/assets/app-icon/LICENSE.txt b/desktop/assets/app-icon/LICENSE.txt
new file mode 100644
index 0000000..838912d
--- /dev/null
+++ b/desktop/assets/app-icon/LICENSE.txt
@@ -0,0 +1,10 @@
+Craft Anything app icon (craft-anything.svg and every file derived from it in this
+directory: craft-anything-1024.png, craft-anything.icns, craft-anything.ico and
+hicolor/).
+
+This is the raven, the Crafting Apps' shared icon, made by the ArtCraft team. It is NOT covered
+by Craft Anything's MIT OR Apache-2.0 license. It is used on the same terms as the ArtCraft
+brand assets in docs/brand/LICENSE-brand.txt, which also say what forks and modified versions
+must do: replace it with their own icon.
+
+trace.py is a tool, covered by the repository's MIT OR Apache-2.0 license.
diff --git a/desktop/assets/app-icon/README.md b/desktop/assets/app-icon/README.md
new file mode 100644
index 0000000..05a7168
--- /dev/null
+++ b/desktop/assets/app-icon/README.md
@@ -0,0 +1,37 @@
+# Craft Anything app icon
+
+The **raven**: the Crafting Apps' shared icon, made by the ArtCraft team. An engraved raven head
+in getartcraft.com's ink `#101014` with paper `#f2f1ee` highlights, on a rounded tile in
+ArtCraft's mark blue `#4e7bfb`. It is not open source: see `LICENSE.txt`.
+
+The interface follows getartcraft.com too (`theme.rs`): its warm near-black and paper
+backgrounds and ink, its deep accent `#1659c4` (`theme::BRAND`) for filled controls so white text
+on them passes WCAG AA, its bright accent `#2d81ff` for hover and its light accent `#74aaff` for
+blue text on dark surfaces. Headings and text are DM Sans.
+
+## Geometry
+
+A 512-unit tile with 110-unit corners (the source's 21.5 %), the raven clipped to it. The macOS
+icons pad the tile onto Apple's 824/1024 icon grid; every other render uses the full tile.
+
+## Files
+
+- `craft-anything.svg`: the master SVG (also the hicolor scalable icon), about 100 KB.
+- `craft-anything-1024.png`: 1024 px render of the tile.
+- `craft-anything.icns`: macOS bundle icon.
+- `craft-anything.ico`: Windows icon (16, 20, 24, 32, 40, 48, 64, 128, 256).
+- `hicolor//apps/ai.storyteller.craft-anything.png` (16–512) and
+ `hicolor/scalable/apps/ai.storyteller.craft-anything.svg`: Linux icon theme.
+- `trace.py`: traces the raven artwork (a 2000 px raster) into the SVG with vtracer.
+- `packaging/icons.sh`: makes every other file above from the SVG. The app draws the SVG itself
+ at the exact pixel size (title bar, welcome screen); the window icon is the 256 px PNG.
+
+## Regenerate
+
+```sh
+# From the 2000 px raven artwork (needs numpy, Pillow and vtracer; svgo 3.3.2 shrinks the paths):
+python trace.py raven.webp master.svg 1000 6 0
+npx svgo@3.3.2 -p 1 master.svg -o craft-anything.svg
+# Every render (needs resvg on PATH; the .icns needs macOS's iconutil):
+packaging/icons.sh
+```
diff --git a/desktop/assets/app-icon/craft-anything-1024.png b/desktop/assets/app-icon/craft-anything-1024.png
new file mode 100644
index 0000000..08888f5
Binary files /dev/null and b/desktop/assets/app-icon/craft-anything-1024.png differ
diff --git a/desktop/assets/app-icon/craft-anything.icns b/desktop/assets/app-icon/craft-anything.icns
new file mode 100644
index 0000000..b9f3c14
Binary files /dev/null and b/desktop/assets/app-icon/craft-anything.icns differ
diff --git a/desktop/assets/app-icon/craft-anything.ico b/desktop/assets/app-icon/craft-anything.ico
new file mode 100644
index 0000000..3457716
Binary files /dev/null and b/desktop/assets/app-icon/craft-anything.ico differ
diff --git a/desktop/assets/app-icon/craft-anything.svg b/desktop/assets/app-icon/craft-anything.svg
new file mode 100644
index 0000000..1bf9bb0
--- /dev/null
+++ b/desktop/assets/app-icon/craft-anything.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/desktop/assets/app-icon/hicolor/128x128/apps/ai.storyteller.craft-anything.png b/desktop/assets/app-icon/hicolor/128x128/apps/ai.storyteller.craft-anything.png
new file mode 100644
index 0000000..fd44e08
Binary files /dev/null and b/desktop/assets/app-icon/hicolor/128x128/apps/ai.storyteller.craft-anything.png differ
diff --git a/desktop/assets/app-icon/hicolor/16x16/apps/ai.storyteller.craft-anything.png b/desktop/assets/app-icon/hicolor/16x16/apps/ai.storyteller.craft-anything.png
new file mode 100644
index 0000000..ea7f7c9
Binary files /dev/null and b/desktop/assets/app-icon/hicolor/16x16/apps/ai.storyteller.craft-anything.png differ
diff --git a/desktop/assets/app-icon/hicolor/24x24/apps/ai.storyteller.craft-anything.png b/desktop/assets/app-icon/hicolor/24x24/apps/ai.storyteller.craft-anything.png
new file mode 100644
index 0000000..b77ac35
Binary files /dev/null and b/desktop/assets/app-icon/hicolor/24x24/apps/ai.storyteller.craft-anything.png differ
diff --git a/desktop/assets/app-icon/hicolor/256x256/apps/ai.storyteller.craft-anything.png b/desktop/assets/app-icon/hicolor/256x256/apps/ai.storyteller.craft-anything.png
new file mode 100644
index 0000000..26cd12d
Binary files /dev/null and b/desktop/assets/app-icon/hicolor/256x256/apps/ai.storyteller.craft-anything.png differ
diff --git a/desktop/assets/app-icon/hicolor/256x256/apps/com.rootcastle.rei.png b/desktop/assets/app-icon/hicolor/256x256/apps/com.rootcastle.rei.png
new file mode 100644
index 0000000..26cd12d
Binary files /dev/null and b/desktop/assets/app-icon/hicolor/256x256/apps/com.rootcastle.rei.png differ
diff --git a/desktop/assets/app-icon/hicolor/32x32/apps/ai.storyteller.craft-anything.png b/desktop/assets/app-icon/hicolor/32x32/apps/ai.storyteller.craft-anything.png
new file mode 100644
index 0000000..3ef5b2b
Binary files /dev/null and b/desktop/assets/app-icon/hicolor/32x32/apps/ai.storyteller.craft-anything.png differ
diff --git a/desktop/assets/app-icon/hicolor/48x48/apps/ai.storyteller.craft-anything.png b/desktop/assets/app-icon/hicolor/48x48/apps/ai.storyteller.craft-anything.png
new file mode 100644
index 0000000..b3e1434
Binary files /dev/null and b/desktop/assets/app-icon/hicolor/48x48/apps/ai.storyteller.craft-anything.png differ
diff --git a/desktop/assets/app-icon/hicolor/512x512/apps/ai.storyteller.craft-anything.png b/desktop/assets/app-icon/hicolor/512x512/apps/ai.storyteller.craft-anything.png
new file mode 100644
index 0000000..41523a9
Binary files /dev/null and b/desktop/assets/app-icon/hicolor/512x512/apps/ai.storyteller.craft-anything.png differ
diff --git a/desktop/assets/app-icon/hicolor/64x64/apps/ai.storyteller.craft-anything.png b/desktop/assets/app-icon/hicolor/64x64/apps/ai.storyteller.craft-anything.png
new file mode 100644
index 0000000..4d9160f
Binary files /dev/null and b/desktop/assets/app-icon/hicolor/64x64/apps/ai.storyteller.craft-anything.png differ
diff --git a/desktop/assets/app-icon/hicolor/scalable/apps/ai.storyteller.craft-anything.svg b/desktop/assets/app-icon/hicolor/scalable/apps/ai.storyteller.craft-anything.svg
new file mode 100644
index 0000000..dd82d55
--- /dev/null
+++ b/desktop/assets/app-icon/hicolor/scalable/apps/ai.storyteller.craft-anything.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/desktop/assets/app-icon/trace.py b/desktop/assets/app-icon/trace.py
new file mode 100644
index 0000000..bf79ed7
--- /dev/null
+++ b/desktop/assets/app-icon/trace.py
@@ -0,0 +1,60 @@
+"""Trace the raven artwork into the app icon's master SVG.
+
+Needs numpy, Pillow and vtracer (`pip install vtracer`); see README.md for the exact runs.
+usage: python trace.py SOURCE OUT_SVG WORK_PX SPECKLE BLUR
+"""
+
+import re
+import sys
+import tempfile
+from pathlib import Path
+
+import numpy as np
+import vtracer
+from PIL import Image, ImageFilter
+
+INK, PAPER, BLUE = "#101014", "#f2f1ee", "#4e7bfb"
+
+
+def trace(mask: np.ndarray, speckle: int, d: Path) -> str:
+ """One colour layer: vtracer's binary trace of `mask`, as path elements."""
+ png, svg = d / "mask.png", d / "mask.svg"
+ Image.fromarray(np.where(mask, 0, 255).astype(np.uint8)).filter(ImageFilter.MedianFilter(3)).save(png)
+ vtracer.convert_image_to_svg_py(
+ str(png), str(svg), colormode="binary", mode="spline", filter_speckle=speckle,
+ corner_threshold=60, length_threshold=4.0, splice_threshold=45, path_precision=1,
+ )
+ found = re.findall(r']*transform="translate\(([-\d.]+),([-\d.]+)\)"', svg.read_text())
+ return "".join(f' ' for p, x, y in found)
+
+
+def main() -> None:
+ src, out, n, speckle, blur = sys.argv[1], Path(sys.argv[2]), int(sys.argv[3]), int(sys.argv[4]), float(sys.argv[5])
+ im = Image.open(src).convert("RGB").resize((n, n), Image.LANCZOS)
+ if blur:
+ im = im.filter(ImageFilter.GaussianBlur(blur))
+ a = np.asarray(im).astype(np.int32)
+ r, g, b = a[..., 0], a[..., 1], a[..., 2]
+ blue = (b - r > 70) & (b > 150)
+ dark = 0.299 * r + 0.587 * g + 0.114 * b < 128
+
+ # Only what lies inside the source's rounded tile (21.5 % corner radius).
+ rad = 0.215 * n
+ yy, xx = np.mgrid[0:n, 0:n] + 0.5
+ inside = (xx - np.clip(xx, rad, n - rad)) ** 2 + (yy - np.clip(yy, rad, n - rad)) ** 2 <= (rad - 2) ** 2
+
+ with tempfile.TemporaryDirectory() as d:
+ bird = trace(inside & ~blue, speckle, Path(d))
+ ink = trace(inside & ~blue & dark, speckle, Path(d))
+ out.write_text(
+ ''
+ ' '
+ f' '
+ f''
+ f'{bird} {ink} \n',
+ encoding="utf-8",
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/desktop/assets/fonts/DMSans-Bold.ttf b/desktop/assets/fonts/DMSans-Bold.ttf
new file mode 100644
index 0000000..32bc5df
Binary files /dev/null and b/desktop/assets/fonts/DMSans-Bold.ttf differ
diff --git a/desktop/assets/fonts/DMSans-Medium.ttf b/desktop/assets/fonts/DMSans-Medium.ttf
new file mode 100644
index 0000000..fc8a5eb
Binary files /dev/null and b/desktop/assets/fonts/DMSans-Medium.ttf differ
diff --git a/desktop/assets/fonts/DMSans-Regular.ttf b/desktop/assets/fonts/DMSans-Regular.ttf
new file mode 100644
index 0000000..40c9bfd
Binary files /dev/null and b/desktop/assets/fonts/DMSans-Regular.ttf differ
diff --git a/desktop/assets/fonts/GeistMono-Medium.ttf b/desktop/assets/fonts/GeistMono-Medium.ttf
new file mode 100644
index 0000000..0970296
Binary files /dev/null and b/desktop/assets/fonts/GeistMono-Medium.ttf differ
diff --git a/desktop/assets/fonts/GeistMono-Regular.ttf b/desktop/assets/fonts/GeistMono-Regular.ttf
new file mode 100644
index 0000000..643e6ba
Binary files /dev/null and b/desktop/assets/fonts/GeistMono-Regular.ttf differ
diff --git a/desktop/assets/fonts/JetBrainsMono-Regular.ttf b/desktop/assets/fonts/JetBrainsMono-Regular.ttf
new file mode 100644
index 0000000..711830e
Binary files /dev/null and b/desktop/assets/fonts/JetBrainsMono-Regular.ttf differ
diff --git a/desktop/assets/fonts/OFL-DMSans.txt b/desktop/assets/fonts/OFL-DMSans.txt
new file mode 100644
index 0000000..a6fa62b
--- /dev/null
+++ b/desktop/assets/fonts/OFL-DMSans.txt
@@ -0,0 +1,93 @@
+Copyright 2014-2017 Indian Type Foundry (info@indiantypefoundry.com). Copyright 2019 Google LLC.
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/desktop/assets/fonts/OFL-GeistMono.txt b/desktop/assets/fonts/OFL-GeistMono.txt
new file mode 100644
index 0000000..04e95fc
--- /dev/null
+++ b/desktop/assets/fonts/OFL-GeistMono.txt
@@ -0,0 +1,93 @@
+Copyright 2024 The Geist Project Authors (https://github.com/vercel/geist-font)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+https://openfontlicense.org
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
\ No newline at end of file
diff --git a/desktop/assets/fonts/OFL-JetBrainsMono.txt b/desktop/assets/fonts/OFL-JetBrainsMono.txt
new file mode 100644
index 0000000..5ceee00
--- /dev/null
+++ b/desktop/assets/fonts/OFL-JetBrainsMono.txt
@@ -0,0 +1,93 @@
+Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+https://openfontlicense.org
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/desktop/assets/icons/LICENSE-lucide.txt b/desktop/assets/icons/LICENSE-lucide.txt
new file mode 100644
index 0000000..718bb3f
--- /dev/null
+++ b/desktop/assets/icons/LICENSE-lucide.txt
@@ -0,0 +1,43 @@
+ISC License
+
+Copyright (c) 2026 Lucide Icons and Contributors
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+---
+
+The following Lucide icons are derived from the Feather project:
+
+airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
+
+The MIT License (MIT) (for the icons listed above)
+
+Copyright (c) 2013-present Cole Bemis
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/desktop/assets/icons/activity.svg b/desktop/assets/icons/activity.svg
new file mode 100644
index 0000000..c0c4762
--- /dev/null
+++ b/desktop/assets/icons/activity.svg
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/desktop/assets/icons/app-window.svg b/desktop/assets/icons/app-window.svg
new file mode 100644
index 0000000..67b7295
--- /dev/null
+++ b/desktop/assets/icons/app-window.svg
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/desktop/assets/icons/binary.svg b/desktop/assets/icons/binary.svg
new file mode 100644
index 0000000..fece4f0
--- /dev/null
+++ b/desktop/assets/icons/binary.svg
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
diff --git a/desktop/assets/icons/book-open.svg b/desktop/assets/icons/book-open.svg
new file mode 100644
index 0000000..53554d0
--- /dev/null
+++ b/desktop/assets/icons/book-open.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/desktop/assets/icons/bot.svg b/desktop/assets/icons/bot.svg
new file mode 100644
index 0000000..5fe0569
--- /dev/null
+++ b/desktop/assets/icons/bot.svg
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
diff --git a/desktop/assets/icons/boxes.svg b/desktop/assets/icons/boxes.svg
new file mode 100644
index 0000000..a887627
--- /dev/null
+++ b/desktop/assets/icons/boxes.svg
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/desktop/assets/icons/braces.svg b/desktop/assets/icons/braces.svg
new file mode 100644
index 0000000..8cd03c9
--- /dev/null
+++ b/desktop/assets/icons/braces.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/desktop/assets/icons/bug.svg b/desktop/assets/icons/bug.svg
new file mode 100644
index 0000000..17ebc40
--- /dev/null
+++ b/desktop/assets/icons/bug.svg
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/desktop/assets/icons/cable.svg b/desktop/assets/icons/cable.svg
new file mode 100644
index 0000000..68358dc
--- /dev/null
+++ b/desktop/assets/icons/cable.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
diff --git a/desktop/assets/icons/check.svg b/desktop/assets/icons/check.svg
new file mode 100644
index 0000000..307f4d6
--- /dev/null
+++ b/desktop/assets/icons/check.svg
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/desktop/assets/icons/chevron-down.svg b/desktop/assets/icons/chevron-down.svg
new file mode 100644
index 0000000..621978a
--- /dev/null
+++ b/desktop/assets/icons/chevron-down.svg
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/desktop/assets/icons/chevron-right.svg b/desktop/assets/icons/chevron-right.svg
new file mode 100644
index 0000000..379a56f
--- /dev/null
+++ b/desktop/assets/icons/chevron-right.svg
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/desktop/assets/icons/chevron-up.svg b/desktop/assets/icons/chevron-up.svg
new file mode 100644
index 0000000..5cd5fe0
--- /dev/null
+++ b/desktop/assets/icons/chevron-up.svg
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/desktop/assets/icons/circle-help.svg b/desktop/assets/icons/circle-help.svg
new file mode 100644
index 0000000..98d2fa0
--- /dev/null
+++ b/desktop/assets/icons/circle-help.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/desktop/assets/icons/circle-stop.svg b/desktop/assets/icons/circle-stop.svg
new file mode 100644
index 0000000..8af9a48
--- /dev/null
+++ b/desktop/assets/icons/circle-stop.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/desktop/assets/icons/clipboard-list.svg b/desktop/assets/icons/clipboard-list.svg
new file mode 100644
index 0000000..8c1e000
--- /dev/null
+++ b/desktop/assets/icons/clipboard-list.svg
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
diff --git a/desktop/assets/icons/code.svg b/desktop/assets/icons/code.svg
new file mode 100644
index 0000000..4494eea
--- /dev/null
+++ b/desktop/assets/icons/code.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/desktop/assets/icons/copy.svg b/desktop/assets/icons/copy.svg
new file mode 100644
index 0000000..f60b2a5
--- /dev/null
+++ b/desktop/assets/icons/copy.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/desktop/assets/icons/cpu.svg b/desktop/assets/icons/cpu.svg
new file mode 100644
index 0000000..a48a600
--- /dev/null
+++ b/desktop/assets/icons/cpu.svg
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/desktop/assets/icons/database.svg b/desktop/assets/icons/database.svg
new file mode 100644
index 0000000..c48eab1
--- /dev/null
+++ b/desktop/assets/icons/database.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/desktop/assets/icons/download.svg b/desktop/assets/icons/download.svg
new file mode 100644
index 0000000..c8882d7
--- /dev/null
+++ b/desktop/assets/icons/download.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/desktop/assets/icons/external-link.svg b/desktop/assets/icons/external-link.svg
new file mode 100644
index 0000000..4dda110
--- /dev/null
+++ b/desktop/assets/icons/external-link.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/desktop/assets/icons/eye-off.svg b/desktop/assets/icons/eye-off.svg
new file mode 100644
index 0000000..42374c6
--- /dev/null
+++ b/desktop/assets/icons/eye-off.svg
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/desktop/assets/icons/eye.svg b/desktop/assets/icons/eye.svg
new file mode 100644
index 0000000..506bce2
--- /dev/null
+++ b/desktop/assets/icons/eye.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/desktop/assets/icons/file-code.svg b/desktop/assets/icons/file-code.svg
new file mode 100644
index 0000000..b17ab09
--- /dev/null
+++ b/desktop/assets/icons/file-code.svg
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/desktop/assets/icons/file.svg b/desktop/assets/icons/file.svg
new file mode 100644
index 0000000..1e60bf6
--- /dev/null
+++ b/desktop/assets/icons/file.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/desktop/assets/icons/fingerprint.svg b/desktop/assets/icons/fingerprint.svg
new file mode 100644
index 0000000..af07df6
--- /dev/null
+++ b/desktop/assets/icons/fingerprint.svg
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/desktop/assets/icons/flask-conical.svg b/desktop/assets/icons/flask-conical.svg
new file mode 100644
index 0000000..7532ad4
--- /dev/null
+++ b/desktop/assets/icons/flask-conical.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/desktop/assets/icons/folder-open.svg b/desktop/assets/icons/folder-open.svg
new file mode 100644
index 0000000..b8671ec
--- /dev/null
+++ b/desktop/assets/icons/folder-open.svg
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/desktop/assets/icons/folder.svg b/desktop/assets/icons/folder.svg
new file mode 100644
index 0000000..199656a
--- /dev/null
+++ b/desktop/assets/icons/folder.svg
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/desktop/assets/icons/gauge.svg b/desktop/assets/icons/gauge.svg
new file mode 100644
index 0000000..cd9f61a
--- /dev/null
+++ b/desktop/assets/icons/gauge.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/desktop/assets/icons/git-branch.svg b/desktop/assets/icons/git-branch.svg
new file mode 100644
index 0000000..0ef9c57
--- /dev/null
+++ b/desktop/assets/icons/git-branch.svg
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/desktop/assets/icons/globe.svg b/desktop/assets/icons/globe.svg
new file mode 100644
index 0000000..b0dfbc6
--- /dev/null
+++ b/desktop/assets/icons/globe.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/desktop/assets/icons/graduation-cap.svg b/desktop/assets/icons/graduation-cap.svg
new file mode 100644
index 0000000..991b7a2
--- /dev/null
+++ b/desktop/assets/icons/graduation-cap.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/desktop/assets/icons/hammer.svg b/desktop/assets/icons/hammer.svg
new file mode 100644
index 0000000..8712f50
--- /dev/null
+++ b/desktop/assets/icons/hammer.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/desktop/assets/icons/hard-drive.svg b/desktop/assets/icons/hard-drive.svg
new file mode 100644
index 0000000..c8dd7ce
--- /dev/null
+++ b/desktop/assets/icons/hard-drive.svg
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/desktop/assets/icons/history.svg b/desktop/assets/icons/history.svg
new file mode 100644
index 0000000..ea6648a
--- /dev/null
+++ b/desktop/assets/icons/history.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/desktop/assets/icons/house.svg b/desktop/assets/icons/house.svg
new file mode 100644
index 0000000..8f45941
--- /dev/null
+++ b/desktop/assets/icons/house.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/desktop/assets/icons/info.svg b/desktop/assets/icons/info.svg
new file mode 100644
index 0000000..e1a2b35
--- /dev/null
+++ b/desktop/assets/icons/info.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/desktop/assets/icons/key-round.svg b/desktop/assets/icons/key-round.svg
new file mode 100644
index 0000000..7fb3049
--- /dev/null
+++ b/desktop/assets/icons/key-round.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/desktop/assets/icons/layers.svg b/desktop/assets/icons/layers.svg
new file mode 100644
index 0000000..4cc41fa
--- /dev/null
+++ b/desktop/assets/icons/layers.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/desktop/assets/icons/link.svg b/desktop/assets/icons/link.svg
new file mode 100644
index 0000000..0b5fa87
--- /dev/null
+++ b/desktop/assets/icons/link.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/desktop/assets/icons/list-checks.svg b/desktop/assets/icons/list-checks.svg
new file mode 100644
index 0000000..c9eca6b
--- /dev/null
+++ b/desktop/assets/icons/list-checks.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
diff --git a/desktop/assets/icons/list-tree.svg b/desktop/assets/icons/list-tree.svg
new file mode 100644
index 0000000..a4c66c8
--- /dev/null
+++ b/desktop/assets/icons/list-tree.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
diff --git a/desktop/assets/icons/loader.svg b/desktop/assets/icons/loader.svg
new file mode 100644
index 0000000..977e7dc
--- /dev/null
+++ b/desktop/assets/icons/loader.svg
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/desktop/assets/icons/lock-open.svg b/desktop/assets/icons/lock-open.svg
new file mode 100644
index 0000000..9652624
--- /dev/null
+++ b/desktop/assets/icons/lock-open.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/desktop/assets/icons/lock.svg b/desktop/assets/icons/lock.svg
new file mode 100644
index 0000000..93cb090
--- /dev/null
+++ b/desktop/assets/icons/lock.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/desktop/assets/icons/message-square.svg b/desktop/assets/icons/message-square.svg
new file mode 100644
index 0000000..9ca1b67
--- /dev/null
+++ b/desktop/assets/icons/message-square.svg
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/desktop/assets/icons/microscope.svg b/desktop/assets/icons/microscope.svg
new file mode 100644
index 0000000..ca36eb4
--- /dev/null
+++ b/desktop/assets/icons/microscope.svg
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
diff --git a/desktop/assets/icons/minus.svg b/desktop/assets/icons/minus.svg
new file mode 100644
index 0000000..d9866f4
--- /dev/null
+++ b/desktop/assets/icons/minus.svg
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/desktop/assets/icons/monitor.svg b/desktop/assets/icons/monitor.svg
new file mode 100644
index 0000000..b05caa8
--- /dev/null
+++ b/desktop/assets/icons/monitor.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/desktop/assets/icons/moon.svg b/desktop/assets/icons/moon.svg
new file mode 100644
index 0000000..014a893
--- /dev/null
+++ b/desktop/assets/icons/moon.svg
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/desktop/assets/icons/network.svg b/desktop/assets/icons/network.svg
new file mode 100644
index 0000000..2292bef
--- /dev/null
+++ b/desktop/assets/icons/network.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
diff --git a/desktop/assets/icons/package.svg b/desktop/assets/icons/package.svg
new file mode 100644
index 0000000..f302729
--- /dev/null
+++ b/desktop/assets/icons/package.svg
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/desktop/assets/icons/paperclip.svg b/desktop/assets/icons/paperclip.svg
new file mode 100644
index 0000000..b51e05c
--- /dev/null
+++ b/desktop/assets/icons/paperclip.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/desktop/assets/icons/pin.svg b/desktop/assets/icons/pin.svg
new file mode 100644
index 0000000..0f7984f
--- /dev/null
+++ b/desktop/assets/icons/pin.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/desktop/assets/icons/play.svg b/desktop/assets/icons/play.svg
new file mode 100644
index 0000000..0e272f3
--- /dev/null
+++ b/desktop/assets/icons/play.svg
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/desktop/assets/icons/plus.svg b/desktop/assets/icons/plus.svg
new file mode 100644
index 0000000..9def62b
--- /dev/null
+++ b/desktop/assets/icons/plus.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/desktop/assets/icons/puzzle.svg b/desktop/assets/icons/puzzle.svg
new file mode 100644
index 0000000..a130963
--- /dev/null
+++ b/desktop/assets/icons/puzzle.svg
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/desktop/assets/icons/radar.svg b/desktop/assets/icons/radar.svg
new file mode 100644
index 0000000..d574642
--- /dev/null
+++ b/desktop/assets/icons/radar.svg
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/desktop/assets/icons/refresh-cw.svg b/desktop/assets/icons/refresh-cw.svg
new file mode 100644
index 0000000..510829a
--- /dev/null
+++ b/desktop/assets/icons/refresh-cw.svg
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/desktop/assets/icons/rocket.svg b/desktop/assets/icons/rocket.svg
new file mode 100644
index 0000000..9d65baa
--- /dev/null
+++ b/desktop/assets/icons/rocket.svg
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/desktop/assets/icons/scan-search.svg b/desktop/assets/icons/scan-search.svg
new file mode 100644
index 0000000..8e41533
--- /dev/null
+++ b/desktop/assets/icons/scan-search.svg
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
diff --git a/desktop/assets/icons/search.svg b/desktop/assets/icons/search.svg
new file mode 100644
index 0000000..1523bcc
--- /dev/null
+++ b/desktop/assets/icons/search.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/desktop/assets/icons/send.svg b/desktop/assets/icons/send.svg
new file mode 100644
index 0000000..2c2aa68
--- /dev/null
+++ b/desktop/assets/icons/send.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/desktop/assets/icons/server.svg b/desktop/assets/icons/server.svg
new file mode 100644
index 0000000..4b4e050
--- /dev/null
+++ b/desktop/assets/icons/server.svg
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/desktop/assets/icons/settings.svg b/desktop/assets/icons/settings.svg
new file mode 100644
index 0000000..4043d11
--- /dev/null
+++ b/desktop/assets/icons/settings.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/desktop/assets/icons/shield-check.svg b/desktop/assets/icons/shield-check.svg
new file mode 100644
index 0000000..def2775
--- /dev/null
+++ b/desktop/assets/icons/shield-check.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/desktop/assets/icons/shield.svg b/desktop/assets/icons/shield.svg
new file mode 100644
index 0000000..4551537
--- /dev/null
+++ b/desktop/assets/icons/shield.svg
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/desktop/assets/icons/smartphone.svg b/desktop/assets/icons/smartphone.svg
new file mode 100644
index 0000000..918d285
--- /dev/null
+++ b/desktop/assets/icons/smartphone.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/desktop/assets/icons/sparkles.svg b/desktop/assets/icons/sparkles.svg
new file mode 100644
index 0000000..2d0898c
--- /dev/null
+++ b/desktop/assets/icons/sparkles.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
diff --git a/desktop/assets/icons/square.svg b/desktop/assets/icons/square.svg
new file mode 100644
index 0000000..3eb1089
--- /dev/null
+++ b/desktop/assets/icons/square.svg
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/desktop/assets/icons/sun.svg b/desktop/assets/icons/sun.svg
new file mode 100644
index 0000000..758c982
--- /dev/null
+++ b/desktop/assets/icons/sun.svg
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/desktop/assets/icons/terminal.svg b/desktop/assets/icons/terminal.svg
new file mode 100644
index 0000000..a084744
--- /dev/null
+++ b/desktop/assets/icons/terminal.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/desktop/assets/icons/trash-2.svg b/desktop/assets/icons/trash-2.svg
new file mode 100644
index 0000000..dff3fc7
--- /dev/null
+++ b/desktop/assets/icons/trash-2.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
diff --git a/desktop/assets/icons/triangle-alert.svg b/desktop/assets/icons/triangle-alert.svg
new file mode 100644
index 0000000..df330c2
--- /dev/null
+++ b/desktop/assets/icons/triangle-alert.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/desktop/assets/icons/upload.svg b/desktop/assets/icons/upload.svg
new file mode 100644
index 0000000..783a01b
--- /dev/null
+++ b/desktop/assets/icons/upload.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/desktop/assets/icons/user.svg b/desktop/assets/icons/user.svg
new file mode 100644
index 0000000..6965bbb
--- /dev/null
+++ b/desktop/assets/icons/user.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/desktop/assets/icons/wand-sparkles.svg b/desktop/assets/icons/wand-sparkles.svg
new file mode 100644
index 0000000..623126f
--- /dev/null
+++ b/desktop/assets/icons/wand-sparkles.svg
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/desktop/assets/icons/workflow.svg b/desktop/assets/icons/workflow.svg
new file mode 100644
index 0000000..ced0167
--- /dev/null
+++ b/desktop/assets/icons/workflow.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/desktop/assets/icons/wrench.svg b/desktop/assets/icons/wrench.svg
new file mode 100644
index 0000000..a162618
--- /dev/null
+++ b/desktop/assets/icons/wrench.svg
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/desktop/assets/icons/x.svg b/desktop/assets/icons/x.svg
new file mode 100644
index 0000000..801c131
--- /dev/null
+++ b/desktop/assets/icons/x.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/desktop/assets/icons/zap.svg b/desktop/assets/icons/zap.svg
new file mode 100644
index 0000000..9ed5cd0
--- /dev/null
+++ b/desktop/assets/icons/zap.svg
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/desktop/clippy.toml b/desktop/clippy.toml
new file mode 100644
index 0000000..66227fb
--- /dev/null
+++ b/desktop/clippy.toml
@@ -0,0 +1,10 @@
+allow-unwrap-in-tests = true
+allow-expect-in-tests = true
+allow-panic-in-tests = true
+allow-indexing-slicing-in-tests = true
+
+# Locks must be able to notice a deadlock and get out of it; see crates/platform/src/sync.rs.
+disallowed-types = [
+ { path = "std::sync::Mutex", reason = "use craft_anything_platform::sync::Mutex, which can't hang" },
+ { path = "std::sync::MutexGuard", reason = "use craft_anything_platform::sync::MutexGuard" },
+]
diff --git a/desktop/crates/ai/Cargo.toml b/desktop/crates/ai/Cargo.toml
new file mode 100644
index 0000000..b34a2f5
--- /dev/null
+++ b/desktop/crates/ai/Cargo.toml
@@ -0,0 +1,37 @@
+[package]
+name = "craft-anything-ai"
+description = "AI backends, the agent loop and crews for Craft Anything"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+publish = false
+
+[lints]
+workspace = true
+
+[dependencies]
+schemars = { workspace = true }
+craft-anything-evidence = { workspace = true }
+craft-anything-engine = { workspace = true }
+craft-anything-engines = { workspace = true }
+craft-anything-formats = { workspace = true }
+craft-anything-platform = { workspace = true }
+serde = { workspace = true }
+serde_json = { workspace = true }
+log = { workspace = true }
+toml = { workspace = true }
+ureq = "3"
+dirs = "6"
+
+[target.'cfg(target_os = "linux")'.dependencies]
+keyring = { version = "3", default-features = false, features = ["async-secret-service", "async-io", "crypto-rust"] }
+
+[target.'cfg(target_os = "macos")'.dependencies]
+keyring = { version = "3", default-features = false, features = ["apple-native"] }
+
+[target.'cfg(windows)'.dependencies]
+keyring = { version = "3", default-features = false, features = ["windows-native"] }
+
+[dev-dependencies]
+tempfile = "3"
+craft-anything-testkit = { workspace = true }
diff --git a/desktop/crates/ai/src/agent.rs b/desktop/crates/ai/src/agent.rs
new file mode 100644
index 0000000..cfbf958
--- /dev/null
+++ b/desktop/crates/ai/src/agent.rs
@@ -0,0 +1,1110 @@
+//! The built-in agent loop: a model, the engine's tools, consent, memory and the journal.
+//!
+//! Everything here runs on a background worker thread owned by [`ApiConversation`]; the UI only
+//! ever calls the non-blocking [`Conversation`] methods.
+
+use crate::anthropic::INVALID_INPUT_KEY;
+use crate::context;
+use crate::model::{Block, ChatModel, Delta, Effort, Msg, Speaker, Stop, StopReason, ToolDef, TurnRequest, Usage};
+use crate::price::{self, Meter};
+use crate::prompt;
+use crate::{AgentEvent, Conversation};
+use craft_anything_engine::{CallOptions, Caller, CancelToken, Consent, Engine, Registry, ToolOutput, Toolset};
+use craft_anything_evidence::{ErrorCode, ToolError};
+use craft_anything_platform::sync::Mutex;
+use serde_json::{Value, json};
+use std::collections::{BTreeMap, BTreeSet, HashMap};
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender};
+use std::sync::{Arc, PoisonError};
+use std::time::Duration;
+
+/// Byte budget for one tool result sent to the model.
+pub const RESULT_BUDGET: usize = 16 * 1024;
+/// Default most model turns per user message.
+pub const DEFAULT_MAX_TURNS: u32 = 40;
+/// Steps one builder message may take (writing, building and fixing a project takes many).
+pub const BUILDER_MAX_TURNS: u32 = 400;
+
+/// What the loop needs from the engine (a trait so tests can inject behaviour).
+pub trait ToolHost: Send + Sync {
+ /// Tool definitions for the enabled toolsets (without the meta-tools).
+ fn tools(&self, enabled: &BTreeSet) -> Vec;
+ /// The toolset a tool belongs to, if it exists.
+ fn toolset_of(&self, id: &str) -> Option;
+ fn call(&self, id: &str, input: Value, consent: bool, cancel: CancelToken) -> Result;
+ fn journal(&self, kind: &str, entry: Value);
+ /// `core_investigation_show` as compact JSON text.
+ fn investigation(&self) -> String;
+ fn clean_room(&self) -> bool;
+ /// A tool's title (for consent prompts).
+ fn title(&self, id: &str) -> String;
+ /// Every toolset in use (built-in and added at run time).
+ fn toolsets(&self) -> Vec {
+ Toolset::ALL.to_vec()
+ }
+}
+
+/// The real host: the engine.
+pub struct EngineHost(pub Arc);
+
+/// Remove keys model APIs dislike at the schema root, and write boolean sub-schemas as objects.
+fn clean_schema(mut s: Value) -> Value {
+ if let Some(o) = s.as_object_mut() {
+ o.remove("$schema");
+ o.remove("title");
+ if !o.contains_key("type") {
+ o.insert("type".into(), json!("object"));
+ }
+ if !o.contains_key("properties") && o.get("type") == Some(&json!("object")) {
+ o.insert("properties".into(), json!({}));
+ }
+ }
+ object_subschemas(&mut s, 0);
+ s
+}
+
+/// Deepest schema nesting rewritten (run-time tools bring their own schemas).
+const MAX_SCHEMA_DEPTH: usize = 64;
+
+/// `true` and `false` as sub-schemas (schemars writes `"items": true` for a list of any JSON) become
+/// `{}` and `{"not": {}}`: local servers such as LM Studio refuse anything but an object there.
+/// `additionalProperties` stays as it is: a boolean is what every API expects in it.
+fn object_subschemas(schema: &mut Value, depth: usize) {
+ let Some(o) = schema.as_object_mut().filter(|_| depth < MAX_SCHEMA_DEPTH) else { return };
+ for (key, sub) in o.iter_mut() {
+ let subs: Vec<&mut Value> = match (key.as_str(), sub) {
+ ("items" | "additionalItems" | "contains" | "not" | "if" | "then" | "else", s) => vec![s],
+ ("properties" | "patternProperties" | "$defs" | "definitions", Value::Object(m)) => m.values_mut().collect(),
+ ("anyOf" | "oneOf" | "allOf" | "prefixItems", Value::Array(a)) => a.iter_mut().collect(),
+ _ => Vec::new(),
+ };
+ for s in subs {
+ match s {
+ Value::Bool(true) => *s = json!({}),
+ Value::Bool(false) => *s = json!({"not": {}}),
+ _ => object_subschemas(s, depth + 1),
+ }
+ }
+ }
+}
+
+pub fn registry_tools(reg: &Registry, enabled: &BTreeSet) -> Vec {
+ reg.all()
+ .iter()
+ .filter(|s| enabled.contains(&s.toolset))
+ .map(|s| ToolDef { name: s.id.to_string(), description: s.summary.to_string(), schema: clean_schema(reg.input_schema(s)) })
+ .collect()
+}
+
+impl ToolHost for EngineHost {
+ fn tools(&self, enabled: &BTreeSet) -> Vec {
+ // Clean room offers only the tools it runs.
+ let clean = self.clean_room();
+ registry_tools(self.0.registry(), enabled).into_iter().filter(|t| !clean || craft_anything_engine::clean_room::allows(&t.name)).collect()
+ }
+
+ fn toolset_of(&self, id: &str) -> Option {
+ self.0.registry().get(id).map(|s| s.toolset)
+ }
+
+ fn toolsets(&self) -> Vec {
+ self.0.registry().toolsets()
+ }
+
+ fn call(&self, id: &str, input: Value, consent: bool, cancel: CancelToken) -> Result {
+ let opts = CallOptions::new(Caller::Agent).with_cancel(cancel).with_consent(if consent { Consent::Granted } else { Consent::NotAsked });
+ self.0.call(id, input, opts)
+ }
+
+ fn journal(&self, kind: &str, entry: Value) {
+ self.0.journal(kind, entry);
+ }
+
+ fn investigation(&self) -> String {
+ match self.0.call("core_investigation_show", json!({}), CallOptions::new(Caller::Agent)) {
+ Ok(o) => {
+ let v = o.to_json();
+ let inner = v.get("result").cloned().unwrap_or(v);
+ truncate_json(&inner, 24 * 1024)
+ }
+ Err(e) => format!("(couldn't load: {})", e.message),
+ }
+ }
+
+ fn clean_room(&self) -> bool {
+ self.0.try_session().map(|s| s.clean_room()).unwrap_or(true)
+ }
+
+ fn title(&self, id: &str) -> String {
+ self.0.registry().get(id).map(|s| s.title.to_string()).unwrap_or_else(|| id.to_string())
+ }
+}
+
+/// The engine with only the tools that change nothing: no files written, nothing started, no
+/// network, nobody asked (recording evidence in the session is fine). For questions about a build.
+pub struct ReadOnlyHost(pub EngineHost);
+
+impl ReadOnlyHost {
+ fn changes_nothing(&self, id: &str) -> bool {
+ use craft_anything_engine::Effects;
+ let harmless = Effects::IDEMPOTENT.union(Effects::MUTATES_SESSION).union(Effects::CARRIES_TARGET_CONTENT);
+ self.0.0.registry().get(id).is_some_and(|s| harmless.contains(s.effects))
+ }
+}
+
+impl ToolHost for ReadOnlyHost {
+ fn tools(&self, enabled: &BTreeSet) -> Vec {
+ self.0.tools(enabled).into_iter().filter(|t| self.changes_nothing(&t.name)).collect()
+ }
+ fn toolset_of(&self, id: &str) -> Option {
+ self.0.toolset_of(id)
+ }
+ fn toolsets(&self) -> Vec {
+ self.0.toolsets()
+ }
+ fn call(&self, id: &str, input: Value, _consent: bool, cancel: CancelToken) -> Result {
+ if !self.changes_nothing(id) {
+ return Err(ToolError::new(ErrorCode::NoPermission, format!("{id} changes something, and this is only a question."))
+ .with_remediation("Answer from what you can read."));
+ }
+ // Nothing that needs consent is offered, so none is given.
+ self.0.call(id, input, false, cancel)
+ }
+ fn journal(&self, kind: &str, entry: Value) {
+ self.0.journal(kind, entry);
+ }
+ fn investigation(&self) -> String {
+ self.0.investigation()
+ }
+ fn clean_room(&self) -> bool {
+ self.0.clean_room()
+ }
+ fn title(&self, id: &str) -> String {
+ self.0.title(id)
+ }
+}
+
+/// JSON text no longer than `budget` bytes (cut on a char boundary with a note).
+pub fn truncate_json(v: &Value, budget: usize) -> String {
+ let s = v.to_string();
+ if s.len() <= budget {
+ return s;
+ }
+ let mut cut = budget.saturating_sub(120);
+ while cut > 0 && !s.is_char_boundary(cut) {
+ cut -= 1;
+ }
+ format!("{}… [truncated: {} of {} bytes shown; page with offset/limit or fetch the evidence by id]", s.get(..cut).unwrap_or_default(), cut, s.len())
+}
+
+/// What the model sees for a tool's output: evidence summary + id + (bounded) result.
+pub fn compact(out: &ToolOutput) -> (String, String, Option) {
+ match out {
+ ToolOutput::Evidence(ev) => {
+ let mut v = ev.summary_view();
+ let result_text = ev.result.to_string();
+ let summary_len = v.to_string().len();
+ if let Some(o) = v.as_object_mut() {
+ if result_text.len() + summary_len <= RESULT_BUDGET {
+ o.insert("result".into(), ev.result.clone());
+ } else {
+ o.insert("result_preview".into(), json!(truncate_json(&ev.result, RESULT_BUDGET.saturating_sub(summary_len).max(512))));
+ o.insert(
+ "note".into(),
+ json!("Result is large: this is a preview. Page with offset/limit, or get the full record with core_evidence_show {evidence_id}."),
+ );
+ }
+ }
+ let summary = if ev.summary.is_empty() { ev.operation.clone() } else { ev.summary.clone() };
+ (truncate_json(&v, RESULT_BUDGET + 1024), summary, Some(ev.id.clone()))
+ }
+ ToolOutput::Value(v) => {
+ let text = truncate_json(&out.to_json(), RESULT_BUDGET);
+ let summary: String = v.to_string().chars().take(160).collect();
+ (text, summary, None)
+ }
+ }
+}
+
+fn error_text(e: &ToolError) -> String {
+ serde_json::to_string(&json!({"error": e})).unwrap_or_else(|_| e.message.clone())
+}
+
+/// Consent waiters shared between the worker and the UI-facing handle.
+type Waiters = Arc>>>;
+
+/// One model with its display name (a link in a fallback chain).
+pub struct ModelSlot {
+ pub name: String,
+ pub model: Box,
+ /// Local models cost nothing.
+ pub local: bool,
+ pub effort: Option,
+ pub max_turns: u32,
+}
+
+/// Settings for one conversation.
+#[derive(Default)]
+pub struct LoopConfig {
+ pub beginner: bool,
+ pub prices: BTreeMap,
+ /// Keep the conversation in this file after every step, and continue it when it's there.
+ pub transcript: Option,
+ /// Input tokens the conversation may grow to before it's trimmed; 0 = by model (local or
+ /// cloud, see [`crate::context`]).
+ pub context_tokens: u64,
+}
+
+/// The state the worker keeps between user messages.
+pub struct AgentLoop {
+ host: Arc,
+ chain: Vec,
+ current: usize,
+ history: Vec,
+ system: Option,
+ enabled: BTreeSet