diff --git a/.github/actions/build-native/action.yml b/.github/actions/build-native/action.yml index 10bd00f85..e6f758a01 100644 --- a/.github/actions/build-native/action.yml +++ b/.github/actions/build-native/action.yml @@ -12,7 +12,7 @@ inputs: description: Target arch (x64, arm64) required: true variant: - description: Optional build variant (baseline, modern) + description: Optional build variant (baseline, modern); required for native x64 builds. required: false default: "" target: @@ -46,17 +46,54 @@ runs: toolchain_bin="$(dirname "$(rustup which cargo)")" echo "$toolchain_bin" >> "$GITHUB_PATH" echo "Prepended $toolchain_bin to PATH" + # `Swatinem/rust-cache` keys target/ off its restore-time environment, so + # set RUSTFLAGS before restoring it. If x64 target-cpu is only selected + # inside ci-build-native.ts/build-native.ts, cargo invalidates the restored + # target/ but rust-cache sees an exact key and refuses to save the rebuilt + # artifacts, causing macOS x64 baseline to rebuild forever. + # + # Include the native source hash in the shared key as well: rust-cache's + # lockfile scan misses the workspace root Cargo.toml version that Cargo + # fingerprints for workspace crates. Without it, release version bumps can + # get an exact hit for artifacts Cargo must rebuild. + # + # sccache is still layered on top of rust-cache: target/ wins when warm, + # sccache fills the gaps when target/ is cold. + - name: Configure native Rust flags + if: inputs.target == '' + shell: bash + env: + TARGET_ARCH: ${{ inputs.arch }} + TARGET_VARIANT: ${{ inputs.variant }} + run: | + case "$TARGET_ARCH:$TARGET_VARIANT" in + x64:modern) + rustflags="-C target-cpu=x86-64-v3" + ;; + x64:baseline) + rustflags="-C target-cpu=x86-64-v2" + ;; + x64:*) + echo "::error::x64 native builds require variant=modern or variant=baseline" + exit 1 + ;; + *) + if [ -n "${RUSTFLAGS:-}" ]; then + echo "Using caller-provided RUSTFLAGS=$RUSTFLAGS" + exit 0 + fi + rustflags="-C target-cpu=native" + ;; + esac + + echo "RUSTFLAGS=$rustflags" >> "$GITHUB_ENV" + echo "Configured RUSTFLAGS=$rustflags" - uses: Swatinem/rust-cache@v2 with: - shared-key: native-${{ inputs.platform }}-${{ inputs.arch }}-${{ inputs.variant || 'default' }} + shared-key: native-${{ inputs.platform }}-${{ inputs.arch }}-${{ inputs.variant || 'default' }}-h${{ inputs.hash }} cache-on-failure: true save-if: ${{ inputs.save_cache == 'true' }} cache-workspace-crates: true - # `Swatinem/rust-cache` keys target/ off Cargo.lock content; release - # tag pushes bump workspace versions, busting that key every time. sccache - # caches at the rustc-invocation level (source + flags), so it still hits - # across version bumps. Layered on top of rust-cache: target/ wins when - # warm, sccache fills the gaps when target/ is cold. - name: Setup sccache uses: mozilla-actions/sccache-action@v0.0.10 - name: Enable sccache for cargo diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b838310ac..09245ef0a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,6 @@ name: CI on: push: branches: [main] - tags: ["v*"] pull_request: branches: [main] workflow_dispatch: @@ -18,6 +17,54 @@ concurrency: cancel-in-progress: true jobs: + # scripts/release.ts pushes the version-bump commit and its `v*` tag + # atomically (`git push --atomic origin main refs/tags/v*`), so a release + # now arrives as a single `push` to `refs/heads/main` — we no longer trigger + # on the tag ref at all (see `on.push`). This one branch-push run is therefore + # authoritative: it runs the full build AND, when HEAD carries a release tag, + # the release/publish jobs. `gate` resolves that tag once so downstream jobs + # switch on `is-release` and address the tag by name — `github.ref` is + # `refs/heads/main` here, not the tag. A `workflow_dispatch` from a `v*` tag + # ref is also treated as a release (the manual re-publish escape hatch). + gate: + runs-on: ubuntu-22.04 + outputs: + is-release: ${{ steps.check.outputs.is-release }} + release-tag: ${{ steps.check.outputs.release-tag }} + steps: + # Only a main-branch push needs tags fetched, so `git tag --points-at + # HEAD` can see the freshly-pushed `v*`. A tag-ref dispatch reads the + # tag straight from `github.ref_name`, and fetching `--tags` while + # checkout uses an explicit tag refspec makes git refuse — so scope + # fetch-tags to main pushes. + - uses: actions/checkout@v4 + with: + fetch-tags: ${{ github.ref == 'refs/heads/main' }} + - name: Detect release tag at HEAD + id: check + shell: bash + run: | + is_release=false + release_tag="" + case "${{ github.ref }}" in + refs/tags/v[0-9]*) + release_tag="${{ github.ref_name }}" + ;; + refs/heads/main) + if [ "${{ github.event_name }}" != "pull_request" ]; then + release_tag=$(git tag --points-at HEAD | grep -E '^v[0-9]' | head -n1 || true) + fi + ;; + esac + if [ -n "$release_tag" ]; then + echo "HEAD carries release tag $release_tag; this run builds and publishes the release." + is_release=true + fi + { + echo "is-release=$is_release" + echo "release-tag=$release_tag" + } >> "$GITHUB_OUTPUT" + # Compute a stable hash of every input that affects the native cdylib output, # then look for any prior successful main run that already uploaded the # native artifacts for this hash. Two independent outputs: @@ -134,10 +181,10 @@ jobs: run: bun run ci:check:full # Linux x64 baseline + modern: required by `test`, so it runs on every PR - # unless rust-hash found a cached run. Tags always rebuild for fresh artifacts. + # unless rust-hash found a cached run. Release pushes always rebuild for fresh artifacts. native_linux: - needs: [rust-hash] - if: ${{ startsWith(github.ref, 'refs/tags/v') || needs.rust-hash.outputs.linux-run-id == '' }} + needs: [gate, rust-hash] + if: ${{ needs.gate.outputs.is-release == 'true' || needs.rust-hash.outputs.linux-run-id == '' }} runs-on: ubuntu-22.04 strategy: fail-fast: false @@ -154,14 +201,14 @@ jobs: arch: x64 variant: ${{ matrix.variant }} rust_checks: ${{ matrix.rust_checks && 'true' || 'false' }} - save_cache: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) }} + save_cache: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} # Pre-warm the cross-platform native build cache on `main`, in addition to # building the artifacts that ship in release tags. Skipped on main when the # rust-hash canary already found a recent run with all artifacts intact. native_release: - needs: [rust-hash] - if: ${{ startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.rust-hash.outputs.release-run-id == '') }} + needs: [gate, rust-hash] + if: ${{ needs.gate.outputs.is-release == 'true' || (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.rust-hash.outputs.release-run-id == '') }} strategy: fail-fast: false matrix: @@ -180,7 +227,7 @@ jobs: arch: ${{ matrix.arch }} variant: ${{ matrix.variant }} target: ${{ matrix.target }} - save_cache: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) }} + save_cache: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} test: runs-on: ubuntu-22.04 @@ -222,13 +269,10 @@ jobs: run-id: ${{ steps.source.outputs.run-id }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: Test workspace (TS) - env: - # Bun's `bun test` emits `::group::`/`::endgroup::` per file under - # GHA. `--workspaces` prefixes each output line with ` test: `, - # which breaks GHA's column-0 parsing and leaks the markers as - # literal text. Unset for this step only — the annotations would be - # equally broken by the prefix, so we lose nothing. - GITHUB_ACTIONS: "" + # `test:ts` sets GITHUB_ACTIONS=0 inline so `bun test` skips its + # per-file `::group::`/`::endgroup::` annotations. Under `--workspaces` + # every line is prefixed with ` test: `, which breaks GHA's + # column-0 parsing and would leak those markers as literal log spam. run: bun run test:ts - name: CLI smoke test run: bun run ci:test:smoke @@ -247,8 +291,7 @@ jobs: with: shared-key: install-methods-linux-x64 cache-on-failure: true - save-if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || - startsWith(github.ref, 'refs/tags/v')) }} + save-if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} cache-workspace-crates: true # Layer sccache on top of rust-cache for the same reason as the # build-native action: tag pushes bump workspace versions and bust @@ -279,11 +322,11 @@ jobs: run: bun run ci:test:install-methods release_binary: - if: ${{ startsWith(github.ref, 'refs/tags/v') && !cancelled() && + if: ${{ needs.gate.outputs.is-release == 'true' && !cancelled() && needs.native_linux.result == 'success' && needs.native_release.result == 'success' && needs.test.result == 'success' && needs.check.result == 'success' && needs.install_methods.result == 'success' }} - needs: [check, native_linux, native_release, test, install_methods, rust-hash] + needs: [gate, check, native_linux, native_release, test, install_methods, rust-hash] strategy: fail-fast: false matrix: @@ -326,11 +369,20 @@ jobs: runs-on: ${{ matrix.os }} permissions: contents: read + id-token: write steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 with: bun-version: "1.3" + - uses: actions/setup-node@v4 + with: + node-version: "24" + registry-url: "https://registry.npmjs.org" + # Trusted publishing allowed-actions flags require npm >= 11.16.0. + - name: Ensure npm supports trusted publishing + if: ${{ !inputs.skip_npm }} + run: npm install -g npm@latest - name: Cache bun dependencies uses: actions/cache@v4 with: @@ -357,6 +409,14 @@ jobs: runtime_dir="$(mktemp -d)" HOME="$runtime_dir/home" XDG_DATA_HOME="$runtime_dir/xdg" "${{ matrix.binary_path }}" --version HOME="$runtime_dir/home" XDG_DATA_HOME="$runtime_dir/xdg" "${{ matrix.binary_path }}" --smoke-test + - name: Publish native addon package + if: ${{ !inputs.skip_npm }} + env: + # Fallback auth: setup-node wrote an .npmrc referencing + # NODE_AUTH_TOKEN; npm uses it only when OIDC has no trusted + # publisher for the package (or on a first publish). + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: bun run ci:release:publish-native-leaf ${{ matrix.target_id }} - name: Upload release binary artifact uses: actions/upload-artifact@v4 with: @@ -364,9 +424,9 @@ jobs: path: ${{ matrix.binary_path }} release-github: - if: ${{ startsWith(github.ref, 'refs/tags/v') && !cancelled() && + if: ${{ needs.gate.outputs.is-release == 'true' && !cancelled() && needs.release_binary.result == 'success' }} - needs: [release_binary] + needs: [gate, release_binary] runs-on: ubuntu-22.04 permissions: contents: write @@ -376,7 +436,7 @@ jobs: with: bun-version: "1.3" - name: Generate release notes from CHANGELOGs - run: bun scripts/ci-release-notes.ts + run: bun scripts/ci-release-notes.ts ${{ needs.gate.outputs.release-tag }} - name: Download release binaries uses: actions/download-artifact@v4 with: @@ -386,6 +446,7 @@ jobs: - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: + tag_name: ${{ needs.gate.outputs.release-tag }} files: | packages/coding-agent/binaries/omp-* body_path: release-notes.md @@ -393,16 +454,16 @@ jobs: release_github_verify: - if: ${{ startsWith(github.ref, 'refs/tags/v') && !cancelled() && + if: ${{ needs.gate.outputs.is-release == 'true' && !cancelled() && needs['release-github'].result == 'success' }} - needs: [release-github] + needs: [gate, release-github] runs-on: macos-14 permissions: contents: read steps: - name: Download published macOS arm64 binary run: | - curl -fsSL -o omp-darwin-arm64 "https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/omp-darwin-arm64" + curl -fsSL -o omp-darwin-arm64 "https://github.com/${{ github.repository }}/releases/download/${{ needs.gate.outputs.release-tag }}/omp-darwin-arm64" chmod +x omp-darwin-arm64 - name: Verify published macOS arm64 binary run: | @@ -411,12 +472,19 @@ jobs: HOME="$runtime_dir/home" XDG_DATA_HOME="$runtime_dir/xdg" ./omp-darwin-arm64 --version release-npm: - if: ${{ startsWith(github.ref, 'refs/tags/v') && !cancelled() && + if: ${{ needs.gate.outputs.is-release == 'true' && !cancelled() && needs.release_binary.result == 'success' && needs.release_github_verify.result == 'success' && !inputs.skip_npm }} - needs: [release_binary, release_github_verify, rust-hash] + needs: [gate, release_binary, release_github_verify] runs-on: ubuntu-22.04 + # `id-token: write` lets npm mint the GitHub OIDC token it exchanges for a + # short-lived publish token (trusted publishing + provenance). When a + # package has no matching trusted publisher configured, npm silently falls + # back to NODE_AUTH_TOKEN below — which also covers first-ever publishes. + permissions: + id-token: write + contents: read steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 @@ -426,19 +494,19 @@ jobs: with: node-version: "24" registry-url: "https://registry.npmjs.org" + # Trusted publishing (OIDC) and auto-provenance need npm >= 11.5.1. + - name: Ensure npm supports OIDC trusted publishing + run: npm install -g npm@latest - name: Cache bun dependencies uses: actions/cache@v4 with: path: ~/.bun/install/cache key: bun-${{ runner.os }}-${{ hashFiles('**/bun.lock') }} - run: bun install --frozen-lockfile - - name: Download native addons - uses: actions/download-artifact@v4 - with: - pattern: pi-natives-*-h${{ needs.rust-hash.outputs.hash }} - path: packages/natives/native - merge-multiple: true - name: Publish to npm env: - NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }} + # Fallback auth: setup-node wrote an .npmrc referencing + # NODE_AUTH_TOKEN; npm uses it only when OIDC has no trusted + # publisher for the package (or on a first publish). + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: bun run ci:release:publish diff --git a/.gitignore b/.gitignore index fd2add32e..044b6ea46 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,7 @@ pi-*.html # Generated files packages/coding-agent/src/internal-urls/docs-index.generated.ts +packages/natives/npm/ /runs/ python/omp-rpc/src/omp_rpc.egg-info/ # parallel-agent worktrees diff --git a/Cargo.lock b/Cargo.lock index 993a16d58..93e6053bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -238,9 +238,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" [[package]] name = "bitvec" @@ -501,9 +501,9 @@ checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" [[package]] name = "cc" -version = "1.2.62" +version = "1.2.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", "shlex", @@ -769,9 +769,9 @@ dependencies = [ [[package]] name = "ctor" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d765eb1c0bda10d31e0ea185f5ee15da532d60b0912d2bd1441783439e749c5" +checksum = "01334b89b69ff726750c5ce5073fc8bd860e99aa9a8fc5ca11b04730e3aee97a" [[package]] name = "darling" @@ -872,7 +872,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "objc2", ] @@ -1748,9 +1748,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.30" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" +checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f" [[package]] name = "lru" @@ -1805,9 +1805,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "wasi 0.11.1+wasi-snapshot-preview1", @@ -1830,7 +1830,7 @@ version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1d395473824516f38dd1071a1a37bc57daa7be65b293ebba4ead5f7abb017a2" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "ctor", "futures", "napi-build", @@ -1894,7 +1894,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "cfg-if", "cfg_aliases 0.1.1", "libc", @@ -1906,7 +1906,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "cfg-if", "cfg_aliases 0.2.1", "libc", @@ -1997,7 +1997,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "objc2", "objc2-core-graphics", "objc2-foundation", @@ -2009,7 +2009,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "dispatch2", "objc2", ] @@ -2020,7 +2020,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "dispatch2", "objc2", "objc2-core-foundation", @@ -2039,7 +2039,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "objc2", "objc2-core-foundation", ] @@ -2050,7 +2050,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "objc2", "objc2-core-foundation", ] @@ -2331,7 +2331,7 @@ dependencies = [ [[package]] name = "pi-ast" -version = "15.5.10" +version = "15.8.3" dependencies = [ "anyhow", "ast-grep-core", @@ -2399,7 +2399,7 @@ dependencies = [ [[package]] name = "pi-iso" -version = "15.5.10" +version = "15.8.3" dependencies = [ "async-trait", "libc", @@ -2411,7 +2411,7 @@ dependencies = [ [[package]] name = "pi-natives" -version = "15.5.10" +version = "15.8.3" dependencies = [ "anyhow", "arboard", @@ -2457,7 +2457,7 @@ dependencies = [ [[package]] name = "pi-shell" -version = "15.5.10" +version = "15.8.3" dependencies = [ "anyhow", "brush-builtins", @@ -2497,7 +2497,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "crc32fast", "fdeflate", "flate2", @@ -2567,7 +2567,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25485360a54d6861439d60facef26de713b1e126bf015ec8f98239467a2b82f7" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "chrono", "flate2", "procfs-core", @@ -2580,7 +2580,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6401bf7b6af22f78b563665d15a22e9aef27775b79b149a66ca022468a4e405" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "chrono", "hex", ] @@ -2735,7 +2735,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", ] [[package]] @@ -2833,7 +2833,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "errno", "libc", "linux-raw-sys", @@ -2975,9 +2975,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-registry" @@ -3033,9 +3033,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -3886,9 +3886,9 @@ dependencies = [ [[package]] name = "tree-sitter-swift" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3b98fb6bc8e6a6a10023f401aa6a1858115e849dfaf7de57dd8b8ea0f257bd9" +checksum = "fe36052155b9dd69ca82b3b8f1b4ccfb2d867125ac1a4db1dd7331829242668c" dependencies = [ "cc", "tree-sitter-language", @@ -4002,9 +4002,9 @@ dependencies = [ [[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" @@ -4038,9 +4038,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-width" @@ -4130,9 +4130,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.23.1" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" dependencies = [ "js-sys", "wasm-bindgen", @@ -4279,7 +4279,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "hashbrown 0.15.5", "indexmap", "semver", @@ -4304,7 +4304,7 @@ version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "rustix", "wayland-backend", "wayland-scanner", @@ -4316,7 +4316,7 @@ version = "0.32.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "wayland-backend", "wayland-client", "wayland-scanner", @@ -4328,7 +4328,7 @@ version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "wayland-backend", "wayland-client", "wayland-protocols", @@ -4836,7 +4836,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.1", + "bitflags 2.12.1", "indexmap", "log", "serde", @@ -4947,18 +4947,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.49" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.49" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index e9124c1a7..2c3d2eab9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ exclude = ["crates/brush-core-vendored", "crates/brush-builtins-vendored"] resolver = "3" [workspace.package] -version = "15.5.10" +version = "15.8.3" edition = "2024" license = "MIT" authors = ["Can Boluk"] diff --git a/README.md b/README.md index 59a450a5f..cf28855c2 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,21 @@ mise use -g github:can1357/oh-my-pi macOS · Linux · Windows · bun ≥ 1.3.14 +### Shell completions + +`omp` generates its own completion scripts for **bash**, **zsh**, and **fish** from the live command/flag metadata, so they never drift from the actual CLI. Subcommands, flags, and enum values complete statically; model names (`--model`, `--smol`, `--slow`, `--plan`) resolve against the bundled model catalog and `--resume` against your on-disk sessions. + +```sh +# zsh — add to ~/.zshrc (or write the output into a file on your $fpath) +eval "$(omp completions zsh)" + +# bash — add to ~/.bashrc +eval "$(omp completions bash)" + +# fish +omp completions fish > ~/.config/fish/completions/omp.fish +``` + ## Every tool, _benchmaxxed_. Edits that land on the first attempt. Reads that summarize files instead of dumping their content. Searches that return instantly. Pick any model — omp will get it right. @@ -198,7 +213,6 @@ Stealth's on by default, so pages see a normal user instead of a headless bot. T - `bash` — workspace shell, with optional PTY or background-job dispatch. - `eval` — persistent Python and JavaScript cells with shared prelude and tool re-entry. -- `recipe` — invoke a target from a detected task runner — bun, just, make, cargo. - `ssh` — one remote command against a configured host. **Code intelligence** @@ -233,11 +247,10 @@ Stealth's on by default, so pages see a normal user instead of a headless bot. T **Misc** -- `calc` — deterministic arithmetic — no model in the loop. - `resolve` — apply or discard a queued preview action. - `search_tool_bm25` — BM25 over the hidden tool index; activates top matches mid-session. -Setting-gated, off by default: `github`, `calc`, `inspect_image`, `render_mermaid`, `checkpoint`, `rewind`, `search_tool_bm25`, `retain`, `recall`, `reflect`. Flip them on once, scoped per project. +Setting-gated, off by default: `github`, `inspect_image`, `render_mermaid`, `checkpoint`, `rewind`, `search_tool_bm25`, `retain`, `recall`, `reflect`. Flip them on once, scoped per project. [Full reference →](https://omp.sh/docs/tools) diff --git a/bun.lock b/bun.lock index 0d66285f8..847d14690 100644 --- a/bun.lock +++ b/bun.lock @@ -15,7 +15,7 @@ }, "packages/agent": { "name": "@oh-my-pi/pi-agent-core", - "version": "15.5.10", + "version": "15.8.3", "dependencies": { "@oh-my-pi/pi-ai": "catalog:", "@oh-my-pi/pi-natives": "catalog:", @@ -30,9 +30,8 @@ }, "packages/ai": { "name": "@oh-my-pi/pi-ai", - "version": "15.5.10", + "version": "15.8.3", "dependencies": { - "@anthropic-ai/sdk": "catalog:", "@bufbuild/protobuf": "catalog:", "@oh-my-pi/pi-utils": "catalog:", "openai": "catalog:", @@ -45,7 +44,7 @@ }, "packages/coding-agent": { "name": "@oh-my-pi/pi-coding-agent", - "version": "15.5.10", + "version": "15.8.3", "bin": { "omp": "src/cli.ts", }, @@ -57,6 +56,7 @@ "@oh-my-pi/omp-stats": "catalog:", "@oh-my-pi/pi-agent-core": "catalog:", "@oh-my-pi/pi-ai": "catalog:", + "@oh-my-pi/pi-mnemopi": "catalog:", "@oh-my-pi/pi-natives": "catalog:", "@oh-my-pi/pi-tui": "catalog:", "@oh-my-pi/pi-utils": "catalog:", @@ -78,12 +78,33 @@ "devDependencies": { "@types/bun": "catalog:", }, + "optionalDependencies": { + "@huggingface/transformers": "catalog:", + }, }, "packages/hashline": { "name": "@oh-my-pi/hashline", - "version": "15.5.10", + "version": "15.8.3", "dependencies": { "diff": "catalog:", + "lru-cache": "catalog:", + }, + "devDependencies": { + "@types/bun": "catalog:", + }, + }, + "packages/mnemopi": { + "name": "@oh-my-pi/pi-mnemopi", + "version": "15.8.3", + "bin": { + "mnemopi": "src/cli.ts", + }, + "dependencies": { + "@oh-my-pi/pi-ai": "catalog:", + "@oh-my-pi/pi-utils": "catalog:", + "fastembed": "catalog:", + "lru-cache": "catalog:", + "onnxruntime-node": "catalog:", }, "devDependencies": { "@types/bun": "catalog:", @@ -91,7 +112,7 @@ }, "packages/natives": { "name": "@oh-my-pi/pi-natives", - "version": "15.5.10", + "version": "15.8.3", "devDependencies": { "@napi-rs/cli": "catalog:", "@types/bun": "catalog:", @@ -99,7 +120,7 @@ }, "packages/stats": { "name": "@oh-my-pi/omp-stats", - "version": "15.5.10", + "version": "15.8.3", "bin": { "omp-stats": "./src/index.ts", }, @@ -124,7 +145,7 @@ }, "packages/swarm-extension": { "name": "@oh-my-pi/swarm-extension", - "version": "15.5.10", + "version": "15.8.3", "bin": { "omp-swarm": "src/cli.ts", }, @@ -140,7 +161,7 @@ }, "packages/tui": { "name": "@oh-my-pi/pi-tui", - "version": "15.5.10", + "version": "15.8.3", "dependencies": { "@oh-my-pi/pi-natives": "catalog:", "@oh-my-pi/pi-utils": "catalog:", @@ -181,7 +202,7 @@ }, "packages/utils": { "name": "@oh-my-pi/pi-utils", - "version": "15.5.10", + "version": "15.8.3", "dependencies": { "@oh-my-pi/pi-natives": "catalog:", "beautiful-mermaid": "catalog:", @@ -203,83 +224,92 @@ "@tailwindcss/vite": "catalog:", "@types/bun": "catalog:", "tailwindcss": "catalog:", - "typescript": "^5.7.3", + "typescript": "catalog:", "vite": "catalog:", "vite-plugin-solid": "catalog:", }, }, }, "catalog": { - "@agentclientprotocol/sdk": "0.21.0", - "@anthropic-ai/sdk": "^0.94.0", - "@babel/generator": "^7.29.1", - "@babel/parser": "^7.29.3", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "@biomejs/biome": "^2.4.14", + "@agentclientprotocol/sdk": "0.22.1", + "@babel/generator": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@biomejs/biome": "^2.4.16", "@bufbuild/protobuf": "^2.12.0", "@bufbuild/protoc-gen-es": "^2.12.0", + "@huggingface/transformers": "^4.2.0", "@mozilla/readability": "^0.6.0", - "@napi-rs/cli": "3.6.2", - "@oh-my-pi/hashline": "15.5.10", - "@oh-my-pi/omp-stats": "15.5.10", - "@oh-my-pi/pi-agent-core": "15.5.10", - "@oh-my-pi/pi-ai": "15.5.10", - "@oh-my-pi/pi-coding-agent": "15.5.10", - "@oh-my-pi/pi-natives": "15.5.10", - "@oh-my-pi/pi-tui": "15.5.10", - "@oh-my-pi/pi-utils": "15.5.10", - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^2.0.0", - "@opentelemetry/sdk-trace-base": "^2.0.0", - "@puppeteer/browsers": "^2.13.0", - "@tailwindcss/node": "^4.2.4", - "@tailwindcss/vite": "^4.2.4", + "@napi-rs/cli": "3.7.0", + "@oh-my-pi/hashline": "15.8.3", + "@oh-my-pi/omp-stats": "15.8.3", + "@oh-my-pi/pi-agent-core": "15.8.3", + "@oh-my-pi/pi-ai": "15.8.3", + "@oh-my-pi/pi-coding-agent": "15.8.3", + "@oh-my-pi/pi-mnemopi": "15.8.3", + "@oh-my-pi/pi-natives": "15.8.3", + "@oh-my-pi/pi-tui": "15.8.3", + "@oh-my-pi/pi-utils": "15.8.3", + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/context-async-hooks": "^2.7.1", + "@opentelemetry/sdk-trace-base": "^2.7.1", + "@puppeteer/browsers": "^3.0.4", + "@tailwindcss/node": "^4.3.0", + "@tailwindcss/vite": "^4.3.0", "@types/babel__generator": "^7.27.0", "@types/babel__traverse": "^7.28.0", "@types/bun": "^1.3.14", - "@types/react": "^19.2.14", + "@types/react": "^19.2.15", "@types/react-dom": "^19.2.3", "@types/turndown": "5.0.6", - "@typescript/native-preview": "7.0.0-dev.20260505.1", + "@typescript/native-preview": "7.0.0-dev.20260527.1", "@xterm/headless": "^6.0.0", "beautiful-mermaid": "^1.1.3", "chalk": "^5.6.2", "chart.js": "^4.5.1", - "date-fns": "^4.1.0", + "date-fns": "^4.3.0", "diff": "^9.0.0", - "fflate": "0.8.2", + "fastembed": "2.1.0", + "fflate": "0.8.3", "handlebars": "^4.7.9", "linkedom": "^0.18.12", - "lint-staged": "^16.4.0", - "lru-cache": "11.3.6", - "lucide-react": "^1.14.0", - "marked": "^18.0.3", + "lint-staged": "^17.0.5", + "lru-cache": "11.5.1", + "lucide-react": "^1.16.0", + "marked": "^18.0.4", "markit-ai": "0.5.3", - "openai": "^6.36.0", + "onnxruntime-node": "1.24.3", + "openai": "^6.39.0", "partial-json": "^0.1.7", - "postcss": "^8.5.14", + "postcss": "^8.5.15", "prettier": "^3.8.3", - "puppeteer-core": "^24.42.0", - "react": "19.2.5", + "puppeteer-core": "^25.1.0", + "react": "19.2.6", "react-chartjs-2": "^5.3.1", - "react-dom": "19.2.5", + "react-dom": "19.2.6", "regexp-tree": "^0.1.27", - "solid-js": "^1.9.12", - "tailwindcss": "^4.2.4", + "solid-js": "^1.9.13", + "tailwindcss": "^4.3.0", "turndown": "7.2.4", "turndown-plugin-gfm": "1.0.2", "typescript": "^6.0.3", - "vite": "^5.4.14", - "vite-plugin-solid": "^2.11.6", + "vite": "^8.0.14", + "vite-plugin-solid": "^2.11.12", "winston": "^3.19.0", "winston-daily-rotate-file": "^5.0.0", "zod": "4.4.3", }, "packages": { - "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.21.0", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-ONj+Q8qOdNQp5XbH5jnMwzT9IKZJsSN0p0lkceS4GtUtNOPVLpNzSS8gqQdGMKfBvA0ESbkL8BTaSN1Rc9miEw=="], + "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.22.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-DfqXtl/8gO9NImq094MTaCXEU2vkhh6v7q/kT+9UjZxUqj8hYaya2OjLVIqn16MzNHcXEpShTR2RIauLSYeDQQ=="], - "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.94.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-OVlCttk5MyeTGtrWX5+F3MJOfEMDuEjK8+rm9aQMDfRPWndVMbhk37QG8WLnVbcc7huyUGngVMjT7iMN2llySA=="], + "@anush008/tokenizers": ["@anush008/tokenizers@0.0.0", "", { "optionalDependencies": { "@anush008/tokenizers-darwin-universal": "0.0.0", "@anush008/tokenizers-linux-x64-gnu": "0.0.0", "@anush008/tokenizers-win32-x64-msvc": "0.0.0" } }, "sha512-IQD9wkVReKAhsEAbDjh/0KrBGTEXelqZLpOBRDaIRvlzZ9sjmUP+gKbpvzyJnei2JHQiE8JAgj7YcNloINbGBw=="], + + "@anush008/tokenizers-darwin-universal": ["@anush008/tokenizers-darwin-universal@0.0.0", "", { "os": "darwin" }, "sha512-SACpWEooTjFX89dFKRVUhivMxxcZRtA3nJGVepdLyrwTkQ1TZQ8581B5JoXp0TcTMHfgnDaagifvVoBiFEdNCQ=="], + + "@anush008/tokenizers-linux-x64-gnu": ["@anush008/tokenizers-linux-x64-gnu@0.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-TLjByOPWUEq51L3EJkS+slyH57HKJ7lAz/aBtEt7TIPq4QsE2owOPGovByOLIq1x5Wgh9b+a4q2JasrEFSDDhg=="], + + "@anush008/tokenizers-win32-x64-msvc": ["@anush008/tokenizers-win32-x64-msvc@0.0.0", "", { "os": "win32", "cpu": "x64" }, "sha512-/5kP0G96+Cr6947F0ZetXnmL31YCaN15dbNbh2NHg7TXXRwfqk95+JtPP5Q7v4jbR2xxAmuseBqB4H/V7zKWuw=="], "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], @@ -311,31 +341,29 @@ "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="], - "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], - "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], "@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - "@biomejs/biome": ["@biomejs/biome@2.4.15", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.15", "@biomejs/cli-darwin-x64": "2.4.15", "@biomejs/cli-linux-arm64": "2.4.15", "@biomejs/cli-linux-arm64-musl": "2.4.15", "@biomejs/cli-linux-x64": "2.4.15", "@biomejs/cli-linux-x64-musl": "2.4.15", "@biomejs/cli-win32-arm64": "2.4.15", "@biomejs/cli-win32-x64": "2.4.15" }, "bin": { "biome": "bin/biome" } }, "sha512-j5VH3a/h/HXTKBM50MDMxRCzkeLv9S2XJcW2WgnZT1+xyisi+0bISrXR82gCX+8S9lvK0skEvHJRN+3Ktr2hlw=="], + "@biomejs/biome": ["@biomejs/biome@2.4.16", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.16", "@biomejs/cli-darwin-x64": "2.4.16", "@biomejs/cli-linux-arm64": "2.4.16", "@biomejs/cli-linux-arm64-musl": "2.4.16", "@biomejs/cli-linux-x64": "2.4.16", "@biomejs/cli-linux-x64-musl": "2.4.16", "@biomejs/cli-win32-arm64": "2.4.16", "@biomejs/cli-win32-x64": "2.4.16" }, "bin": { "biome": "bin/biome" } }, "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rF3PPqLq1yoST79zaQbDjVJwsuIeci/O+9bgNmC5QpgOqz6aqYuzA4abyAGx+mgyiDXn4A049xAN8gijbuR1Qg=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-/5KHXYMfSJs1fNXiX30xFtI8JcCFV6zaVVLxOa0M2sfqBKHkpQhRTv94yxQWxeTY2lzo2OuTlNvPC+hDQt2wcQ=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-owaAMZD/T4LrD0ELNCk0Km3qrRHuM0X6EAyVE1FSqGY0rbLoiDLrO4Us2tllm6cAeB2Ioa9C2C08NZPdr8+0Ug=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZPcxznxm0pogHBLZhYntyR3sR+MrZjqJIKEr7ZqVen0Rl+P/4upVmfYXjftizi9RoqZntg33fv/1fbdhbYXpEQ=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.15", "", { "os": "linux", "cpu": "x64" }, "sha512-0jj7THz12GbUOLmMibktK6DZjqz2zV64KFxyBtcFTKPiiOIY0a7vns1elpO1dERvxpsZ5ik0oFfz0oGwFde1+g=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.16", "", { "os": "linux", "cpu": "x64" }, "sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.15", "", { "os": "linux", "cpu": "x64" }, "sha512-CNq/9W38SYSH023lfcQ4KKU8K0YX8T//FZUhcgtMMRABDojx5XsMV7jlweAvGSl389wJQB29Qo6Zb/a+jdvt+w=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.16", "", { "os": "linux", "cpu": "x64" }, "sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-ouhkYdlhp/1GghEJPdWwD/Vi3gQ1nFxuSpMolWsbq3Lsq3QUR4jl6UdhhscdCugKU5vOEuMiJhvKj66O0OCq+w=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.15", "", { "os": "win32", "cpu": "x64" }, "sha512-zBrGq5mx5wwpnow4+2BxUvleDM+GNd4sLbPaMapsSLQLD0NGRCquqPBTgN+7XkUteHvj7M+BstuI8tmnV7+HgQ=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.16", "", { "os": "win32", "cpu": "x64" }, "sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw=="], "@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="], @@ -351,83 +379,103 @@ "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + "@huggingface/blake3-jit": ["@huggingface/blake3-jit@0.0.2", "", {}, "sha512-Bq7B5qabyjrJfhBsl85Jd2QBtf+HzRD7h7A9GfN2lzrrsABhOa5evVPgzoCTxR7Ub0QFj7YDK1YkYRWBU25+2w=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + "@huggingface/hub": ["@huggingface/hub@2.13.0", "", { "dependencies": { "@huggingface/tasks": "^0.21.1", "@huggingface/xetchunk-wasm": "^0.0.6" }, "optionalDependencies": { "cli-progress": "^3.12.0" }, "bin": { "hfjs": "dist/cli.js" } }, "sha512-IAoqdpTV9HeMyooxKVvVGWirOJ+S2IAKnU2FSQSMz62ehPTKzxADAATy1fwAlYYQdHCV119GHFf4p9+ECQ+I5g=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + "@huggingface/jinja": ["@huggingface/jinja@0.5.9", "", {}, "sha512-uWTG+l3VJRsl7EXxYizuL3P+cCPoc3cRqbWWRcQN0FhejRfbdq0RNhCmbY/YDtnTcz9icdLYuLDjsnz4d8JMuw=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + "@huggingface/tasks": ["@huggingface/tasks@0.21.2", "", {}, "sha512-e8dw3tZ7mbZ/mytr9zIFmsr67tMpd9rm/pURCi5ciFqlVvLvdH9FjnoZxVO4KfRXyqzPeV0shXv9z5s2M8Msmw=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + "@huggingface/tokenizers": ["@huggingface/tokenizers@0.1.3", "", {}, "sha512-8rF/RRT10u+kn7YuUbUg0OF30K8rjTc78aHpxT+qJ1uWSqxT1MHi8+9ltwYfkFYJzT/oS+qw3JVfHtNMGAdqyA=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + "@huggingface/transformers": ["@huggingface/transformers@4.2.0", "", { "dependencies": { "@huggingface/jinja": "^0.5.6", "@huggingface/tokenizers": "^0.1.3", "onnxruntime-node": "1.24.3", "onnxruntime-web": "1.26.0-dev.20260416-b7804b056c", "sharp": "^0.34.5" } }, "sha512-8BRCoBMH0XsWaEIamuR0LrJGAfftgHAfb2Vrffy0VKlSAE/MnUJ5/h/zTfEP3fDIft+nk7TqB8xXEyABGitBjQ=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + "@huggingface/xetchunk-wasm": ["@huggingface/xetchunk-wasm@0.0.6", "", { "dependencies": { "@huggingface/blake3-jit": "0.0.2", "gearhash-jit": "1.0.2" } }, "sha512-LoYPl7jvUvOnkysrhMjAeBzK5mVe2GFu8O3IsZb1w7l7v+NqjDsyRduwct3yraPAGmzTxxdb/2aIORL98Y949g=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], - "@inquirer/ansi": ["@inquirer/ansi@2.0.5", "", {}, "sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw=="], + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], - "@inquirer/checkbox": ["@inquirer/checkbox@5.1.5", "", { "dependencies": { "@inquirer/ansi": "^2.0.5", "@inquirer/core": "^11.1.10", "@inquirer/figures": "^2.0.5", "@inquirer/type": "^4.0.5" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Jmf9tgBHIEK5SAOB7swYfStqmtkZb00xOTpSQmkoGEpdxOTpJi9RS0A8bkfDPHTTItZRJrRdZrEMu25wyj0VfQ=="], + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], - "@inquirer/confirm": ["@inquirer/confirm@6.0.13", "", { "dependencies": { "@inquirer/core": "^11.1.10", "@inquirer/type": "^4.0.5" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-wkGPC7yJ5WJk1DJ5SX7fzk+gfj4BM8cf5dDDi71B/551xHrdsZVRJOC0WyikXd0pEsb/9cLniuE4atbsMqmFkw=="], + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], - "@inquirer/core": ["@inquirer/core@11.1.10", "", { "dependencies": { "@inquirer/ansi": "^2.0.5", "@inquirer/figures": "^2.0.5", "@inquirer/type": "^4.0.5", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-a4Q5BXHQAHa9eO202sTaFCHFYVB3x5fauDuThEAdZ9gfn76pSxiKU7wWcEH0N1O0XmQvNfQNU6QXpiRxmYQx+A=="], + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], - "@inquirer/editor": ["@inquirer/editor@5.1.2", "", { "dependencies": { "@inquirer/core": "^11.1.10", "@inquirer/external-editor": "^3.0.0", "@inquirer/type": "^4.0.5" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Y3Nor7S/DhIPo+8Ym/dSY4efwKI4BsflKDwXh0jNeXJsSF3dteS/3Yf+z4wkibVZDvYMyCgknSTQlNahfunGHg=="], + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], - "@inquirer/expand": ["@inquirer/expand@5.0.14", "", { "dependencies": { "@inquirer/core": "^11.1.10", "@inquirer/type": "^4.0.5" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-qyY9zcIX2eKYwaAUiQo9zORd61Lc3sXeM72fVbeHkYnDkqfr8/armcRbmVAIrExeJhI2puk+uomeKtWrpUVUmQ=="], + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], - "@inquirer/external-editor": ["@inquirer/external-editor@3.0.0", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-lDSwMgg+M5rq6JKBYaJwSX6T9e/HK2qqZ1oxmOwn4AQoJE5D+7TumsxLGC02PWS//rkIVqbZv3XA3ejsc9FYvg=="], + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], - "@inquirer/figures": ["@inquirer/figures@2.0.5", "", {}, "sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ=="], + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], - "@inquirer/input": ["@inquirer/input@5.0.13", "", { "dependencies": { "@inquirer/core": "^11.1.10", "@inquirer/type": "^4.0.5" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-0l0jCHlJnXIV8CTxwQC0C+5Ziq8WP22edWgmciW2xYvoeoSck4v5FvCS1ctKdqLLR0dUo93uAHgWHywgBSoRyw=="], + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], - "@inquirer/number": ["@inquirer/number@4.0.13", "", { "dependencies": { "@inquirer/core": "^11.1.10", "@inquirer/type": "^4.0.5" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-WHmkYnnJAou5gx7RgcvAfUggnHNM1zWfoh0dFPl3dxVssuqt+dK5rIbaOYQXNyOegvFnopbKupjnhw2O8gANNg=="], + "@inquirer/ansi": ["@inquirer/ansi@2.0.7", "", {}, "sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q=="], - "@inquirer/password": ["@inquirer/password@5.0.13", "", { "dependencies": { "@inquirer/ansi": "^2.0.5", "@inquirer/core": "^11.1.10", "@inquirer/type": "^4.0.5" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-XDGu64ROHZjOOXLAANvJN7iIxWKhOSCG5VakrZ5kaScVR+snVJCFglD/hL3/677awtWcu4pXoWa280CDIYcBeg=="], + "@inquirer/checkbox": ["@inquirer/checkbox@5.2.1", "", { "dependencies": { "@inquirer/ansi": "^2.0.7", "@inquirer/core": "^11.2.1", "@inquirer/figures": "^2.0.7", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-b6xmA/VlTe0ZgDQHDui+Nav470u7u49nRd8/iuhOcQPO9Ch7lGuogydhi2VOmNlZ+zXcM8IcPuNSwQcdJaF/kw=="], - "@inquirer/prompts": ["@inquirer/prompts@8.4.3", "", { "dependencies": { "@inquirer/checkbox": "^5.1.5", "@inquirer/confirm": "^6.0.13", "@inquirer/editor": "^5.1.2", "@inquirer/expand": "^5.0.14", "@inquirer/input": "^5.0.13", "@inquirer/number": "^4.0.13", "@inquirer/password": "^5.0.13", "@inquirer/rawlist": "^5.2.9", "@inquirer/search": "^4.1.9", "@inquirer/select": "^5.1.5" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-ai5LseTw9HhegupIgmo4cn7RpnCGznjjXu4OI+7jMR8vu7T1ZCCNMzFFAovUCjL1fl0cceksIN1++yQE59SmZw=="], + "@inquirer/confirm": ["@inquirer/confirm@6.1.1", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ=="], - "@inquirer/rawlist": ["@inquirer/rawlist@5.2.9", "", { "dependencies": { "@inquirer/core": "^11.1.10", "@inquirer/type": "^4.0.5" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-a1ErXEfgjfPYpyQ89dp+7n2IISjH9oQg3ygvF5adz8B7aHn4n2PjEgu1wpVTp69K3bj3lVLxP0qJ2b1clk1Whw=="], + "@inquirer/core": ["@inquirer/core@11.2.1", "", { "dependencies": { "@inquirer/ansi": "^2.0.7", "@inquirer/figures": "^2.0.7", "@inquirer/type": "^4.0.7", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA=="], - "@inquirer/search": ["@inquirer/search@4.1.9", "", { "dependencies": { "@inquirer/core": "^11.1.10", "@inquirer/figures": "^2.0.5", "@inquirer/type": "^4.0.5" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-ZlbM28Q9lmLkFPNAIv+ZuY530n5Km8U1WW48oYEvDhe9yc2uL3m3t+JSdRUkQlk5fuIuskgiIVjcb7czFzQpuA=="], + "@inquirer/editor": ["@inquirer/editor@5.2.0", "", { "dependencies": { "@inquirer/core": "^11.2.0", "@inquirer/external-editor": "^3.0.1", "@inquirer/type": "^4.0.6" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-/m+sgRmzSdK6HDtVnl3PmI6MnZC4O+LLezedoJcrX7mINhTjjb0hlC7aEDGZXkFTB4b5uQ0q59AhYTah88KbNg=="], - "@inquirer/select": ["@inquirer/select@5.1.5", "", { "dependencies": { "@inquirer/ansi": "^2.0.5", "@inquirer/core": "^11.1.10", "@inquirer/figures": "^2.0.5", "@inquirer/type": "^4.0.5" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-6SRg6kHfK/sjLXOsuqNebuir+sjwrf/iWuRUnXgB2slzEewppI1WfzeS16XxDcOQmXBruMmmB9Cgrz7wsAxqMg=="], + "@inquirer/expand": ["@inquirer/expand@5.1.1", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-YmQpenjbFSHAK3sOd44puHh3V1KXXr+JiNpUztoSQ4drLh2rTVzTap/YtlAVu/5xavifIlBfNEzJ/neZJ1a/1g=="], - "@inquirer/type": ["@inquirer/type@4.0.5", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q=="], + "@inquirer/external-editor": ["@inquirer/external-editor@3.0.1", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-tam+Gwjsxg2sx3iUVPkAnhKT/yrk2rd2NAa7XJU/J8OYpU0ifXsnp12xlvzp/DCpWBXVv+vLQsqnpAWwUcWD5Q=="], + + "@inquirer/figures": ["@inquirer/figures@2.0.7", "", {}, "sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw=="], + + "@inquirer/input": ["@inquirer/input@5.1.0", "", { "dependencies": { "@inquirer/core": "^11.2.0", "@inquirer/type": "^4.0.6" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-sVZCz6P6e8tW5g2bSFel1oLpa6jK/u7BexFfrgTqR8syIdnHqy+iopnlSbYBZMsCK52chLjhGNBxt0eRqhsghw=="], + + "@inquirer/number": ["@inquirer/number@4.1.1", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-XF4IXAbPnGPgw0wsbC/i2tPcyfdZgDpUlhsqU0SfT4IRIGWha6Xm9VRgN5yYxJq+jnyXlfXI/nQ3ulfk0iEICA=="], + + "@inquirer/password": ["@inquirer/password@5.1.1", "", { "dependencies": { "@inquirer/ansi": "^2.0.7", "@inquirer/core": "^11.2.1", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-3XBfF7DAsp5qeDsvN5Rd1HmbNokVvEQoUM0QLrRcybC9nX96w3Pbmu7qUsb3IT3J3jBvs2+mTXaKHOUsgHMLzg=="], + + "@inquirer/prompts": ["@inquirer/prompts@8.5.0", "", { "dependencies": { "@inquirer/checkbox": "^5.2.0", "@inquirer/confirm": "^6.1.0", "@inquirer/editor": "^5.2.0", "@inquirer/expand": "^5.1.0", "@inquirer/input": "^5.1.0", "@inquirer/number": "^4.1.0", "@inquirer/password": "^5.1.0", "@inquirer/rawlist": "^5.3.0", "@inquirer/search": "^4.2.0", "@inquirer/select": "^5.2.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-pLjXOnY4y3R1mgyHP3pXD/8eXejp+L/dde/0N2NLKgKfMstqhNZrpvs7Wkzbl9FYFQh10LRQ7QZwq+cz9rrhyw=="], + + "@inquirer/rawlist": ["@inquirer/rawlist@5.3.1", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-QqdTqQddL3qPX/PPrjobpsO25NZ4dWXgTLenrR445L2ptLEYE6Z+PD5c5CNDJNx4ugRgELAIpSIJxZaO2jJ2Og=="], + + "@inquirer/search": ["@inquirer/search@4.2.1", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/figures": "^2.0.7", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-xJj8QWKRSrfKoBIITLZK61dD3zwo0Rz11fgDImku30/Oe81zMdIdGgrLY2h6RkJ+KZ/GhNYIRMKnH/62qBTA5g=="], + + "@inquirer/select": ["@inquirer/select@5.2.1", "", { "dependencies": { "@inquirer/ansi": "^2.0.7", "@inquirer/core": "^11.2.1", "@inquirer/figures": "^2.0.7", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-FlDndEUww8m7BfukO2nJa25vhD+H5jxxCv4oGioKqzyWz3nPHhhw4LKdYRSlXuAx7DsdWia7iyaBPKKS95Evfw=="], + + "@inquirer/type": ["@inquirer/type@4.0.7", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g=="], + + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], @@ -445,7 +493,7 @@ "@mozilla/readability": ["@mozilla/readability@0.6.0", "", {}, "sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ=="], - "@napi-rs/cli": ["@napi-rs/cli@3.6.2", "", { "dependencies": { "@inquirer/prompts": "^8.0.0", "@napi-rs/cross-toolchain": "^1.0.3", "@napi-rs/wasm-tools": "^1.0.1", "@octokit/rest": "^22.0.1", "clipanion": "^4.0.0-rc.4", "colorette": "^2.0.20", "emnapi": "^1.9.1", "es-toolkit": "^1.41.0", "js-yaml": "^4.1.0", "obug": "^2.0.0", "semver": "^7.7.3", "typanion": "^3.14.0" }, "peerDependencies": { "@emnapi/runtime": "^1.7.1" }, "optionalPeers": ["@emnapi/runtime"], "bin": { "napi": "dist/cli.js", "napi-raw": "cli.mjs" } }, "sha512-jy5rABUh9tbE/vPRzw9kGzGuqZiVslyDQUV8LkvjzqVX/oJMN7g0U1uhtr9L3W1H+iRM/urXHXUf+CE4n8FvLA=="], + "@napi-rs/cli": ["@napi-rs/cli@3.7.0", "", { "dependencies": { "@inquirer/prompts": "^8.0.0", "@napi-rs/cross-toolchain": "^1.0.3", "@napi-rs/wasm-tools": "^1.0.1", "@octokit/rest": "^22.0.1", "clipanion": "^4.0.0-rc.4", "colorette": "^2.0.20", "emnapi": "^1.10.0", "es-toolkit": "^1.41.0", "js-yaml": "^4.1.0", "obug": "^2.0.0", "semver": "^7.7.3", "typanion": "^3.14.0" }, "peerDependencies": { "@emnapi/runtime": "^1.7.1" }, "optionalPeers": ["@emnapi/runtime"], "bin": { "napi": "dist/cli.js", "napi-raw": "cli.mjs" } }, "sha512-3d3+rmxlOIV/G1zPWeX4PCxuYnhcCQM2BvY9rtimC8RO0dFR9gtYP+Grov+WoduZtfWRj5N1XvytWeRxxCk5zw=="], "@napi-rs/cross-toolchain": ["@napi-rs/cross-toolchain@1.0.3", "", { "dependencies": { "@napi-rs/lzma": "^1.4.5", "@napi-rs/tar": "^1.1.0", "debug": "^4.4.1" }, "peerDependencies": { "@napi-rs/cross-toolchain-arm64-target-aarch64": "^1.0.3", "@napi-rs/cross-toolchain-arm64-target-armv7": "^1.0.3", "@napi-rs/cross-toolchain-arm64-target-ppc64le": "^1.0.3", "@napi-rs/cross-toolchain-arm64-target-s390x": "^1.0.3", "@napi-rs/cross-toolchain-arm64-target-x86_64": "^1.0.3", "@napi-rs/cross-toolchain-x64-target-aarch64": "^1.0.3", "@napi-rs/cross-toolchain-x64-target-armv7": "^1.0.3", "@napi-rs/cross-toolchain-x64-target-ppc64le": "^1.0.3", "@napi-rs/cross-toolchain-x64-target-s390x": "^1.0.3", "@napi-rs/cross-toolchain-x64-target-x86_64": "^1.0.3" }, "optionalPeers": ["@napi-rs/cross-toolchain-arm64-target-aarch64", "@napi-rs/cross-toolchain-arm64-target-armv7", "@napi-rs/cross-toolchain-arm64-target-ppc64le", "@napi-rs/cross-toolchain-arm64-target-s390x", "@napi-rs/cross-toolchain-arm64-target-x86_64", "@napi-rs/cross-toolchain-x64-target-aarch64", "@napi-rs/cross-toolchain-x64-target-armv7", "@napi-rs/cross-toolchain-x64-target-ppc64le", "@napi-rs/cross-toolchain-x64-target-s390x", "@napi-rs/cross-toolchain-x64-target-x86_64"] }, "sha512-ENPfLe4937bsKVTDA6zdABx4pq9w0tHqRrJHyaGxgaPq03a2Bd1unD5XSKjXJjebsABJ+MjAv1A2OvCgK9yehg=="], @@ -549,7 +597,7 @@ "@napi-rs/wasm-tools-win32-x64-msvc": ["@napi-rs/wasm-tools-win32-x64-msvc@1.0.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rEAf05nol3e3eei2sRButmgXP+6ATgm0/38MKhz9Isne82T4rPIMYsCIFj0kOisaGeVwoi2fnm7O9oWp5YVnYQ=="], - "@nodable/entities": ["@nodable/entities@2.1.0", "", {}, "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA=="], + "@nodable/entities": ["@nodable/entities@2.1.1", "", {}, "sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg=="], "@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="], @@ -567,7 +615,7 @@ "@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@17.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw=="], - "@octokit/request": ["@octokit/request@10.0.9", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "content-type": "^2.0.0", "fast-content-type-parse": "^3.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-o8Bi3f608eyM+7BmBiUWxFsdjLb3/ym1cQek5LZOv9KkZcxRrHCPhhRzm6xjO6HVZ85ItD6+sTsjxo821SVa/A=="], + "@octokit/request": ["@octokit/request@10.0.10", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "content-type": "^2.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-KxNC2pTqqhszMNrf12ZRd4PonRgyJdsM4F/jySiddQK+DsRcfBtUvqn8t7UsyZhnRJHvX46OohDt5N3VqIWC2w=="], "@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], @@ -585,6 +633,8 @@ "@oh-my-pi/pi-coding-agent": ["@oh-my-pi/pi-coding-agent@workspace:packages/coding-agent"], + "@oh-my-pi/pi-mnemopi": ["@oh-my-pi/pi-mnemopi@workspace:packages/mnemopi"], + "@oh-my-pi/pi-natives": ["@oh-my-pi/pi-natives@workspace:packages/natives"], "@oh-my-pi/pi-tui": ["@oh-my-pi/pi-tui@workspace:packages/tui"], @@ -607,57 +657,61 @@ "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="], - "@puppeteer/browsers": ["@puppeteer/browsers@2.13.2", "", { "dependencies": { "debug": "^4.4.3", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.7.4", "tar-fs": "^3.1.1", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw=="], + "@oxc-project/types": ["@oxc-project/types@0.132.0", "", {}, "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.4", "", { "os": "android", "cpu": "arm" }, "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ=="], + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.4", "", { "os": "android", "cpu": "arm64" }, "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw=="], + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA=="], + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg=="], + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.1", "", {}, "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g=="], + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw=="], + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.4", "", { "os": "linux", "cpu": "arm" }, "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA=="], + "@protobufjs/inquire": ["@protobufjs/inquire@1.1.2", "", {}, "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.4", "", { "os": "linux", "cpu": "arm" }, "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w=="], + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg=="], + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A=="], + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.1", "", {}, "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ=="], + "@puppeteer/browsers": ["@puppeteer/browsers@3.0.4", "", { "dependencies": { "modern-tar": "^0.7.6", "yargs": "^17.7.2" }, "peerDependencies": { "proxy-agent": ">=8.0.1" }, "optionalPeers": ["proxy-agent"], "bin": { "browsers": "lib/main-cli.js" } }, "sha512-HGM8iAmGTf+Y7t0373szVbTmt3d7vPkYL/1bpOkOFO0YUYLgSeuYBCzESklogNPvOBnZ/MRD5f07OkpqH1trtA=="], - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.2", "", { "os": "android", "cpu": "arm64" }, "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg=="], + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w=="], - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A=="], + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA=="], + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw=="], + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ=="], + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.4", "", { "os": "linux", "cpu": "x64" }, "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ=="], + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg=="], + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA=="], - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA=="], + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.4", "", { "os": "none", "cpu": "arm64" }, "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg=="], + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw=="], + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA=="], + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w=="], - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.4", "", { "os": "win32", "cpu": "x64" }, "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw=="], + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.2", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.4", "", { "os": "win32", "cpu": "x64" }, "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw=="], + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], "@so-ric/colorspace": ["@so-ric/colorspace@1.1.6", "", { "dependencies": { "color": "^5.0.2", "text-hex": "1.0.x" } }, "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw=="], @@ -695,8 +749,6 @@ "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], - "@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="], - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -709,8 +761,6 @@ "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="], "@types/react": ["@types/react@19.2.15", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q=="], @@ -721,29 +771,27 @@ "@types/turndown": ["@types/turndown@5.0.6", "", {}, "sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg=="], - "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], + "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260527.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260527.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260527.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260527.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260527.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260527.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260527.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260527.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-j81qKiwCPgMEjtk8uDLP+TDW60l6mugoJ7SNzfHWv1PJ6bUjIAHuag4P1jSLm1IpKuMuB3TTi4f61n7TJi8Jog=="], - "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260505.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260505.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260505.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260505.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260505.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260505.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260505.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260505.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-o82qX7L97dwQMpj6DzzokF6SQlChcxduNaL4OWzJhJkz1EP//gZOa0/xNPbPLufoJojHLQcANnpkA4JDXZDFhQ=="], + "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260527.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bDi6FJ644n3uKdp/ZI7j50ChVyGOsrJrkwihQb6x3yByFQkTINLu3e6ZkY+HveQ2Zw2vy9SGN8E7b3A5iSOO0A=="], - "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260505.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5W94O493huwcjrAkuP9yTQVPosXjX/0fEjCZsDn2D59m7VuPLy78R9D2i3UwlnajC75ubFiLcp/sh5o6/dFZVg=="], + "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260527.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-r6GXrTdalXZu1/b5goMpAe+efZvOfwdE45gl8Tti3fckP9icK3xdiN+VnNi0RL2/c2L86RyN8nGxihaCHGCKbw=="], - "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260505.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-j+N/276dONuTv2mOLgZy/jLsEZ2JLrxbZ8wBS/LIsMGtvp6elaN/ZESEntpUpIUbeoc5H6nHkjicJKNxQTZ90Q=="], + "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260527.1", "", { "os": "linux", "cpu": "arm" }, "sha512-BlfQBatMkZHi3o+atxoUW0czGJNjo9cpO1BoQeB3gxZ7D/cDZHYHmKFSSRx8UxMktwP5k5lPxi0wgA3Ic2mQyQ=="], - "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260505.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Vo7nGP0Wbs+VafCMabS4pSDcfJj60fLAmuZ2+hfdsUMFMO0BzHIUFyKBhbaeKVgO5V0yAqvBKrWkovZy0YXxGA=="], + "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260527.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-QJAFyPJgJqJVLbVPHl5xL7FCn3HNPLdpEm8l7KBgiYpltLhU1p/LJ3iN0XpFRAhq9ojWbZebo8t/h8MX35QjTQ=="], - "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260505.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-pP/LpkknUTeyQkIiC916BpW2R4ToXDZI7zTbkG6Llh5bGTPcTbtM/5SxXSzYH04ogrc5AP6yYRZsUxtv1GGeQA=="], + "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260527.1", "", { "os": "linux", "cpu": "x64" }, "sha512-UFB7ZdK2/vIIi62nfn3JhyGV7qR/qXjKPQaPVXwzCvaPieTZcsNsALjKU0W5WHThyi+5p3U7O3dGE7n6P4q4Yw=="], - "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260505.1", "", { "os": "linux", "cpu": "x64" }, "sha512-90Bpi2xCPCE3S/pcL5uXn793AKSf8qLVvQ+w87FpwKknHYXQqOQ38KBO9jX2lynoxr8YcVO1S8BS7PngkwicYg=="], + "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260527.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-rp/q9+9H77JQvepC/UpDP8CdeTGSGyhp9BVbmFwqUV2NhMHPldfys3ihY7OQdoVBgWIKQyxEHB+FTr8Z7kre1Q=="], - "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260505.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-VkNazv418LbiI0X6SQPCqVFTiBBvCrIxGkdVD7WBO/M3WHZam4qhK8fF61uQclK2NqYPClI2hPbuR5i8+4s4cg=="], - - "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260505.1", "", { "os": "win32", "cpu": "x64" }, "sha512-QhueS4Y0hxYnkQoXrAmB0JKpnXn18nNJwqxLSpyEHCEr+XnggiHBNfjT+p1LeG42TEn0w+skcfwc/Mkmk/gyCg=="], + "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260527.1", "", { "os": "win32", "cpu": "x64" }, "sha512-864Pq4qoDcacUJhs2/kQplyfwNO0APUmx1k8qUaJt2P9ZGF0Pu++afJi7OagImHMiEQcmigjmZPuOodOk5YmqQ=="], "@xmldom/xmldom": ["@xmldom/xmldom@0.8.13", "", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="], "@xterm/headless": ["@xterm/headless@6.0.0", "", {}, "sha512-5Yj1QINYCyzrZtf8OFIHi47iQtI+0qYFPHmouEfG8dHNxbZ9Tb9YGSuLcsEwj9Z+OL75GJqPyJbyoFer80a2Hw=="], - "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "adm-zip": ["adm-zip@0.5.17", "", {}, "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ=="], "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], @@ -753,33 +801,15 @@ "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], - "ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="], - "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], - "b4a": ["b4a@1.8.1", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw=="], - "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.7", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-/O6JWUmjv03OI9lL2ry9bUjpD5S3PclM55RRJEyCdcFZ5W2SEA/59d+l2hNsk3gI6kiWRdRPdOtqZmsQzFN1pQ=="], "babel-preset-solid": ["babel-preset-solid@1.9.12", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.6" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.12" }, "optionalPeers": ["solid-js"] }, "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg=="], - "bare-events": ["bare-events@2.8.3", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-HdUm8EMQBLaJvGUdidNNbqpA1kYkwNcb+MYxkxCLAPJGQzlv9J0C24h8V65Z4c5GLd/JEALDvpFCQgpLJqc0zw=="], - - "bare-fs": ["bare-fs@4.7.1", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw=="], - - "bare-os": ["bare-os@3.9.1", "", {}, "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ=="], - - "bare-path": ["bare-path@3.0.0", "", { "dependencies": { "bare-os": "^3.0.1" } }, "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw=="], - - "bare-stream": ["bare-stream@2.13.1", "", { "dependencies": { "streamx": "^2.25.0", "teex": "^1.0.1" }, "peerDependencies": { "bare-abort-controller": "*", "bare-buffer": "*", "bare-events": "*" }, "optionalPeers": ["bare-abort-controller", "bare-buffer", "bare-events"] }, "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow=="], - - "bare-url": ["bare-url@2.4.3", "", { "dependencies": { "bare-path": "^3.0.0" } }, "sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ=="], - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.32", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg=="], - - "basic-ftp": ["basic-ftp@5.3.1", "", {}, "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.33", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw=="], "beautiful-mermaid": ["beautiful-mermaid@1.1.3", "", { "dependencies": { "elkjs": "^0.11.0", "entities": "^7.0.1" } }, "sha512-TItrtrAyHp1vwFfFVYauWGrquouk/6SS21Aq3RsxindSYZODcN4xYrPZD6BiZRU+o5mKJzDPz9MUSMvELdylyg=="], @@ -789,9 +819,9 @@ "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], - "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], + "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="], - "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], + "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], @@ -803,10 +833,14 @@ "chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="], - "chromium-bidi": ["chromium-bidi@14.0.0", "", { "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw=="], + "chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], + + "chromium-bidi": ["chromium-bidi@16.0.1", "", { "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-J63PGu/9PpeCwLIcKYyzWP6yaVL5pxuBc0shlYCYM8BaAkmlwiQboXO1iNbOgSDbVklEyYFfNEcHD8oOAWacUA=="], "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + "cli-progress": ["cli-progress@3.12.0", "", { "dependencies": { "string-width": "^4.2.3" } }, "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A=="], + "cli-truncate": ["cli-truncate@5.2.0", "", { "dependencies": { "slice-ansi": "^8.0.0", "string-width": "^8.2.0" } }, "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw=="], "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], @@ -841,17 +875,19 @@ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - "data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], - - "date-fns": ["date-fns@4.3.0", "", {}, "sha512-OYcL+3N/jyWbYdFGqoMAhytDgxP9pbYPUUiRCOgn4Fewaadk9l/Wam4Avciiyp2BgkpfQyBV9B+ehnVJych+eQ=="], + "date-fns": ["date-fns@4.4.0", "", {}, "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "degenerator": ["degenerator@5.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" } }, "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ=="], + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "devtools-protocol": ["devtools-protocol@0.0.1608973", "", {}, "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ=="], + "detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="], + + "devtools-protocol": ["devtools-protocol@0.0.1624250", "", {}, "sha512-YFAat/lOiIk0ARmBweG+ygrEcbZrq5B9urRyUoeQKp53MlidHXE2TmTbxKcaXoQj7u/aX+jebDO4BW55rs0WwA=="], "diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="], @@ -867,7 +903,7 @@ "duck": ["duck@0.1.12", "", { "dependencies": { "underscore": "^1.13.1" } }, "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg=="], - "electron-to-chromium": ["electron-to-chromium@1.5.361", "", {}, "sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.364", "", {}, "sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw=="], "elkjs": ["elkjs@0.11.1", "", {}, "sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg=="], @@ -877,40 +913,28 @@ "enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="], - "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], - - "enhanced-resolve": ["enhanced-resolve@5.22.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-xYcDWrpELkFzz9SpZ3PlI6Eu6eD93Yf0WLDRxikGhWJ3MAir2SNZTIVCVZqZ/NUyx8AdMc2gT9C0gPiw18kG+A=="], + "enhanced-resolve": ["enhanced-resolve@5.22.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww=="], "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + "es-toolkit": ["es-toolkit@1.47.0", "", {}, "sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw=="], - "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + "es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="], - - "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], - - "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - - "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], - "events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="], - "exifr": ["exifr@7.1.3", "", {}, "sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw=="], - "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="], - - "fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="], - - "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="], - "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], @@ -921,44 +945,54 @@ "fast-xml-parser": ["fast-xml-parser@5.8.0", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.2.0", "path-expression-matcher": "^1.5.0", "strnum": "^2.3.0", "xml-naming": "^0.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg=="], - "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], + "fastembed": ["fastembed@2.1.0", "", { "dependencies": { "@anush008/tokenizers": "^0.0.0", "@huggingface/hub": "^2.7.1", "onnxruntime-node": "1.21.0", "progress": "^2.0.3", "tar": "^6.2.0" } }, "sha512-oQkpcRHBppJ3+a3w9dU0uytSY0N1cnEa/iVMc8AXEd+tvT529GekOEFhNviJy89R3lvQXF6cdIMTXHj1Gi00xQ=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], - "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], + "fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="], "file-stream-rotator": ["file-stream-rotator@0.6.1", "", { "dependencies": { "moment": "^2.29.1" } }, "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ=="], "file-type": ["file-type@21.3.4", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g=="], + "flatbuffers": ["flatbuffers@25.9.23", "", {}, "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ=="], + "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], + "fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "gearhash-jit": ["gearhash-jit@1.0.2", "", {}, "sha512-UhzJL4KXSdqAKepy/tZwmi2Rcy0YMmtiC4DQS4SURCuIWdh8ECZtnXK2ePRMLigfB61hRKdLK/Vgg2bSw73izQ=="], + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], - "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], + "global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="], - "get-uri": ["get-uri@6.0.5", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg=="], + "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "guid-typescript": ["guid-typescript@1.0.9", "", {}, "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ=="], + "handlebars": ["handlebars@4.7.9", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ=="], + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="], "htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="], - "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], - - "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -967,8 +1001,6 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="], - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], @@ -985,7 +1017,7 @@ "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], - "json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="], + "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], "json-with-bigint": ["json-with-bigint@3.5.8", "", {}, "sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw=="], @@ -1023,19 +1055,21 @@ "linkedom": ["linkedom@0.18.12", "", { "dependencies": { "css-select": "^5.1.0", "cssom": "^0.5.0", "html-escaper": "^3.0.3", "htmlparser2": "^10.0.0", "uhyphen": "^0.2.0" }, "peerDependencies": { "canvas": ">= 2" }, "optionalPeers": ["canvas"] }, "sha512-jalJsOwIKuQJSeTvsgzPe9iJzyfVaEJiEXl+25EkKevsULHvMJzpNqwvj1jOESWdmgKDiXObyjOYwlUqG7wo1Q=="], - "lint-staged": ["lint-staged@16.4.0", "", { "dependencies": { "commander": "^14.0.3", "listr2": "^9.0.5", "picomatch": "^4.0.3", "string-argv": "^0.3.2", "tinyexec": "^1.0.4", "yaml": "^2.8.2" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw=="], + "lint-staged": ["lint-staged@17.0.7", "", { "dependencies": { "listr2": "^10.2.1", "picomatch": "^4.0.4", "string-argv": "^0.3.2", "tinyexec": "^1.2.4" }, "optionalDependencies": { "yaml": "^2.9.0" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-JrSobt+tW3rH8IOMi8tDZd3foorM5yPEkLD/V2NxobgHrFfHWGee4MOLVuZeScgxftEwbHrPHIFA/ZL+nUJeuA=="], - "listr2": ["listr2@9.0.5", "", { "dependencies": { "cli-truncate": "^5.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g=="], + "listr2": ["listr2@10.2.1", "", { "dependencies": { "cli-truncate": "^5.2.0", "eventemitter3": "^5.0.4", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^10.0.0" } }, "sha512-7I5knELsJKTUjXG+A6BkKAiGkW1i25fNa/xlUl9hFtk15WbE9jndA89xu5FzQKrY5llajE1hfZZFMILXkDHk/Q=="], "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], "logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="], + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + "lop": ["lop@0.4.2", "", { "dependencies": { "duck": "^0.1.12", "option": "~0.2.1", "underscore": "^1.13.1" } }, "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw=="], - "lru-cache": ["lru-cache@11.3.6", "", {}, "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A=="], + "lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="], - "lucide-react": ["lucide-react@1.16.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ=="], + "lucide-react": ["lucide-react@1.17.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], @@ -1045,6 +1079,8 @@ "markit-ai": ["markit-ai@0.5.3", "", { "dependencies": { "chalk": "^5.6.2", "commander": "^14.0.3", "exifr": "^7.1.3", "fast-xml-parser": "^5.5.9", "jszip": "^3.10.1", "mammoth": "^1.9.0", "mupdf": "^1.27.0", "music-metadata": "^11.12.3", "rss-parser": "^3.13.0", "turndown": "^7.2.0", "turndown-plugin-gfm": "^1.0.2" }, "bin": { "markit": "dist/main.js" } }, "sha512-h4nhn6a/SNXEdc3kLVtL37TspxjUNCNL0OM7LRWxd389ZByI/B7bjNNgxFdVAT0O+H7ZekSwLdVe/lws1l2AZQ=="], + "matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="], + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], "merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="], @@ -1053,8 +1089,16 @@ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], + + "minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], + "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], + "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + + "modern-tar": ["modern-tar@0.7.6", "", {}, "sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg=="], + "moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -1069,30 +1113,30 @@ "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], - "netmask": ["netmask@2.1.1", "", {}, "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA=="], - "node-releases": ["node-releases@2.0.46", "", {}, "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ=="], "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], - "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], - "openai": ["openai@6.39.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-O61LIsimY3acVabwvomwFhwrnN36yvHY2quIfy9keEcFytGgWeV35yLHQ6NVMLSBxRpHmcg2yuhCnlu2HT4pLQ=="], + "onnxruntime-common": ["onnxruntime-common@1.24.3", "", {}, "sha512-GeuPZO6U/LBJXvwdaqHbuUmoXiEdeCjWi/EG7Y1HNnDwJYuk6WUbNXpF6luSUY8yASul3cmUlLGrCCL1ZgVXqA=="], + + "onnxruntime-node": ["onnxruntime-node@1.24.3", "", { "dependencies": { "adm-zip": "^0.5.16", "global-agent": "^3.0.0", "onnxruntime-common": "1.24.3" }, "os": [ "linux", "win32", "darwin", ] }, "sha512-JH7+czbc8ALA819vlTgcV+Q214/+VjGeBHDjX81+ZCD0PCVCIFGFNtT0V4sXG/1JXypKPgScQcB3ij/hk3YnTg=="], + + "onnxruntime-web": ["onnxruntime-web@1.26.0-dev.20260416-b7804b056c", "", { "dependencies": { "flatbuffers": "^25.1.24", "guid-typescript": "^1.0.9", "long": "^5.2.3", "onnxruntime-common": "1.24.0-dev.20251116-b39e144322", "platform": "^1.3.6", "protobufjs": "^7.2.4" } }, "sha512-MD6Ss4GSpQBo6zqoJzyT9LRbKYs7x/JVN23FT24EcEvlqF4VuzPOeH6X38orZPKHQDbprn7K+SBpu0/mj2CQiw=="], + + "openai": ["openai@6.39.1", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-z3dO9fEWOXBzlXynVb/xZ/tujzUjFWQWn3C0n0mw6Vo0zJTbEkaN4b2cLWjhJ6haJQx8LlREoafHRl+Gu/Hl+A=="], "option": ["option@0.2.4", "", {}, "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A=="], - "pac-proxy-agent": ["pac-proxy-agent@7.2.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA=="], - - "pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="], - "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], @@ -1103,12 +1147,12 @@ "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], - "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "platform": ["platform@1.3.6", "", {}, "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="], + "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], "prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="], @@ -1117,19 +1161,15 @@ "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], - "proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="], + "protobufjs": ["protobufjs@7.6.2", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-N9EiLovGEQOJSPF26Ij7qUGvahfEnq0eeYZ02aigIedkmz1qZSwjnP9SBITHJuF/6MYbIW4HDN8zdYjsjqJKXQ=="], - "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + "puppeteer-core": ["puppeteer-core@25.1.0", "", { "dependencies": { "@puppeteer/browsers": "3.0.4", "chromium-bidi": "16.0.1", "devtools-protocol": "0.0.1624250", "typed-query-selector": "^2.12.2", "webdriver-bidi-protocol": "0.4.2", "ws": "^8.21.0" } }, "sha512-jKzy5y4WG6uNuFbTWgW1D7mqoT9o0nllc/6a1DGF775T1mPmgw3scdFEtEq67yVFikavQmbYq6NLfbTfxHSlqQ=="], - "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], - - "puppeteer-core": ["puppeteer-core@24.43.1", "", { "dependencies": { "@puppeteer/browsers": "2.13.2", "chromium-bidi": "14.0.0", "debug": "^4.4.3", "devtools-protocol": "0.0.1608973", "typed-query-selector": "^2.12.2", "webdriver-bidi-protocol": "0.4.1", "ws": "^8.20.0" } }, "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw=="], - - "react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="], + "react": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="], "react-chartjs-2": ["react-chartjs-2@5.3.1", "", { "peerDependencies": { "chart.js": "^4.1.1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A=="], - "react-dom": ["react-dom@19.2.5", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.5" } }, "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag=="], + "react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="], "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], @@ -1141,9 +1181,11 @@ "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="], + "robomp-web": ["robomp-web@workspace:python/robomp/web"], - "rollup": ["rollup@4.60.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.4", "@rollup/rollup-android-arm64": "4.60.4", "@rollup/rollup-darwin-arm64": "4.60.4", "@rollup/rollup-darwin-x64": "4.60.4", "@rollup/rollup-freebsd-arm64": "4.60.4", "@rollup/rollup-freebsd-x64": "4.60.4", "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", "@rollup/rollup-linux-arm-musleabihf": "4.60.4", "@rollup/rollup-linux-arm64-gnu": "4.60.4", "@rollup/rollup-linux-arm64-musl": "4.60.4", "@rollup/rollup-linux-loong64-gnu": "4.60.4", "@rollup/rollup-linux-loong64-musl": "4.60.4", "@rollup/rollup-linux-ppc64-gnu": "4.60.4", "@rollup/rollup-linux-ppc64-musl": "4.60.4", "@rollup/rollup-linux-riscv64-gnu": "4.60.4", "@rollup/rollup-linux-riscv64-musl": "4.60.4", "@rollup/rollup-linux-s390x-gnu": "4.60.4", "@rollup/rollup-linux-x64-gnu": "4.60.4", "@rollup/rollup-linux-x64-musl": "4.60.4", "@rollup/rollup-openbsd-x64": "4.60.4", "@rollup/rollup-openharmony-arm64": "4.60.4", "@rollup/rollup-win32-arm64-msvc": "4.60.4", "@rollup/rollup-win32-ia32-msvc": "4.60.4", "@rollup/rollup-win32-x64-gnu": "4.60.4", "@rollup/rollup-win32-x64-msvc": "4.60.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g=="], + "rolldown": ["rolldown@1.0.2", "", { "dependencies": { "@oxc-project/types": "=0.132.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.2", "@rolldown/binding-darwin-arm64": "1.0.2", "@rolldown/binding-darwin-x64": "1.0.2", "@rolldown/binding-freebsd-x64": "1.0.2", "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", "@rolldown/binding-linux-arm64-gnu": "1.0.2", "@rolldown/binding-linux-arm64-musl": "1.0.2", "@rolldown/binding-linux-ppc64-gnu": "1.0.2", "@rolldown/binding-linux-s390x-gnu": "1.0.2", "@rolldown/binding-linux-x64-gnu": "1.0.2", "@rolldown/binding-linux-x64-musl": "1.0.2", "@rolldown/binding-openharmony-arm64": "1.0.2", "@rolldown/binding-wasm32-wasi": "1.0.2", "@rolldown/binding-win32-arm64-msvc": "1.0.2", "@rolldown/binding-win32-x64-msvc": "1.0.2" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g=="], "rss-parser": ["rss-parser@3.13.0", "", { "dependencies": { "entities": "^2.0.3", "xml2js": "^0.5.0" } }, "sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w=="], @@ -1159,22 +1201,22 @@ "semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], + "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], + + "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], + "seroval": ["seroval@1.5.4", "", {}, "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw=="], "seroval-plugins": ["seroval-plugins@1.5.4", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw=="], "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], + "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], - "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], - - "socks": ["socks@2.8.9", "", { "dependencies": { "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" } }, "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw=="], - - "socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="], - "solid-js": ["solid-js@1.9.13", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-6hJeJMOcEX8ktqjpDoJZEmld3ijvcvWBDtiXBm7f4332SiFN66QeAQI1REQshvyUoISsSeJ4PHDauKYbwao9JQ=="], "solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="], @@ -1187,8 +1229,6 @@ "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], - "streamx": ["streamx@2.25.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg=="], - "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -1205,24 +1245,18 @@ "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], - "tar-fs": ["tar-fs@3.1.2", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw=="], - - "tar-stream": ["tar-stream@3.2.0", "", { "dependencies": { "b4a": "^1.6.4", "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg=="], - - "teex": ["teex@1.0.1", "", { "dependencies": { "streamx": "^2.12.5" } }, "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg=="], - - "text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="], + "tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], - "tinyexec": ["tinyexec@1.2.2", "", {}, "sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g=="], + "tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], - "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "turndown": ["turndown@7.2.4", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ=="], @@ -1231,6 +1265,8 @@ "typanion": ["typanion@3.14.0", "", {}, "sha512-ZW/lVMRabETuYCd9O9ZvMhAh8GslSqaUjxmK/JLPCh6l73CvLBiuXswj/+7LdnWOgYsQ130FqLzFz5aGT4I3Ug=="], + "type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], + "typed-query-selector": ["typed-query-selector@2.12.2", "", {}, "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ=="], "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], @@ -1251,13 +1287,13 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], + "vite": ["vite@8.0.14", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.2", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw=="], "vite-plugin-solid": ["vite-plugin-solid@2.11.12", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA=="], "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], - "webdriver-bidi-protocol": ["webdriver-bidi-protocol@0.4.1", "", {}, "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw=="], + "webdriver-bidi-protocol": ["webdriver-bidi-protocol@0.4.2", "", {}, "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA=="], "win-guid": ["win-guid@0.2.1", "", {}, "sha512-gEIQU4mkgl2OPeoNrWflcJFJ3Ae2BPd4eCsHHA/XikslkIVms/nHhvnvzIZV7VLmBvtFlDOzLt9rrZT+n6D67A=="], @@ -1269,9 +1305,7 @@ "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], - "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], - - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "wrap-ansi": ["wrap-ansi@10.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0", "strip-ansi": "^7.1.2" } }, "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ=="], "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], @@ -1283,7 +1317,7 @@ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], @@ -1291,8 +1325,6 @@ "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], - "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -1301,6 +1333,8 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@isaacs/fs-minipass/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + "@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], @@ -1327,17 +1361,25 @@ "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "fastembed/onnxruntime-node": ["onnxruntime-node@1.21.0", "", { "dependencies": { "global-agent": "^3.0.0", "onnxruntime-common": "1.21.0", "tar": "^7.0.1" }, "os": [ "linux", "win32", "darwin", ] }, "sha512-NeaCX6WW2L8cRCSqy3bInlo5ojjQqu2fD3D+9W5qb5irwxhEyWKXeH2vZ8W9r6VxaMPUan+4/7NDwZMtouZxEw=="], + + "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + "js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "jszip/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "log-update/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], + "log-update/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + + "minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "onnxruntime-web/onnxruntime-common": ["onnxruntime-common@1.24.0-dev.20251116-b39e144322", "", {}, "sha512-BOoomdHYmNRL5r4iQ4bMvsl2t0/hzVQ3OM3PHD0gxeXu1PmggqBv3puZicEUVOA3AtHHYmqZtjMj9FOfGrATTw=="], + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - "proxy-agent/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], - - "robomp-web/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "roarr/sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], "rss-parser/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="], @@ -1347,24 +1389,40 @@ "string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "wrap-ansi/string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], "xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], + "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "fastembed/onnxruntime-node/onnxruntime-common": ["onnxruntime-common@1.21.0", "", {}, "sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ=="], + + "fastembed/onnxruntime-node/tar": ["tar@7.5.15", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ=="], + "jszip/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], "log-update/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + "log-update/wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], - "cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + "fastembed/onnxruntime-node/tar/chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + + "fastembed/onnxruntime-node/tar/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "fastembed/onnxruntime-node/tar/minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + + "fastembed/onnxruntime-node/tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + + "log-update/wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + "cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], } } diff --git a/crates/brush-core-vendored/src/commands.rs b/crates/brush-core-vendored/src/commands.rs index ac97d0011..0cdf8d406 100644 --- a/crates/brush-core-vendored/src/commands.rs +++ b/crates/brush-core-vendored/src/commands.rs @@ -617,37 +617,46 @@ pub(crate) fn execute_external_command( // Set up process group/session state. + // + // A child we are about to `setsid()` (`DetachSession`) must NOT also be + // handed a `process_group(...)`. For a would-be new-group leader it would + // duplicate the group `setsid` already creates; for a pipeline stage joining + // an established group it is a cross-session `setpgid` that fails with EPERM + // now that the leader (and every prior stage) has moved into its own session. + // In both cases `setsid` alone gives the child its own session and process + // group. See `child_session_action` for the decision rationale. let command_leads_session = new_pg && matches!(session_action, ChildSessionAction::TakeForeground) && context.shell.options().external_cmd_leads_session; - if new_pg { - match session_action { - ChildSessionAction::DetachSession => { - // `detach_session()` calls `setsid()`, which creates a fresh session - // and process group; requesting `process_group(0)` as well would - // conflict with that setup. - } - ChildSessionAction::TakeForeground if command_leads_session => { - // Don't set process_group(0) - setsid() in pre_exec will handle it. - cmd.lead_session(); - } - ChildSessionAction::TakeForeground | ChildSessionAction::None => { - // Normal case: create new process group in current session. + match session_action { + ChildSessionAction::DetachSession => { + // setsid() creates the fresh session + process group; no process_group(). + cmd.detach_session(); + } + ChildSessionAction::TakeForeground if command_leads_session => { + // Don't set process_group(0) - setsid() in pre_exec will handle it. + cmd.lead_session(); + } + ChildSessionAction::TakeForeground => { + // Foreground a child that is not leading its own session: create/join + // the process group in the current session, then grab the terminal. + if new_pg { cmd.process_group(0); + } else if let Some(pgid) = process_group_id { + cmd.process_group(pgid); + } + cmd.take_foreground(); + } + ChildSessionAction::None => { + // Normal case: create a new process group in the current session, or + // join an established one (later pipeline stages). + if new_pg { + cmd.process_group(0); + } else if let Some(pgid) = process_group_id { + cmd.process_group(pgid); } } - } else if let Some(pgid) = process_group_id { - // We need to join an established process group. - cmd.process_group(pgid); - } - - // See `child_session_action` for the decision rationale and call-out about - // pipeline groups. - match session_action { - ChildSessionAction::DetachSession => cmd.detach_session(), - ChildSessionAction::TakeForeground if !command_leads_session => cmd.take_foreground(), - ChildSessionAction::TakeForeground | ChildSessionAction::None => {} } // When tracing is enabled, report. @@ -964,18 +973,27 @@ pub enum ChildSessionAction { /// child inherited the host's controlling tty, and any `/dev/tty` open or /// `tcsetpgrp` call from the child could SIGTTIN/SIGTTOU and stop the host. /// -/// `detach_session()` is unsafe for any member of a multi-command pipeline: -/// for the first stage it puts the process-group leader in a different session, -/// causing later stages' `setpgid()` to fail with EPERM; for later stages it -/// either fails with EPERM or moves the child into a fresh session, breaking the -/// pipeline's shared process group and job-control signal propagation. Pipeline -/// stages therefore keep their pre-fix behavior (no detach). +/// A child whose stdin is **not** a terminal therefore always detaches, even +/// when it is a stage of a multi-command pipeline. An interactive program in a +/// pipeline (`zsh -i ... | awk`) would otherwise open `/dev/tty`, `tcsetpgrp` +/// itself to the foreground, and leave the host stopped on its next tty read. +/// `setsid()` puts each stage in its own session with no controlling tty, so it +/// cannot reach `/dev/tty` at all. The historical EPERM hazard — a later stage +/// `setpgid()`-joining a leader that already moved to a new session — is avoided +/// in `execute_external_command`, which skips `process_group(...)` entirely for +/// detached children; pipeline stages no longer share one process group, which +/// the embedded host does not rely on (it cancels via the descendant tree, and +/// pipes are session-independent). +/// +/// `in_pipeline_group` is no longer consulted: a pipeline stage that legitimately +/// needs the shared tty group has terminal stdin and is handled by the +/// `child_stdin_is_terminal` arm before pipeline membership would ever matter. /// /// Foregrounding remains gated on `new_pg && child_stdin_is_terminal`. pub fn child_session_action( new_pg: bool, child_stdin_is_terminal: bool, - in_pipeline_group: bool, + _in_pipeline_group: bool, ) -> ChildSessionAction { if new_pg && child_stdin_is_terminal { return ChildSessionAction::TakeForeground; @@ -985,9 +1003,5 @@ pub fn child_session_action( return ChildSessionAction::None; } - if in_pipeline_group { - return ChildSessionAction::None; - } - ChildSessionAction::DetachSession } diff --git a/crates/brush-core-vendored/src/jobs.rs b/crates/brush-core-vendored/src/jobs.rs index 02f4f08b9..0a575b23c 100644 --- a/crates/brush-core-vendored/src/jobs.rs +++ b/crates/brush-core-vendored/src/jobs.rs @@ -437,6 +437,26 @@ impl Job { } } + /// Aborts shell-internal background tasks and drops their join handles. + /// + /// External process jobs are intentionally left alone; callers that abort + /// internal tasks are still responsible for signalling any process trees + /// those tasks may have spawned. + pub fn abort_internal_tasks(&mut self) { + let mut aborted = false; + self.tasks.retain_mut(|task| { + if let JobTask::Internal(handle) = task { + handle.abort(); + aborted = true; + return false; + } + true + }); + if aborted && self.tasks.is_empty() { + self.state = JobState::Done; + } + } + /// Tries to retrieve a "representative" pid for the job. pub fn representative_pid(&self) -> Option { for task in &self.tasks { diff --git a/crates/pi-ast/src/block.rs b/crates/pi-ast/src/block.rs new file mode 100644 index 000000000..567e9f95f --- /dev/null +++ b/crates/pi-ast/src/block.rs @@ -0,0 +1,223 @@ +//! Resolve the syntactic block that begins on a given source line. +//! +//! Powers the hashline `replace block N:` operator: given a 1-indexed line, +//! parse the source with tree-sitter and return the line span of the outermost +//! named node that *begins* on that line (excluding the whole-file root). Brace +//! languages anchor a construct's block to its opening line, so pointing at the +//! line that opens an `if` / `function` / `struct` resolves to that construct's +//! full span; pointing at a continuation line or a lone closing delimiter +//! resolves to nothing. + +use anyhow::{Result, anyhow}; +use ast_grep_core::tree_sitter::LanguageExt; +use serde::{Deserialize, Serialize}; +use tree_sitter::{Parser, Point}; + +use crate::summary::{node_content_end_line, node_start_line, resolve_language}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BlockRangeOptions { + /// Source code to inspect. + pub code: String, + /// Language alias (e.g. "rust", "typescript") used before path inference. + pub lang: Option, + /// File path used to infer language by extension when `lang` is omitted. + pub path: Option, + /// 1-indexed source line the block must begin on. + pub line: u32, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +pub struct BlockRange { + /// 1-indexed inclusive first line of the resolved block. + pub start_line: u32, + /// 1-indexed inclusive last line of the resolved block. + pub end_line: u32, +} + +/// Count of leading space/tab bytes on `row` (0-indexed), i.e. the byte column +/// of the first content character. Returns `None` when `row` is out of range +/// or the line is blank / whitespace-only — there is no block to resolve there. +fn first_content_column(code: &str, row: usize) -> Option { + let line = code.split('\n').nth(row)?; + for (col, byte) in line.bytes().enumerate() { + if byte != b' ' && byte != b'\t' { + return Some(col); + } + } + None +} + +/// Resolve the block beginning on `options.line`. +/// +/// Returns `None` (a soft "no block here", surfaced as a hard error one layer +/// up) when the language is unrecognized, the line is out of range / blank, no +/// node begins on that line, or the resolved subtree contains a syntax error. +pub fn block_range_at(options: BlockRangeOptions) -> Result> { + let BlockRangeOptions { code, lang, path, line } = options; + if line == 0 || code.is_empty() { + return Ok(None); + } + let Some(language) = resolve_language(lang.as_deref(), path.as_deref()) else { + return Ok(None); + }; + let row = (line - 1) as usize; + let Some(col) = first_content_column(&code, row) else { + return Ok(None); + }; + + let mut parser = Parser::new(); + parser + .set_language(&language.get_ts_language()) + .map_err(|err| anyhow!("Failed to load tree-sitter language: {err}"))?; + let Some(tree) = parser.parse(&code, None) else { + return Ok(None); + }; + let root = tree.root_node(); + + let point = Point::new(row, col); + let Some(leaf) = root.named_descendant_for_point_range(point, point) else { + return Ok(None); + }; + // A leaf whose own start row is earlier than `row` means `point` landed on + // a continuation line or a closing delimiter of a block that opened earlier + // — there is no block *beginning* on line N. + if leaf.start_position().row != row { + return Ok(None); + } + // Climb to the outermost named ancestor that still begins on `row`, + // excluding the whole-file root. Ancestors can only begin on an earlier + // row, so the first parent that starts before `row` stops the climb. + let mut node = leaf; + while let Some(parent) = node.parent() { + if parent.id() == root.id() { + break; + } + if parent.start_position().row != row { + break; + } + node = parent; + } + // Refuse degenerate error-recovery spans: a missing brace can make + // tree-sitter wrap a huge region in an ERROR node. Checking only the + // resolved node's subtree (not the whole file) keeps an unrelated syntax + // error elsewhere from disabling the feature. + if node.has_error() { + return Ok(None); + } + Ok(Some(BlockRange { + start_line: node_start_line(node), + end_line: node_content_end_line(node), + })) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn resolve(code: &str, path: &str, line: u32) -> Option { + block_range_at(BlockRangeOptions { + code: code.to_string(), + lang: None, + path: Some(path.to_string()), + line, + }) + .expect("block resolution succeeds") + } + + const TS_EXAMPLE: &str = "function x() {\n if (y) {\n }\n}\n"; + + #[test] + fn resolves_inner_if_block() { + assert_eq!(resolve(TS_EXAMPLE, "x.ts", 2), Some(BlockRange { start_line: 2, end_line: 3 })); + } + + #[test] + fn resolves_enclosing_function_block() { + assert_eq!(resolve(TS_EXAMPLE, "x.ts", 1), Some(BlockRange { start_line: 1, end_line: 4 })); + } + + #[test] + fn lone_closing_brace_resolves_to_nothing() { + // Line 3 is ` }` — the closing delimiter of a block that opened on an + // earlier line, so no block *begins* there. + assert_eq!(resolve(TS_EXAMPLE, "x.ts", 3), None); + } + + #[test] + fn blank_line_resolves_to_nothing() { + let code = "function x() {\n\n return 1;\n}\n"; + assert_eq!(resolve(code, "x.ts", 2), None); + } + + #[test] + fn out_of_range_line_resolves_to_nothing() { + assert_eq!(resolve(TS_EXAMPLE, "x.ts", 99), None); + assert_eq!(resolve(TS_EXAMPLE, "x.ts", 0), None); + } + + #[test] + fn unrecognized_extension_resolves_to_nothing() { + assert_eq!(resolve(TS_EXAMPLE, "x.unknownext", 2), None); + } + + #[test] + fn resolves_top_level_python_def() { + let code = "x = 1\ndef greet():\n return 1\n"; + assert_eq!(resolve(code, "g.py", 2), Some(BlockRange { start_line: 2, end_line: 3 })); + } + + #[test] + fn resolves_inner_python_block() { + // Point at the `for` loop inside the function body. The suite's first + // statement is `total = 0` (line 2), so the `for` at line 3 is not the + // suite's first child and climbs only to the `for_statement`, not the + // whole function suite. + let code = + "def f(xs):\n total = 0\n for x in xs:\n total += x\n return total\n"; + assert_eq!(resolve(code, "f.py", 3), Some(BlockRange { start_line: 3, end_line: 4 })); + } + + #[test] + fn resolves_nested_block_to_outermost_on_line() { + // Point at the inner `if` line; it resolves the whole `if` block + // (header through its closing brace), not just the call inside it. + let code = "function f() {\n if (a) {\n g();\n }\n}\n"; + assert_eq!(resolve(code, "f.ts", 2), Some(BlockRange { start_line: 2, end_line: 4 })); + } + + #[test] + fn multi_statement_line_resolves_first_statement_node() { + // `let a = 1; let b = 2;` — pointing at the line resolves the first + // statement that begins at the line's first content column. + let code = "let a = 1; let b = 2;\n"; + let range = resolve(code, "m.ts", 1); + assert!(range.is_some(), "expected a block on a single-statement-bearing line"); + assert_eq!(range.unwrap().start_line, 1); + } + + #[test] + fn continuation_line_resolves_to_nothing() { + // A bare argument-continuation line whose first content does not open a + // new named node beginning on that row. + let code = "foo(\n a,\n b,\n);\n"; + // Line 2 (` a,`) is an argument — `a` is an identifier beginning on the + // row, so it DOES resolve. Use the closing `);` line instead, which is + // a continuation/closer of the call begun earlier. + assert_eq!(resolve(code, "c.ts", 4), None); + } + + #[test] + fn error_subtree_resolves_to_nothing() { + // Missing closing brace: the function's subtree carries an ERROR, so we + // refuse to resolve a degenerate recovery span. + let code = "function broken() {\n if (y) {\n}\n"; + assert_eq!(resolve(code, "b.ts", 1), None); + } + + #[test] + fn resolves_rust_struct_block() { + let code = "struct A;\nstruct B {\n x: u32,\n}\n"; + assert_eq!(resolve(code, "r.rs", 2), Some(BlockRange { start_line: 2, end_line: 4 })); + } +} diff --git a/crates/pi-ast/src/lib.rs b/crates/pi-ast/src/lib.rs index 51f21642f..971081275 100644 --- a/crates/pi-ast/src/lib.rs +++ b/crates/pi-ast/src/lib.rs @@ -1,3 +1,4 @@ +pub mod block; pub mod language; pub mod ops; pub mod summary; diff --git a/crates/pi-ast/src/summary.rs b/crates/pi-ast/src/summary.rs index a246d2b29..19cb7f061 100644 --- a/crates/pi-ast/src/summary.rs +++ b/crates/pi-ast/src/summary.rs @@ -208,7 +208,7 @@ pub fn summarize_code(options: SummaryOptions) -> Result { }) } -fn resolve_language(lang: Option<&str>, path: Option<&str>) -> Option { +pub(crate) fn resolve_language(lang: Option<&str>, path: Option<&str>) -> Option { if let Some(lang) = lang.map(str::trim).filter(|lang| !lang.is_empty()) { return SupportLang::from_alias(lang); } @@ -354,7 +354,7 @@ fn flush_groupable_run( } } -fn node_start_line(node: Node<'_>) -> u32 { +pub(crate) fn node_start_line(node: Node<'_>) -> u32 { node .start_position() .row @@ -376,7 +376,7 @@ fn node_end_line(node: Node<'_>) -> u32 { /// When that byte is a newline, the resulting position lands at column 0 of /// the next row, which makes the naive `row + 1` answer one greater than the /// row of the last visible content. This helper subtracts that off. -fn node_content_end_line(node: Node<'_>) -> u32 { +pub(crate) fn node_content_end_line(node: Node<'_>) -> u32 { let pos = node.end_position(); let row = if pos.column == 0 && pos.row > 0 { pos.row - 1 diff --git a/crates/pi-natives/src/block.rs b/crates/pi-natives/src/block.rs new file mode 100644 index 000000000..88d942693 --- /dev/null +++ b/crates/pi-natives/src/block.rs @@ -0,0 +1,47 @@ +//! Resolve the syntactic block beginning on a source line (tree-sitter). + +use napi::bindgen_prelude::*; +use napi_derive::napi; + +#[napi(object)] +pub struct BlockRangeOptions { + /// Source code to inspect. + pub code: String, + /// Language alias (e.g. "rust", "typescript") used before path inference. + pub lang: Option, + /// File path used to infer language by extension when `lang` is omitted. + pub path: Option, + /// 1-indexed source line the block must begin on. + pub line: u32, +} + +#[napi(object)] +pub struct BlockRange { + /// 1-indexed inclusive first line of the resolved block. + pub start_line: u32, + /// 1-indexed inclusive last line of the resolved block. + pub end_line: u32, +} + +impl From for BlockRange { + fn from(value: pi_ast::block::BlockRange) -> Self { + Self { start_line: value.start_line, end_line: value.end_line } + } +} + +/// Find the outermost named tree-sitter node that begins on `options.line`. +/// +/// Returns its 1-indexed inclusive line span, or `null` when the language is +/// unrecognized, the line is out of range / blank, no node begins on that line, +/// or the resolved subtree contains a syntax error. +#[napi] +pub fn block_range_at(options: BlockRangeOptions) -> Result> { + pi_ast::block::block_range_at(pi_ast::block::BlockRangeOptions { + code: options.code, + lang: options.lang, + path: options.path, + line: options.line, + }) + .map(|range| range.map(Into::into)) + .map_err(|error| Error::from_reason(error.to_string())) +} diff --git a/crates/pi-natives/src/keys.rs b/crates/pi-natives/src/keys.rs index 74cd4c036..ccdb9a935 100644 --- a/crates/pi-natives/src/keys.rs +++ b/crates/pi-natives/src/keys.rs @@ -200,8 +200,6 @@ static LEGACY_SEQUENCES: phf::Map<&'static [u8], &'static str> = phf_map! { b"\x1b[[A" => "f1", b"\x1b[[B" => "f2", b"\x1b[[C" => "f3", b"\x1b[[D" => "f4", b"\x1b[[E" => "f5", b"\x1b[15~" => "f5", b"\x1b[17~" => "f6", b"\x1b[18~" => "f7", b"\x1b[19~" => "f8", b"\x1b[20~" => "f9", b"\x1b[21~" => "f10", b"\x1b[23~" => "f11", b"\x1b[24~" => "f12", - // Alt+arrow (legacy) - b"\x1bb" => "alt+left", b"\x1bf" => "alt+right", b"\x1bp" => "alt+up", b"\x1bn" => "alt+down", }; /// Pre-allocated single ASCII printable characters (33-126) @@ -701,9 +699,9 @@ fn matches_key_inner(bytes: &[u8], key_id: &str, kitty_protocol_active: bool) -> } if key.eq_ignore_ascii_case("enter") || key.eq_ignore_ascii_case("return") { - // alt+enter is commonly ESC + CR even when kitty disambiguation is on (Enter is - // an exception). - if modifier == MOD_ALT && bytes == b"\x1b\r" { + // alt+enter is commonly ESC + CR/LF even when kitty disambiguation is on + // (Enter is an exception). + if modifier == MOD_ALT && (bytes == b"\x1b\r" || bytes == b"\x1b\n") { return true; } @@ -799,7 +797,7 @@ fn matches_key_inner(bytes: &[u8], key_id: &str, kitty_protocol_active: bool) -> if key.eq_ignore_ascii_case("up") { if modifier == MOD_ALT { - return bytes == b"\x1bp" || kitty_matches(ARROW_UP, MOD_ALT); + return kitty_matches(ARROW_UP, MOD_ALT); } if modifier == 0 { return matches_legacy_key(bytes, "up") || kitty_matches(ARROW_UP, 0); @@ -810,7 +808,7 @@ fn matches_key_inner(bytes: &[u8], key_id: &str, kitty_protocol_active: bool) -> if key.eq_ignore_ascii_case("down") { if modifier == MOD_ALT { - return bytes == b"\x1bn" || kitty_matches(ARROW_DOWN, MOD_ALT); + return kitty_matches(ARROW_DOWN, MOD_ALT); } if modifier == 0 { return matches_legacy_key(bytes, "down") || kitty_matches(ARROW_DOWN, 0); @@ -823,7 +821,6 @@ fn matches_key_inner(bytes: &[u8], key_id: &str, kitty_protocol_active: bool) -> if modifier == MOD_ALT { return bytes == b"\x1b[1;3D" || (!kitty_protocol_active && bytes == b"\x1bB") - || bytes == b"\x1bb" || kitty_matches(ARROW_LEFT, MOD_ALT); } if modifier == MOD_CTRL { @@ -842,7 +839,6 @@ fn matches_key_inner(bytes: &[u8], key_id: &str, kitty_protocol_active: bool) -> if modifier == MOD_ALT { return bytes == b"\x1b[1;3C" || (!kitty_protocol_active && bytes == b"\x1bF") - || bytes == b"\x1bf" || kitty_matches(ARROW_RIGHT, MOD_ALT); } if modifier == MOD_CTRL { @@ -883,14 +879,15 @@ fn matches_key_inner(bytes: &[u8], key_id: &str, kitty_protocol_active: bool) -> let codepoint = ch as i32; let is_letter = ch.is_ascii_lowercase(); - // ctrl+alt+letter in legacy mode - // Legacy: ctrl+alt+letter is ESC followed by the control character. - // If that legacy form does not match, continue so CSI-u and + // Legacy ctrl+alt+letter is ESC followed by the control character. + // tmux extkeys/CSI-u and Kitty mixed modes can still pass these legacy Meta + // pairs through, so accept them even when enhanced keyboard reporting is + // active. If that legacy form does not match, continue so CSI-u and // modifyOtherKeys sequences from tmux can still be recognized. // Legacy ESC+ctrl-char would also match Alt+Enter/Alt+Backspace/etc; // skip the legacy fast-path for those bytes and let kitty/modifyOtherKeys // disambiguate. - if modifier == (MOD_CTRL | MOD_ALT) && !kitty_protocol_active && is_letter { + if modifier == (MOD_CTRL | MOD_ALT) && is_letter { let ctrl_char = raw_ctrl_char(ch); if bytes.len() == 2 && bytes[0] == 0x1b @@ -901,14 +898,22 @@ fn matches_key_inner(bytes: &[u8], key_id: &str, kitty_protocol_active: bool) -> } } - // alt+letter in legacy mode - if modifier == MOD_ALT && !kitty_protocol_active && is_letter { - return bytes.len() == 2 && bytes[0] == 0x1b && bytes[1] == ch; + // alt+letter can remain ESC+letter inside tmux/Kitty mixed modes. If that + // legacy form does not match, fall through so CSI-u and modifyOtherKeys + // encodings still match. + if modifier == MOD_ALT && is_letter && bytes.len() == 2 && bytes[0] == 0x1b && bytes[1] == ch + { + return true; } - // alt+shift+letter in legacy mode (ESC + UPPERCASE letter) - if modifier == (MOD_ALT | MOD_SHIFT) && !kitty_protocol_active && is_letter { - return bytes.len() == 2 && bytes[0] == 0x1b && bytes[1] == ch.to_ascii_uppercase(); + // alt+shift+letter can remain ESC+UPPERCASE inside tmux/Kitty mixed modes. + if modifier == (MOD_ALT | MOD_SHIFT) + && is_letter + && bytes.len() == 2 + && bytes[0] == 0x1b + && bytes[1] == ch.to_ascii_uppercase() + { + return true; } // ctrl+key @@ -1031,6 +1036,15 @@ fn parse_key_inner(bytes: &[u8], kitty_protocol_active: bool) -> Option Option Some(Cow::Borrowed("shift+tab")), @@ -1108,26 +1116,29 @@ fn parse_esc_pair(code: u8, kitty_protocol_active: bool) -> Option return Some(Cow::Borrowed("alt+backspace")), - b'\r' => return Some(Cow::Borrowed("alt+enter")), + b'\r' | b'\n' => return Some(Cow::Borrowed("alt+enter")), b'\t' => return Some(Cow::Borrowed("alt+tab")), _ => {}, } - // Legacy ALT-prefix parsing only when kitty protocol isn't expected to - // disambiguate. + // Historical cursor-key aliases used by some legacy terminals. Keep them in + // legacy mode only; in mixed modes (tmux extkeys/CSI-u, Kitty, etc.) ESC+B/F + // are real Alt+Shift+B/F keypresses. if !kitty_protocol_active { match code { b' ' => return Some(Cow::Borrowed("alt+space")), b'B' => return Some(Cow::Borrowed("alt+left")), b'F' => return Some(Cow::Borrowed("alt+right")), - 1..=26 => return Some(Cow::Borrowed(CTRL_ALT_LETTERS[(code - 1) as usize])), - b'a'..=b'z' => return Some(Cow::Borrowed(ALT_LETTERS[(code - b'a') as usize])), - b'A'..=b'Z' => return Some(Cow::Borrowed(ALT_SHIFT_LETTERS[(code - b'A') as usize])), _ => {}, } } - None + match code { + 1..=26 => Some(Cow::Borrowed(CTRL_ALT_LETTERS[(code - 1) as usize])), + b'a'..=b'z' => Some(Cow::Borrowed(ALT_LETTERS[(code - b'a') as usize])), + b'A'..=b'Z' => Some(Cow::Borrowed(ALT_SHIFT_LETTERS[(code - b'A') as usize])), + _ => None, + } } // ============================================================================= @@ -1519,6 +1530,45 @@ mod tests { assert_eq!(parse_key_inner(b"\x1b\x1b", true).as_deref(), None); } + #[test] + fn esc_pair_alt_letters_mixed_mode() { + // tmux 3.6 with `extended-keys-format csi-u` can enable enhanced keyboard + // handling while still forwarding Alt+letter as the legacy ESC+letter form. + for active in [false, true] { + assert_eq!(parse_key_inner(b"\x1bp", active).as_deref(), Some("alt+p")); + assert_eq!(parse_key_inner(b"\x1bh", active).as_deref(), Some("alt+h")); + assert_eq!(parse_key_inner(b"\x1bP", active).as_deref(), Some("alt+shift+p")); + assert_eq!(parse_key_inner(b"\x1b\x10", active).as_deref(), Some("ctrl+alt+p")); + assert!(matches_key_inner(b"\x1bp", "alt+p", active)); + assert!(matches_key_inner(b"\x1bh", "alt+h", active)); + assert!(matches_key_inner(b"\x1bP", "alt+shift+p", active)); + assert!(matches_key_inner(b"\x1b\x10", "ctrl+alt+p", active)); + assert!(!matches_key_inner(b"\x1bp", "alt+up", active)); + assert!(!matches_key_inner(b"\x1bn", "alt+down", active)); + assert!(!matches_key_inner(b"\x1bb", "alt+left", active)); + assert!(!matches_key_inner(b"\x1bf", "alt+right", active)); + } + assert!(matches_key_inner(b"\x1b[1;3A", "alt+up", true)); + assert!(matches_key_inner(b"\x1b[112;3u", "alt+p", true)); + assert!(matches_key_inner(b"\x1b[27;3;112~", "alt+p", false)); + for active in [false, true] { + assert_eq!(parse_key_inner(b"\x1b\n", active).as_deref(), Some("alt+enter")); + assert!(matches_key_inner(b"\x1b\n", "alt+enter", active)); + } + } + + #[test] + fn uppercase_meta_b_f_stay_legacy_arrow_aliases_only_without_kitty() { + assert_eq!(parse_key_inner(b"\x1bB", false).as_deref(), Some("alt+left")); + assert_eq!(parse_key_inner(b"\x1bF", false).as_deref(), Some("alt+right")); + assert_eq!(parse_key_inner(b"\x1bB", true).as_deref(), Some("alt+shift+b")); + assert_eq!(parse_key_inner(b"\x1bF", true).as_deref(), Some("alt+shift+f")); + assert!(matches_key_inner(b"\x1bB", "alt+left", false)); + assert!(matches_key_inner(b"\x1bF", "alt+right", false)); + assert!(!matches_key_inner(b"\x1bB", "alt+left", true)); + assert!(!matches_key_inner(b"\x1bF", "alt+right", true)); + } + #[test] fn esc_prefix_csi_only() { // Only CSI and SS3 inner sequences parse as Alt; other double-ESC does not diff --git a/crates/pi-natives/src/lib.rs b/crates/pi-natives/src/lib.rs index 48984d2e7..8427bbc0e 100644 --- a/crates/pi-natives/src/lib.rs +++ b/crates/pi-natives/src/lib.rs @@ -23,6 +23,7 @@ pub mod appearance; pub mod ast; +pub mod block; pub mod clipboard; pub mod fd; pub mod fs_cache; @@ -67,5 +68,5 @@ use napi_derive::napi; /// MUST stay in sync with `VERSION_SENTINEL_EXPORT` in /// `packages/natives/native/index.js` (which derives the name from /// `package.json#version`). -#[napi(js_name = "__piNativesV15_5_10")] +#[napi(js_name = "__piNativesV15_8_3")] pub const fn pi_natives_version_sentinel() {} diff --git a/crates/pi-natives/src/shell.rs b/crates/pi-natives/src/shell.rs index 3fa494ab2..719c0cf34 100644 --- a/crates/pi-natives/src/shell.rs +++ b/crates/pi-natives/src/shell.rs @@ -356,9 +356,11 @@ mod tests { } #[test] - fn non_terminal_stdin_leading_new_pgroup_detaches_unless_pipeline() { + fn non_terminal_stdin_detaches_regardless_of_pipeline() { assert_eq!(child_session_action(true, false, false), ChildSessionAction::DetachSession); - assert_eq!(child_session_action(true, false, true), ChildSessionAction::None); + // A leading-new-pgroup stage of a pipeline still detaches: setsid keeps + // it off the host's controlling tty. + assert_eq!(child_session_action(true, false, true), ChildSessionAction::DetachSession); } #[test] @@ -377,8 +379,11 @@ mod tests { } #[test] - fn pipeline_stage_does_not_detach() { - assert_eq!(child_session_action(false, false, true), ChildSessionAction::None); + fn pipeline_stage_with_non_terminal_stdin_detaches() { + // Regression: an interactive child inside a pipeline (`zsh -i | awk`) + // must not stay in the host session and seize its tty. Pre-fix this + // returned `None`, leaving the stage attached and able to SIGTTIN the host. + assert_eq!(child_session_action(false, false, true), ChildSessionAction::DetachSession); } } diff --git a/crates/pi-natives/src/text.rs b/crates/pi-natives/src/text.rs index ab2716c3e..10039cdbe 100644 --- a/crates/pi-natives/src/text.rs +++ b/crates/pi-natives/src/text.rs @@ -378,14 +378,34 @@ const fn ascii_cell_width_u16(u: u16, tab_width: usize) -> usize { } } +const MACOS_HANGUL_COMPAT_JAMO_WIDTH: usize = 1; + +#[inline] +const fn is_macos_hangul_compat_jamo(c: char) -> bool { + let cp = c as u32; + cfg!(target_os = "macos") && cp >= 0x3131 && cp <= 0x318e +} + +#[inline] +fn apply_macos_hangul_compat_jamo_delta(width: usize, c: char) -> usize { + if !is_macos_hangul_compat_jamo(c) { + return width; + } + let unicode_width = UnicodeWidthChar::width(c).unwrap_or(0); + if unicode_width > MACOS_HANGUL_COMPAT_JAMO_WIDTH { + width.saturating_sub(unicode_width - MACOS_HANGUL_COMPAT_JAMO_WIDTH) + } else { + width.saturating_add(MACOS_HANGUL_COMPAT_JAMO_WIDTH - unicode_width) + } +} + #[inline] fn char_width_corrected(c: char) -> Option { // Hangul Compatibility Jamo U+3131..=U+318E render as 1 cell on macOS // terminals (Ghostty, Terminal.app, iTerm2), but follow UAX#11 at 2 // cells on WezTerm and most Linux terminals. Only force 1 on macOS. - let cp = c as u32; - if cfg!(target_os = "macos") && (0x3131..=0x318e).contains(&cp) { - return Some(1); + if is_macos_hangul_compat_jamo(c) { + return Some(MACOS_HANGUL_COMPAT_JAMO_WIDTH); } UnicodeWidthChar::width(c) } @@ -402,13 +422,18 @@ fn grapheme_width_str(g: &str, tab_width: usize) -> usize { if it.next().is_none() { return char_width_corrected(c0).unwrap_or(0); } + // Multi-char grapheme: keep UnicodeWidthStr as the source of truth for + // sequence-level width rules (VS16 emoji presentation, keycaps, ZWJ emoji, + // CRLF, script ligatures). A per-char sum is not equivalent. On macOS, + // apply only the same local Compatibility Jamo delta that + // char_width_corrected applies to standalone code points. + let mut width = UnicodeWidthStr::width(g); if cfg!(target_os = "macos") { - g.chars() - .map(|c| char_width_corrected(c).unwrap_or(0)) - .sum() - } else { - UnicodeWidthStr::width(g) + for c in g.chars() { + width = apply_macos_hangul_compat_jamo_delta(width, c); + } } + width } thread_local! { @@ -1311,6 +1336,31 @@ mod tests { assert_eq!(visible_width_u16(&to_u16("a\tb"), DEFAULT_TAB_WIDTH), 1 + DEFAULT_TAB_WIDTH + 1); } + #[test] + fn test_visible_width_vs16_emoji_presentation() { + // Variation-selector-16 (U+FE0F) promotes a default-text-presentation + // symbol to emoji presentation, which renders as 2 cells. A naive + // per-char sum would count U+26A0 (1) + U+FE0F (0) = 1 and shift table + // borders one column. Guards the regression where ⚠️ measured as 1. + assert_eq!(visible_width_u16(&to_u16("\u{26A0}\u{FE0F}"), DEFAULT_TAB_WIDTH), 2); // ⚠️ + assert_eq!(visible_width_u16(&to_u16("\u{2139}\u{FE0F}"), DEFAULT_TAB_WIDTH), 2); // ℹ️ + assert_eq!(visible_width_u16(&to_u16("\u{2764}\u{FE0F}"), DEFAULT_TAB_WIDTH), 2); // ❤️ + assert_eq!(visible_width_u16(&to_u16("0\u{FE0F}\u{20E3}"), DEFAULT_TAB_WIDTH), 2); // 0️⃣ keycap + // Bare symbol without VS16 keeps text-presentation width (1 cell). + assert_eq!(visible_width_u16(&to_u16("\u{26A0}"), DEFAULT_TAB_WIDTH), 1); + // Intrinsically wide emoji are unaffected. + assert_eq!(visible_width_u16(&to_u16("\u{2705}"), DEFAULT_TAB_WIDTH), 2); // ✅ + assert_eq!(visible_width_u16(&to_u16("\u{274C}"), DEFAULT_TAB_WIDTH), 2); // ❌ + } + + #[test] + fn test_visible_width_jamo_correction_inside_combining_cluster() { + let jamo_cells = if cfg!(target_os = "macos") { 1 } else { 2 }; + let filler_cells = if cfg!(target_os = "macos") { 1 } else { 0 }; + assert_eq!(visible_width_u16(&to_u16("\u{3141}\u{0301}"), DEFAULT_TAB_WIDTH), jamo_cells); + assert_eq!(visible_width_u16(&to_u16("\u{3164}\u{0301}"), DEFAULT_TAB_WIDTH), filler_cells); + } + #[test] fn test_ansi_detection() { let data = to_u16("\x1b[31mred\x1b[0m"); diff --git a/crates/pi-shell/src/shell.rs b/crates/pi-shell/src/shell.rs index 919f0ec6d..4de375c1c 100644 --- a/crates/pi-shell/src/shell.rs +++ b/crates/pi-shell/src/shell.rs @@ -663,7 +663,7 @@ async fn run_shell_command( .await; if cancel_token.is_cancelled() { - terminate_background_jobs(&session.shell); + terminate_background_jobs(&mut session.shell); } if env_scope_pushed { @@ -833,7 +833,7 @@ async fn run_shell_command_streams( .await; if cancel_token.is_cancelled() { - terminate_background_jobs(&session.shell); + terminate_background_jobs(&mut session.shell); } if env_scope_pushed { @@ -998,9 +998,10 @@ async fn terminate_new_descendants(baseline: & } } } -fn terminate_background_jobs(shell: &BrushShell) { +fn terminate_background_jobs(shell: &mut BrushShell) { let mut targets = process::TerminationTargets::new(); - for job in &shell.jobs().jobs { + for job in &mut shell.jobs_mut().jobs { + job.abort_internal_tasks(); if let Some(pgid) = job.process_group_id() { targets.add_pgid(pgid); } @@ -1009,11 +1010,9 @@ fn terminate_background_jobs(shell: &BrushShell) { } } if targets.is_empty() { - // Pure descendant cleanup is handled by `process_cancel_bridge` while - // the cancel was still in flight. Here we only signal brush's own - // job-tracked targets — pgids of background-group leaders that may have - // already exited (so the descendant walk would no longer find them as - // new descendants, but their group still holds live grandchildren). + // Shell-internal jobs were aborted above. Pure descendant cleanup is + // handled by `process_cancel_bridge` while the cancel was in flight; + // without job-tracked pgids or pids there is nothing else to signal here. return; } @@ -1634,13 +1633,16 @@ mod tests { assert_eq!(child_session_action(true, true, true), ChildSessionAction::TakeForeground,); } - /// Brush leading a new pgroup with non-terminal stdin detaches only when - /// it is not part of a multi-command pipeline. Pipeline leaders must stay - /// in the parent session so later stages can join their process group. + /// Brush leading a new pgroup with non-terminal stdin always detaches — + /// including the first stage of a pipeline. `setsid()` keeps the child + /// off the host's controlling tty; the spawn path skips + /// `process_group(...)` for detached children, so later stages no + /// longer try to `setpgid`-join a leader that has moved sessions (the + /// historical EPERM hazard). #[test] - fn non_terminal_stdin_leading_new_pgroup_detaches_unless_pipeline() { + fn non_terminal_stdin_detaches_regardless_of_pipeline() { assert_eq!(child_session_action(true, false, false), ChildSessionAction::DetachSession,); - assert_eq!(child_session_action(true, false, true), ChildSessionAction::None,); + assert_eq!(child_session_action(true, false, true), ChildSessionAction::DetachSession,); } /// Non-interactive brush, terminal stdin, no pipeline: nothing to do. @@ -1665,16 +1667,16 @@ mod tests { assert_eq!(child_session_action(false, false, false), ChildSessionAction::DetachSession,); } - /// **Pipeline carve-out.** Non-interactive brush, non-terminal stdin - /// (pipe), and a multi-command pipeline: MUST NOT detach. For the first - /// external stage, `setsid()` puts the process-group leader into a - /// different session, so later stages fail to join its group with - /// EPERM. For later stages, `setsid()` would either fail with EPERM or - /// move the child into a new session, breaking the pipeline's shared - /// process group and job-control signal propagation. + /// **Pipeline tty-safety.** Non-interactive brush, non-terminal stdin + /// (pipe), and a multi-command pipeline: detach. An interactive child in + /// a pipeline (`zsh -i ... | awk`) would otherwise open `/dev/tty`, + /// `tcsetpgrp` itself to the foreground, and leave the host stopped on + /// its next tty read (`suspended (tty input)`). Each stage gets its own + /// session instead; the embedded host cancels via the descendant tree, + /// not a shared pgroup, and pipes are session-independent. #[test] - fn pipeline_stage_does_not_detach() { - assert_eq!(child_session_action(false, false, true), ChildSessionAction::None,); + fn pipeline_stage_with_non_terminal_stdin_detaches() { + assert_eq!(child_session_action(false, false, true), ChildSessionAction::DetachSession,); } } @@ -1796,6 +1798,126 @@ mod tests { ); } + /// Regression for the `suspended (tty input)` bug: an **interactive child + /// inside a pipeline** (`zsh -i ... | awk`) used to stay in the host + /// session, open `/dev/tty`, `tcsetpgrp` itself to the foreground, and + /// leave the embedded host (OMP) stopped on its next tty read. The earlier + /// embedded-host fix carved pipelines out of `detach_session` because a + /// later stage that `setpgid`-joined a detached leader failed with EPERM. + /// + /// This test boots a real embedded `BrushShell` and runs a two-stage + /// pipeline whose first stage prints its PID then sleeps (forwarded to us + /// by `cat`). It asserts two contracts at once: + /// 1. the first stage runs in its **own session** (`getsid == own pid`), + /// so it can never reach the host's controlling tty — guards the + /// decision; and + /// 2. the pipeline still exits **successfully**, proving the second stage + /// spawned without the cross-session `setpgid` EPERM — guards the + /// wiring that skips `process_group(...)` for detached children. + #[cfg(unix)] + #[tokio::test(flavor = "multi_thread")] + async fn embedded_pipeline_stage_runs_in_its_own_session() { + use std::io::Read as _; + + // SAFETY: `getsid(0)` only queries the current process session; checked below. + let host_sid = unsafe { libc::getsid(0) }; + assert!(host_sid > 0, "getsid(0) failed: {}", std::io::Error::last_os_error()); + + let config = ShellConfig { session_env: None, snapshot_path: None, minimizer: None }; + let mut session = create_session(&config).await.expect("create_session"); + + let (mut reader, writer) = pipe_to_files("e2e-pipe").expect("pipe"); + let stdout_file = OpenFile::from(writer.try_clone().expect("clone")); + let stderr_file = OpenFile::from(writer); + + let mut params = session.shell.default_exec_params(); + params.set_fd(OpenFiles::STDIN_FD, null_file().expect("null stdin")); + params.set_fd(OpenFiles::STDOUT_FD, stdout_file); + params.set_fd(OpenFiles::STDERR_FD, stderr_file); + + let (pid_tx, pid_rx) = tokio::sync::oneshot::channel::(); + let reader_handle = tokio::task::spawn_blocking(move || { + let mut buf = Vec::new(); + let mut chunk = [0u8; 64]; + let mut pid_tx = Some(pid_tx); + while let Ok(n) = reader.read(&mut chunk) + && n > 0 + { + buf.extend_from_slice(&chunk[..n]); + if pid_tx.is_some() + && let Some(line_end) = buf.iter().position(|&byte| byte == b'\n') + && let Ok(line) = std::str::from_utf8(&buf[..line_end]) + && let Ok(pid) = line.trim().parse::() + { + let _ = pid_tx + .take() + .expect("pid sender should be present") + .send(pid); + } + } + buf + }); + + let shell_handle = tokio::spawn(async move { + let source_info = SourceInfo::from("pi-natives:test"); + // First stage prints its own PID and sleeps; `cat` forwards the PID + // line to our reader and exits on EOF. The first stage leads the + // pipeline's process group, the second (`cat`) is the join-or-detach + // stage that would EPERM without the wiring fix. + let exec = session + .shell + .run_string( + "/bin/sh -c 'printf \"%d\\n\" \"$$\"; sleep 1' | /bin/cat", + &source_info, + ¶ms, + ) + .await + .expect("run_string"); + drop(params); + (session, exec) + }); + + let child_pid = time::timeout(Duration::from_secs(5), pid_rx) + .await + .expect("timed out waiting for first-stage PID") + .expect("reader closed pid channel without sending"); + assert!(child_pid > 0, "got non-positive child pid: {child_pid}"); + + // SAFETY: `child_pid` is a live positive PID (still in `sleep`); the return + // value is checked. + let child_sid = unsafe { libc::getsid(child_pid) }; + assert!( + child_sid > 0, + "getsid({child_pid}) failed: {} (child may have already exited)", + std::io::Error::last_os_error(), + ); + + let (_session, exec) = time::timeout(Duration::from_secs(5), shell_handle) + .await + .expect("shell timed out") + .expect("shell task panicked"); + // Guards the wiring: the second stage spawned without a cross-session + // `setpgid` EPERM, so the whole pipeline succeeded. + assert!( + matches!(exec.exit_code, ExecutionExitCode::Success), + "pipeline did not succeed (second stage may have hit setpgid EPERM): {}", + exit_code(&exec), + ); + let _ = time::timeout(Duration::from_secs(2), reader_handle).await; + + // Guards the decision: a pipeline stage must not share the host session, + // or it could seize the controlling tty and SIGTTIN the host. + assert_ne!( + child_sid, host_sid, + "pipeline stage PID {child_pid} inherited host session {host_sid}; it could seize the \ + controlling tty — the pipeline tty-suspend bug is back", + ); + assert_eq!( + child_sid, child_pid, + "pipeline stage PID {child_pid} should be its own session leader after setsid", + ); + } + #[tokio::test] async fn abort_state_signals_cancel_token() { let abort_state = ShellAbortState::default(); @@ -1811,6 +1933,66 @@ mod tests { assert!(matches!(reason, AbortReason::Signal)); } + #[tokio::test(flavor = "multi_thread")] + async fn cancellation_aborts_internal_background_jobs() { + let unique = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock before epoch") + .as_nanos(); + let dir = + std::env::temp_dir().join(format!("pi-shell-bg-cancel-{}-{unique}", std::process::id())); + std::fs::create_dir(&dir).expect("create temp dir"); + let started = dir.join("started"); + let release = dir.join("release"); + let marker = dir.join("marker"); + + let config = ShellConfig { session_env: None, snapshot_path: None, minimizer: None }; + let mut session = create_session(&config).await.expect("create session"); + session + .shell + .set_working_dir(dir.to_string_lossy().as_ref()) + .expect("set cwd"); + + let mut params = session.shell.default_exec_params(); + params.set_fd(OpenFiles::STDIN_FD, null_file().expect("null stdin")); + params.set_fd(OpenFiles::STDOUT_FD, null_file().expect("null stdout")); + params.set_fd(OpenFiles::STDERR_FD, null_file().expect("null stderr")); + + let source_info = SourceInfo::from("pi-shell:test"); + let result = session + .shell + .run_string( + "{ echo started > started; while [ ! -f release ]; do sleep 0.05; done; echo done > \ + marker; } &", + &source_info, + ¶ms, + ) + .await + .expect("spawn background job"); + assert_eq!(exit_code(&result), 0); + + let mut background_started = false; + for _ in 0..200 { + if started.exists() { + background_started = true; + break; + } + time::sleep(Duration::from_millis(10)).await; + } + assert!(background_started, "background job did not reach its wait loop"); + + terminate_background_jobs(&mut session.shell); + std::fs::write(&release, b"").expect("release marker"); + time::sleep(Duration::from_millis(250)).await; + let marker_exists = marker.exists(); + std::fs::remove_dir_all(&dir).expect("cleanup temp dir"); + + assert!( + !marker_exists, + "internal background job survived cancellation and wrote marker after release", + ); + } + #[cfg(unix)] #[tokio::test] async fn read_output_stops_when_cancelled_before_pipe_eof() { diff --git a/docs/ERRATA-GPT5-HARMONY.md b/docs/ERRATA-GPT5-HARMONY.md index 194bcc9c3..78f886678 100644 --- a/docs/ERRATA-GPT5-HARMONY.md +++ b/docs/ERRATA-GPT5-HARMONY.md @@ -1,5 +1,9 @@ # ERRATA — GPT-5 Harmony-Header Leakage +Historical research note, not a current runtime contract. The statistics below +come from the named local stats database snapshot, not from checked-in tests or +runtime code. + ## 1. The problem OpenAI frames tool calls in the Harmony chat protocol: @@ -50,22 +54,22 @@ Source: `~/.omp/stats.db` (`ss_tool_calls`, `ss_assistant_msgs`), through ### 2.1 Rate -| Model | Leaks in tool args | Calls | per million | -|------------------|-------------------:|--------:|------------:| -| gpt-5.4 | 37 | 226,957 | 163 | -| gpt-5.3-codex | 17 | 112,243 | 151 | -| gpt-5.5 | 2 | 80,750 | 25 | -| gpt-5.2-codex | 0 | — | — | +| Model | Leaks in tool args | Calls | per million | +| ------------- | -----------------: | ------: | ----------: | +| gpt-5.4 | 37 | 226,957 | 163 | +| gpt-5.3-codex | 17 | 112,243 | 151 | +| gpt-5.5 | 2 | 80,750 | 25 | +| gpt-5.2-codex | 0 | — | — | Plus 15 hits in assistant visible text / thinking blobs. ### 2.2 Tool distribution -| Tool | Hits | -|---------------------|-----:| -| `edit` | 38 | -| `eval` | 11 | -| `report_tool_issue` | 3 | +| Tool | Hits | +| ------------------------------ | -----: | +| `edit` | 38 | +| `eval` | 11 | +| `report_tool_issue` | 3 | | `grep`/`read`/`search`/`yield` | 1 each | Concentrated in tools with free-form (non-JSON-schema) argument formats. @@ -83,8 +87,8 @@ JUNK_PREFIX ::= (GLITCH_TOKEN | CHANNEL_WORD | NON_LATIN_RUN | "}" | "】【")+ records, 39 contain ≥2 markers and 7 contain ≥3 — the model emits multiple fake `to=functions.X code …` blocks back-to-back, often with fake `code_output\nCell N:\n…` framing between them. Once the -plain-text scaffolding is in the residual stream, the prefix now *looks -like* a fresh tool envelope start, so the macro prior over continuations +plain-text scaffolding is in the residual stream, the prefix now _looks +like_ a fresh tool envelope start, so the macro prior over continuations keeps voting for more scaffolding. Self-amplifying. ### 2.4 Glitch tokens @@ -93,13 +97,13 @@ Single-token identifiers in `o200k_base` whose embeddings appear to be near-init from underrepresentation in post-training. ASCII residue immediately before the marker in the natural corpus: -| Surface string | Single-token | Token ID | Hits in corpus | -|-------------------|:-:|---------:|---:| -| `Japgolly` | ✅ | 199,745 | 1 | -| `Jsii` | ✅ | 114,318 | (subtoken of `Jsii_commentary`) | -| `Jsii_commentary` | — (3 toks) | — | 2 | -| `changedFiles` | — (2 toks) | — | 8 | -| `RTLU` | — (2 toks) | — | 3 | +| Surface string | Single-token | Token ID | Hits in corpus | +| ----------------- | :----------: | -------: | ------------------------------: | +| `Japgolly` | ✅ | 199,745 | 1 | +| `Jsii` | ✅ | 114,318 | (subtoken of `Jsii_commentary`) | +| `Jsii_commentary` | — (3 toks) | — | 2 | +| `changedFiles` | — (2 toks) | — | 8 | +| `RTLU` | — (2 toks) | — | 3 | `Japgolly` is in the last 0.13% of the vocabulary — the same family of GitHub-corpus residue that produced `SolidGoldMagikarp` in the 2023 @@ -136,17 +140,17 @@ reproduction (§7.3), independent of the prompt's natural language. The `edit` tool exists in two variants in the corpus: -| Variant | Calls | Recovery | -|--------------------------|------:|----------| -| Patch-DSL (`§PATH`/anchor/`«»≔` ops) | 27 | **Recoverable** by op-truncation (§3.3) | -| JSON-schema (`{path,edits:[…]}`) | 11 | **Not recoverable** — contamination is escaped *inside* JSON strings, parser accepts it cleanly, content would be written verbatim into source files | +| Variant | Calls | Recovery | +| ------------------------------------ | ----: | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| Patch-DSL (`§PATH`/anchor/`«»≔` ops) | 27 | **Recoverable** by op-truncation (§3.3) | +| JSON-schema (`{path,edits:[…]}`) | 11 | **Not recoverable** — contamination is escaped _inside_ JSON strings, parser accepts it cleanly, content would be written verbatim into source files | For Patch-DSL leaks specifically: - 20/27 cases: contamination on the last input line; nothing follows. - 7/27 cases: contamination mid-input; what follows is one of: a duplicate replay of an earlier file/anchor, intended content for a - *different* tool call (the model started its next call inline), or + _different_ tool call (the model started its next call inline), or pure hallucination. Post-contamination content is never trustworthy. ### 2.8 Mechanism (confirmed) @@ -167,7 +171,7 @@ Step by step: merge corpus but barely in LM/RL training, so its **input embedding `e_g` ≈ near-init noise of small norm**. 3. At position t+1, the residual update `h_{t+1} ≈ LN(h_t + e_g + Attn + - MLP)` is dominated by the prefix-derived terms; the just-emitted-token +MLP)` is dominated by the prefix-derived terms; the just-emitted-token signal is effectively absent. Generation diversity normally comes from `e_x` steering the residual into different sub-regions — stripped here. @@ -179,7 +183,7 @@ Step by step: 5. The mask zeros the control-token IDs. Mass redistributes onto the **next-best continuation**: the un-bracketed surface-form spelling of the same protocol (`analysis`, `commentary`, ` to=functions.X`, - ` code `). This spelling is unmasked because those characters are + `code`). This spelling is unmasked because those characters are ordinary tokens. 6. Once a few tokens of plain-text scaffolding land in the residual stream, the prefix now resembles a fresh envelope start. The macro @@ -194,7 +198,7 @@ explained:** - **The brackets never appear** (§1, §2.5). The mask is what makes the leak land in plain text instead of as a real envelope-close. -- **Counterintuitive grammar dependency** (§7.4). The leak is *worse* in +- **Counterintuitive grammar dependency** (§7.4). The leak is _worse_ in formats closest to OpenAI's training distribution. Off-distribution custom grammars dampen the macro-prior basin; the official `*** Begin Patch` format is the strongest collapse target. @@ -202,4 +206,4 @@ explained:** The 2023 SolidGoldMagikarp paper documented mechanism (1)+(2)+(4). The new piece is (5): when constrained decoding masks the natural collapse target, the mass laundered through the un-masked plain-text shadow -becomes a structurally-invisible exfiltration channel. \ No newline at end of file +becomes a structurally-invisible exfiltration channel. diff --git a/docs/ai-schema-normalize.md b/docs/ai-schema-normalize.md index a1a2e1e6b..2eb734493 100644 --- a/docs/ai-schema-normalize.md +++ b/docs/ai-schema-normalize.md @@ -43,14 +43,14 @@ Removed in the unified-flow refactor: ## Dispatcher mapping -| Provider transport(s) | Dispatcher | -| -------------------------------------------------------------------- | -------------------------------------------- | -| `openai-completions`, `openai-responses`, `openai-codex-responses` | `adaptSchemaForStrict` (sanitize + enforce) | -| `openai-responses` family (`oneOf` → `anyOf` only) | `normalizeSchemaForOpenAIResponses` | -| `google-generative-ai`, `google-vertex`, Gemini CLI | `normalizeSchemaForGoogle` | -| Cloud Code Assist Claude (Antigravity + GCA, `claude-*` model ids) | `normalizeSchemaForCCA` | -| MCP `inputSchema` ingestion | `normalizeSchemaForMCP` | -| `anthropic-messages` (native, not CCA) | per-provider whitelist in `anthropic.ts` | +| Provider transport(s) | Dispatcher | +| ------------------------------------------------------------------ | ------------------------------------------- | +| `openai-completions`, `openai-responses`, `openai-codex-responses` | `adaptSchemaForStrict` (sanitize + enforce) | +| `openai-responses` family (`oneOf` → `anyOf` only) | `normalizeSchemaForOpenAIResponses` | +| `google-generative-ai`, `google-vertex`, Gemini CLI | `normalizeSchemaForGoogle` | +| Cloud Code Assist Claude (Antigravity + GCA, `claude-*` model ids) | `normalizeSchemaForCCA` | +| MCP `inputSchema` ingestion | `normalizeSchemaForMCP` | +| `anthropic-messages` (native, not CCA) | per-provider whitelist in `anthropic.ts` | Gemini CLI / Antigravity CCA MUST run the full `normalizeSchemaForCCA` pipeline (not just the first keyword-stripping pass) to keep parity with the @@ -58,25 +58,25 @@ shared Google Claude path. ## Walk semantics -`normalizeSchema` first upgrades the input to JSON Schema 2020-12, then -walks the tree with the option set pinned by the dispatcher. Each node: +`normalizeSchema` first detoxifies serialized Zod-instance-shaped inputs, upgrades them to +JSON Schema 2020-12, dereferences the tree, then walks it with the option set +pinned by the dispatcher. Each node: -1. Inlines `$ref` (see "Edge cases" below). -2. Renames `snake_case` combinator/property keys to camelCase +1. Renames `snake_case` combinator/property keys to camelCase (`any_of` → `anyOf`, etc.; collisions follow python-genai `pop(from)`/`set(to)` semantics — snake_case wins). -3. Applies the `handle_null_fields` collapse for nullable unions before +2. Applies the `handle_null_fields` collapse for nullable unions before recursing into children. -4. Strips keys the target provider does not support, optionally lifting +3. Strips keys the target provider does not support, optionally lifting human-meaningful keys (`pattern`, `format`, min/max, `default`, `examples`, ...) into the sibling `description` via the spill formatter (`spill.ts`). Structural/meta keys (`$ref`, `$defs`, `additionalProperties`) are not spilled. -5. Normalizes type unions (`type: ["T", "null"]` → `type: "T"` + nullable +4. Normalizes type unions (`type: ["T", "null"]` → `type: "T"` + nullable marker on Google, plain `type: "T"` on CCA). -6. Collapses object-only / same-type combiners, optionally lossy-collapses +5. Collapses object-only / same-type combiners, optionally lossy-collapses mixed-type combiners (CCA only), and runs the residual-combiner fixpoint. -7. Validates against AJV 2020 when `validateAndFallback` is set (CCA path) +6. Validates against AJV 2020 when `validateAndFallback` is set (CCA path) and emits the per-tool fallback `{ "type": "object", "properties": {} }` on residual incompatibility — `type` array, `type: "null"`, `nullable` key, or any remaining `anyOf`/`oneOf`/`allOf`. @@ -99,11 +99,10 @@ which composes: (`anyOf: [, { "type": "null" }]`). Tuple `prefixItems` are strictified recursively. -The two passes share node-level caches and the same epoch-based cycle -guard, so a single walk on the wire path normalizes refs, allOf, and -nullable wrapping consistently. `tryEnforceStrictSchema` is fail-open: -if anything throws, it returns `{ strict: false, schema: original }` so -callers MUST emit `strict: true` only when enforcement actually succeeded. +The two passes use cache/cycle guards, so refs, `allOf`, and nullable wrapping +stay deterministic without recursing forever. `tryEnforceStrictSchema` is +fail-open: if anything throws, it returns `{ strict: false, schema: upgraded }` +so callers MUST emit `strict: true` only when enforcement actually succeeded. ### Edge cases the strict-mode normalizer handles diff --git a/docs/approval-mode.md b/docs/approval-mode.md index 910d4e955..97c59d3a0 100644 --- a/docs/approval-mode.md +++ b/docs/approval-mode.md @@ -6,7 +6,7 @@ Tool approval has two independent inputs: - `read`: reads data or updates UI-only session metadata. - `write`: mutates workspace/session state but does not execute arbitrary code. - `exec`: executes code, shells out, drives a browser, spawns agents, or performs similarly broad actions. -2. **User policy** — `tools.approval.: allow | deny | prompt` overrides the mode for that tool. +2. **User policy** — `tools.approval.: allow | deny | prompt` overrides the mode for that tool unless a non-yolo safety override forces a prompt. Tools without an `approval` declaration are treated as `exec`. This is the safe default for MCP and unknown custom tools. @@ -14,13 +14,11 @@ Tools without an `approval` declaration are treated as `exec`. This is the safe Configure with `tools.approvalMode`: -## Modes - -| Mode | Auto-approves | Prompts for | -| --- | --- | --- | -| `always-ask` | `read` | `write`, `exec` | -| `write` | `read`, `write` | `exec` | -| `yolo` (default) | `read`, `write`, `exec` | none | +| Mode | Auto-approves | Prompts for | +| ---------------- | ----------------------- | --------------- | +| `always-ask` | `read` | `write`, `exec` | +| `write` | `read`, `write` | `exec` | +| `yolo` (default) | `read`, `write`, `exec` | none | `--auto-approve` and `--yolo` force `tools.approvalMode: yolo` for the session. @@ -40,12 +38,11 @@ tools: Resolution per tool call: 1. Compute the tool's approval decision from `tool.approval(args)`; omitted means `exec`. -2. A user policy in `tools.approval.` is always applied. -3. In `yolo` mode, with no user policy, the call is auto-approved. -4. In non-yolo modes, if the tool sets `override: true`, `deny` is blocked and all other cases prompt. -5. Otherwise, the active mode auto-approves or prompts by tier. - -Invalid policy values are ignored and fall back to the tool tier/mode decision. +2. Normalize `tools.approval.` if present; invalid values are ignored. +3. In `yolo` mode, the user policy is used when present; otherwise the call is allowed. Safety `override` reasons do not force a prompt in `yolo`. +4. In non-yolo modes, if the tool sets `override: true`, `deny` is blocked and all other cases prompt, even if user policy says `allow`. +5. Otherwise, a valid user policy wins. +6. Otherwise, the active mode auto-approves or prompts by tier. ## Safety overrides @@ -55,7 +52,7 @@ A tool can force a prompt with object-form approval: approval: { tier: "exec", override: true, reason: "Critical pattern detected" } ``` - `bash` uses this for critical destructive patterns such as `rm -rf /`, fork bombs, remote-fetch-then-execute, writes to `/etc/passwd`, and host shutdown commands. These surface as `reason` in the approval prompt, but in `yolo` mode they are auto-approved unless a user policy for the tool is set to `prompt` or `deny`. +`bash` uses this for critical destructive patterns such as `rm -rf /`, fork bombs, remote-fetch-then-execute, writes to `/etc/passwd`, and host shutdown commands. These surface as `reason` in the approval prompt, but in `yolo` mode they are auto-approved unless a user policy for the tool is set to `prompt` or `deny`. ## Per-tool prompt details @@ -82,13 +79,14 @@ formatApprovalDetails?: (args: unknown) => string | string[] | undefined; Examples: ```ts -approval: "read" +approval: "read"; -approval: args => LSP_READONLY_ACTIONS.has(args.action) ? "read" : "write" +approval: (args) => (LSP_READONLY_ACTIONS.has(args.action) ? "read" : "write"); -approval: args => isCritical(args.command) - ? { tier: "exec", override: true, reason: "Critical pattern detected" } - : "exec" +approval: (args) => + isCritical(args.command) + ? { tier: "exec", override: true, reason: "Critical pattern detected" } + : "exec"; ``` ## Subagents diff --git a/docs/auth-broker-gateway.md b/docs/auth-broker-gateway.md index e13219a91..ec7948d1b 100644 --- a/docs/auth-broker-gateway.md +++ b/docs/auth-broker-gateway.md @@ -2,8 +2,8 @@ The auth broker and auth gateway are two cooperating HTTP services that move OAuth refresh tokens and provider access tokens off developer laptops and into a single broker host. -- **`omp auth-broker serve`** holds the canonical SQLite credential vault, performs OAuth refreshes, and exposes a small REST API (`/v1/snapshot`, `/v1/credential/:id/refresh`, `/v1/credential/:id/disable`, `/v1/credential`, `/v1/usage`, `/v1/healthz`). -- **`omp auth-gateway serve`** is a forward-proxy. It accepts OpenAI Chat Completions, Anthropic Messages, and OpenAI Responses requests, injects the broker-resolved access token, and forwards the bytes to the real provider. Clients (containerised omp, llm-git, the macOS usage widget, …) never see the access token. +- **`omp auth-broker serve`** holds the canonical SQLite credential vault, performs OAuth refreshes, and exposes a small REST API (`/v1/snapshot`, `/v1/snapshot/stream`, `/v1/credential/:id/refresh`, `/v1/credential/:id/disable`, `/v1/credential`, `/v1/usage`, `/v1/healthz`). +- **`omp auth-gateway serve`** is a forward-proxy. It accepts OpenAI Chat Completions, Anthropic Messages, OpenAI Responses, and pi-native stream requests, resolves the broker-backed credential, and dispatches through `pi-ai` provider logic. Clients (containerised omp, llm-git, the macOS usage widget, …) never see the access token. Transport security between operator, broker, and gateway is delegated to the operator (Tailscale / Wireguard / reverse proxy + TLS). Every endpoint except `/v1/healthz` (broker) and `/healthz` (gateway) requires a bearer token. @@ -25,20 +25,21 @@ Source: `packages/ai/src/auth-broker/`, `packages/ai/src/auth-gateway/`, `packag │ ▼ │ │ ┌──────────────────────────┐ │ │ │ omp auth-gateway serve │ RemoteAuthCredentialStore │ - │ │ /v1/{chat,messages,…} │ pulls /v1/snapshot at boot, │ - │ │ /v1/usage, /v1/models │ refreshes credentials by id │ - │ └─────────┬────────────────┘ via the broker on expiry │ + │ │ /v1/{chat,messages,…} │ receives snapshot stream, │ + │ │ /v1/usage,/v1/models │ refreshes credentials by id │ + │ │ /v1/credentials/check │ via the broker on expiry │ + │ └─────────┬────────────────┘ │ └────────────┼───────────────────────────────────────────────┘ │ bearer ($CONFIG_DIR/auth-gateway.token) ▼ - unauthenticated clients + gateway clients (llm-git, macOS widget, robomp containers, IDE plugins, …) │ - ▼ same path is forwarded with Authorization + ▼ provider request with broker-resolved credential api.anthropic.com / api.openai.com / … ``` -The broker is the only writer of OAuth refresh tokens. Clients (including the gateway itself) load a redacted snapshot in which every `refresh` field has been replaced with `REMOTE_REFRESH_SENTINEL`; when an access token expires the client calls `POST /v1/credential/:id/refresh` and the broker performs the refresh server-side. `RemoteAuthCredentialStore` rejects any local code path that tries to write through it, with an error pointing at `omp auth-broker login` / `omp auth-broker logout`. +The broker is the only writer of OAuth refresh tokens. Clients (including the gateway itself) load a redacted snapshot in which every `refresh` field has been replaced with `REMOTE_REFRESH_SENTINEL`; when an access token expires the client calls `POST /v1/credential/:id/refresh` and the broker performs the refresh server-side. `RemoteAuthCredentialStore` rejects local replace/upsert/delete-by-provider mutations, with errors pointing at `omp auth-broker login` / `omp auth-broker logout`. ## auth-broker @@ -51,7 +52,7 @@ omp auth-broker login [] [--via=user@host] [--dry-run] omp auth-broker logout [] omp auth-broker list [--json] omp auth-broker import [--provider=] [--include-disabled] [--dry-run] [--json] -omp auth-broker migrate --from-local [--dry-run] [--json] +omp auth-broker migrate --from-local [--include-oauth] [--include-env] [--dry-run] [--json] omp auth-broker status [--json] ``` @@ -61,19 +62,20 @@ omp auth-broker status [--json] - `logout []` deletes every credential row for ``. With no argument it shows an interactive numbered picker of currently-stored providers. - `list` enumerates every registered OAuth provider id/name (the union of built-ins + `registerOAuthProvider` custom providers). `--json` emits a machine-readable array. - `import ` imports CLIProxyAPI-style JSON credentials into the local SQLite store. Maps `type` field → omp provider (`claude → anthropic`, `codex → openai-codex`, `gemini → google-gemini-cli`, `antigravity → google-antigravity`, `gemini-cli → google-gemini-cli`). -- `migrate --from-local` walks the local SQLite store + env-derived credentials and idempotently uploads them to the configured broker (`POST /v1/credential`). +- `migrate --from-local` uploads local SQLite credentials to the configured broker (`POST /v1/credential`). Local API keys are included by default; local OAuth rows are skipped unless `--include-oauth` is set; environment-derived API keys are skipped unless `--include-env` is set. Re-runs are idempotent against the broker snapshot. - `status` health-pings the configured remote broker. ### Endpoints -| Method | Path | Auth | Purpose | -| ------ | ---- | ---- | ------- | -| `GET` | `/v1/healthz` | none | Liveness + version | -| `GET` | `/v1/snapshot` | bearer | Redacted snapshot (refresh tokens replaced by sentinel) | -| `POST` | `/v1/credential` | bearer | Upsert one OAuth or API-key credential | -| `POST` | `/v1/credential/:id/refresh` | bearer | Force-refresh one OAuth credential | -| `POST` | `/v1/credential/:id/disable` | bearer | Disable one credential with a recorded cause | -| `GET` | `/v1/usage` | bearer | Aggregate `UsageReport[]` across credentials | +| Method | Path | Auth | Purpose | +| ------ | ---------------------------- | ------ | ------------------------------------------------------- | +| `GET` | `/v1/healthz` | none | Liveness + version | +| `GET` | `/v1/snapshot` | bearer | Redacted snapshot (refresh tokens replaced by sentinel) | +| `GET` | `/v1/snapshot/stream` | bearer | SSE snapshot stream with delta events and keepalives | +| `POST` | `/v1/credential` | bearer | Upsert one OAuth or API-key credential | +| `POST` | `/v1/credential/:id/refresh` | bearer | Force-refresh one OAuth credential | +| `POST` | `/v1/credential/:id/disable` | bearer | Disable one credential with a recorded cause | +| `GET` | `/v1/usage` | bearer | Aggregate `UsageReport[]` across credentials | Requests use `Authorization: Bearer `. The server compares against an in-memory token allow-list; the gateway’s implementation uses a timing-safe comparison. @@ -92,26 +94,29 @@ Requests use `Authorization: Bearer `. The server compares against an in- omp auth-gateway serve [--bind=host:port] [--no-auth] omp auth-gateway token [--regenerate] [--json] omp auth-gateway status [--json] +omp auth-gateway check [--strict] [--json] ``` - `serve` requires `OMP_AUTH_BROKER_URL` (or `auth.broker.url` in `config.yml`) — the gateway is itself a broker client. It calls `AuthBrokerClient.fetchSnapshot()`, wraps it in `RemoteAuthCredentialStore`, and constructs an `AuthStorage` that resolves access tokens through the broker. Default bind is `127.0.0.1:4000`. The gateway token is stored at `/auth-gateway.token` (`0600`); `--no-auth` disables the bearer check entirely (loopback-only use). -- `token` / `status` mirror the broker’s equivalents. +- `token` / `status` manage and inspect the gateway bearer token and upstream broker readiness. +- `check` probes broker-backed credentials through the gateway store. Without `--strict` it uses provider usage probes; `--strict` also exercises each credential against its chat-completion endpoint and can consume a small amount of quota. ### Endpoints -| Method | Path | Auth | Purpose | -| ------ | ---- | ---- | ------- | -| `GET` | `/healthz` | none | Liveness + version | -| `GET` | `/v1/usage` | bearer | Aggregate `UsageReport[]` (proxied through `AuthStorage`) | -| `GET` | `/v1/models` | bearer | Bundled-model catalog filtered to providers with credentials | -| `POST` | `/v1/chat/completions` | bearer | OpenAI Chat Completions wire format | -| `POST` | `/v1/messages` | bearer | Anthropic Messages wire format | -| `POST` | `/v1/responses` | bearer | OpenAI Responses wire format | +| Method | Path | Auth | Purpose | +| ------ | ----------------------- | ------ | ------------------------------------------------------------ | +| `GET` | `/healthz` | none | Liveness + version | +| `GET` | `/v1/usage` | bearer | Aggregate `UsageReport[]` (proxied through `AuthStorage`) | +| `GET` | `/v1/models` | bearer | Bundled-model catalog filtered to providers with credentials | +| `GET` | `/v1/credentials/check` | bearer | Per-credential auth health probe | +| `POST` | `/v1/chat/completions` | bearer | OpenAI Chat Completions wire format | +| `POST` | `/v1/messages` | bearer | Anthropic Messages wire format | +| `POST` | `/v1/responses` | bearer | OpenAI Responses wire format | +| `POST` | `/v1/pi/stream` | bearer | Native `pi-ai` stream wire format | -The model id is read from the top-level `model` field. The gateway picks the first bundled `Model` matching that id and: +The model id is read from the top-level `model` field for foreign wire formats and from the pi-native request body for `/v1/pi/stream`. The gateway picks the first bundled `Model` matching that id, parses the inbound wire format into an omp `Context`, resolves the provider credential from broker-backed `AuthStorage`, dispatches through `streamSimple()`, and re-encodes the result to the inbound format (SSE for streamed responses). -- **Passthrough fast-path** — when the inbound wire format matches the model’s native API (`openai-chat → openai-completions`, `anthropic-messages → anthropic-messages`, `openai-responses → openai-responses`), the request body is forwarded byte-for-byte with the client `Authorization`/`x-api-key` stripped and replaced by `Authorization: Bearer `. Provider-specific fields (`cache_control`, `service_tier`, tool-choice extensions, …) flow through unmodified. Hop-by-hop headers (RFC 7230) plus `Content-Encoding`/`Content-Length` are stripped from the upstream response. -- **Translate path** — when the inbound format and the resolved model’s API differ (e.g. `/v1/chat/completions` targeting an Anthropic model, or `/v1/responses` targeting `openai-codex-responses` which runs over a websocket transport), the request is parsed against the wire schema, rebuilt into an omp `Context`, dispatched through `streamSimple()`, and re-encoded back to the inbound format (SSE for streamed responses). +There is no raw provider passthrough path. All supported routes go through `pi-ai` provider logic so credential-specific request shaping, OAuth refresh-on-auth-error, and provider quirks stay centralized. `idleTimeout` on the underlying `Bun.serve` is set to `255 s` so long thinking-budget calls do not get killed by Bun’s default idle timeout. @@ -141,14 +146,14 @@ The broker is **off** unless `OMP_AUTH_BROKER_URL` (or `auth.broker.url` in `con ### Environment variables -| Variable | Purpose | Required when | -| -------- | ------- | ------------- | -| `OMP_AUTH_BROKER_URL` | Base URL of the remote auth-broker (e.g. `https://broker.tailnet:8765`). Selecting this puts the client in broker mode — local SQLite is bypassed. | Any time the omp client should resolve credentials through a broker (and required by `omp auth-gateway serve`). | -| `OMP_AUTH_BROKER_TOKEN` | Bearer token used for every broker endpoint except `/v1/healthz`. | When `OMP_AUTH_BROKER_URL` is set and no token is available from `auth.broker.token` or `/auth-broker.token`. | +| Variable | Purpose | Required when | +| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | +| `OMP_AUTH_BROKER_URL` | Base URL of the remote auth-broker (e.g. `https://broker.tailnet:8765`). Selecting this puts the client in broker mode — local SQLite is bypassed. | Any time the omp client should resolve credentials through a broker (and required by `omp auth-gateway serve`). | +| `OMP_AUTH_BROKER_TOKEN` | Bearer token used for every broker endpoint except `/v1/healthz`. | When `OMP_AUTH_BROKER_URL` is set and no token is available from `auth.broker.token` or `/auth-broker.token`. | Resolution order in `resolveAuthBrokerConfig()`: -1. `OMP_AUTH_BROKER_URL` env (else `auth.broker.url` from `config.yml`, with `$ENV_NAME` resolution); +1. `OMP_AUTH_BROKER_URL` env (else `auth.broker.url` from `config.yml`, resolved through `resolveConfigValue`); 2. `OMP_AUTH_BROKER_TOKEN` env (else `auth.broker.token` from `config.yml`, else `/auth-broker.token`); 3. URL set but no token resolvable → hard error pointing at the token file path. @@ -156,16 +161,16 @@ The gateway has no dedicated env vars — it inherits `OMP_AUTH_BROKER_*` becaus ### `config.yml` keys -| Key | Default | Purpose | -| --- | ------- | ------- | -| `auth.broker.url` | unset | Same as `OMP_AUTH_BROKER_URL`; env wins. Hidden from the settings UI. | -| `auth.broker.token` | unset | Same as `OMP_AUTH_BROKER_TOKEN`; env wins. Values may be the literal token or `$ENV_NAME` to indirect through env. | +| Key | Default | Purpose | +| ------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `auth.broker.url` | unset | Same as `OMP_AUTH_BROKER_URL`; env wins. Hidden from the settings UI. Values are resolved as a literal, an environment variable name, or `!` to use trimmed stdout. | +| `auth.broker.token` | unset | Same as `OMP_AUTH_BROKER_TOKEN`; env wins. Values are resolved the same way. | ### Token files -| Path | Owner | Mode | -| ---- | ----- | ---- | -| `/auth-broker.token` | `omp auth-broker serve` (created at first start) | `0600` in a `0700` parent dir | +| Path | Owner | Mode | +| --------------------------------- | ---------------------------------------------------- | ----------------------------- | +| `/auth-broker.token` | `omp auth-broker serve` (created at first start) | `0600` in a `0700` parent dir | | `/auth-gateway.token` | `omp auth-gateway serve` (skipped under `--no-auth`) | `0600` in a `0700` parent dir | `` resolves to `~/.omp/` (respecting `PI_CONFIG_DIR`). @@ -178,6 +183,6 @@ The broker only owns OAuth credentials and provider-API-key credentials that wer ## See also -- [`secrets.md`](./secrets.md) — secret obfuscation around tokens that *do* leak through (e.g. `OMP_AUTH_BROKER_TOKEN` in shell output). +- [`secrets.md`](./secrets.md) — secret obfuscation around tokens that _do_ leak through (e.g. `OMP_AUTH_BROKER_TOKEN` in shell output). - [`models.md`](./models.md) — provider auth resolution order; the broker plugs in at layers 2–3 (stored credentials). - [`environment-variables.md`](./environment-variables.md) — full env reference including `OMP_AUTH_BROKER_URL` / `OMP_AUTH_BROKER_TOKEN`. diff --git a/docs/bash-tool-runtime.md b/docs/bash-tool-runtime.md index 4fc506533..01d9bb2ce 100644 --- a/docs/bash-tool-runtime.md +++ b/docs/bash-tool-runtime.md @@ -10,7 +10,7 @@ There are two different bash execution surfaces in coding-agent: 1. **Tool-call surface** (`toolName: "bash"`): used when the model calls the bash tool. - Entry point: `BashTool.execute()`. - - Parameters include `command`, optional `env`, `timeout`, `cwd`, `head`, `tail`, `pty`, and, when `async.enabled` is true, `async`. + - Parameters include `command`, optional `env`, `timeout`, `cwd`, `pty`, and, when `async.enabled` is true, `async`. 2. **User bang-command surface** (`!cmd` from interactive input or RPC `bash` command): session-level helper path. - Entry point: `AgentSession.executeBash()`. @@ -23,11 +23,11 @@ Both eventually use `executeBash()` in `src/exec/bash-executor.ts` for non-PTY e `BashTool.execute()` currently handles input before execution as follows: - validates optional `env` names against shell-variable syntax, -- extracts a leading `cd && ...` into `cwd` when `cwd` was not supplied, -- rejects `async: true` when `async.enabled` is false, -- uses only explicit `head`/`tail` tool args for post-run filtering. +- when `bash.stripTrailingHeadTail` is enabled (default), applies conservative native fixups that remove safe trailing `| head` / `| tail` pipes and redundant trailing `2>&1`, +- extracts a leading single-line `cd && ...` into `cwd` when `cwd` was not supplied, +- rejects `async: true` when `async.enabled` is false. -`normalizeBashCommand()` was previously in `src/tools/bash-normalize.ts` but has been removed. Trailing shell pipes such as `| head -n 50` remain part of the shell command unless the caller uses the structured `head`/`tail` args. +There are no structured `head` or `tail` tool parameters in the current schema. Output limiting is handled by `OutputSink` truncation/artifacts, and the optional trailing-pipe fixup exists to avoid hiding output before the harness can capture it. ## 2) Optional interception (blocked-command path) @@ -173,6 +173,10 @@ Both PTY and non-PTY paths use `OutputSink`. Runtime truncation is byte-threshold based in `OutputSink` (50KB default). It does not enforce a hard 2000-line cap in this code path. +### Shell output minimizer + +Non-PTY execution also passes shell-minimizer settings into the native `Shell` session. When the minimizer rewrites verbose output, the executor replaces the sink's visible text with the minimized text and, when possible, saves the raw original capture as a separate `bash-original` artifact referenced by a `[raw output: artifact://]` footer. + ## Live tool updates and async jobs For non-PTY foreground execution, `BashTool` uses a separate `TailBuffer` for partial updates and emits `onUpdate` snapshots while command is running. @@ -189,12 +193,11 @@ After execution: - if abort signal is aborted -> throw `ToolAbortError` (abort semantics), - else -> throw `ToolError` (treated as tool failure). 2. PTY `timedOut` -> throw `ToolError`. -3. apply head/tail filters to final output text (`applyHeadTail`, head then tail). -4. empty output becomes `(no output)`. -5. attach truncation metadata via `toolResult(...).truncationFromSummary(result, { direction: "tail" })`. -6. exit-code mapping: - - missing exit code -> `ToolError("... missing exit status")` - - non-zero exit -> `ToolError("... Command exited with code N")` +3. empty output becomes `(no output)`. +4. attach truncation metadata via `toolResult(...).truncationFromSummary(result, { direction: "tail" })`. +5. exit-code mapping: + - missing exit code -> throw `ToolError("... missing exit status")` + - non-zero exit -> error result with `"Command exited with code N"` and `details.exitCode` - zero exit -> success result. Success payload structure: @@ -236,13 +239,13 @@ This component is wired by `CommandController.handleBashCommand()` and fed from ## Mode-specific behavior differences -| Surface | Entry path | PTY eligible | Live output UX | Error surfacing | -| ------------------------------ | ----------------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------ | -| Interactive tool call | `BashTool.execute` | Yes, when `pty=true` and UI exists and `PI_NO_PTY!=1` | PTY overlay (interactive) or streamed tail updates | Tool errors become `toolResult.isError` | -| Print mode tool call | `BashTool.execute` | No (no UI context) | No TUI overlay; output appears in event stream/final assistant text flow | Same tool error mapping | -| RPC tool call (agent tooling) | `BashTool.execute` | Usually no UI -> non-PTY | Structured tool events/results | Same tool error mapping | -| Interactive bang command (`!`) | `AgentSession.executeBash` + `BashExecutionComponent` | No (uses executor directly) | Dedicated bash execution component | Controller catches exceptions and shows UI error | -| RPC `bash` command | `rpc-mode` -> `session.executeBash` | No | Returns `BashResult` directly | Consumer handles returned fields | +| Surface | Entry path | PTY eligible | Live output UX | Error surfacing | +| ------------------------------ | ----------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------ | +| Interactive tool call | `BashTool.execute` | Yes, when `pty=true` and UI exists and `PI_NO_PTY!=1` | PTY overlay (interactive) or streamed tail updates | Tool errors become `toolResult.isError` | +| Print mode tool call | `BashTool.execute` | No (no UI context) | No TUI overlay; output appears in event stream/final assistant text flow | Same tool error mapping | +| RPC tool call (agent tooling) | `BashTool.execute` | Usually no UI -> non-PTY | Structured tool events/results | Same tool error mapping | +| Interactive bang command (`!`) | `AgentSession.executeBash` + `BashExecutionComponent` | No (uses executor directly) | Dedicated bash execution component | Controller catches exceptions and shows UI error | +| RPC `bash` command | `rpc-mode` -> `session.executeBash` | No | Returns `BashResult` directly | Consumer handles returned fields | ## Operational caveats @@ -256,7 +259,7 @@ This component is wired by `CommandController.handleBashCommand()` and fed from ## Implementation files - [`src/tools/bash.ts`](../packages/coding-agent/src/tools/bash.ts) — tool entrypoint, input handling/interception, async and PTY/non-PTY selection, result/error mapping, bash tool renderer. -- ~~`src/tools/bash-normalize.ts`~~ — removed (post-run head/tail filtering is now handled inline). +- [`src/tools/bash-command-fixup.ts`](../packages/coding-agent/src/tools/bash-command-fixup.ts) — native-backed conservative cleanup for trailing `head`/`tail` pipes and redundant `2>&1`. - [`src/tools/bash-interceptor.ts`](../packages/coding-agent/src/tools/bash-interceptor.ts) — interceptor rule matching and blocked-command messages. - [`src/exec/bash-executor.ts`](../packages/coding-agent/src/exec/bash-executor.ts) — non-PTY executor, shell session reuse, cancellation wiring, output sink integration. - [`src/tools/bash-interactive.ts`](../packages/coding-agent/src/tools/bash-interactive.ts) — PTY runtime, overlay UI, input normalization, non-interactive env defaults. diff --git a/docs/blob-artifact-architecture.md b/docs/blob-artifact-architecture.md index afbd0088c..9233f3b90 100644 --- a/docs/blob-artifact-architecture.md +++ b/docs/blob-artifact-architecture.md @@ -16,9 +16,9 @@ They are intentionally separate: ## Storage boundaries and on-disk layout -## Blob store boundary (global) +### Blob store boundary (global) -`SessionManager` constructs `BlobStore(getBlobsDir())`, so blob files live in a shared global blob directory (not in a session folder). +`SessionManager` constructs `BlobStore(getBlobsDir())`, so blob files live in a shared global blob directory, not in a session folder. Blob file naming: @@ -43,12 +43,15 @@ Artifact types share this directory: - truncated tool output files: `..log` (for `artifact://`) - subagent output files: `.md` (for `agent://`) +- subagent session JSONL sidecars: `.jsonl` when task execution receives an artifacts directory + +Subagents can adopt the parent `ArtifactManager`; in that case parent and subagent tree share one artifact directory and numeric artifact ID space. ## ID and name allocation schemes -## Blob IDs: content hash +### Blob IDs: content hash -`BlobStore.put()` computes SHA-256 over the bytes it is given and returns: +`BlobStore.put()` / `putSync()` computes SHA-256 over the bytes it is given and returns: - `hash`: hex digest, - `path`: `/`, @@ -56,27 +59,30 @@ Artifact types share this directory: No session-local counter is used. -## Artifact IDs: session-local monotonic integer +### Artifact IDs: session-local monotonic integer -`ArtifactManager` scans existing `*.log` artifact files on first use to find max existing numeric ID and sets `nextId = max + 1`. +`ArtifactManager` scans existing `*.log` artifact files on first directory-backed allocation to find max existing numeric ID and sets `nextId = max + 1`. Allocation behavior: - file format: `{id}.{toolType}.log` - IDs are sequential strings (`"0"`, `"1"`, ...) -- resume does not overwrite existing artifacts because scan happens before allocation. +- resume does not overwrite existing artifacts because scan happens before allocation +- the directory is created lazily on first save/allocation -If artifact directory is missing, scanning yields empty list and allocation starts from `0`. +If the artifact directory is missing, scanning yields an empty list and allocation starts from `0`. -## Agent output IDs (`agent://`) +Non-persistent sessions without an adopted manager can store `saveArtifact(...)` content in memory under numeric IDs, but `artifact://` resolution is file-backed through registered artifact directories. -`AgentOutputManager` allocates IDs for subagent outputs as `-` (optionally nested under parent prefix, e.g. `0-Parent.1-Child`). It scans existing `.md` files on initialization to continue from the next index on resume. +### Agent output IDs (`agent://`) + +`AgentOutputManager` allocates IDs for subagent outputs from the requested name, used verbatim the first time and suffixed (`-2`, `-3`, …) only when the same name repeats (e.g. `Anna`, `Anna-2`). Nested outputs are grouped under the parent prefix (e.g. `Parent.Child`). It scans existing `.md` files on initialization so a resumed session never reuses a name that would clobber a prior output. ## Persistence dataflow -## 1) Session entry persistence rewrite path +### 1) Session entry persistence rewrite path -Before session entries are written (`#rewriteFile` / incremental persist), `SessionManager` calls `prepareEntryForPersistence()` (via `truncateForPersistence`). +Before session entries are written (`#rewriteFile` / incremental persist), `SessionManager` calls `prepareEntryForPersistence()` / `prepareEntryForPersistenceSync()` through the truncation pipeline. Key behaviors: @@ -91,7 +97,7 @@ Key behaviors: This keeps session JSONL compact while preserving recoverability. -## 2) Session load rehydration path +### 2) Session load rehydration path When opening a session (`setSessionFile`), after migrations, `SessionManager` runs `resolveBlobRefsInEntries()`. @@ -102,122 +108,125 @@ For message/custom-message image blocks with `blob:sha256:` and for persis - converts provider `image_url` blobs back to the original string, - mutates in-memory entry fields for runtime consumers. -If blob is missing: +If a blob is missing: -- `resolveImageData()` logs warning, -- returns original ref string unchanged, -- load continues (no hard crash). +- image-block resolution logs a warning and keeps the original `blob:sha256:` ref string in memory, +- provider `image_url` resolution logs a warning and keeps the original ref string, +- load continues. -## 3) Tool output spill/truncation path +### 3) Tool output spill/truncation path `OutputSink` powers streaming output in bash/python/ssh and related executors. Behavior: -1. Every chunk is sanitized and appended to in-memory tail buffer. -2. When in-memory bytes exceed spill threshold (`DEFAULT_MAX_BYTES`, 50KB), sink marks output truncated. -3. If an artifact path is available, sink opens a file writer and writes: - - existing buffered content once, - - all subsequent chunks. -4. In-memory buffer is always trimmed to tail window for display. -5. `dump()` returns summary including `artifactId` only when file sink was successfully created. +1. Every chunk is sanitized with `sanitizeWithOptionalSixelPassthrough(..., sanitizeText)` and appended to in-memory accounting. +2. Optional live `onChunk` receives sanitized pre-column-cap chunks, throttled if configured. +3. A per-line column cap can drop bytes from long lines in the LLM-facing buffer; when this happens, artifact mirroring starts so the on-disk file keeps the full sanitized stream. +4. When the in-memory tail buffer would exceed spill threshold (`DEFAULT_MAX_BYTES`, 50KB), sink marks output truncated and starts artifact mirroring if an artifact path is available. +5. If a file sink is opened, it first writes the current buffer, then all queued/subsequent sanitized chunks. +6. In-memory buffer is trimmed to a tail window, or to head + elision marker + tail when head retention is configured. +7. `dump()` returns summary including `artifactId` only when file sink creation succeeded. Practical effect: -- UI/tool return shows truncated tail, -- full output is preserved in artifact file and referenced as `artifact://`. +- UI/tool return shows bounded output, +- full sanitized output is preserved in artifact file and referenced as `artifact://` when file-backed artifact mirroring succeeded. -If file sink creation fails (I/O error, missing path, etc.), sink silently falls back to in-memory truncation only; full output is not persisted. +If file sink creation fails (I/O error, missing path, etc.), sink falls back to in-memory truncation only; full output is not persisted. ## URL access model -## `blob:` references +### `blob:` references `blob:sha256:` is a persistence reference inside session entry payloads, not an internal URL scheme handled by the router. Resolution is done by `SessionManager` during session load. -## `artifact://` +### `artifact://` -Handled by `ArtifactProtocolHandler`: +Handled by `ArtifactProtocolHandler` over registered active session artifact directories: -- requires active session artifact directory, -- ID must be numeric, -- resolves by matching filename prefix `.`, +- requires a numeric ID, +- searches each registered artifacts directory for filename prefix `.`, - returns raw text (`text/plain`) from the matched `.log` file, -- when missing, error includes list of available artifact IDs. +- when missing, error includes available numeric artifact IDs from existing artifact files. -Missing directory behavior: +Failure behavior: -- if artifacts directory does not exist, throws `No artifacts directory found`. +- if no artifact directories are registered: throws `No session - artifacts unavailable`, +- if registered directories exist but none are present on disk: throws `No artifacts directory found`, +- if ID is not numeric: throws `artifact:// ID must be numeric, got: `. -## `agent://` +### `agent://` -Handled by `AgentProtocolHandler` over `/.md`: +Handled by `AgentProtocolHandler` over registered active session artifact directories and `/.md`: - plain form returns markdown text, - `/path` or `?q=` forms perform JSON extraction, - path and query extraction cannot be combined, - if extraction requested, file content must parse as JSON. -Missing directory behavior: +Failure behavior: -- throws `No artifacts directory found`. - -Missing output behavior: - -- throws `Not found: ` with available IDs from existing `.md` files. +- if no artifact directories are registered: throws `No session - agent outputs unavailable`, +- if registered directories exist but none are present on disk: throws `No artifacts directory found`, +- missing output throws `Not found: ` with available `.md` output IDs when directory listing succeeds. Read tool integration: - `read` supports offset/limit pagination for non-extraction internal URL reads, -- rejects `offset/limit` when `agent://` extraction is used. +- rejects offset/limit when `agent://` extraction is used. ## Resume, fork, and move semantics -## Resume +### Resume - `ArtifactManager` scans existing `{id}.*.log` files on first allocation and continues numbering. - `AgentOutputManager` scans existing `.md` output IDs and continues numbering. -- `SessionManager` rehydrates blob refs to base64 on load. +- `SessionManager` rehydrates blob refs to base64/data URLs on load. -## Fork +### Fork `SessionManager.fork()` creates a new session file with new session ID and `parentSession` link, then returns old/new file paths. Artifact copying is handled by `AgentSession.fork()`: +- flushes current session first, - attempts recursive copy of old artifact directory to new artifact directory, - missing old directory is tolerated, - non-ENOENT copy errors are logged as warnings and fork still completes. ID implications after fork: -- if copy succeeded, artifact counters in new session continue after max copied ID, +- if copy succeeded, artifact counters in the new session continue after max copied ID when the new `ArtifactManager` first scans, - if copy failed/skipped, new session artifact IDs start from `0`. Blob implications after fork: - blobs are global and content-addressed, so no blob directory copy is required. -## Move to new cwd +### Move to new cwd `SessionManager.moveTo()` renames both session file and artifact directory to the new default session directory, with rollback logic if a later step fails. This preserves artifact identity while relocating session scope. ## Failure handling and fallback paths -| Case | Behavior | -| -------------------------------------------------------- | --------------------------------------------------------------------- | -| Blob file missing during rehydration | Warn and keep `blob:sha256:` ref string in-memory | -| Blob read ENOENT via `BlobStore.get` | Returns `null` | -| Artifact directory missing (`ArtifactManager.listFiles`) | Returns empty list (allocation can start fresh) | -| Artifact directory missing (`artifact://` / `agent://`) | Throws explicit `No artifacts directory found` | -| Artifact ID not found | Throws with available IDs listing | -| OutputSink artifact writer init fails | Continues with tail-only truncation (no full-output artifact) | -| No session file (some task paths) | Task tool falls back to temp artifacts directory for subagent outputs | +| Case | Behavior | +| --------------------------------------------------------- | -------------------------------------------------------------------- | +| Blob file missing during image-block rehydration | Warn and keep `blob:sha256:` ref string in memory | +| Blob file missing during provider `image_url` rehydration | Warn and keep `blob:sha256:` ref string in memory | +| Blob read ENOENT via `BlobStore.get` | Returns `null` | +| Artifact directory missing (`ArtifactManager.listFiles`) | Returns empty list (allocation can start fresh) | +| No registered artifact dirs (`artifact://`) | Throws `No session - artifacts unavailable` | +| No registered artifact dirs (`agent://`) | Throws `No session - agent outputs unavailable` | +| Registered artifact dirs missing on disk | Throws explicit `No artifacts directory found` | +| Artifact ID not found | Throws with available IDs listing | +| OutputSink artifact writer init fails | Continues with bounded in-memory output only | +| Non-persistent `saveArtifact` | Stores text in `SessionManager` memory map; not file-backed URL data | ## Binary blob externalization vs text-output artifacts - **Blob externalization** is for image payloads inside persisted session entry content and provider image data URLs; it replaces inline payload strings in JSONL with stable content refs. -- **Artifacts** are plain text files for execution output and subagent output; they are addressable by session-local IDs through internal URLs. +- **Artifacts** are plain text files for execution output and subagent output; file-backed artifacts are addressable by session-local IDs through internal URLs. -The two systems intersect only indirectly (both reduce session JSONL bloat) but have different identity, lifetime, and retrieval paths. +The two systems intersect only indirectly: both reduce session JSONL bloat, but they have different identity, lifetime, and retrieval paths. ## Implementation files @@ -228,6 +237,6 @@ The two systems intersect only indirectly (both reduce session JSONL bloat) but - [`src/session/agent-session.ts`](../packages/coding-agent/src/session/agent-session.ts) — artifact directory copy during interactive fork. - [`src/internal-urls/artifact-protocol.ts`](../packages/coding-agent/src/internal-urls/artifact-protocol.ts) — `artifact://` resolver. - [`src/internal-urls/agent-protocol.ts`](../packages/coding-agent/src/internal-urls/agent-protocol.ts) — `agent://` resolver + JSON extraction. -- [`src/sdk.ts`](../packages/coding-agent/src/sdk.ts) — internal URL router wiring and artifacts-dir resolver. +- [`src/internal-urls/router.ts`](../packages/coding-agent/src/internal-urls/router.ts) — internal URL router wiring. - [`src/task/output-manager.ts`](../packages/coding-agent/src/task/output-manager.ts) — session-scoped agent output ID allocation for `agent://`. -- [`src/task/executor.ts`](../packages/coding-agent/src/task/executor.ts) — subagent output artifact writes (`.md`) and temp artifact directory fallback. +- [`src/task/executor.ts`](../packages/coding-agent/src/task/executor.ts) — subagent output artifact writes (`.md`) and session JSONL sidecars. diff --git a/docs/compaction.md b/docs/compaction.md index 118254a65..2fb5a22e9 100644 --- a/docs/compaction.md +++ b/docs/compaction.md @@ -53,12 +53,13 @@ Those custom roles are then transformed into LLM-facing user messages in `conver ### Triggers -Compaction/context maintenance can run in four ways: +Compaction/context maintenance can run in five ways: 1. **Manual context compaction**: `/compact [instructions]` calls `AgentSession.compact(...)`. 2. **Automatic overflow recovery**: after a same-model assistant error that matches context overflow. -3. **Automatic threshold maintenance**: after a successful turn when context exceeds the resolved threshold. -4. **Idle maintenance**: `runIdleCompaction()` can invoke the same auto-maintenance path with reason `"idle"`. +3. **Automatic incomplete-output recovery**: after a same-model assistant message ends with `stopReason === "length"` (OpenAI/Codex `response.incomplete`). +4. **Automatic threshold maintenance**: after a successful turn when context exceeds the resolved threshold. +5. **Idle maintenance**: `runIdleCompaction()` can invoke the same auto-maintenance path with reason `"idle"`. ### Compaction shape (visual) @@ -94,7 +95,7 @@ What the LLM sees: prompt from cmp messages from firstKeptEntryId ``` -### Overflow-retry vs threshold/idle maintenance +### Overflow/incomplete recovery vs threshold/idle maintenance The automatic paths are intentionally different: @@ -102,15 +103,23 @@ The automatic paths are intentionally different: - Trigger: current-model assistant error is detected as context overflow and the error is not older than the latest compaction. - The failing assistant error message is removed from active agent state before retry. - Context promotion is tried first; if a configured larger model is available, the agent switches model and retries without compacting. - - If promotion is unavailable and compaction is enabled, context-full compaction runs with `reason: "overflow"` and `willRetry: true`; handoff strategy is not used for overflow. - - On success, agent auto-continues (`agent.continue()`) after compaction. + - If promotion is unavailable and compaction is enabled, context-full compaction runs with `reason: "overflow"` and `willRetry: true`; handoff strategy is not used for overflow because the handoff request would reuse the overflowing input. + - On success, `agent.continue()` is scheduled to retry the turn. + +- **Incomplete-output recovery** + - Trigger: same-model assistant message ends with `stopReason === "length"` and the message is not older than the latest compaction. + - The incomplete assistant message is removed from active agent state before recovery. + - Context promotion is tried first. + - If promotion is unavailable and compaction is enabled, auto maintenance runs with `reason: "incomplete"` and `willRetry: true`. + - Unlike overflow, `compaction.strategy: "handoff"` is allowed for incomplete-output recovery because the input context is still usable. + - On context-full success, `agent.continue()` is scheduled to retry the turn. - **Threshold maintenance** - Trigger: successful, non-error assistant message whose adjusted context tokens exceed `resolveThresholdTokens(...)`. - Tool-output pruning can reduce the measured token count before threshold comparison. - Context promotion is tried before compaction. - If promotion is unavailable, auto maintenance runs with `reason: "threshold"` and `willRetry: false`. - - With `compaction.strategy: "handoff"`, threshold maintenance starts a new handoff session instead of writing a compaction entry; if handoff returns no document without aborting, it falls back to context-full compaction. + - With `compaction.strategy: "handoff"`, threshold maintenance normally schedules a post-prompt auto-handoff task instead of writing a compaction entry; pre-prompt checks run it inline to avoid racing the next turn. If handoff returns no document without aborting, it falls back to context-full compaction. - On success, if `compaction.autoContinue !== false`, schedules an agent-authored developer auto-continue prompt from `prompts/system/auto-continue.md`. - **Idle maintenance** @@ -188,7 +197,7 @@ Final stored summary is merged as: 2. Serialize with `serializeConversation()`. 3. Wrap in `...`. 4. Optionally include `...`. -5. Optionally inject hook context as `` list. +5. Optionally inject extension hook context and active memory-backend compaction context as `` entries. 6. Execute summarization prompt with `SUMMARIZATION_SYSTEM_PROMPT`. Prompt selection: @@ -244,7 +253,8 @@ After summary generation (or hook-provided summary), agent session: 1. Appends `CompactionEntry` with `appendCompaction(...)` for context-full maintenance; handoff strategy creates a new session and injects a handoff `custom_message` instead. 2. Rebuilds display context from the active leaf via `buildDisplaySessionContext()`. 3. Replaces live agent messages with rebuilt context. -4. Emits `session_compact` hook event. +4. Synchronizes active todo phases from the rebuilt branch and closes provider sessions whose history was rewritten. +5. Emits `session_compact` hook event. ## Branch summarization pipeline @@ -348,13 +358,14 @@ Post-navigation event exposing new/old leaf and optional summary entry. ## Runtime behavior and failure semantics - Manual compaction aborts current agent operation first. -- `abortCompaction()` cancels both manual and auto-compaction controllers. +- `abortCompaction()` cancels manual compaction, auto-compaction, and handoff generation controllers. - Auto compaction emits start/end session events for UI/state updates. - Auto compaction can try multiple model candidates and retry transient failures; long retry delays prefer the next candidate when one is available. - Overflow errors are excluded from generic retry path because they are handled by context promotion/compaction. - If auto-compaction fails: - overflow path emits `Context overflow recovery failed: ...` - - threshold path emits `Auto-compaction failed: ...` + - incomplete-output path emits `Incomplete response recovery failed: ...` + - threshold/idle paths emit `Auto-compaction failed: ...` - Branch summarization can be cancelled via abort signal (e.g., Escape), returning canceled/aborted navigation result. ## Settings and defaults @@ -369,7 +380,9 @@ From `settings-schema.ts`: - `compaction.remoteEnabled` = `true` - `compaction.remoteEndpoint` = `undefined` - `compaction.thresholdPercent` = `-1` and `compaction.thresholdTokens` = `-1`; when no positive override is set, the threshold is `contextWindow - max(15% of contextWindow, reserveTokens)` -- `compaction.idleEnabled` = `true` +- `compaction.idleEnabled` = `false` +- `compaction.idleThresholdTokens` = `200000` +- `compaction.idleTimeoutSeconds` = `300` - `branchSummary.enabled` = `false` - `branchSummary.reserveTokens` = `16384` diff --git a/docs/config-usage.md b/docs/config-usage.md index 561a73753..f2d778214 100644 --- a/docs/config-usage.md +++ b/docs/config-usage.md @@ -137,7 +137,7 @@ Legacy migration still supported: The runtime settings model is layered: 1. Global settings: `~/.omp/agent/config.yml` -2. Project settings: discovered via settings capability (`settings.json` from providers) +2. Project settings: discovered via settings capability (`settings.json` and `config.yml` from providers) 3. Runtime overrides: in-memory, non-persistent 4. Schema defaults: from `SETTINGS_SCHEMA` @@ -217,7 +217,7 @@ Native provider (`id: native`) reads native config from: - Slash commands, rules, prompts, instructions, hooks, tools, extensions, extension modules, and settings use a project/user root only when the root directory exists and is non-empty. - Skills scan `/.omp/skills` for each ancestor from the current working directory up to the repo root/home boundary, plus `~/.omp/agent/skills`, without requiring the root `.omp` directory itself to be non-empty. -- `SYSTEM.md` and `AGENTS.md` read user-level files directly and use nearest-ancestor project `.omp` lookup for project files, but the project `.omp` directory must be non-empty. +- `SYSTEM.md` and `AGENTS.md` read user-level files directly and use nearest-ancestor project `.omp` lookup for project files, but the project `.omp` directory must be non-empty. See [`docs/system-prompt-customization.md`](./system-prompt-customization.md) for the full `SYSTEM.md` / `APPEND_SYSTEM.md` contract (replace vs. append, templating). ### Scope-specific loading @@ -230,7 +230,7 @@ Native provider (`id: native`) reads native config from: - Tools: `tools/*.{json,md,ts,js,sh,bash,py}` and `tools//index.ts` - Extension modules: discovered under `extensions/` (+ legacy `settings.json.extensions` string array) - Extensions: `extensions//gemini-extension.json` -- Settings capability: `settings.json` +- Settings capability: `settings.json`, then `config.yml` ### Nearest-project lookup nuance @@ -240,7 +240,7 @@ Native provider (`id: native`) reads native config from: ## Settings subsystem -- `Settings.init()` loads global `config.yml` + discovered project `settings.json` capability items. +- `Settings.init()` loads global `config.yml` + discovered project settings capability items. - Only capability items with `level === "project"` are merged into project layer. ## Skills subsystem @@ -285,7 +285,7 @@ Settings capability items are not deduplicated; `Settings.#loadProjectSettings() - `ConfigFile` JSON -> YAML migration for YAML-targeted files. - Settings migration from `settings.json` and `agent.db` to `config.yml`. -- Settings key migrations (`queueMode`, `ask.timeout`, flat `theme`, `task.isolation.enabled`, `statusLine.plan_mode`). +- Settings key migrations include `queueMode`, `ask.timeout`, flat `theme`, `task.isolation.enabled`, legacy `task.isolation.mode` values, removed edit modes, `statusLine.plan_mode`, `memories.enabled`, and hindsight scoping/name fields. - Legacy setting names `skills.enablePiUser` / `skills.enablePiProject` are still active gates for native skill source. If these compatibility paths are removed in code, update this document immediately; several runtime behaviors still depend on them today. diff --git a/docs/custom-tools.md b/docs/custom-tools.md index 46b85541b..775be04a8 100644 --- a/docs/custom-tools.md +++ b/docs/custom-tools.md @@ -67,39 +67,43 @@ A custom tool module must export a function (default export preferred): import type { CustomToolFactory } from "@oh-my-pi/pi-coding-agent"; const factory: CustomToolFactory = (pi) => ({ - name: "repo_stats", - label: "Repo Stats", - description: "Counts tracked TypeScript files", - parameters: pi.zod.object({ - glob: pi.zod.string().optional().default("**/*.ts"), - }), + name: "repo_stats", + label: "Repo Stats", + description: "Counts tracked TypeScript files", + parameters: pi.zod.object({ + glob: pi.zod.string().optional().default("**/*.ts"), + }), - async execute(toolCallId, params, onUpdate, ctx, signal) { - onUpdate?.({ - content: [{ type: "text", text: "Scanning files..." }], - details: { phase: "scan" }, - }); + async execute(toolCallId, params, onUpdate, ctx, signal) { + onUpdate?.({ + content: [{ type: "text", text: "Scanning files..." }], + details: { phase: "scan" }, + }); - const result = await pi.exec("git", ["ls-files", params.glob ?? "**/*.ts"], { signal, cwd: pi.cwd }); - if (result.killed) { - throw new Error("Scan was cancelled"); - } - if (result.code !== 0) { - throw new Error(result.stderr || "git ls-files failed"); - } + const result = await pi.exec( + "git", + ["ls-files", params.glob ?? "**/*.ts"], + { signal, cwd: pi.cwd }, + ); + if (result.killed) { + throw new Error("Scan was cancelled"); + } + if (result.code !== 0) { + throw new Error(result.stderr || "git ls-files failed"); + } - const files = result.stdout.split("\n").filter(Boolean); - return { - content: [{ type: "text", text: `Found ${files.length} files` }], - details: { count: files.length, sample: files.slice(0, 10) }, - }; - }, + const files = result.stdout.split("\n").filter(Boolean); + return { + content: [{ type: "text", text: `Found ${files.length} files` }], + details: { count: files.length, sample: files.slice(0, 10) }, + }; + }, - onSession(event) { - if (event.reason === "shutdown") { - // cleanup resources if needed - } - }, + onSession(event) { + if (event.reason === "shutdown") { + // cleanup resources if needed + } + }, }); export default factory; @@ -122,11 +126,11 @@ From `types.ts` and `loader.ts`: - `ui`: UI context (can be no-op in headless modes) - `hasUI`: `false` in non-interactive flows - `logger`: shared file logger -- `zod`: injected `zod` module (use `pi.zod.object`, `pi.zod.string`, …) +- `typebox`: zod-backed compatibility shim for legacy TypeBox-style schemas +- `zod`: injected `zod/v4` module (canonical for new schemas) - `pi`: injected `@oh-my-pi/pi-coding-agent` exports - `pushPendingAction(action)`: register a preview action for hidden `resolve` tool (`docs/resolve-tool-runtime.md`) - -Loader starts with a no-op UI context and requires host code to call `setUIContext(...)` when real UI is ready. + Loader starts with a no-op UI context and requires host code to call `setUIContext(...)` when real UI is ready. ## Execution contract and typing @@ -136,14 +140,16 @@ Loader starts with a no-op UI context and requires host code to call `setUIConte execute(toolCallId, params, onUpdate, ctx, signal); ``` -- `params` is statically typed from your Zod schema via `z.infer` (`Static` in API types). +- `params` is statically typed from your Zod/TypeBox schema via `Static`. - Runtime argument validation happens before execution in the agent loop. - `onUpdate` emits partial results for UI streaming. -- `ctx` includes session/model state and an `abort()` helper. +- `ctx` includes `sessionManager`, `modelRegistry`, current `model`, `isIdle()`, `hasQueuedMessages()`, `abort()`, and optional `settings` / `autoApprove`. - `signal` carries cancellation. `CustomToolAdapter` bridges this to the agent tool interface and forwards calls in the correct argument order. +Tool definitions may also declare `strict`, `hidden`, `deferrable`, `mcpServerName`, `mcpToolName`, `approval`, and `formatApprovalDetails`. + ## How tools are exposed to the model - Tools are wrapped into `AgentTool` instances (`CustomToolAdapter` or extension wrappers). diff --git a/docs/environment-variables.md b/docs/environment-variables.md index dd472006c..66398a220 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -41,6 +41,7 @@ These are consumed via `getEnvApiKey()` (`packages/ai/src/stream.ts`) unless not | `GROQ_API_KEY` | Groq auth | Using Groq models | | | `CEREBRAS_API_KEY` | Cerebras auth | Using Cerebras models | | | `FIREWORKS_API_KEY` | Fireworks auth | Using Fireworks models | | +| `FIREPASS_API_KEY` | Fire Pass auth | Using Fire Pass models | | | `TOGETHER_API_KEY` | Together auth | Using `together` provider | | | `HUGGINGFACE_HUB_TOKEN` | Hugging Face auth | Using `huggingface` provider | Primary Hugging Face token env var | | `HF_TOKEN` | Hugging Face auth | Using `huggingface` provider | Fallback when `HUGGINGFACE_HUB_TOKEN` is unset | @@ -54,10 +55,12 @@ These are consumed via `getEnvApiKey()` (`packages/ai/src/stream.ts`) unless not | `LLAMA_CPP_API_KEY` | llama.cpp auth (optional) | Using `llama.cpp` provider with authenticated hosts | Local llama.cpp usually runs without auth; any non-empty token works when a key is configured | | `XIAOMI_API_KEY` | Xiaomi MiMo auth | Using `xiaomi` provider | | | `MOONSHOT_API_KEY` | Moonshot auth | Using `moonshot` provider | | -| `XAI_API_KEY` | xAI auth | Using xAI models | | +| `XAI_API_KEY` | xAI auth | Using xAI models or as fallback for `xai-oauth` | | +| `XAI_OAUTH_TOKEN` | xAI OAuth/SuperGrok auth | Using `xai-oauth` provider | Takes precedence over `XAI_API_KEY` for `xai-oauth` | | `OPENROUTER_API_KEY` | OpenRouter auth | Using OpenRouter models | Also used by image tool when preferred/auto provider is OpenRouter | | `MISTRAL_API_KEY` | Mistral auth | Using Mistral models | | | `ZAI_API_KEY` | z.ai auth | Using z.ai models | Also used by z.ai web search provider | +| `ZHIPU_API_KEY` | Zhipu Coding Plan auth | Using `zhipu-coding-plan` provider | | | `MINIMAX_API_KEY` | MiniMax auth | Using `minimax` provider | | | `MINIMAX_CODE_API_KEY` | MiniMax Code auth | Using `minimax-code` provider | | | `MINIMAX_CODE_CN_API_KEY` | MiniMax Code CN auth | Using `minimax-code-cn` provider | | @@ -90,10 +93,10 @@ These are consumed via `getEnvApiKey()` (`packages/ai/src/stream.ts`) unless not When the broker is enabled, the local SQLite credential store is bypassed and all OAuth refresh / access tokens live on the broker host. See [`auth-broker-gateway.md`](./auth-broker-gateway.md) for the full protocol, CLI surface, and 5-min/15-s usage cache layering. -| Variable | Used for | Required when | Notes / precedence | -| ----------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `OMP_AUTH_BROKER_URL` | Base URL of the remote auth-broker (e.g. `https://broker.tailnet:8765`); selects broker mode | Resolving credentials through a broker; also required by `omp auth-gateway serve` (the gateway is itself a broker client) | Wins over `auth.broker.url` in `config.yml`. When set with no resolvable token, `resolveAuthBrokerConfig()` hard-errors instead of falling back to local SQLite. | -| `OMP_AUTH_BROKER_TOKEN` | Bearer token sent on every broker endpoint except `/v1/healthz` | `OMP_AUTH_BROKER_URL` is set and no token is available from `auth.broker.token` or `/auth-broker.token` | Resolution: this env → `auth.broker.token` (`$ENV_NAME` indirection supported) → `/auth-broker.token` (mode `0600`). `` is `~/.omp/` (respecting `PI_CONFIG_DIR`). | +| Variable | Used for | Required when | Notes / precedence | +| ----------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `OMP_AUTH_BROKER_URL` | Base URL of the remote auth-broker (e.g. `https://broker.tailnet:8765`); selects broker mode | Resolving credentials through a broker; also required by `omp auth-gateway serve` (the gateway is itself a broker client) | Wins over `auth.broker.url` in `config.yml`. When set with no resolvable token, `resolveAuthBrokerConfig()` hard-errors instead of falling back to local SQLite. | +| `OMP_AUTH_BROKER_TOKEN` | Bearer token sent on every broker endpoint except `/v1/healthz` | `OMP_AUTH_BROKER_URL` is set and no token is available from `auth.broker.token` or `/auth-broker.token` | Resolution: this env → `auth.broker.token` (`$ENV_NAME` indirection supported) → `/auth-broker.token` (mode `0600`). `` is `~/.omp/` (respecting `PI_CONFIG_DIR`). | The gateway has no dedicated env vars — it inherits `OMP_AUTH_BROKER_*`. Its own inbound bearer token lives at `/auth-gateway.token` and is managed via `omp auth-gateway token`. @@ -108,7 +111,11 @@ When `CLAUDE_CODE_USE_FOUNDRY` is enabled, Anthropic requests switch to Foundry - Base URL resolves from `FOUNDRY_BASE_URL` (fallback remains model/default base URL if unset). - API key resolution for provider `anthropic` becomes: `ANTHROPIC_FOUNDRY_API_KEY` → `ANTHROPIC_OAUTH_TOKEN` → `ANTHROPIC_API_KEY`. -- `ANTHROPIC_CUSTOM_HEADERS` is parsed as comma/newline-separated `key: value` pairs and merged into request headers. +- `ANTHROPIC_CUSTOM_HEADERS` is parsed as comma/newline-separated `key: value` + pairs and merged into request headers. They are also forwarded when + `ANTHROPIC_BASE_URL` points to a non-Anthropic host (e.g. a corporate API + gateway), so enterprise gateways requiring proprietary auth headers work + without enabling Foundry mode. - TLS client/server material can be injected from env values: `NODE_EXTRA_CA_CERTS`, `CLAUDE_CODE_CLIENT_CERT`, `CLAUDE_CODE_CLIENT_KEY`. Each accepts either: @@ -120,7 +127,7 @@ When `CLAUDE_CODE_USE_FOUNDRY` is enabled, Anthropic requests switch to Foundry | `CLAUDE_CODE_USE_FOUNDRY` | Boolean-like string (`1`, `true`, `yes`, `on`) | Enables Foundry mode for Anthropic provider | | `FOUNDRY_BASE_URL` | URL string | Anthropic endpoint base URL in Foundry mode | | `ANTHROPIC_FOUNDRY_API_KEY` | Token string | Used for `Authorization: Bearer ` | -| `ANTHROPIC_CUSTOM_HEADERS` | Header list string | Extra headers; format `header-a: value, header-b: value` or newline-separated | +| `ANTHROPIC_CUSTOM_HEADERS` | Header list string | Extra headers; format `header-a: value, header-b: value` or newline-separated. Also forwarded outside Foundry whenever `ANTHROPIC_BASE_URL` is non-Anthropic. | | `NODE_EXTRA_CA_CERTS` | PEM path or inline PEM | Extra CA chain for server certificate validation | | `CLAUDE_CODE_CLIENT_CERT` | PEM path or inline PEM | mTLS client certificate | | `CLAUDE_CODE_CLIENT_KEY` | PEM path or inline PEM | mTLS client private key (must be paired with cert) | @@ -133,7 +140,7 @@ When `CLAUDE_CODE_USE_FOUNDRY` is enabled, Anthropic requests switch to Foundry | `AWS_DEFAULT_REGION` | Fallback if `AWS_REGION` unset | | `AWS_PROFILE` | Enables named profile auth path | | `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` | Enables IAM key auth path | -| `AWS_BEARER_TOKEN_BEDROCK` | Highest-precedence bearer token auth path; skips AWS profile/credential-chain lookup when set | +| `AWS_BEARER_TOKEN_BEDROCK` | Highest-precedence bearer token auth path; skips AWS profile/credential-chain lookup when set | | `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` / `AWS_CONTAINER_CREDENTIALS_FULL_URI` | Enables ECS task credential path | | `AWS_WEB_IDENTITY_TOKEN_FILE` + `AWS_ROLE_ARN` | Enables web identity auth path | | `AWS_BEDROCK_SKIP_AUTH` | If `1`, injects dummy credentials (proxy/non-auth scenarios) | @@ -159,10 +166,13 @@ Base URL resolution: option `azureBaseUrl` → env `AZURE_OPENAI_BASE_URL` → o | Variable | Required? | Notes | | -------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------- | -| `GOOGLE_CLOUD_PROJECT` | Yes (unless passed in options) | Fallback: `GCLOUD_PROJECT` | -| `GCLOUD_PROJECT` | Fallback | Used as alternate project ID source | +| `GOOGLE_CLOUD_PROJECT` | Yes (unless passed in options) | Primary project ID source | +| `GCP_PROJECT` | Fallback | Alternate project ID source | +| `GCLOUD_PROJECT` | Fallback | Alternate project ID source | | `GOOGLE_CLOUD_PROJECT_ID` | OAuth login helper only | Used by Gemini CLI OAuth project discovery | -| `GOOGLE_CLOUD_LOCATION` | Yes (unless passed in options) | No default in provider | +| `GOOGLE_VERTEX_LOCATION` | Yes (unless passed in options) | Primary Vertex location source | +| `GOOGLE_CLOUD_LOCATION` | Fallback | Alternate Vertex location source | +| `VERTEX_LOCATION` | Fallback | Alternate Vertex location source | | `GOOGLE_CLOUD_API_KEY` | Conditional | Direct Vertex API-key auth; otherwise ADC fallback can authenticate when project and location are set | | `GOOGLE_APPLICATION_CREDENTIALS` | Conditional | If set, file must exist; otherwise ADC fallback path is checked (`~/.config/gcloud/application_default_credentials.json`) | @@ -184,15 +194,16 @@ OAuth host chain: `KIMI_CODE_OAUTH_HOST` → `KIMI_OAUTH_HOST` → `https://auth ### OpenAI Codex responses (feature/debug controls) -| Variable | Behavior | -| ------------------------------------ | ---------------------------------------------------- | -| `PI_CODEX_DEBUG` | `1`/`true` enables Codex provider debug logging | -| `PI_CODEX_WEBSOCKET` | `1`/`true` enables websocket transport preference | -| `PI_CODEX_WEBSOCKET_V2` | `1`/`true` enables websocket v2 path | -| `PI_CODEX_WEBSOCKET_IDLE_TIMEOUT_MS` | Positive integer override (default 300000) | -| `PI_CODEX_WEBSOCKET_RETRY_BUDGET` | Non-negative integer override (default 5) | -| `PI_CODEX_WEBSOCKET_RETRY_DELAY_MS` | Positive integer base backoff override (default 500) | -| `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` | Positive integer OpenAI stream idle timeout override | +| Variable | Behavior | +| ------------------------------------------ | ---------------------------------------------------- | +| `PI_CODEX_DEBUG` | `1`/`true` enables Codex provider debug logging | +| `PI_CODEX_WEBSOCKET` | `1`/`true` enables websocket transport preference | +| `PI_CODEX_WEBSOCKET_V2` | `1`/`true` enables websocket v2 path | +| `PI_CODEX_WEBSOCKET_IDLE_TIMEOUT_MS` | Positive integer override (default 300000) | +| `PI_CODEX_WEBSOCKET_RETRY_BUDGET` | Non-negative integer override (default 5) | +| `PI_CODEX_WEBSOCKET_RETRY_DELAY_MS` | Positive integer base backoff override (default 500) | +| `PI_OPENAI_STREAM_FIRST_EVENT_TIMEOUT_MS` | Positive integer OpenAI first-event timeout override | +| `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` | Positive integer OpenAI stream idle timeout override | ### Cursor provider debug @@ -235,22 +246,28 @@ SearXNG also reads the equivalent `searxng.endpoint`, `searxng.token`, `searxng. ### Anthropic web search auth chain -Anthropic web search uses `findAnthropicAuth()` from `packages/ai/src/utils/anthropic-auth.ts` in this order: +`searchAnthropic()` resolves credentials in this order: -1. `ANTHROPIC_SEARCH_API_KEY` (+ optional `ANTHROPIC_SEARCH_BASE_URL`) -2. `ANTHROPIC_FOUNDRY_API_KEY` when `CLAUDE_CODE_USE_FOUNDRY` is enabled -3. Anthropic OAuth credentials from `agent.db` (must not expire within 5-minute buffer) -4. Anthropic API-key credentials from `agent.db` -5. Generic Anthropic env fallback: provider key (`ANTHROPIC_FOUNDRY_API_KEY` in Foundry mode, otherwise `ANTHROPIC_OAUTH_TOKEN`/`ANTHROPIC_API_KEY`) + optional `ANTHROPIC_BASE_URL` (`FOUNDRY_BASE_URL` when Foundry mode is enabled) +1. `ANTHROPIC_SEARCH_API_KEY` +2. `authStorage.getApiKey("anthropic")` fallback credentials (runtime/config overrides, stored API-key credentials, stored OAuth credentials, then generic Anthropic env fallback: `ANTHROPIC_FOUNDRY_API_KEY` in Foundry mode, otherwise `ANTHROPIC_OAUTH_TOKEN` / `ANTHROPIC_API_KEY`) + +For either credential path, base URL resolution is: + +1. `ANTHROPIC_SEARCH_BASE_URL` +2. `FOUNDRY_BASE_URL` when `CLAUDE_CODE_USE_FOUNDRY` is enabled +3. `ANTHROPIC_BASE_URL` +4. `https://api.anthropic.com` Related vars: -| Variable | Default / behavior | -| --------------------------- | ---------------------------------------------------- | -| `ANTHROPIC_SEARCH_API_KEY` | Highest-priority explicit search key | -| `ANTHROPIC_SEARCH_BASE_URL` | Defaults to `https://api.anthropic.com` when omitted | -| `ANTHROPIC_SEARCH_MODEL` | Defaults to `claude-haiku-4-5` | -| `ANTHROPIC_BASE_URL` | Generic fallback base URL for tier-4 auth path | +| Variable | Default / behavior | +| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ANTHROPIC_SEARCH_API_KEY` | API key used exclusively for the Anthropic web search provider. Highest-priority search auth; overrides `ANTHROPIC_API_KEY` / OAuth / Foundry for search calls without affecting chat completions. | +| `ANTHROPIC_SEARCH_BASE_URL` | Base URL used exclusively for the Anthropic web search provider. Applied to either `ANTHROPIC_SEARCH_API_KEY` or fallback Anthropic credentials; overrides `ANTHROPIC_BASE_URL` (and `FOUNDRY_BASE_URL` in Foundry mode) for search calls. | +| `ANTHROPIC_SEARCH_MODEL` | Search model override. Defaults to `claude-haiku-4-5`. | +| `ANTHROPIC_BASE_URL` | Generic fallback base URL for Anthropic requests when no search-specific base URL is set. | + +Use `ANTHROPIC_SEARCH_BASE_URL` (optionally with `ANTHROPIC_SEARCH_API_KEY`) to keep chat routed through an enterprise gateway (`ANTHROPIC_BASE_URL` or `CLAUDE_CODE_USE_FOUNDRY=true`) while pointing web search at a direct Anthropic endpoint, or vice versa. ### Perplexity OAuth flow behavior flag @@ -262,13 +279,13 @@ Related vars: ## 4) Python tooling and kernel runtime -| Variable | Default / behavior | -| ------------------------- | ------------------------------------------------------------------------------------------------------------------- | -| `PI_PY` | Eval backend override: `0`/`bash`=JavaScript only, `1`/`py`=Python only, `mix`/`both`=both; invalid values ignored | -| `PI_PYTHON_SKIP_CHECK` | If `1`, skips Python interpreter availability checks (subprocess runner still starts on demand) | -| `PI_PYTHON_INTEGRATION` | If `1`, opts gated integration tests in (e.g. `python-runner.integration.test.ts`) into running against real Python | -| `PI_PYTHON_IPC_TRACE` | If `1`, logs NDJSON frames exchanged with the Python runner subprocess | -| `VIRTUAL_ENV` | Highest-priority venv path for Python runtime resolution | +| Variable | Default / behavior | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------- | +| `PI_PY` | Eval backend override: `0`/`bash`=JavaScript only, `1`/`py`=Python only, `mix`/`both`=both; invalid values ignored | +| `PI_PYTHON_SKIP_CHECK` | If `1`, skips Python interpreter availability checks (subprocess runner still starts on demand) | +| `PI_PYTHON_INTEGRATION` | If `1`, opts gated integration tests in (e.g. `python-runner.integration.test.ts`) into running against real Python | +| `PI_PYTHON_IPC_TRACE` | If `1`, logs NDJSON frames exchanged with the Python runner subprocess | +| `VIRTUAL_ENV` | Highest-priority venv path for Python runtime resolution | Extra conditional behavior: @@ -279,32 +296,36 @@ Extra conditional behavior: ## 5) Agent/runtime behavior toggles -| Variable | Default / behavior | -| ---------------------------- | -------------------------------------------------------------------------------------------------- | -| `PI_SMOL_MODEL` | Ephemeral model-role override for `smol` (CLI `--smol` takes precedence) | -| `PI_SLOW_MODEL` | Ephemeral model-role override for `slow` (CLI `--slow` takes precedence) | -| `PI_PLAN_MODEL` | Ephemeral model-role override for `plan` (CLI `--plan` takes precedence) | -| `PI_NO_TITLE` | If set (any non-empty value), disables auto session title generation on first user message | -| `NULL_PROMPT` | If `true`, system prompt builder returns empty string | -| `PI_BLOCKED_AGENT` | Blocks a specific subagent type in task tool | -| `PI_SUBPROCESS_CMD` | Overrides subagent spawn command (`omp` / `omp.cmd` resolution bypass) | -| `PI_TASK_MAX_OUTPUT_BYTES` | Max captured output bytes per subagent (default `500000`) | -| `PI_TASK_MAX_OUTPUT_LINES` | Max captured output lines per subagent (default `5000`) | +| Variable | Default / behavior | +| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `PI_SMOL_MODEL` | Ephemeral model-role override for `smol` (CLI `--smol` takes precedence) | +| `PI_SLOW_MODEL` | Ephemeral model-role override for `slow` (CLI `--slow` takes precedence) | +| `PI_PLAN_MODEL` | Ephemeral model-role override for `plan` (CLI `--plan` takes precedence) | +| `PI_NO_TITLE` | If set (any non-empty value), disables auto session title generation on first user message | +| `PI_TINY_DEVICE` | ONNX execution provider for local tiny models; overrides the `providers.tinyModelDevice` setting (default: CPU; supports `cpu`, `gpu`, `metal`/`webgpu`, `auto`, `cuda`, `dml`, `coreml`, `wasm`, `webnn`, `webnn-gpu`, `webnn-cpu`, `webnn-npu`) | +| `PI_TINY_DTYPE` | ONNX quantization/precision for local tiny models; overrides the `providers.tinyModelDtype` setting (default: each model's shipped dtype, currently `q4`; supports `auto`, `fp32`, `fp16`, `q8`, `int8`, `uint8`, `q4`, `bnb4`, `q4f16`, `q2`, `q2f16`, `q1`, `q1f16`) | +| `PI_NO_INTERLEAVED_THINKING` | If `1`, disables Anthropic interleaved thinking budget behavior and uses output-token inflation for older thinking mode | +| `NULL_PROMPT` | If `true`, system prompt builder returns empty string | +| `PI_BLOCKED_AGENT` | Blocks a specific subagent type in task tool | +| `PI_SUBPROCESS_CMD` | Overrides subagent spawn command (`omp` / `omp.cmd` resolution bypass) | +| `PI_TASK_MAX_OUTPUT_BYTES` | Max captured output bytes per subagent (default `500000`) | +| `PI_TASK_MAX_OUTPUT_LINES` | Max captured output lines per subagent (default `5000`) | | `PI_TIMING` | If set (any non-empty value), prints a hierarchical timing-span tree to **stderr** via `logger.printTimings()`. In interactive mode the tree prints once the agent is ready (before the TUI starts); in print mode it prints after the whole prompt batch completes. Print-mode prompts are wrapped in `print:prompt:initial` / `print:prompt:next` spans so each user message shows up as its own row. `PI_TIMING=x` exits the process with code 0 right after printing in interactive mode (use to measure cold startup only). `PI_TIMING=full` lists every module-load entry instead of just the top N. | -| `PI_PACKAGE_DIR` | Overrides package asset base dir resolution (docs/examples/changelog path lookup) | -| `PI_DISABLE_LSPMUX` | If `1`, disables lspmux detection/integration and forces direct LSP server spawning | -| `PI_RPC_EMIT_TITLE` | Boolean-like flag enabling title events in RPC mode | -| `SMITHERY_URL` | Smithery web URL override (default `https://smithery.ai`) | -| `SMITHERY_API_URL` | Smithery API base URL override (default `https://api.smithery.ai`) | -| `PUPPETEER_EXECUTABLE_PATH` | Browser tool Chromium executable override | -| `LM_STUDIO_BASE_URL` | Default implicit LM Studio discovery base URL override (`http://127.0.0.1:1234/v1` if unset) | -| `OLLAMA_BASE_URL` | Default implicit Ollama discovery base URL override (`http://127.0.0.1:11434` if unset) | -| `LLAMA_CPP_BASE_URL` | Default implicit Llama.cpp discovery base URL override (`http://127.0.0.1:8080` if unset) | -| `PI_EDIT_VARIANT` | Forces edit tool variant when valid (`patch`, `replace`, `hashline`, `apply_patch`) | -| `PI_FORCE_IMAGE_PROTOCOL` | Forces supported image protocol (`kitty`, `iterm2`/`iterm`, `sixel`, `none`) where used | -| `PI_ALLOW_SIXEL_PASSTHROUGH` | Allows SIXEL passthrough when `PI_FORCE_IMAGE_PROTOCOL=sixel` | -| `PI_NO_PTY` | If `1`, disables interactive PTY path for bash tool | -| `OMP_MCP_TIMEOUT_MS` | Overrides MCP client request timeout (ms) for every MCP server. `0` disables client-side timeouts (`AbortSignal` never fires). Invalid (negative or non-numeric) values are ignored with a warning and the per-server config or default (`30000`) is used. | +| `PI_PACKAGE_DIR` | Overrides package asset base dir resolution (`docs/`, `examples/`, `CHANGELOG.md`) | +| `PI_DISABLE_LSPMUX` | If `1`, disables lspmux detection/integration and forces direct LSP server spawning | +| `PI_RPC_EMIT_TITLE` | Boolean-like flag enabling title events in RPC mode | +| `SMITHERY_URL` | Smithery web URL override (default `https://smithery.ai`) | +| `SMITHERY_API_URL` | Smithery API base URL override (default `https://api.smithery.ai`) | +| `SMITHERY_API_KEY` | Smithery API key for managed MCP auth lookup | +| `PUPPETEER_EXECUTABLE_PATH` | Browser tool Chromium executable override | +| `LM_STUDIO_BASE_URL` | Default implicit LM Studio discovery base URL override (`http://127.0.0.1:1234/v1` if unset) | +| `OLLAMA_BASE_URL` | Default implicit Ollama discovery base URL override (`http://127.0.0.1:11434` if unset) | +| `LLAMA_CPP_BASE_URL` | Default implicit Llama.cpp discovery base URL override (`http://127.0.0.1:8080` if unset) | +| `PI_EDIT_VARIANT` | Forces edit tool variant when valid (`patch`, `replace`, `hashline`, `apply_patch`) | +| `PI_FORCE_IMAGE_PROTOCOL` | Forces supported image protocol (`kitty`, `iterm2`/`iterm`, `sixel`, `none`) where used | +| `PI_ALLOW_SIXEL_PASSTHROUGH` | Allows SIXEL passthrough when `PI_FORCE_IMAGE_PROTOCOL=sixel` | +| `PI_NO_PTY` | If `1`, disables interactive PTY path for bash tool | +| `OMP_MCP_TIMEOUT_MS` | Overrides MCP client request timeout (ms) for every MCP server. `0` disables client-side timeouts (`AbortSignal` never fires). Invalid (negative or non-numeric) values are ignored with a warning and the per-server config or default (`30000`) is used. | `PI_NO_PTY` is also set internally when CLI `--no-pty` is used. @@ -366,6 +387,7 @@ These are read as runtime signals; they are usually set by the terminal/OS rathe | `PI_TUI_WRITE_LOG` | If set, logs TUI writes to file | | `PI_HARDWARE_CURSOR` | If `1`, enables hardware cursor mode | | `PI_CLEAR_ON_SHRINK` | If `1`, clears empty rows when content shrinks | +| `PI_NO_SYNC_OUTPUT` | If `1`, disables DEC 2026 synchronized-output wrappers while keeping TUI autowrap guards | | `PI_DEBUG_REDRAW` | If `1`, enables redraw debug logging | | `PI_TUI_DEBUG` | If `1`, enables deep TUI debug dump path | | `PI_FORCE_IMAGE_PROTOCOL` | Forces terminal image protocol detection (`kitty`, `iterm2`/`iterm`, `sixel`, `none`) | diff --git a/docs/extension-loading.md b/docs/extension-loading.md index ac0a07fd1..d5b9778e7 100644 --- a/docs/extension-loading.md +++ b/docs/extension-loading.md @@ -28,17 +28,18 @@ Extension loading builds a list of module entry files, imports each module with `discoverAndLoadExtensions()` first asks discovery providers for `extension-module` capability items, then keeps only provider `native` items. -Effective native locations: +Native `extension-module` discovery comes from: -- Project: `/.omp/extensions` -- User: `~/.omp/agent/extensions` +- Project directory: `/.omp/extensions` +- User directory: `~/.omp/agent/extensions` +- Native legacy/settings JSON entries: `/.omp/settings.json#extensions` and `~/.omp/agent/settings.json#extensions` -Path roots come from the native provider (`SOURCE_PATHS.native`). +Path roots come from the native provider (`SOURCE_PATHS.native`). Project lookup is cwd-only for these native roots; it does not walk ancestors. Notes: - Native auto-discovery is currently `.omp` based. -- Legacy `.pi` is still accepted in `package.json` manifest keys (`pi.extensions`), but not as a native root here. +- Legacy `.pi` is still accepted in package manifests (`pi.extensions`) and project override lookup, but `.pi/extensions` is not a native root here. ### 2) Installed plugin extension entries @@ -53,14 +54,16 @@ After plugin extension entries, configured paths are appended and resolved. Configured path sources in the main session startup path (`sdk.ts`): 1. CLI-provided paths (`--extension/-e`, and `--hook` is also treated as an extension path) -2. Settings `extensions` array (merged global + project settings) +2. Merged settings `extensions` array -Global settings file: +Settings files: -- `~/.omp/agent/config.yml` (or custom agent dir via `PI_CODING_AGENT_DIR`) +- User: `~/.omp/agent/config.yml` (or custom agent dir via `PI_CODING_AGENT_DIR`) +- Project/native settings capability: `/.omp/config.yml` and `/.omp/settings.json` -Project settings file: +Native extension-module discovery also reads legacy JSON extension lists from: +- `~/.omp/agent/settings.json` - `/.omp/settings.json` Examples: diff --git a/docs/extensions.md b/docs/extensions.md index 6b40ba71c..119d0f2cb 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -10,7 +10,7 @@ This document covers the current extension runtime in: - `src/extensibility/extensions/index.ts` - `src/modes/controllers/extension-ui-controller.ts` -For discovery paths and filesystem loading rules, see `docs/extension-loading.md`. +For discovery paths and filesystem loading rules, see [`extension-loading.md`](./extension-loading.md). ## What an extension is @@ -112,9 +112,11 @@ Core methods: - `on(event, handler)` - `registerTool`, `registerCommand`, `registerShortcut`, `registerFlag` -- `registerMessageRenderer` -- `sendMessage`, `sendUserMessage`, `appendEntry` +- `registerMessageRenderer`, `registerAssistantThinkingRenderer` +- `setLabel`, `getFlag` +- `sendMessage`, `sendUserMessage`, `appendEntry`, `exec` - `getActiveTools`, `getAllTools`, `setActiveTools` +- `getCommands` - `getSessionName`, `setSessionName` - `setModel`, `getThinkingLevel`, `setThinkingLevel` - `registerProvider` @@ -125,7 +127,8 @@ In interactive mode, `input` handlers run before the built-in first-message auto Also exposed: - `pi.logger` -- `pi.zod` (injected `zod` module — use for tool parameter schemas) +- `pi.typebox` (zod-backed compatibility shim for legacy TypeBox-style schemas) +- `pi.zod` (injected `zod/v4` module — canonical for tool parameter schemas) - `pi.pi` (package exports) ### Message delivery semantics @@ -191,6 +194,8 @@ Cancelable pre-events: - `input` - `before_agent_start` +- `before_provider_request` (may replace provider request payload) +- `after_provider_response` - `context` - `agent_start` / `agent_end` - `turn_start` / `turn_end` @@ -210,6 +215,8 @@ Cancelable pre-events: - `auto_retry_start` / `auto_retry_end` - `ttsr_triggered` - `todo_reminder` +- `goal_updated` +- `credential_disabled` ### User command interception @@ -247,6 +254,9 @@ pi.registerTool({ label: "My Tool", description: "...", parameters: z.object({}), + hidden: false, + defaultInactive: false, + deferrable: false, async execute(_id, _params, signal, onUpdate, ctx) { if (signal?.aborted) { return { content: [{ type: "text", text: "Cancelled" }] }; @@ -266,7 +276,7 @@ pi.registerTool({ }); ``` -`tool_call`/`tool_result` intercept all tools once the registry is wrapped in `sdk.ts`, including built-ins and extension/custom tools. +`tool_call`/`tool_result` intercept all tools once the registry is wrapped in `sdk.ts`, including built-ins and extension/custom tools. `ToolDefinition` also supports optional `hidden`, `defaultInactive`, `deferrable`, `mcpServerName`, `mcpToolName`, `renderCall`, and `renderResult` fields. ## UI integration points @@ -277,6 +287,8 @@ pi.registerTool({ Supported: - dialogs: `select`, `confirm`, `input`, `editor` +- input editing: `setEditorText`, `getEditorText`, `pasteToEditor`, `editor` +- terminal title and working message (`setTitle`, `setWorkingMessage`) - notifications/status/editor text/terminal input/custom overlays - theme listing/loading by name (`setTheme` supports string names) - tools expanded toggle @@ -347,6 +359,20 @@ pi.registerMessageRenderer("my-type", (message, { expanded }, theme) => { Used by interactive rendering when custom messages are displayed. +## Assistant thinking renderer + +```ts +import { Container, Text } from "@oh-my-pi/pi-tui"; + +pi.registerAssistantThinkingRenderer((context, theme) => { + const container = new Container(); + container.addChild(new Text(theme.fg("dim", `thinking chars: ${context.text.length}`), 1, 0)); + return container; +}); +``` + +Used by interactive rendering to add display-only supplemental UI below each visible assistant thinking block. The renderer receives the already-visible thinking text, content/thinking indexes, theme, and a `requestRender()` callback for async renderers. All registered renderers that return a component are appended in registration order. Renderers must not mutate messages; the original thinking block remains the provider/session source of truth. + ## Tool call/result renderer Provide `renderCall` / `renderResult` on `registerTool` definitions for custom tool visualization in TUI. diff --git a/docs/fs-scan-cache-architecture.md b/docs/fs-scan-cache-architecture.md index 9bc516f3d..e251f0569 100644 --- a/docs/fs-scan-cache-architecture.md +++ b/docs/fs-scan-cache-architecture.md @@ -4,12 +4,12 @@ This document defines the current contract for the shared filesystem scan cache ## What this cache is -The cache stores full directory-scan entry lists (`GlobMatch[]`) keyed by scan scope and traversal policy, then lets higher-level operations (glob filtering, fuzzy scoring, grep file selection) run against those cached entries. +The cache stores full directory-scan entry lists (`GlobMatch[]`) keyed by scan scope, traversal policy, and requested metadata detail. Higher-level operations (`glob` filtering, `fuzzyFind` scoring, and cached `grep` candidate selection) run against those cached entries. Primary goals: - avoid repeated filesystem walks for repeated discovery/search calls -- keep consistency across `glob`, `fuzzyFind`, and `grep` when they share the same scan policy +- keep consistency across native discovery/search flows when they share the same scan policy - allow explicit staleness recovery for empty results and explicit invalidation after file mutations ## Ownership and public surface @@ -18,11 +18,10 @@ Primary goals: - Native consumers: - `crates/pi-natives/src/glob.rs` - `crates/pi-natives/src/fd.rs` (`fuzzyFind`) - - `crates/pi-natives/src/grep.rs` + - `crates/pi-natives/src/grep.rs` (cached directory mode only) - JS binding/export: - - `packages/natives/src/glob/index.ts` (`invalidateFsScanCache`) - - `packages/natives/src/glob/types.ts` - - `packages/natives/src/grep/types.ts` + - `packages/natives/native/index.d.ts` (`invalidateFsScanCache`) + - `packages/natives/native/index.js` - Coding-agent mutation invalidation helpers: - `packages/coding-agent/src/tools/fs-cache-invalidation.ts` @@ -34,25 +33,30 @@ Each entry is keyed by: - `include_hidden` boolean - `use_gitignore` boolean - `skip_node_modules` boolean +- `detail` (`ScanDetail::Minimal` or `ScanDetail::Full`) Implications: - Hidden and non-hidden scans do **not** share entries. - Gitignore-respecting and ignore-disabled scans do **not** share entries. - Scans that prune `node_modules` do **not** share entries with scans that include it. -- Consumers must pass stable semantics for hidden/gitignore/node_modules behavior; changing any flag creates a different cache partition. +- Minimal scans (path + file type only) do **not** share entries with full scans (mtime + regular-file size metadata). +- `follow_links` is part of `ScanOptions` used to build the walker, but is not currently part of `CacheKey`; calls that differ only by `follow_links` can share a cache entry. + +Consumers must pass stable semantics for hidden/gitignore/node_modules/detail behavior; changing any keyed flag creates a different cache partition. ## Scan collection behavior -Cache population uses a deterministic walker (`ignore::WalkBuilder`) configured by `include_hidden`, `use_gitignore`, and `skip_node_modules`: +Cache population uses `ignore::WalkBuilder` configured by `include_hidden`, `use_gitignore`, `skip_node_modules`, and `follow_links`: -- `follow_links(false)` - sorted by file path -- `.git` is always skipped +- `.git` is always pruned - `node_modules` is pruned at traversal time when `skip_node_modules=true` -- entry file type + `mtime` are captured via `symlink_metadata` +- cancellation is checked before the walk and every 128 visited entries per parallel visitor +- `ScanDetail::Minimal` records normalized relative path and file type only +- `ScanDetail::Full` also records mtime and regular-file size -Search roots are resolved by `resolve_search_path`: +Search roots for cache scans are resolved by `fs_cache::resolve_search_path`: - relative paths are resolved against current cwd - target must be an existing directory @@ -70,9 +74,11 @@ Behavior: - `get_or_scan(...)` - if TTL is `0`: bypass cache entirely, always fresh scan (`cache_age_ms = 0`) - - on cache hit within TTL: return cached entries + non-zero `cache_age_ms` + - on cache hit within TTL: return cloned cached entries + non-zero `cache_age_ms` - on expired hit: evict key, rescan, store fresh entry -- max entry enforcement is oldest-first eviction by `created_at` +- `force_rescan(..., store=false)`: remove any matching key, scan fresh, and do not repopulate cache +- `force_rescan(..., store=true)`: remove any matching key, scan fresh, then store the new entry +- max entry enforcement is oldest-first eviction by `created_at` after insert ## Empty-result fast recheck (separate from normal hits) @@ -83,46 +89,45 @@ Normal cache hit: Empty-result fast recheck: - this is a **caller-side** policy using `ScanResult.cache_age_ms` -- if filtered/query result is empty and cached scan age is at least `empty_recheck_ms()`, caller performs one `force_rescan(...)` and retries -- intended to reduce stale-negative results when files were recently added but cache is still within TTL +- if filtered/query result is empty and cached scan age is at least `empty_recheck_ms()`, caller performs one `force_rescan(..., store=true)` and retries +- intended to reduce stale-negative results when files were added while the cache is still inside TTL Current consumers: - `glob`: rechecks when filtered matches are empty and scan age exceeds threshold - `fuzzyFind` (`fd.rs`): rechecks only when query is non-empty and scored matches are empty -- `grep`: rechecks when selected candidate file list is empty +- `grep`: rechecks when cached directory candidate file list is empty ## Consumer defaults and cache usage -Cache is opt-in on all exposed APIs (`cache?: boolean`, default `false`). +Cache is opt-in on exposed scan/search APIs (`cache?: boolean`, default `false`). Current defaults in native APIs: -- `glob`: `hidden=false`, `gitignore=true`, `cache=false`, and `node_modules` included only when the pattern mentions `node_modules` -- `fuzzyFind`: `hidden=false`, `gitignore=true`, `cache=false`, and `node_modules` is skipped -- `grep`: `hidden=true`, `gitignore=true`, `cache=false`, and `node_modules` included only when the glob mentions `node_modules` +- `glob`: `hidden=false`, `gitignore=true`, `cache=false`; `node_modules` is included only when `includeNodeModules=true` or the pattern mentions `node_modules`; full detail is used only when `sortByMtime=true` +- `fuzzyFind`: `hidden=false`, `gitignore=true`, `cache=false`, `node_modules` is skipped, `follow_links=true`, minimal detail +- `grep`: `hidden=true`, `gitignore=true`, `cache=false`; cached directory mode skips `node_modules` unless the glob mentions `node_modules`; minimal detail Coding-agent callers today: - High-volume mention candidate discovery enables cache: - `packages/coding-agent/src/utils/file-mentions.ts` - - profile: `hidden=true`, `gitignore=true`, `includeNodeModules=true`, `cache=true` -- Tool-level `grep` integration currently disables scan cache (`cache: false`): - - `packages/coding-agent/src/tools/grep.ts` +- Mutation flows invalidate through `packages/coding-agent/src/tools/fs-cache-invalidation.ts`. +- Tool-level search integration (`packages/coding-agent/src/tools/search.ts`) currently calls native `grep` with `cache: false`. ## Invalidation contract Native invalidation entrypoint: - `invalidateFsScanCache(path?: string)` - - with `path`: remove cache entries whose root is a prefix of target path + - with `path`: remove cache entries whose root is a prefix of the target path - without path: clear all scan cache entries Path handling details: - relative invalidation paths are resolved against cwd - invalidation attempts canonicalization -- if target does not exist (e.g., delete), fallback canonicalizes parent and reattaches filename when possible +- if target does not exist (for example after delete), fallback canonicalizes the parent and reattaches the filename when possible - this preserves invalidation behavior for create/delete/rename where one side may not exist ## Coding-agent mutation flow responsibilities @@ -135,10 +140,12 @@ Central helpers: - `invalidateFsScanAfterDelete(path)` - `invalidateFsScanAfterRename(oldPath, newPath)` (invalidates both sides when paths differ) -Current mutation tool callsites: +Current mutation callsites include: - `packages/coding-agent/src/tools/write.ts` -- `packages/coding-agent/src/patch/index.ts` (hashline/patch/replace flows) +- `packages/coding-agent/src/edit/hashline/filesystem.ts` +- `packages/coding-agent/src/edit/modes/patch.ts` +- `packages/coding-agent/src/edit/modes/replace.ts` Rule: if a flow mutates filesystem content or location and bypasses these helpers, cache staleness bugs are expected. @@ -147,7 +154,7 @@ Rule: if a flow mutates filesystem content or location and bypasses these helper When introducing cache use in a new scanner/search path: 1. **Use stable scan policy inputs** - - decide hidden/gitignore/node_modules semantics first + - decide hidden/gitignore/node_modules/detail semantics first - pass them consistently to `get_or_scan`/`force_rescan` so cache partitions are intentional 2. **Treat cache data as pre-filtered only by traversal policy** @@ -160,7 +167,7 @@ When introducing cache use in a new scanner/search path: - keep this path separate from normal cache-hit logic 4. **Respect no-cache mode explicitly** - - when caller disables cache, call `force_rescan(..., store=false, ...)` + - when caller disables cache, call `force_rescan(..., store=false, ...)` or use an uncached streaming walker - do not populate shared cache in a no-cache request path 5. **Wire mutation invalidation for any new write path** @@ -174,5 +181,5 @@ When introducing cache use in a new scanner/search path: - Cache scope is process-local in-memory (`DashMap`), not persisted across process restarts. - Cache stores scan entries, not final tool results. -- `glob`/`fuzzyFind`/`grep` share scan entries only when key dimensions (`root`, `hidden`, `gitignore`, `skip_node_modules`) match. +- `glob`/`fuzzyFind`/cached `grep` share scan entries only when key dimensions (`root`, `hidden`, `gitignore`, `skip_node_modules`, `detail`) match. - `.git` is always excluded at scan collection time regardless of caller options. diff --git a/docs/gemini-manifest-extensions.md b/docs/gemini-manifest-extensions.md index 3a53f9f0c..6e80e88e0 100644 --- a/docs/gemini-manifest-extensions.md +++ b/docs/gemini-manifest-extensions.md @@ -6,12 +6,12 @@ It does **not** cover TypeScript/JavaScript extension module loading (`extension ## Implementation files -- [`../src/discovery/gemini.ts`](../packages/coding-agent/src/discovery/gemini.ts) -- [`../src/discovery/builtin.ts`](../packages/coding-agent/src/discovery/builtin.ts) -- [`../src/discovery/helpers.ts`](../packages/coding-agent/src/discovery/helpers.ts) -- [`../src/capability/extension.ts`](../packages/coding-agent/src/capability/extension.ts) -- [`../src/capability/index.ts`](../packages/coding-agent/src/capability/index.ts) -- [`../src/extensibility/extensions/loader.ts`](../packages/coding-agent/src/extensibility/extensions/loader.ts) +- [`packages/coding-agent/src/discovery/gemini.ts`](../packages/coding-agent/src/discovery/gemini.ts) +- [`packages/coding-agent/src/discovery/builtin.ts`](../packages/coding-agent/src/discovery/builtin.ts) +- [`packages/coding-agent/src/discovery/helpers.ts`](../packages/coding-agent/src/discovery/helpers.ts) +- [`packages/coding-agent/src/capability/extension.ts`](../packages/coding-agent/src/capability/extension.ts) +- [`packages/coding-agent/src/capability/index.ts`](../packages/coding-agent/src/capability/index.ts) +- [`packages/coding-agent/src/extensibility/extensions/loader.ts`](../packages/coding-agent/src/extensibility/extensions/loader.ts) --- @@ -169,7 +169,7 @@ For Gemini manifests specifically: `gemini-extension.json` discovery currently feeds capability metadata (`Extension` items). It does **not** directly load runnable TS/JS extension modules. -Runtime module loading (`discoverAndLoadExtensions()` / `loadExtensions()`) uses `extension-modules` and explicit paths, and currently filters auto-discovered modules to provider `native` only. +Runtime module loading (`discoverAndLoadExtensions()` / `loadExtensions()`) uses the `extension-module` capability and explicit paths, and currently filters auto-discovered modules to provider `native` only. Practical implication: diff --git a/docs/handoff-generation-pipeline.md b/docs/handoff-generation-pipeline.md index 80b02568a..3e29416ce 100644 --- a/docs/handoff-generation-pipeline.md +++ b/docs/handoff-generation-pipeline.md @@ -54,7 +54,7 @@ The same minimum-content guard exists again inside `AgentSession.handoff()` and - the live tool array (`agent.state.tools`), - optional focus instructions, - coding-agent message conversion (`convertToLlm`), - - provider metadata and `initiatorOverride: "agent"`. + - provider metadata, current thinking level, and `initiatorOverride: "agent"`. `generateHandoff(...)` lives in `packages/agent/src/compaction/compaction.ts` next to summarization. It renders `packages/agent/src/compaction/prompts/handoff-document.md` via `renderHandoffPrompt(...)` with optional `additionalFocus`. @@ -75,7 +75,7 @@ await completeSimple( { apiKey, signal, - reasoning: Effort.High, + reasoning: resolveCompactionEffort(model, options.thinkingLevel), toolChoice: "none", initiatorOverride, metadata, @@ -113,7 +113,7 @@ If text was generated and not aborted: 3. Start a brand-new session with `parentSession` pointing at the previous session file when one exists. 4. Reset in-memory agent state (`agent.reset()`). 5. Rebind `agent.sessionId` to the new session id. -6. Rekey/reset hindsight state for the new session. +6. Rekey/reset Hindsight and Mnemopi memory session tracking for the new session. 7. Clear queued context arrays (`#steeringMessages`, `#followUpMessages`, `#pendingNextTurnMessages`) and any scheduled hidden next-turn generation. 8. Reset todo reminder counter. @@ -132,7 +132,13 @@ The above is a handoff document from a previous session. Use this context to con Insertion call: ```ts -this.sessionManager.appendCustomMessageEntry("handoff", handoffContent, true, undefined, "agent"); +this.sessionManager.appendCustomMessageEntry( + "handoff", + handoffContent, + true, + undefined, + "agent", +); ``` Semantics: @@ -233,7 +239,7 @@ High-level state flow: 1. Interactive slash command intercepted. 2. Preflight message-count guard. 3. `#handoffAbortController` created (`isGeneratingHandoff = true`). -4. `generateHandoff(...)` issues one `completeSimple(...)` request with live system prompt, tools, message history, and trailing handoff prompt. +4. `generateHandoff(...)` issues one `completeSimple(...)` request with live system prompt, tools, message history, current thinking level, and trailing handoff prompt. 5. Assistant response text blocks are joined; tool-call blocks are discarded. 6. If missing text → return `undefined`; if aborted → cancellation error path. 7. If present: diff --git a/docs/hooks.md b/docs/hooks.md index f3eb2ce30..902f10589 100644 --- a/docs/hooks.md +++ b/docs/hooks.md @@ -47,6 +47,7 @@ The factory can: - register slash commands via `pi.registerCommand(...)` - register custom message renderers via `pi.registerMessageRenderer(...)` - run shell commands via `pi.exec(...)` +- author schemas/helpers with injected `pi.zod`, `pi.typebox`, and package exports via `pi.pi` ## Discovery and loading @@ -218,7 +219,7 @@ Command/renderer conflicts: - `setEditorText`, `getEditorText` - `theme` getter -`ctx.hasUI` indicates whether interactive UI is available. +`ctx` includes `hasUI`, `cwd`, `sessionManager`, `modelRegistry`, current `model`, `isIdle()`, `abort()`, and `hasQueuedMessages()`. When running with no UI, the default no-op context behavior is: diff --git a/docs/install-id.md b/docs/install-id.md index 4c7571132..445757570 100644 --- a/docs/install-id.md +++ b/docs/install-id.md @@ -6,12 +6,12 @@ A persistent per-install UUID that identifies a single oh-my-pi installation acr Exported from `@oh-my-pi/pi-utils` (`packages/utils/src/dirs.ts`): -| Symbol | Purpose | -| --- | --- | -| `getInstallId(): string` | Returns the install ID, generating and persisting one on first call. Result is cached in-process for the lifetime of the runtime. | -| `__resetInstallIdCacheForTests(): void` | Clears the in-process cache. Test-only — MUST NOT be called from production code. | +| Symbol | Purpose | +| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| `getInstallId(): string` | Returns the install ID, generating and persisting one on first call. Result is cached in-process for the lifetime of the runtime. | +| `__resetInstallIdCacheForTests(): void` | Clears the in-process cache. Test-only — MUST NOT be called from production code. | -The returned value is a canonical lowercase RFC 4122 UUID matching `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`. +Generated IDs are lowercase RFC 4122 UUIDs. Existing persisted values are accepted case-insensitively when they match `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$` with the regex `i` flag, and are returned exactly as stored. ## Storage diff --git a/docs/keybindings.md b/docs/keybindings.md index 96c335b4c..8e8158bbf 100644 --- a/docs/keybindings.md +++ b/docs/keybindings.md @@ -4,44 +4,43 @@ Run `/hotkeys` inside an `omp` session to see the active chords for your current ## Customize keybindings -User remaps live in `~/.omp/agent/keybindings.json`. The file is a JSON object whose keys are keybinding action IDs and whose values are either one chord string or an array of chord strings. It is not read from `~/.omp/agent/config.yml`, and there is no nested `keybindings` object. +User remaps live in `~/.omp/agent/keybindings.yml`. The file is a YAML mapping whose keys are keybinding action IDs and whose values are either one chord string or an array of chord strings. It is not read from `~/.omp/agent/config.yml`, and there is no nested `keybindings` object. -```json -{ - "app.model.cycleForward": "Ctrl+P", - "app.model.selectTemporary": "Alt+P", - "app.plan.toggle": "Alt+Shift+P" -} +```yaml +app.model.cycleForward: Ctrl+P +app.model.selectTemporary: Alt+P +app.plan.toggle: Alt+Shift+P ``` Chord names are case-insensitive and use the same notation shown in the UI, such as `Ctrl+P`, `Alt+Shift+P`, `Shift+Enter`, and `Ctrl+Backspace`. Set an action to an empty array to disable it: -```json -{ - "app.stt.toggle": [] -} +```yaml +app.stt.toggle: [] ``` ## Common action IDs -| Action ID | Default | Meaning | -| --- | --- | --- | -| `app.model.cycleForward` | `Ctrl+P` | Cycle role models forward | -| `app.model.cycleBackward` | `Shift+Ctrl+P` | Cycle role models backward | -| `app.model.selectTemporary` | `Alt+P` | Pick a model temporarily for this session | -| `app.model.select` | `Ctrl+L` | Open the model selector and set roles | -| `app.plan.toggle` | `Alt+Shift+P` | Toggle plan mode | -| `app.history.search` | `Ctrl+R` | Search prompt history | -| `app.tools.expand` | `Ctrl+O` | Toggle tool-output expansion | -| `app.thinking.toggle` | `Ctrl+T` | Toggle thinking-block visibility | -| `app.thinking.cycle` | `Shift+Tab` | Cycle thinking level | -| `app.editor.external` | `Ctrl+G` | Edit the draft in `$VISUAL` / `$EDITOR` | -| `app.message.followUp` | `Ctrl+Enter` | Queue a follow-up message | -| `app.message.dequeue` | `Alt+Up` | Dequeue a queued message back into the editor | -| `app.clipboard.copyLine` | `Alt+Shift+L` | Copy the current line | -| `app.clipboard.copyPrompt` | `Alt+Shift+C` | Copy the whole prompt | -| `app.stt.toggle` | `Alt+H` | Toggle speech-to-text recording | +| Action ID | Default | Meaning | +| --------------------------- | -------------------------------------- | --------------------------------------------- | +| `app.model.cycleForward` | `Ctrl+P` | Cycle role models forward | +| `app.model.cycleBackward` | `Shift+Ctrl+P` | Cycle role models in temporary mode | +| `app.model.selectTemporary` | `Alt+P` | Pick a model temporarily for this session | +| `app.model.select` | `Ctrl+L` | Open the model selector and set roles | +| `app.plan.toggle` | `Alt+Shift+P` | Toggle plan mode | +| `app.history.search` | `Ctrl+R` | Search prompt history | +| `app.tools.expand` | `Ctrl+O` | Toggle tool-output expansion | +| `app.thinking.toggle` | `Ctrl+T` | Toggle thinking-block visibility | +| `app.thinking.cycle` | `Shift+Tab` | Cycle thinking level | +| `app.editor.external` | `Ctrl+G` | Edit the draft in `$VISUAL` / `$EDITOR` | +| `app.message.followUp` | `Ctrl+Enter` | Queue a follow-up message | +| `app.message.dequeue` | `Alt+Up` | Dequeue a queued message back into the editor | +| `app.clipboard.copyLine` | `Alt+Shift+L` | Copy the current line | +| `app.clipboard.copyPrompt` | `Alt+Shift+C` | Copy the whole prompt | +| `app.clipboard.pasteImage` | `Ctrl+V` (`Alt+V` fallback on Windows) | Paste an image from the clipboard | +| `app.stt.toggle` | `Alt+H` | Toggle speech-to-text recording | -Older unqualified action names are migrated when `keybindings.json` is loaded, but new docs and new configs should use the namespaced action IDs above. +On Windows Terminal, `Ctrl+V` may be handled by the terminal paste command before `omp` sees it; use the `Alt+V` fallback when clipboard image paste appears to do nothing. + +Older unqualified action names are migrated when `keybindings.yml` is loaded, but new docs and new configs should use the namespaced action IDs above. Existing `keybindings.json` files are still accepted and migrated to `keybindings.yml`; `keybindings.yaml` is also accepted. diff --git a/docs/local-models.md b/docs/local-models.md new file mode 100644 index 000000000..68502d27b --- /dev/null +++ b/docs/local-models.md @@ -0,0 +1,148 @@ +# Embedded Local Tiny-Model Experiments + +This document summarizes the experiments behind the optional **local** tiny-model paths for +session-title generation (`providers.tinyModel`), Mnemopi memory extraction/consolidation +(`providers.memoryModel`), and the `auto` thinking-level difficulty classifier +(`providers.autoThinkingModel`, which reuses the memory-model registry). It is a factual engineering +record for maintainers: what we measured, which recipes won, and which models we shipped. All three +settings default to `online`, so existing users incur no downloads or on-device inference cost unless +they opt in. + +## Runtime / environment findings + +- **Stack**: `@huggingface/transformers` (transformers.js) v4 running under Bun. In Bun the library + loads the **native `onnxruntime-node` backend** (not the WASM build). +- **Device policy**: local tiny models default to CPU-only inference and retry once on CPU if an + explicit accelerated provider cannot initialize. + - Pick a provider persistently with the `providers.tinyModelDevice` setting (`default` keeps CPU), + or per-run with the `PI_TINY_DEVICE` env var (which overrides the setting). + - Accepted values are `cpu`, `gpu`, `metal`/`webgpu`, `auto`, `cuda`, `dml`, `coreml`, `wasm`, + `webnn`, `webnn-gpu`, `webnn-cpu`, and `webnn-npu`. + - Direct `coreml` remains opt-in via `PI_TINY_DEVICE=coreml`; it is not part of the default because + cached decoder-LLM ONNX loads can fail during session initialization. + - WebGPU/Metal works for the single-process eval harness, but the production worker forces + Darwin `gpu`/`webgpu`/`auto` requests back to CPU because ONNX Runtime/Bun currently + hard-crashes on worker teardown after WebGPU inference. + - Use `providers.tinyModelDevice` or `PI_TINY_DEVICE` only when explicitly opting out of the CPU + default. +- **Quantization: q4 is the sweet spot** — smaller on disk, faster to load, and fast at inference. + q8/int8 loads slower _and_ infers slower on CPU. Every shipped model defaults to `q4`; override the + precision persistently with the `providers.tinyModelDtype` setting (`default` keeps `q4`, e.g. `fp16` + for higher fidelity), or per-run with `PI_TINY_DTYPE` (which overrides the setting). Accepts `auto`, + `fp32`, `fp16`, `q8`, `int8`, `uint8`, `q4`, `bnb4`, `q4f16`, `q2`, `q2f16`, `q1`, `q1f16`; an + unrecognized value fails loudly at worker startup. +- **Load-time correction (important).** An earlier belief that "q4 >=1B models take minutes to load" + was a **measurement artifact** caused by running ~5 multi-GB HuggingFace downloads in parallel + (I/O saturation). Clean, isolated **warm** loads are all sub-3s: + - TinyLlama-1.1B q4: ~0.5s + - Llama-3.2-1B q4: ~2.8s (`graphOpt=all`) / ~0.5s (`disabled`) + - LFM2-1.2B q4: ~0.36s + - Qwen2.5-1.5B q4: ~1.5s + - Qwen3-1.7B q4: ~1.6s + - gemma-3-1b q4: ~1.1s + - Conclusion: **1B–1.7B models are viable on CPU.** +- **`session_options.graphOptimizationLevel`** trades load vs inference speed: `disabled` = fastest + load, slightly slower inference; `all` = default. +- **First run** downloads weights from the HF Hub to a cache dir (q4 weights ~200MB–1.1GB depending + on model); subsequent **warm** loads are sub-second to ~3s. Inference is async and + background-friendly for memory tasks; titles are semi-interactive. + +## Task 1: Session title generation (`providers.tinyModel`) + +**Task**: turn the first user message into a 3–6 word title. Tiny models (sub-1B) suffice. + +**Winning recipe**: + +- Plain system prompt (no few-shot). +- **Prefill** the assistant turn with `` and **stop at ``**, then take the first line. +- Greedy decoding (`do_sample:false`), `enable_thinking:false` in the chat template. + +**What we learned**: + +- **Few-shot examples HURT sub-0.6B models** for titles; the tag-prefill rescues even 270M models. +- **Token biasing (`bad_words_ids`) is a confirmed no-op** here — the prefill already controls the + opener. + +**Leaderboard** (tag trick, CPU, warm): + +| Model | Verdict | +| ------------- | ----------------------------------- | +| LFM2-350M | Best speed/quality balance (~212MB) | +| Qwen3-0.6B | Most robust | +| gemma-3-270m | Smallest viable | +| Qwen2.5-0.5B | Acceptable | +| SmolLM2-135M | Too small | +| flan-t5-small | Rejected — just echoes the input | + +**Shipped local options**: `lfm2-350m`, `qwen3-0.6b`, `gemma-270m`, `qwen2.5-0.5b`, `lfm2-700m`. +**Default**: `online` (pi/smol). + +## Task 2: Mnemopi memory (`providers.memoryModel`) + +Mnemopi runs two small-LLM tasks: + +1. **Extraction** — pull durable, structured items from a single message. +2. **Consolidation** — summarize a list of memories into 1–3 faithful sentences. + +These need **bigger models than titles: 1B–1.7B**. We tested LFM2-1.2B, Qwen2.5-1.5B, Qwen3-1.7B, +and gemma-3-1b (q4, CPU) via four parallel agents each running 27–31 experiments. + +### Extraction findings + +The stock 5-category JSON prompt fails on small models in two ways: + +1. The all-empty example `{"facts":[],...}` gets **copied verbatim** → 0 facts extracted. +2. Capable models emit **JSON objects inside arrays**, which Mnemopi's `String(item)` coerces into + the literal string `[object Object]`. + +The robust fix is a **one-item-per-line output format** (consumed by Mnemopi's parser line-fallback) +or a **flat JSON array of strings**. Every model also over-extracts pure small talk; an explicit +chit-chat → NONE example is the best mitigation. + +### Technique polarity flips vs titles + +- At 1B+, **few-shot is the dominant quality lever**: e.g. Qwen2.5-1.5B extraction F1 0.52 → 0.83 + going 1 → 3 shots; gemma recall 0.65 → 0.92 with 2 shots. +- **Prefill HURTS extraction** — it forces output on small talk, producing false positives. +- **System-split** (instructions in the system role) helps models that have a system role. +- **Greedy >= temperature** for both tasks. +- **Token biasing** is again a no-op. + +### Per-model verdicts (head-to-head, 16-fixture set) + +- **Qwen3-1.7B** — most disciplined extraction: returns empty on small talk, no buried-fact leak, + preserves language, clean flat JSON. Weaknesses: coarse granularity, missed a multi-turn value + update. +- **Qwen2.5-1.5B** — best extraction granularity (atomic facts), caught the value update, zero + small-talk leakage. Weaknesses: weakest consolidation (run-on, no dedup) and one degenerate + buried-fact output. +- **gemma-3-1b** — best consolidation (dedup works, faithful, clean single-memory). Weaknesses: leaks + small talk and translated German. +- **LFM2-1.2B** — solid and fastest to load. Weaknesses: `Label: value` noise, small-talk + buried + leaks, a fluffy single-memory summary. + +### Recommendation + +Extraction favors **precision** (do not pollute long-term memory) → **Qwen3-1.7B is the best single +pick** (its consolidation is good enough). If running a second model for consolidation, **gemma-3-1b** +wins that task. + +**Shipped local options**: `qwen3-1.7b` (recommended), `gemma-3-1b`, `qwen2.5-1.5b`, `lfm2-1.2b`. +**Default**: `online` (the configured smol model). + +### Known Mnemopi parser bugs (surfaced by these experiments) + +- `String(item)` produces `[object Object]` on object array items. +- The line-fallback drops items `<=10` chars, so a correct short fact like `Name: Can` is discarded. + + +## Integration notes + +- `providers.tinyModel`, `providers.memoryModel`, and `providers.autoThinkingModel` default to + `online`, so existing users get **no downloads or on-device inference cost** unless they opt in. +- Local inference runs **in a worker** (off the main thread); models are cached on disk and + downloaded on first use. +- The memory local path applies the refined recipes (line-format + small-talk-guarded extraction + prompt, hardened consolidation prompt) via Mnemopi prompt overrides; the **online path is + unchanged**. +- `providers.autoThinkingModel` uses the same shipped local options as `providers.memoryModel`. diff --git a/docs/lsp-config.md b/docs/lsp-config.md index 7ce67db43..e6fa0a3f3 100644 --- a/docs/lsp-config.md +++ b/docs/lsp-config.md @@ -21,22 +21,22 @@ No configuration is required for common setups. The built-in server list covers OMP merges LSP config from multiple files, lowest to highest priority: -| Priority | Location | -|----------|----------| -| 5 (lowest) | `~/lsp.json`, `~/.lsp.json`, `~/lsp.yaml`, `~/.lsp.yaml` | -| 4 | Plugin LSP configs (marketplace / `--plugin-dir` roots) | -| 3 | `~/.omp/agent/lsp.json`, `~/.omp/agent/lsp.yaml`, `~/.claude/lsp.*` | -| 2 | `/.omp/lsp.json`, `/.omp/lsp.yaml`, `/.claude/lsp.*` | -| 1 (highest) | `/lsp.json`, `/.lsp.json`, `/lsp.yaml` | +| Priority | Location | +| ----------- | --------------------------------------------------------------------------------------------------------------------------- | +| 5 (lowest) | `~/lsp.json`, `~/.lsp.json`, `~/lsp.yaml`, `~/.lsp.yaml`, `~/lsp.yml`, `~/.lsp.yml` | +| 4 | Plugin LSP configs (marketplace / `--plugin-dir` roots) | +| 3 | User config dirs: `~/.omp/agent/lsp.*`, `~/.claude/lsp.*`, `~/.codex/lsp.*`, `~/.gemini/lsp.*` | +| 2 | Project config dirs: `/.omp/lsp.*`, `/.claude/lsp.*`, `/.codex/lsp.*`, `/.gemini/lsp.*` | +| 1 (highest) | Project root: `/lsp.*` and `/.lsp.*` | -Each location accepts both `.json` and `.yaml` / `.yml` variants, as well as hidden-file versions (`.lsp.json`, `.lsp.yaml`). Files are merged in order: higher-priority files override lower-priority fields for the same server. Servers not mentioned in any override file remain at their built-in defaults. +Each location accepts `.json`, `.yaml`, and `.yml` variants, including hidden-file versions (`.lsp.json`, `.lsp.yaml`, `.lsp.yml`). Files are merged in order: higher-priority files override lower-priority fields for the same server. Servers not mentioned in any override file remain at their built-in defaults. **Recommended locations:** - User-wide preferences → `~/.omp/agent/lsp.json` - Project-specific overrides → `/.omp/lsp.json` -> **Note:** The presence of any LSP config file disables auto-detection. When at least one file is found, OMP skips the binary-scan phase and loads all servers that have matching `rootMarkers`, an available binary, and are not explicitly `disabled`. +> **Note:** Auto-detection is skipped only when at least one config file contributes server overrides. A config file that only sets `idleTimeoutMs` still lets OMP auto-detect built-in servers. When server overrides exist, OMP merges them with defaults and then loads servers that have matching `rootMarkers`, an available binary, and are not explicitly `disabled`. ## File shape @@ -67,18 +67,18 @@ Top-level keys: ## ServerConfig fields -| Field | Type | Required | Description | -|-------|------|----------|-------------| -| `command` | `string` | yes | Binary name (resolved via PATH/local bins) or absolute path | -| `args` | `string[]` | no | Arguments passed to the binary | -| `fileTypes` | `string[]` | yes | File extensions this server handles, e.g. `[".ts", ".tsx"]` | -| `rootMarkers` | `string[]` | yes | Files/dirs that indicate a project root; glob patterns (e.g. `*.cabal`) are supported | -| `initOptions` | `object` | no | Sent as `initializationOptions` during LSP handshake | -| `settings` | `object` | no | Workspace settings pushed via `workspace/didChangeConfiguration` | -| `disabled` | `boolean` | no | Set to `true` to disable this server entirely | -| `warmupTimeoutMs` | `number` | no | Startup timeout in ms for this server (overrides the global default) | -| `isLinter` | `boolean` | no | Mark server as linter/formatter only; excluded from type-intelligence operations (hover, go-to-definition, etc.) | -| `capabilities` | `object` | no | Opt-in server-specific features; see [Capabilities](#capabilities) | +| Field | Type | Required | Description | +| ----------------- | ---------- | -------- | ---------------------------------------------------------------------------------------------------------------- | +| `command` | `string` | yes | Binary name (resolved via PATH/local bins) or absolute path | +| `args` | `string[]` | no | Arguments passed to the binary | +| `fileTypes` | `string[]` | yes | File extensions this server handles, e.g. `[".ts", ".tsx"]` | +| `rootMarkers` | `string[]` | yes | Files/dirs that indicate a project root; glob patterns (e.g. `*.cabal`) are supported | +| `initOptions` | `object` | no | Sent as `initializationOptions` during LSP handshake | +| `settings` | `object` | no | Workspace settings pushed via `workspace/didChangeConfiguration` | +| `disabled` | `boolean` | no | Set to `true` to disable this server entirely | +| `warmupTimeoutMs` | `number` | no | Startup timeout in ms for this server (overrides the global default) | +| `isLinter` | `boolean` | no | Mark server as linter/formatter only; excluded from type-intelligence operations (hover, go-to-definition, etc.) | +| `capabilities` | `object` | no | Opt-in server-specific features; see [Capabilities](#capabilities) | `resolvedCommand` is populated automatically at runtime — do not set it manually. @@ -184,57 +184,57 @@ The user-level config in `~/.omp/agent/lsp.json` is unaffected; pylsp is only su The following servers ship in `defaults.json` and are eligible for auto-detection: -| Server key | Language(s) | Binary | -|---|---|---| -| `rust-analyzer` | Rust | `rust-analyzer` | -| `clangd` | C, C++, ObjC | `clangd` | -| `zls` | Zig | `zls` | -| `gopls` | Go | `gopls` | -| `typescript-language-server` | TypeScript, JavaScript | `typescript-language-server` | -| `denols` | TypeScript, JavaScript (Deno) | `deno` | -| `biome` | TS/JS/JSON (linter) | `biome` | -| `eslint` | TS/JS/Vue/Svelte (linter) | `vscode-eslint-language-server` | -| `vscode-html-language-server` | HTML | `vscode-html-language-server` | -| `vscode-css-language-server` | CSS, SCSS, Less | `vscode-css-language-server` | -| `vscode-json-language-server` | JSON | `vscode-json-language-server` | -| `tailwindcss` | HTML, CSS, TS/JS | `tailwindcss-language-server` | -| `svelte` | Svelte | `svelteserver` | -| `vue-language-server` | Vue | `vue-language-server` | -| `astro` | Astro | `astro-ls` | -| `pyright` | Python | `pyright-langserver` | -| `basedpyright` | Python | `basedpyright-langserver` | -| `pylsp` | Python | `pylsp` | -| `ruff` | Python (linter) | `ruff` | -| `jdtls` | Java | `jdtls` | -| `kotlin-lsp` | Kotlin | `kotlin-lsp` | -| `metals` | Scala | `metals` | -| `hls` | Haskell | `haskell-language-server-wrapper` | -| `ocamllsp` | OCaml | `ocamllsp` | -| `elixirls` | Elixir | `elixir-ls` | -| `erlangls` | Erlang | `erlang_ls` | -| `gleam` | Gleam | `gleam` | -| `solargraph` | Ruby | `solargraph` | -| `ruby-lsp` | Ruby | `ruby-lsp` | -| `rubocop` | Ruby (linter) | `rubocop` | -| `bashls` | Bash, Zsh | `bash-language-server` | -| `lua-language-server` | Lua | `lua-language-server` | -| `intelephense` | PHP | `intelephense` | -| `phpactor` | PHP | `phpactor` | -| `omnisharp` | C# | `omnisharp` | -| `yamlls` | YAML | `yaml-language-server` | -| `terraformls` | Terraform | `terraform-ls` | -| `dockerls` | Dockerfile | `docker-langserver` | -| `helm-ls` | Helm | `helm_ls` | -| `nixd` | Nix | `nixd` | -| `nil` | Nix | `nil` | -| `ols` | Odin | `ols` | -| `dartls` | Dart | `dart` | -| `marksman` | Markdown | `marksman` | -| `texlab` | LaTeX | `texlab` | -| `graphql` | GraphQL | `graphql-lsp` | -| `prismals` | Prisma | `prisma-language-server` | -| `vimls` | Vim script | `vim-language-server` | -| `emmet-language-server` | HTML, CSS, JSX | `emmet-language-server` | -| `sourcekit-lsp` | Swift | `sourcekit-lsp` | -| `swiftlint` | Swift (linter) | `swiftlint` | -| `tlaplus` | TLA+ | `tlapm_lsp` | +| Server key | Language(s) | Binary | +| ----------------------------- | ----------------------------- | --------------------------------- | +| `rust-analyzer` | Rust | `rust-analyzer` | +| `clangd` | C, C++, ObjC | `clangd` | +| `zls` | Zig | `zls` | +| `gopls` | Go | `gopls` | +| `typescript-language-server` | TypeScript, JavaScript | `typescript-language-server` | +| `denols` | TypeScript, JavaScript (Deno) | `deno` | +| `biome` | TS/JS/JSON (linter) | `biome` | +| `eslint` | TS/JS/Vue/Svelte (linter) | `vscode-eslint-language-server` | +| `vscode-html-language-server` | HTML | `vscode-html-language-server` | +| `vscode-css-language-server` | CSS, SCSS, Less | `vscode-css-language-server` | +| `vscode-json-language-server` | JSON | `vscode-json-language-server` | +| `tailwindcss` | HTML, CSS, TS/JS | `tailwindcss-language-server` | +| `svelte` | Svelte | `svelteserver` | +| `vue-language-server` | Vue | `vue-language-server` | +| `astro` | Astro | `astro-ls` | +| `pyright` | Python | `pyright-langserver` | +| `basedpyright` | Python | `basedpyright-langserver` | +| `pylsp` | Python | `pylsp` | +| `ruff` | Python (linter) | `ruff` | +| `jdtls` | Java | `jdtls` | +| `kotlin-lsp` | Kotlin | `kotlin-lsp` | +| `metals` | Scala | `metals` | +| `hls` | Haskell | `haskell-language-server-wrapper` | +| `ocamllsp` | OCaml | `ocamllsp` | +| `elixirls` | Elixir | `elixir-ls` | +| `erlangls` | Erlang | `erlang_ls` | +| `gleam` | Gleam | `gleam` | +| `solargraph` | Ruby | `solargraph` | +| `ruby-lsp` | Ruby | `ruby-lsp` | +| `rubocop` | Ruby (linter) | `rubocop` | +| `bashls` | Bash, Zsh | `bash-language-server` | +| `lua-language-server` | Lua | `lua-language-server` | +| `intelephense` | PHP | `intelephense` | +| `phpactor` | PHP | `phpactor` | +| `omnisharp` | C# | `omnisharp` | +| `yamlls` | YAML | `yaml-language-server` | +| `terraformls` | Terraform | `terraform-ls` | +| `dockerls` | Dockerfile | `docker-langserver` | +| `helm-ls` | Helm | `helm_ls` | +| `nixd` | Nix | `nixd` | +| `nil` | Nix | `nil` | +| `ols` | Odin | `ols` | +| `dartls` | Dart | `dart` | +| `marksman` | Markdown | `marksman` | +| `texlab` | LaTeX | `texlab` | +| `graphql` | GraphQL | `graphql-lsp` | +| `prismals` | Prisma | `prisma-language-server` | +| `vimls` | Vim script | `vim-language-server` | +| `emmet-language-server` | HTML, CSS, JSX | `emmet-language-server` | +| `sourcekit-lsp` | Swift | `sourcekit-lsp` | +| `swiftlint` | Swift (linter) | `swiftlint` | +| `tlaplus` | TLA+ | `tlapm_lsp` | diff --git a/docs/marketplace.md b/docs/marketplace.md index a803620f3..a6e229fc2 100644 --- a/docs/marketplace.md +++ b/docs/marketplace.md @@ -1,6 +1,6 @@ # Marketplace plugin system -The marketplace system lets you discover, install, and manage plugins from Git-hosted catalogs. It is compatible with the Claude Code plugin registry format. +The marketplace system lets you discover, install, and manage plugins from Git, local, or direct-catalog sources. It is compatible with the Claude Code plugin registry format. ## Quick start @@ -9,20 +9,20 @@ The marketplace system lets you discover, install, and manage plugins from Git-h /marketplace install wordpress.com@claude-plugins-official ``` -Or just type `/marketplace` with no arguments to open the interactive plugin browser. +In the TUI, `/marketplace` with no arguments opens the interactive plugin browser. In non-TUI command handling, `/marketplace` lists configured marketplaces; use `/marketplace discover` to browse. ## Concepts A **marketplace** is a Git repository (or local directory) containing a catalog file at `.omp-plugin/marketplace.json` (preferred) or `.claude-plugin/marketplace.json` (Claude Code-compatible fallback). The catalog lists available plugins with their sources, descriptions, and metadata. -A **plugin** is a directory containing skills, commands, hooks, MCP servers, or LSP servers. Plugins are identified by `name@marketplace` (e.g. `code-review@claude-plugins-official`). +A **plugin** is a directory containing Claude/OMP plugin content such as skills, commands, hooks, tools, MCP servers, LSP servers, rules, prompts, or extension modules. Plugins are identified by `name@marketplace` (e.g. `code-review@claude-plugins-official`). -**Scopes**: plugins can be installed at two scopes: +**Scopes**: marketplace plugins can be installed at two scopes: - **user** (default) -- available in all projects, stored in `~/.omp/plugins/installed_plugins.json` -- **project** -- available only in the current project, stored in `.omp/plugins/installed_plugins.json` +- **project** -- available only in the active project, stored in the nearest project `.omp/plugins/installed_plugins.json` -Project-scoped installs shadow user-scoped installs of the same plugin. +Enabled project-scoped installs shadow enabled user-scoped installs of the same plugin. A disabled project install does not shadow the user install. ## Commands @@ -43,13 +43,16 @@ Project-scoped installs shadow user-scoped installs of the same plugin. ### Plugin operations -| Command | Effect | -| ------------------------------------------------------------------------- | ---------------------------------- | -| `/marketplace discover [marketplace]` | Browse available plugins | -| `/marketplace install [--force] [--scope user\|project] name@marketplace` | Install a plugin | -| `/marketplace uninstall [--scope user\|project] name@marketplace` | Uninstall a plugin | -| `/marketplace installed` | List installed marketplace plugins | -| `/marketplace upgrade [--scope user\|project] [name@marketplace]` | Upgrade one or all plugins | +| Command | Effect | +| ------------------------------------------------------------------------- | -------------------------------------------------- | +| `/marketplace discover [marketplace]` | Browse available plugins | +| `/marketplace install [--force] [--scope user\|project] name@marketplace` | Install a plugin | +| `/marketplace uninstall [--scope user\|project] name@marketplace` | Uninstall a plugin; no args opens the TUI selector | +| `/marketplace installed` | List installed marketplace plugins | +| `/marketplace upgrade [--scope user\|project] [name@marketplace]` | Upgrade one or all plugins | +| `/plugins list` | List npm/link and marketplace plugins | +| `/plugins enable [--scope user\|project] name@marketplace` | Enable a marketplace plugin | +| `/plugins disable [--scope user\|project] name@marketplace` | Disable a marketplace plugin | ### CLI equivalents @@ -64,20 +67,23 @@ omp plugin discover [marketplace] omp plugin install [--force] [--scope user|project] name@marketplace omp plugin uninstall [--scope user|project] name@marketplace omp plugin upgrade [--scope user|project] [name@marketplace] +omp plugin enable [--scope user|project] name@marketplace +omp plugin disable [--scope user|project] name@marketplace ``` ## Marketplace sources When you run `/marketplace add `, the system classifies the source: -| Source format | Type | Example | -| ------------------------------- | ------------------ | -------------------------------------- | -| `owner/repo` | GitHub shorthand | `anthropics/claude-plugins-official` | -| `https://...*.json` | Direct catalog URL | `https://example.com/marketplace.json` | -| `https://...*.git` or `git@...` | Git repository | `https://github.com/org/repo.git` | -| `./path` or `~/path` or `/path` | Local directory | `./my-marketplace` | +| Source format | Type | Example | +| ------------------------------- | -------------------------------------------------- | -------------------------------------- | +| `owner/repo` | GitHub shorthand | `anthropics/claude-plugins-official` | +| `https://...*.json` | Direct catalog URL | `https://example.com/marketplace.json` | +| `https://...` / `http://...` | Git repository unless the URL path ends in `.json` | `https://github.com/org/repo` | +| `git@...` / `ssh://...` | Git repository | `git@github.com:org/repo.git` | +| `./path` or `~/path` or `/path` | Local directory | `./my-marketplace` | -The system clones the repository (or reads the local directory), locates the catalog (`.omp-plugin/marketplace.json` if present, otherwise `.claude-plugin/marketplace.json`), validates it, and caches the catalog locally. +Git and local sources must contain a catalog at `.omp-plugin/marketplace.json` (preferred) or `.claude-plugin/marketplace.json` (Claude Code-compatible fallback). Direct catalog URLs cache only the JSON catalog; plugins in URL-sourced catalogs cannot use relative string sources like `"./plugins/foo"`. ## Catalog format (marketplace.json) @@ -91,12 +97,16 @@ A marketplace catalog lives at `.omp-plugin/marketplace.json` in the repository "name": "Your Name", "email": "you@example.com" }, - "description": "A collection of plugins", + "metadata": { + "description": "A collection of plugins", + "version": "1.0.0", + "pluginRoot": "plugins" + }, "plugins": [ { "name": "my-plugin", "description": "What this plugin does", - "source": "./plugins/my-plugin", + "source": "./my-plugin", "category": "development", "homepage": "https://github.com/you/my-plugin" } @@ -112,33 +122,38 @@ A marketplace catalog lives at `.omp-plugin/marketplace.json` in the repository | `owner.name` | Marketplace owner name | | `plugins` | Array of plugin entries | +Top-level `metadata.description`, `metadata.version`, and `metadata.pluginRoot` are optional. When `metadata.pluginRoot` is set, it is prepended to relative plugin `source` paths. + ### Plugin entry fields -| Field | Required | Description | -| ------------- | -------- | ---------------------------------------------------------------- | -| `name` | yes | Plugin name (same rules as marketplace name) | -| `source` | yes | Where to find the plugin (see below) | -| `description` | no | Short description | -| `version` | no | Version string | -| `author` | no | `{ name, email? }` | -| `homepage` | no | URL | -| `category` | no | Category string (e.g. `development`, `productivity`, `security`) | -| `tags` | no | Array of string tags | -| `strict` | no | Boolean | -| `commands` | no | Slash commands provided | -| `agents` | no | Agents provided | -| `hooks` | no | Hook definitions | -| `mcpServers` | no | MCP server definitions | -| `lspServers` | no | LSP server definitions | +| Field | Required | Description | +| ------------- | -------- | --------------------------------------------------------------------------------------- | +| `name` | yes | Plugin name (same rules as marketplace name) | +| `source` | yes | Where to find the plugin (see below) | +| `description` | no | Short description | +| `version` | no | Version string; install version falls back to plugin manifest, source SHA, then `0.0.0` | +| `author` | no | `{ name, email? }` | +| `homepage` | no | URL | +| `repository` | no | Repository URL/string | +| `license` | no | License string | +| `keywords` | no | Array of string keywords | +| `category` | no | Category string (e.g. `development`, `productivity`, `security`) | +| `tags` | no | Array of string tags | +| `strict` | no | Boolean | +| `commands` | no | Slash commands provided | +| `agents` | no | Agents provided | +| `hooks` | no | Hook definitions | +| `mcpServers` | no | MCP server definitions | +| `lspServers` | no | LSP server definitions or path; copied to `.lsp.json` on install | ### Plugin source formats -The `source` field supports several formats: +The `source` field supports these formats. String sources must start with `./` and are resolved inside the marketplace root, after optional `metadata.pluginRoot` is prepended: **Relative path** (within the marketplace repo): ```json -"source": "./plugins/my-plugin" +"source": "./my-plugin" ``` **Git repository URL**: @@ -174,7 +189,7 @@ The `source` field supports several formats: } ``` -**npm package**: +**npm package** (parsed but not installable yet): ```json "source": { @@ -184,20 +199,22 @@ The `source` field supports several formats: } ``` +Current installer behavior rejects npm marketplace sources with `npm plugin sources are not yet supported`; use relative, GitHub, URL, or git-subdir sources. + ## On-disk layout ``` ~/.omp/ marketplaces.json # Registry of added marketplaces plugins/ - installed_plugins.json # User-scoped installed plugins + installed_plugins.json # User-scoped marketplace plugins (version: 2) cache/ - marketplaces/ # Cached marketplace catalogs - plugins/ # Cached plugin directories + marketplaces// # Cached marketplace clone/catalog + plugins/______/ # Cached plugin directories /.omp/ plugins/ - installed_plugins.json # Project-scoped installed plugins + installed_plugins.json # Project-scoped marketplace plugins (version: 2) ``` ## Naming rules diff --git a/docs/mcp-config.md b/docs/mcp-config.md index aee084f8f..a583ecdf9 100644 --- a/docs/mcp-config.md +++ b/docs/mcp-config.md @@ -12,11 +12,13 @@ Source of truth in code: ## Preferred config locations -OMP can discover MCP servers from multiple tools (`.claude/`, `.cursor/`, `.vscode/`, `opencode.json`, and more), but for OMP-native configuration you should usually use one of these files: +OMP can discover MCP servers from multiple tools (`.claude/`, `.cursor/`, `.vscode/`, `opencode.json`, and more), but for OMP-native configuration you should usually use one of these primary files: - Project: `.omp/mcp.json` - User: `~/.omp/agent/mcp.json` +The native provider also reads `.omp/.mcp.json` and `~/.omp/agent/.mcp.json` for compatibility, but OMP writes to the primary `mcp.json` paths above. + OMP also accepts fallback standalone files in the project root: - `mcp.json` @@ -317,7 +319,27 @@ This matches GitHub's official local Docker image `ghcr.io/github/github-mcp-ser This is the part that usually trips people up. -### In `.omp/mcp.json` and `~/.omp/agent/mcp.json` +### Discovery-time `${...}` expansion + +OMP expands `${VAR}` and `${VAR:-default}` placeholders while discovering MCP configs from OMP-native files and standalone fallback files. Expansion applies recursively to string values in `command`, `args`, `env`, `cwd`, `url`, `headers`, `auth`, and `oauth`; unresolved placeholders remain literal strings. + +Example: + +```json +{ + "mcpServers": { + "github": { + "type": "http", + "url": "https://api.githubcopilot.com/mcp/", + "headers": { + "Authorization": "Bearer ${GITHUB_TOKEN}" + } + } + } +} +``` + +### Pre-connect env/header resolution Before OMP launches a stdio server or makes an HTTP/SSE request, it resolves stdio `env` values and HTTP/SSE `headers` values like this: @@ -345,28 +367,6 @@ That means this is valid and convenient for local secrets: - `"Authorization": "Bearer hardcoded-token"` → use the literal value - `"Authorization": "!printf 'Bearer %s' \"$GITHUB_TOKEN\""` → build the header from a command -### In root `mcp.json` and `.mcp.json` - -The standalone fallback loader also expands `${VAR}` and `${VAR:-default}` inside strings during discovery for `command`, `args`, `env`, `cwd`, `url`, `headers`, `auth`, and `oauth`. - -Example: - -```json -{ - "mcpServers": { - "github": { - "type": "http", - "url": "https://api.githubcopilot.com/mcp/", - "headers": { - "Authorization": "Bearer ${GITHUB_TOKEN}" - } - } - } -} -``` - -If you want the least surprising OMP behavior, prefer `.omp/mcp.json` or `~/.omp/agent/mcp.json` and use explicit env/header values. - ## `disabledServers` `disabledServers` is read from the user config file (`~/.omp/agent/mcp.json`) when a server is discovered from any source and you want OMP to ignore it without editing that other tool's config. diff --git a/docs/mcp-protocol-transports.md b/docs/mcp-protocol-transports.md index 76c0417e6..c9c8f46e0 100644 --- a/docs/mcp-protocol-transports.md +++ b/docs/mcp-protocol-transports.md @@ -185,7 +185,7 @@ For `notify()`: - timeout uses an internal `AbortController` (`config.timeout ?? 30000`) - there is no external abort option on the transport interface -For HTTP OAuth configs managed by `MCPManager`, `request()` retries once on `HTTP 401`/`403` if token refresh returns replacement headers. +For HTTP OAuth configs managed by `MCPManager`, outbound requests and best-effort server-request responses retry once on `HTTP 401`/`403` if token refresh returns replacement headers. ## HTTP error propagation @@ -212,14 +212,15 @@ Two SSE paths exist: 2. **Background SSE listener** (`startSSEListener()`) - optional GET listener for server-initiated notifications and server-to-client requests - `connectToServer()` starts it for HTTP/SSE transports after `initialize` and before `notifications/initialized` - - if GET returns `405`, another non-OK status, or no body, listener silently disables itself + - listener startup waits up to one second, or less for very small request timeouts; `timeout: 0` / `OMP_MCP_TIMEOUT_MS=0` disables that startup deadline + - if GET returns `405`, another non-OK status, no body, or times out, listener silently disables itself ## Malformed payload and disconnect handling SSE JSON parsing errors bubble out of `readSseJson` and reject request/listener. - Request SSE parse errors reject the active request. -- Background listener errors trigger `onError` (except AbortError). +- Background listener errors trigger `onError` (except AbortError), and an established listener ending while still connected triggers `onClose` so the manager can reconnect. - Transport does not restart the listener itself; managed connections may reconnect through manager `onClose` handling. ## `json-rpc.ts` utility vs transport abstraction diff --git a/docs/mcp-runtime-lifecycle.md b/docs/mcp-runtime-lifecycle.md index 420fdae25..b6327d47b 100644 --- a/docs/mcp-runtime-lifecycle.md +++ b/docs/mcp-runtime-lifecycle.md @@ -5,7 +5,7 @@ This document describes how MCP servers are discovered, connected, exposed as to ## Lifecycle at a glance 1. **SDK startup** calls `discoverAndLoadMCPTools()` (unless MCP is disabled). -2. **Discovery** (`loadAllMCPConfigs`) resolves MCP server configs from capability sources, filters disabled/project/Exa entries, and preserves source metadata. +2. **Discovery** (`loadAllMCPConfigs`) resolves MCP server configs from capability sources, filters disabled/project/Exa entries and browser MCP servers when the built-in browser tool is enabled, and preserves source metadata. 3. **Manager connect phase** (`MCPManager.connectServers`) starts per-server connect + `tools/list` in parallel. 4. **Fast startup gate** waits up to 250ms, then may return: - fully loaded `MCPTool`s, @@ -23,7 +23,7 @@ This document describes how MCP servers are discovered, connected, exposed as to `createAgentSession()` in `src/sdk.ts` performs MCP startup when `enableMCP` is true (default): - calls `discoverAndLoadMCPTools(cwd, { ... })`, -- passes `authStorage`, cache storage, and `mcp.enableProjectConfig` setting, +- passes `authStorage`, cache storage, `mcp.enableProjectConfig`, and browser-MCP filtering based on the `browser.enabled` setting, - always sets `filterExa: true`, - logs per-server load/connect errors, - stores returned manager in `toolSession.mcpManager` and session result. @@ -38,7 +38,7 @@ Filtering behavior: - `enableProjectConfig: false` removes project-level entries (`_source.level === "project"`). - `enabled: false` servers are skipped before connect attempts. -- Exa servers are filtered out by default and API keys are extracted for native Exa tool integration. +- Exa servers are filtered out by default and API keys are extracted for native Exa tool integration; browser automation MCP servers are filtered when `filterBrowser` is true. Result includes both `configs` and `sources` (metadata used later for provider labeling). @@ -180,7 +180,7 @@ Operationally: - removes pending entries, source metadata, saved config, resource refresh/subscription state, - detaches `onClose` so explicit close does not trigger reconnect, - closes transport if connected, -- filters manager tool state for names beginning with `mcp__${name}_`. +- removes manager tool entries using the current raw-name prefix filter (`mcp__${name}_`); generated tool names are sanitized by `tool-bridge.ts`. ### Global teardown diff --git a/docs/mcp-server-tool-authoring.md b/docs/mcp-server-tool-authoring.md index 4d5415ea1..2d262a2a9 100644 --- a/docs/mcp-server-tool-authoring.md +++ b/docs/mcp-server-tool-authoring.md @@ -74,17 +74,16 @@ In practice MCP servers also come from higher-priority providers (for example na Key behavior: - transport inferred as `server.transport ?? (command ? "stdio" : url ? "http" : "stdio")` -- disabled servers (`enabled === false`) are dropped before connection +- disabled servers (`enabled === false`) and names in the user `disabledServers` list are dropped before connection - optional fields are preserved when present ### Environment expansion during discovery -`mcp-json.ts` expands env placeholders in string fields with `expandEnvVarsDeep()`: +OMP-native MCP config (`.omp/mcp.json`, `~/.omp/agent/mcp.json`, plus their `.mcp.json` variants) expands `${VAR}` and `${VAR:-default}` placeholders recursively before converting to runtime config. It also accepts boolean/string forms for `enabled` (`true`, `false`, `1`, `0`) and numeric strings for `timeout`. -- supports `${VAR}` and `${VAR:-default}` -- unresolved values remain literal `${VAR}` strings +The standalone fallback provider in `src/discovery/mcp-json.ts` reads project-root `mcp.json` and `.mcp.json`, expands the same `${...}` placeholders, and type-checks `enabled`/`timeout` without coercing string values. -`mcp-json.ts` also performs runtime type checks for user JSON and logs warnings for invalid `enabled`/`timeout` values instead of hard-failing the whole file. +Invalid `enabled`/`timeout` values are ignored with warnings rather than failing the whole file. ## 3) Auth and runtime value resolution @@ -138,7 +137,7 @@ This avoids many collisions, but not all. Different raw names can still sanitize ### Schema mapping -`tool-bridge.ts` passes each MCP `inputSchema` through `sanitizeSchemaForMCP()` before registering it as a `CustomTool` schema. +`tool-bridge.ts` passes each MCP `inputSchema` through `normalizeSchemaForMCP()` before registering it as a `CustomTool` schema. ### Execution mapping diff --git a/docs/memory.md b/docs/memory.md index 4b2bc64fc..d5c07f9ba 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -1,12 +1,12 @@ # Autonomous Memory -When enabled, the agent automatically extracts durable knowledge from past sessions and injects a compact summary into each new session. Over time it builds a project-scoped memory store — technical decisions, recurring workflows, pitfalls — that carries forward without manual effort. +When the local memory backend is enabled, the agent automatically extracts durable knowledge from past sessions and injects a compact summary into future sessions for the same project. Over time it builds a project-scoped memory store — technical decisions, recurring workflows, pitfalls — that carries forward without manual effort. -Disabled by default. Enable via `/settings` or `config.yml`: +Disabled by default. Enable the local summary pipeline via `/settings` or `config.yml`: ```yaml -memories: - enabled: true +memory: + backend: local ``` ## Usage @@ -31,17 +31,19 @@ The agent can read memory files directly using `memory://` URLs with the `read` ### `/memory` slash command -| Subcommand | Effect | -| --------------------- | ---------------------------------------------- | -| `view` | Show the current memory injection payload | -| `clear` / `reset` | Delete all memory data and generated artifacts | -| `enqueue` / `rebuild` | Force consolidation to run at next startup | +| Subcommand | Effect | +| --------------------- | --------------------------------------------------------- | +| `view` | Show the current backend injection payload | +| `stats` | Show backend-specific memory statistics, when supported | +| `diagnose` | Show backend-specific diagnostics, when supported | +| `clear` / `reset` | Delete active backend memory data/artifacts | +| `enqueue` / `rebuild` | Force consolidation/retention work for the active backend | ## How it works -Memories are built by a background pipeline that runs at startup or when manually triggered via slash command. +Local summary memories are built by a background pipeline that runs at startup or when manually triggered via slash command. The pipeline is skipped for subagents and for sessions that are not persisted to a session file. -**Phase 1 — per-session extraction:** For each past session that has changed since it was last processed, a model reads the session history and extracts durable signal: technical decisions, constraints, resolved failures, recurring workflows. Sessions that are too recent, too old, or currently active are skipped. Each extraction produces a raw memory block and a short synopsis for that session. +**Phase 1 — per-session extraction:** For each past session that has changed since it was last processed, a model reads the session history and extracts durable signal: technical decisions, constraints, resolved failures, recurring workflows. Sessions that are too recent, too old, currently active, or beyond the configured scan/age limits are skipped. Each extraction produces a raw memory block and a short synopsis for that session. **Phase 2 — consolidation:** After extraction, a second model pass reads all per-session extractions and produces three outputs written to disk: @@ -49,9 +51,9 @@ Memories are built by a background pipeline that runs at startup or when manuall - `memory_summary.md` — the compact text injected at session start - `skills/` — reusable procedural playbooks, each in its own subdirectory -Phase 2 uses a lease to prevent double-running when multiple processes start simultaneously. Stale skill directories from prior runs are pruned automatically. +Phase 2 uses a lease and heartbeat to prevent double-running when multiple processes start simultaneously. Stale skill directories from prior runs are pruned automatically. -All output is scanned for secrets before being written to disk. +Consolidated output is redacted for common secret/token patterns before `MEMORY.md`, `memory_summary.md`, or generated skills are written to disk. ### Extraction behavior @@ -77,13 +79,13 @@ If the requested memory role is not configured, memory model resolution falls ba ## Configuration -| Setting | Default | Description | -| ------------------------------------- | ------- | --------------------------------------------------------- | -| `memories.enabled` | `false` | Master switch | -| `memories.maxRolloutAgeDays` | `30` | Sessions older than this are not processed | -| `memories.minRolloutIdleHours` | `12` | Sessions active more recently than this are skipped | -| `memories.maxRolloutsPerStartup` | `64` | Cap on sessions processed in a single startup | -| `memories.summaryInjectionTokenLimit` | `5000` | Max tokens of the summary injected into the system prompt | +| Setting | Default | Description | +| ------------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `memory.backend` | `off` | Select `local` for this pipeline; legacy `memories.enabled: true` is migrated to `memory.backend: local` when no explicit backend is set | +| `memories.maxRolloutAgeDays` | `30` | Sessions older than this are not processed | +| `memories.minRolloutIdleHours` | `12` | Sessions active more recently than this are skipped | +| `memories.maxRolloutsPerStartup` | `64` | Cap on sessions processed in a single startup | +| `memories.summaryInjectionTokenLimit` | `5000` | Max tokens of the summary injected into the system prompt | Additional tuning knobs (concurrency, lease durations, token budgets) are available in config for advanced use. diff --git a/docs/mnemosyne-memory-backend.md b/docs/mnemosyne-memory-backend.md new file mode 100644 index 000000000..482ed8d6a --- /dev/null +++ b/docs/mnemosyne-memory-backend.md @@ -0,0 +1,157 @@ +# Mnemopi memory backend + +Oh My Pi can use `@oh-my-pi/pi-mnemopi` as a local long-term memory backend. + +Set: + +```yaml +memory: + backend: mnemopi +``` + +Example: + +```yaml +memory: + backend: mnemopi +mnemopi: + scoping: per-project-tagged +``` + +With this backend enabled, the coding agent: + +1. Opens one or more local Mnemopi SQLite databases according to the configured bank scoping. +2. Recalls relevant memories into a `` block for the first model turn of a session and refreshes the base prompt if recall happens from the `agent_start` listener. +3. Retains completed conversation turns into the retain bank after agent turns, no more often than `mnemopi.retainEveryNTurns`. +4. Adds recalled memory as extra compaction context when compaction asks the memory backend for `preCompactionContext`. +5. Uses the normal `/memory view`, `/memory stats`, `/memory diagnose`, `/memory clear`, and `/memory enqueue` commands through the shared memory backend interface. + +Recalled memory is background context, not instructions. Current user messages and tool output take precedence when they conflict. + +## Settings + +| Setting | Default | Description | +| ------------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `memory.backend` | `off` | Set to `mnemopi` to enable this backend. | +| `mnemopi.dbPath` | agent memories dir | Optional SQLite database path. | +| `mnemopi.bank` | project directory name | Base bank name passed to `Mnemopi`; the coding-agent wrapper scopes from this base according to `mnemopi.scoping`. | +| `mnemopi.scoping` | `per-project` | Memory visibility mode: `global` = one shared bank, `per-project` = isolated project memory, `per-project-tagged` = project-local writes plus global recall visibility. | +| `mnemopi.autoRecall` | `true` | Recall memory on the first turn of a session. | +| `mnemopi.autoRetain` | `true` | Retain completed turns automatically. | +| `mnemopi.retainEveryNTurns` | `4` | Minimum user turns between automatic retain writes. | +| `mnemopi.recallLimit` | `8` | Maximum recalled memories in the prompt block. | +| `mnemopi.recallContextTurns` | `3` | Prior user-bounded turns included in recall queries. | +| `mnemopi.recallMaxQueryChars` | `4000` | Maximum composed recall query length. | +| `mnemopi.injectionTokenLimit` | `5000` | Approximate token budget for memory prompt injection. | +| `mnemopi.debug` | `false` | Enable debug logging for backend failures. | +| `mnemopi.noEmbeddings` | `false` | Pass `noEmbeddings` to `Mnemopi` and force FTS-only recall. | +| `mnemopi.embeddingModel` | env/default | Embedding model passed to `Mnemopi`. | +| `mnemopi.embeddingApiUrl` | env/default | OpenAI-compatible embedding endpoint passed to `Mnemopi`. | +| `mnemopi.embeddingApiKey` | env/default | Embedding API key passed to `Mnemopi`. | +| `mnemopi.llmMode` | `smol` | `smol` uses the configured pi-ai smol model, `remote` uses the settings below, and `none` disables LLM calls. | +| `mnemopi.llmBaseUrl` | env/default | OpenAI-compatible LLM endpoint for `llmMode: remote`. | +| `mnemopi.llmApiKey` | env/default | LLM API key for `llmMode: remote`. | +| `mnemopi.llmModel` | env/default | LLM model id for `llmMode: remote`. | + +## Scoping + +The coding-agent wrapper applies scoping on top of the underlying `Mnemopi` package: + +- `global` uses one shared bank for recall and writes. +- `per-project` writes to and recalls from a bank derived from the current git repository root (or cwd) plus a stable hash. +- `per-project-tagged` writes to the project-local bank and recalls from both the project-local bank and the shared global bank, with duplicate recall results merged. + +The combined project-plus-global behavior lives in the wrapper. The `@oh-my-pi/pi-mnemopi` package itself still exposes banks and constructor options directly, including `bank` for selecting a bank name. Project-local banks other than the shared bank are stored as sibling bank databases managed by Mnemopi's `BankManager`. + +## LLM and embeddings + +The backend passes these settings to the `Mnemopi` constructor; if a setting is omitted, Mnemopi falls back to its `MNEMOPI_*` environment defaults. The backend does not download or run a local GGUF LLM. LLM-dependent paths use a configured pi-ai model, a dynamic completion function, a remote OpenAI-compatible endpoint, or deterministic no-LLM fallbacks. + +FTS-only: + +```yaml +memory: + backend: mnemopi +mnemopi: + noEmbeddings: true +``` + +Equivalent constructor shape: + +```ts +new Mnemopi({ noEmbeddings: true }); +``` + +Remote embeddings: + +```yaml +mnemopi: + embeddingModel: text-embedding-3-small + embeddingApiUrl: https://api.openai.com/v1 + embeddingApiKey: ${OPENAI_API_KEY} +``` + +Equivalent constructor shape: + +```ts +new Mnemopi({ + embeddingModel: "text-embedding-3-small", + embeddingApiUrl: "https://api.openai.com/v1", + embeddingApiKey, +}); +``` + +Remote LLM: + +```yaml +mnemopi: + llmMode: remote + llmBaseUrl: https://api.openai.com/v1 + llmApiKey: ${OPENAI_API_KEY} + llmModel: gpt-4.1-mini +``` + +Equivalent constructor shapes: + +```ts +new Mnemopi({ llm: { baseUrl, apiKey, model } }); +new Mnemopi({ llmBaseUrl: baseUrl, llmApiKey: apiKey, llmModel: model }); +``` + +Dynamic function LLM for rotating OAuth tokens: + +```ts +new Mnemopi({ + llm: async (prompt, opts) => { + const token = await getFreshOauthToken(); + return await completeWithPiAi(prompt, { + token, + maxTokens: opts?.maxTokens, + temperature: opts?.temperature, + }); + }, +}); +``` + +pi-ai smol model LLM: + +```yaml +mnemopi: + llmMode: smol +``` + +The coding agent resolves its configured smol role and passes a dynamic completion function so every Mnemopi LLM call can fetch the current provider credentials at call time: + +```ts +new Mnemopi({ + llm: async (prompt, opts) => completeSmolWithCurrentAuth(prompt, opts), +}); +``` + +## Operational notes + +- The default shared database lives under the agent memories directory in `mnemopi/mnemopi.db`; project-scoped banks use sibling database paths under that Mnemopi directory. +- `/memory clear` removes every scoped Mnemopi SQLite database and sidecar WAL/SHM files for the active configuration. +- `/memory enqueue` forces retention of the current session, flushes pending fact extractions, and runs Mnemopi sleep/consolidation. +- `/memory stats` and `/memory diagnose` render backend-specific bank statistics/diagnostics when the Mnemopi backend is active. +- Subagents do not own separate Mnemopi retain loops; they alias the parent state when a parent Mnemopi state exists, and otherwise remain inert. diff --git a/docs/models.md b/docs/models.md index f8a311697..7539d0925 100644 --- a/docs/models.md +++ b/docs/models.md @@ -55,7 +55,7 @@ providers: X-Team: platform authHeader: true auth: apiKey - disableStrictTools: false # set true for Anthropic-compatible endpoints that reject the strict field + disableStrictTools: false # set true for Anthropic-compatible endpoints that reject the strict field discovery: type: ollama modelOverrides: @@ -103,7 +103,8 @@ providers: ### Allowed auth/discovery values - `auth`: `apiKey` (default), `none`, or `oauth`; for `models.yml` custom models, `oauth` is accepted by schema but does not waive the `apiKey` requirement -- `discovery.type`: `ollama`, `llama.cpp`, or `lm-studio` +- `discovery.type`: `ollama`, `llama.cpp`, `lm-studio`, `openai-models-list`, or `proxy` +- `transport`: `pi-native` only. When set, every model under that provider is sent to an `omp auth-gateway` compatible `baseUrl` via `POST /v1/pi/stream`; `apiKey` is the gateway bearer. ## Validation rules (current) @@ -120,6 +121,7 @@ Required: Must define at least one of: - `baseUrl` +- `apiKey` - `headers` - `compat` - `disableStrictTools` @@ -229,7 +231,7 @@ Provider defaults vs per-model overrides: - Provider `headers` are baseline. - Model `headers` override provider header keys. -- `modelOverrides` can override model metadata (`name`, `reasoning`, `input`, `cost`, `contextWindow`, `maxTokens`, `headers`, `compat`, `contextPromotionTarget`). +- `modelOverrides` can override model metadata (`name`, `reasoning`, `thinking`, `input`, `cost`, `premiumMultiplier`, `contextWindow`, `maxTokens`, `headers`, `compat`, `contextPromotionTarget`). - `compat` is deep-merged for nested routing blocks (`openRouterRouting`, `vercelGatewayRouting`, `extraBody`). ## Runtime discovery integration @@ -288,6 +290,33 @@ providers: type: llama.cpp ``` +### Proxy discovery (`discovery.type: proxy`) + +For Anthropic+OpenAI-compatible proxies (new-api / one-api / similar) +that expose both `/v1/messages` and `/v1/chat/completions` behind the same +host. Discovery hits `GET /v1/models` (10s timeout, OpenAI-style payload) and +derives each model's `api` from the entry's `supported_endpoint_types`: + +- contains `"anthropic"` -> `api: anthropic-messages` (routes via `/v1/messages`) +- contains `"openai"` -> `api: openai-completions` (routes via `/v1/chat/completions`) +- otherwise -> falls back to provider-level `api` if set, else dropped + +Provider-level `api` is **optional** with `discovery.type: proxy` because the +per-model wire is auto-detected. The Anthropic SDK strips a trailing `/v1` +from `baseUrl` before appending `/v1/messages`, so a single discovery `baseUrl` +(ending in `/v1`) round-trips correctly to both wires. + +```yaml +providers: + newapi-reseller: + baseUrl: https://api.example.com/v1 + apiKey: xxxx + authHeader: true # injects Authorization: Bearer for openai models + disableStrictTools: true # most anthropic-fronted proxies reject `strict` + discovery: + type: proxy +``` + ### Extension provider registration Extensions can register providers at runtime (`pi.registerProvider(...)`), including: @@ -463,21 +492,21 @@ Configure fallback directly in model metadata via `contextPromotionTarget`. - `provider/model-id` (explicit) - `model-id` (resolved within current provider) -Example (`models.yml`) for Spark -> non-Spark on the same provider: +Example (`models.yml`) for an explicit OpenAI fallback: ```yaml providers: openai-codex: modelOverrides: - gpt-5.3-codex-spark: - contextPromotionTarget: openai-codex/gpt-5.3-codex + gpt-5.5: + contextPromotionTarget: openai-codex/gpt-5.4 ``` -The built-in model generator also assigns this automatically for `*-spark` models when a same-provider base model exists. +The built-in model policy currently links OpenAI `codex-spark` variants to `gpt-5.5`, and `gpt-5.5` to `gpt-5.4`, when that target exists on the same provider/API. ## Compatibility and routing fields -The `compat` block on a provider or model overrides the URL-based auto-detection in `packages/ai/src/providers/openai-completions-compat.ts`. It is validated by `OpenAICompatSchema` in `packages/coding-agent/src/config/model-registry.ts` and consumed by every `openai-completions` transport (`packages/ai/src/providers/openai-completions.ts`). The canonical type is `OpenAICompat` in `packages/ai/src/types.ts`. +The `compat` block on a provider or model overrides the URL-based auto-detection in `packages/ai/src/providers/openai-completions-compat.ts`. It is validated by `OpenAICompatSchema` in `packages/coding-agent/src/config/models-config-schema.ts` and consumed by every `openai-completions` transport (`packages/ai/src/providers/openai-completions.ts`). The canonical type is `OpenAICompat` in `packages/ai/src/types.ts`. `models.yml` accepts the following keys (all optional; unset falls back to URL detection): @@ -485,10 +514,12 @@ Request shaping: - `supportsStore` — emit `store: false` on requests. Default: auto (off for non-standard endpoints). - `supportsDeveloperRole` — use the `developer` system role for reasoning models instead of `system`. Default: auto. +- `supportsMultipleSystemMessages` — preserve separate leading system/developer messages instead of coalescing them. Default: auto (known OpenAI-compatible hosted APIs preserve; strict-template/local hosts coalesce). - `supportsUsageInStreaming` — send `stream_options: { include_usage: true }` to receive token usage on streaming responses. Default: `true`. - `maxTokensField` — `"max_completion_tokens"` or `"max_tokens"`. Default: auto. - `supportsToolChoice` — emit the `tool_choice` parameter when the caller forces a specific tool. Default: `true`. Set `false` for endpoints that 400 on `tool_choice` (e.g. DeepSeek when reasoning is on). - `disableReasoningOnForcedToolChoice` — drop `reasoning_effort` / OpenRouter `reasoning` whenever `tool_choice` forces a call. Default: auto (Kimi/Anthropic-fronted endpoints). +- `disableReasoningOnToolChoice` — drop reasoning fields whenever any `tool_choice` is sent. Default: auto (DeepSeek reasoning models). - `extraBody` — extra top-level fields merged into every request body (gateway hints, controller selectors, etc.). Reasoning / thinking: @@ -498,6 +529,7 @@ Reasoning / thinking: - `thinkingFormat` — request shape for thinking: `"openai"` (`reasoning_effort`), `"openrouter"` (`reasoning: { effort }`), `"zai"` (`thinking: { type: "enabled" }`), `"qwen"` (top-level `enable_thinking`), or `"qwen-chat-template"` (`chat_template_kwargs.enable_thinking`). Default: `"openai"`. - `reasoningContentField` — assistant field carrying chain-of-thought: `"reasoning_content"`, `"reasoning"`, or `"reasoning_text"`. Default: auto. - `requiresReasoningContentForToolCalls` — assistant tool-call turns must round-trip the reasoning field (DeepSeek-R1, Kimi, OpenRouter when reasoning is on). Default: `false`. +- `allowsSyntheticReasoningContentForToolCalls` — allow a placeholder reasoning field when a prior assistant tool-call turn lacks provider reasoning content. Default: `true`; set `false` for providers that validate the exact reasoning value. - `requiresAssistantContentForToolCalls` — assistant tool-call turns must include non-empty text content (Kimi). Default: `false`. Tool / message normalization: @@ -518,7 +550,7 @@ Provider-level `compat` is the baseline; per-model `compat` is deep-merged on to ### Anthropic compatibility (`anthropic-messages`) -For `anthropic-messages` models the runtime uses a separate `AnthropicCompat` shape (`packages/ai/src/types.ts`). The `models.yml` schema currently exposes only the strict-tools opt-out as a top-level provider field (see below); the remaining Anthropic-side knobs (`disableAdaptiveThinking`, `supportsEagerToolInputStreaming`, `supportsLongCacheRetention`) are set by built-in catalog metadata and are not user-configurable from `models.yml`. +For `anthropic-messages` models the runtime uses a separate `AnthropicCompat` shape (`packages/ai/src/types.ts`). The `models.yml` schema currently exposes only the strict-tools opt-out as a top-level provider field (see below); the remaining Anthropic-side knobs (`disableAdaptiveThinking`, `supportsEagerToolInputStreaming`, `supportsLongCacheRetention`, `supportsMidConversationSystem`) are set by built-in catalog metadata and are not user-configurable from `models.yml`. ### Strict tool schemas (`disableStrictTools`) @@ -555,6 +587,7 @@ plus the OpenAI strict-mode sanitize+enforce pipeline). See edge cases (local `$ref` inlining, single-item `allOf` collapse, `anyOf`-wrapper description hoist, enum/const primitive-type inference) and the per-provider dispatcher mapping. + ## Practical examples ### Local OpenAI-compatible endpoint (no auth) @@ -579,7 +612,7 @@ providers: apiKey: ANTHROPIC_PROXY_API_KEY api: anthropic-messages authHeader: true - disableStrictTools: true # if the proxy doesn't support strict tool schemas + disableStrictTools: true # if the proxy doesn't support strict tool schemas models: - id: claude-sonnet-4-20250514 name: Claude Sonnet 4 (Proxy) diff --git a/docs/natives-addon-loader-runtime.md b/docs/natives-addon-loader-runtime.md index 5fc02a185..9423dac49 100644 --- a/docs/natives-addon-loader-runtime.md +++ b/docs/natives-addon-loader-runtime.md @@ -15,11 +15,12 @@ This document covers the runtime loader shipped by `@oh-my-pi/pi-natives`: how ` The loader is intentionally narrow: - Build a platform/CPU-aware candidate list for addon filenames and directories. -- Treat an embedded-addon manifest as the authoritative compiled-binary signal when present. -- Optionally materialize an embedded addon into a versioned per-user cache directory. -- Attempt candidates in deterministic order and return the first addon that `require(...)` loads. +- Treat an embedded-addon manifest as a compiled-binary signal when present. +- Optionally materialize embedded addon archive contents into a versioned per-user cache directory. +- On Windows `node_modules` installs, stage addon files into the versioned cache to avoid locked-DLL update failures. +- Attempt candidates in deterministic order and return the first addon that `require(...)` loads and validates. -The current loader does **not** run a separate `validateNative(...)` export-presence gate. API shape is provided by the generated N-API binding file (`native/index.d.ts`) and the loaded addon itself. A stale binary therefore normally fails as a missing property or native load error rather than as a custom "missing exports" validation error. +For install and compiled-binary paths, the loader verifies a release sentinel export named from `package.json#version` (for example `__piNativesV15_7_2`). Workspace-dev loads skip this validation so a local checkout can rebuild after a pull. The loader does not validate the full export surface; stale same-version or incomplete binaries still surface as missing members or native errors at use sites. ## Runtime inputs and derived state @@ -28,6 +29,7 @@ At module initialization, `native/index.js` computes: - **Platform tag**: `${process.platform}-${process.arch}` (for example `darwin-arm64`). - **Package version**: from `packages/natives/package.json`. - **Core directories**: + - `leafPackageDir`: directory of the platform leaf package, resolved via `require.resolve("@oh-my-pi/pi-natives-/package.json")`; `null` when no leaf is installed (e.g. local dev). - `nativeDir`: package-local `packages/natives/native`. - `execDir`: directory containing `process.execPath`. - `versionedDir`: `/`. @@ -41,6 +43,7 @@ At module initialization, `native/index.js` computes: - embedded-addon manifest is non-null, - `PI_COMPILED` env var is set, - `import.meta.url` contains Bun embedded markers (`$bunfs`, `~BUN`, `%7EBUN`). +- **Windows staging mode** (`shouldStageNodeModulesAddon`): true only on Windows, in non-compiled mode, when `nativeDir` is inside `node_modules`. - **Variant override**: `PI_NATIVE_VARIANT` (`modern`/`baseline` only; invalid values ignored). - **Selected variant**: explicit override, otherwise runtime AVX2 detection on x64 (`modern` if AVX2, else `baseline`). @@ -92,10 +95,15 @@ The default unsuffixed fallback remains part of the x64 candidate list. ### Non-compiled runtime -For each filename, candidates are: +For each filename, candidates are, in order: -1. `/` -2. `/` +1. `/` (omitted when `leafPackageDir` is `null`) +2. `/` +3. `/` + +The leaf package dir comes first so the optional-dependency binary published with the release is preferred over any `.node` left in the core package's `native/` (e.g. a stale local-dev build). + +On Windows installs where `nativeDir` is inside a `node_modules` segment (`shouldStageNodeModulesAddon`), `/` staging candidates are prepended ahead of the leaf candidates so a locked `node_modules` binary can be sidestepped during `bun install -g` updates. The staged file is copied from `leafPackageDir ?? nativeDir` before probing. ### Compiled runtime @@ -106,7 +114,7 @@ For each filename, candidates are: 3. `/` 4. `/` -At load time, an extracted embedded candidate, when produced, is prepended ahead of these de-duplicated candidates. +At load time, an extracted embedded candidate, or a staged Windows candidate when no embedded candidate exists, is prepended ahead of these de-duplicated candidates. ## Embedded addon extraction lifecycle @@ -114,7 +122,8 @@ At load time, an extracted embedded candidate, when produced, is prepended ahead - `platformTag` - `version` -- `files[]` entries with `variant`, `filename`, and `filePath` +- `archive`: `{ format: "tar.gz", filename, filePath }` +- `files[]` entries with `variant`, `filename`, and `size` Extraction (`maybeExtractEmbeddedAddon`) runs only when: @@ -133,11 +142,12 @@ Variant file selection: Materialization: 1. Ensure `` exists. -2. Reuse `/` if it already exists. -3. Otherwise read `selectedEmbeddedFile.filePath` and write the target path. -4. Return the target path as the first candidate. +2. Select `/`. +3. If the current cached file exists and its size matches manifest metadata, reuse it. +4. Otherwise extract `embeddedAddon.archive.filePath` into `` using the manifest `files[]` allowlist. +5. Verify the selected target by size and return it as the first candidate. -Directory creation or write failures are appended to the loader error list; probing continues through normal candidates. +Archive, directory, or write failures are appended to the loader error list; probing continues through normal candidates. ## Lifecycle and state transitions @@ -146,11 +156,14 @@ Init -> Load package metadata and embedded-addon manifest -> Compute platform/version/variant/filenames/candidate paths -> (compiled + embedded manifest matches?) - yes -> try extract to versionedDir (record errors, continue) + yes -> extract archive to versionedDir when needed (record errors, continue) no -> skip extraction + -> (Windows non-compiled node_modules install and no embedded candidate?) + yes -> stage leaf/core addon to versionedDir (record errors, continue) + no -> skip staging -> For each runtime candidate in order: require(candidate) - -> success: return addon exports (READY) + -> sentinel validation passes or is workspace-dev: return addon exports (READY) -> failure: record error, continue -> none loaded: if unsupported platform tag -> throw Unsupported platform @@ -172,7 +185,7 @@ If all candidates fail and `platformTag` is not supported, the loader throws: If the platform is supported but no candidate can be loaded, the final error includes: - `Failed to load pi_natives native addon for ` or ` ()` -- every attempted path with the corresponding `require(...)` error +- every attempted path with the corresponding `require(...)` or sentinel-validation error - mode-specific remediation hints ### Compiled-binary startup failures @@ -182,6 +195,7 @@ Compiled mode diagnostics include: - expected versioned cache target paths (`/`), - remediation to delete the versioned cache and rerun, - direct release download `curl` commands for each expected filename. +- release sentinel mismatch details when a loadable `.node` belongs to another `@oh-my-pi/pi-natives` version. ### Non-compiled startup failures diff --git a/docs/natives-architecture.md b/docs/natives-architecture.md index f8271da77..c705f6aec 100644 --- a/docs/natives-architecture.md +++ b/docs/natives-architecture.md @@ -1,8 +1,8 @@ # Natives Architecture -`@oh-my-pi/pi-natives` is now a two-layer package around a loader: +`@oh-my-pi/pi-natives` is a two-layer package around an ESM loader: -1. **CommonJS loader/package entrypoint** resolves and loads the correct `.node` addon and patches generated enum objects onto the export object. +1. **ESM loader/package entrypoint** resolves and loads the correct `.node` addon with `createRequire`, validates the release sentinel outside workspace-dev loads, and re-exports generated classes/functions plus enum runtime objects as explicit named ESM exports. 2. **Rust N-API module layer** implements the exported functions/classes and emits the generated TypeScript declarations. This document is the foundation for deeper module-level docs. @@ -21,20 +21,20 @@ This document is the foundation for deeper module-level docs. ## Package entrypoint and public surface -`packages/natives/package.json` points directly at generated native bindings: +`packages/natives/package.json` points at generated native artifacts: - `main`: `./native/index.js` - `types`: `./native/index.d.ts` - `exports["."].types`: `./native/index.d.ts` - `exports["."].import`: `./native/index.js` -There is no current `packages/natives/src` TypeScript wrapper layer. Consumers import functions/classes/enums directly from `@oh-my-pi/pi-natives`; the type contract is the generated `native/index.d.ts` plus enum exports appended by `scripts/gen-enums.ts`. +There is no current `packages/natives/src` TypeScript wrapper layer. Consumers import functions/classes/enums directly from `@oh-my-pi/pi-natives`; the type contract is the generated `native/index.d.ts` plus the explicit named exports generated into `native/index.js` by `scripts/gen-enums.ts`. Current capability groups in the generated API include: -- **Search/text/code primitives**: `grep`, `search`, `hasMatch`, `fuzzyFind`, `glob`, `astGrep`, `astEdit`, text width/slicing/wrapping/sanitization, syntax highlighting, token counting. -- **Execution/process/terminal primitives**: `executeShell`, `Shell`, `PtySession`, process-tree helpers, key parsing. -- **System/media/conversion primitives**: clipboard, image resize/encode/SIXEL, HTML-to-Markdown, macOS appearance/power helpers, work profiling, Windows ProjFS overlay helpers. +- **Search/text/code primitives**: `grep`, `search`, `hasMatch`, `fuzzyFind`, `glob`, `astGrep`, `astEdit`, `blockRangeAt`, `summarizeCode`, text width/slicing/wrapping/sanitization, syntax highlighting, token counting. +- **Execution/process/terminal primitives**: `executeShell`, `Shell`, `PtySession`, `Process`, key parsing, bash fixups. +- **System/media/isolation/conversion primitives**: clipboard, SIXEL encoding, HTML-to-Markdown, macOS appearance/power helpers, work profiling, workspace scanning, isolation backend helpers (`iso*`). ## Loader layer @@ -72,7 +72,9 @@ For x64, variant selection uses: ### Binary distribution and extraction model -`packages/natives/package.json` publishes `native/`, which contains the loader, generated declarations, generated enum patch, embedded-addon manifest stub, and prebuilt `.node` artifacts. +The published `@oh-my-pi/pi-natives` package ships **only** the loader layer in `native/`: the ESM loader (`index.js`), generated declarations (`index.d.ts`), the `loader-state.js`/`.d.ts` helpers, and the embedded-addon manifest stub (`embedded-addon.js`). It carries no `.node` binaries. + +Each platform's prebuilt `.node` is published as a separate optional-dependency leaf package — `@oh-my-pi/pi-natives--`, one per supported tag — which the core lists in `optionalDependencies` at the lockstep version during publish. npm/bun install only the leaf whose `os`/`cpu` match the host. The working-tree package keeps built `.node` files under `native/` for local dev; the release-publish rewrite (`prepareNativeCorePackage` in `scripts/ci-release-publish.ts`) strips them from the core tarball, and the leaves are generated by `packages/natives/scripts/gen-npm-packages.ts` (`LEAF_TARGETS`). Adding a build target therefore requires a matching `LEAF_TARGETS` entry, or the binary never reaches npm users. For compiled binaries, loader behavior is: @@ -84,7 +86,9 @@ For compiled binaries, loader behavior is: `getNativesDir()` uses `$XDG_DATA_HOME/omp/natives` when `$XDG_DATA_HOME/omp` exists; otherwise it uses `~/.omp/natives`. -If a populated embedded addon manifest is present, it is also treated as a compiled-binary signal. The loader can extract the matching embedded `.node` into the versioned cache directory before candidate probing. +If a populated embedded addon manifest is present, it is also treated as a compiled-binary signal. Current embedded manifests point at a gzip-compressed tar archive (`embedded-addons..tar.gz`) that contains one or more matching `.node` files. The loader extracts the archive into the versioned cache directory, validates the selected file by size, and prepends that cache path before normal candidate probing. + +For npm/bun installs (non-compiled), `loader-state.js` resolves the platform leaf directory via `require.resolve("@oh-my-pi/pi-natives-/package.json")` and probes its `.node` **before** the core package's `native/` directory and the executable directory. The optional-dependency binary is therefore preferred over any `.node` left in the core (e.g. a stale local-dev build). On Windows `node_modules` installs, the loader first stages the selected leaf/core addon into `//...` and prepends that staged path so running processes do not lock the `node_modules` copy during global updates. ### Failure modes @@ -92,9 +96,8 @@ Loader failures are explicit: - **Unsupported platform tag**: after failed probing, throws with supported platform list. - **No loadable candidate**: throws with all attempted paths and remediation hints. -- **Embedded extraction errors**: directory/write failures are recorded and included in final load diagnostics if no candidate loads. - -The current loader does not perform a separate post-`require` export validation pass. +- **Embedded/staging errors**: directory/write/archive/staging failures are recorded and included in final load diagnostics if no candidate loads. +- **Release mismatch**: outside workspace-dev loads, a candidate that loads but lacks the version sentinel export for `package.json#version` is rejected with a reinstall hint. ## Rust N-API module layer @@ -102,6 +105,7 @@ The current loader does not perform a separate post-`require` export validation - `appearance` - `ast` +- `block` - `clipboard` - `fd` - `fs_cache` @@ -110,19 +114,21 @@ The current loader does not perform a separate post-`require` export validation - `grep` - `highlight` - `html` -- `image` +- `iso` - `keys` -- `language` +- `language` (re-exported from `pi_ast`) - `power` - `prof` -- `projfs_overlay` - `ps` - `pty` - `shell` +- `sixel` +- `summary` - `task` - `text` - `tokens` - `utils` (crate-private helpers) +- `workspace` N-API exports are generated from Rust `#[napi]` functions/classes/objects/enums. Snake_case Rust names are exposed as camelCase JavaScript names unless explicitly configured by napi-rs. @@ -131,8 +137,9 @@ N-API exports are generated from Rust `#[napi]` functions/classes/objects/enums. - **Loader/package ownership (`packages/natives/native`, `packages/natives/scripts`)** - runtime binary selection - CPU variant selection and override handling - - compiled-binary embedded extraction - - generated TypeScript declarations and enum export patching + - compiled-binary embedded archive extraction + - Windows `node_modules` addon staging + - generated TypeScript declarations and explicit ESM export/enum patching - **Rust ownership (`crates/pi-natives/src`)** - algorithmic and system-level implementation - platform-native behavior and performance-sensitive logic @@ -145,16 +152,18 @@ N-API exports are generated from Rust `#[napi]` functions/classes/objects/enums. 1. Consumer imports from `@oh-my-pi/pi-natives`. 2. `native/index.js` computes platform/arch/variant and candidate paths. -3. Optional embedded binary extraction occurs for compiled distributions. -4. The first `require(candidate)` that succeeds becomes the exported addon object. -5. Generated enum objects are appended to `module.exports`. +3. Optional embedded archive extraction or Windows `node_modules` staging can prepend a versioned-cache candidate. +4. Each candidate is `require(...)`d; install/compiled loads must expose the package-version sentinel. +5. The loaded addon object is bound to explicit named ESM exports, including generated enum objects. 6. Caller invokes generated N-API functions/classes directly. ## Glossary - **Native addon**: A `.node` binary loaded via Node-API (N-API). - **Platform tag**: Runtime tuple `platform-arch` (for example `darwin-arm64`). +- **Platform leaf package**: Per-platform npm package `@oh-my-pi/pi-natives-` that carries one platform's prebuilt `.node`. The core depends on every leaf via `optionalDependencies`; the package manager installs only the host-matching one (`os`/`cpu`). - **Variant**: x64 CPU-specific build flavor (`modern` AVX2, `baseline` fallback). - **Generated binding declaration**: `native/index.d.ts` emitted by napi-rs during `build-native.ts`. +- **Version sentinel**: Rust export named from the package version (for example `__piNativesV15_7_2`) that lets the loader reject a `.node` from a different release. - **Compiled binary mode**: Runtime mode where the CLI is bundled and native addons are resolved from embedded/cache paths before package-local paths. -- **Embedded addon**: Build artifact metadata and file references generated into `native/embedded-addon.js` so compiled binaries can extract matching `.node` payloads. +- **Embedded addon**: Build artifact metadata and archive reference generated into `native/embedded-addon.js` so compiled binaries can extract matching `.node` payloads. diff --git a/docs/natives-binding-contract.md b/docs/natives-binding-contract.md index f787b58d2..375d6adf3 100644 --- a/docs/natives-binding-contract.md +++ b/docs/natives-binding-contract.md @@ -2,7 +2,7 @@ This document defines the JS/TS contract between `@oh-my-pi/pi-natives` callers and the loaded N-API addon. -Current package shape is direct-to-native: there is no `packages/natives/src/` TypeScript wrapper layer. The public API is the generated `packages/natives/native/index.d.ts` declaration file, the CommonJS loader in `packages/natives/native/index.js`, and the Rust `#[napi]` exports in `crates/pi-natives/src`. +Current package shape is direct-to-native: there is no `packages/natives/src/` TypeScript wrapper layer. The public API is the generated `packages/natives/native/index.d.ts` declaration file, the ESM loader/export wrapper in `packages/natives/native/index.js`, and the Rust `#[napi]` exports in `crates/pi-natives/src`. ## Implementation files @@ -19,10 +19,10 @@ Current package shape is direct-to-native: there is no `packages/natives/src/` | -| Grep | `search(content, options)` | `grep.rs` | `SearchResult` | -| Grep | `hasMatch(content, pattern, ignoreCase?, multiline?)` | `grep.rs` | `boolean` | -| Fuzzy path search | `fuzzyFind(options)` | `fd.rs` | `Promise` | -| Glob | `glob(options, onMatch?)` | `glob.rs` | `Promise` | -| Glob cache | `invalidateFsScanCache(path?)` | `fs_cache.rs` | `void` | -| AST search/edit | `astGrep(options)`, `astEdit(options)` | `ast.rs` | `Promise<...>` | -| Shell | `executeShell(options, onChunk?)` | `shell.rs` | `Promise` | -| Shell | `new Shell(options?)`, `shell.run(...)`, `shell.abort()` | `shell.rs` | class / promises | -| PTY | `new PtySession()`, `start/write/resize/kill` | `pty.rs` | class / promises | -| Process | `killTree(pid, signal)`, `listDescendants(pid)` | `ps.rs` | sync | -| Keys | `parseKey`, `matchesKey`, Kitty/legacy helpers | `keys.rs` | sync | -| Text | `wrapTextWithAnsi`, `truncateToWidth`, `sliceWithWidth`, `extractSegments`, `visibleWidth` | `text.rs` | sync | -| Highlight | `highlightCode`, `supportsLanguage`, `getSupportedLanguages` | `highlight.rs` | sync | -| HTML | `htmlToMarkdown(html, options?)` | `html.rs` | `Promise` | -| Image | `PhotonImage`, `encodeSixel` | `image.rs` | class / sync / promises | -| Clipboard | `copyToClipboard`, `readImageFromClipboard` | `clipboard.rs` | sync / promise | -| Tokens | `countTokens(input, encoding?)` | `tokens.rs` | sync | -| System | `detectMacOSAppearance`, `MacAppearanceObserver`, `MacOSPowerAssertion`, `getWorkProfile`, ProjFS helpers | `appearance.rs`, `power.rs`, `prof.rs`, `projfs_overlay.rs` | mixed | +| Category | Public JS API | Rust source | Return style | +| ----------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | -------------------------- | +| Grep | `grep(options, onMatch?)` | `grep.rs` | `Promise` | +| Grep | `search(content, options)` | `grep.rs` | `SearchResult` | +| Grep | `hasMatch(content, pattern, ignoreCase?, multiline?)` | `grep.rs` | `boolean` | +| Fuzzy path search | `fuzzyFind(options)` | `fd.rs` | `Promise` | +| Glob/workspace | `glob(options, onMatch?)`, `listWorkspace(options)` | `glob.rs`, `workspace.rs` | `Promise<...>` | +| Glob cache | `invalidateFsScanCache(path?)` | `fs_cache.rs` | `void` | +| AST/block/summary | `astGrep(options)`, `astEdit(options)`, `blockRangeAt(options)`, `summarizeCode(options)` | `ast.rs`, `block.rs`, `summary.rs` | mixed | +| Shell | `executeShell(options, onChunk?)` | `shell.rs` | `Promise` | +| Shell | `new Shell(options?)`, `shell.run(...)`, `shell.abort()` | `shell.rs` | class / promises | +| PTY | `new PtySession()`, `start/write/resize/kill` | `pty.rs` | class / promises | +| Process | `Process.fromPid/fromPath`, `status/children/killTree/terminate/waitForExit` | `ps.rs` | class / mixed | +| Keys | `parseKey`, `matchesKey`, Kitty/legacy helpers | `keys.rs` | sync | +| Text | `wrapTextWithAnsi`, `truncateToWidth`, `sliceWithWidth`, `extractSegments`, `visibleWidth` | `text.rs` | sync | +| Highlight | `highlightCode`, `supportsLanguage`, `getSupportedLanguages` | `highlight.rs` | sync | +| HTML | `htmlToMarkdown(html, options?)` | `html.rs` | `Promise` | +| SIXEL | `encodeSixel` | `sixel.rs` | sync | +| Clipboard | `copyToClipboard`, `readImageFromClipboard` | `clipboard.rs` | sync / promise | +| Tokens | `countTokens(input, encoding?)` | `tokens.rs` | sync | +| System/isolation | `detectMacOSAppearance`, `MacAppearanceObserver`, `MacOSPowerAssertion`, `getWorkProfile`, `iso*` helpers | `appearance.rs`, `power.rs`, `prof.rs`, `iso.rs` | mixed | ## Sync vs async contract differences The contract preserves Rust/N-API call style: -- **Promise-returning exports** for worker-thread or async runtime work (`grep`, `glob`, `fuzzyFind`, `astGrep`, `astEdit`, `htmlToMarkdown`, shell/PTY runs, image parse/resize/encode, clipboard image read). -- **Synchronous exports** for deterministic in-memory transforms/parsers or direct system calls (`search`, `hasMatch`, highlighting, text utilities, token counting, process queries, `copyToClipboard`, `encodeSixel`). -- **Constructor exports** for stateful runtime objects (`Shell`, `PtySession`, `PhotonImage`, macOS observer/power handles). +- **Promise-returning exports** for worker-thread or async runtime work (`grep`, `glob`, `fuzzyFind`, `astGrep`, `astEdit`, `htmlToMarkdown`, shell/PTY runs, `isoStart`/`isoStop`/`isoDiff`, clipboard image read, workspace scan). +- **Synchronous exports** for deterministic in-memory transforms/parsers or direct system calls (`search`, `hasMatch`, highlighting, text utilities, token counting, process construction/status, `copyToClipboard`, `encodeSixel`, isolation probe/resolve helpers). +- **Constructor exports** for stateful runtime objects (`Shell`, `PtySession`, `Process`, macOS observer/power handles). Changing sync ↔ async for an existing export is a breaking public API change because consumers call these exports directly. @@ -94,29 +94,30 @@ Changing sync ↔ async for an existing export is a breaking public API change b - `GrepResult`, `SearchResult`, `GlobResult`, `FuzzyFindResult` - `ShellRunResult`, `ShellExecuteResult`, `PtyRunResult`, `MinimizerResult` -- `AstFindResult`, `AstReplaceResult` -- `System`/media payloads such as `ClipboardImage`, `WorkProfile`, `ParsedKittyResult` +- `AstFindResult`, `AstReplaceResult`, `BlockRange`, `SummaryResult` +- `System`/media/isolation payloads such as `ClipboardImage`, `WorkProfile`, `ParsedKittyResult`, `IsoResolveResult` Runtime shape correctness is owned by napi-rs and the Rust implementation. ### Enum patterns -Native enums are represented in generated declarations and also appended to `module.exports` by `scripts/gen-enums.ts`, because the loader is hand-maintained CommonJS around the generated addon. Current enum objects include: +Native enums are represented in generated declarations and also emitted as runtime objects by `scripts/gen-enums.ts`, because napi-rs string enums are TS-only without explicit JS exports. Current enum objects include: - `AstMatchStrictness` - `Ellipsis` - `Encoding` - `FileType` - `GrepOutputMode` -- `ImageFormat` +- `IsoBackendKind` +- `IsoChangeKind` - `KeyEventType` - `MacOSAppearance` -- `SamplingFilter` +- `ProcessStatus` ## Error behavior and caveats - Addon load failure or unsupported platform throws during package import from `native/index.js`. -- The loader does not verify the full export set after `require(...)`; stale or mismatched binaries surface as native load errors or missing members at use sites. +- The loader rejects install/compiled candidates that lack the package-version sentinel export. It does not verify the full export set after `require(...)`; stale same-version or incomplete binaries surface as native load errors or missing members at use sites. - N-API conversion validates basic argument conversion, but TS optional fields do not guarantee semantic validity for untyped callers. - Numeric enum declarations do not prevent out-of-range numeric values from untyped callers unless the Rust function rejects them during conversion. - Callback exports use napi-rs `ThreadsafeFunction` shape: `(error: Error | null, value) => void`. Native code generally emits successful values; hard failures reject/throw through the owning call. diff --git a/docs/natives-build-release-debugging.md b/docs/natives-build-release-debugging.md index 83872e8c3..b779f50ad 100644 --- a/docs/natives-build-release-debugging.md +++ b/docs/natives-build-release-debugging.md @@ -24,8 +24,8 @@ It follows the architecture terms from `docs/natives-architecture.md`: `packages/natives/package.json` scripts: -- `bun scripts/build-native.ts` (`build`) → N-API build, addon install, generated declarations install, enum export patch. -- `bun scripts/embed-native.ts` (`embed:native`) → generate `native/embedded-addon.js` from built files. +- `bun scripts/build-native.ts` (`build`) → N-API build, addon install, generated declarations install, explicit ESM export and enum runtime patch. +- `bun scripts/embed-native.ts` (`embed:native`) → generate `native/embedded-addon.js` plus `native/embedded-addons..tar.gz` from built files. Root scripts include `build:native` as `bun --cwd=packages/natives run build`. @@ -51,10 +51,10 @@ After napi-rs succeeds, `build-native.ts`: 1. resolves the built addon in the isolated output directory; 2. normalizes its name to `pi_natives.-(-variant).node` when needed; 3. installs the addon into `packages/natives/native/` with temp-file + rename semantics; -4. copies generated `index.js` and `index.d.ts` into `packages/natives/native/` when present; -5. runs `generateEnumExports()` to append enum runtime objects to `native/index.js`. +4. copies generated `index.d.ts` into `packages/natives/native/`; +5. runs `generateEnumExports()` to render explicit named ESM exports for classes/functions and runtime enum objects in the checked-in `native/index.js`. -Windows locked-DLL replacement failures are reported with an explicit close-running-processes hint. +Windows locked-DLL update failures are handled at runtime by staging install candidates into the versioned native cache; install/rename failures during local builds still include explicit file-operation diagnostics. ## Target/variant model and naming conventions @@ -85,7 +85,7 @@ Runtime x64 candidate order also includes the unsuffixed default filename after ## Runtime flags - `PI_NATIVE_VARIANT`: x64 runtime override; valid values are `modern` and `baseline`. -- `PI_COMPILED`: legacy compiled-mode signal. A populated embedded-addon manifest is also a compiled-mode signal and is the authoritative signal for Bun standalone builds that do not preserve `process.env.PI_COMPILED`. +- `PI_COMPILED`: legacy compiled-mode signal. A populated embedded-addon manifest is also a compiled-mode signal; compiled release builds additionally define `process.env.PI_COMPILED="true"` during `bun build --compile`. ## Build-time flags/options @@ -115,11 +115,11 @@ Runtime x64 candidate order also includes the unsuffixed default filename after 4. **Compile**: run napi-rs against `crates/pi-natives` into an isolated output directory. 5. **Locate artifact**: accept the canonical filename or a single napi-rs-generated `pi_natives.-*.node` candidate. 6. **Install**: copy/rename addon into `packages/natives/native`. -7. **Install generated bindings**: copy `index.js`/`index.d.ts` if needed. -8. **Patch enums**: append generated enum runtime exports. +7. **Install generated declarations**: copy `index.d.ts`. +8. **Patch exports/enums**: regenerate explicit ESM exports and enum runtime objects. 9. **Cleanup**: remove the temporary build output directory. -Failure exits have explicit error text for invalid variants, failed napi build, missing/multiple output artifacts, generated binding install failure, and install/rename failure. +Failure exits have explicit error text for invalid variants, failed napi build, missing/multiple output artifacts, generated binding install failure, stripped CI ELF artifacts that still contain forbidden symbol/string-table sections, and install/rename failure. ### Embed lifecycle (`embed-native.ts`) @@ -128,7 +128,7 @@ Failure exits have explicit error text for invalid variants, failed napi build, - x64 looks for `modern` and `baseline` files; - non-x64 looks for one default file. 3. **Validate availability**: at least one expected file must exist in `packages/natives/native`. -4. **Generate manifest** (`native/embedded-addon.js`) with Bun `file` imports and package version. +4. **Generate archive + manifest**: write `native/embedded-addons.-.tar.gz` containing all available target addon files and `native/embedded-addon.js` with package version, archive metadata, and file sizes. 5. **Runtime extraction ready** for compiled mode. `--reset` writes the null manifest stub (`embeddedAddon = null`) without validating addon availability. @@ -148,12 +148,13 @@ Typical local loop: In compiled mode (`PI_COMPILED`, Bun embedded URL markers, or populated embedded manifest): 1. Loader computes versioned cache dir: `/`. -2. If embedded manifest matches current platform+version, loader may extract the selected embedded file into that versioned dir. +2. If embedded manifest matches current platform+version, loader extracts the selected file from `embedded-addons..tar.gz` into that versioned dir when the cached file is absent or has the wrong size. 3. Runtime candidate order includes: + - extracted versioned cache path, if available, - versioned cache dir, - legacy compiled-binary dir (`%LOCALAPPDATA%/omp` on Windows, `~/.local/bin` elsewhere), - package/executable directories. -4. First successfully loaded addon is returned. +4. First successfully loaded addon with the expected version sentinel is returned. This is why packaging + runtime loader expectations must align: filenames, platform tags, CPU variants, and embedded manifest version must match what `native/index.js` probes. @@ -161,13 +162,13 @@ This is why packaging + runtime loader expectations must align: filenames, platf Generated declarations currently include exports from these Rust modules: -| Area | Representative JS exports | Rust source | -| ---------------------- | ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | -| Search | `grep`, `search`, `hasMatch`, `fuzzyFind`, `glob`, `invalidateFsScanCache` | `grep.rs`, `fd.rs`, `glob.rs`, `fs_cache.rs` | -| AST | `astGrep`, `astEdit` | `ast.rs` | -| Text/highlight/tokens | `visibleWidth`, `truncateToWidth`, `highlightCode`, `countTokens` | `text.rs`, `highlight.rs`, `tokens.rs` | -| Shell/PTY/process/keys | `executeShell`, `Shell`, `PtySession`, `killTree`, `parseKey` | `shell.rs`, `pty.rs`, `ps.rs`, `keys.rs` | -| Media/system | `PhotonImage`, `encodeSixel`, clipboard, macOS appearance/power, `getWorkProfile`, ProjFS helpers | `image.rs`, `clipboard.rs`, `appearance.rs`, `power.rs`, `prof.rs`, `projfs_overlay.rs` | +| Area | Representative JS exports | Rust source | +| ---------------------- | ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| Search/workspace | `grep`, `search`, `hasMatch`, `fuzzyFind`, `glob`, `listWorkspace`, `invalidateFsScanCache` | `grep.rs`, `fd.rs`, `glob.rs`, `workspace.rs`, `fs_cache.rs` | +| AST/block/summary | `astGrep`, `astEdit`, `blockRangeAt`, `summarizeCode` | `ast.rs`, `block.rs`, `summary.rs` | +| Text/highlight/tokens | `visibleWidth`, `truncateToWidth`, `highlightCode`, `countTokens` | `text.rs`, `highlight.rs`, `tokens.rs` | +| Shell/PTY/process/keys | `executeShell`, `Shell`, `PtySession`, `Process`, `parseKey`, `applyBashFixups` | `shell.rs`, `pty.rs`, `ps.rs`, `keys.rs` | +| Media/system/iso | `encodeSixel`, clipboard, macOS appearance/power, `getWorkProfile`, `isoBackend`, `isoStart`, `isoDiff` | `sixel.rs`, `clipboard.rs`, `appearance.rs`, `power.rs`, `prof.rs`, `iso.rs` | ## Failure behavior and diagnostics @@ -186,18 +187,19 @@ Generated declarations currently include exports from these Rust modules: - Unsupported platform tag: throws with supported platform list after probing fails. - No candidate could load: throws with full candidate error list and mode-specific remediation hints. -- Embedded extraction problems: extraction mkdir/write errors are recorded and included in final diagnostics if load fails. +- Embedded extraction and Windows staging problems: archive/mkdir/write/copy errors are recorded and included in final diagnostics if load fails. +- Version mismatch: install/compiled loads that lack the package-version sentinel are rejected during candidate probing. ## Troubleshooting matrix -| Symptom | Likely cause | Verify | Fix | -| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | -| `Cannot find module` or dynamic library load error for every candidate | Missing release artifact, wrong platform tag, or stale compiled cache | Inspect loader error list and `packages/natives/native` filenames | Build correct target/variant; delete stale cache for the package version | -| Export is missing at runtime but present in TypeScript | Stale `.node` loaded, generated declarations newer than binary, or Rust export not compiled | Require the actual candidate and inspect `Object.keys(mod)` | Rebuild native package and remove stale candidate/cache paths | -| x64 machine loads baseline when modern expected | `PI_NATIVE_VARIANT=baseline`, no AVX2 detected, or modern file unavailable | Check env and filenames in `native/` | Build modern variant (`TARGET_VARIANT=modern ... build`) and ship it | -| Cross-build produces wrong-labeled binary | Mismatch between `CROSS_TARGET` and `TARGET_PLATFORM`/`TARGET_ARCH`, or missing x64 variant | Confirm env tuple and output filename | Re-run with consistent env values and explicit x64 `TARGET_VARIANT` | -| Compiled binary fails after upgrade | Stale extracted cache or embedded manifest version mismatch | Inspect `/` and loader error list | Delete versioned cache for the package version; regenerate embedded manifest during packaging | -| `embed:native` fails with `No native addons found` | Required platform artifact was not built before embedding | Check expected list in error text | Build at least one expected artifact for the target, then rerun `embed:native` | +| Symptom | Likely cause | Verify | Fix | +| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| `Cannot find module` or dynamic library load error for every candidate | Missing release artifact, wrong platform tag, or stale compiled cache | Inspect loader error list and `packages/natives/native` filenames | Build correct target/variant; delete stale cache for the package version | +| Export is missing at runtime but present in TypeScript | Stale `.node` loaded, generated declarations newer than binary, or Rust export not compiled | Require the actual candidate and inspect `Object.keys(mod)` | Rebuild native package and remove stale candidate/cache paths | +| x64 machine loads baseline when modern expected | `PI_NATIVE_VARIANT=baseline`, no AVX2 detected, or modern file unavailable | Check env and filenames in `native/` | Build modern variant (`TARGET_VARIANT=modern ... build`) and ship it | +| Cross-build produces wrong-labeled binary | Mismatch between `CROSS_TARGET` and `TARGET_PLATFORM`/`TARGET_ARCH`, or missing x64 variant | Confirm env tuple and output filename | Re-run with consistent env values and explicit x64 `TARGET_VARIANT` | +| Compiled binary fails after upgrade | Stale extracted cache, embedded archive mismatch, or embedded manifest version mismatch | Inspect `/` and loader error list | Delete versioned cache for the package version; regenerate embedded archive/manifest during packaging | +| `embed:native` fails with `No native addons found` | Required platform artifact was not built before embedding | Check expected list in error text | Build at least one expected artifact for the target, then rerun `embed:native` | ## Operational commands @@ -211,6 +213,7 @@ TARGET_VARIANT=baseline bun --cwd=packages/natives run build # Generate embedded addon manifest from built native files bun --cwd=packages/natives run embed:native +# Output archive: packages/natives/native/embedded-addons.-.tar.gz # Reset embedded manifest to null stub bun --cwd=packages/natives run embed:native -- --reset @@ -270,13 +273,13 @@ Workspaces that hardlinked a `.node` before GC retain access via the kernel inod ### Configuration (settings on `robomp.config.Settings`) -| Env var | Default | Effect | -| -------------------------------------------- | ------------------------ | ------------------------------------------------------------- | -| `ROBOMP_NATIVES_CACHE_ENABLED` | `true` | Master switch. When false the populate/capture hooks no-op and every workspace builds from scratch. | -| `ROBOMP_NATIVES_CACHE_ROOT` | `/data/cache/pi-natives` | Cache root directory. Must be `root:omp 02770` for cross-slot reads. | -| `ROBOMP_NATIVES_CACHE_MAX_ENTRIES_PER_REPO` | `8` | LRU entry-count cap, per repo slug. | -| `ROBOMP_NATIVES_CACHE_MAX_BYTES` | `4294967296` (4 GiB) | LRU byte cap, per repo slug. | -| `ROBOMP_NATIVES_CACHE_GC_INTERVAL_SECONDS` | `3600` | Period of the background GC loop in `WorkerPool`. | +| Env var | Default | Effect | +| ------------------------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------- | +| `ROBOMP_NATIVES_CACHE_ENABLED` | `true` | Master switch. When false the populate/capture hooks no-op and every workspace builds from scratch. | +| `ROBOMP_NATIVES_CACHE_ROOT` | `/data/cache/pi-natives` | Cache root directory. Must be `root:omp 02770` for cross-slot reads. | +| `ROBOMP_NATIVES_CACHE_MAX_ENTRIES_PER_REPO` | `8` | LRU entry-count cap, per repo slug. | +| `ROBOMP_NATIVES_CACHE_MAX_BYTES` | `4294967296` (4 GiB) | LRU byte cap, per repo slug. | +| `ROBOMP_NATIVES_CACHE_GC_INTERVAL_SECONDS` | `3600` | Period of the background GC loop in `WorkerPool`. | ### Manual invalidation diff --git a/docs/natives-media-system-utils.md b/docs/natives-media-system-utils.md index 8a07b75f9..e025559b5 100644 --- a/docs/natives-media-system-utils.md +++ b/docs/natives-media-system-utils.md @@ -1,83 +1,64 @@ # Natives media + system utilities -This document covers the media/system/conversion exports in `@oh-my-pi/pi-natives`: image processing, HTML conversion, clipboard access, token counting, macOS appearance/power helpers, ProjFS helpers, and work profiling. +This document covers the media/system/conversion exports currently present in `@oh-my-pi/pi-natives`: terminal SIXEL image encoding, HTML conversion, clipboard access, token counting, macOS appearance/power helpers, and work profiling. ## Implementation files -- `crates/pi-natives/src/image.rs` +- `crates/pi-natives/src/sixel.rs` - `crates/pi-natives/src/html.rs` - `crates/pi-natives/src/clipboard.rs` - `crates/pi-natives/src/tokens.rs` - `crates/pi-natives/src/appearance.rs` - `crates/pi-natives/src/power.rs` -- `crates/pi-natives/src/projfs_overlay.rs` - `crates/pi-natives/src/prof.rs` - `crates/pi-natives/src/task.rs` - `packages/natives/native/index.d.ts` -> Note: there is no `crates/pi-natives/src/work.rs`; work profiling is implemented in `prof.rs` and fed by instrumentation in `task.rs`. +There is no native `PhotonImage` class, `image.rs`, or ProjFS overlay helper module in the current `pi-natives` addon. General-purpose image decode/resize/encode is expected to live outside this native surface; the native image export here is only terminal SIXEL encoding. ## JS API ↔ Rust export/module mapping -| JS export | Rust N-API export | Rust module | -| --------------------------------------------------- | ------------------------------ | ------------------- | -| `PhotonImage.parse(bytes)` | `PhotonImage::parse` | `image.rs` | -| `PhotonImage#resize(width, height, filter)` | `PhotonImage::resize` | `image.rs` | -| `PhotonImage#encode(format, quality)` | `PhotonImage::encode` | `image.rs` | -| `encodeSixel(bytes, targetWidthPx, targetHeightPx)` | `encode_sixel` | `image.rs` | -| `htmlToMarkdown(html, options?)` | `html_to_markdown` | `html.rs` | -| `copyToClipboard(text)` | `copy_to_clipboard` | `clipboard.rs` | -| `readImageFromClipboard()` | `read_image_from_clipboard` | `clipboard.rs` | -| `countTokens(input, encoding?)` | `count_tokens` | `tokens.rs` | -| `detectMacOSAppearance()` | `detect_mac_os_appearance` | `appearance.rs` | -| `MacAppearanceObserver.start(callback)` | `MacAppearanceObserver::start` | `appearance.rs` | -| `MacOSPowerAssertion.start(options?)` | `MacOSPowerAssertion::start` | `power.rs` | -| `projfsOverlayProbe/start/stop` | ProjFS exports | `projfs_overlay.rs` | -| `getWorkProfile(lastSeconds)` | `get_work_profile` | `prof.rs` | +| JS export | Rust N-API export | Rust module | +| ------------------------------------- | ------------------------------ | --------------- | +| `encodeSixel(bytes, width, height)` | `encode_sixel` | `sixel.rs` | +| `htmlToMarkdown(html, options?)` | `html_to_markdown` | `html.rs` | +| `copyToClipboard(text)` | `copy_to_clipboard` | `clipboard.rs` | +| `readImageFromClipboard()` | `read_image_from_clipboard` | `clipboard.rs` | +| `countTokens(input, encoding?)` | `count_tokens` | `tokens.rs` | +| `detectMacOSAppearance()` | `detect_mac_os_appearance` | `appearance.rs` | +| `MacAppearanceObserver.start(cb)` | `MacAppearanceObserver::start` | `appearance.rs` | +| `MacOSPowerAssertion.start(options?)` | `MacOSPowerAssertion::start` | `power.rs` | +| `getWorkProfile(lastSeconds)` | `get_work_profile` | `prof.rs` | ## Data format boundaries and conversions -### Image (`image`) +### SIXEL image encoding (`sixel`) -- **JS input boundary**: `Uint8Array` encoded image bytes for `PhotonImage.parse` and `encodeSixel`. -- **Rust decode boundary**: bytes are copied/read, format is guessed with `ImageReader::with_guessed_format()`, then decoded to `DynamicImage`. -- **In-memory state**: `PhotonImage` stores `Arc`. -- **Output boundary**: - - `PhotonImage#encode(format, quality)` returns a promise for encoded bytes (`Vec` in Rust; generated TS currently declares `Promise>`). - - `encodeSixel(...)` returns a SIXEL escape string synchronously. +- **JS input boundary**: `Uint8Array` containing encoded image bytes. +- **Rust decode boundary**: format is guessed with `ImageReader::with_guessed_format()`, then decoded to `DynamicImage`. +- **Resize boundary**: image is resized with `resize_exact(..., FilterType::Lanczos3)` only when source dimensions differ from `targetWidthPx`/`targetHeightPx`. +- **Output boundary**: `encodeSixel(...)` returns a SIXEL escape string synchronously. -Format IDs: - -- `0`: PNG -- `1`: JPEG -- `2`: WebP -- `3`: GIF - -Encoding behavior: - -- JPEG uses the provided `quality` with `JpegEncoder::new_with_quality`. -- WebP uses the `webp` crate encoder with `quality` as `f32` in the same 0..=100 range. -- PNG/GIF ignore `quality`. -- Invalid dimensions for SIXEL (`0` width or height) fail with `Target SIXEL dimensions must be greater than zero`. +Supported decode formats are whatever the compiled `image` crate supports for `ImageReader` in this build (commonly PNG/JPEG/WebP/GIF). Invalid target dimensions (`0` width or height) fail with `Target SIXEL dimensions must be greater than zero`. ### HTML conversion (`html`) - **JS input boundary**: HTML `string` + optional `{ cleanContent?: boolean; skipImages?: boolean }`. -- **Rust conversion boundary**: conversion is scheduled through `task::blocking("html_to_markdown", (), ...)`. +- **Rust conversion boundary**: conversion is scheduled through `task::blocking("html_to_markdown", (), ...)`; there is no timeout/abort option on this export. - **Output boundary**: Markdown `string` promise. Conversion behavior: - `cleanContent` defaults to `false`. -- When `cleanContent=true`, preprocessing uses `PreprocessingPreset::Aggressive` and hard-removal flags for navigation/forms. -- `skipImages` defaults to `false`. +- When `cleanContent=true`, preprocessing is enabled with `PreprocessingPreset::Aggressive`, `remove_navigation=true`, and `remove_forms=true`. +- `skipImages` defaults to `false` and is passed to `html_to_markdown_rs::ConversionOptions`. ### Clipboard (`clipboard`) - `copyToClipboard(text)` is a synchronous native call using `arboard::Clipboard::set_text`. - `readImageFromClipboard()` runs in `task::blocking("clipboard.read_image", (), ...)`. - Image read returns `null`/`undefined` when `arboard` reports `ContentNotAvailable`. -- Successful image read re-encodes clipboard RGBA data as PNG and returns `{ data: Uint8Array, mimeType: "image/png" }`. +- Successful image read converts clipboard RGBA data into PNG bytes and returns `{ data: Uint8Array, mimeType: "image/png" }`. - Clipboard access or image encoding failures reject/throw as native errors. There is no current `packages/natives` TS wrapper that emits OSC52, handles Termux, or suppresses native clipboard failures. Any best-effort clipboard policy must live in consumers. @@ -85,25 +66,19 @@ There is no current `packages/natives` TS wrapper that emits OSC52, handles Term ### Tokens (`tokens`) - `countTokens(input, encoding?)` accepts a single string or an array of strings. -- Arrays return one aggregate token count; encoding work is parallelized in Rust. +- Arrays return one aggregate token count; array elements are encoded in parallel via rayon. - Default encoding is `O200kBase`; `Cl100kBase` is also exported. -- The implementation uses ordinary encoding, not special-token handling. +- The implementation uses `encode_ordinary`, not special-token handling. +- BPE tables are initialized once through `LazyLock` and reused. ### macOS appearance and power helpers - `detectMacOSAppearance()` returns `"dark"`, `"light"`, or `null` on non-macOS. - `MacAppearanceObserver.start(callback)` returns a handle with `stop()`; on macOS it uses distributed notifications plus a 2-second polling fallback, and on non-macOS it is a no-op observer. -- `MacOSPowerAssertion.start(options?)` returns a handle with `stop()`; on macOS it acquires an IOKit assertion, and on other platforms it is a no-op handle. +- `MacOSPowerAssertion.start(options?)` returns a handle with `stop()`; on macOS it acquires one or more IOKit assertions, and on other platforms it is a no-op handle. +- Power assertion options are `{ reason?, idle?, system?, user?, display? }`. If every boolean is unset or omitted, `idle` behavior is used by default. -### Windows ProjFS helpers - -- `projfsOverlayProbe()` reports whether ProjFS APIs are available. -- `projfsOverlayStart(lowerRoot, projectionRoot)` starts an overlay. -- `projfsOverlayStop(projectionRoot)` stops an overlay session. - -These helpers are platform-specific; availability must be checked before relying on overlay behavior. - -### Work profiling (`work`) +### Work profiling (`prof`) - **Collection boundary**: profiling samples are produced by `profile_region(tag)` guards in `task::blocking` and `task::future`. - **Storage format**: fixed-size circular buffer (`MAX_SAMPLES = 10_000`) storing stack path, duration, and timestamp. @@ -115,25 +90,25 @@ These helpers are platform-specific; availability must be checked before relying ## Lifecycle and state transitions -### Image lifecycle +### SIXEL lifecycle -1. `PhotonImage.parse(bytes)` schedules a blocking decode task (`image.decode`). -2. On success, a native `PhotonImage` handle exists in JS. -3. `resize(...)` creates a new native handle (`image.resize`); old and new handles can coexist. -4. `encode(...)` schedules `image.encode` and materializes bytes without mutating image dimensions. -5. `encodeSixel(...)` decodes, optionally resizes to exact target dimensions with Lanczos3, and returns SIXEL text synchronously. +1. `encodeSixel(bytes, targetWidthPx, targetHeightPx)` validates target dimensions. +2. Rust guesses and decodes the encoded image. +3. Image is resized exactly to the target dimensions when needed. +4. Pixels are converted to RGBA8 and encoded with `icy_sixel::sixel_encode`. +5. The SIXEL escape string is returned synchronously. Failure transitions: -- Format detection/decode failure rejects parse promise or throws from SIXEL encoding. -- Encode failure rejects encode promise. -- Invalid SIXEL dimensions throw. +- Format detection/decode failure throws. +- Invalid target dimensions throw. +- SIXEL encoding failure throws with `Failed to encode SIXEL: ...`. ### HTML lifecycle 1. `htmlToMarkdown(html, options)` schedules a blocking conversion task. 2. Conversion runs with defaulted options (`cleanContent=false`, `skipImages=false`) unless specified. -3. Returns markdown string or rejects. +3. Returns markdown string or rejects with `Conversion error: ...`. ### Clipboard lifecycle @@ -154,11 +129,11 @@ Failure transitions: ## Unsupported operations and error propagation -### Image +### SIXEL -- Unsupported decode input or corrupted bytes: strict failure. -- Invalid SIXEL target dimensions: strict failure. -- No JS fallback path in the natives package. +- Unsupported or corrupted image input is a strict failure. +- Invalid SIXEL target dimensions are a strict failure. +- No JS fallback path is exposed by the natives package. ### HTML @@ -180,4 +155,4 @@ Failure transitions: - Clipboard access depends on OS/session support exposed through `arboard`. - macOS appearance and power helpers intentionally return no-op/null behavior on unsupported platforms. -- ProjFS helpers are Windows-specific and should be gated by `projfsOverlayProbe()`. +- ProjFS is not exposed by this media/system native utility surface. Isolation backend selection, including any ProjFS support, lives in the separate `iso` subsystem. diff --git a/docs/natives-rust-task-cancellation.md b/docs/natives-rust-task-cancellation.md index 4570d03c2..e69712470 100644 --- a/docs/natives-rust-task-cancellation.md +++ b/docs/natives-rust-task-cancellation.md @@ -12,7 +12,7 @@ This document describes how `crates/pi-natives` schedules native work and how ca - `crates/pi-natives/src/shell.rs` - `crates/pi-natives/src/pty.rs` - `crates/pi-natives/src/html.rs` -- `crates/pi-natives/src/image.rs` +- `crates/pi-natives/src/sixel.rs` - `crates/pi-natives/src/clipboard.rs` - `crates/pi-natives/src/text.rs` - `crates/pi-natives/src/ps.rs` @@ -36,8 +36,8 @@ This document describes how `crates/pi-natives` schedules native work and how ca 3. `CancelToken` / `AbortToken` / `AbortReason` - `CancelToken::new(timeout_ms, signal)` combines an optional deadline and optional JS `AbortSignal` converted from `Unknown`. - `CancelToken::heartbeat()` is cooperative cancellation for blocking loops. - - `CancelToken::wait()` asynchronously waits for signal, timeout, or Ctrl-C. - - `CancelToken::emplace_abort_token()` creates an abortable flag when a later `Shell.abort()`/internal bridge needs one. + - `CancelToken::wait()` asynchronously waits for signal or timeout. + - `CancelToken::emplace_abort_token()` creates an abortable flag when `AbortSignal`, `Shell.abort()`, or an internal bridge needs one. - `AbortToken::abort(reason)` lets external code request abort. ## `blocking` vs `future`: execution model and selection @@ -48,8 +48,6 @@ Use when work is CPU-heavy or fundamentally synchronous/blocking: - regex/file scanning (`grep`, `glob`, `fuzzyFind`) - ast-grep search/edit worker work -- PTY loop internals through `tokio::task::spawn_blocking` -- image decode/resize/encode - HTML conversion - clipboard image read @@ -65,7 +63,7 @@ Use when work must `await` async operations: - shell session orchestration (`Shell.run`, `executeShell`) - PTY outer promise (`PtySession.start`) before it enters `spawn_blocking` -- task racing (`tokio::select!`) between completion and cancellation +- async task orchestration that must bridge completion and cancellation Behavior: @@ -74,20 +72,20 @@ Behavior: ## JS API ↔ Rust export mapping (task/cancel relevant) -| JS-facing API | Rust export | Scheduler | Cancellation hookup | -| --------------------------------------- | ------------------------------------ | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | -| `grep(options, onMatch?)` | `grep` | `task::blocking("grep", ct, ...)` | `CancelToken::new(options.timeoutMs, options.signal)` + heartbeat checks | -| `glob(options, onMatch?)` | `glob` | `task::blocking("glob", ct, ...)` | `CancelToken::new(...)` + heartbeat checks | -| `fuzzyFind(options)` | `fuzzy_find` | `task::blocking("fuzzy_find", ct, ...)` | `CancelToken::new(...)` + heartbeat checks | -| `astGrep(options)` / `astEdit(options)` | ast exports | blocking worker path | timeout/signal fields are accepted by options and checked cooperatively in worker loops | -| `Shell#run(options, onChunk?)` | `Shell::run` | `task::future(env, "shell.run", ...)` | `ct.wait()` raced against run task; bridges to Tokio cancellation token and `AbortToken` | -| `executeShell(options, onChunk?)` | `execute_shell` | `task::future(env, "shell.execute", ...)` | same cancel race and 2s graceful window | -| `PtySession#start(options, onChunk?)` | `PtySession::start` | `task::future(env, "pty.start", ...)` + inner `spawn_blocking` | `CancelToken` checked in sync PTY loop via `heartbeat()` | -| `htmlToMarkdown(html, options?)` | `html_to_markdown` | `task::blocking("html_to_markdown", (), ...)` | none (`()` token) | -| `PhotonImage.parse/encode/resize` | `PhotonImage::{parse,encode,resize}` | `task::blocking(...)` | none (`()` token) | -| `readImageFromClipboard()` | `read_image_from_clipboard` | `task::blocking("clipboard.read_image", (), ...)` | none (`()` token) | +| JS-facing API | Rust export | Scheduler | Cancellation hookup | +| --------------------------------------- | --------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| `grep(options, onMatch?)` | `grep` | `task::blocking("grep", ct, ...)` | `CancelToken::new(options.timeoutMs, options.signal)` + heartbeat checks | +| `glob(options, onMatch?)` | `glob` | `task::blocking("glob", ct, ...)` | `CancelToken::new(...)` + heartbeat checks | +| `fuzzyFind(options)` | `fuzzy_find` | `task::blocking("fuzzy_find", ct, ...)` | `CancelToken::new(...)` + heartbeat checks | +| `astGrep(options)` / `astEdit(options)` | ast exports | blocking worker path | timeout/signal fields are accepted by options and checked cooperatively in worker loops | +| `Shell#run(options, onChunk?)` | `Shell::run` | `task::future(env, "shell.run", ...)` | JS `CancelToken` is converted into `pi_shell::cancel::CancelToken`; shell races it against command completion and descendant cleanup | +| `executeShell(options, onChunk?)` | `execute_shell` | `task::future(env, "shell.execute", ...)` | same cancel race and 2s graceful window | +| `PtySession#start(options, onChunk?)` | `PtySession::start` | `task::future(env, "pty.start", ...)` + inner `spawn_blocking` | `CancelToken` checked in sync PTY loop via `heartbeat()` | +| `htmlToMarkdown(html, options?)` | `html_to_markdown` | `task::blocking("html_to_markdown", (), ...)` | none (`()` token) | +| `encodeSixel(...)` | `encode_sixel` | synchronous native function | none | +| `readImageFromClipboard()` | `read_image_from_clipboard` | `task::blocking("clipboard.read_image", (), ...)` | none (`()` token) | -`text.rs`, `tokens.rs`, `keys.rs`, most `ps.rs` functions, and synchronous utility exports do not use `task::blocking`/`task::future` and therefore do not participate in this cancellation path. +`text.rs`, `tokens.rs`, `keys.rs`, most `ps.rs` functions, SIXEL encoding, and synchronous utility exports do not use `task::blocking`/`task::future` cancellation and therefore do not participate in this cancellation path. ## Cancellation lifecycle and state transitions @@ -102,7 +100,6 @@ Created Running ├─ heartbeat()/wait() sees signal -> AbortReason::Signal ├─ heartbeat()/wait() sees deadline -> AbortReason::Timeout - ├─ wait() sees Ctrl-C -> AbortReason::User └─ no abort -> continue Aborted @@ -118,8 +115,8 @@ Aborted - **Mid-execution**: - `blocking`: next `heartbeat()` returns `Err("Aborted: ...")`. - `future`: `ct.wait()` branch wins `select!`, then code cancels subordinate async machinery. - - shell: cancellation triggers a Tokio cancellation token, waits up to 2 seconds, then aborts the task if needed. - - PTY: heartbeat failure or `kill()` terminates PTY child/process tree and drains output briefly. + - shell: cancellation triggers a Tokio cancellation token, sends descendant termination waves, waits up to 2 seconds for the command task, then aborts the task if needed. + - PTY: heartbeat failure or `kill()` terminates PTY child/process targets and drains output briefly. ## Heartbeat expectations for long-running loops diff --git a/docs/natives-shell-pty-process.md b/docs/natives-shell-pty-process.md index 1d6ee8a8e..146277e50 100644 --- a/docs/natives-shell-pty-process.md +++ b/docs/natives-shell-pty-process.md @@ -1,11 +1,14 @@ # Natives Shell, PTY, Process, and Key Internals -This document covers the execution/process/terminal primitives in `@oh-my-pi/pi-natives`: `shell`, `pty`, `ps`, and `keys`, using the architecture terms from `docs/natives-architecture.md`. +This document covers execution/process/terminal primitives in `@oh-my-pi/pi-natives`: `shell`, `pty`, `ps`, and `keys`, using the architecture terms from `docs/natives-architecture.md`. ## Implementation files - `crates/pi-natives/src/shell.rs` -- `crates/pi-natives/src/shell/windows.rs` (Windows-only PATH enrichment) +- `crates/pi-shell/src/shell.rs` +- `crates/pi-shell/src/fixup.rs` +- `crates/pi-shell/src/windows.rs` (Windows-only PATH enrichment) +- `crates/pi-shell/src/process.rs` - `crates/pi-natives/src/pty.rs` - `crates/pi-natives/src/ps.rs` - `crates/pi-natives/src/keys.rs` @@ -15,19 +18,24 @@ This document covers the execution/process/terminal primitives in `@oh-my-pi/pi- ## Layer ownership - **Package entrypoint** (`packages/natives/native/index.js`): loads the `.node` addon and exports generated N-API bindings. -- **Rust N-API module layer** (`crates/pi-natives/src/*`): shell/PTY process execution, process-tree traversal/termination, and key-sequence parsing. +- **Rust N-API module layer** (`crates/pi-natives/src/*`): JS-facing shell/PTY/process/key exports and callback bridging. +- **Runtime core** (`crates/pi-shell/src/*`): brush shell execution, cancellation cleanup, minimizer integration, command fixups, and cross-platform process references. - **Consumers** (`packages/coding-agent`, `packages/tui`): higher-level session policy, output artifact/minimizer handling, render policy, and UI key handling. ## Shell subsystem (`shell`) ### API model -Two execution modes are exposed: +Shell execution modes: 1. **One-shot** via `executeShell(options, onChunk?)`. 2. **Persistent session** via `new Shell(options?)` then `shell.run(...)` repeatedly. -Both stream output through a threadsafe callback and return `{ exitCode?, cancelled, timedOut, minimized? }`. +Both stream merged stdout/stderr text through a threadsafe callback and return `{ exitCode?, cancelled, timedOut, minimized? }`. + +Related synchronous helper: + +- `applyBashFixups(command)` strips safe trailing `| head`/`| tail` pipeline caps and redundant trailing `2>&1` according to `pi_shell::fixup` rules. It returns `{ command, stripped }` and does not execute anything. `ShellOptions` supports `sessionEnv`, `snapshotPath`, and optional output `minimizer`. `ShellExecuteOptions` supports command-scoped `env`, session-level `sessionEnv`, `snapshotPath`, timeout/signal, and optional minimizer. `ShellRunOptions` supports command, cwd, command-scoped env, timeout, and signal. @@ -35,19 +43,19 @@ Both stream output through a threadsafe callback and return `{ exitCode?, cancel Rust creates `brush_core::Shell` with: -- non-interactive, non-login mode, -- `no_profile` and `no_rc`, -- `do_not_inherit_env: true`, +- inherited environment disabled (`do_not_inherit_env: true`), followed by explicit environment reconstruction from host env, +- profile and rc loading skipped, - bash-mode builtins, with `exec` and `suspend` disabled, -- explicit environment reconstruction from host env, -- skip-list for shell-sensitive vars (`PS1`, `PWD`, `SHLVL`, bash function exports, etc.). +- native `sleep` and `timeout` builtins registered, +- skip-list for shell-sensitive vars (`PS1`, `PWD`, `SHLVL`, bash function exports, etc.), +- a non-exported `env="$env"` fallback so PowerShell-style `$env:NAME` survives brush parameter expansion unless the user shadows `env`. Session env behavior: - `ShellOptions.sessionEnv` / one-shot `sessionEnv` is applied at session creation. - `ShellRunOptions.env` / one-shot `env` is command-scoped (`EnvironmentScope::Command`) and popped after the command. - `PATH` is merged specially on Windows with case-insensitive dedupe. -- Windows-only path enrichment (`shell/windows.rs`) appends discovered Git-for-Windows paths when present and not already included. +- Windows-only path enrichment (`pi-shell/src/windows.rs`) appends discovered Git-for-Windows paths when present and not already included. - `snapshotPath`, when present, is sourced during session creation with stdout/stderr/stdin wired to null files. ### Runtime lifecycle and state transitions @@ -58,7 +66,7 @@ Persistent shell (`Shell.run`) uses this state machine: - **Running**: first `run()` lazily creates a session, stores an abort token, executes command. - **Completed + keepalive**: if execution control flow is normal, abort state is cleared and session is reused. - **Completed + teardown**: if control flow is loop/script/shell-exit related, session is dropped. -- **Cancelled/Timed out**: run task is cancelled, grace wait is 2 seconds, task may be force-aborted, session is dropped if lock can be acquired. +- **Cancelled/Timed out**: Tokio cancellation token is triggered, descendants started after the baseline snapshot receive termination waves, a 2-second graceful wait is allowed, the task may be aborted, and the persistent session is dropped if the lock can be acquired. - **Error**: session is dropped. One-shot shell (`executeShell`) always creates and drops a fresh session per call. @@ -67,14 +75,15 @@ One-shot shell (`executeShell`) always creates and drops a fresh session per cal - Stdout/stderr are routed into a shared pipe and read concurrently. - Reader decodes UTF-8 incrementally; invalid byte sequences emit `U+FFFD` replacement chunks. -- The command runs in a new process group policy. +- The command runs with `ProcessGroupPolicy::NewProcessGroup`. +- After the foreground command completes, the reader drains until EOF, 250ms of idle output, or 2s maximum; reader shutdown then gets a 250ms timeout. - Optional minimizer configuration can capture and rewrite output. When minimization occurs, the result includes `minimized` with filter name, replacement text, original text, and byte counts. - Consumers are responsible for persisting or displaying minimizer artifacts; the native result only carries the data. ### Cancellation, timeout, and abort -- `CancelToken` is constructed from `timeoutMs` and optional `AbortSignal`. -- On cancellation/timeout, shell cancellation token is triggered, then task gets a 2-second graceful window before forced abort. +- `CancelToken` is constructed from `timeoutMs` and optional `AbortSignal`, then converted into the shared `pi_shell::cancel::CancelToken`. +- On cancellation/timeout, shell cancellation token is triggered, descendant cleanup runs, then the task gets a 2-second graceful window before forced abort. - Structured result flags are used: - timeout -> `exitCode` omitted, `timedOut: true`. - abort signal / `Shell.abort()` -> `exitCode` omitted, `cancelled: true`. @@ -107,7 +116,7 @@ Common surfaced errors include: - `resize(cols, rows)` - `kill()` -`PtyStartOptions` supports `command`, optional `cwd`, optional `env`, `timeoutMs`, `signal`, `cols`, and `rows`. +`PtyStartOptions` supports `command`, optional `cwd`, optional `env`, `timeoutMs`, `signal`, `cols`, `rows`, and optional `shell`. The default shell is `sh`. ### Runtime lifecycle and state transitions @@ -126,11 +135,15 @@ Concurrency guard: ### Spawn/attach/write/read/terminate patterns - PTY opened via `portable_pty::native_pty_system().openpty(...)`. -- Command currently runs as `sh -lc ` with optional `cwd` and env overrides. -- Default size is `120x40`; dimensions are clamped (`cols 20..400`, `rows 5..200`). +- On Windows, `openpty()` is run on a helper thread with a 5s startup timeout; timeout rejects with `PTY creation timed out (5s). ConPTY may be unavailable on this system.` +- Command runs through the configured shell: + - `cmd.exe`/`cmd` gets `/c`, + - `powershell`/`pwsh` gets `-Command`, + - other shells get `-lc`. +- Default size is `120x40`; dimensions are clamped (`cols 20..400`, `rows 5..200`) on start and resize. - `write()` sends raw bytes to PTY stdin. - `resize()` sends a control message and clamps dimensions again. -- `kill()` sends a control message that marks the run cancelled and terminates the child/process tree. +- `kill()` sends a control message that marks the run cancelled and terminates PTY process targets. Output path: @@ -140,8 +153,9 @@ Output path: Termination path: -- Unix: terminate process group when known, terminate child tree, call child kill, then repeat with SIGKILL. -- Non-Unix: terminate child tree, call child kill, then repeat with SIGKILL-equivalent process-tree helper. +- `terminate_pty_processes` targets the PTY process group when available and the child pid when available. +- It sends the platform `TERM_SIGNAL`, calls `child.kill()`, then sends the platform `KILL_SIGNAL`. +- On Windows, ConPTY input is closed before dropping the master; master drop is offloaded to a background thread and waited for up to 2s to avoid deadlock. ### Cancellation and timeout semantics @@ -149,12 +163,14 @@ Termination path: - Loop calls `ct.heartbeat()` periodically with a 16ms maximum wait cadence. - Timeout classification is based on the heartbeat error string containing `Timeout`. - Cancellation/kill starts a 300ms post-cancel drain window; normal child exit starts a 300ms post-exit drain window. +- Final reader drain is 50ms on non-Windows and 500ms on Windows. ### Failure behavior Error surfaces include: - PTY allocation/open failure, +- Windows PTY startup timeout, - PTY spawn failure, - writer/reader acquisition failure, - child status/wait failures, @@ -165,38 +181,27 @@ Control call failures when not running: - `write/resize/kill` return `PTY session is not running`. -## Process-tree subsystem (`ps`) +## Process subsystem (`ps`) ### API model -- `killTree(pid, signal) -> number` -- `listDescendants(pid) -> number[]` +Current JS surface is the `Process` class: -### Platform-specific implementation +- `Process.fromPid(pid) -> Process | null` +- `Process.fromPath(path) -> Process[]` +- getters: `pid`, `ppid` +- methods: `args()`, `killTree(signal?)`, `terminate(options?)`, `waitForExit(options?)`, `groupId()`, `children()`, `status()` -- **Linux**: recursively reads `/proc//task//children`. -- **macOS**: uses `libproc` `proc_listchildpids`. -- **Windows**: snapshots process table with `CreateToolhelp32Snapshot`, builds parent->children map, terminates with `OpenProcess(PROCESS_TERMINATE)` + `TerminateProcess`. +`ProcessTerminateOptions` supports `{ group?, gracefulMs?, timeoutMs?, signal? }`. `ProcessWaitOptions` supports `{ timeoutMs?, signal? }`. -### Kill-tree behavior +### Behavior -- Descendants are collected recursively. -- Kill order is bottom-up (deepest descendants first). -- Root pid is killed last. -- Return value is count of successful terminations. +- `killTree(signal?)` sends the requested signal to the process and descendants, children first; on Windows the signal argument is ignored and processes are terminated via `TerminateProcess`. +- `terminate(options?)` is async. By default it uses a 1000ms graceful phase and a 5000ms post-hard-kill wait. Passing `gracefulMs < 0` skips the graceful phase. +- `waitForExit(options?)` resolves `true` when the process exits and `false` on timeout. +- `status()` returns `"running"` or `"exited"`. -Signal behavior: - -- POSIX: provided `signal` is passed to `kill`. -- Windows: `signal` is ignored; termination is unconditional process terminate. - -### Failure behavior - -This module is intentionally non-throwing at API surface for ordinary process misses: - -- missing/inaccessible process tree branches are skipped, -- per-pid kill failures are counted as unsuccessful, -- lookup miss typically yields `[]` from `listDescendants` and `0` from `killTree`. +The platform-specific implementation lives in `pi_shell::process`; `crates/pi-natives/src/ps.rs` is a N-API shim plus re-exports used by PTY termination. ## Key parsing subsystem (`keys`) @@ -239,19 +244,25 @@ Layout behavior: ### Shell + PTY + Process -| JS API | Rust N-API export | Notes | -| --------------------------------- | -------------------------------------- | ----------------------------------------- | -| `executeShell(options, onChunk?)` | `executeShell` (`execute_shell`) | One-shot shell execution | -| `new Shell(options?)` | `Shell` class | Persistent shell session | -| `shell.run(options, onChunk?)` | `Shell::run` | Reuses session on keepalive control flow | -| `shell.abort()` | `Shell::abort` | Aborts active run for that shell instance | -| `new PtySession()` | `PtySession` class | Stateful PTY session | -| `pty.start(options, onChunk?)` | `PtySession::start` | Interactive PTY run | -| `pty.write(data)` | `PtySession::write` | Raw stdin passthrough | -| `pty.resize(cols, rows)` | `PtySession::resize` | Clamped terminal dimensions | -| `pty.kill()` | `PtySession::kill` | Force-kills active PTY child | -| `killTree(pid, signal)` | `killTree` (`kill_tree`) | Children-first process tree termination | -| `listDescendants(pid)` | `listDescendants` (`list_descendants`) | Recursive descendants listing | +| JS API | Rust N-API export | Notes | +| --------------------------------- | --------------------------------------- | ----------------------------------------- | +| `executeShell(options, onChunk?)` | `executeShell` (`execute_shell`) | One-shot shell execution | +| `new Shell(options?)` | `Shell` class | Persistent shell session | +| `shell.run(options, onChunk?)` | `Shell::run` | Reuses session on keepalive control flow | +| `shell.abort()` | `Shell::abort` | Aborts active run for that shell instance | +| `applyBashFixups(command)` | `applyBashFixups` (`apply_bash_fixups`) | Synchronous command rewrite helper | +| `new PtySession()` | `PtySession` class | Stateful PTY session | +| `pty.start(options, onChunk?)` | `PtySession::start` | Interactive PTY run | +| `pty.write(data)` | `PtySession::write` | Raw stdin passthrough | +| `pty.resize(cols, rows)` | `PtySession::resize` | Clamped terminal dimensions | +| `pty.kill()` | `PtySession::kill` | Terminates active PTY child/targets | +| `Process.fromPid(pid)` | `Process::from_pid` | Stable process reference lookup | +| `Process.fromPath(path)` | `Process::from_path` | Executable-path process lookup | +| `process.killTree(signal?)` | `Process::kill_tree` | Children-first process tree termination | +| `process.terminate(options?)` | `Process::terminate` | Graceful then hard process termination | +| `process.waitForExit(options?)` | `Process::wait_for_exit` | Async exit wait | +| `process.children()` | `Process::children` | Direct children as `Process[]` | +| `process.status()` | `Process::status` | `running` / `exited` | ### Keys diff --git a/docs/natives-text-search-pipeline.md b/docs/natives-text-search-pipeline.md index 82bccfbb8..ec0b3b760 100644 --- a/docs/natives-text-search-pipeline.md +++ b/docs/natives-text-search-pipeline.md @@ -77,9 +77,8 @@ Terminology follows `docs/natives-architecture.md`: - Output modes: - `content` -> one `GrepMatch` per hit. - `count` and `filesWithMatches` map to count-style entries (`lineNumber=0`, `line=""`, `matchCount` set). -- Limits: - - Global `offset` and `maxCount` apply across files. - - Parallel path is used only when `maxCount` is unset and `offset == 0`; otherwise sequential path preserves deterministic global offset/limit semantics. + - `offset` and `maxCount` are applied during aggregation across sorted file results. + - Directory searches use parallel filesystem walking/searching, then aggregate per-file results to preserve global offset/limit semantics in the returned result and callback stream. ### Result shaping back to JS @@ -158,11 +157,15 @@ These exports are direct native APIs used by tooling; they are not mediated by a ## 4) Shared scan/cache lifecycle (`fs_cache`) -`fs_cache` stores scan results as normalized relative entries (`path`, `fileType`, optional `mtime`) keyed by: +`fs_cache` stores scan results as normalized relative entries (`path`, `fileType`, optional `mtime` and regular-file `size`) keyed by: - canonical search root, - `include_hidden`, -- `use_gitignore`. +- `use_gitignore`, +- `skip_node_modules`, +- scan detail (`Minimal` vs `Full`). + +`follow_links` affects a fresh scan but is not currently part of the cache key. ### Cache state transitions @@ -193,7 +196,7 @@ These are pure, in-memory utilities. - `text.rs` owns terminal-cell semantics: - ANSI sequence parsing, - grapheme-aware width and slicing, - - wrap/truncate/sanitize behavior, + - wrap/truncate/slice behavior, - explicit tab-width parameter on width-sensitive APIs. - `grep.rs` line truncation (`maxColumns`) is separate: - simple character-boundary truncation of matched lines with `...`, @@ -242,7 +245,7 @@ Text functions generally return deterministic transformed output; errors are lim | Flow | Filesystem access | Shared cache | Notes | | ---------------------------- | ----------------- | -------------------- | --------------------------------------------- | | `search` / `hasMatch` | No | No | regex on provided bytes/string only | -| `text` module functions | No | No | ANSI/width/sanitization only | +| `text` module functions | No | No | ANSI/width utilities only | | `highlight` module functions | No | No | syntax + ANSI coloring only | | `countTokens` | No | No | tokenization only | | `astGrep` / `astEdit` | Yes | No | syntax-aware file search/edit | diff --git a/docs/non-compaction-retry-policy.md b/docs/non-compaction-retry-policy.md index d795865d2..ea5f9e114 100644 --- a/docs/non-compaction-retry-policy.md +++ b/docs/non-compaction-retry-policy.md @@ -65,10 +65,11 @@ Flow (`#handleRetryableError`): 6. Compute base delay: `retry.baseDelayMs * 2^(attempt-1)`. 7. For usage-limit errors, parse retry hints and call auth storage (`markUsageLimitReached(...)`); if credential switching succeeds, force delay to `0`, otherwise use a larger retry-after/backoff hint when present. 8. If no credential switch occurred, suppress the current model selector for cooldown, try configured retry model fallback chains, and force delay to `0` on model switch. -9. Emit `auto_retry_start`. -10. Remove the trailing assistant error message from agent runtime state (kept in persisted session history). -11. Sleep with abort support. -12. Schedule `agent.continue()` through the post-prompt task scheduler (`delayMs: 1`) for the same prompt generation. +9. If the final delay exceeds `retry.maxDelayMs` and no credential/model switch happened, emit final failure and do not sleep. +10. Emit `auto_retry_start`. +11. Remove the trailing assistant error message from agent runtime state (kept in persisted session history). +12. Sleep with abort support. +13. Schedule `agent.continue()` through the post-prompt task scheduler (`delayMs: 1`) for the same prompt generation. ### What resets retry counters @@ -77,8 +78,9 @@ Flow (`#handleRetryableError`): - first successful non-error, non-aborted assistant message after retries started (emits `auto_retry_end { success: true }`) - retry cancellation during backoff sleep - max retries exceeded path +- max delay exceeded path -`#retryPromise` resolves/clears when retry chain ends (success, cancellation, or max-exceeded), via `#resolveRetry()`. +`#retryPromise` resolves/clears when retry chain ends (success, cancellation, max-exceeded, or max-delay failure), via `#resolveRetry()`. ## Backoff and max-attempt semantics @@ -87,6 +89,7 @@ Settings: - `retry.enabled` (default `true`) - `retry.maxRetries` (default `3`) - `retry.baseDelayMs` (default `2000`) +- `retry.maxDelayMs` (default `300000`, 5 minutes; `<= 0` disables the fail-fast cap) Attempt numbering: @@ -100,7 +103,7 @@ Backoff sequence with default settings: - attempt 2: 4000 ms - attempt 3: 8000 ms -Delay override inputs can come from parsed retry headers (`retry-after-ms`, `retry-after`, `x-ratelimit-reset-ms`, `x-ratelimit-reset`) or usage-limit backoff. Credential/model fallback switches set delay to `0`; otherwise parsed hints can extend the exponential local delay. +Delay override inputs can come from parsed retry headers (`retry-after-ms`, `retry-after`, `x-ratelimit-reset-ms`, `x-ratelimit-reset`) or usage-limit backoff. Credential/model fallback switches set delay to `0`; otherwise parsed hints can extend the exponential local delay. If the computed delay is greater than `retry.maxDelayMs` and no switch succeeded, retry ends immediately with a final error instead of sleeping. ## Abort mechanics @@ -149,6 +152,7 @@ Defined in settings schema under retry group: - `retry.enabled` - `retry.maxRetries` - `retry.baseDelayMs` +- `retry.maxDelayMs` - `retry.fallbackChains` - `retry.fallbackRevertPolicy` (`"cooldown-expiry"` by default; `"never"` disables automatic restoration) @@ -190,7 +194,7 @@ Propagation: Final failure surfacing: -- On max-exceeded or cancellation, `auto_retry_end.success === false` +- On max-exceeded, max-delay failure, or cancellation, `auto_retry_end.success === false` - TUI shows: `Retry failed after N attempts: ` - Extensions/hooks receive `auto_retry_end` with same fields - RPC consumers receive same event object on stdout stream @@ -203,6 +207,7 @@ Retry stops and will not auto-continue when any of these occur: - error is not retry-classified - error is context overflow (delegated to compaction path) - max retries exceeded +- provider-requested delay exceeds `retry.maxDelayMs` and no credential/model switch is available - user cancels retry (`abort_retry` or `Esc` during retry loader) - global abort (`abort`) cancels retry first diff --git a/docs/notebook-tool-runtime.md b/docs/notebook-tool-runtime.md index cc33e3f9d..84ab459a0 100644 --- a/docs/notebook-tool-runtime.md +++ b/docs/notebook-tool-runtime.md @@ -1,77 +1,79 @@ -# Notebook tool runtime internals +# Notebook file runtime internals -This document describes the current `notebook` tool implementation and its relationship to the kernel-backed Python runtime. +This document describes current `.ipynb` handling in `coding-agent` and its relationship to the kernel-backed Python runtime. -The critical distinction: **`notebook` is a JSON notebook editor, not a notebook executor**. It edits `.ipynb` cell sources directly; it does not start or talk to a Python kernel. +The critical distinction: **notebook support is file conversion/editing, not notebook execution**. `.ipynb` files are exposed as editable cell-marked text through `read` and the edit pipeline; no notebook-specific tool starts or talks to a Python kernel. ## Implementation files - [`src/edit/notebook.ts`](../packages/coding-agent/src/edit/notebook.ts) +- [`src/edit/read-file.ts`](../packages/coding-agent/src/edit/read-file.ts) +- [`src/tools/read.ts`](../packages/coding-agent/src/tools/read.ts) +- [`src/tools/eval.ts`](../packages/coding-agent/src/tools/eval.ts) - [`src/eval/py/executor.ts`](../packages/coding-agent/src/eval/py/executor.ts) - [`src/eval/py/kernel.ts`](../packages/coding-agent/src/eval/py/kernel.ts) - [`src/session/streaming-output.ts`](../packages/coding-agent/src/session/streaming-output.ts) -- [`src/tools/eval.ts`](../packages/coding-agent/src/tools/eval.ts) ## 1) Runtime boundary: editing vs executing -## `notebook` tool (`src/edit/notebook.ts`) +## `.ipynb` file conversion (`src/edit/notebook.ts`) -- Supports `action: edit | insert | delete` on a `.ipynb` file. -- Resolves path relative to session CWD (`resolveToCwd`). -- Loads notebook JSON, validates `cells` array, validates `cell_index` bounds. -- Applies source edits in-memory and writes full notebook JSON back with `JSON.stringify(notebook, null, 1)`. -- Returns textual summary + structured `details` (`action`, `cellIndex`, `cellType`, `totalCells`, `cellSource`). +- `read` treats `.ipynb` files as notebooks unless the selector is `:raw`. +- The default notebook view is editable text with markers: + - `# %% [code] cell:N` + - `# %% [markdown] cell:N` + - `# %% [raw] cell:N` +- Line selectors and multi-range selectors operate on that virtual text. +- Edit/write paths round-trip virtual text back to notebook JSON through `serializeEditedNotebookText(...)`. +- Existing notebook metadata is preserved when a marker references an existing `cell:N`; new cells get fresh empty metadata. +- Missing notebooks edited through this path start from an empty nbformat 4.5 notebook. -No kernel lifecycle exists in this tool: +No kernel lifecycle exists in this path: -- no gateway acquisition - no kernel session ID -- no `execute_request` -- no stream chunks from kernel channels -- no rich display capture (`image/png`, JSON display, status MIME) +- no code execution +- no stream chunks from Python +- no rich display capture +- no output artifact pipeline from execution -## Notebook-like execution path (`src/tools/eval.ts` + `src/eval/py/*`) +## Kernel-backed execution path (`src/tools/eval.ts` + `src/eval/py/*`) -When the agent needs to run cell-style Python code (sequential cells, persistent state, rich displays), that goes through the **`eval` tool** with `language: "python"`, not `notebook`. +When the agent needs to run cell-style Python code (sequential cells, persistent state, rich displays), that goes through the **`eval` tool** with per-cell `language: "py"`, not through notebook file handling. -That path is where kernel modes, restart/cancel behavior, chunk streaming, and output artifact truncation live. +That path is where Python subprocess lifecycle, reset/cancel behavior, chunk streaming, rich displays, and output artifact truncation live. -## 2) Notebook cell handling semantics (`notebook` tool) +## 2) Notebook cell handling semantics ## Source normalization -`content` is split into `source: string[]` with newline preservation: +Notebook JSON `source` is converted to virtual text by joining source arrays. When virtual text is serialized back, cell source is split with newline preservation: -- each non-final line keeps trailing `\n` -- final line has no forced trailing newline +- each line ending in `\n` stays as a separate source entry with the newline +- a final non-newline-terminated line is stored without forcing a trailing newline +- empty content becomes an empty `source` array This mirrors notebook JSON conventions and avoids accidental line concatenation on later edits. -## Action behavior +## Marker parsing and cell preservation -- `edit` - - replaces `cells[cell_index].source` - - preserves existing `cell_type` -- `insert` - - inserts at `[0..cellCount]` - - `cell_type` defaults to `code` - - code cells initialize `execution_count: null` and `outputs: []` - - markdown cells initialize only `metadata` + `source` -- `delete` - - removes `cells[cell_index]` - - returns removed `source` in details for renderer preview +- The first representation line must be a marker; text before the first marker, including a blank line, is rejected. +- Markers must match `# %% [code|markdown|raw]` with optional `cell:N`. +- If `cell:N` points at an unused existing cell, that cell is cloned, its `cell_type` and `source` are updated, and unrelated metadata is preserved. +- If no valid unused original index is present, a new cell is created. +- Code cells ensure `execution_count` exists and `outputs` exists. +- Markdown/raw cells remove `execution_count` and `outputs`. ## Error surfaces Hard failures are thrown for: -- missing notebook file +- missing notebook on read - invalid JSON - missing/non-array `cells` -- out-of-range index (insert and non-insert have different valid ranges) -- missing `content` for `edit`/`insert` +- invalid cell objects or cell types +- invalid editable representation (for example, text before the first cell marker) -These become `Error:` tool responses upstream; renderer uses notebook path + formatted error text. +These surface through the caller (`read`, edit, or `write`) as normal tool errors. ## 3) Kernel session semantics (where they actually exist) @@ -82,136 +84,103 @@ Kernel semantics are implemented in `executePython` / `PythonKernel` and apply t `PythonKernelMode`: - `session` (default) - - kernels cached in `kernelSessions` map - - max 4 sessions; oldest evicted on overflow - - idle/dead cleanup every 30s, timeout after 5 minutes - - per-session queue serializes execution (`session.queue`) + - kernels are cached by `(session id, cwd)` + - multiple owners can share a retained kernel for the same key + - execution is serialized by the tool's exclusive concurrency and backend execution path + - dead kernels are replaced before execution - `per-call` - - creates kernel for request + - creates a subprocess for the request - executes - - always shuts down kernel in `finally` + - always shuts down the subprocess in `finally` ## Reset behavior -`eval` passes `reset` only for the first cell in a multi-cell Python call; later cells always run with `reset: false`. +Each eval cell has its own optional `reset` flag. `reset: true` resets the selected Python session before that cell executes; it is not a top-level tool parameter. ## Kernel death / restart / retry -In session mode (`withKernelSession`): +In session mode: -- dead kernel detected by heartbeat (`kernel.isAlive()` check every 5s) or execute failure. -- pre-run dead state triggers `restartKernelSession`. -- execute-time crash path retries once: restart kernel, rerun handler. -- `restartCount > 1` in same session throws `Python kernel restarted too many times in this session`. - -Startup retry behavior: - -- shared gateway kernel creation retries once on `SharedGatewayCreateError` with HTTP 5xx. - -Resource exhaustion recovery: - -- detects `EMFILE`/`ENFILE`/"Too many open files" style failures -- clears tracked sessions -- calls `shutdownSharedGateway()` -- retries kernel session creation once +- if the retained subprocess is not alive before execution, it is replaced +- if execution fails because the subprocess died, the kernel is replaced and the code is retried once +- explicit `reset` is rejected while another reset for the same session key is already in progress ## 4) Environment/session variable injection -Kernel startup receives the optional session file path from executor: +Kernel startup and per-execution environment patching can receive: -- `PI_SESSION_FILE` (session state file path) +- `PI_SESSION_FILE` +- `PI_ARTIFACTS_DIR` +- `PI_TOOL_BRIDGE_URL` +- `PI_TOOL_BRIDGE_TOKEN` +- `PI_TOOL_BRIDGE_SESSION` -`PythonKernel.#initializeKernelEnvironment(...)` then runs init script inside kernel to: - -- `os.chdir(cwd)` -- inject env entries into `os.environ` -- prepend cwd to `sys.path` if missing - -Implication: - -- prelude helpers that read session context rely on this env var in Python process state. +The runner initializes process state so code executes in the requested cwd, managed env entries are reflected in `os.environ`, and cwd is available on `sys.path`. ## 5) Streaming/chunk and display handling (kernel-backed path) -The kernel client processes Jupyter protocol messages per execution: +The Python backend uses an NDJSON subprocess runner. The host processes frames per execution: -- `stream` -> text chunk to `onChunk` -- `execute_result` / `display_data` -> - - display text chosen by MIME precedence: `text/markdown` > `text/plain` > converted `text/html` - - structured outputs captured separately: - - `application/json` -> `{ type: "json" }` - - `image/png` -> `{ type: "image" }` - - `application/x-omp-status` -> `{ type: "status" }` (no text emission) -- `error` -> traceback text pushed to chunk stream + structured error metadata -- `input_request` -> emits stdin warning text, sends empty `input_reply`, marks stdin requested -- completion waits for both `execute_reply` and kernel `status=idle` +- `stdout` / `stderr` -> text chunks to `onChunk` +- `display` / `result` -> MIME bundle rendering +- `error` -> traceback text and structured error metadata +- `done` -> final status, execution count, cancellation state + +Display text MIME precedence: + +1. `text/markdown` +2. `text/plain` +3. converted `text/html` + +Structured outputs captured separately include: + +- `application/json` -> JSON display output +- `image/png` / `image/jpeg` -> image output +- `application/x-omp-status` -> status event Cancellation/timeout: -- abort signal triggers `interrupt()` (REST `/interrupt` + control-channel `interrupt_request`) -- result marks `cancelled=true` -- timeout path annotates output with `Command timed out after seconds` +- abort/timeout sends `SIGINT` to the runner +- if the runner does not settle after the interrupt grace window, shutdown escalates and the kernel is recreated on the next call +- timeout output is annotated with a timeout message ## 6) Truncation and artifact behavior -`OutputSink` in `src/session/streaming-output.ts` is used by kernel execution paths (`executeWithKernel`): +`OutputSink` in `src/session/streaming-output.ts` is used by kernel execution paths: -- sanitizes every chunk (`sanitizeText`) +- sanitizes every chunk - tracks total/output lines and bytes -- optional artifact spill file (`artifactPath`, `artifactId`) -- when in-memory buffer exceeds threshold (`DEFAULT_MAX_BYTES` unless overridden): - - marks truncated - - keeps tail bytes in memory (UTF-8 safe boundary) - - can spill full stream to artifact sink - -`dump()` returns: - -- visible output text (possibly tail-truncated) -- truncation flag + counts -- artifact ID (for `artifact://` references) +- optionally spills full output to an artifact file +- keeps a UTF-8-safe in-memory tail buffer when output exceeds the configured threshold `eval` converts this metadata into result truncation notices and TUI warnings. -`notebook` tool does **not** use `OutputSink`; it has no stream/artifact truncation pipeline because it does not execute code. +Notebook file conversion does **not** use `OutputSink`; it has no stream/artifact truncation pipeline because it does not execute code. ## 7) Renderer assumptions and formatting -## Notebook renderer (`notebookToolRenderer`) +## Read/edit notebook representation -- call view: status line with action + notebook path + cell/type metadata -- result view: - - success summary derived from `details` - - `cellSource` rendered via `renderCodeCell` - - markdown cells set language hint `markdown`; other cells have no explicit language override - - collapsed code preview limit is `PREVIEW_LIMITS.COLLAPSED_LINES * 2` - - supports expanded mode via shared render options - - uses render cache keyed by width + expanded state - -Error rendering assumption: - -- if first text content starts with `Error:`, renderer formats as notebook error block. +Notebook files are rendered to the model as text. The visible cell markers are part of the editable representation, not comments that are ignored during serialization. ## Python renderer (for actual execution output) Kernel-backed execution rendering expects: -- per-cell status transitions (`pending/running/complete/error`) -- optional structured status event section +- per-cell status transitions (`pending` / `running` / `complete` / `error`) +- optional structured status events - optional JSON output trees +- image outputs - truncation warnings + optional `artifact://` pointer -This renderer behavior is unrelated to `notebook` JSON editing results except that both reuse shared TUI primitives. +This renderer behavior is unrelated to notebook JSON editing except that both reuse shared TUI primitives. -## 8) Divergence from eval Python backend behavior +## 8) Practical workflow -If "plain Python execution" means the `eval` tool with `language: "python"`: +If a workflow needs both notebook mutation and execution: -- `eval` executes code in a kernel, persists state by mode, streams chunks, captures rich displays, handles interrupts/timeouts, and supports output truncation/artifacts. -- `notebook` performs deterministic notebook JSON mutations only; no execution, no kernel state, no chunk stream, no display outputs, no artifact pipeline. - -If a workflow needs both: - -1. edit notebook source with `notebook` -2. execute code cells via `eval` with `language: "python"` (manually passing code), not through `notebook` +1. read or edit the `.ipynb` file through the normal file tools +2. copy the desired cell source into `eval` cells with `language: "py"` to execute it +3. write resulting source changes back to the notebook if needed Current implementation does not provide a single tool that both mutates `.ipynb` and executes notebook cells through kernel context. diff --git a/docs/plugin-manager-installer-plumbing.md b/docs/plugin-manager-installer-plumbing.md index da14fbce5..dadbcb444 100644 --- a/docs/plugin-manager-installer-plumbing.md +++ b/docs/plugin-manager-installer-plumbing.md @@ -1,6 +1,6 @@ # Plugin manager and installer plumbing -This document describes how `omp plugin` operations mutate plugin state on disk and how installed plugins become runtime capabilities (tools and extensions today, hooks/commands path resolution available). +This document describes how `omp plugin` npm/link operations mutate plugin state on disk and how installed npm/link plugins become runtime capabilities (tools and extensions today, hooks/commands path resolution available). Marketplace installs use separate marketplace registries and cache plumbing; see `docs/marketplace.md`. ## Scope and architecture @@ -9,14 +9,14 @@ There are two plugin-management implementations in the codebase: 1. **Active path used by CLI commands**: `PluginManager` (`src/extensibility/plugins/manager.ts`) 2. **Legacy helper module**: installer functions (`src/extensibility/plugins/installer.ts`) -`omp plugin ...` command execution goes through `PluginManager`. +`omp plugin` npm/link actions go through `PluginManager`; marketplace actions go through `MarketplaceManager`. `installer.ts` still documents important safety checks and filesystem behavior, but it is not the path used by `src/commands/plugin.ts` + `src/cli/plugin-cli.ts`. ## Lifecycle: from CLI invocation to runtime availability ```text -omp plugin ... +omp plugin ... -> src/commands/plugin.ts -> runPluginCommand(...) in src/cli/plugin-cli.ts -> PluginManager method (install/list/uninstall/link/...) @@ -24,22 +24,28 @@ omp plugin ... -> runtime discovery: discoverAndLoadCustomTools(...) and discoverAndLoadExtensions(...) -> getAllPluginToolPaths(cwd) / getAllPluginExtensionPaths(cwd) -> custom tool loader imports tool modules; extension loader imports extension modules + +omp plugin install name@marketplace / omp install name@marketplace + -> MarketplaceManager + -> mutate ~/.omp/marketplaces.json, ~/.omp/plugins/installed_plugins.json, cache dirs + -> installed marketplace plugin cache is surfaced as plugin roots/capabilities ``` ### Command entrypoints - `src/commands/plugin.ts` defines command/flags and forwards to `runPluginCommand`. -- `src/cli/plugin-cli.ts` maps subcommands to `PluginManager` methods: +- `src/cli/plugin-cli.ts` maps npm/link subcommands to `PluginManager` methods: - `install`, `uninstall`, `list`, `link`, `doctor`, `features`, `config`, `enable`, `disable` -- No explicit `update` action exists; update is done by re-running `install` with a new package/version spec. +- `discover`, `upgrade`, and `marketplace ...` subcommands use `MarketplaceManager`. +- No explicit npm-plugin `update` action exists; update is done by re-running `install` with a new package/version spec. ## On-disk model Global plugin state lives under `~/.omp/plugins`: -- `package.json` — dependency manifest used by `bun install`/`bun uninstall` -- `node_modules/` — installed plugin packages or symlinks -- `omp-plugins.lock.json` — runtime state: +- `package.json` — dependency manifest used by `bun install`/`bun uninstall` for npm-installed plugins +- `node_modules/` — installed npm plugin packages or symlinks +- `omp-plugins.lock.json` — runtime state for npm/link plugins: - enabled/disabled per plugin - selected feature set per plugin - persisted plugin settings @@ -50,6 +56,13 @@ Project-local overrides live at: Overrides are read-only from manager/loader perspective (no write path here) and can disable plugins or override features/settings for this project. +Marketplace registries live separately: + +- `~/.omp/marketplaces.json` — configured marketplace catalogs +- `~/.omp/plugins/installed_plugins.json` — user-scoped marketplace installs +- `/.omp/plugins/installed_plugins.json` — project-scoped marketplace installs when available +- `~/.omp/plugins/cache/{marketplaces,plugins}/` — cached catalogs and plugin directories + ## Plugin spec parsing and metadata interpretation ## Install spec grammar @@ -171,10 +184,11 @@ For each enabled plugin: Each resolver includes base entries plus feature entries: +- base entries are always included - explicit feature list -> only selected features - `enabledFeatures === null` -> enable features marked `default: true` -Missing files are silently skipped (`existsSync` guard). +Manifest entries may point to a file or to a directory containing `index.ts`, `index.js`, `index.mjs`, or `index.cjs`. Missing files are silently skipped (`existsSync` guard). ## Current runtime wiring differences diff --git a/docs/porting-to-natives.md b/docs/porting-to-natives.md index 0fb9b84de..bdf4cd9ec 100644 --- a/docs/porting-to-natives.md +++ b/docs/porting-to-natives.md @@ -18,12 +18,12 @@ Avoid ports that depend on JS-only state or dynamic imports. N-API exports shoul `@oh-my-pi/pi-natives` no longer has a `packages/natives/src/` TypeScript wrapper layer. The package root points at generated native artifacts: -- runtime entry: `packages/natives/native/index.js` +- runtime entry/export wrapper: `packages/natives/native/index.js` - types entry: `packages/natives/native/index.d.ts` - loader helpers: `packages/natives/native/loader-state.js` - embedded manifest: `packages/natives/native/embedded-addon.js` -Consumers import directly from `@oh-my-pi/pi-natives`. The generated declarations are produced during `bun --cwd=packages/natives run build`. +Consumers import directly from `@oh-my-pi/pi-natives`. The generated declarations and explicit ESM exports are produced during `bun --cwd=packages/natives run build`. ## Anatomy of a native export @@ -38,9 +38,9 @@ Consumers import directly from `@oh-my-pi/pi-natives`. The generated declaration **Package/build side:** -- `packages/natives/scripts/build-native.ts` runs napi-rs, installs the `.node` artifact, copies generated `index.js`/`index.d.ts`, and appends enum runtime exports. -- `packages/natives/native/index.js` is the loader that chooses a candidate `.node` file and returns the loaded addon. -- `packages/natives/package.json` exposes only the package root (`@oh-my-pi/pi-natives`). +- `packages/natives/scripts/build-native.ts` runs napi-rs, installs the `.node` artifact, copies generated `index.d.ts`, and regenerates explicit ESM class/function exports plus enum runtime exports in the checked-in `native/index.js`. +- `packages/natives/native/index.js` is the ESM entrypoint that calls the loader, exposes named exports, and rejects install/compiled `.node` files that do not expose the package-version sentinel. +- `packages/natives/package.json` exposes only the package root (`@oh-my-pi/pi-natives`) as the import surface. At publish time the binaries are split out: the core ships the loader only (no `.node`), and each platform's `.node` is published as an optional-dependency leaf package `@oh-my-pi/pi-natives-` (`scripts/ci-release-publish.ts` + `packages/natives/scripts/gen-npm-packages.ts`). This is transparent to importers — you still `import` from `@oh-my-pi/pi-natives`. **Consumer side:** @@ -62,7 +62,7 @@ Consumers import directly from `@oh-my-pi/pi-natives`. The generated declaration - Run `bun --cwd=packages/natives run build`. - Confirm the generated `packages/natives/native/index.d.ts` includes the new export with the intended JS name/signature. -- Confirm `packages/natives/native/index.js` still has generated enum exports appended when enum changes are involved. +- Confirm `packages/natives/native/index.js` has generated explicit ESM exports for the new class/function and enum objects when enum changes are involved. 3. **Update consumers** @@ -94,7 +94,7 @@ The loader probes platform-tagged artifacts in deterministic order. For x64, sel Non-x64 uses `pi_natives..node`. -Compiled binaries also probe `//...` and a legacy user-data directory before package/executable locations. If any earlier candidate is stale, a new export may appear missing. +Compiled binaries also probe `//...` and a legacy user-data directory before package/executable locations. Windows `node_modules` installs stage leaf/core addons into the same versioned directory before probing. If any earlier candidate is stale, a new export may appear missing unless the version sentinel rejects it first. **Fix:** remove stale candidate/cache files and rebuild. @@ -105,16 +105,16 @@ rm packages/natives/native/pi_natives.--baseline.node bun --cwd=packages/natives run build ``` -For compiled binaries, delete the versioned addon cache shown in the loader error (normally under `~/.omp/natives/` unless `$XDG_DATA_HOME/omp` is used). +For compiled binaries or Windows staging, delete the versioned addon cache shown in the loader error (normally under `~/.omp/natives/` unless `$XDG_DATA_HOME/omp` is used). ### 2) Generated types do not match loaded binary -This can happen when `native/index.d.ts` was regenerated but the `.node` file being loaded is stale or from a different platform/variant. +This can happen when `native/index.d.ts` was regenerated but the `.node` file being loaded is stale, same-version incomplete, or from a different platform/variant. Different-version install/compiled binaries should be rejected by the version sentinel during loading. -Verify the loaded export set from the actual candidate path: +Verify the loaded export set from the actual candidate path reported by the loader: ```bash -bun -e 'const tag = `${process.platform}-${process.arch}`; const mod = require(`./packages/natives/native/pi_natives.${tag}.node`); console.log(Object.keys(mod).sort())' +bun -e 'import { createRequire } from "node:module"; const require = createRequire(import.meta.url); const mod = require(process.argv[2]); console.log(Object.keys(mod).sort())' -- /path/from/loader/error/pi_natives.[-variant].node ``` Fix the build/candidate mismatch. Do not paper over it with optional consumer checks if the export is required. @@ -123,9 +123,9 @@ Fix the build/candidate mismatch. Do not paper over it with optional consumer ch Keep N-API signatures simple and owned. Avoid borrowed references like `&str` in public exports. If you need structured data, use `#[napi(object)]` structs. If you need callbacks, use napi-rs `ThreadsafeFunction` and keep callback error/value behavior explicit. -### 4) Enum runtime exports +### 4) Enum runtime exports and ESM named exports -napi-rs declarations alone are not enough for JS callers that use enum objects at runtime. `scripts/gen-enums.ts` appends enum objects to `native/index.js`. If you add or change a native enum, verify both `native/index.d.ts` and the generated enum export block in `native/index.js`. +napi-rs declarations alone are not enough for JS callers that import named symbols or use enum objects at runtime. `scripts/gen-enums.ts` reads `native/index.d.ts`, writes explicit `export const ... = nativeBindings...` entries for public classes/functions, and emits enum objects in `native/index.js`. If you add or change a native export, verify both `native/index.d.ts` and the generated export block in `native/index.js`. ### 5) Benchmarking mistakes @@ -161,8 +161,8 @@ bench("feature/native", () => { ## Verification checklist - Generated `native/index.d.ts` includes the new export and intended TS signature. -- The loaded `.node` file's `Object.keys(require(candidate))` includes the new export. -- Runtime enum objects are present when the change adds/changes enums. +- `native/index.js` includes the generated named export; enum objects are present when the change adds/changes enums. +- The loaded `.node` file's `Object.keys(require(candidate))` includes the new export and the package-version sentinel. - Bench numbers are recorded in the PR/notes. - Call sites are updated only if native is faster/equal and behavior-compatible. - Obsolete JS code is removed when the native implementation becomes canonical. diff --git a/docs/provider-streaming-internals.md b/docs/provider-streaming-internals.md index f4df818ee..7ad3c1e47 100644 --- a/docs/provider-streaming-internals.md +++ b/docs/provider-streaming-internals.md @@ -5,7 +5,7 @@ This document explains how token/tool streaming is normalized in `@oh-my-pi/pi-a ## End-to-end flow 1. `streamSimple()` (`packages/ai/src/stream.ts`) maps generic options and dispatches to a provider stream function. -2. Provider stream functions translate provider-native stream events into the unified `AssistantMessageEvent` sequence. Current built-ins include Anthropic, OpenAI Responses/Completions/Codex/Azure Responses, Google Gemini/Gemini CLI/Vertex, Bedrock Converse, Ollama, Cursor, plus GitLab Duo/Kimi wrappers and extension-registered custom APIs. +2. Provider stream functions translate provider-native stream events into the unified `AssistantMessageEvent` sequence. Current built-ins include Anthropic, OpenAI Responses/Completions/Codex/Azure Responses, Google Gemini/Gemini CLI/Vertex, Bedrock Converse, Ollama, Cursor, pi-native gateway transport, plus GitLab Duo/Kimi/Synthetic wrappers and extension-registered custom APIs. 3. Each provider pushes events into `AssistantMessageEventStream` (`packages/ai/src/utils/event-stream.ts`), which throttles delta events and exposes: - async iteration for incremental updates - `result()` for final `AssistantMessage` @@ -72,16 +72,17 @@ Sources: `packages/ai/src/providers/openai-responses.ts`, `openai-codex-response Normalization points: -- `response.output_item.added` starts reasoning/text/function-call blocks -- reasoning summary events (`response.reasoning_summary_text.delta`) become `thinking_delta` +- `response.output_item.added` starts reasoning/text/function-call/custom-tool blocks +- reasoning summary events (`response.reasoning_summary_text.delta`) and raw reasoning events (`response.reasoning_text.delta`) become `thinking_delta` - output/refusal deltas become `text_delta` -- `response.function_call_arguments.delta` becomes `toolcall_delta` +- `response.function_call_arguments.delta` and `response.custom_tool_call_input.delta` become `toolcall_delta` - `response.output_item.done` emits `thinking_end` / `text_end` / `toolcall_end` -- `response.completed` maps status to stop reason and usage +- `response.completed` maps status to stop reason and usage; `response.failed` / SDK `error` events throw into the wrapper's terminal `error` path Tool-call argument streaming: -- same `partialJson` accumulation pattern as Anthropic +- same `partialJson` accumulation pattern as Anthropic for function-call JSON arguments +- custom tools stream raw string input and expose final arguments as `{ input: }` - providers that send only `response.function_call_arguments.done` still populate final args - tool call IDs are normalized as `"|"` @@ -139,13 +140,14 @@ If provider stream throws or signals failure, each provider wrapper catches and ## Malformed chunk / SSE parse failure behavior -For these provider paths, chunk/SSE framing is handled by vendor SDK streams (Anthropic SDK, OpenAI SDK, Google SDK). This code does not implement a custom SSE decoder here. +Most provider paths delegate chunk/SSE framing to vendor SDK streams (Anthropic SDK, OpenAI SDK, Google SDK). The Codex SSE fallback uses `readSseJson()` directly, and websocket Codex frames are normalized through the same event handler. Observed behavior in current implementation: -- malformed chunk/SSE parsing at SDK level surfaces as an exception or stream `error` event -- provider wrapper converts that into unified terminal `error` event -- no provider-specific resume/retry inside the stream function itself +- malformed SDK stream parsing surfaces as an exception or stream `error` event +- malformed Codex SSE JSON/framing throws from the local SSE reader +- provider wrapper converts failures into unified terminal `error` events +- no provider-specific resume/retry inside the stream function itself, except Codex websocket-to-SSE transport fallback before replay-unsafe output is emitted - higher-level retries are handled in `AgentSession` auto-retry logic (message-level retry, not stream-chunk replay) ## Cancellation boundaries @@ -211,9 +213,9 @@ Provider-specific (not fully abstracted): - [`../../ai/src/utils/event-stream.ts`](../packages/ai/src/utils/event-stream.ts) — generic stream queue + assistant delta throttling. - [`../../ai/src/utils/json-parse.ts`](../packages/ai/src/utils/json-parse.ts) — partial JSON parsing for streamed tool arguments. - [`../../ai/src/providers/anthropic.ts`](../packages/ai/src/providers/anthropic.ts) — Anthropic event translation and tool JSON delta accumulation. -- [`../../ai/src/providers/openai-responses.ts`](../packages/ai/src/providers/openai-responses.ts), [`openai-codex-responses.ts`](../packages/ai/src/providers/openai-codex-responses.ts), [`azure-openai-responses.ts`](../packages/ai/src/providers/azure-openai-responses.ts) — Responses-family event translation and status mapping. +- [`../../ai/src/providers/openai-responses.ts`](../packages/ai/src/providers/openai-responses.ts), [`openai-responses-shared.ts`](../packages/ai/src/providers/openai-responses-shared.ts), [`openai-codex-responses.ts`](../packages/ai/src/providers/openai-codex-responses.ts), [`azure-openai-responses.ts`](../packages/ai/src/providers/azure-openai-responses.ts) — Responses-family event translation and status mapping. - [`../../ai/src/providers/google.ts`](../packages/ai/src/providers/google.ts), [`google-gemini-cli.ts`](../packages/ai/src/providers/google-gemini-cli.ts), [`google-vertex.ts`](../packages/ai/src/providers/google-vertex.ts) — Gemini stream chunk-to-block translation variants. - [`../../ai/src/providers/google-shared.ts`](../packages/ai/src/providers/google-shared.ts) — Gemini finish-reason mapping and shared conversion rules. -- [`../../ai/src/providers/amazon-bedrock.ts`](../packages/ai/src/providers/amazon-bedrock.ts), [`openai-completions.ts`](../packages/ai/src/providers/openai-completions.ts), [`ollama.ts`](../packages/ai/src/providers/ollama.ts), [`cursor.ts`](../packages/ai/src/providers/cursor.ts) — additional built-in stream adapters using the same event contract. +- [`../../ai/src/providers/amazon-bedrock.ts`](../packages/ai/src/providers/amazon-bedrock.ts), [`openai-completions.ts`](../packages/ai/src/providers/openai-completions.ts), [`ollama.ts`](../packages/ai/src/providers/ollama.ts), [`cursor.ts`](../packages/ai/src/providers/cursor.ts), [`pi-native-client.ts`](../packages/ai/src/providers/pi-native-client.ts) — additional built-in stream adapters using the same event contract. - [`../../agent/src/agent-loop.ts`](../packages/agent/src/agent-loop.ts) — provider stream consumption and `message_update` bridging. - [`../src/session/agent-session.ts`](../packages/coding-agent/src/session/agent-session.ts) — session-level handling of streaming updates, abort, retry, and persistence. diff --git a/docs/python-repl.md b/docs/python-repl.md index e9a35c539..11a0ad631 100644 --- a/docs/python-repl.md +++ b/docs/python-repl.md @@ -10,21 +10,26 @@ It covers tool behavior, runner lifecycle, environment handling, execution seman - Subprocess kernel client: `src/eval/py/kernel.ts` - Python wrapper / NDJSON server: `src/eval/py/runner.py` - Prelude helpers loaded into every kernel: `src/eval/py/prelude.py` +- Host-side subagent helper bridge: `src/eval/agent-bridge.ts` - MIME bundle renderer (text + structured outputs): `src/eval/py/display.ts` - Interactive-mode renderer for user-triggered Python runs: `src/modes/components/eval-execution.ts` - Runtime/env filtering and Python resolution: `src/eval/py/runtime.ts` ## What eval's Python backend is -The `eval` tool executes one or more Python cells inside a long-lived `python3` subprocess that speaks NDJSON over stdin/stdout. No Jupyter, no kernel gateway, no extra pip dependencies — a vanilla Python 3.8+ interpreter is enough. Rich `display()` output (PIL, pandas, plotly, matplotlib figures) keeps working because the wrapper reimplements the MIME-bundle dispatch that IPython previously provided. +The `eval` tool executes one or more Python cells inside a retained `python` subprocess that speaks NDJSON over stdin/stdout. No Jupyter gateway and no extra pip dependencies are required — a vanilla Python 3.8+ interpreter is enough. Rich `display()` output (PIL, pandas, plotly, matplotlib figures) keeps working because the wrapper implements MIME-bundle dispatch. Tool params: ```ts { - cells: Array<{ code: string; title?: string }>; - timeout?: number; // seconds, clamped to 1..600, default 30 - reset?: boolean; // reset selected runtime before the first cell only + cells: Array<{ + language: "py" | "js"; + code: string; + title?: string; + timeout?: number; // seconds, clamped to 1..600, default 30. Inactivity budget — see "Cell timeout". + reset?: boolean; // reset this cell's selected runtime before execution + }>; } ``` @@ -32,7 +37,7 @@ The tool is `concurrency = "exclusive"` for a session, so calls do not overlap. ## Kernel lifecycle -Each kernel is a single Python subprocess: `python -u `. The runner is bundled with the host binary (Bun text import), written to `~/.omp/python-env`-adjacent tmp cache once per script-hash, and reused by every subsequent spawn. +Each Python kernel is a single subprocess: ` -u `. The runner is bundled with the host binary (Bun text import), written to an `omp-python-runner` cache under the OS temp directory once per script hash, and reused by subsequent spawns. Kernel startup sequence: @@ -76,25 +81,25 @@ Status events the prelude emits (e.g. `_emit_status("find", count=…)`) ship in The runner's source transformer rewrites IPython-style magics to plain Python calls before parsing. Supported set: -| Magic | Effect | -| --- | --- | -| `%pip ` | `python -m pip ` with live streaming output. Newly installed packages are evicted from `sys.modules` so the next `import` picks up the fresh install. | -| `%cd ` | `os.chdir(path)` (with `~` expansion); emits status event. | -| `%pwd` | Returns `os.getcwd()`. | -| `%ls [path]` | Returns `sorted(os.listdir(path))`. | -| `%env [KEY[=VAL]]` | List, read, or set env vars (matches prelude `env()` semantics). | -| `%set_env KEY VALUE` | Set `os.environ[KEY]`. | -| `%time ` / `%timeit ` | Time the expression; emits status event with elapsed ms. | -| `%who` / `%whos` | List user-namespace names. | -| `%reset` | Clear user globals and re-inject prelude. | -| `%load ` | Read a file into a fresh cell and execute. | -| `%run ` | `runpy.run_path` and merge globals back. | -| `%%bash` / `%%sh` | Run the cell body via `bash`/`sh`. | -| `%%capture [name]` | Run body with stdout/stderr captured into `name`. | -| `%%timeit` | Time the cell body. | -| `%%writefile ` | Write body to file. | -| `!cmd` / `var = !cmd` | Run command via subprocess shell; returns an SList-style result with `.n` / `.s` helpers. | -| `var = %name args` | Assignment forms work for line magics and `!cmd`. | +| Magic | Effect | +| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `%pip ` | `python -m pip ` with live streaming output. Newly installed packages are evicted from `sys.modules` so the next `import` picks up the fresh install. | +| `%cd ` | `os.chdir(path)` (with `~` expansion); emits status event. | +| `%pwd` | Returns `os.getcwd()`. | +| `%ls [path]` | Returns `sorted(os.listdir(path))`. | +| `%env [KEY[=VAL]]` | List, read, or set env vars (matches prelude `env()` semantics). | +| `%set_env KEY VALUE` | Set `os.environ[KEY]`. | +| `%time ` / `%timeit ` | Time the expression; emits status event with elapsed ms. | +| `%who` / `%whos` | List user-namespace names. | +| `%reset` | Clear user globals and re-inject prelude. | +| `%load ` | Read a file into a fresh cell and execute. | +| `%run ` | `runpy.run_path` and merge globals back. | +| `%%bash` / `%%sh` | Run the cell body via `bash`/`sh`. | +| `%%capture [name]` | Run body with stdout/stderr captured into `name`. | +| `%%timeit` | Time the cell body. | +| `%%writefile ` | Write body to file. | +| `!cmd` / `var = !cmd` | Run command via subprocess shell; returns an SList-style result with `.n` / `.s` helpers. | +| `var = %name args` | Assignment forms work for line magics and `!cmd`. | Unknown magic names raise `NameError: UsageError: ...` inside the cell. @@ -103,12 +108,11 @@ Unknown magic names raise `NameError: UsageError: ...` inside the cell. `python.kernelMode` controls retained kernel reuse: - `session` (default) - - Reuses kernel sessions keyed by session file plus cwd when a session file exists; otherwise by cwd. - - Execution is serialized per session via a queue. - - Idle sessions are evicted after 5 minutes. - - At most 4 sessions; oldest is evicted on overflow. - - Heartbeat checks detect dead kernels. - - Auto-restart allowed once; repeated crash ⇒ hard failure. + - Reuses kernel sessions keyed by namespaced eval session id plus cwd. + - Multiple owners can share the same retained kernel for that key. + - Calls through the tool are exclusive, so tool invocations do not overlap. + - A dead retained subprocess is replaced before execution. + - If the subprocess dies during execution, it is replaced and the cell is retried once. - `per-call` - Spawns a fresh subprocess for each request. - Shuts the subprocess down after the request. @@ -116,7 +120,7 @@ Unknown magic names raise `NameError: UsageError: ...` inside the cell. ### Multi-cell behavior in a single tool call -Cells run sequentially in the same kernel instance for that tool call. +Python cells run sequentially in the same selected Python kernel instance for that tool call. If an intermediate cell fails: @@ -124,7 +128,7 @@ If an intermediate cell fails: - Tool returns a targeted error indicating which cell failed. - Later cells are not executed. -`reset=true` only applies to the first cell execution in that call. +`reset=true` is per cell and resets that language runtime before the cell executes. ## Environment filtering and runtime resolution @@ -146,25 +150,25 @@ The runner additionally receives `PYTHONUNBUFFERED=1` and `PYTHONIOENCODING=utf- ## Tool availability and mode selection -`eval.py` / `eval.js` (both default `true`) plus optional `PI_PY` override controls eval backend exposure: +`eval.py` / `eval.js` (both default `true`) plus optional boolean env flags `PI_PY` / `PI_JS` control eval backend exposure: -- Python backend only (`eval.py=true`, `eval.js=false`) -- JavaScript backend only (`eval.py=false`, `eval.js=true`) -- both backends +- Python backend only (`eval.py=true`, `eval.js=false`, or `PI_PY=1 PI_JS=0`) +- JavaScript backend only (`eval.py=false`, `eval.js=true`, or `PI_PY=0 PI_JS=1`) +- both backends (`eval.py=true`, `eval.js=true`, or `PI_PY=1 PI_JS=1`) -`PI_PY` accepted values: +`PI_PY` and `PI_JS` use normal boolean flag parsing. If either env var is set, the env pair overrides the per-key settings; an unset member of the pair defaults to enabled. -- `0` / `bash` → JavaScript backend only -- `1` / `py` → Python backend only -- `mix` / `both` → both backends +If Python preflight fails and `eval.js` is enabled, `eval` remains available for `js` cells; `py` cells fail with a Python-backend availability error. -If Python preflight fails and `eval.js` is enabled, `eval` remains available and dispatches to JavaScript unless `language: "python"` is explicitly requested. +Python prelude helpers include `agent(prompt, *, agent_type="task", model=None, context=None, label=None, schema=None)`. It synchronously calls the host bridge, runs one subagent through the task executor, and returns the final text. When `schema` is supplied, the helper parses the subagent's JSON output and returns the object. ## Execution flow and cancellation/timeout -### Tool-level timeout +### Cell timeout -`eval` timeout is in seconds, default 30, clamped to `1..600`. The tool combines caller abort signal and timeout signal with `AbortSignal.any(...)`. +Each eval cell `timeout` is in seconds, defaults to 30, and is clamped to `1..600`. It is a **wall-clock budget on the cell's own work** that the watchdog (`IdleTimeout`, `src/eval/idle-timeout.ts`) enforces, **but it is paused while a host-side `agent()`/`parallel()`/`llm()` bridge call is in flight**: those calls pump a heartbeat (`withBridgeHeartbeat`, `src/eval/heartbeat.ts`) that re-arms the watchdog, so a long fanout or a slow completion runs to completion instead of being killed mid-stream. + +The heartbeat is the **sole** signal that extends the budget. Everything else the cell does — compute, `stdout`/`stderr`, `log()`/`phase()`, and ordinary (non-agent) tool calls — counts against `timeout`, so a cell that is not delegating to an agent/llm is bounded by a plain wall-clock timeout. The tool combines the caller abort signal, the session abort signal, and the watchdog's signal with `AbortSignal.any(...)`; no wall-clock deadline is passed to the backend, so neither runtime arms a competing fixed timer. ### Kernel execution cancellation @@ -172,7 +176,7 @@ On abort/timeout: - The host sends `kill("SIGINT")` to the runner subprocess. - The runner's exec-time signal handler raises `KeyboardInterrupt` inside the user code. -- Result includes `cancelled=true`; timeout path annotates output as `Command timed out after seconds`. +- Result includes `cancelled=true`; the timeout path annotates output as `Command timed out after seconds`. - Between requests the runner installs `SIG_IGN` for SIGINT so a stray cancel does not tear down the kernel. If a second cancel is required (runner stuck in C code), the host escalates to `SIGTERM` and the session restarts on the next call. @@ -217,7 +221,7 @@ Output is streamed through `OutputSink` and may be persisted to artifact storage - Tool renderer (`eval.ts`): - shows code-cell blocks with per-cell status - collapsed preview defaults to 10 lines - - supports expanded mode for full output and richer status detail + - supports expanded mode for all output retained in the tool result - Interactive renderer (`eval-execution.ts`): - used for user-triggered Python execution in TUI - collapsed preview defaults to 20 lines @@ -226,7 +230,7 @@ Output is streamed through `OutputSink` and may be persisted to artifact storage ## Operational troubleshooting -- **Python backend not available** — Check `eval.py`, `PI_PY`, and that `python`/`python3` is on PATH. If preflight fails and `eval.js` is enabled, omit `language` or pass `language: "js"` to use JavaScript. +- **Python backend not available** — Check `eval.py`, `PI_PY`, and that `python`/`python3` is on PATH. If preflight fails and `eval.js` is enabled, use a `js` cell. - **No Python on PATH** — Install a system Python 3.8+ or place a venv at `~/.omp/python-env`. `omp setup python --check` reports the resolved interpreter. - **Execution hangs then times out** — Increase tool `timeout` (max 600s) if workload is legitimate. For stuck native code, cancellation triggers `SIGINT` first then escalates; the session restarts on the next request. - **stdin/input prompts in Python code** — `input()` is not supported; pass data programmatically. @@ -234,7 +238,7 @@ Output is streamed through `OutputSink` and may be persisted to artifact storage ## Relevant environment variables -- `PI_PY` — tool exposure override +- `PI_PY` / `PI_JS` — eval backend exposure overrides - `PI_PYTHON_SKIP_CHECK=1` — bypass Python preflight/warm checks - `PI_PYTHON_INTEGRATION=1` — enable gated integration tests that spawn a real Python - `PI_PYTHON_IPC_TRACE=1` — log NDJSON frames exchanged with the runner subprocess diff --git a/docs/resolve-tool-runtime.md b/docs/resolve-tool-runtime.md index 63d65233c..aa2c3c829 100644 --- a/docs/resolve-tool-runtime.md +++ b/docs/resolve-tool-runtime.md @@ -14,8 +14,9 @@ This document explains how preview/apply workflows are modeled in coding-agent a `resolve` is a hidden tool that finalizes a pending preview action. -- `action: "apply"` executes the queued action's `apply(reason)` callback and returns that result with resolve metadata. -- `action: "discard"` invokes `reject(reason)` if provided; otherwise returns `Discarded: