diff --git a/.cargo/audit.toml b/.cargo/audit.toml index 0476ece6..efda3d4f 100644 --- a/.cargo/audit.toml +++ b/.cargo/audit.toml @@ -12,8 +12,8 @@ # 0.103.x (the patched copy, also in Cargo.lock). The vulnerable 0.102 code is # linked but never executed. The relay is also an opt-in remote feature. # -# Revisit when rumqttc ships a webpki-0.103 release, or if relay TLS adds custom -# certificate verification (CRLs / name constraints). +# Revisit by 2026-10-21, when rumqttc ships a webpki-0.103 release, or if relay +# TLS adds custom certificate verification (CRLs / name constraints). [advisories] ignore = [ diff --git a/.github/build-setup.yml b/.github/build-setup.yml index b08a8077..fefdf6c2 100644 --- a/.github/build-setup.yml +++ b/.github/build-setup.yml @@ -1,21 +1,21 @@ - name: Set up JDK 17 (required by Android SDK tools) if: ${{ contains(matrix.targets, 'aarch64-linux-android') }} - uses: actions/setup-java@v5.2.0 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: java-version: '17' distribution: temurin - name: Set up Android SDK if: ${{ contains(matrix.targets, 'aarch64-linux-android') }} - uses: android-actions/setup-android@v4.0.1 + uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1 - name: Install Android NDK if: ${{ contains(matrix.targets, 'aarch64-linux-android') }} - run: yes | sdkmanager "ndk;25.2.9519653" > /dev/null + run: yes | sdkmanager "ndk;27.3.13750724" > /dev/null - name: Configure Android target if: ${{ contains(matrix.targets, 'aarch64-linux-android') }} shell: bash run: | rustup target add aarch64-linux-android - NDK_BIN="$ANDROID_HOME/ndk/25.2.9519653/toolchains/llvm/prebuilt/linux-x86_64/bin" + NDK_BIN="$ANDROID_HOME/ndk/27.3.13750724/toolchains/llvm/prebuilt/linux-x86_64/bin" echo "CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=$NDK_BIN/aarch64-linux-android24-clang" >> "$GITHUB_ENV" echo "CC_aarch64_linux_android=$NDK_BIN/aarch64-linux-android24-clang" >> "$GITHUB_ENV" echo "AR_aarch64_linux_android=$NDK_BIN/llvm-ar" >> "$GITHUB_ENV" diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index d244fe14..3ede18bd 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -37,10 +37,12 @@ jobs: manylinux: auto runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Build wheel - uses: PyO3/maturin-action@v1.50.1 + uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: target: ${{ matrix.target }} args: --release --out dist @@ -48,7 +50,7 @@ jobs: docker-options: ${{ matrix.docker-options || '' }} - name: Upload wheel - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: wheel-${{ matrix.target }} path: dist/*.whl @@ -56,24 +58,26 @@ jobs: build-android: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - - uses: actions/setup-java@v5.2.0 + - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: java-version: '17' distribution: temurin - - uses: android-actions/setup-android@v4.0.1 + - uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1 - - name: Install NDK r25b - run: yes | sdkmanager "ndk;25.2.9519653" > /dev/null + - name: Install NDK r27d + run: yes | sdkmanager "ndk;27.3.13750724" > /dev/null - name: Build wheel run: | rustup target add aarch64-linux-android - pip install maturin==1.12.6 + pip install maturin==1.14.1 - NDK_BIN="$ANDROID_HOME/ndk/25.2.9519653/toolchains/llvm/prebuilt/linux-x86_64/bin" + NDK_BIN="$ANDROID_HOME/ndk/27.3.13750724/toolchains/llvm/prebuilt/linux-x86_64/bin" export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$NDK_BIN/aarch64-linux-android24-clang" export CC_aarch64_linux_android="$NDK_BIN/aarch64-linux-android24-clang" export AR_aarch64_linux_android="$NDK_BIN/llvm-ar" @@ -81,7 +85,7 @@ jobs: maturin build --release --target aarch64-linux-android --out dist - name: Upload wheel - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: wheel-aarch64-linux-android path: dist/*.whl diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 982e4dbc..29528a6e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,35 +2,52 @@ name: CI on: [push, pull_request] +permissions: + contents: read + jobs: rust-tests: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - # Pinned so a new stable release can't fail CI on a fresh lint with no - # code change. Bump deliberately in its own PR. Local dev stays floating. - - uses: dtolnay/rust-toolchain@master + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + # Keep lint behavior reproducible across fresh runners and local installs. + - uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # 1.97.1 with: toolchain: "1.97.1" components: rustfmt, clippy - - uses: Swatinem/rust-cache@v2.9.1 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - run: cargo fmt --all -- --check - run: cargo clippy --all-targets --locked -- -D warnings - run: cargo test --locked + msrv: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # 1.88 + with: + toolchain: "1.88" + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - run: cargo check --locked --all-targets + # Native Windows test gate. Release artifacts are built by cargo-dist on tags; # the pinned real-tool lifecycle has its own matrix below. windows-build: runs-on: windows-latest steps: - - uses: actions/checkout@v6 - # Pinned so a new stable release can't fail CI on a fresh lint with no - # code change. Bump deliberately in its own PR. Local dev stays floating. - - uses: dtolnay/rust-toolchain@master + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + # Keep lint behavior reproducible across fresh runners and local installs. + - uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # 1.97.1 with: toolchain: "1.97.1" components: rustfmt, clippy - - uses: Swatinem/rust-cache@v2.9.1 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - run: cargo fmt --all -- --check - run: cargo clippy --all-targets --locked -- -D warnings - run: cargo test --all-targets --locked @@ -40,12 +57,12 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: actions/setup-node@v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 22 + node-version: 22.23.1 cache: npm - run: bash ./scripts/typecheck.sh @@ -57,11 +74,11 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Install pinned cargo-dist - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.31.0/cargo-dist-installer.sh | sh" + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.32.0/cargo-dist-installer.sh | sh" - run: dist generate --check real-tool-tests: @@ -76,44 +93,44 @@ jobs: include: - os: ubuntu-24.04 tool: codex - package: "@openai/codex@0.141.0" - cache: codex-0.141.0 + package: "@openai/codex@0.145.0" + cache: codex-0.145.0 test: real_tool_codex - os: ubuntu-latest tool: claude - package: "@anthropic-ai/claude-code@2.1.185" - cache: claude-2.1.185 + package: "@anthropic-ai/claude-code@2.1.216" + cache: claude-2.1.216 test: real_tool_claude - os: ubuntu-latest tool: relay - package: "@anthropic-ai/claude-code@2.1.185" - cache: relay-claude-2.1.185 + package: "@anthropic-ai/claude-code@2.1.216" + cache: relay-claude-2.1.216 test: test_relay_roundtrip - os: windows-latest tool: codex - package: "@openai/codex@0.141.0" - cache: codex-0.141.0 + package: "@openai/codex@0.145.0" + cache: codex-0.145.0 test: real_tool_codex - os: windows-latest tool: claude - package: "@anthropic-ai/claude-code@2.1.185" - cache: claude-2.1.185 + package: "@anthropic-ai/claude-code@2.1.216" + cache: claude-2.1.216 test: real_tool_claude - os: windows-latest tool: relay - package: "@anthropic-ai/claude-code@2.1.185" - cache: relay-claude-2.1.185 + package: "@anthropic-ai/claude-code@2.1.216" + cache: relay-claude-2.1.216 test: test_relay_roundtrip steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: dtolnay/rust-toolchain@master + - uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # 1.97.1 with: toolchain: "1.97.1" - - uses: actions/setup-node@v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 22 + node-version: 22.23.1 - name: Configure Codex Linux sandbox if: runner.os == 'Linux' && matrix.tool == 'codex' run: | @@ -123,8 +140,8 @@ jobs: /usr/share/apparmor/extra-profiles/bwrap-userns-restrict \ /etc/apparmor.d/bwrap-userns-restrict sudo apparmor_parser -r /etc/apparmor.d/bwrap-userns-restrict - - uses: Swatinem/rust-cache@v2.9.1 - - uses: actions/cache@v4 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: | target/mock-tools diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index b0466a8a..32097c43 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -9,13 +9,14 @@ on: permissions: id-token: write + contents: read jobs: publish: runs-on: ubuntu-latest steps: - name: Download all wheels - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: wheel-* path: dist/ @@ -25,6 +26,8 @@ jobs: run: ls -la dist/ - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: password: ${{ secrets.PYPI_API_TOKEN }} + verbose: false + print-hash: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d650985f..f46dbcbe 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,7 +56,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: persist-credentials: false submodules: recursive @@ -64,9 +64,9 @@ jobs: # we specify bash to get pipefail; it guards against the `curl` command # failing. otherwise `sh` won't catch that `curl` returned non-0 shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.31.0/cargo-dist-installer.sh | sh" + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.32.0/cargo-dist-installer.sh | sh" - name: Cache dist - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: name: cargo-dist-cache path: ~/.cargo/bin/dist @@ -82,7 +82,7 @@ jobs: cat plan-dist-manifest.json echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" - name: "Upload dist-manifest.json" - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: name: artifacts-plan-dist-manifest path: plan-dist-manifest.json @@ -116,7 +116,7 @@ jobs: - name: enable windows longpaths run: | git config --global core.longpaths true - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: persist-credentials: false submodules: recursive @@ -128,22 +128,22 @@ jobs: echo "$HOME/.cargo/bin" >> $GITHUB_PATH fi - name: "Set up JDK 17 (required by Android SDK tools)" - uses: "actions/setup-java@v5.2.0" + uses: "actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654" if: "${{ contains(matrix.targets, 'aarch64-linux-android') }}" with: "distribution": "temurin" "java-version": "17" - name: "Set up Android SDK" - uses: "android-actions/setup-android@v4.0.1" + uses: "android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699" if: "${{ contains(matrix.targets, 'aarch64-linux-android') }}" - name: "Install Android NDK" if: "${{ contains(matrix.targets, 'aarch64-linux-android') }}" - run: "yes | sdkmanager \"ndk;25.2.9519653\" > /dev/null" + run: "yes | sdkmanager \"ndk;27.3.13750724\" > /dev/null" - name: "Configure Android target" if: "${{ contains(matrix.targets, 'aarch64-linux-android') }}" run: | rustup target add aarch64-linux-android - NDK_BIN="$ANDROID_HOME/ndk/25.2.9519653/toolchains/llvm/prebuilt/linux-x86_64/bin" + NDK_BIN="$ANDROID_HOME/ndk/27.3.13750724/toolchains/llvm/prebuilt/linux-x86_64/bin" echo "CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=$NDK_BIN/aarch64-linux-android24-clang" >> "$GITHUB_ENV" echo "CC_aarch64_linux_android=$NDK_BIN/aarch64-linux-android24-clang" >> "$GITHUB_ENV" echo "AR_aarch64_linux_android=$NDK_BIN/llvm-ar" >> "$GITHUB_ENV" @@ -152,7 +152,7 @@ jobs: run: ${{ matrix.install_dist.run }} # Get the dist-manifest - name: Fetch local artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with: pattern: artifacts-* path: target/distrib/ @@ -179,7 +179,7 @@ jobs: cp dist-manifest.json "$BUILD_MANIFEST_NAME" - name: "Upload artifacts" - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: name: artifacts-build-local-${{ join(matrix.targets, '_') }} path: | @@ -206,19 +206,19 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: persist-credentials: false submodules: recursive - name: Install cached dist - uses: actions/download-artifact@v7 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with: name: cargo-dist-cache path: ~/.cargo/bin/ - run: chmod +x ~/.cargo/bin/dist # Get all the local artifacts for the global tasks to use (for e.g. checksums) - name: Fetch local artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with: pattern: artifacts-* path: target/distrib/ @@ -236,7 +236,7 @@ jobs: cp dist-manifest.json "$BUILD_MANIFEST_NAME" - name: "Upload artifacts" - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: name: artifacts-build-global path: | @@ -257,19 +257,19 @@ jobs: outputs: val: ${{ steps.host.outputs.manifest }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: persist-credentials: false submodules: recursive - name: Install cached dist - uses: actions/download-artifact@v7 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with: name: cargo-dist-cache path: ~/.cargo/bin/ - run: chmod +x ~/.cargo/bin/dist # Fetch artifacts from scratch-storage - name: Fetch artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with: pattern: artifacts-* path: target/distrib/ @@ -282,14 +282,14 @@ jobs: cat dist-manifest.json echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" - name: "Upload dist-manifest.json" - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: # Overwrite the previous copy name: artifacts-dist-manifest path: dist-manifest.json # Create a GitHub Release while uploading all files to it - name: "Download GitHub Artifacts" - uses: actions/download-artifact@v7 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with: pattern: artifacts-* path: artifacts @@ -322,14 +322,14 @@ jobs: GITHUB_EMAIL: "admin+bot@axo.dev" if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: persist-credentials: true repository: "aannoo/homebrew-hcom" token: ${{ secrets.HOMEBREW_TAP_TOKEN }} # So we have access to the formula - name: Fetch homebrew formulae - uses: actions/download-artifact@v7 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with: pattern: artifacts-* path: Formula/ @@ -367,7 +367,7 @@ jobs: secrets: inherit # publish jobs get escalated permissions permissions: - "contents": "write" + "contents": "read" "id-token": "write" announce: @@ -384,7 +384,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: persist-credentials: false submodules: recursive diff --git a/.node-version b/.node-version new file mode 100644 index 00000000..f9e7451e --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +22.23.1 diff --git a/Cargo.lock b/Cargo.lock index 4f7cd345..4eaadacd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,12 +4,12 @@ version = 4 [[package]] name = "aead" -version = "0.5.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" dependencies = [ - "crypto-common 0.1.7", - "generic-array", + "crypto-common 0.2.2", + "inout", ] [[package]] @@ -88,15 +88,24 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "approx" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" [[package]] name = "atomic" @@ -109,15 +118,15 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.17.0" +version = "1.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" dependencies = [ "aws-lc-sys", "zeroize", @@ -125,14 +134,15 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.41.0" +version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" dependencies = [ "cc", "cmake", "dunce", "fs_extra", + "pkg-config", ] [[package]] @@ -164,9 +174,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "block-buffer" @@ -179,30 +189,36 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", ] [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" [[package]] name = "bytemuck" -version = "1.25.0" +version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "castaway" @@ -215,9 +231,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.62" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" dependencies = [ "find-msvc-tools", "jobserver", @@ -239,50 +255,39 @@ checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "chacha20" -version = "0.9.1" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cipher", - "cpufeatures 0.2.17", -] - -[[package]] -name = "chacha20" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" -dependencies = [ - "cfg-if", "cpufeatures 0.3.0", "rand_core 0.10.1", ] [[package]] name = "chacha20poly1305" -version = "0.10.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +checksum = "9b89e1c441e926b9c82a8d023f6e1b7ae0adcfaa7d621814e4d60789bac751cb" dependencies = [ "aead", - "chacha20 0.9.1", + "chacha20", "cipher", "poly1305", - "zeroize", ] [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -293,20 +298,20 @@ dependencies = [ [[package]] name = "cipher" -version = "0.4.4" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ - "crypto-common 0.1.7", + "block-buffer 0.12.1", + "crypto-common 0.2.2", "inout", - "zeroize", ] [[package]] name = "clap" -version = "4.6.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7" dependencies = [ "clap_builder", "clap_derive", @@ -314,9 +319,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" dependencies = [ "anstream", "anstyle", @@ -326,14 +331,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.2", ] [[package]] @@ -351,6 +356,12 @@ dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "colorchoice" version = "1.0.5" @@ -359,9 +370,9 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "compact_str" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" dependencies = [ "castaway", "cfg-if", @@ -420,13 +431,19 @@ dependencies = [ "libc", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossterm" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "crossterm_winapi", "derive_more", "document-features", @@ -454,17 +471,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", - "rand_core 0.6.4", "typenum", ] [[package]] name = "crypto-common" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ + "getrandom 0.4.3", "hybrid-array", + "rand_core 0.10.1", ] [[package]] @@ -477,6 +495,15 @@ dependencies = [ "phf", ] +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "darling" version = "0.23.0" @@ -497,7 +524,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -508,7 +535,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -522,9 +549,6 @@ name = "deranged" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", -] [[package]] name = "derive_more" @@ -545,7 +569,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -564,9 +588,9 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.0", + "block-buffer 0.12.1", "const-oid", - "crypto-common 0.2.1", + "crypto-common 0.2.2", ] [[package]] @@ -613,9 +637,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "equivalent" @@ -664,11 +688,17 @@ dependencies = [ "regex", ] +[[package]] +name = "fast-srgb8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" + [[package]] name = "fastrand" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "filedescriptor" @@ -722,12 +752,6 @@ 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" @@ -742,15 +766,15 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" dependencies = [ "futures-core", "futures-task", @@ -759,21 +783,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-core", "futures-sink", @@ -817,32 +841,21 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", "rand_core 0.10.1", - "wasip2", - "wasip3", ] [[package]] name = "glob" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "hashbrown" @@ -852,7 +865,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash 0.2.0", + "foldhash", ] [[package]] @@ -860,12 +873,17 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] name = "hashlink" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" dependencies = [ "hashbrown 0.16.1", ] @@ -887,7 +905,7 @@ dependencies = [ "lru", "nix 0.31.3", "portable-pty", - "rand 0.10.1", + "rand 0.10.2", "ratatui", "regex", "rumqttc", @@ -901,7 +919,7 @@ dependencies = [ "shell-words", "signal-hook 0.4.4", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.19", "toml", "toml_edit", "unicode-segmentation", @@ -909,7 +927,7 @@ dependencies = [ "uuid", "vt100", "webpki-roots", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -926,9 +944,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hybrid-array" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" dependencies = [ "typenum", ] @@ -957,12 +975,6 @@ dependencies = [ "cc", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -977,8 +989,6 @@ checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown 0.17.1", - "serde", - "serde_core", ] [[package]] @@ -992,11 +1002,11 @@ dependencies = [ [[package]] name = "inout" -version = "0.1.4" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" dependencies = [ - "generic-array", + "hybrid-array", ] [[package]] @@ -1009,7 +1019,7 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -1035,23 +1045,22 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jobserver" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.3", "libc", ] [[package]] name = "js-sys" -version = "0.3.98" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -1063,7 +1072,7 @@ checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" dependencies = [ "hashbrown 0.16.1", "portable-atomic", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -1079,22 +1088,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] -name = "leb128fmt" -version = "0.1.0" +name = "libc" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] -name = "libc" -version = "0.2.186" +name = "libm" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.16" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" dependencies = [ "libc", ] @@ -1116,7 +1125,7 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", ] [[package]] @@ -1142,17 +1151,17 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lru" -version = "0.16.4" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +checksum = "0b6180140927ee907000b0aa540091f6ea512ead4447c92b8fc35bc72788a5a6" dependencies = [ - "hashbrown 0.16.1", + "hashbrown 0.17.1", ] [[package]] @@ -1167,9 +1176,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memmem" @@ -1194,9 +1203,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" -version = "1.2.0" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "log", @@ -1210,7 +1219,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "cfg-if", "cfg_aliases 0.1.1", "libc", @@ -1222,9 +1231,9 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "cfg-if", - "cfg_aliases 0.2.1", + "cfg_aliases 0.2.2", "libc", "memoffset", ] @@ -1235,9 +1244,9 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "cfg-if", - "cfg_aliases 0.2.1", + "cfg_aliases 0.2.2", "libc", ] @@ -1265,7 +1274,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -1304,12 +1313,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - [[package]] name = "openssl-probe" version = "0.2.1" @@ -1331,6 +1334,30 @@ dependencies = [ "num-traits", ] +[[package]] +name = "palette" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +dependencies = [ + "approx", + "fast-srgb8", + "libm", + "palette_derive", +] + +[[package]] +name = "palette_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -1356,9 +1383,9 @@ dependencies = [ [[package]] name = "pest" -version = "2.8.6" +version = "2.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +checksum = "47627dd7305c6a2d6c8c6bcd24c5a4c17dbbf425f4f9c5313e724b38fc9782e9" dependencies = [ "memchr", "ucd-trie", @@ -1366,9 +1393,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.6" +version = "2.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +checksum = "4b4254325ecad416ab689e27ba51da03ba01a9632bc6e108f5fe7c3c4ad29d58" dependencies = [ "pest", "pest_generator", @@ -1376,25 +1403,24 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.6" +version = "2.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +checksum = "6c4c0e91ead7a8f7acecbca6f003fc2e8282b1dbe2dd9c9d2f16aba42995e0a7" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "pest_meta" -version = "2.8.6" +version = "2.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +checksum = "f9744bc48116fee06334924bb5f2bad41eed5e89bd26e29b0b799f9a3f82c210" dependencies = [ "pest", - "sha2 0.10.9", ] [[package]] @@ -1424,7 +1450,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand 0.8.6", + "rand 0.8.7", ] [[package]] @@ -1437,7 +1463,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -1463,20 +1489,19 @@ checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "poly1305" -version = "0.8.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +checksum = "6e2d0073b297041425c7c3df6eb4792d598a15323fe63346852b092eca02904c" dependencies = [ - "cpufeatures 0.2.17", - "opaque-debug", + "cpufeatures 0.3.0", "universal-hash", ] [[package]] name = "portable-atomic" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" [[package]] name = "portable-pty" @@ -1505,30 +1530,20 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.117", -] - [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.45" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -1547,21 +1562,21 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ "rand_core 0.6.4", ] [[package]] name = "rand" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ - "chacha20 0.10.0", - "getrandom 0.4.2", + "chacha20", + "getrandom 0.4.3", "rand_core 0.10.1", ] @@ -1570,9 +1585,6 @@ 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" @@ -1582,34 +1594,38 @@ checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "ratatui" -version = "0.30.0" +version = "0.30.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" +checksum = "3274ba0a2c5e1bcad2a2005d20f4dc59dad26b2eb0940fb094500dba4099d57d" dependencies = [ "instability", "ratatui-core", "ratatui-crossterm", "ratatui-macros", + "ratatui-termina", "ratatui-termion", "ratatui-termwiz", "ratatui-widgets", + "serde", ] [[package]] name = "ratatui-core" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" +checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "compact_str", - "hashbrown 0.16.1", - "indoc", + "critical-section", + "hashbrown 0.17.1", "itertools", "kasuari", "lru", + "palette", + "serde", "strum", - "thiserror 2.0.18", + "thiserror 2.0.19", "unicode-segmentation", "unicode-truncate", "unicode-width", @@ -1617,9 +1633,9 @@ dependencies = [ [[package]] name = "ratatui-crossterm" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" +checksum = "567584a3b0e6a8203c23de40b4861497266725eb5363dbfd18a1edd603cca9f0" dependencies = [ "cfg-if", "crossterm", @@ -1629,19 +1645,30 @@ dependencies = [ [[package]] name = "ratatui-macros" -version = "0.7.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4" +checksum = "ed7dc68daa7498a43e4d68e0eb078427e10c38fbcfbb1e42d955f1fa2140d814" dependencies = [ "ratatui-core", "ratatui-widgets", ] [[package]] -name = "ratatui-termion" +name = "ratatui-termina" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cade85a8591fbc911e147951422f0d6fd40f4948b271b6216c7dc01838996f8" +checksum = "c0bf912d9e66f057a759d92e386a280ea886b352ab757d6ac4d653c7ed2c43c2" +dependencies = [ + "instability", + "ratatui-core", + "termina", +] + +[[package]] +name = "ratatui-termion" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87c732202fa5a71a9da0991013f0853e53f87048f45198e3a1ca3ee722accc2f" dependencies = [ "instability", "ratatui-core", @@ -1650,9 +1677,9 @@ dependencies = [ [[package]] name = "ratatui-termwiz" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c" +checksum = "faf03e0380b7744054d6cb74224fe3adf062a029754933f575ca1e3b4c2ce977" dependencies = [ "ratatui-core", "termwiz", @@ -1660,17 +1687,18 @@ dependencies = [ [[package]] name = "ratatui-widgets" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" +checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1" dependencies = [ - "bitflags 2.11.1", - "hashbrown 0.16.1", + "bitflags 2.13.1", + "hashbrown 0.17.1", "indoc", "instability", "itertools", "line-clipping", "ratatui-core", + "serde", "strum", "time", "unicode-segmentation", @@ -1683,7 +1711,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", ] [[package]] @@ -1694,14 +1722,14 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] name = "regex" -version = "1.12.3" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -1711,9 +1739,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" dependencies = [ "aho-corasick", "memchr", @@ -1722,9 +1750,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "ring" @@ -1742,12 +1770,12 @@ dependencies = [ [[package]] name = "rsqlite-vfs" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" +checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" dependencies = [ "hashbrown 0.16.1", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -1764,7 +1792,7 @@ dependencies = [ "rustls-native-certs", "rustls-pemfile", "rustls-webpki 0.102.8", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-rustls", "tokio-stream", @@ -1777,7 +1805,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -1801,7 +1829,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "errno", "libc", "linux-raw-sys", @@ -1810,9 +1838,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" dependencies = [ "aws-lc-rs", "log", @@ -1825,9 +1853,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -1846,9 +1874,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" dependencies = [ "zeroize", ] @@ -1878,9 +1906,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "ryu" @@ -1888,15 +1916,6 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" -[[package]] -name = "scc" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" -dependencies = [ - "sdd", -] - [[package]] name = "schannel" version = "0.1.29" @@ -1912,19 +1931,13 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sdd" -version = "3.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" - [[package]] name = "security-framework" version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "core-foundation", "core-foundation-sys", "libc", @@ -1949,9 +1962,9 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -1959,29 +1972,29 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.2", ] [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -2012,28 +2025,27 @@ dependencies = [ [[package]] name = "serial_test" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" +checksum = "699f4197115b8a7e7ff19c9a315a4bd6fffec26cc4626ef45ecaea389e081c6d" dependencies = [ "futures-executor", "futures-util", "log", "once_cell", "parking_lot", - "scc", "serial_test_derive", ] [[package]] name = "serial_test_derive" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" +checksum = "94e153fc76e1c6a068703d6d29c508a0b15c061c4b7e43da59cc097bc342673c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -2076,9 +2088,9 @@ checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook" @@ -2135,15 +2147,15 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", "windows-sys 0.61.2", @@ -2151,18 +2163,18 @@ dependencies = [ [[package]] name = "spin" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" dependencies = [ "lock_api", ] [[package]] name = "sqlite-wasm-rs" -version = "0.5.3" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b2c760607300407ddeaee518acf28c795661b7108c75421303dbefb237d3a36" +checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" dependencies = [ "cc", "js-sys", @@ -2184,23 +2196,23 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -2222,9 +2234,20 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.117" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3" dependencies = [ "proc-macro2", "quote", @@ -2238,12 +2261,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys 0.61.2", ] +[[package]] +name = "termina" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9048a889effe34a5cddee0af7f53285198b16dca3be510858d38dfdb3e62a04e" +dependencies = [ + "bitflags 2.13.1", + "parking_lot", + "rustix", + "signal-hook 0.3.18", + "windows-sys 0.61.2", +] + [[package]] name = "terminfo" version = "0.9.0" @@ -2283,7 +2319,7 @@ checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" dependencies = [ "anyhow", "base64", - "bitflags 2.11.1", + "bitflags 2.13.1", "fancy-regex", "filedescriptor", "finl_unicode", @@ -2328,11 +2364,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.19", ] [[package]] @@ -2343,25 +2379,25 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.2", ] [[package]] name = "time" -version = "0.3.47" +version = "0.3.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" dependencies = [ "deranged", "libc", @@ -2374,15 +2410,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "tokio" -version = "1.52.3" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -2395,13 +2431,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -2427,22 +2463,23 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", "futures-sink", + "libc", "pin-project-lite", "tokio", ] [[package]] name = "toml" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" dependencies = [ "indexmap", "serde_core", @@ -2464,9 +2501,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.11+spec-1.1.0" +version = "0.25.13+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ "indexmap", "toml_datetime", @@ -2486,15 +2523,15 @@ dependencies = [ [[package]] name = "toml_writer" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "ucd-trie" @@ -2510,9 +2547,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-truncate" @@ -2531,20 +2568,14 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "universal-hash" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" dependencies = [ - "crypto-common 0.1.7", - "subtle", + "crypto-common 0.2.2", + "ctutils", ] [[package]] @@ -2561,12 +2592,12 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.1" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ "atomic", - "getrandom 0.4.2", + "getrandom 0.4.3", "js-sys", "wasm-bindgen", ] @@ -2620,27 +2651,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "wit-bindgen 0.51.0", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.121" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -2651,9 +2673,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.121" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2661,65 +2683,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.121" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.121" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.11.1", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "webpki-roots" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" dependencies = [ "rustls-pki-types", ] @@ -2839,7 +2827,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -2850,7 +2838,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -2883,16 +2871,7 @@ 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.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", + "windows-targets", ] [[package]] @@ -2910,31 +2889,14 @@ 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", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -2943,101 +2905,53 @@ 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 = "winnow" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" dependencies = [ "memchr", ] @@ -3051,108 +2965,20 @@ dependencies = [ "winapi", ] -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.11.1", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index 1f132e8d..4caa4b4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,12 +41,12 @@ uuid = { version = "1.23.0", features = ["v4"] } rumqttc = { version = "0.25.1", default-features = false, features = ["use-rustls"] } base64 = "0.22" sha2 = "0.11.0" -chacha20poly1305 = "0.10" +chacha20poly1305 = "0.11.0" bytes = "1" rustls = "0.23.37" webpki-roots = "1.0.6" rustls-native-certs = "0.8.3" -lru = "0.16" +lru = "0.18.1" # Unix-only: PTY, signals, fd polling, file locks, process groups. [target.'cfg(unix)'.dependencies] @@ -56,7 +56,7 @@ signal-hook = "0.4.4" # Windows-only: process/job-object control, winsock select, file locking. [target.'cfg(windows)'.dependencies] -windows-sys = { version = "0.60", features = [ +windows-sys = { version = "0.61", features = [ "Win32_Foundation", "Win32_Security", "Win32_System_Threading", diff --git a/README.md b/README.md index 2da91ec5..11a96d5c 100644 --- a/README.md +++ b/README.md @@ -326,6 +326,7 @@ hcom config -i # per-agent override at runtime | `auto_approve` | Auto-approve safe hcom commands (send/list/events/…) | | `auto_subscribe` | Event subscription presets: `collision`, `created`, `stopped`, `blocked` | | `name_export` | Export instance name to a custom env var | +| `title_mode` | Terminal/tab title behavior: `combined` (default), `label`, or `off` | | `terminal` | Where new agent windows open (`hcom config terminal --info`) | | `timeout` | Idle timeout for headless/vanilla Claude (seconds) | | `subagent_timeout` | Keep-alive for Claude subagents (seconds) | diff --git a/dist-workspace.toml b/dist-workspace.toml index 73dd375e..928cecbe 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -4,7 +4,7 @@ members = ["cargo:."] # Config for 'dist' [dist] # The preferred dist version to use in CI (Cargo.toml SemVer syntax) -cargo-dist-version = "0.31.0" +cargo-dist-version = "0.32.0" # CI backends to support ci = "github" # Target platforms to build apps for (Rust target-triple syntax) @@ -23,7 +23,7 @@ post-announce-jobs = ["./post-announce-compare-link"] # Extra setup steps to run before dist builds github-build-setup = "../build-setup.yml" # Custom permissions for reusable workflow jobs -github-custom-job-permissions = { "publish-pypi" = { id-token = "write", contents = "write" } } +github-custom-job-permissions = { "publish-pypi" = { id-token = "write", contents = "read" } } # A GitHub repo to push Homebrew formulas to tap = "aannoo/homebrew-hcom" # Whether dist should create a Github Release or use an existing draft @@ -36,3 +36,8 @@ install-updater = false [dist.github-custom-runners.aarch64-linux-android] runner = "ubuntu-22.04" host = "x86_64-unknown-linux-gnu" + +[dist.github-action-commits] +"actions/checkout" = "de0fac2e4500dabe0009e67214ff5f5447ce83dd" +"actions/upload-artifact" = "043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" +"actions/download-artifact" = "3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c" diff --git a/package-lock.json b/package-lock.json index c30a16b5..d7d4c041 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,19 +6,22 @@ "": { "name": "hcom-plugin-typecheck", "devDependencies": { - "@earendil-works/pi-coding-agent": "0.80.3", - "@oh-my-pi/pi-coding-agent": "16.2.13", - "@opencode-ai/plugin": "1.17.13", - "@opencode-ai/sdk": "1.17.13", - "@types/bun": "1.2.22", - "@types/node": "24.3.1", - "typescript": "6.0.3" + "@earendil-works/pi-coding-agent": "0.81.1", + "@oh-my-pi/pi-coding-agent": "17.0.6", + "@opencode-ai/plugin": "1.18.4", + "@opencode-ai/sdk": "1.18.4", + "@types/bun": "1.3.14", + "@types/node": "22.20.1", + "typescript": "7.0.2" + }, + "engines": { + "node": ">=22.22.2 <23" } }, "node_modules/@agentclientprotocol/sdk": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.25.0.tgz", - "integrity": "sha512-wU1VgXNtMvdVotX49txc3WJUDV+/QbLpsgjMvFhlRmp37osdLbI7L7y+iwAlQATwfjLxcv1r1p3ZxZBcXlGhcQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-1.2.1.tgz", + "integrity": "sha512-jwYUdOQR7tc+Zfch53VL4JJyUNK/46q03uUTYb+PjECsmnNl94XFXOfYLJ8RBpMNidXd1rpOAVgb0vqD98xImA==", "dev": true, "license": "Apache-2.0", "peerDependencies": { @@ -39,19 +42,19 @@ } }, "node_modules/@ark/schema": { - "version": "0.56.1", - "resolved": "https://registry.npmjs.org/@ark/schema/-/schema-0.56.1.tgz", - "integrity": "sha512-1Cf2g9nKD8K/3JGRu+gCCfYw5d4qR8YLLjDs5W5kpmaButCYWAPFUJqSXyBATPjglzCd4tIkp398iPYVs8MjRA==", + "version": "0.56.2", + "resolved": "https://registry.npmjs.org/@ark/schema/-/schema-0.56.2.tgz", + "integrity": "sha512-Qx4D2JFbBWpntiHZaTv7bGG4H/M2rigiknezKg/WVyDSaLdE4YCcWAOoFB7pjjDqHbbV2OqRfntm1nnXvwMexg==", "dev": true, "license": "MIT", "dependencies": { - "@ark/util": "0.56.1" + "@ark/util": "0.56.2" } }, "node_modules/@ark/util": { - "version": "0.56.1", - "resolved": "https://registry.npmjs.org/@ark/util/-/util-0.56.1.tgz", - "integrity": "sha512-Tp1rTik3q5Z+jAeeDxr5JZpmVIw0miti1ykSEHyZv5Pw3TIJl2xbN6KTacOxITp0l3s9ytlrWd30Zvqcy5vzoQ==", + "version": "0.56.2", + "resolved": "https://registry.npmjs.org/@ark/util/-/util-0.56.2.tgz", + "integrity": "sha512-9kU2sUE38FZEGG7l3hamYMBieLYEJh2L1mrYD2eXpT+78EnQSV1bhjxJhnxGBMSTbtwpBSDNSK+K60WvaI/DTQ==", "dev": true, "license": "MIT" }, @@ -106,9 +109,9 @@ } }, "node_modules/@bufbuild/protobuf": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.12.1.tgz", - "integrity": "sha512-BvAMfS6LrgZiryOAZ4pBYucu4wG/Ei/9o9DZ9akbREnMLbPJiom2i8b9C8IsKErQoiKqVhrerzt3kOT/RrzLHg==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.13.0.tgz", + "integrity": "sha512-acq7c49vxfm1ggJ95P70TX7ABDM0vxr1SYD3BB0o0jnBLB4OAqeHyKuN+cD3w80gXEDQ2zxHpR6CUeA+O/aU9g==", "dev": true, "license": "(Apache-2.0 AND BSD-3-Clause)" }, @@ -135,16 +138,16 @@ } }, "node_modules/@earendil-works/pi-coding-agent": { - "version": "0.80.3", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.80.3.tgz", - "integrity": "sha512-TIggw9gCXpA+Ph7OjdTA7ka2NPwTVuPmy39KDSyUzaKq8VvHfMGR7vtRz4JB7Um/RMRblmzhu4p9tUCk6MTgGA==", + "version": "0.81.1", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.81.1.tgz", + "integrity": "sha512-r6ovAsZOgAqbC/aU6s+/dPnv/sGZBuWyZNvi3pXjpbuX5wvp3XvGkQI7/VLvX2o9XpmpFaPUxKNym1WfkN/P8A==", "dev": true, "hasShrinkwrap": true, "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.80.3", - "@earendil-works/pi-ai": "^0.80.3", - "@earendil-works/pi-tui": "^0.80.3", + "@earendil-works/pi-agent-core": "^0.81.1", + "@earendil-works/pi-ai": "^0.81.1", + "@earendil-works/pi-tui": "^0.81.1", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -634,12 +637,12 @@ } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-agent-core": { - "version": "0.80.3", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.80.3.tgz", + "version": "0.81.1", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.81.1.tgz", "dev": true, "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.80.3", + "@earendil-works/pi-ai": "^0.81.1", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -649,8 +652,8 @@ } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai": { - "version": "0.80.3", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.80.3.tgz", + "version": "0.81.1", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.81.1.tgz", "dev": true, "license": "MIT", "dependencies": { @@ -667,15 +670,15 @@ "typebox": "1.1.38" }, "bin": { - "pi-ai": "./dist/cli.js" + "pi-ai": "dist/cli.js" }, "engines": { "node": ">=22.19.0" } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-tui": { - "version": "0.80.3", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.80.3.tgz", + "version": "0.81.1", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.81.1.tgz", "dev": true, "license": "MIT", "dependencies": { @@ -790,9 +793,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -810,9 +810,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -830,9 +827,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -850,9 +844,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -870,9 +861,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1241,9 +1229,9 @@ "license": "MIT" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "dev": true, "license": "MIT", "dependencies": { @@ -2877,9 +2865,9 @@ ] }, "node_modules/@nodable/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.0.0.tgz", + "integrity": "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==", "dev": true, "funding": [ { @@ -2890,37 +2878,37 @@ "license": "MIT" }, "node_modules/@oh-my-pi/hashline": { - "version": "16.2.13", - "resolved": "https://registry.npmjs.org/@oh-my-pi/hashline/-/hashline-16.2.13.tgz", - "integrity": "sha512-EVrq7q7wQmMNGHjo6kM3EXZUpYUT8zxx4QvGsmI4rTk5fDvHI7sam4NlPm67mgFE4rK0rLoc+7CyjKK4dX2org==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/hashline/-/hashline-17.0.6.tgz", + "integrity": "sha512-cvnR9fpeFiRntAOUNniQd1Ct57eXuuVOOWLLo/Sw//ROcTZxBBdBcPZ2lXgcVKv5vlhxRslkWhZPX59QJzmfYQ==", "dev": true, "license": "MIT", "dependencies": { "diff": "^9.0.0", - "lru-cache": "11.5.1" + "lru-cache": "11.5.2" }, "engines": { "bun": ">=1.3.14" } }, "node_modules/@oh-my-pi/omp-stats": { - "version": "16.2.13", - "resolved": "https://registry.npmjs.org/@oh-my-pi/omp-stats/-/omp-stats-16.2.13.tgz", - "integrity": "sha512-2dRmA2bOcwP0r5ZFdkHBXNw05tIAgyhPQQnQHEKZrQ/wjGLHn2fzI9Eaw7uE/GybJTYHcuPmb2f7/VK/3LRE2g==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/omp-stats/-/omp-stats-17.0.6.tgz", + "integrity": "sha512-4nf9+GJuTRJanYNo7JbEDPsm22eH05yZSYHvDZ3WeU2JOaF+ebSIO9xgiiAqOPL9ogGMyMCIKFGDNp7v0XxBVg==", "dev": true, "license": "MIT", "dependencies": { - "@oh-my-pi/pi-ai": "16.2.13", - "@oh-my-pi/pi-catalog": "16.2.13", - "@oh-my-pi/pi-utils": "16.2.13", - "@tailwindcss/node": "^4.3.0", + "@oh-my-pi/pi-ai": "17.0.6", + "@oh-my-pi/pi-catalog": "17.0.6", + "@oh-my-pi/pi-utils": "17.0.6", + "@tailwindcss/node": "^4.3.2", "chart.js": "^4.5.1", "date-fns": "^4.4.0", - "lucide-react": "^1.17.0", + "lucide-react": "^1.24.0", "react": "19.2.7", "react-chartjs-2": "^5.3.1", "react-dom": "19.2.7", - "tailwindcss": "^4.3.0" + "tailwindcss": "^4.3.2" }, "bin": { "omp-stats": "src/index.ts" @@ -2930,18 +2918,18 @@ } }, "node_modules/@oh-my-pi/pi-agent-core": { - "version": "16.2.13", - "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-agent-core/-/pi-agent-core-16.2.13.tgz", - "integrity": "sha512-JgV46rg5PebIyBIa52I/R06Vy/eNe8IqpCzJtgUMkrIySIGujlELHYyckg0GvXryUbK2QjWWWdOGvGmqV67oxw==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-agent-core/-/pi-agent-core-17.0.6.tgz", + "integrity": "sha512-UhryJs+IuemTDiZ+wm7+fh8T+W9F9yWAUjbfB+8om2D9fBWlethRp27uA4DwnCD315kAZ9SjezqyFsbKe/UEmQ==", "dev": true, "license": "MIT", "dependencies": { - "@oh-my-pi/pi-ai": "16.2.13", - "@oh-my-pi/pi-catalog": "16.2.13", - "@oh-my-pi/pi-natives": "16.2.13", - "@oh-my-pi/pi-utils": "16.2.13", - "@oh-my-pi/pi-wire": "16.2.13", - "@oh-my-pi/snapcompact": "16.2.13", + "@oh-my-pi/pi-ai": "17.0.6", + "@oh-my-pi/pi-catalog": "17.0.6", + "@oh-my-pi/pi-natives": "17.0.6", + "@oh-my-pi/pi-utils": "17.0.6", + "@oh-my-pi/pi-wire": "17.0.6", + "@oh-my-pi/snapcompact": "17.0.6", "@opentelemetry/api": "^1.9.1" }, "engines": { @@ -2949,17 +2937,17 @@ } }, "node_modules/@oh-my-pi/pi-ai": { - "version": "16.2.13", - "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-ai/-/pi-ai-16.2.13.tgz", - "integrity": "sha512-/+qgh8f1CeyTTspigsLTjLnTALtSccg006WTQtmIPuvhm/a/3OJFrpWaa77dlMdUL1fpaUJMTZbXvHwQwdBKeg==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-ai/-/pi-ai-17.0.6.tgz", + "integrity": "sha512-h6UXajqeKk2x2daVEB3I/pTdhHm+goyJFjgbJlMszPw4x7041Vs6i8r/HPnzrNbiciZyQrG6phmlBk+gYUk/pg==", "dev": true, "license": "MIT", "dependencies": { - "@bufbuild/protobuf": "^2.12.0", - "@oh-my-pi/pi-catalog": "16.2.13", - "@oh-my-pi/pi-utils": "16.2.13", - "@oh-my-pi/pi-wire": "16.2.13", - "arktype": "^2.2.0", + "@bufbuild/protobuf": "^2.12.1", + "@oh-my-pi/pi-catalog": "17.0.6", + "@oh-my-pi/pi-utils": "17.0.6", + "@oh-my-pi/pi-wire": "17.0.6", + "arktype": "2.2.3", "zod": "^4" }, "engines": { @@ -2967,15 +2955,15 @@ } }, "node_modules/@oh-my-pi/pi-catalog": { - "version": "16.2.13", - "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-catalog/-/pi-catalog-16.2.13.tgz", - "integrity": "sha512-eo7WOBmrgWun+A9DX/BNR1D4q9GzoT5kq/mgDqqBulktNFVR2Cp7csCpxsPr2hvDUhKq20u6hL/nKclUjXIJVQ==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-catalog/-/pi-catalog-17.0.6.tgz", + "integrity": "sha512-RunNuysuPCO/9r+VjMrvw7rl6TNVEK6U9p3lOHUlHUoRCOJI+570qbkv3HM98zJwwUQEj4O7SGhhdV6kNXzxzA==", "dev": true, "license": "MIT", "dependencies": { - "@bufbuild/protobuf": "^2.12.0", - "@oh-my-pi/pi-utils": "16.2.13", - "arktype": "^2.2.0", + "@bufbuild/protobuf": "^2.12.1", + "@oh-my-pi/pi-utils": "17.0.6", + "arktype": "2.2.3", "zod": "^4" }, "engines": { @@ -2983,45 +2971,51 @@ } }, "node_modules/@oh-my-pi/pi-coding-agent": { - "version": "16.2.13", - "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-coding-agent/-/pi-coding-agent-16.2.13.tgz", - "integrity": "sha512-ZKi9xul+Ef6wmq4VoETCmEFrAOOqx0Wq24MxHM6vL7al2NW/q7RCJc3StlLmHs8zN06C2tI/XQTMJbm5UDX0fA==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-coding-agent/-/pi-coding-agent-17.0.6.tgz", + "integrity": "sha512-lYLAfsoNvdTPXZ8TgC8FiXRYbsT4fTqUzMhaDV7kGLwkFQqBS0Q8/6DjTnXWrwTkQGbYlksOkBpQzvOWm/slrQ==", "dev": true, "license": "MIT", "dependencies": { - "@agentclientprotocol/sdk": "0.25.0", + "@agentclientprotocol/sdk": "1.2.1", "@babel/parser": "^7.29.7", "@mozilla/readability": "^0.6.0", - "@oh-my-pi/hashline": "16.2.13", - "@oh-my-pi/omp-stats": "16.2.13", - "@oh-my-pi/pi-agent-core": "16.2.13", - "@oh-my-pi/pi-ai": "16.2.13", - "@oh-my-pi/pi-catalog": "16.2.13", - "@oh-my-pi/pi-mnemopi": "16.2.13", - "@oh-my-pi/pi-natives": "16.2.13", - "@oh-my-pi/pi-tui": "16.2.13", - "@oh-my-pi/pi-utils": "16.2.13", - "@oh-my-pi/pi-wire": "16.2.13", - "@oh-my-pi/snapcompact": "16.2.13", + "@oh-my-pi/hashline": "17.0.6", + "@oh-my-pi/omp-stats": "17.0.6", + "@oh-my-pi/pi-agent-core": "17.0.6", + "@oh-my-pi/pi-ai": "17.0.6", + "@oh-my-pi/pi-catalog": "17.0.6", + "@oh-my-pi/pi-mnemopi": "17.0.6", + "@oh-my-pi/pi-natives": "17.0.6", + "@oh-my-pi/pi-tui": "17.0.6", + "@oh-my-pi/pi-utils": "17.0.6", + "@oh-my-pi/pi-wire": "17.0.6", + "@oh-my-pi/snapcompact": "17.0.6", "@opentelemetry/api": "^1.9.1", - "@opentelemetry/context-async-hooks": "^2.7.1", - "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0", - "@opentelemetry/resources": "^2.7.1", - "@opentelemetry/sdk-trace-base": "^2.7.1", - "@opentelemetry/sdk-trace-node": "^2.7.1", - "@puppeteer/browsers": "^3.0.4", + "@opentelemetry/api-logs": "^0.220.0", + "@opentelemetry/context-async-hooks": "^2.9.0", + "@opentelemetry/exporter-logs-otlp-proto": "^0.220.0", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.220.0", + "@opentelemetry/exporter-trace-otlp-proto": "^0.220.0", + "@opentelemetry/resources": "^2.9.0", + "@opentelemetry/sdk-logs": "^0.220.0", + "@opentelemetry/sdk-metrics": "^2.9.0", + "@opentelemetry/sdk-trace-base": "^2.9.0", + "@opentelemetry/sdk-trace-node": "^2.9.0", + "@puppeteer/browsers": "^3.0.6", "@types/turndown": "5.0.6", "@xterm/headless": "^6.0.0", - "arktype": "^2.2.0", + "arktype": "2.2.3", "chalk": "^5.6.2", "diff": "^9.0.0", - "fast-xml-parser": "^5.9.0", + "fast-xml-parser": "^5.9.3", "handlebars": "^4.7.9", - "linkedom": "^0.18.12", - "lru-cache": "11.5.1", + "header-generator": "^2.1.82", + "linkedom": "^0.18.13", + "lru-cache": "11.5.2", "mammoth": "^1.12.0", - "mupdf": "^1.27.0", - "puppeteer-core": "^25.1.0", + "mupdf": "^1.28.0", + "puppeteer-core": "25.3.0", "turndown": "7.2.4", "turndown-plugin-gfm": "1.0.2", "zod": "^4" @@ -3038,16 +3032,16 @@ } }, "node_modules/@oh-my-pi/pi-mnemopi": { - "version": "16.2.13", - "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-mnemopi/-/pi-mnemopi-16.2.13.tgz", - "integrity": "sha512-qA6uURnOFuOIkd6+xr7PjAj8rcWVq6uzyxws/9fIIRAVNiKNbzyYUbz8Ku+L2hsRpAGC5enRbWFPOaviJuEEuA==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-mnemopi/-/pi-mnemopi-17.0.6.tgz", + "integrity": "sha512-ZQJClNFEw/aT9Xfebt1Ud8SsR27HuWGaaB26219pTl50EvtHOhddLrBVLuaETfyCHr0Y0KWrlnlOxlwnHEpMUg==", "dev": true, "license": "MIT", "dependencies": { - "@oh-my-pi/pi-ai": "16.2.13", - "@oh-my-pi/pi-catalog": "16.2.13", - "@oh-my-pi/pi-utils": "16.2.13", - "lru-cache": "11.5.1" + "@oh-my-pi/pi-ai": "17.0.6", + "@oh-my-pi/pi-catalog": "17.0.6", + "@oh-my-pi/pi-utils": "17.0.6", + "lru-cache": "11.5.2" }, "bin": { "mnemopi": "src/cli.ts" @@ -3069,26 +3063,26 @@ } }, "node_modules/@oh-my-pi/pi-natives": { - "version": "16.2.13", - "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-natives/-/pi-natives-16.2.13.tgz", - "integrity": "sha512-UyqqnW1LP2mUFdXcNnGuVLgw5M1CWqzSYqHV50tzt1GEkf4WaERs51/EjtOhqb8ViMhO+OkXEKtrQdggwXJruA==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-natives/-/pi-natives-17.0.6.tgz", + "integrity": "sha512-QHpZmfkEM0O7nfez516pIZGjkR5Ez5Td8XPcRW54y7zzTXkjQA6ndBvcce8ckjXCOrHU1pGTGc7D1ZQlI1/Vug==", "dev": true, "license": "MIT", "engines": { "bun": ">=1.3.14" }, "optionalDependencies": { - "@oh-my-pi/pi-natives-darwin-arm64": "16.2.13", - "@oh-my-pi/pi-natives-darwin-x64": "16.2.13", - "@oh-my-pi/pi-natives-linux-arm64": "16.2.13", - "@oh-my-pi/pi-natives-linux-x64": "16.2.13", - "@oh-my-pi/pi-natives-win32-x64": "16.2.13" + "@oh-my-pi/pi-natives-darwin-arm64": "17.0.6", + "@oh-my-pi/pi-natives-darwin-x64": "17.0.6", + "@oh-my-pi/pi-natives-linux-arm64": "17.0.6", + "@oh-my-pi/pi-natives-linux-x64": "17.0.6", + "@oh-my-pi/pi-natives-win32-x64": "17.0.6" } }, "node_modules/@oh-my-pi/pi-natives-darwin-arm64": { - "version": "16.2.13", - "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-natives-darwin-arm64/-/pi-natives-darwin-arm64-16.2.13.tgz", - "integrity": "sha512-8rWCepUQBkS604kC4AYCQP+lpKOIsxJ0ExPS/wU8bx5qztyfxg8O4X2oOIOxenOm7WegaZFu5lwvRuXDoAEorQ==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-natives-darwin-arm64/-/pi-natives-darwin-arm64-17.0.6.tgz", + "integrity": "sha512-1xX2dOTLIDvYwLMFxH3C5pC+2TCRsWEfbFL/89rQMdGK55ksaToYEMdG7M2KgkIC4cku5Xa0CWkCjcZeYXrMiw==", "cpu": [ "arm64" ], @@ -3103,9 +3097,9 @@ } }, "node_modules/@oh-my-pi/pi-natives-darwin-x64": { - "version": "16.2.13", - "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-natives-darwin-x64/-/pi-natives-darwin-x64-16.2.13.tgz", - "integrity": "sha512-k4qp+en6eXCfdn9nUzL8x4dDl3lcUcp6J7+GmotdwFXhTGzxk5yskJxRYzdPd1bxAK+3lCvUkAfmWIjl09O/pg==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-natives-darwin-x64/-/pi-natives-darwin-x64-17.0.6.tgz", + "integrity": "sha512-cRFzRQZH8J1SXd3ibbABqxJFlnGtdjyODGCzHJIrdnLnPa1+d/1PldGlNuGenemOKDzitfIe8n2NTTPY4982vw==", "cpu": [ "x64" ], @@ -3120,9 +3114,9 @@ } }, "node_modules/@oh-my-pi/pi-natives-linux-arm64": { - "version": "16.2.13", - "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-natives-linux-arm64/-/pi-natives-linux-arm64-16.2.13.tgz", - "integrity": "sha512-hszV4B89seTnM5Hvv2ITOzlbDVsCkJpZLnYoNLqgObX3ovrqwRQwVbBnX/4wM2U4QqOh91qCK1mpb/ZfZ1qTJQ==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-natives-linux-arm64/-/pi-natives-linux-arm64-17.0.6.tgz", + "integrity": "sha512-Ug6GcZvMx5qBR1UtY2SiPf85TqI4mOiXGREOCpWR3LXTStow5ZV7Q48LAllqvXoMdMCvmGfq4+iYn2Z6p2Kkww==", "cpu": [ "arm64" ], @@ -3137,9 +3131,9 @@ } }, "node_modules/@oh-my-pi/pi-natives-linux-x64": { - "version": "16.2.13", - "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-natives-linux-x64/-/pi-natives-linux-x64-16.2.13.tgz", - "integrity": "sha512-CnYycmySUNe6f6eXOZOaqEIgohU9FbTCmYVyS3MyLhlxA8f+iduwsmyxHma+eX+IhoOkN2VE67JtCHy9DTnK2A==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-natives-linux-x64/-/pi-natives-linux-x64-17.0.6.tgz", + "integrity": "sha512-oVNOtYzT0X8ZZu6D8fGJd2Z9r2w/UdK5ip63KSRBFksXApUhAEyOcjo2OyM+oSRz/Joc0zYAaHod5QIa2X9u5Q==", "cpu": [ "x64" ], @@ -3154,9 +3148,9 @@ } }, "node_modules/@oh-my-pi/pi-natives-win32-x64": { - "version": "16.2.13", - "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-natives-win32-x64/-/pi-natives-win32-x64-16.2.13.tgz", - "integrity": "sha512-8wZqn1AT2twHCx5+kDsljUdEdeZfICAhYrT4+T7DuYTZiieZNjgz7hKnFajFE62mIM5TRptD8cBHdAMiUlLX4g==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-natives-win32-x64/-/pi-natives-win32-x64-17.0.6.tgz", + "integrity": "sha512-Ci2DIa2aeSJ9CDa0wUgG6fOymkSCU2+aR1m0T4tUEZQO3FNUtQLB6g8hYlpebu0/9lADnG8rPPqEfrhC2NxnZQ==", "cpu": [ "x64" ], @@ -3171,29 +3165,29 @@ } }, "node_modules/@oh-my-pi/pi-tui": { - "version": "16.2.13", - "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-tui/-/pi-tui-16.2.13.tgz", - "integrity": "sha512-qbzZco/gIePVM5Guw6Jw5EVywAXOATq9/27GLqKN8pGMRG/sQy5ajoE8FAMNAS+wtb8BAbT1E+9kx3NHfrzbog==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-tui/-/pi-tui-17.0.6.tgz", + "integrity": "sha512-qyQONnFfZFrhbmWxexh2kupXX+HPNiODWaspxeCBQWVVjlNEC9w6GhzfP3y23PfrpWgzYGgS+7KaDQCBpq7irw==", "dev": true, "license": "MIT", "dependencies": { - "@oh-my-pi/pi-natives": "16.2.13", - "@oh-my-pi/pi-utils": "16.2.13", - "lru-cache": "11.5.1", - "marked": "^18.0.5" + "@oh-my-pi/pi-natives": "17.0.6", + "@oh-my-pi/pi-utils": "17.0.6", + "lru-cache": "11.5.2", + "marked": "^18.0.6" }, "engines": { "bun": ">=1.3.14" } }, "node_modules/@oh-my-pi/pi-utils": { - "version": "16.2.13", - "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-utils/-/pi-utils-16.2.13.tgz", - "integrity": "sha512-vhX1BxHAwncCNJFQfqJ/OEwtW1RoETuyfNPFJUbzQAU5I4xsQwfgUJ8GILExf7CJwuuoaDBzmrnscwkv+ySKOQ==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-utils/-/pi-utils-17.0.6.tgz", + "integrity": "sha512-K20KkGhHv5hA4T1hRuGL/lzbxuwHg5MNJZ8dgSzmKE6gwYMJPAMNknOioQCiUz3Gy4qFY0YSOI/SCW+uyhbDBA==", "dev": true, "license": "MIT", "dependencies": { - "@oh-my-pi/pi-natives": "16.2.13", + "@oh-my-pi/pi-natives": "17.0.6", "handlebars": "^4.7.9", "winston": "^3.19.0", "winston-daily-rotate-file": "^5.0.0" @@ -3203,9 +3197,9 @@ } }, "node_modules/@oh-my-pi/pi-wire": { - "version": "16.2.13", - "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-wire/-/pi-wire-16.2.13.tgz", - "integrity": "sha512-tr/TwVhfHMXhqKuQ1eYSLNf97BvYwGQEBlio+M5FZDxtXHxo5bUPG9dkT0j38K8hUeWxRdkbrXbcYZbM7dgucA==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-wire/-/pi-wire-17.0.6.tgz", + "integrity": "sha512-c/QJsRWzNslkm70rh2DRk+0EdP/Sc964Oc/HP2kpdkzTkCvLA7LCoH+RiznKE7xap8k7adj1N8me6EcSih7r6Q==", "dev": true, "license": "MIT", "engines": { @@ -3213,37 +3207,37 @@ } }, "node_modules/@oh-my-pi/snapcompact": { - "version": "16.2.13", - "resolved": "https://registry.npmjs.org/@oh-my-pi/snapcompact/-/snapcompact-16.2.13.tgz", - "integrity": "sha512-qi5i4TC7zPxj0+thnaMvcP36nhKus6k8cz6p9BP+090TylAjMG7kF2A1ccH0RVxmqa7gg8G2d1J1zqC8k6QVdw==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@oh-my-pi/snapcompact/-/snapcompact-17.0.6.tgz", + "integrity": "sha512-13KbdxqQvD1UswgB871dM/N1wDHMewUcSEGkyv/IVXkEWAgoT32qKA5MIAtm7YMCecGxcylP+N18Ysaas02JWA==", "dev": true, "license": "MIT", "dependencies": { - "@oh-my-pi/pi-ai": "16.2.13", - "@oh-my-pi/pi-natives": "16.2.13", - "@oh-my-pi/pi-utils": "16.2.13", - "@oh-my-pi/pi-wire": "16.2.13" + "@oh-my-pi/pi-ai": "17.0.6", + "@oh-my-pi/pi-natives": "17.0.6", + "@oh-my-pi/pi-utils": "17.0.6", + "@oh-my-pi/pi-wire": "17.0.6" }, "engines": { "bun": ">=1.3.14" } }, "node_modules/@opencode-ai/plugin": { - "version": "1.17.13", - "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.17.13.tgz", - "integrity": "sha512-JjoIs6qTPl0IrXo+J1GTqX9lcb2h2dMoW6BWMR2IbTT3MY2FQ/lvBXDzkx0d3zVRaYN+NTmV4u8Nth396xq+sQ==", + "version": "1.18.4", + "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.18.4.tgz", + "integrity": "sha512-Mkq128aLJo4E8Sb2bX8zrRlQ+I2WPaJ/n1kzaor8nTi/K/zNP4t8LGKwyMbuRoD/lhw4veSbzDOASSSypv3mcQ==", "dev": true, "license": "MIT", "dependencies": { "@ai-sdk/provider": "3.0.8", - "@opencode-ai/sdk": "1.17.13", + "@opencode-ai/sdk": "1.18.4", "effect": "4.0.0-beta.83", "zod": "4.1.8" }, "peerDependencies": { - "@opentui/core": ">=0.3.4", - "@opentui/keymap": ">=0.3.4", - "@opentui/solid": ">=0.3.4" + "@opentui/core": ">=0.4.5", + "@opentui/keymap": ">=0.4.5", + "@opentui/solid": ">=0.4.5" }, "peerDependenciesMeta": { "@opentui/core": { @@ -3268,9 +3262,9 @@ } }, "node_modules/@opencode-ai/sdk": { - "version": "1.17.13", - "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.17.13.tgz", - "integrity": "sha512-VItOGjMzRQx3zypwmeFLNhCiIx32kxS7FqzIJvVZLfyNGCifs3rfGC9qzNKWcxQo4SjNvAw++v4gWWU6Inv+JQ==", + "version": "1.18.4", + "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.18.4.tgz", + "integrity": "sha512-p/3P0KtWknoLvpsk8QrUzCKd4Q1A5Z3RmACEvwuQBGxnUy4AGo379lUYMgt7fczFn53079oroLuo6BosQri+HA==", "dev": true, "license": "MIT", "dependencies": { @@ -3288,9 +3282,9 @@ } }, "node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.220.0.tgz", + "integrity": "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3301,9 +3295,9 @@ } }, "node_modules/@opentelemetry/context-async-hooks": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.8.0.tgz", - "integrity": "sha512-/3FIraneMcng67SUJCxvyInk/oxzwsxyadufk0wwfOBLf5wqtAGX4MoQASwSbndBPeARzBryUM9Azr5kHIdWLw==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.10.0.tgz", + "integrity": "sha512-bvyMcgLEkozzSzpEEEo1OMoeQ97bxj6Qs2uN3mPrSdDvObMI1myffD/BPqcLlzZO9//d1SqQA/WPw7Cz2AiqhA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3314,9 +3308,9 @@ } }, "node_modules/@opentelemetry/core": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.1.tgz", - "integrity": "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3329,18 +3323,16 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-proto": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.218.0.tgz", - "integrity": "sha512-r1Msf8SNLRmwh9J6XQ5uh82D7CdDWMNHnPB7LAVHjzut0TkSeKc5KcIvr4SvHvfk/xwN5gxC+VLKQ1k0o8PSPw==", + "node_modules/@opentelemetry/exporter-logs-otlp-proto": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.220.0.tgz", + "integrity": "sha512-8LZAxdJ0ENDAFwr4j0oY35mHBltiSzvlhdQAPGiC7p9VnxtuSq4SW1gfBAdW6t6hiQG6OwUl8w7KHaOdJPKHWg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1" + "@opentelemetry/otlp-exporter-base": "0.220.0", + "@opentelemetry/otlp-transformer": "0.220.0", + "@opentelemetry/sdk-logs": "0.220.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3349,14 +3341,89 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/resources": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", - "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", + "node_modules/@opentelemetry/exporter-metrics-otlp-http": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.220.0.tgz", + "integrity": "sha512-Yqt3RBw/bRVncaE9qIIhk4WfjbAQqXuP9FgAaU+IKPndnLEp/cUqZlSC324+bpmduRz7DoTjig8Ub0PeILWXUA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/otlp-exporter-base": "0.220.0", + "@opentelemetry/otlp-transformer": "0.220.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-metrics": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.9.0.tgz", + "integrity": "sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.220.0.tgz", + "integrity": "sha512-lyO+IQBdSvqHN/ZOW/OzrSWemtfD+HgWngn+HBNLhjy0YrCQQTz0OE/kSekH2Pl340dn9DWzhqHdz5Eftr+HLA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.220.0", + "@opentelemetry/otlp-exporter-base": "0.220.0", + "@opentelemetry/otlp-transformer": "0.220.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-metrics": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", + "@opentelemetry/core": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -3366,15 +3433,51 @@ "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", - "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", + "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.9.0.tgz", + "integrity": "sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.220.0.tgz", + "integrity": "sha512-voTAD8XgJxlK7zLkXh8EzMB09zrQr3tyY/BsnDTlDiQU/UdK58MZ63A3mUjdEDrxMjCVmBHU3WQJhRmQe+Dvzg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/otlp-exporter-base": "0.220.0", + "@opentelemetry/otlp-transformer": "0.220.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-trace": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", + "@opentelemetry/core": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -3385,14 +3488,14 @@ } }, "node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.218.0.tgz", - "integrity": "sha512-ZwqpkNL5W7RyGJPDZ9g06DvKp8KFTWPJPN12anpMQYSKpTSU0z3EIZuPq9vPGpS8siFyOqDYDAuCwlNO9FqgbA==", + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.220.0.tgz", + "integrity": "sha512-CXYo8UD5Mn9YbgebO2EL4wejtA+gxLmLiu6HCk2KH2BR7XhFN6/6p1UlCb23DYCjeYkndevLHuejCCN1yx4+OQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-transformer": "0.218.0" + "@opentelemetry/core": "2.9.0", + "@opentelemetry/otlp-transformer": "0.220.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3402,18 +3505,18 @@ } }, "node_modules/@opentelemetry/otlp-transformer": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.218.0.tgz", - "integrity": "sha512-CFaKH87WAzjuJ4awowTTLzUvMfaRfiOFG5+qm5S5ncyalRtN4ecQ+YmuANJSCrVPuvZFEkUgKhBPBndxi3rHsQ==", + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.220.0.tgz", + "integrity": "sha512-lXGrv7KXZ0gNH9SVNUaa6vv6phVYGvJxfXAlMbzbakiXru75f5MZl8Z7oqiMMQD77riVHJCFlQvbZs/VVN2/4A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-logs": "0.218.0", - "@opentelemetry/sdk-metrics": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1" + "@opentelemetry/api-logs": "0.220.0", + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-logs": "0.220.0", + "@opentelemetry/sdk-metrics": "2.9.0", + "@opentelemetry/sdk-trace": "2.9.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3423,13 +3526,13 @@ } }, "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", - "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", + "@opentelemetry/core": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -3439,32 +3542,31 @@ "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", - "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.9.0.tgz", + "integrity": "sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "node_modules/@opentelemetry/resources": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", - "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.10.0.tgz", + "integrity": "sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.8.0", + "@opentelemetry/core": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -3475,9 +3577,9 @@ } }, "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", - "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3491,15 +3593,15 @@ } }, "node_modules/@opentelemetry/sdk-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.218.0.tgz", - "integrity": "sha512-QvnNdugatFTVCJXH0Mcu7GOOJSylA9j127kIezOE4YwTI4YbowRons2K4WZTv5FMS8T4q9P0NdaRHdkSmeAIag==", + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.220.0.tgz", + "integrity": "sha512-WywcTkQtv2iNmt+6y5Kcd4rzvx9bLVsBa2Nwcmg01IUaBTkTow3W4d9KE5vNBpEDtb9tp21WcRBY/lANRrApYA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", + "@opentelemetry/api-logs": "0.220.0", + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -3510,13 +3612,13 @@ } }, "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", - "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", + "@opentelemetry/core": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -3527,14 +3629,14 @@ } }, "node_modules/@opentelemetry/sdk-metrics": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.1.tgz", - "integrity": "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.10.0.tgz", + "integrity": "sha512-t6r1VSvXNtSDnPXU1FbZeetJb7yyovHmgu0wRSoftxtE0g2rSNhQZQUy69sRUCL+iioJpX8SN/S6wq6ZtvLySQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1" + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3543,14 +3645,31 @@ "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", - "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", + "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/core": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.9.0.tgz", + "integrity": "sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -3561,14 +3680,15 @@ } }, "node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz", - "integrity": "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.10.0.tgz", + "integrity": "sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/resources": "2.8.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -3579,9 +3699,9 @@ } }, "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/core": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", - "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3594,16 +3714,34 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/sdk-trace": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.10.0.tgz", + "integrity": "sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/sdk-trace-node": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.8.0.tgz", - "integrity": "sha512-nZt9OGufioAc3AfoLTqA9bsAeaMJAictYDdI2VcNQ+PmT+3rfKjAZDZvgPfd8VPX0O5Bw1hdQF6kDK8VSpZiWg==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.10.0.tgz", + "integrity": "sha512-GZK/G6oZyBLGlH1pUgeDch7D91KoHd2uotUGIkWCPi9GI5T9X0p4L7nNAMDR1BQjkRYoDqo+ddfVx9t5Uhys+Q==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/context-async-hooks": "2.8.0", - "@opentelemetry/core": "2.8.0", - "@opentelemetry/sdk-trace-base": "2.8.0" + "@opentelemetry/context-async-hooks": "2.10.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/sdk-trace-base": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3613,9 +3751,9 @@ } }, "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/core": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", - "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3628,10 +3766,27 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, + "node_modules/@opentelemetry/sdk-trace/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.41.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", - "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz", + "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3742,6 +3897,19 @@ } } }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, "node_modules/@so-ric/colorspace": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", @@ -3761,50 +3929,39 @@ "license": "MIT" }, "node_modules/@tailwindcss/node": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz", - "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "5.21.6", + "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.3.2" + "tailwindcss": "4.3.3" } }, "node_modules/@types/bun": { - "version": "1.2.22", - "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.2.22.tgz", - "integrity": "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.3.14.tgz", + "integrity": "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw==", "dev": true, "license": "MIT", "dependencies": { - "bun-types": "1.2.22" + "bun-types": "1.3.14" } }, "node_modules/@types/node": { - "version": "24.3.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", - "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.10.0" - } - }, - "node_modules/@types/react": { - "version": "19.2.17", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", - "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "csstype": "^3.2.2" + "undici-types": "~6.21.0" } }, "node_modules/@types/triple-beam": { @@ -3821,78 +3978,417 @@ "dev": true, "license": "MIT" }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.13", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", - "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">=10.0.0" + "node": ">=16.20.0" } }, - "node_modules/@xterm/headless": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.0.0.tgz", - "integrity": "sha512-5Yj1QINYCyzrZtf8OFIHi47iQtI+0qYFPHmouEfG8dHNxbZ9Tb9YGSuLcsEwj9Z+OL75GJqPyJbyoFer80a2Hw==", - "dev": true, - "license": "MIT", - "workspaces": [ - "addons/*" - ] - }, - "node_modules/adm-zip": { - "version": "0.5.18", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.18.tgz", - "integrity": "sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng==", + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=12.0" + "node": ">=16.20.0" } }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=16.20.0" } }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=16.20.0" } }, - "node_modules/anynum": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", - "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" ], - "license": "MIT" + "engines": { + "node": ">=16.20.0" + } }, - "node_modules/argparse": { - "version": "1.0.10", + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@xterm/headless": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.0.0.tgz", + "integrity": "sha512-5Yj1QINYCyzrZtf8OFIHi47iQtI+0qYFPHmouEfG8dHNxbZ9Tb9YGSuLcsEwj9Z+OL75GJqPyJbyoFer80a2Hw==", + "dev": true, + "license": "MIT", + "workspaces": [ + "addons/*" + ] + }, + "node_modules/adm-zip": { + "version": "0.5.18", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.18.tgz", + "integrity": "sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anynum": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", + "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, @@ -3902,25 +4398,25 @@ } }, "node_modules/arkregex": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/arkregex/-/arkregex-0.0.7.tgz", - "integrity": "sha512-O/Ltrn9EUSn3ui0KVzfyrWGDUsHlzKxDVBtpQxL/6JmLRMAZAebfSNf/A/J5Ny5S6QIwrXX+RfXsu888HMs35A==", + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/arkregex/-/arkregex-0.0.8.tgz", + "integrity": "sha512-PJcx6G1kQTgLKPUbeYlYecDRaKq15AMSGVajlKFYWlPeJRQL+j3dKE6tyMs40HZ99djS1l9Vhl3ezAHy9JBIqQ==", "dev": true, "license": "MIT", "dependencies": { - "@ark/util": "0.56.1" + "@ark/util": "0.56.2" } }, "node_modules/arktype": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/arktype/-/arktype-2.2.2.tgz", - "integrity": "sha512-YYf1xhL2dh5aPZFlsY0RAsxv5HZqfLGLptH2ZP3JidTmsGRW8VOymhPjjMTkerL12vR2YtX0SK4c1mATtae8SA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/arktype/-/arktype-2.2.3.tgz", + "integrity": "sha512-7W+0RLTUNJiBFIIZXwOQxSR8Z273IAd6IvqBeG9+gHnQKFsIx2C0iOtGTmMrPnlX4qLXyc5+ll7A0BIj9WrbTg==", "dev": true, "license": "MIT", "dependencies": { - "@ark/schema": "0.56.1", - "@ark/util": "0.56.1", - "arkregex": "0.0.7" + "@ark/schema": "0.56.2", + "@ark/util": "0.56.2", + "arkregex": "0.0.8" } }, "node_modules/async": { @@ -3951,6 +4447,19 @@ ], "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.0.tgz", + "integrity": "sha512-oCu2wfipvX3AePSgmOuKkIywOu+8n9psz7hXYmk56ghpu3+7KzNIBopaOs4c9BrtdnTtW30unG9GTfHo7EwERQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/bluebird": { "version": "3.4.7", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", @@ -3959,11 +4468,18 @@ "license": "MIT" }, "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-2.0.0.tgz", + "integrity": "sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==", "dev": true, - "license": "ISC" + "license": "ISC", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } }, "node_modules/boolean": { "version": "3.2.0", @@ -3974,19 +4490,81 @@ "license": "MIT", "optional": true }, + "node_modules/browserslist": { + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/bun-types": { - "version": "1.2.22", - "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.2.22.tgz", - "integrity": "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.14.tgz", + "integrity": "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*" - }, - "peerDependencies": { - "@types/react": "^19" + "engines": { + "node": ">=6" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -4128,32 +4706,37 @@ } }, "node_modules/css-select": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", - "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-7.0.0.tgz", + "integrity": "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" + "boolbase": "^2.0.0", + "css-what": "^8.0.0", + "domhandler": "^6.0.1", + "domutils": "^4.0.2", + "nth-check": "^3.0.1" + }, + "engines": { + "node": ">=20.19.0" }, "funding": { + "type": "github", "url": "https://github.com/sponsors/fb55" } }, "node_modules/css-what": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-8.0.0.tgz", + "integrity": "sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw==", "dev": true, "license": "BSD-2-Clause", "engines": { - "node": ">= 6" + "node": ">=20.19.0" }, "funding": { + "type": "github", "url": "https://github.com/sponsors/fb55" } }, @@ -4164,14 +4747,6 @@ "dev": true, "license": "MIT" }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/date-fns": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz", @@ -4264,24 +4839,28 @@ "license": "BSD-2-Clause" }, "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.1.1.tgz", + "integrity": "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==", "dev": true, "license": "MIT", "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0", + "entities": "^8.0.0" + }, + "engines": { + "node": ">=20.19.0" }, "funding": { + "type": "github", "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz", + "integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==", "dev": true, "funding": [ { @@ -4289,39 +4868,63 @@ "url": "https://github.com/sponsors/fb55" } ], - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + } }, "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz", + "integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "domelementtype": "^2.3.0" + "domelementtype": "^3.0.0" }, "engines": { - "node": ">= 4" + "node": ">=20.19.0" }, "funding": { + "type": "github", "url": "https://github.com/fb55/domhandler?sponsor=1" } }, "node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz", + "integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" + "dom-serializer": "^3.0.0", + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0" + }, + "engines": { + "node": ">=20.19.0" }, "funding": { + "type": "github", "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dot-prop": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", + "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/duck": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", @@ -4351,6 +4954,13 @@ "yaml": "^2.9.0" } }, + "node_modules/electron-to-chromium": { + "version": "1.5.395", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.395.tgz", + "integrity": "sha512-7zt9Aw+SrmxLWLN0zhaTWZQiCdryLVrYTq5R7iZakLvi2UQPYMMsROYV/2qVCzMeCiSXHwKOU+sZ4zOVVlrtKA==", + "dev": true, + "license": "ISC" + }, "node_modules/emoji-regex": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", @@ -4366,9 +4976,9 @@ "license": "MIT" }, "node_modules/enhanced-resolve": { - "version": "5.21.6", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", - "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", + "version": "5.24.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.3.tgz", + "integrity": "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4380,13 +4990,13 @@ } }, "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", "dev": true, "license": "BSD-2-Clause", "engines": { - "node": ">=0.12" + "node": ">=20.19.0" }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" @@ -4487,9 +5097,9 @@ } }, "node_modules/fast-xml-parser": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.9.3.tgz", - "integrity": "sha512-brCNCeScma/kqa54J4PIDriSSSLssRkuYaUCpvHJulGc3HGI/xxKUCTDcYkAdqJsyb//ydpbxecjC3hB9+tb/g==", + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.10.1.tgz", + "integrity": "sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw==", "dev": true, "funding": [ { @@ -4499,17 +5109,33 @@ ], "license": "MIT", "dependencies": { - "@nodable/entities": "^2.2.0", + "@nodable/entities": "^3.0.0", "fast-xml-builder": "^1.2.0", - "is-unsafe": "^1.0.1", - "path-expression-matcher": "^1.5.0", + "is-unsafe": "^2.0.0", + "path-expression-matcher": "^1.6.2", "strnum": "^2.4.1", - "xml-naming": "^0.1.0" + "xml-naming": "^0.3.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, + "node_modules/fast-xml-parser/node_modules/xml-naming": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz", + "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", @@ -4549,6 +5175,17 @@ "dev": true, "license": "MIT" }, + "node_modules/generative-bayesian-network": { + "version": "2.1.83", + "resolved": "https://registry.npmjs.org/generative-bayesian-network/-/generative-bayesian-network-2.1.83.tgz", + "integrity": "sha512-LssI9es+oUoezoHloFGw0Hts0YEfujjBOE8KNl70oBt4HPjD/4rpUqcgZ/M7RCmgKvkmCyPB2KWowyJHuKyhfw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "adm-zip": "^0.5.9", + "tslib": "^2.4.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -4674,6 +5311,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/header-generator": { + "version": "2.1.82", + "resolved": "https://registry.npmjs.org/header-generator/-/header-generator-2.1.82.tgz", + "integrity": "sha512-4NjPB0+bAKjPoponSmTOkK58IEF2W22sOJA5O48k/MxbCZgOm+jrU4WVR53Z2I6xFgIPkVrQmKtt1LAbWtfqXw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "browserslist": "^4.21.1", + "generative-bayesian-network": "^2.1.82", + "ow": "^0.28.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/html-escaper": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", @@ -4701,6 +5354,78 @@ "entities": "^7.0.1" } }, + "node_modules/htmlparser2/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/htmlparser2/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/htmlparser2/node_modules/entities": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", @@ -4738,6 +5463,16 @@ "node": "^22.22.2 || ^24.15.0 || >=26.0.0" } }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -4752,9 +5487,9 @@ } }, "node_modules/is-unsafe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-1.0.1.tgz", - "integrity": "sha512-CLK2+VdgERgD96EYm5lUQssZYlRg2tkZnbsxZoacmSiRxiFJ4Nk4SzjCl+Ur+v3kXIY9dTIdb3IH22y1mZ56LA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.0.tgz", + "integrity": "sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA==", "dev": true, "funding": [ { @@ -5114,16 +5849,16 @@ } }, "node_modules/linkedom": { - "version": "0.18.12", - "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.12.tgz", - "integrity": "sha512-jalJsOwIKuQJSeTvsgzPe9iJzyfVaEJiEXl+25EkKevsULHvMJzpNqwvj1jOESWdmgKDiXObyjOYwlUqG7wo1Q==", + "version": "0.18.13", + "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.13.tgz", + "integrity": "sha512-ES/o9qotMpzpN2MHs+Iq/JcVoOj8Fa5wiQYrTdFpvAnwXL0g66XHHUc9WUMk6nAlBtGsFQ24ne+SYnvnaQ2FSw==", "dev": true, "license": "ISC", "dependencies": { - "css-select": "^5.1.0", + "css-select": "^7.0.0", "cssom": "^0.5.0", "html-escaper": "^3.0.3", - "htmlparser2": "^10.0.0", + "htmlparser2": "^10.1.0", "uhyphen": "^0.2.0" }, "engines": { @@ -5138,6 +5873,14 @@ } } }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "dev": true, + "license": "MIT" + }, "node_modules/logform": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", @@ -5177,9 +5920,9 @@ } }, "node_modules/lru-cache": { - "version": "11.5.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", - "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -5187,9 +5930,9 @@ } }, "node_modules/lucide-react": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.23.0.tgz", - "integrity": "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw==", + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.25.0.tgz", + "integrity": "sha512-/mdJTRbiwcLOQ1NZZK1amZF9rIZyvO18D6r9TngE6TG1NmqHgFuT4eE7Xrkm9UsXMbBJD1NlfwHVltCDWHrOTw==", "dev": true, "license": "ISC", "peerDependencies": { @@ -5232,9 +5975,9 @@ } }, "node_modules/marked": { - "version": "18.0.5", - "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz", - "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==", + "version": "18.0.7", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.7.tgz", + "integrity": "sha512-iDVQ5ldaiKXn6b2JroX5kgRfmwgqolW7NpaEzTl1k/2Zh1njIEN9yniyLV/mOvWwtsE8OGgkjsCYvijuPk1dtA==", "dev": true, "license": "MIT", "bin": { @@ -5372,16 +6115,30 @@ "node-gyp-build-optional-packages-test": "build-test.js" } }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-3.0.1.tgz", + "integrity": "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "boolbase": "^1.0.0" + "boolbase": "^2.0.0" + }, + "engines": { + "node": ">=20.19.0" }, "funding": { + "type": "github", "url": "https://github.com/fb55/nth-check?sponsor=1" } }, @@ -5447,6 +6204,26 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/ow": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/ow/-/ow-0.28.2.tgz", + "integrity": "sha512-dD4UpyBh/9m4X2NVjA+73/ZPBRF+uF4zIMFvvQsabMiEK8x41L3rQ8EENOi35kyyoaJwNxEeJcP6Fj1H4U409Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.2.0", + "callsites": "^3.1.0", + "dot-prop": "^6.0.1", + "lodash.isequal": "^4.5.0", + "vali-date": "^1.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -5455,9 +6232,9 @@ "license": "(MIT AND Zlib)" }, "node_modules/path-expression-matcher": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.1.tgz", - "integrity": "sha512-h7bxdzhHk8Knyc4Tj+jMaa7fEEoUJy7p1qtbVgkYg1Uhpe5Np5VuGXCRZnkZvU+Q42M1vStt0ifa3ueykRJPmQ==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", + "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==", "dev": true, "funding": [ { @@ -5490,6 +6267,13 @@ "node": ">=8" } }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, "node_modules/platform": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", @@ -5506,9 +6290,9 @@ "license": "MIT" }, "node_modules/protobufjs": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", - "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", "dev": true, "hasInstallScript": true, "license": "BSD-3-Clause", @@ -5979,9 +6763,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz", - "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", "dev": true, "license": "MIT" }, @@ -6031,8 +6815,7 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD", - "optional": true + "license": "0BSD" }, "node_modules/turndown": { "version": "7.2.4", @@ -6077,17 +6860,38 @@ "license": "MIT" }, "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", "dev": true, "license": "Apache-2.0", "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" + "tsc": "bin/tsc" }, "engines": { - "node": ">=14.17" + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" } }, "node_modules/uglify-js": { @@ -6119,12 +6923,43 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -6146,6 +6981,16 @@ "uuid": "dist-node/bin/uuid" } }, + "node_modules/vali-date": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz", + "integrity": "sha512-sgECfZthyaCKW10N0fm27cg8HYTFK5qMWgypqkXMQ4Wbl/zZKx7xZICgcoxIIE+WFAP/MBL2EFwC/YvLxw3Zeg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/webdriver-bidi-protocol": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz", diff --git a/package.json b/package.json index 595b3fd3..b64997a6 100644 --- a/package.json +++ b/package.json @@ -2,16 +2,20 @@ "name": "hcom-plugin-typecheck", "private": true, "description": "Type-checks the embedded agent plugins against their upstream SDK types.", + "engines": { + "node": ">=22.22.2 <23" + }, + "packageManager": "npm@10.9.8", "scripts": { "typecheck": "tsc --noEmit" }, "devDependencies": { - "@earendil-works/pi-coding-agent": "0.80.3", - "@oh-my-pi/pi-coding-agent": "16.2.13", - "@opencode-ai/plugin": "1.17.13", - "@opencode-ai/sdk": "1.17.13", - "@types/bun": "1.2.22", - "@types/node": "24.3.1", - "typescript": "6.0.3" + "@earendil-works/pi-coding-agent": "0.81.1", + "@oh-my-pi/pi-coding-agent": "17.0.6", + "@opencode-ai/plugin": "1.18.4", + "@opencode-ai/sdk": "1.18.4", + "@types/bun": "1.3.14", + "@types/node": "22.20.1", + "typescript": "7.0.2" } } diff --git a/pyproject.toml b/pyproject.toml index a4928d93..33b3570b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["maturin>=1.12.6,<2.0"] +requires = ["maturin>=1.14.1,<2.0"] build-backend = "maturin" [project] diff --git a/rust-toolchain.toml b/rust-toolchain.toml index d0ead5ec..59277fee 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "stable" +channel = "1.97.1" components = ["clippy", "rustfmt"] diff --git a/scripts/install-mock-tools.ps1 b/scripts/install-mock-tools.ps1 index 6bc0c1df..68522365 100644 --- a/scripts/install-mock-tools.ps1 +++ b/scripts/install-mock-tools.ps1 @@ -18,8 +18,8 @@ $cache = if ($env:HCOM_MOCK_TOOLS_NPM_CACHE) { if (-not $Packages -or $Packages.Count -eq 0) { $Packages = @( - "@openai/codex@0.141.0", - "@anthropic-ai/claude-code@2.1.185" + "@openai/codex@0.145.0", + "@anthropic-ai/claude-code@2.1.216" ) } diff --git a/scripts/install-mock-tools.sh b/scripts/install-mock-tools.sh index 4cde348f..6d570712 100755 --- a/scripts/install-mock-tools.sh +++ b/scripts/install-mock-tools.sh @@ -11,8 +11,8 @@ if [[ "$#" -gt 0 ]]; then packages=("$@") else packages=( - "@openai/codex@0.141.0" - "@anthropic-ai/claude-code@2.1.185" + "@openai/codex@0.145.0" + "@anthropic-ai/claude-code@2.1.216" ) fi @@ -32,7 +32,7 @@ for package in "${packages[@]}"; do claude_version="${package##*@}" ;; @anthropic-ai/claude-code) - claude_version="2.1.185" + claude_version="2.1.216" ;; @anthropic-ai/claude-code-*) has_claude_native=1 diff --git a/scripts/typecheck.sh b/scripts/typecheck.sh index 82878a94..614ae1c1 100644 --- a/scripts/typecheck.sh +++ b/scripts/typecheck.sh @@ -27,6 +27,8 @@ cd "$project_root" if [[ "${CI:-}" == "true" ]]; then npm ci --ignore-scripts else - npm install --ignore-scripts --prefer-offline + # CI enforces the pinned Node 22 runtime. Local typechecking can also run on a + # newer Node even when the user's global npm config enables engine-strict. + npm install --ignore-scripts --prefer-offline --engine-strict=false fi npm run typecheck diff --git a/src/commands/config.rs b/src/commands/config.rs index 4dc5d7f1..999a8df5 100644 --- a/src/commands/config.rs +++ b/src/commands/config.rs @@ -154,6 +154,11 @@ pub const CONFIG_KEYS: &[(&str, &str, &str)] = &[ "Export instance name to custom env var", "string", ), + ( + "HCOM_TITLE_MODE", + "Terminal title mode (combined | label | off)", + "string", + ), ( "HCOM_RELAY", "Relay MQTT broker URL (config file only)", @@ -195,6 +200,7 @@ const INSTANCE_KEYS: &[(&str, &str)] = &[ fn toml_path_for_key(field_name: &str) -> Option<&'static str> { match field_name { "terminal" => Some("terminal.active"), + "title_mode" => Some("terminal.title_mode"), "tag" => Some("launch.tag"), "hints" => Some("launch.hints"), "notes" => Some("launch.notes"), @@ -366,6 +372,16 @@ fn config_set_at_path(path: &Path, key: &str, value: &str) -> Result<(), String> } } + if field_name == "title_mode" + && !value.is_empty() + && !crate::shared::VALID_TITLE_MODES.contains(&value) + { + return Err(format!( + "title_mode must be one of: {}. Got '{value}'", + crate::shared::VALID_TITLE_MODES.join(", ") + )); + } + if let Some(dotted_path) = toml_path_for_key(&field_name) { set_nested_toml(&mut doc, dotted_path, value); } else { @@ -433,6 +449,7 @@ pub fn config_get(key: &str) -> (String, &'static str) { "HCOM_SUBAGENT_TIMEOUT" => "30", "HCOM_AUTO_APPROVE" => "true", "HCOM_AUTO_TRUST_WORKSPACE" => "true", + "HCOM_TITLE_MODE" => "combined", _ => "", }; (default.to_string(), "default") @@ -1526,6 +1543,34 @@ Notes: - See 'hcom events --help' for subscription management", ), + "HCOM_TITLE_MODE" => Some( + "\ +HCOM_TITLE_MODE - What appears in the terminal/tab title for hcom-launched agents + +Default: combined + +Purpose: + Controls whether hcom replaces, combines, or leaves alone the wrapped tool's + terminal title. In combined mode, hcom keeps its live status and appends the + tool's own live title (for example, a Codex spinner). + +Values: + combined - Show '{icon} name - {tool title}' and update it live. + label - Show hcom's status label only: '{icon} name [tool]'. + off - Pass the tool's own terminal title through unchanged; hcom writes none. + +Usage: + hcom config title_mode combined # hcom status + live tool title (default) + hcom config title_mode label # hcom status label only + hcom config title_mode off # use the tool's title + hcom config title_mode # reset to the default + +Notes: + - This affects terminal/tab titles, not the visible PTY output. + - Tools that do not emit terminal titles have no live child title to append. + - The same setting can be provided with HCOM_TITLE_MODE in the environment.", + ), + "HCOM_NAME_EXPORT" => Some( "\ HCOM_NAME_EXPORT - Export instance name to custom env var diff --git a/src/commands/help.rs b/src/commands/help.rs index 626fd545..685727e1 100644 --- a/src/commands/help.rs +++ b/src/commands/help.rs @@ -487,6 +487,10 @@ const CONFIG_HELP: &[HelpEntry] = &[ "Auto-trust launch dir (skip folder-trust prompt)", ), (" name_export", "Export agent name to custom env var"), + ( + " title_mode", + "Terminal/tab title: combined, label, or off", + ), ("", "hcom config --info for details"), ("", ""), ("", "Precedence: defaults < config.toml < env vars"), diff --git a/src/config.rs b/src/config.rs index 46423719..4f307f22 100644 --- a/src/config.rs +++ b/src/config.rs @@ -149,6 +149,7 @@ const TOML_KEY_MAP: &[(&str, &str)] = &[ ("auto_approve", "preferences.auto_approve"), ("name_export", "preferences.name_export"), ("auto_trust_workspace", "launch.auto_trust_workspace"), + ("title_mode", "terminal.title_mode"), ]; /// Mapping: HcomConfig field name -> HCOM_* env var key. @@ -186,6 +187,7 @@ const FIELD_TO_ENV: &[(&str, &str)] = &[ ("auto_subscribe", "HCOM_AUTO_SUBSCRIBE"), ("name_export", "HCOM_NAME_EXPORT"), ("auto_trust_workspace", "HCOM_AUTO_TRUST_WORKSPACE"), + ("title_mode", "HCOM_TITLE_MODE"), ]; /// Relay fields — file-only, no env var override. @@ -297,6 +299,11 @@ pub struct HcomConfig { pub auto_subscribe: String, pub name_export: String, pub auto_trust_workspace: bool, + /// Terminal-title behavior: `"combined"` (default) shows + /// `{icon} name - {tool's live title}`, `"label"` shows hcom's + /// `{icon} name [tool]` only, `"off"` leaves the tool's own title untouched. + /// See [`crate::shared::TitleMode`]. + pub title_mode: String, } impl Default for HcomConfig { @@ -330,6 +337,7 @@ impl Default for HcomConfig { auto_subscribe: "collision".to_string(), name_export: String::new(), auto_trust_workspace: true, + title_mode: "combined".to_string(), } } } @@ -414,6 +422,17 @@ impl HcomConfig { ); } + if !crate::shared::VALID_TITLE_MODES.contains(&self.title_mode.as_str()) { + errors.insert( + "title_mode".into(), + format!( + "title_mode must be one of: {}. Got '{}'", + crate::shared::VALID_TITLE_MODES.join(", "), + self.title_mode + ), + ); + } + // Validate shell-quoted args fields for (field, value) in [ ("claude_args", &self.claude_args), @@ -504,6 +523,7 @@ impl HcomConfig { "auto_trust_workspace" => { Some(if self.auto_trust_workspace { "1" } else { "0" }.into()) } + "title_mode" => Some(self.title_mode.clone()), _ => None, } } @@ -554,6 +574,10 @@ impl HcomConfig { "auto_subscribe" => self.auto_subscribe = value.to_string(), "name_export" => self.name_export = value.to_string(), "auto_trust_workspace" => self.auto_trust_workspace = !is_falsy(value), + // Stored leniently; `TitleMode::from_config` maps unknown → default. + // The CLI set path (`config_set_at_path`) validates against + // `VALID_TITLE_MODES` before this is ever written to the file. + "title_mode" => self.title_mode = value.to_string(), _ => return Err(format!("unknown field: {field}")), } Ok(()) @@ -667,6 +691,7 @@ impl HcomConfig { "codex_system_prompt", "auto_subscribe", "name_export", + "title_mode", ]; for str_field in &str_fields { if let Some(val) = get_var(str_field) { @@ -979,6 +1004,7 @@ pub fn load_toml_presets(path: &std::path::Path) -> Option { fn default_toml_structure() -> toml::Value { let toml_str = r#"[terminal] active = "default" +title_mode = "combined" [relay] url = "" @@ -2080,6 +2106,20 @@ mod tests { assert!(!config.relay_enabled); } + #[test] + fn test_load_from_sources_title_mode_and_env_override() { + let mut file_config = HashMap::new(); + file_config.insert( + "title_mode".to_string(), + TomlFieldValue::Str("label".to_string()), + ); + let mut env = HashMap::new(); + env.insert("HCOM_TITLE_MODE".to_string(), "off".to_string()); + + let config = HcomConfig::load_from_sources(&file_config, Some(&env)).unwrap(); + assert_eq!(config.title_mode, "off"); + } + #[test] fn test_load_from_sources_env_overrides_toml() { let mut file_config = HashMap::new(); @@ -2371,6 +2411,7 @@ auto_approve = false let structure = default_toml_structure(); // Verify key paths exist assert!(get_nested(&structure, "terminal.active").is_some()); + assert!(get_nested(&structure, "terminal.title_mode").is_some()); assert!(get_nested(&structure, "launch.tag").is_some()); assert!(get_nested(&structure, "launch.claude.args").is_some()); assert!(get_nested(&structure, "relay.url").is_some()); diff --git a/src/db/kv.rs b/src/db/kv.rs index 57634e6c..4ee02434 100644 --- a/src/db/kv.rs +++ b/src/db/kv.rs @@ -5,6 +5,14 @@ use rusqlite::params; use super::HcomDb; +fn prefix_like_pattern(prefix: &str) -> String { + let escaped = prefix + .replace('\\', "\\\\") + .replace('%', "\\%") + .replace('_', "\\_"); + format!("{escaped}%") +} + impl HcomDb { /// Get value from kv table. pub fn kv_get(&self, key: &str) -> Result> { @@ -38,12 +46,7 @@ impl HcomDb { /// Get all kv entries whose key starts with prefix. Returns Vec<(key, value)>. pub fn kv_prefix(&self, prefix: &str) -> Result> { - // Escape LIKE wildcards (%, _, \) in prefix to avoid unintended matches - let escaped = prefix - .replace('\\', "\\\\") - .replace('%', "\\%") - .replace('_', "\\_"); - let pattern = format!("{}%", escaped); + let pattern = prefix_like_pattern(prefix); let mut stmt = self .conn .prepare_cached("SELECT key, value FROM kv WHERE key LIKE ? ESCAPE '\\'")?; @@ -55,6 +58,16 @@ impl HcomDb { .collect(); Ok(rows) } + + /// Delete all kv entries whose key starts with prefix. Returns count deleted. + pub fn kv_delete_prefix(&self, prefix: &str) -> Result { + let pattern = prefix_like_pattern(prefix); + let n = self.conn.execute( + "DELETE FROM kv WHERE key LIKE ? ESCAPE '\\'", + params![pattern], + )?; + Ok(n) + } } #[cfg(test)] @@ -107,4 +120,23 @@ mod tests { cleanup_test_db(db_path); } + + #[test] + fn test_kv_delete_prefix() { + let (db, db_path) = setup_full_test_db(); + + db.kv_set("claim:100%_done:1", Some("one")).unwrap(); + db.kv_set("claim:100%_done:2", Some("two")).unwrap(); + db.kv_set("claim:100x_done:1", Some("other")).unwrap(); + + assert_eq!(db.kv_delete_prefix("claim:100%_done:").unwrap(), 2); + assert!(db.kv_get("claim:100%_done:1").unwrap().is_none()); + assert!(db.kv_get("claim:100%_done:2").unwrap().is_none()); + assert_eq!( + db.kv_get("claim:100x_done:1").unwrap(), + Some("other".to_string()) + ); + + cleanup_test_db(db_path); + } } diff --git a/src/db/mod.rs b/src/db/mod.rs index ca0ea087..73851b3e 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -18,7 +18,7 @@ use anyhow::{Context, Result}; use chrono::Utc; -use rusqlite::Connection; +use rusqlite::{Connection, Transaction, TransactionBehavior}; use crate::shared::time::now_epoch_f64; @@ -122,6 +122,21 @@ impl HcomDb { &self.conn } + /// Run `f` inside a `BEGIN IMMEDIATE` transaction and commit on success. + /// + /// The immediate write lock makes read-modify-write sequences atomic + /// across the separate database connections used by hook processes. + /// Queries inside `f` must use the provided transaction. + pub fn with_immediate_transaction( + &self, + f: impl FnOnce(&Transaction<'_>) -> Result, + ) -> Result { + let txn = Transaction::new_unchecked(&self.conn, TransactionBehavior::Immediate)?; + let result = f(&txn)?; + txn.commit()?; + Ok(result) + } + /// Access the filesystem path backing this DB handle. pub fn path(&self) -> &std::path::Path { &self.db_path diff --git a/src/hooks/claude.rs b/src/hooks/claude.rs index 1cebccdb..89caf3ca 100644 --- a/src/hooks/claude.rs +++ b/src/hooks/claude.rs @@ -39,6 +39,10 @@ const HOOK_SUBAGENT_STOP: &str = "subagent-stop"; const HOOK_SESSIONEND: &str = "sessionend"; const HOOK_POLL: &str = "poll"; +fn is_subagent_tool(tool_name: &str) -> bool { + matches!(tool_name, "Agent" | "Task") +} + /// Handle a Claude hook — entry point from router. /// /// Reads JSON from stdin, builds context, dispatches to appropriate handler. @@ -235,93 +239,69 @@ fn route_claude_hook( return (result.0, result.1, None, timing); } - // Task transitions + // Claude identifies the hook's actor with agent_id. All actors share the + // root session_id, while running_tasks is lifecycle state rather than + // identity, so actor routing must happen before task-state handling. + let raw_agent_id = payload + .raw + .get("agent_id") + .and_then(|v| v.as_str()) + .filter(|s| !s.is_empty()) + .map(str::to_string); + + if let Some(agent_id) = raw_agent_id { + // Every Claude subagent carries agent_id on its hooks regardless of + // whether its root ever ran `hcom start` — that's a property of + // Claude's hook schema, not of hcom participation. Act only if the + // shared session_id actually has an hcom root binding; otherwise this + // whole branch (including the `hcom start --name ...` hint at + // SubagentStart) must be a silent no-op, same as any other + // nonparticipant hook. + let Ok(Some(root_name)) = db.get_session_binding(&session_id) else { + return (0, String::new(), None, timing); + }; + let subagent_check_start = Instant::now(); + let (exit_code, stdout, delivery_ack) = route_subagent_actor_hook( + db, + hook_type, + payload, + &agent_id, + &session_id, + &root_name, + &mut timing, + ); + timing.subagent_check_ms = Some(subagent_check_start.elapsed().as_secs_f64() * 1000.0); + return (exit_code, stdout, delivery_ack, timing); + } + + // ---- Root/main-thread hook (no agent_id) from here on ---- + let tool_name = payload.tool_name.as_str(); - // Claude Code renamed the Task tool to Agent (Task kept as legacy alias), - // so match both to keep subagent lifecycle tracking working across versions (useless change!). - let is_task_tool = tool_name == "Task" || tool_name == "Agent"; - if hook_type == HOOK_PRE && is_task_tool { + if hook_type == HOOK_PRE && is_subagent_tool(tool_name) { let task_start = Instant::now(); - let (stdout, exit_code) = start_task(db, &session_id, &payload.raw); + let (exit_code, stdout) = match db.get_session_binding(&session_id).ok().flatten() { + Some(instance_name) => start_task(db, &instance_name, &session_id, &payload.raw), + None => (0, String::new()), + }; timing.task_ms = Some(task_start.elapsed().as_secs_f64() * 1000.0); return (exit_code, stdout, None, timing); } - if hook_type == HOOK_POST && is_task_tool { + if hook_type == HOOK_POST && is_subagent_tool(tool_name) { let task_start = Instant::now(); - let stdout = end_task(db, &session_id, &payload.raw, false).unwrap_or_default(); + let stdout = match db.get_session_binding(&session_id).ok().flatten() { + Some(instance_name) => end_task(db, &instance_name, &payload.raw).unwrap_or_default(), + None => String::new(), + }; timing.task_ms = Some(task_start.elapsed().as_secs_f64() * 1000.0); return (0, stdout, None, timing); } - // Subagent context check - let subagent_check_start = Instant::now(); - let is_in_subagent_ctx = instances::in_subagent_context(db, &session_id); - timing.subagent_check_ms = Some(subagent_check_start.elapsed().as_secs_f64() * 1000.0); - - if is_in_subagent_ctx { - timing.context = Some("subagent"); - - if hook_type == HOOK_USERPROMPTSUBMIT { - let transcript_path = payload.transcript_path.as_deref().unwrap_or(""); - cleanup_dead_subagents(db, &session_id, transcript_path); - // Fall through to parent handler for PTY message delivery - } - - if hook_type == HOOK_SUBAGENT_START { - let agent_id = payload - .raw - .get("agent_id") - .and_then(|v| v.as_str()) - .unwrap_or(""); - let agent_type = payload - .raw - .get("agent_type") - .and_then(|v| v.as_str()) - .unwrap_or(""); - if !agent_id.is_empty() && !agent_type.is_empty() { - track_subagent(db, &session_id, agent_id, agent_type); - // Eagerly allocate an instance row so the subagent shows up - // in the TUI and is a valid `hcom send` target from birth — - // without injecting any hcom context into the subagent itself. - // The subagent stays dormant (name_announced=0) until either a - // message arrives at SubagentStop or it runs `hcom start`. - ensure_subagent_row(db, &session_id, agent_id, agent_type); - } - let output = subagent_start(&payload.raw); - if let Some(out) = output { - return ( - 0, - serde_json::to_string(&out).unwrap_or_default(), - None, - timing, - ); - } - return (0, String::new(), None, timing); - } - - if hook_type == HOOK_SUBAGENT_STOP { - let (exit_code, stdout) = subagent_stop(db, &payload.raw, &session_id); - return (exit_code, stdout, None, timing); - } - - if hook_type == HOOK_NOTIFY { - return (0, String::new(), None, timing); - } - - if hook_type == HOOK_PRE || hook_type == HOOK_POST { - if tool_name == "Bash" { - let tool_input = &payload.tool_input; - let command = tool_input - .get("command") - .and_then(|v| v.as_str()) - .unwrap_or(""); - if hook_type == HOOK_POST && extract_name(command).is_some() { - let (exit_code, stdout, delivery_ack) = subagent_posttooluse(db, &payload.raw); - return (exit_code, stdout, delivery_ack, timing); - } - } - return (0, String::new(), None, timing); - } + if hook_type == HOOK_USERPROMPTSUBMIT { + // Reap subagents that died without a clean SubagentStop before root + // delivery. This is lifecycle cleanup, not actor identification. + let transcript_path = payload.transcript_path.as_deref().unwrap_or(""); + cleanup_dead_subagents(db, &session_id, transcript_path); + // Fall through to parent handler for PTY message delivery } // Resolve parent instance @@ -395,7 +375,8 @@ fn route_claude_hook( handle_userpromptsubmit(db, ctx, payload, instance_name, &updates, &instance_data) } HOOK_SESSIONEND => { - let (code, stdout) = handle_sessionend(db, instance_name, &payload.raw, &updates); + let (code, stdout) = + handle_sessionend(db, instance_name, &session_id, &payload.raw, &updates); (code, stdout, None) } _ => (0, String::new(), None), @@ -405,6 +386,138 @@ fn route_claude_hook( (exit_code, stdout, delivery_ack, timing) } +/// Route a hook whose payload carries a non-empty `agent_id` — i.e. one that +/// fired inside a subagent's own execution context. See the call site in +/// `route_claude_hook` for why `agent_id`, not `running_tasks.active`, is the +/// actor signal, and for the participation gate that must run before this +/// (root_name is already a proven hcom binding by the time we're called). +/// +/// Returns (exit_code, stdout, delivery_ack). +fn route_subagent_actor_hook( + db: &HcomDb, + hook_type: &str, + payload: &HookPayload, + agent_id: &str, + session_id: &str, + root_name: &str, + timing: &mut DispatchTiming, +) -> (i32, String, Option) { + timing.context = Some("subagent"); + + if hook_type == HOOK_SUBAGENT_START { + return handle_subagent_start(db, payload, agent_id, session_id, root_name, timing); + } + + // SubagentStop resolves its own row and handles a missing row safely. + if hook_type == HOOK_SUBAGENT_STOP { + let (exit_code, stdout) = subagent_stop(db, session_id, &payload.raw); + return (exit_code, stdout, None); + } + + // Every other subagent hook requires a known actor row. A missing row is + // not proof that the hook belongs to the root, so fail closed. + let Ok(Some(subagent_instance)) = db.get_instance_by_agent_id(agent_id) else { + timing.result = Some("unknown_subagent_actor"); + return (0, String::new(), None); + }; + + if hook_type == HOOK_NOTIFY { + return (0, String::new(), None); + } + + // Nested Agent/Task calls mutate the acting subagent, not the shared root. + let tool_name = payload.tool_name.as_str(); + if hook_type == HOOK_PRE && is_subagent_tool(tool_name) { + let (exit_code, stdout) = start_task(db, &subagent_instance, session_id, &payload.raw); + return (exit_code, stdout, None); + } + if hook_type == HOOK_POST && is_subagent_tool(tool_name) { + let stdout = end_task(db, &subagent_instance, &payload.raw).unwrap_or_default(); + return (0, stdout, None); + } + + if (hook_type == HOOK_PRE || hook_type == HOOK_POST) && tool_name == "Bash" { + if hook_type == HOOK_POST { + let command = payload + .tool_input + .get("command") + .and_then(|v| v.as_str()) + .unwrap_or(""); + // A subagent may only route its own inbox: the --name it passed + // to `hcom start`/hook delivery must match the agent_id Claude + // itself stamped on this hook, or one subagent could spoof + // another's --name and read its inbox (see subagent_posttooluse). + if let Some(name_flag) = extract_name(command) + && name_flag == agent_id + { + let (exit_code, stdout, delivery_ack) = subagent_posttooluse(db, &payload.raw); + return (exit_code, stdout, delivery_ack); + } + } + return (0, String::new(), None); + } + + (0, String::new(), None) +} + +fn handle_subagent_start( + db: &HcomDb, + payload: &HookPayload, + agent_id: &str, + session_id: &str, + root_name: &str, + timing: &mut DispatchTiming, +) -> (i32, String, Option) { + let agent_type = payload + .raw + .get("agent_type") + .and_then(|value| value.as_str()) + .unwrap_or(""); + + if !agent_type.is_empty() { + let parent_instance = + match resolve_spawn_owner(db, &payload.raw, session_id, agent_id, root_name) { + SpawnOwner::LegacyRoot(name) + | SpawnOwner::Resolved(name) + | SpawnOwner::Resumed(name) => name, + SpawnOwner::Unresolved => { + log::log_warn( + "hooks", + "subagent.spawn_owner.unresolved", + &format!( + "agent_id={} prompt_id={:?}", + agent_id, + payload + .raw + .get("prompt_id") + .and_then(|value| value.as_str()) + ), + ); + timing.result = Some("spawn_owner_unresolved"); + return (0, String::new(), None); + } + }; + + let _ = db.kv_set( + &agent_owner_kv_key(session_id, agent_id), + Some(&parent_instance), + ); + + // Allocate the row first, and only track the child once it has one, so + // a tracked actor is always addressable. Tracking a child whose row + // allocation failed would leave the parent pointing at a subagent every + // one of whose later hooks fails closed as an unknown actor. + if ensure_subagent_row(db, &parent_instance, session_id, agent_id, agent_type) { + track_subagent(db, &parent_instance, agent_id, agent_type); + } + } + + let stdout = build_subagent_start_output(&payload.raw) + .and_then(|output| serde_json::to_string(&output).ok()) + .unwrap_or_default(); + (0, stdout, None) +} + /// Get correct session_id, handling Claude Code's fork bug. /// /// For --fork-session, hook_data.session_id reports the parent's old ID. @@ -667,36 +780,165 @@ fn bind_and_bootstrap( Ok(Some(result)) } -/// PreToolUse Task: enter subagent context. +/// kv-table key correlating a Task/Agent tool call's `prompt_id` — scoped to +/// the root session it happened in — with the instance that made it. See +/// `resolve_spawn_owner`. +/// +/// Session-scoped (not just `prompt_id`) because a session's mappings live +/// for the whole session (see `end_task` / `handle_sessionend` for why they +/// can't be deleted per-Task-completion), and prompt_id values are only +/// unique within one Claude conversation. +fn spawn_owner_kv_key(root_session_id: &str, prompt_id: &str) -> String { + format!("spawn_owner:{root_session_id}:{prompt_id}") +} + +/// kv-table key prefix covering every spawn-owner mapping for one root +/// session. See `handle_sessionend`. +fn spawn_owner_kv_prefix(root_session_id: &str) -> String { + format!("spawn_owner:{root_session_id}:") +} + +/// kv-table key remembering which instance owns a given `agent_id`, scoped +/// to the root session — independent of any one `prompt_id`. See +/// `resolve_spawn_owner`'s resume fallback. +fn agent_owner_kv_key(root_session_id: &str, agent_id: &str) -> String { + format!("agent_owner:{root_session_id}:{agent_id}") +} + +/// kv-table key prefix covering every agent-owner mapping for one root +/// session. See `handle_sessionend`. +fn agent_owner_kv_prefix(root_session_id: &str) -> String { + format!("agent_owner:{root_session_id}:") +} + +/// Outcome of resolving which instance spawned a SubagentStart's child. +enum SpawnOwner { + /// `prompt_id` was absent from the payload entirely — Claude Code + /// < 2.1.196, where this correlation doesn't exist on the wire at all. + /// Root attribution is the only thing ever knowable without it; this is + /// not nested-spawn support, just the honest pre-2.1.196 behavior. + LegacyRoot(String), + /// `prompt_id` was present and resolved to a live owner that's verified + /// to belong to this root session. + Resolved(String), + /// `prompt_id` was present but didn't resolve, and this `agent_id` was + /// already known (see `agent_owner_kv_key`) — Claude resuming a + /// previously-spawned subagent under the same `agent_id` stamps a *new* + /// `prompt_id` on the resumed SubagentStart with no corresponding + /// PreToolUse to map it, so the fresh-`prompt_id` lookup always misses on + /// resume. Falling back to this agent_id's own + /// remembered owner is safe specifically because the agent_id is + /// already known — see `Unresolved` for why an *unknown* agent_id must + /// not get the same treatment. + Resumed(String), + /// `prompt_id` was present but no valid owner resolved by either path, + /// and this `agent_id` has never been seen before: no mapping + /// (correlation failed or hasn't landed yet), or the mapped instance is + /// gone or belongs to a different session (stale/foreign). Root is + /// deliberately not used as a fallback here — on a Claude Code version + /// that *does* send `prompt_id`, a miss on a genuinely new agent_id + /// means something is wrong, not that root is a safe guess. + Unresolved, +} + +/// Resolve which instance actually spawned a SubagentStart's child. +/// +/// Claude stamps the same `prompt_id` on a Task/Agent tool's PreToolUse and +/// on the SubagentStart(s) it produces, including parallel siblings spawned +/// by the same call. `start_task` records +/// `(root_session_id, prompt_id) -> acting instance` for exactly this +/// lookup, so a SubagentStart spawned by a *nested* subagent resolves to +/// that subagent, not the session-bound root — parent and every nested +/// subagent share one Claude session_id, so session_id alone can never tell +/// them apart. +/// +/// Falls back to `agent_owner_kv_key(root_session_id, agent_id)` — this +/// `agent_id`'s own remembered owner from when it was first spawned — when +/// the `prompt_id` lookup misses; see `SpawnOwner::Resumed`. +fn resolve_spawn_owner( + db: &HcomDb, + raw: &Value, + root_session_id: &str, + agent_id: &str, + root_name: &str, +) -> SpawnOwner { + let Some(prompt_id) = raw + .get("prompt_id") + .and_then(|v| v.as_str()) + .filter(|s| !s.is_empty()) + else { + return SpawnOwner::LegacyRoot(root_name.to_string()); + }; + + let by_prompt = db + .kv_get(&spawn_owner_kv_key(root_session_id, prompt_id)) + .ok() + .flatten(); + if let Some(owner) = by_prompt + && validate_spawn_owner(db, &owner, root_session_id, root_name) + { + return SpawnOwner::Resolved(owner); + } + + let by_agent = db + .kv_get(&agent_owner_kv_key(root_session_id, agent_id)) + .ok() + .flatten(); + if let Some(owner) = by_agent + && validate_spawn_owner(db, &owner, root_session_id, root_name) + { + return SpawnOwner::Resumed(owner); + } + + SpawnOwner::Unresolved +} + +/// Verify a resolved spawn-owner instance actually belongs to this root +/// session's hierarchy, rather than trusting the kv mapping blindly — a +/// stale entry could name an instance that's since been deleted or (in +/// principle) reused for something else. +fn validate_spawn_owner(db: &HcomDb, owner: &str, root_session_id: &str, root_name: &str) -> bool { + if owner == root_name { + return true; + } + match db.get_instance_full(owner) { + Ok(Some(data)) => data.parent_session_id.as_deref() == Some(root_session_id), + _ => false, + } +} + +/// PreToolUse Task: enter subagent context for `instance_name` — the acting +/// instance's own row (root parent, or the spawning subagent for a nested +/// Agent/Task call; see the call sites in `route_claude_hook` / +/// `route_subagent_actor_hook`). /// -/// Returns (stdout, exit_code). -fn start_task(db: &HcomDb, session_id: &str, raw: &Value) -> (String, i32) { +/// Returns (exit_code, stdout). +fn start_task( + db: &HcomDb, + instance_name: &str, + root_session_id: &str, + raw: &Value, +) -> (i32, String) { log::log_info( "hooks", "start_task.enter", - &format!("session_id={}", session_id), + &format!("instance={}", instance_name), ); - let instance_name = match db.get_session_binding(session_id) { - Ok(Some(name)) => name, - _ => return (String::new(), 0), - }; - - // Set running_tasks.active = true - let instance_data = match db.get_instance_full(&instance_name) { - Ok(Some(data)) => data, - _ => return (String::new(), 0), - }; - - let mut running_tasks = instances::parse_running_tasks(instance_data.running_tasks.as_deref()); - running_tasks.active = true; - let rt_json = serde_json::json!({ - "active": running_tasks.active, - "subagents": running_tasks.subagents, + instances::mutate_running_tasks(db, instance_name, |rt| { + rt.active = true; }); - let mut updates = serde_json::Map::new(); - updates.insert("running_tasks".into(), Value::String(rt_json.to_string())); - instances::update_instance_position(db, &instance_name, &updates); + + if let Some(prompt_id) = raw + .get("prompt_id") + .and_then(|v| v.as_str()) + .filter(|s| !s.is_empty()) + { + let _ = db.kv_set( + &spawn_owner_kv_key(root_session_id, prompt_id), + Some(instance_name), + ); + } let tool_input = raw .get("tool_input") @@ -705,7 +947,7 @@ fn start_task(db: &HcomDb, session_id: &str, raw: &Value) -> (String, i32) { let detail = family::extract_tool_detail("claude", "Task", &tool_input); lifecycle::set_status( db, - &instance_name, + instance_name, ST_ACTIVE, "tool:Task", lifecycle::StatusUpdate { @@ -736,37 +978,47 @@ fn start_task(db: &HcomDb, session_id: &str, raw: &Value) -> (String, i32) { "updatedInput": updated, } }); - return (serde_json::to_string(&output).unwrap_or_default(), 0); + return (0, serde_json::to_string(&output).unwrap_or_default()); } - (String::new(), 0) + (0, String::new()) } -/// PostToolUse Task: deliver freeze-period messages. +/// PostToolUse Task: deliver freeze-period messages for `instance_name` — the +/// acting instance's own row (see `start_task`). /// /// Returns Option — JSON stdout if messages were delivered. /// Dispatcher writes this to stdout before returning exit code. -fn end_task(db: &HcomDb, session_id: &str, _raw: &Value, interrupted: bool) -> Option { - let instance_name = match db.get_session_binding(session_id) { - Ok(Some(name)) => name, - _ => return None, - }; +fn end_task(db: &HcomDb, instance_name: &str, raw: &Value) -> Option { + // Since Claude Code 2.1.198, Agent/Task calls background by default: this + // PostToolUse fires immediately with tool_response.status="async_launched" + // when the call is merely dispatched to the background, not when the + // subagent actually finishes. Treating that as completion would emit a + // false "Subagents have finished and are no longer active" summary and + // advance last_event_id before the subagent's freeze window is over, so + // skip delivery rather than assume anything about how/when completion is + // reported — root-scoped messages still flow through the root's own + // interleaved PostToolUse hooks regardless (see route_claude_hook). + let is_async_launch = raw + .get("tool_response") + .and_then(|v| v.get("status")) + .and_then(|v| v.as_str()) + == Some("async_launched"); + if is_async_launch { + return None; + } - let instance_data = match db.get_instance_full(&instance_name) { + let instance_data = match db.get_instance_full(instance_name) { Ok(Some(data)) => data, _ => return None, }; - if interrupted { - return None; - } - let freeze_event_id = instance_data.last_event_id; - let (last_event_id, stdout) = deliver_freeze_messages(db, &instance_name, freeze_event_id); + let (last_event_id, stdout) = deliver_freeze_messages(db, instance_name, freeze_event_id); let mut updates = serde_json::Map::new(); updates.insert("last_event_id".into(), serde_json::json!(last_event_id)); - instances::update_instance_position(db, &instance_name, &updates); + instances::update_instance_position(db, instance_name, &updates); stdout } @@ -1297,6 +1549,7 @@ fn handle_notify( fn handle_sessionend( db: &HcomDb, instance_name: &str, + session_id: &str, raw: &Value, updates: &serde_json::Map, ) -> (i32, String) { @@ -1314,82 +1567,66 @@ fn handle_sessionend( Some(updates) }, ); + + // Root session is over: no more children can spawn under any of its + // prompt_id mappings (see `spawn_owner_kv_key`), no further resumes can + // arrive for its agent_ids (see `agent_owner_kv_key`), and no further + // SubagentStop invocations can fire for it (see `subagent_stop_claim_key`). + // None of these can be cleaned up incrementally as they're used (parallel + // siblings sharing one prompt_id would race a later SubagentStart against + // an earlier sibling's cleanup — see `end_task`; a resumed agent_id needs + // its mapping to *outlive* its own stop; a stop claim needs to survive + // exactly as long as its invocation could possibly be duplicated), so the + // whole session's worth of each is swept here instead. A session that + // ends without this hook firing (crash, `hcom kill`) just leaves harmless + // unreachable entries. + for (label, prefix) in [ + ("spawn_owner", spawn_owner_kv_prefix(session_id)), + ("agent_owner", agent_owner_kv_prefix(session_id)), + ( + "subagent_stop_claim", + subagent_stop_claim_prefix(session_id), + ), + ] { + if let Err(e) = db.kv_delete_prefix(&prefix) { + log::log_warn( + "hooks", + "sessionend.kv_cleanup_failed", + &format!("kind={} session_id={} err={}", label, session_id, e), + ); + } + } + (0, String::new()) } -/// Track subagent in parent's running_tasks. -fn track_subagent(db: &HcomDb, parent_session_id: &str, agent_id: &str, agent_type: &str) { +/// Track subagent in `parent_instance`'s running_tasks. `parent_instance` is +/// the already-resolved spawning actor (root or a nested subagent — see +/// `resolve_spawn_owner`), not derived from session_id here. +fn track_subagent(db: &HcomDb, parent_instance: &str, agent_id: &str, agent_type: &str) { log::log_info( "hooks", "track_subagent.enter", &format!( - "session_id={} agent_id={} agent_type={}", - parent_session_id, agent_id, agent_type + "parent={} agent_id={} agent_type={}", + parent_instance, agent_id, agent_type ), ); - let instance_name = match db.get_session_binding(parent_session_id) { - Ok(Some(name)) => name, - _ => return, - }; - - let instance_data = match db.get_instance_full(&instance_name) { - Ok(Some(data)) => data, - _ => return, - }; - - let mut running_tasks = instances::parse_running_tasks(instance_data.running_tasks.as_deref()); - running_tasks.active = true; - - // Add subagent if not already tracked - let already_tracked = running_tasks - .subagents - .iter() - .any(|s| s.get("agent_id").and_then(|v| v.as_str()) == Some(agent_id)); - - if !already_tracked { - running_tasks.subagents.push(serde_json::json!({ - "agent_id": agent_id, - "type": agent_type, - })); - let rt_json = serde_json::json!({ - "active": running_tasks.active, - "subagents": running_tasks.subagents, - }); - let mut updates = serde_json::Map::new(); - updates.insert("running_tasks".into(), Value::String(rt_json.to_string())); - instances::update_instance_position(db, &instance_name, &updates); - } + // Dedup-check-and-insert happens inside the transaction (see + // `mutate_running_tasks`), so concurrent SubagentStart hooks for sibling + // subagents of the same parent (parallel Task calls) cannot race and + // silently drop each other's entry. + instances::mutate_running_tasks(db, parent_instance, |rt| { + rt.track_subagent(agent_id, agent_type); + }); } /// Remove subagent from parent's running_tasks. fn remove_subagent_from_parent(db: &HcomDb, parent_name: &str, agent_id: &str) { - let parent_data = match db.get_instance_full(parent_name) { - Ok(Some(data)) => data, - _ => return, - }; - - let mut running_tasks = instances::parse_running_tasks(parent_data.running_tasks.as_deref()); - - if running_tasks.subagents.is_empty() { - return; - } - - running_tasks - .subagents - .retain(|s| s.get("agent_id").and_then(|v| v.as_str()) != Some(agent_id)); - - if running_tasks.subagents.is_empty() { - running_tasks.active = false; - } - - let rt_json = serde_json::json!({ - "active": running_tasks.active, - "subagents": running_tasks.subagents, + instances::mutate_running_tasks(db, parent_name, |rt| { + rt.remove_subagent(agent_id); }); - let mut updates = serde_json::Map::new(); - updates.insert("running_tasks".into(), Value::String(rt_json.to_string())); - instances::update_instance_position(db, parent_name, &updates); } /// Maximum depth when searching for nested subagent transcripts. @@ -1583,7 +1820,7 @@ fn cleanup_dead_subagents(db: &HcomDb, session_id: &str, transcript_path: &str) } /// SubagentStart: surface agent_id to subagent. -fn subagent_start(raw: &Value) -> Option { +fn build_subagent_start_output(raw: &Value) -> Option { let agent_id = raw.get("agent_id").and_then(|v| v.as_str())?; if agent_id.is_empty() { return None; @@ -1611,22 +1848,35 @@ fn subagent_start(raw: &Value) -> Option { /// /// Idempotent: calls into `allocate_subagent_instance`, which returns the /// existing row's name if one already exists for this agent_id. -fn ensure_subagent_row(db: &HcomDb, parent_session_id: &str, agent_id: &str, agent_type: &str) { - let parent_name = match db.get_session_binding(parent_session_id) { - Ok(Some(name)) => name, - _ => return, - }; - let parent_data = match db.get_instance_full(&parent_name) { +/// +/// `parent_instance` (the row's `parent_name`) is the already-resolved +/// spawning actor, which may be a nested subagent — see +/// `resolve_spawn_owner`. `root_session_id` is always the real Claude +/// session_id (the root's own), never a subagent's, because subagent rows +/// never get their own `session_id` (see `allocate_subagent_instance`) — it +/// is the only value the `parent_session_id` FK column (`REFERENCES +/// instances(session_id)`) can ever validly point at, at any nesting depth. +/// Allocate this subagent's `instances` row. Returns `true` once a row exists +/// and the actor is addressable (allocation is idempotent, so an already-present +/// row also counts), `false` if the parent is unknown or allocation failed. +fn ensure_subagent_row( + db: &HcomDb, + parent_instance: &str, + root_session_id: &str, + agent_id: &str, + agent_type: &str, +) -> bool { + let parent_data = match db.get_instance_full(parent_instance) { Ok(Some(data)) => data, - _ => return, + _ => return false, }; let parent_tag = parent_data.tag.as_deref(); let alloc = instance_names::SubagentAllocation { agent_id, agent_type, - parent_name: &parent_name, - parent_session_id: Some(parent_session_id), + parent_name: parent_instance, + parent_session_id: Some(root_session_id), parent_tag, status: ST_INACTIVE, status_context: Some("subagent:dormant"), @@ -1639,9 +1889,10 @@ fn ensure_subagent_row(db: &HcomDb, parent_session_id: &str, agent_id: &str, age "subagent.row.ensured", &format!( "name={} parent={} agent_id={} type={}", - name, parent_name, agent_id, agent_type + name, parent_instance, agent_id, agent_type ), ); + true } Err(e) => { log::log_warn( @@ -1649,15 +1900,92 @@ fn ensure_subagent_row(db: &HcomDb, parent_session_id: &str, agent_id: &str, age "subagent.row.alloc_failed", &format!("agent_id={} err={}", agent_id, e), ); + false } } } +/// Deterministic, bounded fingerprint of a raw hook payload for use inside a +/// SQLite key. Payloads can carry unbounded fields (e.g. a transcript +/// excerpt), so the payload itself must never be embedded directly into a +/// key — hash it instead. Not for cryptographic use, just content-addressing +/// two invocations as "the same bytes" or not. +fn hash_raw_payload(raw: &Value) -> String { + use sha2::{Digest, Sha256}; + let mut hasher = Sha256::new(); + hasher.update(raw.to_string().as_bytes()); + let hash = hasher.finalize(); + hash.iter().map(|b| format!("{b:02x}")).take(16).collect() +} + +/// kv-table key claiming exactly-once processing for *one specific* +/// SubagentStop invocation — not the agent_id alone, and not `prompt_id` +/// alone either. Claude legitimately re-fires SubagentStop for the same +/// agent_id (and, as far as we've established, possibly the same +/// `prompt_id` too — SubagentStop is explicitly designed to fire again after +/// an exit_code=2 delivery within the same agent prompt/continuation, and we +/// have not confirmed Claude changes `prompt_id` for that re-fire) across the +/// message-poll loop and on resume, so a claim keyed on either alone risks +/// wrongly suppressing a later, genuinely different stop. The full raw +/// payload is hashed instead: two hook registrations firing the *identical* +/// event (e.g. hcom installed in both global and repo Claude settings, seen +/// live) produce byte-identical stdin and collapse onto the same key, while +/// any real difference between invocations (delivered message content, +/// background/transcript state) changes the hash and gets its own key. +/// `prompt_id` is folded in only as a readable, non-load-bearing component. +fn subagent_stop_claim_key(root_session_id: &str, agent_id: &str, raw: &Value) -> String { + let prompt_id = raw.get("prompt_id").and_then(|v| v.as_str()).unwrap_or(""); + format!( + "subagent_stop_claimed:{root_session_id}:{agent_id}:{prompt_id}:{}", + hash_raw_payload(raw) + ) +} + +/// kv-table key prefix covering every SubagentStop claim for one root +/// session. See `handle_sessionend`. +fn subagent_stop_claim_prefix(root_session_id: &str) -> String { + format!("subagent_stop_claimed:{root_session_id}:") +} + +/// Transient claim that prevents concurrent duplicate SubagentStop hooks from +/// polling or tearing down the same actor at the same time. The claim is +/// released on drop so a later, distinct stop with identical content can run. +struct SubagentStopClaim<'a> { + db: &'a HcomDb, + key: String, +} + +impl<'a> SubagentStopClaim<'a> { + fn acquire(db: &'a HcomDb, root_session_id: &str, agent_id: &str, raw: &Value) -> Option { + let key = subagent_stop_claim_key(root_session_id, agent_id, raw); + let claimed = db + .conn() + .execute( + "INSERT OR IGNORE INTO kv (key, value) VALUES (?, '1')", + rusqlite::params![key], + ) + // Fail open rather than leave a stopped actor wedged by a DB error. + .map(|inserted| inserted > 0) + .unwrap_or(true); + + claimed.then_some(Self { db, key }) + } +} + +impl Drop for SubagentStopClaim<'_> { + fn drop(&mut self) { + let _ = self + .db + .conn() + .execute("DELETE FROM kv WHERE key = ?", rusqlite::params![self.key]); + } +} + /// SubagentStop: message polling using agent_id, cleanup on exit. /// /// Returns (exit_code, stdout). exit_code=2 means message delivered /// (SubagentStop fires again). exit_code=0 means cleanup and stop. -fn subagent_stop(db: &HcomDb, raw: &Value, session_id: &str) -> (i32, String) { +fn subagent_stop(db: &HcomDb, root_session_id: &str, raw: &Value) -> (i32, String) { let agent_id = match raw.get("agent_id").and_then(|v| v.as_str()) { Some(id) if !id.is_empty() => id, _ => return (0, String::new()), @@ -1683,11 +2011,17 @@ fn subagent_stop(db: &HcomDb, raw: &Value, session_id: &str) -> (i32, String) { let Some((subagent_name, existing_transcript, parent_name, name_announced)) = row else { // No instance = SubagentStart never allocated one (shouldn't happen for - // in-ctx subagents, but kept as a defensive fallback). Clean up the - // parent's running_tasks entry so it doesn't wedge active. - if let Ok(Some(parent)) = db.get_session_binding(session_id) { - remove_subagent_from_parent(db, &parent, agent_id); - } + // in-ctx subagents, but kept as a defensive fallback). The tracking + // entry could be on the session-bound root *or* a nested subagent + // that spawned this one (see `resolve_spawn_owner`) — scan for + // whichever instance actually tracks `agent_id` rather than assuming + // root, or the true owner is left wedged active forever. + instances::remove_tracked_subagent_by_agent_id(db, agent_id); + return (0, String::new()); + }; + + // A duplicate delivery must not enter polling or teardown. + let Some(_stop_claim) = SubagentStopClaim::acquire(db, root_session_id, agent_id, raw) else { return (0, String::new()); }; @@ -2712,7 +3046,7 @@ mod tests { #[test] fn test_subagent_start_with_agent_id() { let raw = serde_json::json!({"agent_id": "agent-uuid-123"}); - let result = subagent_start(&raw); + let result = build_subagent_start_output(&raw); assert!(result.is_some()); let output = result.unwrap(); let ctx = output["hookSpecificOutput"]["additionalContext"] @@ -2724,13 +3058,13 @@ mod tests { #[test] fn test_subagent_start_no_agent_id() { let raw = serde_json::json!({}); - assert!(subagent_start(&raw).is_none()); + assert!(build_subagent_start_output(&raw).is_none()); } #[test] fn test_subagent_start_empty_agent_id() { let raw = serde_json::json!({"agent_id": ""}); - assert!(subagent_start(&raw).is_none()); + assert!(build_subagent_start_output(&raw).is_none()); } #[test] @@ -3682,4 +4016,1473 @@ mod tests { drop(_guard); } + + // ---- Hook-actor routing (raw.agent_id, not running_tasks.active) ---- + // + // These are dispatcher-level tests: they drive `route_claude_hook` itself + // (the function `dispatch_claude_hook` calls after reading stdin), not the + // individual handler functions, because the bug class here is a routing + // bug — which branch a given hook payload falls into — not a bug inside + // any one handler. They need `isolated_test_env()` because + // `route_claude_hook` exercises real log::log_info call sites (start_task, + // track_subagent, sessionstart, ...), which resolve the hcom log path + // through the global `Config`; without isolation that would touch the + // real `~/.hcom` of whatever machine runs the test. + + fn make_ctx() -> HcomContext { + HcomContext::from_env(&std::collections::HashMap::new(), PathBuf::from("/tmp")) + } + + fn make_isolated_test_db() -> (tempfile::TempDir, EnvGuard, HcomDb) { + let (dir, hcom_dir, _test_home, guard) = isolated_test_env(); + let db = HcomDb::open_raw(&hcom_dir.join("test.db")).unwrap(); + db.init_db().unwrap(); + (dir, guard, db) + } + + /// Same fixture as `make_delivery_test_db` (instance 'nova' + a pending + /// broadcast message from 'luna'), but under `isolated_test_env()` so + /// dispatcher-level tests that log are safe to run. + fn make_isolated_delivery_test_db() -> (tempfile::TempDir, EnvGuard, HcomDb) { + let (dir, guard, db) = make_isolated_test_db(); + db.conn() + .execute( + "INSERT INTO instances (name, session_id, tool, status, status_context, status_time, created_at, last_event_id) + VALUES ('nova', 'sess-1', 'claude', 'listening', 'start', 0, 0, 0)", + [], + ) + .unwrap(); + db.conn() + .execute( + "INSERT INTO events (type, timestamp, instance, data) + VALUES ('message', '2026-01-01T00:00:00Z', 'luna', '{\"from\":\"luna\",\"text\":\"hello\",\"scope\":\"broadcast\"}')", + [], + ) + .unwrap(); + (dir, guard, db) + } + + /// `root_session_id` matches what real `ensure_subagent_row`-created rows + /// carry as `parent_session_id` (always the true root session_id, at any + /// nesting depth — see its doc comment), so fixtures built with this + /// helper pass `validate_spawn_owner`'s hierarchy check the same way a + /// real row would. + fn insert_subagent_row( + db: &HcomDb, + name: &str, + agent_id: &str, + parent_name: &str, + root_session_id: &str, + ) { + db.conn() + .execute( + "INSERT INTO instances (name, tool, status, status_context, status_time, created_at, last_event_id, agent_id, parent_name, parent_session_id) + VALUES (?, 'claude', 'active', 'subagent', 0, 0, 0, ?, ?, ?)", + rusqlite::params![name, agent_id, parent_name, root_session_id], + ) + .unwrap(); + } + + /// Property: a subagent PostToolUse whose agent_id has no resolvable + /// `instances` row (SubagentStart's row allocation is best-effort and may + /// not have happened, or the row is gone) must never deliver anything — + /// and, critically, must never fall through to root/parent delivery. + /// Row absence is identity state, not proof that the actor is the root. + #[test] + #[serial] + fn test_subagent_posttooluse_unknown_row_never_falls_through_to_parent() { + crate::config::Config::init(); + let (_dir, _guard, db) = make_isolated_delivery_test_db(); + db.set_session_binding("sess-1", "nova").unwrap(); + // Parent tracks an agent_id that never got an instances row. + db.conn() + .execute( + "UPDATE instances SET running_tasks = ? WHERE name = 'nova'", + rusqlite::params![ + r#"{"active":true,"subagents":[{"agent_id":"ghost-agent","type":"general"}]}"# + ], + ) + .unwrap(); + + let raw = serde_json::json!({ + "session_id": "sess-1", + "agent_id": "ghost-agent", + "tool_name": "Bash", + "tool_input": {"command": "hcom send --name ghost-agent -- hi"}, + }); + let mut payload = HookPayload::from_claude(raw); + let ctx = make_ctx(); + let (exit_code, stdout, ack, _timing) = + route_claude_hook(&db, &ctx, HOOK_POST, &mut payload); + + assert_eq!(exit_code, 0); + assert!( + stdout.is_empty(), + "unknown subagent actor must not deliver anything, got: {stdout}" + ); + assert!(ack.is_none()); + // nova's own pending broadcast must remain untouched — no fallthrough. + assert_eq!(delivery_cursor(&db), 0); + } + + /// Property: a subagent-context hook whose row *does* resolve, but which + /// doesn't match any actionable branch (e.g. an ordinary Edit tool call), + /// stays a silent no-op rather than falling through to parent handling. + #[test] + #[serial] + fn test_subagent_hook_unrelated_tool_stays_silent() { + crate::config::Config::init(); + let (_dir, _guard, db) = make_isolated_test_db(); + db.conn() + .execute( + "INSERT INTO instances (name, session_id, tool, status, status_context, status_time, created_at, last_event_id) + VALUES ('nova', 'sess-1', 'claude', 'listening', 'start', 0, 0, 0)", + [], + ) + .unwrap(); + db.set_session_binding("sess-1", "nova").unwrap(); + insert_subagent_row(&db, "nova_task_1", "sub-agent-1", "nova", "sess-1"); + + let raw = serde_json::json!({ + "session_id": "sess-1", + "agent_id": "sub-agent-1", + "tool_name": "Edit", + "tool_input": {"file_path": "/tmp/x"}, + }); + let mut payload = HookPayload::from_claude(raw); + let ctx = make_ctx(); + let (exit_code, stdout, ack, _timing) = + route_claude_hook(&db, &ctx, HOOK_POST, &mut payload); + + assert_eq!(exit_code, 0); + assert!(stdout.is_empty()); + assert!(ack.is_none()); + } + + /// Property: stopping a nested native parent must recursively tear down its + /// own children. Native subagent rows carry session_id=NULL and inherit the + /// root session as parent_session_id, so the session-keyed teardown cascade + /// never links a nested parent to its children — only parent_name does. + /// Without the parent_name cascade, stopping parent A would delete A while + /// its child B stayed alive, reparented to a reusable name. + #[test] + #[serial] + fn test_stop_nested_parent_cascades_to_children() { + crate::config::Config::init(); + let (_dir, _guard, db) = make_isolated_test_db(); + db.conn() + .execute( + "INSERT INTO instances (name, session_id, tool, status, status_context, status_time, created_at, last_event_id) + VALUES ('nova', 'sess-1', 'claude', 'listening', 'start', 0, 0, 0)", + [], + ) + .unwrap(); + db.set_session_binding("sess-1", "nova").unwrap(); + // A is a native subagent of root nova; B is a native subagent of A. + // Both share the root session as parent_session_id and have no session + // of their own (session_id=NULL, as insert_subagent_row leaves it). + insert_subagent_row(&db, "nova_a_1", "agent-a", "nova", "sess-1"); + insert_subagent_row(&db, "nova_a_1_b_1", "agent-b", "nova_a_1", "sess-1"); + + crate::hooks::stop_instance(&db, "nova_a_1", "test", "task_completed"); + + assert!( + db.get_instance_by_agent_id("agent-a").unwrap().is_none(), + "nested parent A must be torn down" + ); + assert!( + db.get_instance_by_agent_id("agent-b").unwrap().is_none(), + "child B must be cascaded, not orphaned with parent_name=A" + ); + assert!( + db.get_instance_full("nova").unwrap().is_some(), + "the still-live root must not be touched" + ); + } + + /// Property: the root's own PostToolUse must deliver even while a + /// genuinely live background subagent is tracked active. Since Claude + /// Code 2.1.198, Agent calls background by default, so the root can have + /// its own interleaved tool calls while a subagent still runs — gating + /// root delivery on `running_tasks.active` (the pre-existing design) + /// would freeze the parent for that whole window. + #[test] + #[serial] + fn test_root_posttooluse_delivers_while_subagent_active() { + crate::config::Config::init(); + let (_dir, _guard, db) = make_isolated_delivery_test_db(); + db.set_session_binding("sess-1", "nova").unwrap(); + insert_subagent_row(&db, "nova_task_1", "live-agent-1", "nova", "sess-1"); + db.conn() + .execute( + "UPDATE instances SET running_tasks = ? WHERE name = 'nova'", + rusqlite::params![ + r#"{"active":true,"subagents":[{"agent_id":"live-agent-1","type":"general"}]}"# + ], + ) + .unwrap(); + + // Root's own PostToolUse — no agent_id: this genuinely is nova's own + // tool call, not a hook firing inside the live subagent. + let raw = serde_json::json!({ + "session_id": "sess-1", + "tool_name": "Read", + "tool_input": {}, + }); + let mut payload = HookPayload::from_claude(raw); + let ctx = make_ctx(); + let (_exit_code, stdout, ack, _timing) = + route_claude_hook(&db, &ctx, HOOK_POST, &mut payload); + + assert!( + !stdout.is_empty(), + "root PostToolUse must deliver even while a background subagent is active" + ); + assert!(stdout.contains("hello")); + assert!(ack.is_some()); + } + + /// Property: a nested Agent/Task PreToolUse — one that fires *inside* a + /// subagent's own execution context because that subagent itself called + /// the Agent tool — must mark the spawning subagent's own running_tasks + /// active, not the root parent's. Parent and every nested subagent share + /// the same Claude session_id, so resolving the actor via + /// `get_session_binding(session_id)` (the pre-existing design) always + /// lands on the root regardless of which level actually spawned the call. + #[test] + #[serial] + fn test_nested_task_pre_updates_subagent_not_root() { + crate::config::Config::init(); + let (_dir, _guard, db) = make_isolated_test_db(); + db.conn() + .execute( + "INSERT INTO instances (name, session_id, tool, status, status_context, status_time, created_at, last_event_id) + VALUES ('nova', 'sess-1', 'claude', 'listening', 'start', 0, 0, 0)", + [], + ) + .unwrap(); + db.set_session_binding("sess-1", "nova").unwrap(); + insert_subagent_row(&db, "nova_task_1", "sub-agent-1", "nova", "sess-1"); + + let raw = serde_json::json!({ + "session_id": "sess-1", + "agent_id": "sub-agent-1", + "tool_name": "Task", + "tool_input": {"prompt": "spawn a nested helper"}, + }); + let mut payload = HookPayload::from_claude(raw); + let ctx = make_ctx(); + let _ = route_claude_hook(&db, &ctx, HOOK_PRE, &mut payload); + + let sub_rt = instances::parse_running_tasks( + db.get_instance_full("nova_task_1") + .unwrap() + .unwrap() + .running_tasks + .as_deref(), + ); + assert!( + sub_rt.active, + "the spawning subagent's own running_tasks must be marked active" + ); + + let root_rt = instances::parse_running_tasks( + db.get_instance_full("nova") + .unwrap() + .unwrap() + .running_tasks + .as_deref(), + ); + assert!( + !root_rt.active, + "the root parent's running_tasks must not be touched by a nested Task call" + ); + } + + /// Property: PostToolUse for the Agent/Task tool fires with + /// `tool_response.status == "async_launched"` when Claude merely + /// dispatched the call to the background (default since Claude Code + /// 2.1.198) — this is not completion, so it must not deliver a + /// "Subagents have finished" summary and must not advance the delivery + /// cursor. Foreground completion is covered separately by + /// `test_task_posttooluse_foreground_completed_delivers`. + #[test] + #[serial] + fn test_task_posttooluse_async_launch_skips_delivery() { + crate::config::Config::init(); + let (_dir, _guard, db) = make_isolated_delivery_test_db(); + db.set_session_binding("sess-1", "nova").unwrap(); + let ctx = make_ctx(); + + let raw_async = serde_json::json!({ + "session_id": "sess-1", + "tool_name": "Agent", + "tool_response": {"status": "async_launched"}, + }); + let mut payload_async = HookPayload::from_claude(raw_async); + let (_exit_code, stdout_async, ack_async, _timing) = + route_claude_hook(&db, &ctx, HOOK_POST, &mut payload_async); + assert!( + stdout_async.is_empty(), + "async_launched must not be treated as Task completion, got: {stdout_async}" + ); + assert!(ack_async.is_none()); + assert_eq!( + delivery_cursor(&db), + 0, + "async_launched must not advance the delivery cursor" + ); + } + + /// Property: a foreground (synchronous, non-backgrounded) Agent/Task + /// PostToolUse — no `async_launched` status — is a genuine completion and + /// must deliver freeze-period messages normally. Independent of the + /// async_launched test above: this is not "the same call, later", it's + /// the separately-exercised foreground path. + #[test] + #[serial] + fn test_task_posttooluse_foreground_completed_delivers() { + crate::config::Config::init(); + let (_dir, _guard, db) = make_isolated_delivery_test_db(); + db.set_session_binding("sess-1", "nova").unwrap(); + let ctx = make_ctx(); + + let raw_done = serde_json::json!({ + "session_id": "sess-1", + "tool_name": "Agent", + "tool_response": {"status": "completed"}, + }); + let mut payload_done = HookPayload::from_claude(raw_done); + let (_exit_code, stdout_done, _ack, _timing) = + route_claude_hook(&db, &ctx, HOOK_POST, &mut payload_done); + assert!( + stdout_done.contains("hello"), + "a foreground Task completion must deliver freeze messages, got: {stdout_done}" + ); + assert!(delivery_cursor(&db) > 0); + } + + /// Property: the `--name` a subagent passes to a Bash hcom command must + /// match the agent_id Claude itself stamped on this hook (raw.agent_id), + /// or one subagent could spoof another's `--name` and read its inbox. + #[test] + #[serial] + fn test_subagent_bash_name_mismatch_does_not_leak_other_inbox() { + crate::config::Config::init(); + let (_dir, _guard, db) = make_isolated_test_db(); + db.conn() + .execute( + "INSERT INTO instances (name, session_id, tool, status, status_context, status_time, created_at, last_event_id) + VALUES ('nova', 'sess-1', 'claude', 'listening', 'start', 0, 0, 0)", + [], + ) + .unwrap(); + db.set_session_binding("sess-1", "nova").unwrap(); + insert_subagent_row(&db, "nova_task_1", "agent-a", "nova", "sess-1"); + insert_subagent_row(&db, "nova_task_2", "agent-b", "nova", "sess-1"); + // A direct mention pending for subagent B's inbox only. + db.conn() + .execute( + "INSERT INTO events (type, timestamp, instance, data) + VALUES ('message', '2026-01-01T00:00:00Z', 'luna', '{\"from\":\"luna\",\"text\":\"secret for b\",\"scope\":\"mentions\",\"mentions\":[\"nova_task_2\"]}')", + [], + ) + .unwrap(); + let ctx = make_ctx(); + + // Hook fires inside subagent A's own context (agent_id=agent-a), but + // the Bash command spoofs --name agent-b. + let raw_spoof = serde_json::json!({ + "session_id": "sess-1", + "agent_id": "agent-a", + "tool_name": "Bash", + "tool_input": {"command": "hcom send --name agent-b -- hi"}, + }); + let mut payload_spoof = HookPayload::from_claude(raw_spoof); + let (exit_code, stdout, ack, _timing) = + route_claude_hook(&db, &ctx, HOOK_POST, &mut payload_spoof); + assert_eq!(exit_code, 0); + assert!( + stdout.is_empty(), + "mismatched --name must not deliver another subagent's inbox, got: {stdout}" + ); + assert!(ack.is_none()); + + // The legitimate case still works: agent_id and --name match. + let raw_ok = serde_json::json!({ + "session_id": "sess-1", + "agent_id": "agent-b", + "tool_name": "Bash", + "tool_input": {"command": "hcom send --name agent-b -- hi"}, + }); + let mut payload_ok = HookPayload::from_claude(raw_ok); + let (_exit_code, stdout_ok, ack_ok, _timing) = + route_claude_hook(&db, &ctx, HOOK_POST, &mut payload_ok); + assert!( + stdout_ok.contains("secret for b"), + "matching --name must deliver its own inbox, got: {stdout_ok}" + ); + assert!(ack_ok.is_some()); + } + + /// Property: `running_tasks` is a whole-JSON-blob column mutated by + /// separate hook processes (each hook invocation opens its own DB + /// connection). Concurrent SubagentStart hooks for sibling subagents of + /// the same parent (parallel Task calls) must not race a plain + /// read-then-write into a lost update. + #[test] + #[serial] + fn test_mutate_running_tasks_concurrent_tracking_no_lost_updates() { + crate::config::Config::init(); + let (_dir, hcom_dir, _test_home, _guard) = isolated_test_env(); + let db_path = hcom_dir.join("test.db"); + let db = HcomDb::open_raw(&db_path).unwrap(); + db.init_db().unwrap(); + db.conn() + .execute( + "INSERT INTO instances (name, tool, status, status_context, status_time, created_at, last_event_id) + VALUES ('nova', 'claude', 'listening', 'start', 0, 0, 0)", + [], + ) + .unwrap(); + + let n: usize = 8; + let handles: Vec<_> = (0..n) + .map(|i| { + let path = db_path.clone(); + std::thread::spawn(move || { + let db = HcomDb::open_raw(&path).unwrap(); + track_subagent(&db, "nova", &format!("agent-{i}"), "general"); + }) + }) + .collect(); + for h in handles { + h.join().unwrap(); + } + + let rt = instances::parse_running_tasks( + db.get_instance_full("nova") + .unwrap() + .unwrap() + .running_tasks + .as_deref(), + ); + assert_eq!( + rt.subagents.len(), + n, + "concurrent SubagentStart tracking must not lose sibling entries to a read-modify-write race, got {:?}", + rt.subagents + ); + assert!(rt.active); + for i in 0..n { + let want = format!("agent-{i}"); + assert!( + rt.subagents + .iter() + .any(|s| s.get("agent_id").and_then(|v| v.as_str()) == Some(want.as_str())), + "missing {want}" + ); + } + } + + /// Property: concurrent SubagentStop-driven removals for sibling + /// subagents must not race a plain read-then-write into a lost update + /// either — an entry silently surviving a lost removal is exactly what + /// leaves `running_tasks.active` stuck true (the original stale-freeze + /// symptom). + #[test] + #[serial] + fn test_mutate_running_tasks_concurrent_removal_no_lost_updates() { + crate::config::Config::init(); + let (_dir, hcom_dir, _test_home, _guard) = isolated_test_env(); + let db_path = hcom_dir.join("test.db"); + let db = HcomDb::open_raw(&db_path).unwrap(); + db.init_db().unwrap(); + + let n: usize = 8; + let subagents: Vec = (0..n) + .map(|i| serde_json::json!({"agent_id": format!("agent-{i}"), "type": "general"})) + .collect(); + let rt_json = serde_json::json!({"active": true, "subagents": subagents}); + db.conn() + .execute( + "INSERT INTO instances (name, tool, status, status_context, status_time, created_at, last_event_id, running_tasks) + VALUES ('nova', 'claude', 'listening', 'start', 0, 0, 0, ?)", + rusqlite::params![rt_json.to_string()], + ) + .unwrap(); + + let handles: Vec<_> = (0..n) + .map(|i| { + let path = db_path.clone(); + std::thread::spawn(move || { + let db = HcomDb::open_raw(&path).unwrap(); + remove_subagent_from_parent(&db, "nova", &format!("agent-{i}")); + }) + }) + .collect(); + for h in handles { + h.join().unwrap(); + } + + let rt = instances::parse_running_tasks( + db.get_instance_full("nova") + .unwrap() + .unwrap() + .running_tasks + .as_deref(), + ); + assert!( + rt.subagents.is_empty(), + "concurrent removal must not lose sibling removals to a read-modify-write race, got {:?}", + rt.subagents + ); + assert!(!rt.active); + } + + /// Property: every Claude subagent carries `agent_id` on its hooks + /// regardless of whether its root ever ran `hcom start` — that's a + /// property of Claude's hook schema, not of hcom participation. + /// SubagentStart must stay a silent no-op (no `hcom start --name ...` + /// hint, no allocated row, no running_tasks mutation) when the shared + /// session_id has no hcom root binding at all. + #[test] + #[serial] + fn test_subagent_start_nonparticipant_root_stays_silent() { + crate::config::Config::init(); + let (_dir, _guard, db) = make_isolated_test_db(); + // No session binding: "sess-1" is not an hcom participant. + + let raw = serde_json::json!({ + "session_id": "sess-1", + "agent_id": "child-1", + "agent_type": "general", + }); + let mut payload = HookPayload::from_claude(raw); + let ctx = make_ctx(); + let (exit_code, stdout, ack, _timing) = + route_claude_hook(&db, &ctx, HOOK_SUBAGENT_START, &mut payload); + + assert_eq!(exit_code, 0); + assert!( + stdout.is_empty(), + "nonparticipant SubagentStart must not inject an hcom hint, got: {stdout}" + ); + assert!(ack.is_none()); + assert!( + db.get_instance_by_agent_id("child-1").unwrap().is_none(), + "nonparticipant SubagentStart must not allocate an instances row" + ); + } + + /// Property: a SubagentStart's own `agent_id` names the *new* child, not + /// who spawned it, so a nested spawn (subagent A calling the Agent tool) + /// can only be attributed correctly via the `prompt_id` Claude repeats + /// across the spawning PreToolUse and the resulting SubagentStart(s) (see + /// `resolve_spawn_owner`). Covers the full sequence: nested Pre on A, + /// then SubagentStart for child B with a matching prompt_id, must + /// attribute B to A (not root) — and a genuinely top-level spawn (root's + /// own Pre, no agent_id) must still attribute to root. + #[test] + #[serial] + fn test_nested_subagent_start_resolves_via_prompt_id_not_root() { + crate::config::Config::init(); + let (_dir, _guard, db) = make_isolated_test_db(); + db.conn() + .execute( + "INSERT INTO instances (name, session_id, tool, status, status_context, status_time, created_at, last_event_id, running_tasks) + VALUES ('nova', 'sess-1', 'claude', 'listening', 'start', 0, 0, 0, ?)", + rusqlite::params![ + r#"{"active":true,"subagents":[{"agent_id":"agent-a","type":"general"}]}"# + ], + ) + .unwrap(); + db.set_session_binding("sess-1", "nova").unwrap(); + insert_subagent_row(&db, "nova_task_1", "agent-a", "nova", "sess-1"); + let ctx = make_ctx(); + + // Nested Task PreToolUse fires *inside* subagent A (agent_id=agent-a), + // carrying the prompt_id Claude repeats on the SubagentStart for the + // child it spawns. + let raw_pre = serde_json::json!({ + "session_id": "sess-1", + "agent_id": "agent-a", + "prompt_id": "p1", + "tool_name": "Task", + "tool_input": {"prompt": "spawn a nested helper"}, + }); + let mut payload_pre = HookPayload::from_claude(raw_pre); + let _ = route_claude_hook(&db, &ctx, HOOK_PRE, &mut payload_pre); + + // SubagentStart for the new child B: its own agent_id names B, not A + // — only the shared prompt_id says who spawned it. + let raw_start = serde_json::json!({ + "session_id": "sess-1", + "agent_id": "agent-b", + "agent_type": "general", + "prompt_id": "p1", + }); + let mut payload_start = HookPayload::from_claude(raw_start); + let _ = route_claude_hook(&db, &ctx, HOOK_SUBAGENT_START, &mut payload_start); + + let b_name = db.get_instance_by_agent_id("agent-b").unwrap().unwrap(); + let b_row = db.get_instance_full(&b_name).unwrap().unwrap(); + assert_eq!( + b_row.parent_name.as_deref(), + Some("nova_task_1"), + "the nested child must be attributed to the spawning subagent, not root" + ); + + let a_rt = instances::parse_running_tasks( + db.get_instance_full("nova_task_1") + .unwrap() + .unwrap() + .running_tasks + .as_deref(), + ); + assert!( + a_rt.subagents + .iter() + .any(|s| s.get("agent_id").and_then(|v| v.as_str()) == Some("agent-b")), + "the spawning subagent must track its own child" + ); + + let root_rt = instances::parse_running_tasks( + db.get_instance_full("nova") + .unwrap() + .unwrap() + .running_tasks + .as_deref(), + ); + assert!( + !root_rt + .subagents + .iter() + .any(|s| s.get("agent_id").and_then(|v| v.as_str()) == Some("agent-b")), + "root must not be credited with a grandchild it never spawned" + ); + assert_eq!( + root_rt.subagents.len(), + 1, + "root's own directly-tracked subagent (A) must be untouched" + ); + + // A genuinely top-level spawn (root's own Pre, no agent_id) must + // still resolve via the same correlation to root itself. + let raw_pre2 = serde_json::json!({ + "session_id": "sess-1", + "prompt_id": "p2", + "tool_name": "Task", + "tool_input": {"prompt": "spawn a top-level helper"}, + }); + let mut payload_pre2 = HookPayload::from_claude(raw_pre2); + let _ = route_claude_hook(&db, &ctx, HOOK_PRE, &mut payload_pre2); + + let raw_start2 = serde_json::json!({ + "session_id": "sess-1", + "agent_id": "agent-c", + "agent_type": "general", + "prompt_id": "p2", + }); + let mut payload_start2 = HookPayload::from_claude(raw_start2); + let _ = route_claude_hook(&db, &ctx, HOOK_SUBAGENT_START, &mut payload_start2); + + let c_name = db.get_instance_by_agent_id("agent-c").unwrap().unwrap(); + let c_row = db.get_instance_full(&c_name).unwrap().unwrap(); + assert_eq!( + c_row.parent_name.as_deref(), + Some("nova"), + "a genuinely top-level spawn must still attribute to root" + ); + } + + /// Property: a SubagentStart with no `prompt_id` field at all (Claude + /// Code < 2.1.196, where this correlation doesn't exist on the wire) + /// must still attach to root — the pre-2.1.196 legacy behavior, not + /// nested-spawn support. + #[test] + #[serial] + fn test_subagent_start_without_prompt_id_uses_legacy_root_attribution() { + crate::config::Config::init(); + let (_dir, _guard, db) = make_isolated_test_db(); + db.conn() + .execute( + "INSERT INTO instances (name, session_id, tool, status, status_context, status_time, created_at, last_event_id) + VALUES ('nova', 'sess-1', 'claude', 'listening', 'start', 0, 0, 0)", + [], + ) + .unwrap(); + db.set_session_binding("sess-1", "nova").unwrap(); + + let raw = serde_json::json!({ + "session_id": "sess-1", + "agent_id": "agent-legacy", + "agent_type": "general", + }); + let mut payload = HookPayload::from_claude(raw); + let ctx = make_ctx(); + let _ = route_claude_hook(&db, &ctx, HOOK_SUBAGENT_START, &mut payload); + + let name = db + .get_instance_by_agent_id("agent-legacy") + .unwrap() + .unwrap(); + let row = db.get_instance_full(&name).unwrap().unwrap(); + assert_eq!(row.parent_name.as_deref(), Some("nova")); + } + + /// Property: `prompt_id` is present (Claude Code >= 2.1.196) but no + /// mapping resolves — start_task's write hasn't landed yet, or never + /// will. Correlation failure must fail closed: no allocated row, no + /// running_tasks mutation anywhere (root included), no bootstrap hint. + /// Root is not a safe fallback here — unlike the no-`prompt_id`-at-all + /// case, a version that *does* send `prompt_id` and still misses means + /// something is actually wrong. + #[test] + #[serial] + fn test_subagent_start_with_prompt_id_but_no_mapping_fails_closed() { + crate::config::Config::init(); + let (_dir, _guard, db) = make_isolated_test_db(); + db.conn() + .execute( + "INSERT INTO instances (name, session_id, tool, status, status_context, status_time, created_at, last_event_id) + VALUES ('nova', 'sess-1', 'claude', 'listening', 'start', 0, 0, 0)", + [], + ) + .unwrap(); + db.set_session_binding("sess-1", "nova").unwrap(); + let ctx = make_ctx(); + + // No preceding Task/Agent PreToolUse ever recorded this prompt_id. + let raw = serde_json::json!({ + "session_id": "sess-1", + "agent_id": "orphan-1", + "agent_type": "general", + "prompt_id": "p-missing", + }); + let mut payload = HookPayload::from_claude(raw); + let (exit_code, stdout, ack, _timing) = + route_claude_hook(&db, &ctx, HOOK_SUBAGENT_START, &mut payload); + + assert_eq!(exit_code, 0); + assert!( + stdout.is_empty(), + "an unresolved prompt_id must not inject an hcom hint, got: {stdout}" + ); + assert!(ack.is_none()); + assert!( + db.get_instance_by_agent_id("orphan-1").unwrap().is_none(), + "an unresolved prompt_id must not allocate an instances row" + ); + let root_rt = instances::parse_running_tasks( + db.get_instance_full("nova") + .unwrap() + .unwrap() + .running_tasks + .as_deref(), + ); + assert!( + root_rt.subagents.is_empty(), + "an unresolved prompt_id must not fall back to crediting root" + ); + } + + /// Property: multiple children spawned in parallel by one actor within + /// one turn share the same `prompt_id`. If an earlier sibling's own + /// Task/Agent PostToolUse completes *before* a later sibling's + /// SubagentStart arrives, the shared mapping must survive — completion + /// of one sibling must not delete a mapping the others still need (the + /// interleaving race `end_task` used to be vulnerable to when it deleted + /// the mapping per-completion; see `spawn_owner_kv_key`). + #[test] + #[serial] + fn test_parallel_siblings_survive_interleaved_sibling_completion() { + crate::config::Config::init(); + let (_dir, _guard, db) = make_isolated_test_db(); + db.conn() + .execute( + "INSERT INTO instances (name, session_id, tool, status, status_context, status_time, created_at, last_event_id) + VALUES ('nova', 'sess-1', 'claude', 'listening', 'start', 0, 0, 0)", + [], + ) + .unwrap(); + db.set_session_binding("sess-1", "nova").unwrap(); + let ctx = make_ctx(); + + // Root issues (what will become) two parallel Task calls in one + // turn — Claude stamps both with the same prompt_id. + let raw_pre = serde_json::json!({ + "session_id": "sess-1", + "prompt_id": "p1", + "tool_name": "Task", + "tool_input": {"prompt": "spawn two parallel helpers"}, + }); + let mut payload_pre = HookPayload::from_claude(raw_pre); + let _ = route_claude_hook(&db, &ctx, HOOK_PRE, &mut payload_pre); + + // First sibling starts. + let raw_start1 = serde_json::json!({ + "session_id": "sess-1", + "agent_id": "agent-1", + "agent_type": "general", + "prompt_id": "p1", + }); + let mut payload_start1 = HookPayload::from_claude(raw_start1); + let _ = route_claude_hook(&db, &ctx, HOOK_SUBAGENT_START, &mut payload_start1); + + // That Task tool_use's own PostToolUse completes — interleaved + // *before* the second sibling's SubagentStart arrives. + let raw_post = serde_json::json!({ + "session_id": "sess-1", + "prompt_id": "p1", + "tool_name": "Task", + "tool_response": {"status": "completed"}, + }); + let mut payload_post = HookPayload::from_claude(raw_post); + let _ = route_claude_hook(&db, &ctx, HOOK_POST, &mut payload_post); + + // Second sibling starts, same shared prompt_id. + let raw_start2 = serde_json::json!({ + "session_id": "sess-1", + "agent_id": "agent-2", + "agent_type": "general", + "prompt_id": "p1", + }); + let mut payload_start2 = HookPayload::from_claude(raw_start2); + let _ = route_claude_hook(&db, &ctx, HOOK_SUBAGENT_START, &mut payload_start2); + + let name1 = db.get_instance_by_agent_id("agent-1").unwrap().unwrap(); + let name2 = db.get_instance_by_agent_id("agent-2").unwrap(); + assert!( + name2.is_some(), + "the second sibling must still resolve its owner after the first sibling's PostToolUse completed" + ); + let row1 = db.get_instance_full(&name1).unwrap().unwrap(); + let row2 = db.get_instance_full(&name2.unwrap()).unwrap().unwrap(); + assert_eq!(row1.parent_name.as_deref(), Some("nova")); + assert_eq!( + row2.parent_name.as_deref(), + Some("nova"), + "both siblings must attach to the same true owner despite the interleaved completion" + ); + } + + /// Property: spawn-owner mappings are cleaned up per-session at + /// SessionEnd (see `handle_sessionend`), not per-Task-completion — must + /// remove only the ending session's own keys, never another session's. + #[test] + #[serial] + fn test_sessionend_cleans_only_its_own_session_spawn_owner_keys() { + crate::config::Config::init(); + let (_dir, _guard, db) = make_isolated_test_db(); + db.conn() + .execute( + "INSERT INTO instances (name, session_id, tool, status, status_context, status_time, created_at, last_event_id) + VALUES ('nova', 'sess-1', 'claude', 'listening', 'start', 0, 0, 0)", + [], + ) + .unwrap(); + db.set_session_binding("sess-1", "nova").unwrap(); + let ctx = make_ctx(); + + db.kv_set(&spawn_owner_kv_key("sess-1", "p1"), Some("nova")) + .unwrap(); + db.kv_set(&spawn_owner_kv_key("sess-other", "p1"), Some("other-root")) + .unwrap(); + + let raw = serde_json::json!({ + "session_id": "sess-1", + "reason": "clear", + }); + let mut payload = HookPayload::from_claude(raw); + let _ = route_claude_hook(&db, &ctx, HOOK_SESSIONEND, &mut payload); + + assert!( + db.kv_get(&spawn_owner_kv_key("sess-1", "p1")) + .unwrap() + .is_none(), + "SessionEnd must clean up its own session's spawn-owner mappings" + ); + assert_eq!( + db.kv_get(&spawn_owner_kv_key("sess-other", "p1")).unwrap(), + Some("other-root".to_string()), + "SessionEnd must not touch a different session's spawn-owner mappings" + ); + } + + #[test] + fn test_spawn_owner_kv_key_is_session_scoped() { + assert_eq!(spawn_owner_kv_key("sess-1", "p1"), "spawn_owner:sess-1:p1"); + assert_ne!( + spawn_owner_kv_key("sess-1", "p1"), + spawn_owner_kv_key("sess-2", "p1") + ); + assert!(spawn_owner_kv_key("sess-1", "p1").starts_with(&spawn_owner_kv_prefix("sess-1"))); + } + + /// Property: when SubagentStop's own `instances` row is missing, the + /// stale running_tasks entry could be on the session-bound root *or* a + /// nested subagent that actually spawned it — removal must find and + /// clear it wherever it really is, not assume root. + #[test] + #[serial] + fn test_subagent_stop_missing_row_removes_from_actual_nested_owner() { + crate::config::Config::init(); + let (_dir, _guard, db) = make_isolated_test_db(); + db.conn() + .execute( + "INSERT INTO instances (name, tool, status, status_context, status_time, created_at, last_event_id, running_tasks) + VALUES ('nova', 'claude', 'listening', 'start', 0, 0, 0, ?)", + rusqlite::params![ + r#"{"active":true,"subagents":[{"agent_id":"agent-a","type":"general"}]}"# + ], + ) + .unwrap(); + db.set_session_binding("sess-1", "nova").unwrap(); + db.conn() + .execute( + "INSERT INTO instances (name, tool, status, status_context, status_time, created_at, last_event_id, agent_id, parent_name, running_tasks) + VALUES ('nova_task_1', 'claude', 'active', 'subagent', 0, 0, 0, 'agent-a', 'nova', ?)", + rusqlite::params![ + r#"{"active":true,"subagents":[{"agent_id":"agent-b","type":"general"}]}"# + ], + ) + .unwrap(); + // B (agent-b) has no instances row at all — allocation never + // happened, or the row is gone. + + let raw = serde_json::json!({ + "session_id": "sess-1", + "agent_id": "agent-b", + }); + let mut payload = HookPayload::from_claude(raw); + let ctx = make_ctx(); + let _ = route_claude_hook(&db, &ctx, HOOK_SUBAGENT_STOP, &mut payload); + + let a_rt = instances::parse_running_tasks( + db.get_instance_full("nova_task_1") + .unwrap() + .unwrap() + .running_tasks + .as_deref(), + ); + assert!( + a_rt.subagents.is_empty(), + "the actual nested owner (A) must have the dead child reaped" + ); + assert!(!a_rt.active); + + let root_rt = instances::parse_running_tasks( + db.get_instance_full("nova") + .unwrap() + .unwrap() + .running_tasks + .as_deref(), + ); + assert_eq!( + root_rt.subagents.len(), + 1, + "root must be untouched — it never tracked the nested child directly" + ); + } + + // ---- Resumed-agent correlation (agent_owner) ---- + + /// Property: a resumed subagent — Claude re-firing SubagentStart for a + /// previously-known `agent_id` under a *new* `prompt_id` with no + /// corresponding PreToolUse to map it — must reattach to its + /// original owner via the session-scoped agent_owner memory, not fail + /// closed. Sequential: the original subagent fully spawns and stops + /// (row deleted) before the resume fires. + #[test] + #[serial] + fn test_resumed_agent_id_reattaches_via_agent_owner_after_original_stopped() { + crate::config::Config::init(); + let (_dir, _guard, db) = make_isolated_test_db(); + db.conn() + .execute( + "INSERT INTO instances (name, session_id, tool, status, status_context, status_time, created_at, last_event_id) + VALUES ('nova', 'sess-1', 'claude', 'listening', 'start', 0, 0, 0)", + [], + ) + .unwrap(); + db.set_session_binding("sess-1", "nova").unwrap(); + let ctx = make_ctx(); + + // Original spawn. + let raw_pre = serde_json::json!({ + "session_id": "sess-1", + "prompt_id": "p1", + "tool_name": "Task", + "tool_input": {"prompt": "do a thing"}, + }); + let mut payload_pre = HookPayload::from_claude(raw_pre); + let _ = route_claude_hook(&db, &ctx, HOOK_PRE, &mut payload_pre); + + let raw_start = serde_json::json!({ + "session_id": "sess-1", + "agent_id": "agent-x", + "agent_type": "general", + "prompt_id": "p1", + }); + let mut payload_start = HookPayload::from_claude(raw_start); + let _ = route_claude_hook(&db, &ctx, HOOK_SUBAGENT_START, &mut payload_start); + let original_name = db.get_instance_by_agent_id("agent-x").unwrap().unwrap(); + assert_eq!( + db.get_instance_full(&original_name) + .unwrap() + .unwrap() + .parent_name + .as_deref(), + Some("nova") + ); + + // It stops (dormant + no direct message => immediate idle stop). + let raw_stop = serde_json::json!({ + "session_id": "sess-1", + "agent_id": "agent-x", + "prompt_id": "p1", + }); + let mut payload_stop = HookPayload::from_claude(raw_stop); + let _ = route_claude_hook(&db, &ctx, HOOK_SUBAGENT_STOP, &mut payload_stop); + assert!( + db.get_instance_by_agent_id("agent-x").unwrap().is_none(), + "row must be gone after stop" + ); + + // Resume: same agent_id, new prompt_id, no PreToolUse for it. + let raw_resume = serde_json::json!({ + "session_id": "sess-1", + "agent_id": "agent-x", + "agent_type": "general", + "prompt_id": "p2", + }); + let mut payload_resume = HookPayload::from_claude(raw_resume); + let _ = route_claude_hook(&db, &ctx, HOOK_SUBAGENT_START, &mut payload_resume); + + let resumed_name = db.get_instance_by_agent_id("agent-x").unwrap(); + assert!( + resumed_name.is_some(), + "resume must not fail closed just because its new prompt_id has no mapping" + ); + let resumed_row = db + .get_instance_full(&resumed_name.unwrap()) + .unwrap() + .unwrap(); + assert_eq!( + resumed_row.parent_name.as_deref(), + Some("nova"), + "resume must reattach to the true original owner via agent_owner memory" + ); + } + + /// Property: the resumed subagent's next PostToolUse must resolve its + /// identity (not the reported `unknown_subagent_actor` fail-closed path) + /// once resume has reattached it — otherwise `hcom list`/`send` can't see + /// a subagent Claude's own TUI still shows as live. + #[test] + #[serial] + fn test_resumed_agent_posttooluse_resolves_actor_not_unknown() { + crate::config::Config::init(); + let (_dir, _guard, db) = make_isolated_test_db(); + db.conn() + .execute( + "INSERT INTO instances (name, session_id, tool, status, status_context, status_time, created_at, last_event_id) + VALUES ('nova', 'sess-1', 'claude', 'listening', 'start', 0, 0, 0)", + [], + ) + .unwrap(); + db.set_session_binding("sess-1", "nova").unwrap(); + // Pre-seed agent_owner as if this agent_id was spawned + stopped + // once already (see the sequential test above for the full path). + db.kv_set(&agent_owner_kv_key("sess-1", "agent-x"), Some("nova")) + .unwrap(); + let ctx = make_ctx(); + + let raw_resume = serde_json::json!({ + "session_id": "sess-1", + "agent_id": "agent-x", + "agent_type": "general", + "prompt_id": "p2", + }); + let mut payload_resume = HookPayload::from_claude(raw_resume); + let _ = route_claude_hook(&db, &ctx, HOOK_SUBAGENT_START, &mut payload_resume); + assert!(db.get_instance_by_agent_id("agent-x").unwrap().is_some()); + + let raw_post = serde_json::json!({ + "session_id": "sess-1", + "agent_id": "agent-x", + "tool_name": "Read", + "tool_input": {}, + }); + let mut payload_post = HookPayload::from_claude(raw_post); + let (exit_code, _stdout, _ack, timing) = + route_claude_hook(&db, &ctx, HOOK_POST, &mut payload_post); + assert_eq!(exit_code, 0); + assert_ne!( + timing.result, + Some("unknown_subagent_actor"), + "the resumed agent's own row must resolve, not fail closed as unknown" + ); + } + + /// Property: concurrent duplicate hook delivery (the other live finding) + /// combined with a resume must still converge on the one true owner — + /// no split attribution, no lost row allocation. + #[test] + #[serial] + fn test_concurrent_resumed_subagent_start_resolves_to_same_owner() { + crate::config::Config::init(); + let (_dir, hcom_dir, _test_home, _guard) = isolated_test_env(); + let db_path = hcom_dir.join("test.db"); + let db = HcomDb::open_raw(&db_path).unwrap(); + db.init_db().unwrap(); + db.conn() + .execute( + "INSERT INTO instances (name, session_id, tool, status, status_context, status_time, created_at, last_event_id) + VALUES ('nova', 'sess-1', 'claude', 'listening', 'start', 0, 0, 0)", + [], + ) + .unwrap(); + db.set_session_binding("sess-1", "nova").unwrap(); + db.kv_set(&agent_owner_kv_key("sess-1", "agent-x"), Some("nova")) + .unwrap(); + + let n = 4; + let handles: Vec<_> = (0..n) + .map(|_| { + let path = db_path.clone(); + std::thread::spawn(move || { + let db = HcomDb::open_raw(&path).unwrap(); + let ctx = make_ctx(); + let raw = serde_json::json!({ + "session_id": "sess-1", + "agent_id": "agent-x", + "agent_type": "general", + "prompt_id": "p-resume", + }); + let mut payload = HookPayload::from_claude(raw); + route_claude_hook(&db, &ctx, HOOK_SUBAGENT_START, &mut payload) + }) + }) + .collect(); + for h in handles { + h.join().unwrap(); + } + + let name = db.get_instance_by_agent_id("agent-x").unwrap(); + assert!( + name.is_some(), + "concurrent resumed SubagentStart must not fail closed" + ); + let row = db.get_instance_full(&name.unwrap()).unwrap().unwrap(); + assert_eq!(row.parent_name.as_deref(), Some("nova")); + } + + // ---- Duplicate hook delivery idempotency (SubagentStop) ---- + + /// Property: the claim key must collapse a byte-identical duplicate + /// invocation, but must NOT collapse a same-`prompt_id` invocation whose + /// payload actually differs (SubagentStop legitimately re-fires within + /// one prompt/continuation after an exit_code=2 delivery — we have not + /// established Claude changes `prompt_id` for that re-fire), and must + /// not collapse a genuinely different `prompt_id` either. + #[test] + fn test_subagent_stop_claim_key_semantics() { + let raw = serde_json::json!({"agent_id": "a1", "prompt_id": "p1", "x": 1}); + let raw_dup = serde_json::json!({"agent_id": "a1", "prompt_id": "p1", "x": 1}); + assert_eq!( + subagent_stop_claim_key("sess-1", "a1", &raw), + subagent_stop_claim_key("sess-1", "a1", &raw_dup), + "byte-identical payloads must collapse to the same key" + ); + + let raw_same_prompt_diff_payload = + serde_json::json!({"agent_id": "a1", "prompt_id": "p1", "x": 2}); + assert_ne!( + subagent_stop_claim_key("sess-1", "a1", &raw), + subagent_stop_claim_key("sess-1", "a1", &raw_same_prompt_diff_payload), + "same prompt_id with different payload content must not collapse" + ); + + let raw_diff_prompt = serde_json::json!({"agent_id": "a1", "prompt_id": "p2", "x": 1}); + assert_ne!( + subagent_stop_claim_key("sess-1", "a1", &raw), + subagent_stop_claim_key("sess-1", "a1", &raw_diff_prompt), + "different prompt_id must not collapse" + ); + } + + /// Property: while a claim is held, an identical repeat loses (concurrency + /// guard), and a same-prompt-but-different payload gets its own claim. + #[test] + #[serial] + fn test_claim_subagent_stop_collapses_duplicate_invocation() { + crate::config::Config::init(); + let (_dir, _guard, db) = make_isolated_test_db(); + let raw = serde_json::json!({"agent_id": "agent-x", "prompt_id": "p1"}); + let _first_claim = SubagentStopClaim::acquire(&db, "sess-1", "agent-x", &raw) + .expect("first claim must succeed"); + assert!( + SubagentStopClaim::acquire(&db, "sess-1", "agent-x", &raw).is_none(), + "duplicate identical invocation must not re-claim while held" + ); + + let raw_different_payload = + serde_json::json!({"agent_id": "agent-x", "prompt_id": "p1", "note": "different"}); + let _different_claim = + SubagentStopClaim::acquire(&db, "sess-1", "agent-x", &raw_different_payload) + .expect("same prompt_id but different payload content must be its own claim"); + } + + /// The claim is a transient concurrency guard, not a session-long + /// tombstone. Two distinct SubagentStop invocations can carry identical + /// payloads. While one is + /// in-flight the identical duplicate must lose (concurrency dedup), but + /// once `SubagentStopClaim` releases it, a later identical stop must be able + /// to re-claim and be processed — not suppressed forever. + #[test] + #[serial] + fn test_stop_claim_released_lets_later_identical_stop_reclaim() { + crate::config::Config::init(); + let (_dir, _guard, db) = make_isolated_test_db(); + let raw = serde_json::json!({ + "agent_id": "agent-x", + "prompt_id": "p1", + "last_assistant_message": "Done.", + }); + let key = subagent_stop_claim_key("sess-1", "agent-x", &raw); + + let first_claim = SubagentStopClaim::acquire(&db, "sess-1", "agent-x", &raw) + .expect("first invocation claims"); + assert!( + SubagentStopClaim::acquire(&db, "sess-1", "agent-x", &raw).is_none(), + "a concurrent duplicate still in-flight must lose the claim" + ); + drop(first_claim); + assert!( + db.kv_get(&key).unwrap().is_none(), + "guard drop must release the claim key" + ); + let _later_claim = SubagentStopClaim::acquire(&db, "sess-1", "agent-x", &raw) + .expect("a later, genuinely distinct stop with a byte-identical payload must re-claim"); + } + + /// `subagent_stop` must release its claim so it cannot outlive the + /// invocation. Uses + /// a zero timeout so the poll returns immediately without blocking. + #[test] + #[serial] + fn test_subagent_stop_releases_its_claim_on_completion() { + crate::config::Config::init(); + let (_dir, _guard, db) = make_isolated_test_db(); + db.conn() + .execute( + "INSERT INTO instances (name, session_id, tool, status, status_context, status_time, created_at, last_event_id, subagent_timeout) + VALUES ('nova', 'sess-1', 'claude', 'listening', 'start', 0, 0, 0, 0)", + [], + ) + .unwrap(); + // name_announced=1 -> skip the dormant idle gate and go straight to the + // poll, which returns immediately (timeout 0) with no message. + insert_subagent_row(&db, "nova_task_1", "agent-x", "nova", "sess-1"); + db.conn() + .execute( + "UPDATE instances SET name_announced = 1 WHERE name = 'nova_task_1'", + [], + ) + .unwrap(); + + let raw = serde_json::json!({ + "session_id": "sess-1", + "agent_id": "agent-x", + "prompt_id": "p1", + "last_assistant_message": "Done.", + }); + let _ = subagent_stop(&db, "sess-1", &raw); + + assert!( + db.kv_get(&subagent_stop_claim_key("sess-1", "agent-x", &raw)) + .unwrap() + .is_none(), + "subagent_stop must not leave its claim behind as a permanent tombstone" + ); + } + + /// Property: `subagent_stop` must check the claim *before* the idle gate + /// and before `poll_messages` — a duplicate invocation must skip all + /// processing, not just the final teardown. This is what the live + /// teardown-window failure traced back to: with duplicate hook + /// registrations, one invocation could receive a delivered message + /// (exit_code=2) while the other, unclaimed, independently timed out and + /// deleted the row out from under it. + #[test] + #[serial] + fn test_subagent_stop_skips_all_processing_when_already_claimed() { + crate::config::Config::init(); + let (_dir, _guard, db) = make_isolated_test_db(); + db.conn() + .execute( + "INSERT INTO instances (name, session_id, tool, status, status_context, status_time, created_at, last_event_id) + VALUES ('nova', 'sess-1', 'claude', 'listening', 'start', 0, 0, 0)", + [], + ) + .unwrap(); + insert_subagent_row(&db, "nova_task_1", "agent-x", "nova", "sess-1"); + + let raw = serde_json::json!({ + "session_id": "sess-1", + "agent_id": "agent-x", + "prompt_id": "p1", + }); + // Simulate a concurrent duplicate having already claimed this exact + // invocation (and still in-flight, so the claim is unreleased). + let _existing_claim = SubagentStopClaim::acquire(&db, "sess-1", "agent-x", &raw) + .expect("the simulated concurrent invocation must hold the claim"); + + let (exit_code, stdout) = subagent_stop(&db, "sess-1", &raw); + assert_eq!(exit_code, 0); + assert!(stdout.is_empty()); + assert!( + db.get_instance_by_agent_id("agent-x").unwrap().is_some(), + "an already-claimed duplicate must not delete the row" + ); + let stopped_events: i64 = db + .conn() + .query_row( + "SELECT COUNT(*) FROM events WHERE type = 'life' + AND json_extract(data, '$.action') = 'stopped' + AND json_extract(data, '$.snapshot.agent_id') = 'agent-x'", + [], + |row| row.get(0), + ) + .unwrap(); + assert_eq!( + stopped_events, 0, + "an already-claimed duplicate must not log a second life.stopped event" + ); + } + + /// Property: genuinely concurrent duplicate SubagentStop hook delivery + /// (separate DB connections, as separate hook processes would be) for + /// the same dormant subagent must produce exactly one teardown — one + /// life.stopped event, one row deletion — not one per invocation. + #[test] + #[serial] + fn test_concurrent_duplicate_subagent_stop_produces_one_teardown() { + crate::config::Config::init(); + let (_dir, hcom_dir, _test_home, _guard) = isolated_test_env(); + let db_path = hcom_dir.join("test.db"); + let db = HcomDb::open_raw(&db_path).unwrap(); + db.init_db().unwrap(); + db.conn() + .execute( + "INSERT INTO instances (name, session_id, tool, status, status_context, status_time, created_at, last_event_id) + VALUES ('nova', 'sess-1', 'claude', 'listening', 'start', 0, 0, 0)", + [], + ) + .unwrap(); + insert_subagent_row(&db, "nova_task_1", "agent-x", "nova", "sess-1"); + + let n = 4; + let handles: Vec<_> = (0..n) + .map(|_| { + let path = db_path.clone(); + std::thread::spawn(move || { + let db = HcomDb::open_raw(&path).unwrap(); + // Identical payload from every "duplicate hook registration". + let raw = serde_json::json!({ + "session_id": "sess-1", + "agent_id": "agent-x", + "prompt_id": "p1", + }); + subagent_stop(&db, "sess-1", &raw) + }) + }) + .collect(); + for h in handles { + h.join().unwrap(); + } + + assert!( + db.get_instance_by_agent_id("agent-x").unwrap().is_none(), + "the subagent must end up stopped exactly like a single invocation would" + ); + let stopped_events: i64 = db + .conn() + .query_row( + "SELECT COUNT(*) FROM events WHERE type = 'life' + AND json_extract(data, '$.action') = 'stopped' + AND json_extract(data, '$.snapshot.agent_id') = 'agent-x'", + [], + |row| row.get(0), + ) + .unwrap(); + assert_eq!( + stopped_events, 1, + "concurrent duplicate delivery must log exactly one life.stopped event, not {n}" + ); + } + + /// Property: SessionEnd sweeps agent_owner and subagent_stop_claim + /// entries the same way it sweeps spawn_owner — only for its own + /// session. + #[test] + #[serial] + fn test_sessionend_cleans_agent_owner_and_stop_claim_keys_too() { + crate::config::Config::init(); + let (_dir, _guard, db) = make_isolated_test_db(); + db.conn() + .execute( + "INSERT INTO instances (name, session_id, tool, status, status_context, status_time, created_at, last_event_id) + VALUES ('nova', 'sess-1', 'claude', 'listening', 'start', 0, 0, 0)", + [], + ) + .unwrap(); + db.set_session_binding("sess-1", "nova").unwrap(); + let ctx = make_ctx(); + + db.kv_set(&agent_owner_kv_key("sess-1", "agent-x"), Some("nova")) + .unwrap(); + db.kv_set(&agent_owner_kv_key("sess-other", "agent-x"), Some("other")) + .unwrap(); + let stop_raw = serde_json::json!({"agent_id": "agent-x", "prompt_id": "p1"}); + db.kv_set( + &subagent_stop_claim_key("sess-1", "agent-x", &stop_raw), + Some("1"), + ) + .unwrap(); + db.kv_set( + &subagent_stop_claim_key("sess-other", "agent-x", &stop_raw), + Some("1"), + ) + .unwrap(); + + let raw = serde_json::json!({"session_id": "sess-1", "reason": "clear"}); + let mut payload = HookPayload::from_claude(raw); + let _ = route_claude_hook(&db, &ctx, HOOK_SESSIONEND, &mut payload); + + assert!( + db.kv_get(&agent_owner_kv_key("sess-1", "agent-x")) + .unwrap() + .is_none() + ); + assert!( + db.kv_get(&subagent_stop_claim_key("sess-1", "agent-x", &stop_raw)) + .unwrap() + .is_none() + ); + assert_eq!( + db.kv_get(&agent_owner_kv_key("sess-other", "agent-x")) + .unwrap(), + Some("other".to_string()), + "a different session's agent_owner keys must survive" + ); + assert_eq!( + db.kv_get(&subagent_stop_claim_key("sess-other", "agent-x", &stop_raw)) + .unwrap(), + Some("1".to_string()), + "a different session's stop-claim keys must survive" + ); + } } diff --git a/src/hooks/common.rs b/src/hooks/common.rs index 154cfe34..8ce68927 100644 --- a/src/hooks/common.rs +++ b/src/hooks/common.rs @@ -1113,6 +1113,25 @@ fn stop_instance_inner( } } + // Recursively stop native subagents whose immediate parent is this + // instance. Native subagent rows carry session_id=NULL and inherit the + // *root* session as their parent_session_id, so the session-keyed cascade + // above never links a nested parent to its own children — only parent_name + // does. Without this, stopping a nested parent leaves its children alive and + // reparented to a name that can later be reused. MAX_STOP_DEPTH guards + // against a parent_name cycle; a row already stopped above is a no-op here. + let native_children: Vec = db + .conn() + .prepare("SELECT name FROM instances WHERE parent_name = ?") + .and_then(|mut stmt| { + stmt.query_map(params![instance_name], |row| row.get::<_, String>(0)) + .map(|rows| rows.filter_map(|r| r.ok()).collect()) + }) + .unwrap_or_default(); + for child in native_children { + stop_instance_inner(db, &child, initiated_by, "parent_stopped", false, depth + 1); + } + // Clean notify endpoints and process bindings for this instance let _ = db.delete_notify_endpoints(instance_name); let _ = db.conn().execute( diff --git a/src/instance_names.rs b/src/instance_names.rs index a6294cbe..0f72b449 100644 --- a/src/instance_names.rs +++ b/src/instance_names.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use rusqlite::OptionalExtension; use std::collections::HashSet; use crate::db::HcomDb; @@ -407,60 +408,59 @@ pub struct SubagentAllocation<'a> { /// If an instance row already exists for `agent_id`, returns its name without /// re-inserting (so SubagentStart can run before `hcom start --name` without /// creating duplicates, and vice versa). Otherwise computes the next free -/// suffix, INSERTs the row with `SQLite UNIQUE(name)` as the collision guard, -/// and retries once with `max_n + 2` on constraint violation. +/// suffix and INSERTs the row. +/// +/// The idempotency check, suffix scan, and INSERT share one `BEGIN IMMEDIATE` +/// transaction so concurrent sibling hooks cannot choose the same suffix. pub fn allocate_subagent_instance(db: &HcomDb, info: &SubagentAllocation) -> Result { - // Return early if a row already exists for this agent_id. - let existing: Option = db - .conn() - .query_row( - "SELECT name FROM instances WHERE agent_id = ?", - rusqlite::params![info.agent_id], - |row| row.get(0), - ) - .ok(); - if let Some(name) = existing { - return Ok(name); - } - let sanitized = sanitize_subagent_type(info.agent_type); let pattern = format!("{}_{}_", info.parent_name, sanitized); let like_pattern = format!("{pattern}%"); - let names: Vec = { - let mut stmt = db - .conn() - .prepare("SELECT name FROM instances WHERE name LIKE ?")?; - stmt.query_map(rusqlite::params![like_pattern], |row| row.get(0))? - .filter_map(|r| r.ok()) - .collect() - }; - - let mut max_n: u32 = 0; - for name in &names { - if let Some(suffix) = name.strip_prefix(&pattern) - && let Ok(n) = suffix.parse::() - { - max_n = max_n.max(n); - } - } - - let candidate = format!("{pattern}{}", max_n + 1); let initial_event_id = db.get_last_event_id(); let cwd = std::env::current_dir() .map(|p| p.to_string_lossy().to_string()) .unwrap_or_default(); let now = crate::shared::time::now_epoch_f64(); - let insert_sql = "INSERT INTO instances \ - (name, session_id, parent_session_id, parent_name, tag, agent_id, \ - created_at, last_event_id, directory, last_stop, status, status_context) \ - VALUES (?, NULL, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)"; + db.with_immediate_transaction(|txn| { + // Idempotency check must live inside the transaction too: otherwise a + // concurrent SubagentStart and `hcom start --name ` for the + // same agent_id could both miss it and insert two rows. + let existing: Option = txn + .query_row( + "SELECT name FROM instances WHERE agent_id = ?", + rusqlite::params![info.agent_id], + |row| row.get(0), + ) + .optional()?; + if let Some(name) = existing { + return Ok(name); + } + + let names: Vec = { + let mut stmt = txn.prepare("SELECT name FROM instances WHERE name LIKE ?")?; + stmt.query_map(rusqlite::params![like_pattern], |row| row.get(0))? + .filter_map(|r| r.ok()) + .collect() + }; + + let mut max_n: u32 = 0; + for name in &names { + if let Some(suffix) = name.strip_prefix(&pattern) + && let Ok(n) = suffix.parse::() + { + max_n = max_n.max(n); + } + } - let do_insert = |name: &str| -> rusqlite::Result { - db.conn().execute( - insert_sql, + let candidate = format!("{pattern}{}", max_n + 1); + txn.execute( + "INSERT INTO instances \ + (name, session_id, parent_session_id, parent_name, tag, agent_id, \ + created_at, last_event_id, directory, last_stop, status, status_context) \ + VALUES (?, NULL, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)", rusqlite::params![ - name, + candidate, info.parent_session_id, info.parent_name, info.parent_tag, @@ -471,22 +471,9 @@ pub fn allocate_subagent_instance(db: &HcomDb, info: &SubagentAllocation) -> Res info.status, info.status_context, ], - ) - }; - - match do_insert(&candidate) { - Ok(_) => Ok(candidate), - Err(rusqlite::Error::SqliteFailure(err, _)) - if err.code == rusqlite::ErrorCode::ConstraintViolation => - { - let retry = format!("{pattern}{}", max_n + 2); - do_insert(&retry).map_err(|e| { - anyhow::anyhow!("Failed to create unique subagent name after retry: {e}") - })?; - Ok(retry) - } - Err(e) => Err(anyhow::anyhow!("Failed to insert subagent instance: {e}")), - } + )?; + Ok(candidate) + }) } #[cfg(test)] @@ -579,6 +566,62 @@ mod subagent_alloc_tests { assert_eq!(name, "luna_reviewer_2"); } + #[test] + fn allocate_concurrent_siblings_all_get_rows() { + // Separate connections mirror concurrent hook processes. + let tmp = TempDir::new().unwrap(); + let path = tmp.path().join("race.db"); + { + let db = HcomDb::open_raw(&path).unwrap(); + db.init_db().unwrap(); + } + + const N: usize = 8; + let barrier = std::sync::Arc::new(std::sync::Barrier::new(N)); + let handles: Vec<_> = (0..N) + .map(|i| { + let path = path.clone(); + let barrier = barrier.clone(); + std::thread::spawn(move || { + let db = HcomDb::open_raw(&path).unwrap(); + let agent_id = format!("aid-{i}"); + barrier.wait(); + allocate_subagent_instance( + &db, + &SubagentAllocation { + agent_id: &agent_id, + agent_type: "reviewer", + parent_name: "luna", + parent_session_id: None, + parent_tag: None, + status: "inactive", + status_context: Some("subagent:dormant"), + }, + ) + }) + }) + .collect(); + + let mut ok = 0; + for h in handles { + if h.join().unwrap().is_ok() { + ok += 1; + } + } + + let db = HcomDb::open_raw(&path).unwrap(); + let rows: i64 = db + .conn() + .query_row( + "SELECT COUNT(*) FROM instances WHERE parent_name = 'luna'", + [], + |row| row.get(0), + ) + .unwrap(); + assert_eq!(ok, N, "every concurrent allocation must succeed"); + assert_eq!(rows, N as i64, "every subagent must get its own row"); + } + #[test] fn allocate_writes_status_and_context_columns() { let (_tmp, db) = setup_db(); diff --git a/src/instances.rs b/src/instances.rs index 5b00e773..02a2a684 100644 --- a/src/instances.rs +++ b/src/instances.rs @@ -6,6 +6,7 @@ use crate::db::{HcomDb, InstanceRow}; use crate::shared::ST_INACTIVE; +use rusqlite::{OptionalExtension, Transaction}; pub fn is_remote_instance(data: &InstanceRow) -> bool { data.origin_device_id.is_some() @@ -56,6 +57,34 @@ pub struct RunningTasks { pub subagents: Vec, } +impl RunningTasks { + pub fn tracks_subagent(&self, agent_id: &str) -> bool { + self.subagents.iter().any(|subagent| { + subagent.get("agent_id").and_then(|value| value.as_str()) == Some(agent_id) + }) + } + + pub fn track_subagent(&mut self, agent_id: &str, agent_type: &str) { + self.active = true; + if !self.tracks_subagent(agent_id) { + self.subagents.push(serde_json::json!({ + "agent_id": agent_id, + "type": agent_type, + })); + } + } + + pub fn remove_subagent(&mut self, agent_id: &str) { + let original_len = self.subagents.len(); + self.subagents.retain(|subagent| { + subagent.get("agent_id").and_then(|value| value.as_str()) != Some(agent_id) + }); + if self.subagents.len() != original_len && self.subagents.is_empty() { + self.active = false; + } + } +} + pub fn parse_running_tasks(json_str: Option<&str>) -> RunningTasks { let Some(s) = json_str else { return RunningTasks::default(); @@ -77,6 +106,111 @@ pub fn parse_running_tasks(json_str: Option<&str>) -> RunningTasks { } } +fn write_running_tasks( + txn: &Transaction<'_>, + name: &str, + running_tasks: &RunningTasks, +) -> anyhow::Result<()> { + let serialized = serde_json::json!({ + "active": running_tasks.active, + "subagents": &running_tasks.subagents, + }) + .to_string(); + txn.execute( + "UPDATE instances SET running_tasks = ? WHERE name = ?", + rusqlite::params![serialized, name], + )?; + Ok(()) +} + +/// Atomically read-modify-write an instance's `running_tasks` JSON column. +/// +/// `running_tasks` is a whole-JSON-blob field (`{"active":bool,"subagents":[...]}`) +/// mutated by SubagentStart/SubagentStop/Task-tool hooks. Each hook invocation +/// is a separate process with its own DB connection, so a plain read-then-write +/// (SELECT then UPDATE as two statements) races: parallel SubagentStart/ +/// SubagentStop for sibling subagents of the same parent can each read the +/// same starting JSON and clobber each other's update. Wrapping the +/// read-modify-write in a `BEGIN IMMEDIATE` transaction serializes concurrent +/// mutators through SQLite's write lock instead. +pub fn mutate_running_tasks(db: &HcomDb, name: &str, mutate: impl FnOnce(&mut RunningTasks)) { + let result = db.with_immediate_transaction(|txn| { + let current: Option = txn + .query_row( + "SELECT running_tasks FROM instances WHERE name = ?", + rusqlite::params![name], + |row| row.get(0), + ) + .optional()?; + + let mut running_tasks = parse_running_tasks(current.as_deref()); + mutate(&mut running_tasks); + write_running_tasks(txn, name, &running_tasks) + }); + + if let Err(e) = result { + crate::log::log_error( + "core", + "db.error", + &format!("mutate_running_tasks: {} - {}", name, e), + ); + } +} + +/// Find whichever instance currently tracks `agent_id` in its +/// `running_tasks.subagents`, and atomically remove it from there. +/// +/// Used when a caller can't name the owner directly (e.g. SubagentStop's own +/// `instances` row is missing, so `parent_name` can't be read off it) — the +/// owner could be the session-bound root *or* a nested subagent that spawned +/// this one, so this scans rather than assuming root. No-ops if nothing +/// tracks `agent_id`. +pub fn remove_tracked_subagent_by_agent_id(db: &HcomDb, agent_id: &str) { + let result = db.with_immediate_transaction(|txn| { + let owner: Option = { + let mut stmt = txn.prepare( + "SELECT name, running_tasks FROM instances + WHERE running_tasks IS NOT NULL AND running_tasks != ''", + )?; + // Collect first so a row-decode error surfaces as a hard error + // (propagated via `?` below) instead of being silently treated + // as "no owner found" — this is correctness-critical cleanup, a + // read failure must not look identical to a clean miss. + let rows: Vec<(String, Option)> = stmt + .query_map([], |row| { + Ok((row.get::<_, String>(0)?, row.get::<_, Option>(1)?)) + })? + .collect::>>()?; + rows.into_iter() + .find(|(_, rt)| parse_running_tasks(rt.as_deref()).tracks_subagent(agent_id)) + .map(|(name, _)| name) + }; + + let Some(owner) = owner else { + return Ok(()); + }; + + let current: Option = txn + .query_row( + "SELECT running_tasks FROM instances WHERE name = ?", + rusqlite::params![owner], + |row| row.get(0), + ) + .optional()?; + let mut running_tasks = parse_running_tasks(current.as_deref()); + running_tasks.remove_subagent(agent_id); + write_running_tasks(txn, &owner, &running_tasks) + }); + + if let Err(e) = result { + crate::log::log_error( + "core", + "db.error", + &format!("remove_tracked_subagent_by_agent_id: {} - {}", agent_id, e), + ); + } +} + /// Update instance position atomically. /// If instance doesn't exist, UPDATE silently affects 0 rows. pub fn update_instance_position( diff --git a/src/pty/mod.rs b/src/pty/mod.rs index f3a8c16c..c6540fc8 100644 --- a/src/pty/mod.rs +++ b/src/pty/mod.rs @@ -777,6 +777,15 @@ impl Proxy { // Track last written title to detect changes (delivery thread updates Arcs) let mut last_written_name = String::new(); let mut last_written_status = String::new(); + // Terminal-title behavior. Read once — a session's config doesn't change + // under it. In `Off` we neither strip the tool's titles nor write our own; + // in `Combined` we append the tool's live title (read from `self.screen`, + // which this thread owns — no extra Arc needed). + let title_mode = crate::config::HcomConfig::load(None) + .map(|c| crate::shared::TitleMode::from_config(&c.title_mode)) + .unwrap_or(crate::shared::TitleMode::Combined); + let title_enabled = title_mode != crate::shared::TitleMode::Off; + let mut last_written_child = String::new(); // Track incomplete UTF-8 sequences to defer title writes. // When PTY output ends with partial multi-byte character, writing our title OSC @@ -985,7 +994,9 @@ impl Proxy { eagain_retries = 0; // reset on successful read let data = &buf[..n]; raw_chunks.push(data.to_vec()); - let (filtered, had_title) = if stdout_is_tty { + // In Off mode, don't strip the tool's own titles — + // let them reach the terminal untouched. + let (filtered, had_title) = if stdout_is_tty && title_enabled { title_filter.filter(data) } else { (data.to_vec(), false) @@ -1241,7 +1252,7 @@ impl Proxy { // but only when that write left no incomplete UTF-8 or escape sequence // (`title_write_safe`) — splitting one would corrupt the stream. // pending_utf8/pending_escape carry that state across read boundaries. - if stdout_is_tty && title_write_safe(pending_utf8, pending_escape) { + if stdout_is_tty && title_enabled && title_write_safe(pending_utf8, pending_escape) { let (name, status) = { let n = self .current_name @@ -1257,11 +1268,30 @@ impl Proxy { .unwrap_or_default(); (n, s) }; - if !name.is_empty() && (name != last_written_name || status != last_written_status) + // The wrapped tool's live title (Combined only). Owned by this + // thread via self.screen, so no lock — read fresh each iteration + // and fold into the change check so a new child title re-emits. + let child = if title_mode == crate::shared::TitleMode::Combined { + self.screen.child_title().unwrap_or("") + } else { + "" + }; + if !name.is_empty() + && (name != last_written_name + || status != last_written_status + || child != last_written_child) { - let escape = - shared::build_title_escape(&name, &status, self.config.target.name()); + let child_opt = (!child.is_empty()).then_some(child); + let escape = shared::build_title_escape( + &name, + &status, + self.config.target.name(), + title_mode, + child_opt, + ); write_all(&stdout_fd, escape.as_bytes())?; + last_written_child.clear(); + last_written_child.push_str(child); last_written_name = name; last_written_status = status; } diff --git a/src/pty/screen.rs b/src/pty/screen.rs index 51dd230c..8d121e0e 100644 --- a/src/pty/screen.rs +++ b/src/pty/screen.rs @@ -81,6 +81,44 @@ fn last_osc_title(buffer: &[u8]) -> Option { last_title } +/// Max `char`s of a wrapped tool's title to embed in hcom's own title. +/// Codex/gemini cap their own titles at 80–240; this keeps the combined string +/// readable in tab bars after hcom's `{icon} name [tool]` prefix. +const MAX_CHILD_TITLE_CHARS: usize = 160; + +/// Normalize a wrapped tool's raw title into a single bounded line safe to embed +/// inside hcom's own OSC sequence. +/// +/// The input is untrusted display text (model output, project paths, etc.). We +/// drop control characters (which could terminate or reshape our OSC) and other +/// C0/C1 codepoints, collapse whitespace runs to a single space, trim the ends, +/// and bound the result to [`MAX_CHILD_TITLE_CHARS`]. Mirrors codex's own +/// `sanitize_terminal_title` so passthrough matches what the tool would render. +fn sanitize_child_title(title: &str) -> String { + let mut out = String::new(); + let mut pending_space = false; + for ch in title.chars() { + if ch.is_whitespace() { + pending_space = !out.is_empty(); + continue; + } + // Strip C0/C1 controls and invisible/bidi format chars — anything that + // could break the OSC framing or visually reorder the title. + if ch.is_control() || matches!(ch, '\u{200B}'..='\u{200F}' | '\u{202A}'..='\u{202E}') { + continue; + } + if pending_space { + out.push(' '); + pending_space = false; + } + if out.chars().count() >= MAX_CHILD_TITLE_CHARS { + break; + } + out.push(ch); + } + out +} + /// Trim whitespace including NBSP (U+00A0) from both ends fn trim_with_nbsp(s: &str) -> &str { s.trim_matches(|c: char| c.is_whitespace() || c == '\u{00A0}') @@ -110,6 +148,11 @@ pub struct ScreenTracker { cols: u16, ready_pattern: String, waiting_approval: bool, + // Last complete, sanitized OSC 0/2 title the wrapped tool set, cached for the + // Combined title passthrough. Only ever holds a fully-terminated title (see + // `process`), so a title evicted mid-scan from `output_buffer` leaves the last + // good value intact rather than showing a fragment. + last_child_title: Option, last_output: Instant, last_change: Instant, output_buffer: Vec, @@ -147,6 +190,7 @@ impl ScreenTracker { cols, ready_pattern: String::from_utf8_lossy(ready_pattern).into_owned(), waiting_approval: false, + last_child_title: None, last_output: Instant::now(), last_change: Instant::now(), output_buffer: Vec::with_capacity(4096), @@ -210,8 +254,15 @@ impl ScreenTracker { // Codex emits an ungated OSC terminal title on every state refresh. Treat // approval as a level so a later Working/idle title clears it promptly. + // last_osc_title only returns fully-terminated titles, so caching the + // sanitized value here never stores a fragment (see `last_child_title`). if let Some(title) = last_osc_title(&self.output_buffer) { self.waiting_approval = title.contains(CODEX_ACTION_REQUIRED); + let sanitized = sanitize_child_title(&title); + // An empty, complete title is meaningful: tools use it to clear + // their title on exit or when resetting state. Do not retain an + // obsolete spinner forever in combined mode. + self.last_child_title = Some(sanitized); } // Feed to vt100 parser. vt100 has known panics on malformed/edge-case @@ -308,6 +359,13 @@ impl ScreenTracker { self.waiting_approval } + /// The wrapped tool's last complete, sanitized terminal title, if any. + /// Used by combined title mode to append the tool's own live title + /// to hcom's `{icon} name [tool]` label. + pub fn child_title(&self) -> Option<&str> { + self.last_child_title.as_deref() + } + /// Codex approval fallback for blocker dialogs visible on screen. /// /// Terminal-title detection is primary. This catches dialog variants that @@ -1044,6 +1102,7 @@ mod tests { cols, ready_pattern: ready_pattern.to_string(), waiting_approval: false, + last_child_title: None, last_output: Instant::now(), last_change: Instant::now(), output_buffer: Vec::new(), @@ -1057,6 +1116,46 @@ mod tests { } } + #[test] + fn sanitize_child_title_strips_controls_and_collapses_whitespace() { + assert_eq!( + sanitize_child_title(" Working\t\non task "), + "Working on task" + ); + // Embedded ESC / BEL (the only bytes that could break out of our OSC) + // are dropped; the harmless leftover text stays. + assert_eq!(sanitize_child_title("a\x1b]2;evil\x07b"), "a]2;evilb"); + assert_eq!(sanitize_child_title(""), ""); + } + + #[test] + fn sanitize_child_title_bounds_length() { + let long = "x".repeat(MAX_CHILD_TITLE_CHARS + 50); + assert_eq!( + sanitize_child_title(&long).chars().count(), + MAX_CHILD_TITLE_CHARS + ); + } + + #[test] + fn process_captures_child_osc_title() { + let mut t = make_tracker(24, 80, ""); + assert_eq!(t.child_title(), None); + t.process(b"before\x1b]0;\xe2\xa0\x8b Working\x07after"); + assert_eq!(t.child_title(), Some("⠋ Working")); + } + + #[test] + fn child_title_keeps_last_complete_through_eviction() { + let mut t = make_tracker(24, 80, ""); + t.process(b"\x1b]0;First title\x07"); + assert_eq!(t.child_title(), Some("First title")); + // Flood past the 4KB rolling buffer with plain output containing no + // complete title; the last good title must survive rather than clear. + t.process(&vec![b'.'; 8192]); + assert_eq!(t.child_title(), Some("First title")); + } + // ---- vt100 panic containment (issue #73) ---- #[test] diff --git a/src/pty/shared.rs b/src/pty/shared.rs index c74f9c57..4264eb98 100644 --- a/src/pty/shared.rs +++ b/src/pty/shared.rs @@ -570,8 +570,29 @@ pub(super) fn finalize_launch_failure_after_exit( } /// Build the OSC 1/2 title-set escape for `name`/`status` under `tool_name`. -pub(super) fn build_title_escape(name: &str, status: &str, tool_name: &str) -> String { - let title = crate::shared::format_pane_title(status, name, tool_name); +/// +/// - [`TitleMode::Label`] → `◉ luna [claude]` (hcom's status label only). +/// - [`TitleMode::Combined`] → `◉ luna - ⠋ Working` — hcom's `{icon} name` plus +/// the wrapped tool's live title after ` - ` (dropping the `[tool]` tag). The +/// child text is already sanitized upstream by `ScreenTracker` (control/escape +/// bytes stripped, whitespace collapsed, length bounded) so it cannot break +/// out of the OSC we wrap it in. +/// +/// [`TitleMode::Off`] never reaches here — the caller skips writing entirely and +/// lets the tool's own title pass through — so it falls back to the label. +pub(super) fn build_title_escape( + name: &str, + status: &str, + tool_name: &str, + mode: crate::shared::TitleMode, + child_title: Option<&str>, +) -> String { + let title = match mode { + crate::shared::TitleMode::Combined => { + crate::shared::format_pane_title_combined(status, name, child_title) + } + _ => crate::shared::format_pane_title(status, name, tool_name), + }; format!("\x1b]1;{}\x07\x1b]2;{}\x07", title, title) } @@ -749,6 +770,10 @@ pub(super) struct OutputModeFilter { buf: Vec, dsr_seen: bool, pending_utf8: u8, + /// When true (title_mode `off`), the tool's own OSC 0/1/2 titles are passed + /// through to the terminal instead of stripped. DSR/ground-state tracking is + /// unaffected. Default false preserves the strip-and-override behavior. + passthrough_titles: bool, } #[derive(Default, PartialEq)] @@ -770,6 +795,12 @@ enum FilterState { #[cfg_attr(not(windows), allow(dead_code))] impl OutputModeFilter { + /// Pass the tool's own OSC 0/1/2 titles through instead of stripping them + /// (title_mode `off`). DSR/ground-state tracking is unaffected. + pub(super) fn set_passthrough_titles(&mut self, passthrough: bool) { + self.passthrough_titles = passthrough; + } + pub(super) fn filter(&mut self, input: &[u8], out: &mut Vec) { let output_start = out.len(); for &b in input { @@ -863,11 +894,16 @@ impl OutputModeFilter { FilterState::OscDigit(_digit) => { self.buf.push(b); if b == b';' { - // Confirmed OSC 0/1/2: discard the complete title, - // including a terminator that may arrive in a later read. + // Confirmed OSC 0/1/2: discard the complete title + // (including a terminator that may arrive in a later + // read) — unless title_mode `off`, where we pass the + // tool's own title through untouched. + if self.passthrough_titles { + out.extend_from_slice(&self.buf); + } self.buf.clear(); self.state = FilterState::StringSeq { - strip: true, + strip: !self.passthrough_titles, saw_esc: false, discarded: 0, }; @@ -1129,6 +1165,18 @@ mod tests { ); } + #[test] + fn output_mode_filter_passthrough_keeps_tool_title() { + // title_mode `off`: the tool's own OSC 0/2 title must reach the terminal + // intact, including across a read split, while DSR tracking still works. + let mut f = OutputModeFilter::default(); + f.set_passthrough_titles(true); + let mut out = Vec::new(); + f.filter(b"before\x1b]2;Clau", &mut out); + f.filter(b"de Code\x07after", &mut out); + assert_eq!(out, b"before\x1b]2;Claude Code\x07after".to_vec()); + } + #[test] fn output_mode_filter_preserves_non_title_osc_and_tracks_its_boundary() { let chunks: &[&[u8]] = &[b"\x1b]8;;https://exam", b"ple.test\x1b\\link"]; @@ -1191,9 +1239,10 @@ mod tests { } #[test] - fn build_title_escape_formats_osc_1_and_2() { - // listening icon is the green dot; assert exact OSC framing. - let esc = build_title_escape("alpha", "listening", "claude"); + fn build_title_escape_label_mode_formats_osc_1_and_2() { + use crate::shared::TitleMode; + // Label mode keeps the [tool] tag; assert exact OSC framing. + let esc = build_title_escape("alpha", "listening", "claude", TitleMode::Label, None); let icon = status_icon("listening"); let title = format!("{} alpha [claude]", icon); assert_eq!(esc, format!("\x1b]1;{}\x07\x1b]2;{}\x07", title, title)); @@ -1204,12 +1253,40 @@ mod tests { #[test] fn build_title_escape_uses_status_icon() { + use crate::shared::TitleMode; // Different statuses must change the embedded icon. - let listening = build_title_escape("a", "listening", "claude"); - let blocked = build_title_escape("a", "blocked", "claude"); + let listening = build_title_escape("a", "listening", "claude", TitleMode::Label, None); + let blocked = build_title_escape("a", "blocked", "claude", TitleMode::Label, None); assert_ne!(listening, blocked); } + #[test] + fn build_title_escape_combined_appends_child_and_drops_tool() { + use crate::shared::TitleMode; + // Combined mode: `{icon} name - {child}`, no `[tool]` tag. + let icon = status_icon("active"); + let esc = build_title_escape( + "luna", + "active", + "codex", + TitleMode::Combined, + Some("⠋ Working"), + ); + let title = format!("{} luna - ⠋ Working", icon); + assert_eq!(esc, format!("\x1b]1;{}\x07\x1b]2;{}\x07", title, title)); + assert!(!esc.contains("[codex]"), "combined mode drops the tool tag"); + } + + #[test] + fn build_title_escape_combined_without_child_is_icon_name() { + use crate::shared::TitleMode; + // No child title → just `{icon} name`, no dangling separator, no tag. + let icon = status_icon("active"); + let esc = build_title_escape("luna", "active", "codex", TitleMode::Combined, None); + let title = format!("{} luna", icon); + assert_eq!(esc, format!("\x1b]1;{}\x07\x1b]2;{}\x07", title, title)); + } + #[test] fn note_user_keystroke_cursor_is_noop_and_returns_false() { let target = PtyTarget::AdhocCommand("cursor".to_string()); diff --git a/src/pty/win.rs b/src/pty/win.rs index 77472272..4c63b479 100644 --- a/src/pty/win.rs +++ b/src/pty/win.rs @@ -528,6 +528,15 @@ impl Proxy { let headless = !std::io::stdout().is_terminal(); let mut last_name = String::new(); let mut last_status = String::new(); + // Terminal-title behavior. Read once; the child title comes from the + // reader-owned `screen`, no extra lock needed. In `Off` the filter + // passes the tool's own titles through and we write nothing. + let title_mode = crate::config::HcomConfig::load(None) + .map(|c| crate::shared::TitleMode::from_config(&c.title_mode)) + .unwrap_or(crate::shared::TitleMode::Combined); + let title_enabled = title_mode != crate::shared::TitleMode::Off; + filter.set_passthrough_titles(!title_enabled); + let mut last_child = String::new(); // `hcom term` snapshot refresh (see should_refresh_snapshot). Under // sustained output we render at most once per SNAPSHOT_THROTTLE; @@ -675,16 +684,31 @@ impl Proxy { // under heavy output) and name/status rarely change, so // the common path holds the two read locks briefly but // allocates nothing. + let child = if title_mode == crate::shared::TitleMode::Combined { + screen.child_title().unwrap_or("") + } else { + "" + }; if !headless + && title_enabled && filter.title_write_safe() && let (Ok(name), Ok(status)) = (current_name.read(), current_status.read()) && !name.is_empty() - && (*name != last_name || *status != last_status) + && (*name != last_name || *status != last_status || child != last_child) { - let esc = shared::build_title_escape(&name, &status, target.name()); + let child_opt = (!child.is_empty()).then_some(child); + let esc = shared::build_title_escape( + &name, + &status, + target.name(), + title_mode, + child_opt, + ); let _ = stdout.write_all(esc.as_bytes()); let _ = stdout.flush(); + last_child.clear(); + last_child.push_str(child); last_name = name.clone(); last_status = status.clone(); } diff --git a/src/shared/constants.rs b/src/shared/constants.rs index d36212ab..ccb04a50 100644 --- a/src/shared/constants.rs +++ b/src/shared/constants.rs @@ -95,6 +95,32 @@ pub fn status_icon(status: &str) -> &'static str { // Adhoc instance icon lives on `IntegrationSpec.adhoc_icon` for Tool::Adhoc. +/// Terminal-title behavior, from config `terminal.title_mode`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TitleMode { + /// hcom leaves the title alone: the wrapped tool's own title passes through + /// untouched and hcom writes nothing. + Off, + /// hcom's status label only: `{icon} name [tool]` (the original behavior). + Label, + /// hcom status + the tool's live title: `{icon} name - {tool title}`. + Combined, +} + +/// Valid `terminal.title_mode` config values (for validation + help). +pub const VALID_TITLE_MODES: &[&str] = &["combined", "label", "off"]; + +impl TitleMode { + /// Parse a config value; unknown/empty falls back to the default (`Combined`). + pub fn from_config(s: &str) -> Self { + match s { + "off" => Self::Off, + "label" => Self::Label, + _ => Self::Combined, + } + } +} + /// Build the canonical pane-title label hcom writes into OSC 1/2 and pushes /// to host terminal label APIs (e.g. herdr's `pane.rename`). /// @@ -108,6 +134,21 @@ pub fn format_pane_title(status: &str, display: &str, tool: &str) -> String { format!("{} {} [{}]", status_icon(status), display, tool) } +/// Build the `TitleMode::Combined` label: `"{icon} {display}"`, with the wrapped +/// tool's live title appended after ` - ` when present. Drops the `[tool]` tag +/// (the passthrough title already identifies the tool's activity). Returns an +/// empty string when `display` is empty so callers can short-circuit. +pub fn format_pane_title_combined(status: &str, display: &str, child: Option<&str>) -> String { + if display.is_empty() { + return String::new(); + } + let base = format!("{} {}", status_icon(status), display); + match child { + Some(c) if !c.is_empty() => format!("{base} - {c}"), + _ => base, + } +} + /// Status foreground ANSI color. pub fn status_fg(status: &str) -> &'static str { match status { diff --git a/src/shared/mod.rs b/src/shared/mod.rs index b45c2109..f893ad02 100644 --- a/src/shared/mod.rs +++ b/src/shared/mod.rs @@ -28,9 +28,12 @@ pub use constants::{ ST_LAUNCHING, ST_LISTENING, SYSTEM_SENDER, + TitleMode, + VALID_TITLE_MODES, // Functions extract_mentions, format_pane_title, + format_pane_title_combined, status_bg, status_fg, status_icon, diff --git a/tests/support/claude_mock.rs b/tests/support/claude_mock.rs index 1082e1d7..77fc336d 100644 --- a/tests/support/claude_mock.rs +++ b/tests/support/claude_mock.rs @@ -18,11 +18,16 @@ use super::real_tool::{ FORK_PROOF, INBOUND_PROOF, INITIAL_PROOF, RESUME_PROOF, ScenarioIds, ToolCase, ToolMeta, }; +// Pinned at >= 2.1.198 (not just >= 2.1.196 for `prompt_id`): 2.1.198 is also +// where Agent/Task calls started backgrounding by default +// (tool_response.status="async_launched"), which hcom's hook routing must +// handle. Pinning below 2.1.198 would let real-tool CI pass without ever +// exercising either behavior. const CLAUDE_META: ToolMeta = ToolMeta { tool: "claude", binary: "claude", - pinned_version: "2.1.185", - install_command: "npm install --global @anthropic-ai/claude-code@2.1.185", + pinned_version: "2.1.216", + install_command: "npm install --global @anthropic-ai/claude-code@2.1.216", }; pub const MODEL: &str = "claude-sonnet-4-6"; diff --git a/tests/support/codex_mock.rs b/tests/support/codex_mock.rs index 21095cb1..70b8939f 100644 --- a/tests/support/codex_mock.rs +++ b/tests/support/codex_mock.rs @@ -23,8 +23,8 @@ use super::real_tool::{ const CODEX_META: ToolMeta = ToolMeta { tool: "codex", binary: "codex", - pinned_version: "0.141.0", - install_command: "npm install --global @openai/codex@0.141.0", + pinned_version: "0.145.0", + install_command: "npm install --global @openai/codex@0.145.0", }; /// Codex adapter for the shared real-tool lifecycle. Codex has no fake-response diff --git a/tests/test_relay_roundtrip.rs b/tests/test_relay_roundtrip.rs index 43d5b83e..e42d49f4 100644 --- a/tests/test_relay_roundtrip.rs +++ b/tests/test_relay_roundtrip.rs @@ -1260,7 +1260,7 @@ fn test_relay_roundtrip() { logln!(log, "\n[Phase 7] Device A: remote launch on Device B..."); let claude_version = - std::env::var("HCOM_TEST_CLAUDE_VERSION").unwrap_or_else(|_| "2.1.185".to_string()); + std::env::var("HCOM_TEST_CLAUDE_VERSION").unwrap_or_else(|_| "2.1.216".to_string()); assert_tool_pinned( "claude", &claude_version,