diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e3329b1dad9..a313c4d341b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ on: jobs: quality: name: Format, Lint, Typecheck, Test, Browser Test, Build - runs-on: blacksmith-8vcpu-ubuntu-2404 + runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Checkout @@ -76,7 +76,7 @@ jobs: release_smoke: name: Release Smoke - runs-on: blacksmith-8vcpu-ubuntu-2404 + runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Checkout diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d90479087cf..bb8c22638a8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,81 +1,27 @@ name: Release on: - push: - tags: - - "v*.*.*" - - "!v*-nightly.*" - schedule: - - cron: "0 */3 * * *" workflow_dispatch: inputs: - channel: - description: "Release channel" - required: false - default: stable - type: choice - options: - - stable - - nightly version: description: "Release version (for example 1.2.3 or v1.2.3)" - required: false + required: true type: string permissions: - contents: read + contents: write id-token: none jobs: - check_changes: - name: Check for changes since last nightly - if: github.event_name == 'schedule' - runs-on: blacksmith-8vcpu-ubuntu-2404 - outputs: - has_changes: ${{ steps.check.outputs.has_changes }} - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - id: check - name: Compare HEAD to last nightly tag - run: | - last_nightly_tag=$(git tag --list 'v*-nightly.*' 'nightly-v*' --sort=-creatordate | head -n 1) - if [[ -z "$last_nightly_tag" ]]; then - echo "No previous nightly tag found. Proceeding with release." - echo "has_changes=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - - last_nightly_sha=$(git rev-parse "$last_nightly_tag^{commit}") - head_sha=$(git rev-parse HEAD) - - if [[ "$last_nightly_sha" == "$head_sha" ]]; then - echo "No changes on main since last nightly release ($last_nightly_tag). Skipping." - echo "has_changes=false" >> "$GITHUB_OUTPUT" - else - echo "Changes detected on main since $last_nightly_tag ($last_nightly_sha → $head_sha). Proceeding." - echo "has_changes=true" >> "$GITHUB_OUTPUT" - fi - preflight: name: Preflight - needs: [check_changes] - if: | - !failure() && !cancelled() && - (github.event_name != 'schedule' || needs.check_changes.outputs.has_changes == 'true') - runs-on: blacksmith-8vcpu-ubuntu-2404 + runs-on: ubuntu-latest timeout-minutes: 10 outputs: - release_channel: ${{ steps.release_meta.outputs.release_channel }} version: ${{ steps.release_meta.outputs.version }} tag: ${{ steps.release_meta.outputs.tag }} release_name: ${{ steps.release_meta.outputs.name }} - short_sha: ${{ steps.release_meta.outputs.short_sha }} previous_tag: ${{ steps.previous_tag.outputs.previous_tag }} - cli_dist_tag: ${{ steps.release_meta.outputs.cli_dist_tag }} is_prerelease: ${{ steps.release_meta.outputs.is_prerelease }} make_latest: ${{ steps.release_meta.outputs.make_latest }} ref: ${{ github.sha }} @@ -102,56 +48,22 @@ jobs: name: Resolve release version shell: bash env: - DISPATCH_CHANNEL: ${{ github.event.inputs.channel }} DISPATCH_VERSION: ${{ github.event.inputs.version }} - NIGHTLY_DATE: ${{ github.run_started_at }} - NIGHTLY_SHA: ${{ github.sha }} - NIGHTLY_RUN_NUMBER: ${{ github.run_number }} run: | - if [[ "${GITHUB_EVENT_NAME}" == "schedule" || ( "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && "${DISPATCH_CHANNEL:-stable}" == "nightly" ) ]]; then - nightly_date="$(date -u -d "$NIGHTLY_DATE" +%Y%m%d)" - - node scripts/resolve-nightly-release.ts \ - --date "$nightly_date" \ - --run-number "$NIGHTLY_RUN_NUMBER" \ - --sha "$NIGHTLY_SHA" \ - --github-output - - echo "release_channel=nightly" >> "$GITHUB_OUTPUT" - echo "cli_dist_tag=nightly" >> "$GITHUB_OUTPUT" - echo "is_prerelease=true" >> "$GITHUB_OUTPUT" - echo "make_latest=false" >> "$GITHUB_OUTPUT" - else - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then - raw="${DISPATCH_VERSION}" - if [[ -z "$raw" ]]; then - echo "workflow_dispatch stable releases require the version input." >&2 - exit 1 - fi - else - raw="${GITHUB_REF_NAME}" - fi - - version="${raw#v}" - if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then - echo "Invalid release version: $raw" >&2 - exit 1 - fi - - echo "release_channel=stable" >> "$GITHUB_OUTPUT" - echo "version=$version" >> "$GITHUB_OUTPUT" - echo "tag=v$version" >> "$GITHUB_OUTPUT" - echo "name=T3 Code v$version" >> "$GITHUB_OUTPUT" - echo "cli_dist_tag=latest" >> "$GITHUB_OUTPUT" - if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "is_prerelease=false" >> "$GITHUB_OUTPUT" - echo "make_latest=true" >> "$GITHUB_OUTPUT" - else - echo "is_prerelease=true" >> "$GITHUB_OUTPUT" - echo "make_latest=false" >> "$GITHUB_OUTPUT" - fi + raw="${DISPATCH_VERSION}" + version="${raw#v}" + if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then + echo "Invalid release version: $raw" >&2 + exit 1 fi + echo "release_channel=stable" >> "$GITHUB_OUTPUT" + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "tag=v$version" >> "$GITHUB_OUTPUT" + echo "name=T3 Code v$version" >> "$GITHUB_OUTPUT" + echo "is_prerelease=false" >> "$GITHUB_OUTPUT" + echo "make_latest=true" >> "$GITHUB_OUTPUT" + - name: Lint run: bun run lint @@ -165,7 +77,7 @@ jobs: name: Resolve previous release tag run: | node scripts/resolve-previous-release-tag.ts \ - --channel "${{ steps.release_meta.outputs.release_channel }}" \ + --channel "stable" \ --current-tag "${{ steps.release_meta.outputs.tag }}" \ --github-output @@ -180,30 +92,15 @@ jobs: matrix: include: - label: macOS arm64 - runner: blacksmith-12vcpu-macos-26 + runner: macos-latest platform: mac target: dmg arch: arm64 - - label: macOS x64 - runner: blacksmith-12vcpu-macos-26 - platform: mac - target: dmg - arch: x64 - label: Linux x64 - runner: blacksmith-32vcpu-ubuntu-2404 + runner: ubuntu-latest platform: linux target: AppImage arch: x64 - - label: Windows x64 - runner: blacksmith-32vcpu-windows-2025 - platform: win - target: nsis - arch: x64 - # - label: Windows arm64 - # runner: windows-11-arm - # platform: win - # target: nsis - # arch: arm64 steps: - name: Checkout uses: actions/checkout@v6 @@ -227,147 +124,15 @@ jobs: - name: Align package versions to release version run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}" - - name: Install Spectre-mitigated MSVC libs - if: matrix.platform == 'win' - shell: pwsh - run: | - $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" - $installPath = & $vswhere -products * -latest -property installationPath - $setupExe = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\setup.exe" - $proc = Start-Process -FilePath $setupExe ` - -ArgumentList "modify", "--installPath", "`"$installPath`"", "--add", ` - "Microsoft.VisualStudio.Component.VC.Tools.x86.x64.Spectre", "--quiet", "--norestart" ` - -Wait -PassThru -NoNewWindow - if ($null -eq $proc -or $proc.ExitCode -ne 0) { - $code = if ($null -ne $proc) { $proc.ExitCode } else { 1 } - Write-Error "Visual Studio Installer failed with exit code $code" - exit $code - } - - - name: Prepare Azure Trusted Signing - if: matrix.platform == 'win' - shell: pwsh - env: - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} - AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} - AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }} - AZURE_TRUSTED_SIGNING_PUBLISHER_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_PUBLISHER_NAME }} - run: | - $ErrorActionPreference = "Stop" - - $requiredSecrets = @( - $env:AZURE_TENANT_ID, - $env:AZURE_CLIENT_ID, - $env:AZURE_CLIENT_SECRET, - $env:AZURE_TRUSTED_SIGNING_ENDPOINT, - $env:AZURE_TRUSTED_SIGNING_ACCOUNT_NAME, - $env:AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME, - $env:AZURE_TRUSTED_SIGNING_PUBLISHER_NAME - ) - if ($requiredSecrets | Where-Object { [string]::IsNullOrWhiteSpace($_) }) { - Write-Host "Azure Trusted Signing disabled; skipping TrustedSigning module preparation." - exit 0 - } - - try { - Install-PackageProvider ` - -Name NuGet ` - -MinimumVersion 2.8.5.201 ` - -Force ` - -Scope CurrentUser ` - -ErrorAction Stop - } catch { - Write-Warning "Could not bootstrap NuGet package provider. Continuing because the runner may already have a usable provider. $($_.Exception.Message)" - } - - Install-Module ` - -Name TrustedSigning ` - -MinimumVersion 0.5.0 ` - -Force ` - -AllowClobber ` - -Repository PSGallery ` - -Scope CurrentUser ` - -ErrorAction Stop - - Import-Module TrustedSigning -MinimumVersion 0.5.0 -Force - Get-Command Invoke-TrustedSigning -ErrorAction Stop - - $moduleRoots = @( - [System.IO.Path]::Combine([Environment]::GetFolderPath("MyDocuments"), "PowerShell", "Modules"), - [System.IO.Path]::Combine([Environment]::GetFolderPath("MyDocuments"), "WindowsPowerShell", "Modules"), - [System.IO.Path]::Combine($env:ProgramFiles, "PowerShell", "Modules"), - [System.IO.Path]::Combine($env:ProgramFiles, "WindowsPowerShell", "Modules") - ) - $modulePathEntries = @($moduleRoots + ($env:PSModulePath -split ";")) | - Where-Object { $_ -and (Test-Path $_) } | - Select-Object -Unique - "PSModulePath=$($modulePathEntries -join ';')" >> $env:GITHUB_ENV - - name: Build desktop artifact shell: bash - env: - CSC_LINK: ${{ secrets.CSC_LINK }} - CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} - APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} - APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} - APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} - AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} - AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }} - AZURE_TRUSTED_SIGNING_PUBLISHER_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_PUBLISHER_NAME }} run: | - args=( - --platform "${{ matrix.platform }}" - --target "${{ matrix.target }}" - --arch "${{ matrix.arch }}" - --build-version "${{ needs.preflight.outputs.version }}" + bun run dist:desktop:artifact -- \ + --platform "${{ matrix.platform }}" \ + --target "${{ matrix.target }}" \ + --arch "${{ matrix.arch }}" \ + --build-version "${{ needs.preflight.outputs.version }}" \ --verbose - ) - - has_all() { - for value in "$@"; do - if [[ -z "$value" ]]; then - return 1 - fi - done - return 0 - } - - if [[ "${{ matrix.platform }}" == "mac" ]]; then - if has_all "$CSC_LINK" "$CSC_KEY_PASSWORD" "$APPLE_API_KEY" "$APPLE_API_KEY_ID" "$APPLE_API_ISSUER"; then - key_path="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY_ID}.p8" - printf '%s' "$APPLE_API_KEY" > "$key_path" - export APPLE_API_KEY="$key_path" - echo "macOS signing enabled." - args+=(--signed) - else - echo "macOS signing disabled (missing one or more Apple signing secrets)." - fi - elif [[ "${{ matrix.platform }}" == "win" ]]; then - if has_all \ - "$AZURE_TENANT_ID" \ - "$AZURE_CLIENT_ID" \ - "$AZURE_CLIENT_SECRET" \ - "$AZURE_TRUSTED_SIGNING_ENDPOINT" \ - "$AZURE_TRUSTED_SIGNING_ACCOUNT_NAME" \ - "$AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME" \ - "$AZURE_TRUSTED_SIGNING_PUBLISHER_NAME"; then - echo "Windows signing enabled (Azure Trusted Signing)." - args+=(--signed) - else - echo "Windows signing disabled (missing one or more Azure Trusted Signing secrets)." - fi - else - echo "Signing disabled for ${{ matrix.platform }}." - fi - - bun run dist:desktop:artifact -- "${args[@]}" - name: Collect release assets shell: bash @@ -380,7 +145,6 @@ jobs: "release/*.dmg" \ "release/*.zip" \ "release/*.AppImage" \ - "release/*.exe" \ "release/*.blockmap" \ "release/*.yml"; do for file in $pattern; do @@ -388,25 +152,6 @@ jobs: done done - if [[ "${{ matrix.platform }}" == "mac" && "${{ matrix.arch }}" != "arm64" ]]; then - shopt -s nullglob - for manifest in release-publish/*-mac.yml; do - mv "$manifest" "${manifest%.yml}-${{ matrix.arch }}.yml" - done - fi - - # Enable if Windows arm64 builds are enabled. - # Windows updater metadata is channel-specific (for example - # "latest.yml" or "nightly.yml"). Suffix each per-arch copy so the - # release job can merge matching arm64/x64 manifests back into one - # canonical manifest per channel. - # if [[ "${{ matrix.platform }}" == "win" ]]; then - # shopt -s nullglob - # for manifest in release-publish/*.yml; do - # mv "$manifest" "${manifest%.yml}-win-${{ matrix.arch }}.yml" - # done - # fi - - name: Upload build artifacts uses: actions/upload-artifact@v7 with: @@ -414,15 +159,12 @@ jobs: path: release-publish/* if-no-files-found: error - publish_cli: - name: Publish CLI to npm + release: + name: Publish GitHub Release needs: [preflight, build] if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.build.result == 'success' }} - runs-on: ubuntu-24.04 # blacksmith-8vcpu-ubuntu-2404 + runs-on: ubuntu-latest timeout-minutes: 10 - permissions: - contents: read - id-token: write steps: - name: Checkout uses: actions/checkout@v6 @@ -438,55 +180,23 @@ jobs: uses: actions/setup-node@v6 with: node-version-file: package.json - registry-url: https://registry.npmjs.org - name: Install dependencies - run: bun install --frozen-lockfile --filter=t3 --filter=@t3tools/web --filter=@t3tools/scripts + run: bun install --frozen-lockfile - name: Align package versions to release version run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}" - - name: Build web package - run: bun --filter=@t3tools/web run build - - - name: Build CLI package - run: bun --filter=t3 run build - - - name: Publish CLI package - run: node apps/server/scripts/cli.ts publish --tag "${{ needs.preflight.outputs.cli_dist_tag }}" --app-version "${{ needs.preflight.outputs.version }}" --verbose - - release: - name: Publish GitHub Release - needs: [preflight, build, publish_cli] - if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.build.result == 'success' && needs.publish_cli.result == 'success' }} - runs-on: blacksmith-8vcpu-ubuntu-2404 - timeout-minutes: 10 - steps: - - id: app_token - name: Mint release app token - uses: actions/create-github-app-token@v2 - with: - app-id: ${{ secrets.RELEASE_APP_ID }} - private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} - owner: ${{ github.repository_owner }} - - - name: Checkout - uses: actions/checkout@v6 - with: - ref: ${{ needs.preflight.outputs.ref }} - - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version-file: package.json - - - name: Setup Node - uses: actions/setup-node@v6 - with: - node-version-file: package.json + - name: Build t3 server package + run: bun run build --filter=t3 - - name: Install dependencies - run: bun install --frozen-lockfile --filter=@t3tools/scripts + - name: Pack t3 CLI tarball + run: | + mkdir -p release-assets + node apps/server/scripts/cli.ts pack \ + --app-version "${{ needs.preflight.outputs.version }}" \ + --pack-destination "$(pwd)/release-assets" \ + --verbose - name: Download all desktop artifacts uses: actions/download-artifact@v8 @@ -495,46 +205,6 @@ jobs: merge-multiple: true path: release-assets - - name: Merge macOS updater manifests - run: | - shopt -s nullglob - for x64_manifest in release-assets/*-mac-x64.yml; do - arm64_manifest="${x64_manifest%-x64.yml}.yml" - if [[ -f "$arm64_manifest" ]]; then - node scripts/merge-update-manifests.ts --platform mac "$arm64_manifest" "$x64_manifest" - rm -f "$x64_manifest" - fi - done - - # - name: Merge Windows updater manifests - # run: | - # shopt -s nullglob - # found_windows_manifest=false - # for x64_manifest in release-assets/*-win-x64.yml; do - # if [[ "$(basename "$x64_manifest")" == builder-debug-* ]]; then - # continue - # fi - - # arm64_manifest="${x64_manifest/-x64.yml/-arm64.yml}" - # output_manifest="${x64_manifest/-win-x64.yml/.yml}" - # if [[ ! -f "$arm64_manifest" ]]; then - # echo "Missing matching arm64 Windows manifest for $x64_manifest" >&2 - # exit 1 - # fi - - # found_windows_manifest=true - # node scripts/merge-update-manifests.ts --platform win \ - # "$arm64_manifest" \ - # "$x64_manifest" \ - # "$output_manifest" - # rm -f "$arm64_manifest" "$x64_manifest" - # done - - # if [[ "$found_windows_manifest" != true ]]; then - # echo "No Windows updater manifests found to merge." >&2 - # exit 1 - # fi - - name: Publish release if: needs.preflight.outputs.previous_tag != '' uses: softprops/action-gh-release@v2 @@ -550,11 +220,10 @@ jobs: release-assets/*.dmg release-assets/*.zip release-assets/*.AppImage - release-assets/*.exe - release-assets/*.blockmap release-assets/*.yml + release-assets/*.tgz fail_on_unmatched_files: true - token: ${{ steps.app_token.outputs.token }} + token: ${{ secrets.GITHUB_TOKEN }} - name: Publish first release if: needs.preflight.outputs.previous_tag == '' @@ -570,236 +239,7 @@ jobs: release-assets/*.dmg release-assets/*.zip release-assets/*.AppImage - release-assets/*.exe - release-assets/*.blockmap release-assets/*.yml + release-assets/*.tgz fail_on_unmatched_files: true - token: ${{ steps.app_token.outputs.token }} - - deploy_web: - name: Deploy hosted web app - needs: [preflight, release] - if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.release.result == 'success' }} - runs-on: blacksmith-8vcpu-ubuntu-2404 - timeout-minutes: 10 - env: - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} - VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} - T3CODE_WEB_ROUTER_URL: ${{ vars.T3CODE_WEB_ROUTER_URL }} - T3CODE_WEB_LATEST_DOMAIN: ${{ vars.T3CODE_WEB_LATEST_DOMAIN }} - T3CODE_WEB_NIGHTLY_DOMAIN: ${{ vars.T3CODE_WEB_NIGHTLY_DOMAIN }} - VERCEL_TEAM_SLUG: ${{ vars.VERCEL_TEAM_SLUG }} - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - ref: ${{ needs.preflight.outputs.ref }} - - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version-file: package.json - - - name: Setup Node - uses: actions/setup-node@v6 - with: - node-version-file: package.json - - - name: Install release tooling dependencies - run: bun install --frozen-lockfile --filter=@t3tools/scripts --filter=@t3tools/web - - - name: Align package versions to release version - run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}" - - - name: Refresh release lockfile - run: bun install --lockfile-only --ignore-scripts - - - name: Deploy and alias channel - shell: bash - run: | - set -euo pipefail - - if [[ -z "${VERCEL_TOKEN:-}" || -z "${VERCEL_ORG_ID:-}" || -z "${VERCEL_PROJECT_ID:-}" ]]; then - echo "Missing one or more required Vercel secrets: VERCEL_TOKEN, VERCEL_ORG_ID, VERCEL_PROJECT_ID." >&2 - exit 1 - fi - - router_url="${T3CODE_WEB_ROUTER_URL:-https://app.t3.codes}" - latest_domain="${T3CODE_WEB_LATEST_DOMAIN:-latest.app.t3.codes}" - nightly_domain="${T3CODE_WEB_NIGHTLY_DOMAIN:-nightly.app.t3.codes}" - router_domain="${router_url#http://}" - router_domain="${router_domain#https://}" - router_domain="${router_domain%%/*}" - - if [[ "${{ needs.preflight.outputs.release_channel }}" == "stable" ]]; then - channel_domain="$latest_domain" - channel_name="latest" - else - channel_domain="$nightly_domain" - channel_name="nightly" - fi - - vercel_scope="${VERCEL_TEAM_SLUG:-$VERCEL_ORG_ID}" - vercel_scope_args=(--scope "$vercel_scope") - - echo "Deploying hosted web app for $channel_name channel." - deployment_url="$( - bunx vercel@53.1.1 deploy \ - --prod \ - --skip-domain \ - --yes \ - --token "$VERCEL_TOKEN" \ - "${vercel_scope_args[@]}" \ - --build-env "APP_VERSION=${{ needs.preflight.outputs.version }}" \ - --build-env "VITE_HOSTED_APP_URL=$router_url" \ - --build-env "VITE_HOSTED_APP_CHANNEL=$channel_name" - )" - - echo "Aliasing $deployment_url to $channel_domain." - bunx vercel@53.1.1 alias set "$deployment_url" "$channel_domain" \ - --token "$VERCEL_TOKEN" \ - "${vercel_scope_args[@]}" - - if [[ "$channel_name" == "latest" && -n "$router_domain" && "$router_domain" != "$channel_domain" ]]; then - echo "Aliasing $deployment_url to router domain $router_domain." - bunx vercel@53.1.1 alias set "$deployment_url" "$router_domain" \ - --token "$VERCEL_TOKEN" \ - "${vercel_scope_args[@]}" - fi - - finalize: - name: Finalize release - if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.release.result == 'success' && needs.preflight.outputs.release_channel == 'stable' }} - needs: [preflight, release] - runs-on: blacksmith-8vcpu-ubuntu-2404 - timeout-minutes: 10 - steps: - - id: app_token - name: Mint release app token - uses: actions/create-github-app-token@v2 - with: - app-id: ${{ secrets.RELEASE_APP_ID }} - private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} - owner: ${{ github.repository_owner }} - - - name: Checkout - uses: actions/checkout@v6 - with: - ref: main - fetch-depth: 0 - token: ${{ steps.app_token.outputs.token }} - persist-credentials: true - - - id: app_bot - name: Resolve GitHub App bot identity - env: - GH_TOKEN: ${{ steps.app_token.outputs.token }} - APP_SLUG: ${{ steps.app_token.outputs.app-slug }} - run: | - user_id="$(gh api "/users/${APP_SLUG}[bot]" --jq .id)" - echo "name=${APP_SLUG}[bot]" >> "$GITHUB_OUTPUT" - echo "email=${user_id}+${APP_SLUG}[bot]@users.noreply.github.com" >> "$GITHUB_OUTPUT" - - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version-file: package.json - - - name: Setup Node - uses: actions/setup-node@v6 - with: - node-version-file: package.json - - - name: Install dependencies - run: bun install --frozen-lockfile --filter=@t3tools/scripts - - - id: update_versions - name: Update version strings - env: - RELEASE_VERSION: ${{ needs.preflight.outputs.version }} - run: node scripts/update-release-package-versions.ts "$RELEASE_VERSION" --github-output - - - name: Format package.json files - if: steps.update_versions.outputs.changed == 'true' - run: bunx oxfmt@0.40.0 apps/server/package.json apps/desktop/package.json apps/web/package.json packages/contracts/package.json - - - name: Refresh lockfile - if: steps.update_versions.outputs.changed == 'true' - run: bun install --lockfile-only --ignore-scripts - - - name: Commit and push version bump - if: steps.update_versions.outputs.changed == 'true' - shell: bash - env: - RELEASE_TAG: ${{ needs.preflight.outputs.tag }} - run: | - if git diff --quiet -- apps/server/package.json apps/desktop/package.json apps/web/package.json packages/contracts/package.json bun.lock; then - echo "No version changes to commit." - exit 0 - fi - - git config user.name "${{ steps.app_bot.outputs.name }}" - git config user.email "${{ steps.app_bot.outputs.email }}" - - git add apps/server/package.json apps/desktop/package.json apps/web/package.json packages/contracts/package.json bun.lock - git commit -m "chore(release): prepare $RELEASE_TAG" - git push origin HEAD:main - - announce_discord: - name: Announce release on Discord - if: | - always() && !cancelled() && - needs.preflight.result == 'success' && - needs.release.result == 'success' && - needs.deploy_web.result == 'success' && - (needs.finalize.result == 'success' || needs.finalize.result == 'skipped') - needs: [preflight, release, deploy_web, finalize] - runs-on: blacksmith-8vcpu-ubuntu-2404 - timeout-minutes: 10 - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - ref: ${{ needs.preflight.outputs.ref }} - - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version-file: package.json - - - name: Setup Node - uses: actions/setup-node@v6 - with: - node-version-file: package.json - - - name: Install dependencies - run: bun install --frozen-lockfile --filter=@t3tools/scripts - - - name: Announce prerelease on Discord - if: needs.preflight.outputs.is_prerelease == 'true' - continue-on-error: true - env: - DISCORD_MENTION_ROLE_ID: ${{ secrets.DISCORD_RELEASE_NIGHTLY_ROLE_ID }} - DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_RELEASE_WEBHOOK_URL }} - run: | - node scripts/notify-discord-release.ts prerelease \ - --role-id "$DISCORD_MENTION_ROLE_ID" \ - --release-name "${{ needs.preflight.outputs.release_name }}" \ - --release-version "${{ needs.preflight.outputs.version }}" \ - --tag "${{ needs.preflight.outputs.tag }}" \ - --release-url "https://github.com/${{ github.repository }}/releases/tag/${{ needs.preflight.outputs.tag }}" - - - name: Announce latest release on Discord - if: needs.preflight.outputs.make_latest == 'true' - continue-on-error: true - env: - DISCORD_MENTION_ROLE_ID: ${{ secrets.DISCORD_RELEASE_LATEST_ROLE_ID }} - DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_RELEASE_WEBHOOK_URL }} - run: | - node scripts/notify-discord-release.ts latest \ - --role-id "$DISCORD_MENTION_ROLE_ID" \ - --release-name "${{ needs.preflight.outputs.release_name }}" \ - --release-version "${{ needs.preflight.outputs.version }}" \ - --tag "${{ needs.preflight.outputs.tag }}" \ - --release-url "https://github.com/${{ github.repository }}/releases/tag/${{ needs.preflight.outputs.tag }}" + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/apps/desktop/package.json b/apps/desktop/package.json index ead57b5cbf2..cc751ab54a8 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -32,5 +32,5 @@ "typescript": "catalog:", "vitest": "catalog:" }, - "productName": "T3 Code (Alpha)" + "productName": "T3 Code (Bscholar)" } diff --git a/apps/desktop/scripts/electron-launcher.mjs b/apps/desktop/scripts/electron-launcher.mjs index 1453cbe666e..3c8ebe4a7f7 100644 --- a/apps/desktop/scripts/electron-launcher.mjs +++ b/apps/desktop/scripts/electron-launcher.mjs @@ -17,7 +17,7 @@ import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL); -const APP_DISPLAY_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)"; +const APP_DISPLAY_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Bscholar)"; const APP_BUNDLE_ID = isDevelopment ? "com.t3tools.t3code.dev" : "com.t3tools.t3code"; const LAUNCHER_VERSION = 2; diff --git a/apps/desktop/src/app/DesktopAppIdentity.test.ts b/apps/desktop/src/app/DesktopAppIdentity.test.ts index f95fd1bef71..4f166f4db20 100644 --- a/apps/desktop/src/app/DesktopAppIdentity.test.ts +++ b/apps/desktop/src/app/DesktopAppIdentity.test.ts @@ -118,7 +118,9 @@ const withIdentity = ( Layer.provideMerge( FileSystem.layerNoop({ exists: (path) => - Effect.succeed(input.legacyPathExists === true && path.includes("T3 Code (Alpha)")), + Effect.succeed( + input.legacyPathExists === true && path.includes("T3 Code (Bscholar)"), + ), readFileString: () => Effect.succeed(input.packageJson ?? '{"t3codeCommitHash":"abcdef1234567890"}'), }), @@ -138,7 +140,7 @@ describe("DesktopAppIdentity", () => { const identity = yield* DesktopAppIdentity.DesktopAppIdentity; const userDataPath = yield* identity.resolveUserDataPath; - assert.equal(userDataPath, "/Users/alice/Library/Application Support/T3 Code (Alpha)"); + assert.equal(userDataPath, "/Users/alice/Library/Application Support/T3 Code (Bscholar)"); }), { legacyPathExists: true }, ), @@ -156,8 +158,8 @@ describe("DesktopAppIdentity", () => { const identity = yield* DesktopAppIdentity.DesktopAppIdentity; yield* identity.configure; - assert.deepEqual(calls.setName, ["T3 Code (Alpha)"]); - assert.equal(calls.setAboutPanelOptions[0]?.applicationName, "T3 Code (Alpha)"); + assert.deepEqual(calls.setName, ["T3 Code (Bscholar)"]); + assert.equal(calls.setAboutPanelOptions[0]?.applicationName, "T3 Code (Bscholar)"); assert.equal(calls.setAboutPanelOptions[0]?.applicationVersion, "1.2.3"); assert.equal(calls.setAboutPanelOptions[0]?.version, "0123456789ab"); assert.deepEqual(calls.setDockIcon, ["/icon.png"]); diff --git a/apps/desktop/src/app/DesktopEnvironment.ts b/apps/desktop/src/app/DesktopEnvironment.ts index a5212f25358..5568ff5a9b2 100644 --- a/apps/desktop/src/app/DesktopEnvironment.ts +++ b/apps/desktop/src/app/DesktopEnvironment.ts @@ -90,7 +90,7 @@ function resolveDesktopAppStageLabel(input: { return "Dev"; } - return isNightlyDesktopVersion(input.appVersion) ? "Nightly" : "Alpha"; + return isNightlyDesktopVersion(input.appVersion) ? "Nightly" : "Bscholar"; } function resolveDesktopAppBranding(input: { @@ -161,7 +161,7 @@ const makeDesktopEnvironment = Effect.fn("desktop.environment.make")(function* ( const displayName = branding.displayName; const stateDir = path.join(baseDir, isDevelopment ? "dev" : "userdata"); const userDataDirName = isDevelopment ? "t3code-dev" : "t3code"; - const legacyUserDataDirName = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)"; + const legacyUserDataDirName = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Bscholar)"; const resourcesPath = input.resourcesPath; return DesktopEnvironment.of({ diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 0bc1badff2d..edac225c6eb 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -9,7 +9,7 @@ import * as Option from "effect/Option"; import * as Electron from "electron"; import * as NetService from "@t3tools/shared/Net"; -import { resolveRemoteT3CliPackageSpec } from "@t3tools/ssh/command"; +import { parseReleaseRepository, resolveRemoteT3CliPackageSpec } from "@t3tools/ssh/command"; import type { RemoteT3RunnerOptions } from "@t3tools/ssh/tunnel"; import serverPackageJson from "../../server/package.json" with { type: "json" }; @@ -71,11 +71,15 @@ const resolveDesktopSshCliRunner = ( nodeEngineRange: serverPackageJson.engines.node, }; } + // Public-fork friendly: prefer the GitHub Release tarball URL so remote + // hosts don't need any registry auth (.npmrc tokens) to `npx --yes` t3. + const releaseRepository = parseReleaseRepository(serverPackageJson.repository.url); return { packageSpec: resolveRemoteT3CliPackageSpec({ appVersion: environment.appVersion, updateChannel: settings.updateChannel, isDevelopment: environment.isDevelopment, + ...(releaseRepository ? { releaseRepository } : {}), }), nodeEngineRange: serverPackageJson.engines.node, }; diff --git a/apps/server/package.json b/apps/server/package.json index 387c880a9ab..3e71579eb7a 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -4,7 +4,7 @@ "license": "MIT", "repository": { "type": "git", - "url": "https://github.com/pingdotgg/t3code", + "url": "https://github.com/bscholar-tt/t3code", "directory": "apps/server" }, "bin": { @@ -24,6 +24,7 @@ }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.111", + "@earendil-works/pi-coding-agent": "^0.75.4", "@effect/platform-bun": "catalog:", "@effect/platform-node": "catalog:", "@effect/platform-node-shared": "catalog:", diff --git a/apps/server/scripts/cli.ts b/apps/server/scripts/cli.ts index c7f40de42d7..64379d38098 100644 --- a/apps/server/scripts/cli.ts +++ b/apps/server/scripts/cli.ts @@ -277,13 +277,123 @@ const publishCmd = Command.make( }), ).pipe(Command.withDescription("Publish the server package to npm.")); +// --------------------------------------------------------------------------- +// pack subcommand +// --------------------------------------------------------------------------- + +const packCmd = Command.make( + "pack", + { + appVersion: Flag.string("app-version").pipe(Flag.optional), + packDestination: Flag.string("pack-destination").pipe(Flag.optional), + verbose: Flag.boolean("verbose").pipe(Flag.withDefault(false)), + }, + (config) => + Effect.gen(function* () { + const path = yield* Path.Path; + const fs = yield* FileSystem.FileSystem; + const repoRoot = yield* RepoRoot; + const serverDir = path.join(repoRoot, "apps/server"); + const packageJsonPath = path.join(serverDir, "package.json"); + const backupPath = `${packageJsonPath}.bak`; + + for (const relPath of ["dist/bin.mjs", "dist/client/index.html"]) { + const abs = path.join(serverDir, relPath); + if (!(yield* fs.exists(abs))) { + return yield* new CliError({ + message: `Missing build asset: ${abs}. Run the build subcommand first.`, + }); + } + } + + const version = Option.getOrElse(config.appVersion, () => serverPackageJson.version); + const packDestination = path.resolve( + Option.getOrElse(config.packDestination, () => serverDir), + ); + yield* fs + .makeDirectory(packDestination, { recursive: true }) + .pipe(Effect.catch(() => Effect.void)); + + yield* Effect.acquireUseRelease( + Effect.gen(function* () { + const pkg: PackageJson = { + name: serverPackageJson.name, + repository: serverPackageJson.repository, + bin: serverPackageJson.bin, + type: serverPackageJson.type, + version, + engines: serverPackageJson.engines, + files: serverPackageJson.files, + dependencies: resolveCatalogDependencies( + serverPackageJson.dependencies, + rootPackageJson.workspaces.catalog, + "apps/server", + ), + overrides: resolveCatalogDependencies( + rootPackageJson.overrides, + rootPackageJson.workspaces.catalog, + "apps/server", + ), + }; + + const original = yield* fs.readFileString(packageJsonPath); + const packageJsonString = yield* encodePackageJson(pkg); + yield* fs.writeFileString(backupPath, original); + yield* fs.writeFileString(packageJsonPath, `${packageJsonString}\n`); + yield* Effect.log("[cli] Prepared package.json for pack"); + + const iconBackups = yield* applyPublishIconOverrides(repoRoot, serverDir); + return { iconBackups }; + }), + () => + Effect.gen(function* () { + const args = ["pack", "--pack-destination", packDestination]; + yield* Effect.log(`[cli] Running: npm ${args.join(" ")}`); + yield* runCommand( + ChildProcess.make("npm", args, { + cwd: serverDir, + stdout: config.verbose ? "inherit" : "ignore", + stderr: "inherit", + shell: process.platform === "win32", + }), + ); + + const tarballName = `${serverPackageJson.name}-${version}.tgz`; + const tarballPath = path.join(packDestination, tarballName); + if (!(yield* fs.exists(tarballPath))) { + return yield* new CliError({ + message: `Expected packed tarball was not produced at ${tarballPath}`, + }); + } + yield* Effect.log(`[cli] Packed tarball: ${tarballPath}`); + + // Also emit a rolling alias (t3-latest.tgz) so GitHub's + // /releases/latest/download/ URL stays stable. + const latestAlias = path.join(packDestination, `${serverPackageJson.name}-latest.tgz`); + yield* fs.copyFile(tarballPath, latestAlias); + yield* Effect.log(`[cli] Wrote rolling alias: ${latestAlias}`); + }), + (resource: { readonly iconBackups: ReadonlyArray }) => + Effect.gen(function* () { + yield* restorePublishIconOverrides(resource.iconBackups).pipe( + Effect.catch((error) => + Effect.logError(`[cli] Failed to restore publish icon overrides: ${String(error)}`), + ), + ); + yield* fs.rename(backupPath, packageJsonPath); + if (config.verbose) yield* Effect.log("[cli] Restored original package.json"); + }), + ); + }), +).pipe(Command.withDescription("Pack the server package into a .tgz tarball (no publish).")); + // --------------------------------------------------------------------------- // root command // --------------------------------------------------------------------------- const cli = Command.make("cli").pipe( Command.withDescription("T3 server build & publish CLI."), - Command.withSubcommands([buildCmd, publishCmd]), + Command.withSubcommands([buildCmd, publishCmd, packCmd]), ); Command.run(cli, { version: "0.0.0" }).pipe( diff --git a/apps/server/src/bin.test.ts b/apps/server/src/bin.test.ts index a7bf2686101..f031ad694bf 100644 --- a/apps/server/src/bin.test.ts +++ b/apps/server/src/bin.test.ts @@ -1,17 +1,14 @@ -// @effect-diagnostics-next-line nodeBuiltinImport:off - NodeHttpServer.layer takes `NodeHttp.createServer` as arg -import * as NodeHttp from "node:http"; +// @effect-diagnostics-next-line nodeBuiltinImport:off import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; +// @effect-diagnostics-next-line nodeBuiltinImport:off import { join } from "node:path"; -import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"; import * as NodeServices from "@effect/platform-node/NodeServices"; import * as NetService from "@t3tools/shared/Net"; import { assert, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; -import * as HttpRouter from "effect/unstable/http/HttpRouter"; -import * as HttpServer from "effect/unstable/http/HttpServer"; import * as CliError from "effect/unstable/cli/CliError"; import * as TestConsole from "effect/testing/TestConsole"; import { Command } from "effect/unstable/cli"; @@ -20,19 +17,9 @@ import { cli } from "./bin.ts"; import { deriveServerPaths, ServerConfig, type ServerConfigShape } from "./config.ts"; import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery.ts"; import { OrchestrationLayerLive } from "./orchestration/runtimeLayer.ts"; -import { - orchestrationDispatchRouteLayer, - orchestrationSnapshotRouteLayer, -} from "./orchestration/http.ts"; import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite.ts"; import { RepositoryIdentityResolverLive } from "./project/Layers/RepositoryIdentityResolver.ts"; -import { - makePersistedServerRuntimeState, - persistServerRuntimeState, -} from "./serverRuntimeState.ts"; import { WorkspacePathsLive } from "./workspace/Layers/WorkspacePaths.ts"; -import { ServerSecretStoreLive } from "./auth/Layers/ServerSecretStore.ts"; -import { ServerAuthLive } from "./auth/Layers/ServerAuth.ts"; const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, NetService.layer); @@ -102,53 +89,6 @@ const readPersistedSnapshot = (baseDir: string) => }).pipe(Effect.provide(makeProjectPersistenceLayer(config))); }); -const withLiveProjectCliServer = (baseDir: string, run: () => Effect.Effect) => - Effect.gen(function* () { - const config = yield* makeCliTestServerConfig(baseDir); - const routesLayer = Layer.mergeAll( - orchestrationSnapshotRouteLayer, - orchestrationDispatchRouteLayer, - ); - const appLayer = HttpRouter.serve(routesLayer, { - disableListenLog: true, - disableLogger: true, - }).pipe( - Layer.provideMerge( - ServerAuthLive.pipe( - Layer.provideMerge(SqlitePersistenceLayerLive), - Layer.provide(ServerSecretStoreLive), - ), - ), - Layer.provideMerge(makeProjectPersistenceLayer(config)), - Layer.provideMerge( - NodeHttpServer.layer(NodeHttp.createServer, { - host: "127.0.0.1", - port: 0, - }), - ), - Layer.provideMerge(NodeServices.layer), - Layer.provide(Layer.succeed(ServerConfig, config)), - ); - - return yield* Effect.scoped( - Effect.gen(function* () { - const server = yield* HttpServer.HttpServer; - const address = server.address; - if (typeof address === "string" || !("port" in address)) { - assert.fail(`Expected TCP address, got ${address}`); - } - yield* persistServerRuntimeState({ - path: config.serverRuntimeStatePath, - state: yield* makePersistedServerRuntimeState({ - config, - port: address.port, - }), - }); - return yield* run(); - }).pipe(Effect.provide(Layer.mergeAll(appLayer, NodeServices.layer))), - ); - }); - it.layer(NodeServices.layer)("bin cli parsing", (it) => { it.effect("accepts the built-in lowercase log-level flag values", () => runCliWithRuntime(["--log-level", "debug", "--version"]), @@ -304,34 +244,6 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => { }), ); - it.effect("routes project commands through a running server when runtime state is present", () => - Effect.gen(function* () { - const baseDir = mkdtempSync(join(tmpdir(), "t3-cli-projects-live-test-")); - const workspaceRoot = mkdtempSync(join(tmpdir(), "t3-cli-projects-live-workspace-")); - - yield* withLiveProjectCliServer(baseDir, () => - Effect.gen(function* () { - yield* runCliWithRuntime([ - "project", - "add", - workspaceRoot, - "--title", - "Live Project", - "--base-dir", - baseDir, - ]); - const projectionSnapshotQuery = yield* ProjectionSnapshotQuery; - const readModel = yield* projectionSnapshotQuery.getSnapshot(); - const addedProject = readModel.projects.find( - (project) => project.workspaceRoot === workspaceRoot && project.deletedAt === null, - ); - assert.isTrue(addedProject !== undefined); - assert.equal(addedProject?.title, "Live Project"); - }), - ); - }), - ); - it.effect("rejects dev-url on project commands", () => Effect.gen(function* () { const workspaceRoot = mkdtempSync( diff --git a/apps/server/src/provider/Drivers/PiDriver.ts b/apps/server/src/provider/Drivers/PiDriver.ts new file mode 100644 index 00000000000..89e25d5c9a0 --- /dev/null +++ b/apps/server/src/provider/Drivers/PiDriver.ts @@ -0,0 +1,157 @@ +import { PiSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts"; +import * as Duration from "effect/Duration"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Path from "effect/Path"; +import * as Schema from "effect/Schema"; +import * as Stream from "effect/Stream"; +import { HttpClient } from "effect/unstable/http"; +import { ChildProcessSpawner } from "effect/unstable/process"; + +import { makePiTextGeneration } from "../../textGeneration/PiTextGeneration.ts"; +import { ServerConfig } from "../../config.ts"; +import { ProviderDriverError } from "../Errors.ts"; +import { makePiAdapter } from "../Layers/PiAdapter.ts"; +import { checkPiProviderStatus, makePendingPiProvider } from "../Layers/PiProvider.ts"; +import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts"; +import { makeManagedServerProvider } from "../makeManagedServerProvider.ts"; +import { + defaultProviderContinuationIdentity, + type ProviderDriver, + type ProviderInstance, +} from "../ProviderDriver.ts"; +import type { ServerProviderDraft } from "../providerSnapshot.ts"; +import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts"; +import { + enrichProviderSnapshotWithVersionAdvisory, + makePackageManagedProviderMaintenanceResolver, + resolveProviderMaintenanceCapabilitiesEffect, +} from "../providerMaintenance.ts"; +import { makePiContinuationGroupKey } from "./PiHome.ts"; + +const decodePiSettings = Schema.decodeSync(PiSettings); + +const DRIVER_KIND = ProviderDriverKind.make("piAgent"); +const SNAPSHOT_REFRESH_INTERVAL = Duration.minutes(5); + +const UPDATE = makePackageManagedProviderMaintenanceResolver({ + provider: DRIVER_KIND, + npmPackageName: "@earendil-works/pi-coding-agent", + homebrewFormula: null, + nativeUpdate: null, +}); + +export type PiDriverEnv = + | ChildProcessSpawner.ChildProcessSpawner + | FileSystem.FileSystem + | HttpClient.HttpClient + | Path.Path + | ProviderEventLoggers + | ServerConfig; + +const withInstanceIdentity = + (input: { + readonly instanceId: ProviderInstance["instanceId"]; + readonly displayName: string | undefined; + readonly accentColor: string | undefined; + readonly continuationGroupKey: string; + }) => + (snapshot: ServerProviderDraft): ServerProvider => ({ + ...snapshot, + instanceId: input.instanceId, + driver: DRIVER_KIND, + ...(input.displayName ? { displayName: input.displayName } : {}), + ...(input.accentColor ? { accentColor: input.accentColor } : {}), + continuation: { groupKey: input.continuationGroupKey }, + }); + +export const PiDriver: ProviderDriver = { + driverKind: DRIVER_KIND, + metadata: { + displayName: "Pi Agent", + supportsMultipleInstances: true, + }, + configSchema: PiSettings, + defaultConfig: (): PiSettings => decodePiSettings({}), + create: ({ instanceId, displayName, accentColor, environment, enabled, config }) => + Effect.gen(function* () { + const httpClient = yield* HttpClient.HttpClient; + const eventLoggers = yield* ProviderEventLoggers; + const processEnv = mergeProviderInstanceEnvironment(environment); + const continuationIdentity = defaultProviderContinuationIdentity({ + driverKind: DRIVER_KIND, + instanceId, + }); + const effectiveConfig = { ...config, enabled } satisfies PiSettings; + const maintenanceCapabilities = yield* resolveProviderMaintenanceCapabilitiesEffect(UPDATE, { + binaryPath: effectiveConfig.binaryPath, + env: processEnv, + }); + const continuationGroupKey = yield* makePiContinuationGroupKey(effectiveConfig); + const stampIdentity = withInstanceIdentity({ + instanceId, + displayName, + accentColor, + continuationGroupKey, + }); + + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const path = yield* Path.Path; + const fs = yield* FileSystem.FileSystem; + const serverConfig = yield* ServerConfig; + + const adapter = yield* makePiAdapter(effectiveConfig, { + instanceId, + environment: processEnv, + }); + const textGeneration = yield* makePiTextGeneration(effectiveConfig, processEnv); + + const checkProvider = checkPiProviderStatus(effectiveConfig, serverConfig.cwd, processEnv).pipe( + Effect.map(stampIdentity), + Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), + Effect.provideService(FileSystem.FileSystem, fs), + Effect.provideService(Path.Path, path), + ); + + const snapshot = yield* makeManagedServerProvider({ + maintenanceCapabilities, + getSettings: Effect.succeed(effectiveConfig), + streamSettings: Stream.never, + haveSettingsChanged: () => false, + initialSnapshot: (settings) => + makePendingPiProvider(settings).pipe(Effect.map(stampIdentity)), + checkProvider, + enrichSnapshot: ({ snapshot, publishSnapshot }) => + enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities).pipe( + Effect.provideService(HttpClient.HttpClient, httpClient), + Effect.flatMap((enrichedSnapshot) => publishSnapshot(enrichedSnapshot)), + ), + refreshInterval: SNAPSHOT_REFRESH_INTERVAL, + }).pipe( + Effect.mapError( + (cause) => + new ProviderDriverError({ + driver: DRIVER_KIND, + instanceId, + detail: `Failed to build Pi Agent snapshot: ${cause.message ?? String(cause)}`, + cause, + }), + ), + ); + + return { + instanceId, + driverKind: DRIVER_KIND, + continuationIdentity: { + ...continuationIdentity, + continuationKey: continuationGroupKey, + }, + displayName, + accentColor, + enabled, + snapshot, + adapter, + textGeneration, + } satisfies ProviderInstance; + }), +}; diff --git a/apps/server/src/provider/Drivers/PiHome.ts b/apps/server/src/provider/Drivers/PiHome.ts new file mode 100644 index 00000000000..ab7621d3d07 --- /dev/null +++ b/apps/server/src/provider/Drivers/PiHome.ts @@ -0,0 +1,42 @@ +import * as NodeOS from "node:os"; + +import type { PiSettings } from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as Path from "effect/Path"; + +import { expandHomePath } from "../../pathExpansion.ts"; + +export const resolvePiHomePath = Effect.fn("resolvePiHomePath")(function* ( + config: Pick, +): Effect.fn.Return { + const path = yield* Path.Path; + const homePath = config.homePath.trim(); + return path.resolve(homePath.length > 0 ? expandHomePath(homePath) : NodeOS.homedir()); +}); + +export const makePiEnvironment = Effect.fn("makePiEnvironment")(function* ( + config: Pick, + baseEnv: NodeJS.ProcessEnv = process.env, +): Effect.fn.Return { + const homePath = config.homePath.trim(); + if (homePath.length === 0) return baseEnv; + const resolvedHomePath = yield* resolvePiHomePath(config); + return { + ...baseEnv, + HOME: resolvedHomePath, + }; +}); + +export const makePiContinuationGroupKey = Effect.fn("makePiContinuationGroupKey")(function* ( + config: Pick, +): Effect.fn.Return { + const resolvedHomePath = yield* resolvePiHomePath(config); + return `pi:home:${resolvedHomePath}`; +}); + +export const makePiCapabilitiesCacheKey = Effect.fn("makePiCapabilitiesCacheKey")(function* ( + config: Pick, +): Effect.fn.Return { + const resolvedHomePath = yield* resolvePiHomePath(config); + return `${config.binaryPath}\0${resolvedHomePath}`; +}); diff --git a/apps/server/src/provider/Layers/PiAdapter.ts b/apps/server/src/provider/Layers/PiAdapter.ts new file mode 100644 index 00000000000..4230f5ae9c6 --- /dev/null +++ b/apps/server/src/provider/Layers/PiAdapter.ts @@ -0,0 +1,1077 @@ +import { type AgentSessionEvent, type RpcCommand } from "@earendil-works/pi-coding-agent"; +import { + ApprovalRequestId, + type CanonicalItemType, + EventId, + type PiSettings, + type ProviderApprovalDecision, + ProviderDriverKind, + ProviderInstanceId, + type ProviderRuntimeEvent, + type ProviderRuntimeTurnStatus, + type ProviderSendTurnInput, + type ProviderSession, + type ProviderUserInputAnswers, + RuntimeItemId, + RuntimeRequestId, + ThreadId, + TurnId, + type UserInputQuestion, +} from "@t3tools/contracts"; +import * as DateTime from "effect/DateTime"; +import * as Deferred from "effect/Deferred"; +import * as Effect from "effect/Effect"; +import * as Exit from "effect/Exit"; +import * as Fiber from "effect/Fiber"; +import * as Queue from "effect/Queue"; +import * as Random from "effect/Random"; +import * as Scope from "effect/Scope"; +import * as Stream from "effect/Stream"; +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; + +import { ServerConfig } from "../../config.ts"; +import { makePiEnvironment } from "../Drivers/PiHome.ts"; +import { + ProviderAdapterProcessError, + ProviderAdapterSessionClosedError, + ProviderAdapterSessionNotFoundError, + ProviderAdapterValidationError, + type ProviderAdapterError, +} from "../Errors.ts"; +import type { PiAdapterShape } from "../Services/PiAdapter.ts"; + +// RpcExtensionUIRequest / RpcExtensionUIResponse are defined in the pi package's +// internal rpc-types module but not re-exported from the public index, so we +// mirror the relevant subset here. +type RpcExtensionUIRequest = + | { type: "extension_ui_request"; id: string; method: "select"; title: string; options: string[]; timeout?: number } + | { type: "extension_ui_request"; id: string; method: "confirm"; title: string; message: string; timeout?: number } + | { type: "extension_ui_request"; id: string; method: "input"; title: string; placeholder?: string; timeout?: number } + | { type: "extension_ui_request"; id: string; method: "editor"; title: string; prefill?: string } + | { type: "extension_ui_request"; id: string; method: "notify"; message: string; notifyType?: "info" | "warning" | "error" } + | { type: "extension_ui_request"; id: string; method: "setStatus"; statusKey: string; statusText: string | undefined } + | { type: "extension_ui_request"; id: string; method: "setWidget"; widgetKey: string; widgetLines: string[] | undefined; widgetPlacement?: "aboveEditor" | "belowEditor" } + | { type: "extension_ui_request"; id: string; method: "setTitle"; title: string } + | { type: "extension_ui_request"; id: string; method: "set_editor_text"; text: string }; + +type RpcExtensionUIResponse = + | { type: "extension_ui_response"; id: string; value: string } + | { type: "extension_ui_response"; id: string; confirmed: boolean } + | { type: "extension_ui_response"; id: string; cancelled: true }; + +const PROVIDER = ProviderDriverKind.make("piAgent"); + +interface PiToolItem { + readonly id: RuntimeItemId; + readonly type: CanonicalItemType; + readonly toolName: string; + readonly args: unknown; +} + +interface PiTurnState { + readonly turnId: TurnId; + readonly startedAt: string; + readonly items: Array; +} + +interface PendingExtensionUI { + readonly piId: string; + readonly questionId: string; + readonly deferred: Deferred.Deferred; + readonly method: "select" | "confirm" | "input" | "editor"; + // Populated for `input` requests that were parsed as numbered lists. + // Used to map selected labels back to 1-based indices for Pi's multi-select protocol. + readonly numberedOptions?: ReadonlyArray; +} + +interface PiSessionContext { + session: ProviderSession; + readonly sessionScope: Scope.Closeable; + writeCommand: (cmd: RpcCommand) => Effect.Effect; + writeExtensionResponse: (resp: RpcExtensionUIResponse) => Effect.Effect; + stdoutFiber: Fiber.Fiber | undefined; + readonly pendingRequests: Map>; + readonly pendingExtensionUIs: Map; + readonly startedAt: string; + turnState: PiTurnState | undefined; + readonly turns: Array<{ id: TurnId; items: Array }>; + stopped: boolean; +} + +function toMessage(cause: unknown, fallback: string): string { + if (cause instanceof Error && cause.message.length > 0) { + return cause.message; + } + return fallback; +} + +function readPiResumeState(resumeCursor: unknown): { sessionFile: string } | undefined { + if (!resumeCursor || typeof resumeCursor !== "object") return undefined; + const cursor = resumeCursor as Record; + return typeof cursor.sessionFile === "string" && cursor.sessionFile.trim().length > 0 + ? { sessionFile: cursor.sessionFile } + : undefined; +} + +function classifyToolItemType(toolName: string): CanonicalItemType { + const normalized = toolName.toLowerCase(); + if ( + normalized.includes("agent") || + normalized.includes("subagent") || + normalized.includes("sub-agent") || + normalized === "task" || + normalized === "skill" + ) { + return "collab_agent_tool_call"; + } + if ( + normalized.includes("bash") || + normalized.includes("shell") || + normalized.includes("command") || + normalized.includes("terminal") || + normalized.includes("exec") + ) { + return "command_execution"; + } + if ( + normalized.includes("edit") || + normalized.includes("write") || + normalized.includes("file") || + normalized.includes("patch") || + normalized.includes("apply") || + normalized.includes("replace") || + normalized.includes("create") || + normalized.includes("delete") + ) { + return "file_change"; + } + if (normalized.includes("mcp")) { + return "mcp_tool_call"; + } + if (normalized.includes("websearch") || normalized.includes("web search")) { + return "web_search"; + } + if (normalized.includes("image")) { + return "image_view"; + } + return "dynamic_tool_call"; +} + +// Pi's RPC multi-select fallback encodes options as a numbered list in the title: +// "Which colors?\n1. Red\n2. Blue\n3. Green" +// Returns the question title and extracted option strings, or null if no list found. +function parseNumberedList(text: string): { title: string; items: ReadonlyArray } | null { + const lines = text.split("\n"); + const items: string[] = []; + for (const line of lines.slice(1)) { + const m = /^\d+\.\s+(.+)$/.exec(line.trim()); + if (m?.[1]) items.push(m[1]); + } + return items.length >= 2 ? { title: lines[0] ?? text, items } : null; +} + +function tryParseJsonObject(text: string): Record | null { + try { + // eslint-disable-next-line no-restricted-syntax -- non-Effect context, no schema needed + const v = JSON.parse(text) as unknown; // @effect-diagnostics-ignore preferSchemaOverJson + return v !== null && typeof v === "object" ? (v as Record) : null; + } catch { + return null; + } +} + +function summarizePiToolArgs(args: unknown): string | undefined { + if (!args || typeof args !== "object") return undefined; + const input = args as Record; + + const commandValue = input.command ?? input.cmd; + if (typeof commandValue === "string" && commandValue.trim().length > 0) { + return commandValue.trim().slice(0, 400); + } + + const skillValue = input.skill ?? input.skillName; + if (typeof skillValue === "string" && skillValue.trim().length > 0) { + const skillArgs = typeof input.args === "string" ? input.args.trim() : undefined; + return skillArgs ? `${skillValue.trim()} ${skillArgs}`.slice(0, 400) : skillValue.trim(); + } + + const descValue = input.description ?? input.prompt; + if (typeof descValue === "string" && descValue.trim().length > 0) { + return descValue.trim().slice(0, 400); + } + + const pathValue = input.file_path ?? input.path ?? input.filePath; + if (typeof pathValue === "string" && pathValue.trim().length > 0) { + return pathValue.trim().slice(0, 400); + } + + const patternValue = input.pattern ?? input.query; + if (typeof patternValue === "string" && patternValue.trim().length > 0) { + return patternValue.trim().slice(0, 400); + } + + try { + const serialized = JSON.stringify(input); + if (serialized.length <= 400) return serialized; + return `${serialized.slice(0, 397)}...`; + } catch { + return undefined; + } +} + +export interface PiAdapterLiveOptions { + readonly instanceId?: ProviderInstanceId; + readonly environment?: NodeJS.ProcessEnv; +} + +export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( + piSettings: PiSettings, + options?: PiAdapterLiveOptions, +) { + const boundInstanceId = options?.instanceId ?? ProviderInstanceId.make("piAgent"); + const serverConfig = yield* ServerConfig; + const piEnvironment = yield* makePiEnvironment(piSettings, options?.environment); + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + + const sessions = new Map(); + const runtimeEventQueue = yield* Queue.unbounded(); + + const nowIso = Effect.map(DateTime.now, DateTime.formatIso); + const nextEventId = Effect.map(Random.nextUUIDv4, (id) => EventId.make(id)); + const makeEventStamp = () => Effect.all({ eventId: nextEventId, createdAt: nowIso }); + + const offerRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect => + Queue.offer(runtimeEventQueue, event).pipe(Effect.asVoid); + + const completeTurn = Effect.fn("completeTurn")(function* ( + context: PiSessionContext, + state: ProviderRuntimeTurnStatus, + message?: string, + ) { + const turnState = context.turnState; + if (!turnState) return; + + context.turnState = undefined; + context.turns.push({ + id: turnState.turnId, + items: [...turnState.items], + }); + + const updatedAt = yield* nowIso; + context.session = { + ...context.session, + status: "ready", + activeTurnId: undefined, + updatedAt, + }; + + const stamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "turn.completed", + eventId: stamp.eventId, + provider: PROVIDER, + createdAt: stamp.createdAt, + threadId: context.session.threadId, + turnId: turnState.turnId, + payload: { + state, + ...(message ? { message } : {}), + }, + providerRefs: {}, + }); + }); + + const handlePiEvent = Effect.fn("handlePiEvent")(function* ( + context: PiSessionContext, + event: AgentSessionEvent, + ) { + const stamp = yield* makeEventStamp(); + const base = { + eventId: stamp.eventId, + provider: PROVIDER, + createdAt: stamp.createdAt, + threadId: context.session.threadId, + ...(context.turnState ? { turnId: context.turnState.turnId } : {}), + providerRefs: {}, + raw: { + source: "pi.sdk.event" as const, + method: event.type, + payload: event, + }, + }; + + switch (event.type) { + case "agent_start": + yield* offerRuntimeEvent({ + ...base, + type: "session.state.changed", + payload: { state: "running" }, + }); + return; + + case "turn_start": { + if (!context.turnState) { + const turnId = TurnId.make(yield* Random.nextUUIDv4); + const startedAt = yield* nowIso; + context.turnState = { turnId, startedAt, items: [] }; + const updatedAt = yield* nowIso; + context.session = { + ...context.session, + status: "running", + activeTurnId: turnId, + updatedAt, + }; + yield* offerRuntimeEvent({ + ...base, + turnId, + type: "turn.started", + payload: {}, + }); + } + return; + } + + case "message_update": { + if (!context.turnState) return; + const assistantEvent = event.assistantMessageEvent; + if (!assistantEvent) return; + if (assistantEvent.type === "text_delta") { + yield* offerRuntimeEvent({ + ...base, + turnId: context.turnState.turnId, + type: "content.delta", + payload: { + streamKind: "assistant_text", + delta: assistantEvent.delta, + }, + }); + } else if (assistantEvent.type === "thinking_delta") { + yield* offerRuntimeEvent({ + ...base, + turnId: context.turnState.turnId, + type: "content.delta", + payload: { + streamKind: "reasoning_text", + delta: assistantEvent.delta, + }, + }); + } + return; + } + + case "tool_execution_start": { + if (!context.turnState) return; + const itemId = RuntimeItemId.make(event.toolCallId); + const itemType = classifyToolItemType(event.toolName); + const detail = summarizePiToolArgs(event.args); + const argsObj = + event.args && typeof event.args === "object" + ? (event.args as Record) + : undefined; + context.turnState.items.push({ + id: itemId, + type: itemType, + toolName: event.toolName, + args: event.args, + }); + yield* offerRuntimeEvent({ + ...base, + turnId: context.turnState.turnId, + itemId, + type: "item.started", + payload: { + itemType, + title: event.toolName, + ...(detail ? { detail } : {}), + ...(argsObj ? { data: { toolName: event.toolName, input: argsObj } } : {}), + }, + }); + return; + } + + case "tool_execution_update": { + if (!context.turnState) return; + const itemId = RuntimeItemId.make(event.toolCallId); + const itemType = classifyToolItemType(event.toolName); + if (event.partialResult !== undefined) { + const partial = + typeof event.partialResult === "string" + ? event.partialResult + : String(event.partialResult); + yield* offerRuntimeEvent({ + ...base, + turnId: context.turnState.turnId, + itemId, + type: "content.delta", + payload: { + streamKind: + itemType === "command_execution" ? "command_output" : "file_change_output", + delta: partial, + }, + }); + } + return; + } + + case "tool_execution_end": { + if (!context.turnState) return; + const itemId = RuntimeItemId.make(event.toolCallId); + const itemType = classifyToolItemType(event.toolName); + const storedItem = context.turnState.items.find((item) => item.id === itemId); + const args = storedItem?.args; + const detail = summarizePiToolArgs(args); + const argsObj = + args && typeof args === "object" ? (args as Record) : undefined; + yield* offerRuntimeEvent({ + ...base, + turnId: context.turnState.turnId, + itemId, + type: "item.completed", + payload: { + itemType, + title: event.toolName, + status: event.isError ? "failed" : "completed", + ...(detail ? { detail } : {}), + ...(argsObj ? { data: { toolName: event.toolName, input: argsObj } } : {}), + }, + }); + return; + } + + case "turn_end": { + // Pi fires turn_end after each internal LLM call, but agent_end fires + // after the full agent run. Completing here would fragment the Pi run + // into multiple t3code turns, causing tool activities to disappear and + // the timer to reset on every sub-turn. Let agent_end drive completion. + return; + } + + case "agent_end": { + if (context.turnState) { + const willRetry = "willRetry" in event ? Boolean(event.willRetry) : false; + yield* completeTurn(context, willRetry ? "interrupted" : "completed"); + } + return; + } + + case "compaction_start": { + yield* offerRuntimeEvent({ + ...base, + type: "session.state.changed", + payload: { + state: "waiting", + reason: `compaction:${"reason" in event ? String(event.reason) : "unknown"}`, + }, + }); + return; + } + + case "compaction_end": { + yield* offerRuntimeEvent({ + ...base, + type: "thread.state.changed", + payload: { state: "compacted" }, + }); + return; + } + + default: + return; + } + }); + + const handleExtensionUIRequest = Effect.fn("handleExtensionUIRequest")(function* ( + context: PiSessionContext, + event: RpcExtensionUIRequest, + ) { + // Fire-and-forget side-effects: Pi does not wait for a response. + if ( + event.method === "notify" || + event.method === "setStatus" || + event.method === "setWidget" || + event.method === "setTitle" || + event.method === "set_editor_text" + ) { + return; + } + + // editor: surface as freeform text input (same protocol shape as input). + // Falls through to the input branch below. + + // select / confirm / input: surface as user-input.requested + const ourRequestId = ApprovalRequestId.make(yield* Random.nextUUIDv4); + const questionId = String(ourRequestId); + const deferred = yield* Deferred.make(); + + let question: UserInputQuestion; + let numberedOptions: ReadonlyArray | undefined; + + if (event.method === "select") { + question = { + id: questionId, + header: event.title.slice(0, 12), + question: event.title, + options: event.options.map((label: string) => ({ label, description: label })), + multiSelect: false, + }; + } else if (event.method === "confirm") { + const body = event.message.length > 0 ? `${event.title}\n${event.message}` : event.title; + question = { + id: questionId, + header: "Confirm", + question: body, + options: [ + { label: "Yes", description: "Confirm" }, + { label: "No", description: "Cancel" }, + ], + multiSelect: false, + }; + } else { + // input / editor — both expect { value: "..." } back. + // input: Pi uses this for multi-select (numbered list in title) and freeform. + // If the title contains a numbered list, parse it into real options with multiSelect. + // Otherwise surface with empty options; T3's auto-added "Other" field handles free text. + // editor: multi-line freeform, no options — same freeform path. + const title = "title" in event ? event.title : ""; + const parsed = event.method === "input" ? parseNumberedList(title) : null; + if (parsed) { + numberedOptions = parsed.items; + question = { + id: questionId, + header: parsed.title.slice(0, 12), + question: parsed.title, + options: parsed.items.map((item) => ({ label: item, description: item })), + multiSelect: true, + }; + } else { + question = { + id: questionId, + header: title.slice(0, 12), + question: title, + options: [], + multiSelect: false, + }; + } + } + + context.pendingExtensionUIs.set(ourRequestId, { + piId: event.id, + questionId, + deferred, + method: event.method, + ...(numberedOptions ? { numberedOptions } : {}), + }); + + const stamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "user-input.requested", + eventId: stamp.eventId, + provider: PROVIDER, + createdAt: stamp.createdAt, + threadId: context.session.threadId, + ...(context.turnState ? { turnId: context.turnState.turnId } : {}), + requestId: RuntimeRequestId.make(ourRequestId), + payload: { questions: [question] }, + providerRefs: {}, + raw: { + source: "pi.sdk.event" as const, + method: "extension_ui_request", + payload: event, + }, + }); + }); + + const handleStdoutLine = Effect.fn("handleStdoutLine")(function* ( + context: PiSessionContext, + line: string, + ) { + const trimmed = line.trim(); + if (!trimmed) return; + + const msg = tryParseJsonObject(trimmed); + if (!msg) return; + + if (msg["type"] === "response") { + if (typeof msg["id"] === "string") { + const deferred = context.pendingRequests.get(msg["id"]); + if (deferred) { + context.pendingRequests.delete(msg["id"]); + yield* Deferred.succeed(deferred, msg); + } + } + return; + } + + if (msg["type"] === "extension_ui_request") { + yield* handleExtensionUIRequest(context, msg as RpcExtensionUIRequest); + return; + } + + yield* handlePiEvent(context, msg as AgentSessionEvent); + }); + + const stopSessionInternal = Effect.fn("stopSessionInternal")(function* ( + context: PiSessionContext, + options?: { readonly emitExitEvent?: boolean }, + ) { + if (context.stopped) return; + context.stopped = true; + + if (context.turnState) { + yield* completeTurn(context, "interrupted", "Session stopped."); + } + + // Cancel any pending extension UI requests so Pi doesn't hang + for (const [, pending] of context.pendingExtensionUIs) { + yield* Effect.ignore( + context.writeExtensionResponse({ type: "extension_ui_response", id: pending.piId, cancelled: true }), + ); + yield* Effect.ignore(Deferred.interrupt(pending.deferred)); + } + context.pendingExtensionUIs.clear(); + + if (context.stdoutFiber) { + yield* Fiber.interrupt(context.stdoutFiber); + context.stdoutFiber = undefined; + } + + yield* Effect.ignore(Scope.close(context.sessionScope, Exit.void)); + + const updatedAt = yield* nowIso; + context.session = { + ...context.session, + status: "closed", + activeTurnId: undefined, + updatedAt, + }; + + if (options?.emitExitEvent !== false) { + const stamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "session.exited", + eventId: stamp.eventId, + provider: PROVIDER, + createdAt: stamp.createdAt, + threadId: context.session.threadId, + payload: { + reason: "Session stopped", + exitKind: "graceful", + }, + providerRefs: {}, + }); + } + + sessions.delete(context.session.threadId); + }); + + const requireSession = ( + threadId: ThreadId, + ): Effect.Effect => { + const context = sessions.get(threadId); + if (!context) { + return Effect.fail( + new ProviderAdapterSessionNotFoundError({ + provider: PROVIDER, + threadId, + }), + ); + } + if (context.stopped || context.session.status === "closed") { + return Effect.fail( + new ProviderAdapterSessionClosedError({ + provider: PROVIDER, + threadId, + }), + ); + } + return Effect.succeed(context); + }; + + const startSession: PiAdapterShape["startSession"] = Effect.fn("startSession")(function* (input) { + if (input.provider !== undefined && input.provider !== PROVIDER) { + return yield* new ProviderAdapterValidationError({ + provider: PROVIDER, + operation: "startSession", + issue: `Expected provider '${PROVIDER}' but received '${input.provider}'.`, + }); + } + + const existingContext = sessions.get(input.threadId); + if (existingContext) { + yield* stopSessionInternal(existingContext, { emitExitEvent: false }).pipe( + Effect.catchCause((cause) => + Effect.logWarning("pi.session.replace.stop-failed", { + threadId: input.threadId, + cause, + }), + ), + ); + } + + const startedAt = yield* nowIso; + const threadId = input.threadId; + const modelSelection = + input.modelSelection !== undefined && input.modelSelection.instanceId === boundInstanceId + ? input.modelSelection + : undefined; + + const piResumeState = readPiResumeState(input.resumeCursor); + const baseCwd = input.cwd ?? serverConfig.cwd; + + const spawnArgs: string[] = ["--mode", "rpc"]; + if (piResumeState) { + spawnArgs.push("--session", piResumeState.sessionFile); + } + if (modelSelection?.model) { + spawnArgs.push("--model", modelSelection.model); + } + + const sessionScope = yield* Scope.make(); + + const child = yield* spawner + .spawn( + ChildProcess.make(piSettings.binaryPath || "pi", spawnArgs, { + env: piEnvironment, + cwd: baseCwd, + shell: false, + }), + ) + .pipe( + Effect.provideService(Scope.Scope, sessionScope), + Effect.mapError( + (cause) => + new ProviderAdapterProcessError({ + provider: PROVIDER, + threadId, + detail: toMessage(cause, "Failed to start Pi Agent RPC process."), + cause, + }), + ), + Effect.onError(() => Effect.ignore(Scope.close(sessionScope, Exit.void))), + ); + + const outgoingQueue = yield* Queue.unbounded(); + + const writeLine = (obj: RpcCommand | RpcExtensionUIResponse): Effect.Effect => + Queue.offer(outgoingQueue, Buffer.from(JSON.stringify(obj) + "\n")).pipe(Effect.asVoid); + + const writeCommand = (cmd: RpcCommand): Effect.Effect => writeLine(cmd); + const writeExtensionResponse = (resp: RpcExtensionUIResponse): Effect.Effect => + writeLine(resp); + + const pendingRequests = new Map>(); + const pendingExtensionUIs = new Map(); + + const session: ProviderSession = { + threadId, + provider: PROVIDER, + providerInstanceId: boundInstanceId, + status: "ready", + runtimeMode: input.runtimeMode, + ...(input.cwd ? { cwd: input.cwd } : {}), + ...(modelSelection?.model ? { model: modelSelection.model } : {}), + createdAt: startedAt, + updatedAt: startedAt, + }; + + const context: PiSessionContext = { + session, + sessionScope, + writeCommand, + writeExtensionResponse, + stdoutFiber: undefined, + pendingRequests, + pendingExtensionUIs, + startedAt, + turnState: undefined, + turns: [], + stopped: false, + }; + sessions.set(threadId, context); + + const runtimeContext = yield* Effect.context(); + const runFork = Effect.runForkWith(runtimeContext); + + // Fork stdin writer — continuously drains the outgoing queue to the process stdin + runFork( + Stream.fromQueue(outgoingQueue).pipe( + Stream.run(child.stdin), + Effect.ignore, + ), + ); + + // Fork stdout reader — parses JSONL events and routes them + const stdoutFiber = runFork( + child.stdout.pipe( + Stream.decodeText(), + Stream.splitLines, + Stream.mapEffect((line) => handleStdoutLine(context, line)), + Stream.runDrain, + Effect.ignore, + // Emit session exit if the process dies while the session is still active + Effect.ensuring( + Effect.suspend(() => { + if (!context.stopped && context.session.status !== "closed") { + return stopSessionInternal(context, { emitExitEvent: true }); + } + return Effect.void; + }), + ), + ), + ); + context.stdoutFiber = stdoutFiber; + + // Drain stderr to avoid blocking the process + runFork( + child.stderr.pipe( + Stream.runDrain, + Effect.ignore, + ), + ); + + // Fetch the session file path via get_state for resume cursor + const stateReqId = yield* Random.nextUUIDv4; + const stateDeferred = yield* Deferred.make(); + pendingRequests.set(stateReqId, stateDeferred); + yield* writeCommand({ type: "get_state", id: stateReqId }); + + const stateResponse = yield* Deferred.await(stateDeferred).pipe( + Effect.timeoutOption(5000), + ); + + const sessionFile: string | undefined = (() => { + if (stateResponse._tag === "None") return undefined; + const resp = stateResponse.value as Record; + if (resp["success"] !== true) return undefined; + const data = resp["data"]; + if (!data || typeof data !== "object") return undefined; + const sf = (data as Record)["sessionFile"]; + return typeof sf === "string" && sf.trim().length > 0 ? sf.trim() : undefined; + })(); + + if (sessionFile !== undefined) { + context.session = { + ...context.session, + resumeCursor: { sessionFile }, + }; + } + + const sessionStartedStamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "session.started", + eventId: sessionStartedStamp.eventId, + provider: PROVIDER, + createdAt: sessionStartedStamp.createdAt, + threadId, + payload: {}, + providerRefs: {}, + }); + + const configuredStamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "session.configured", + eventId: configuredStamp.eventId, + provider: PROVIDER, + createdAt: configuredStamp.createdAt, + threadId, + payload: { + config: { + ...(modelSelection?.model ? { model: modelSelection.model } : {}), + ...(input.cwd ? { cwd: input.cwd } : {}), + }, + }, + providerRefs: {}, + }); + + const readyStamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "session.state.changed", + eventId: readyStamp.eventId, + provider: PROVIDER, + createdAt: readyStamp.createdAt, + threadId, + payload: { state: "ready" }, + providerRefs: {}, + }); + + return { ...context.session }; + }); + + const sendTurn: PiAdapterShape["sendTurn"] = Effect.fn("sendTurn")(function* (input) { + const context = yield* requireSession(input.threadId); + + if (context.turnState) { + yield* completeTurn(context, "completed"); + } + + const turnId = TurnId.make(yield* Random.nextUUIDv4); + const turnStartedAt = yield* nowIso; + context.turnState = { turnId, startedAt: turnStartedAt, items: [] }; + context.session = { + ...context.session, + status: "running", + activeTurnId: turnId, + updatedAt: turnStartedAt, + }; + + const turnStartedStamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "turn.started", + eventId: turnStartedStamp.eventId, + provider: PROVIDER, + createdAt: turnStartedStamp.createdAt, + threadId: context.session.threadId, + turnId, + payload: {}, + providerRefs: {}, + }); + + const promptText = typeof input.input === "string" ? input.input : ""; + + yield* context.writeCommand({ type: "prompt", message: promptText }).pipe( + Effect.catchDefect(() => completeTurn(context, "failed", "Failed to send prompt.")), + ); + + return { + threadId: context.session.threadId, + turnId, + }; + }); + + const interruptTurn: PiAdapterShape["interruptTurn"] = Effect.fn("interruptTurn")( + function* (threadId, _turnId) { + const context = yield* requireSession(threadId); + yield* Effect.ignore(context.writeCommand({ type: "abort" })); + }, + ); + + const readThread: PiAdapterShape["readThread"] = Effect.fn("readThread")(function* (threadId) { + const context = yield* requireSession(threadId); + return { + threadId, + turns: context.turns.map((turn) => ({ + id: turn.id, + items: [...turn.items], + })), + }; + }); + + const rollbackThread: PiAdapterShape["rollbackThread"] = Effect.fn("rollbackThread")( + function* (threadId, numTurns) { + const context = yield* requireSession(threadId); + const nextLength = Math.max(0, context.turns.length - numTurns); + context.turns.splice(nextLength); + return { + threadId, + turns: context.turns.map((turn) => ({ + id: turn.id, + items: [...turn.items], + })), + }; + }, + ); + + const respondToRequest: PiAdapterShape["respondToRequest"] = (_threadId, _requestId, _decision) => + Effect.void; + + const respondToUserInput: PiAdapterShape["respondToUserInput"] = Effect.fn("respondToUserInput")( + function* (threadId, requestId, answers) { + const context = yield* requireSession(threadId); + const pending = context.pendingExtensionUIs.get(requestId); + if (!pending) return; + context.pendingExtensionUIs.delete(requestId); + + // Build the response Pi expects based on the original method + let piResponse: RpcExtensionUIResponse; + if (pending.method === "confirm") { + const answer = answers[pending.questionId]; + piResponse = { type: "extension_ui_response", id: pending.piId, confirmed: answer === "Yes" }; + } else if (pending.method === "input" && pending.numberedOptions) { + // Multi-select numbered list: map selected labels back to 1-based indices + const raw = answers[pending.questionId]; + const selected: string[] = Array.isArray(raw) + ? raw.map(String) + : typeof raw === "string" && raw.length > 0 + ? [raw] + : []; + const indices = selected + .map((label) => { + const idx = pending.numberedOptions!.indexOf(label); + return idx >= 0 ? String(idx + 1) : null; + }) + .filter((i): i is string => i !== null); + piResponse = { type: "extension_ui_response", id: pending.piId, value: indices.join(",") }; + } else { + // select or freeform input: answer is a string value + const answer = answers[pending.questionId]; + const value = typeof answer === "string" ? answer : ""; + piResponse = { type: "extension_ui_response", id: pending.piId, value }; + } + + yield* context.writeExtensionResponse(piResponse); + yield* Deferred.succeed(pending.deferred, answers); + + const stamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "user-input.resolved", + eventId: stamp.eventId, + provider: PROVIDER, + createdAt: stamp.createdAt, + threadId: context.session.threadId, + requestId: RuntimeRequestId.make(requestId), + payload: { answers }, + providerRefs: {}, + }); + }, + ); + + const stopSession: PiAdapterShape["stopSession"] = Effect.fn("stopSession")(function* (threadId) { + const context = yield* requireSession(threadId); + yield* stopSessionInternal(context, { emitExitEvent: true }); + }); + + const listSessions: PiAdapterShape["listSessions"] = () => + Effect.sync(() => Array.from(sessions.values(), ({ session }) => ({ ...session }))); + + const hasSession: PiAdapterShape["hasSession"] = (threadId) => + Effect.sync(() => { + const context = sessions.get(threadId); + return context !== undefined && !context.stopped; + }); + + const stopAll: PiAdapterShape["stopAll"] = () => + Effect.forEach( + sessions, + ([, context]) => stopSessionInternal(context, { emitExitEvent: true }), + { discard: true }, + ); + + yield* Effect.addFinalizer(() => + Effect.forEach( + sessions, + ([, context]) => stopSessionInternal(context, { emitExitEvent: false }), + { discard: true }, + ).pipe(Effect.tap(() => Queue.shutdown(runtimeEventQueue))), + ); + + return { + provider: PROVIDER, + capabilities: { + sessionModelSwitch: "unsupported" as const, + }, + startSession, + sendTurn, + interruptTurn, + readThread, + rollbackThread, + respondToRequest, + respondToUserInput, + stopSession, + listSessions, + hasSession, + stopAll, + get streamEvents() { + return Stream.fromQueue(runtimeEventQueue); + }, + } satisfies PiAdapterShape; +}); diff --git a/apps/server/src/provider/Layers/PiProvider.ts b/apps/server/src/provider/Layers/PiProvider.ts new file mode 100644 index 00000000000..e340fac868e --- /dev/null +++ b/apps/server/src/provider/Layers/PiProvider.ts @@ -0,0 +1,399 @@ +import { + type PiSettings, + type ModelCapabilities, + type ServerProviderModel, + type ServerProviderSkill, + ProviderDriverKind, +} from "@t3tools/contracts"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Option from "effect/Option"; +import * as Path from "effect/Path"; +import * as Result from "effect/Result"; +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; +import { createModelCapabilities } from "@t3tools/shared/model"; + +import { + buildSelectOptionDescriptor, + buildServerProvider, + DEFAULT_TIMEOUT_MS, + detailFromResult, + isCommandMissingCause, + parseGenericCliVersion, + providerModelsFromSettings, + spawnAndCollect, + type ServerProviderDraft, +} from "../providerSnapshot.ts"; +import { makePiEnvironment, resolvePiHomePath } from "../Drivers/PiHome.ts"; + +const DEFAULT_PI_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabilities({ + optionDescriptors: [], +}); + +const PROVIDER = ProviderDriverKind.make("piAgent"); +const PI_PRESENTATION = { + displayName: "Pi Agent", + showInteractionModeToggle: true, +} as const; + +const BUILT_IN_MODELS: ReadonlyArray = [ + { + slug: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", + isCustom: false, + capabilities: createModelCapabilities({ + optionDescriptors: [ + buildSelectOptionDescriptor({ + id: "thinking", + label: "Thinking", + options: [ + { value: "off", label: "Off" }, + { value: "low", label: "Low" }, + { value: "medium", label: "Medium", isDefault: true }, + { value: "high", label: "High" }, + { value: "xhigh", label: "Extra High" }, + ], + }), + ], + }), + }, + { + slug: "claude-opus-4-7", + name: "Claude Opus 4.7", + isCustom: false, + capabilities: createModelCapabilities({ + optionDescriptors: [ + buildSelectOptionDescriptor({ + id: "thinking", + label: "Thinking", + options: [ + { value: "off", label: "Off" }, + { value: "low", label: "Low" }, + { value: "medium", label: "Medium" }, + { value: "high", label: "High", isDefault: true }, + { value: "xhigh", label: "Extra High" }, + ], + }), + ], + }), + }, + { + slug: "claude-haiku-4-5", + name: "Claude Haiku 4.5", + isCustom: false, + capabilities: createModelCapabilities({ + optionDescriptors: [ + buildSelectOptionDescriptor({ + id: "thinking", + label: "Thinking", + options: [ + { value: "off", label: "Off" }, + { value: "low", label: "Low", isDefault: true }, + { value: "medium", label: "Medium" }, + { value: "high", label: "High" }, + ], + }), + ], + }), + }, +]; + +export function getPiModelCapabilities(model: string | null | undefined): ModelCapabilities { + const slug = model?.trim(); + return ( + BUILT_IN_MODELS.find((candidate) => candidate.slug === slug)?.capabilities ?? + DEFAULT_PI_MODEL_CAPABILITIES + ); +} + +const runPiCommand = Effect.fn("runPiCommand")(function* ( + piSettings: PiSettings, + args: ReadonlyArray, + environment: NodeJS.ProcessEnv = process.env, +) { + const piEnvironment = yield* makePiEnvironment(piSettings, environment); + const command = ChildProcess.make(piSettings.binaryPath || "pi", [...args], { + env: piEnvironment, + shell: false, + }); + return yield* spawnAndCollect(piSettings.binaryPath || "pi", command); +}); + +const nowIso = Effect.map(DateTime.now, DateTime.formatIso); + +function parseSkillFrontmatter(content: string): { + name?: string; + description?: string; +} { + const match = /^---\s*\n([\s\S]*?)\n---/.exec(content); + if (!match?.[1]) return {}; + const yaml = match[1]; + const result: { name?: string; description?: string } = {}; + for (const line of yaml.split("\n")) { + const kvMatch = /^(\w[\w-]*):\s*(.+)$/.exec(line.trim()); + if (!kvMatch) continue; + const key = kvMatch[1]; + const rawValue = kvMatch[2] ?? ""; + const value = rawValue.replace(/^["']|["']$/g, "").trim(); + if (key === "name" && value.length > 0) result.name = value; + if (key === "description" && value.length > 0) result.description = value; + } + return result; +} + +function skillNameFromPath(filePath: string, pathSep: string): string { + const base = filePath.split(pathSep).pop() ?? filePath; + return base.replace(/\.md$/i, ""); +} + +const scanSkillDir = Effect.fn("scanSkillDir")(function* ( + dir: string, + scope: string, +): Effect.fn.Return< + ServerProviderSkill[], + never, + FileSystem.FileSystem | Path.Path +> { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const skills: ServerProviderSkill[] = []; + + const exists = yield* fs.exists(dir).pipe(Effect.orElseSucceed(() => false)); + if (!exists) return skills; + + const entries = yield* fs.readDirectory(dir).pipe(Effect.orElseSucceed(() => [] as string[])); + + for (const entry of entries) { + const entryPath = path.join(dir, entry); + const stat = yield* fs.stat(entryPath).pipe(Effect.orElseSucceed(() => undefined)); + if (!stat) continue; + + if (stat.type === "Directory") { + const skillMdPath = path.join(entryPath, "SKILL.md"); + const hasSkillMd = yield* fs.exists(skillMdPath).pipe(Effect.orElseSucceed(() => false)); + if (hasSkillMd) { + const content = yield* fs.readFileString(skillMdPath).pipe( + Effect.orElseSucceed(() => ""), + ); + const frontmatter = parseSkillFrontmatter(content); + const name = frontmatter.name ?? entry; + skills.push({ + name, + path: skillMdPath, + enabled: true, + scope, + ...(frontmatter.description ? { description: frontmatter.description } : {}), + ...(frontmatter.name ? { displayName: frontmatter.name } : {}), + }); + } + continue; + } + + if (entry.endsWith(".md")) { + const content = yield* fs.readFileString(entryPath).pipe( + Effect.orElseSucceed(() => ""), + ); + const frontmatter = parseSkillFrontmatter(content); + const name = frontmatter.name ?? skillNameFromPath(entry, path.sep); + skills.push({ + name, + path: entryPath, + enabled: true, + ...(scope ? { scope } : {}), + ...(frontmatter.description ? { description: frontmatter.description } : {}), + ...(frontmatter.name ? { displayName: frontmatter.name } : {}), + }); + } + } + + return skills; +}); + +const discoverPiSkills = Effect.fn("discoverPiSkills")(function* ( + cwd: string, + piSettings: PiSettings, +): Effect.fn.Return< + ReadonlyArray, + never, + FileSystem.FileSystem | Path.Path +> { + const path = yield* Path.Path; + const piHomePath = yield* resolvePiHomePath(piSettings); + + const dirs: Array<{ dir: string; scope: string }> = [ + { dir: path.join(cwd, ".pi", "skills"), scope: "project" }, + { dir: path.join(cwd, ".claude", "skills"), scope: "project" }, + { dir: path.join(piHomePath, ".pi", "agent", "skills"), scope: "user" }, + ]; + + const results = yield* Effect.all( + dirs.map(({ dir, scope }) => scanSkillDir(dir, scope)), + { concurrency: "unbounded" }, + ); + + const seen = new Set(); + const deduped: ServerProviderSkill[] = []; + for (const batch of results) { + for (const skill of batch) { + if (!seen.has(skill.name)) { + seen.add(skill.name); + deduped.push(skill); + } + } + } + return deduped; +}); + +export const checkPiProviderStatus = Effect.fn("checkPiProviderStatus")(function* ( + piSettings: PiSettings, + cwd: string, + environment: NodeJS.ProcessEnv = process.env, +): Effect.fn.Return< + ServerProviderDraft, + never, + ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path +> { + const checkedAt = DateTime.formatIso(yield* DateTime.now); + const allModels = providerModelsFromSettings( + BUILT_IN_MODELS, + PROVIDER, + piSettings.customModels, + DEFAULT_PI_MODEL_CAPABILITIES, + ); + + if (!piSettings.enabled) { + return buildServerProvider({ + presentation: PI_PRESENTATION, + enabled: false, + checkedAt, + models: allModels, + probe: { + installed: false, + version: null, + status: "warning", + auth: { status: "unknown" }, + message: "Pi Agent is disabled in T3 Code settings.", + }, + }); + } + + const versionProbe = yield* runPiCommand(piSettings, ["--version"], environment).pipe( + Effect.timeoutOption(DEFAULT_TIMEOUT_MS), + Effect.result, + ); + + if (Result.isFailure(versionProbe)) { + const error = versionProbe.failure; + return buildServerProvider({ + presentation: PI_PRESENTATION, + enabled: piSettings.enabled, + checkedAt, + models: allModels, + probe: { + installed: !isCommandMissingCause(error), + version: null, + status: "error", + auth: { status: "unknown" }, + message: isCommandMissingCause(error) + ? "Pi Agent CLI (`pi`) is not installed or not on PATH." + : `Failed to execute Pi Agent CLI health check: ${error instanceof Error ? error.message : String(error)}.`, + }, + }); + } + + if (Option.isNone(versionProbe.success)) { + return buildServerProvider({ + presentation: PI_PRESENTATION, + enabled: piSettings.enabled, + checkedAt, + models: allModels, + probe: { + installed: true, + version: null, + status: "error", + auth: { status: "unknown" }, + message: "Pi Agent CLI is installed but failed to run. Timed out while running command.", + }, + }); + } + + const version = versionProbe.success.value; + const parsedVersion = parseGenericCliVersion(`${version.stdout}\n${version.stderr}`); + + if (version.code !== 0) { + const detail = detailFromResult(version); + return buildServerProvider({ + presentation: PI_PRESENTATION, + enabled: piSettings.enabled, + checkedAt, + models: allModels, + probe: { + installed: true, + version: parsedVersion, + status: "error", + auth: { status: "unknown" }, + message: detail ?? "Pi Agent CLI returned an error during health check.", + }, + }); + } + + const skills = yield* discoverPiSkills(cwd, piSettings).pipe( + Effect.orElseSucceed(() => [] as ServerProviderSkill[]), + ); + + return buildServerProvider({ + presentation: PI_PRESENTATION, + enabled: piSettings.enabled, + checkedAt, + models: allModels, + skills, + probe: { + installed: true, + version: parsedVersion, + status: "ready", + auth: { status: "authenticated" }, + }, + }); +}); + +export const makePendingPiProvider = (piSettings: PiSettings): Effect.Effect => + Effect.gen(function* () { + const checkedAt = yield* nowIso; + const models = providerModelsFromSettings( + BUILT_IN_MODELS, + PROVIDER, + piSettings.customModels, + DEFAULT_PI_MODEL_CAPABILITIES, + ); + + if (!piSettings.enabled) { + return buildServerProvider({ + presentation: PI_PRESENTATION, + enabled: false, + checkedAt, + models, + probe: { + installed: false, + version: null, + status: "warning", + auth: { status: "unknown" }, + message: "Pi Agent is disabled in T3 Code settings.", + }, + }); + } + + return buildServerProvider({ + presentation: PI_PRESENTATION, + enabled: true, + checkedAt, + models, + probe: { + installed: false, + version: null, + status: "warning", + auth: { status: "unknown" }, + message: "Pi Agent provider status has not been checked in this session yet.", + }, + }); + }); diff --git a/apps/server/src/provider/Layers/ProviderRegistry.test.ts b/apps/server/src/provider/Layers/ProviderRegistry.test.ts index fb6eb3b443d..af5ffb8e26b 100644 --- a/apps/server/src/provider/Layers/ProviderRegistry.test.ts +++ b/apps/server/src/provider/Layers/ProviderRegistry.test.ts @@ -993,6 +993,13 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T claudeAgent: { enabled: false }, cursor: { enabled: false }, opencode: { enabled: false }, + piAgent: { + enabled: true, + binaryPath: "", + homePath: "", + customModels: [], + launchArgs: "", + }, }, // `providerInstances` keys are branded `ProviderInstanceId`; // the branded index signature rejects plain string literals @@ -1087,6 +1094,13 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T claudeAgent: { enabled: false }, cursor: { enabled: false }, opencode: { enabled: false }, + piAgent: { + enabled: true, + binaryPath: "", + homePath: "", + customModels: [], + launchArgs: "", + }, }, }), ), @@ -1182,6 +1196,13 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T claudeAgent: { enabled: false }, cursor: { enabled: false }, opencode: { enabled: false }, + piAgent: { + enabled: true, + binaryPath: "", + homePath: "", + customModels: [], + launchArgs: "", + }, }, providerInstances: { ghost_main: { @@ -1240,6 +1261,13 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T cursor: { enabled: false, }, + piAgent: { + enabled: true, + binaryPath: "", + homePath: "", + customModels: [], + launchArgs: "", + }, }, }), ), @@ -1301,6 +1329,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T "codex", "cursor", "opencode", + "piAgent", ]); assert.strictEqual(cursorProvider?.enabled, false); assert.strictEqual(cursorProvider?.status, "disabled"); diff --git a/apps/server/src/provider/Layers/ProviderService.test.ts b/apps/server/src/provider/Layers/ProviderService.test.ts index fc0450b8b69..b0e94b1a564 100644 --- a/apps/server/src/provider/Layers/ProviderService.test.ts +++ b/apps/server/src/provider/Layers/ProviderService.test.ts @@ -453,6 +453,13 @@ it.effect( codex: { enabled: false, }, + piAgent: { + enabled: true, + binaryPath: "", + homePath: "", + customModels: [], + launchArgs: "", + }, }, }); const runtimeRepositoryLayer = ProviderSessionRuntimeRepositoryLive.pipe( diff --git a/apps/server/src/provider/Services/PiAdapter.ts b/apps/server/src/provider/Services/PiAdapter.ts new file mode 100644 index 00000000000..97ae701f64b --- /dev/null +++ b/apps/server/src/provider/Services/PiAdapter.ts @@ -0,0 +1,4 @@ +import type { ProviderAdapterError } from "../Errors.ts"; +import type { ProviderAdapterShape } from "./ProviderAdapter.ts"; + +export interface PiAdapterShape extends ProviderAdapterShape {} diff --git a/apps/server/src/provider/builtInDrivers.ts b/apps/server/src/provider/builtInDrivers.ts index 5af56dc6b0e..0390e2c4f6b 100644 --- a/apps/server/src/provider/builtInDrivers.ts +++ b/apps/server/src/provider/builtInDrivers.ts @@ -24,6 +24,7 @@ import { ClaudeDriver, type ClaudeDriverEnv } from "./Drivers/ClaudeDriver.ts"; import { CodexDriver, type CodexDriverEnv } from "./Drivers/CodexDriver.ts"; import { CursorDriver, type CursorDriverEnv } from "./Drivers/CursorDriver.ts"; import { OpenCodeDriver, type OpenCodeDriverEnv } from "./Drivers/OpenCodeDriver.ts"; +import { PiDriver, type PiDriverEnv } from "./Drivers/PiDriver.ts"; import type { AnyProviderDriver } from "./ProviderDriver.ts"; /** @@ -35,7 +36,8 @@ export type BuiltInDriversEnv = | ClaudeDriverEnv | CodexDriverEnv | CursorDriverEnv - | OpenCodeDriverEnv; + | OpenCodeDriverEnv + | PiDriverEnv; /** * Ordered list of built-in drivers. Order matters only for tie-breaking in @@ -47,4 +49,5 @@ export const BUILT_IN_DRIVERS: ReadonlyArray true), -); - const makeAuthTestLayer = () => ServerAuthLive.pipe(Layer.provide(SqlitePersistenceMemory), Layer.provide(ServerSecretStoreLive)); -const makeBrowserOtlpPayload = (spanName: string) => - Effect.gen(function* () { - const collector = yield* Effect.acquireRelease( - Effect.promise(async () => { - const NodeHttp = await import("node:http"); - - return await new Promise<{ - readonly close: () => Promise; - readonly firstRequest: Promise<{ - readonly body: string; - readonly contentType: string | null; - }>; - readonly url: string; - }>((resolve, reject) => { - let resolveFirstRequest: - | ((request: { readonly body: string; readonly contentType: string | null }) => void) - | undefined; - const firstRequest = new Promise<{ - readonly body: string; - readonly contentType: string | null; - }>((resolveRequest) => { - resolveFirstRequest = resolveRequest; - }); - - const server = NodeHttp.createServer((request, response) => { - const chunks: Buffer[] = []; - request.on("data", (chunk) => { - chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); - }); - request.on("end", () => { - resolveFirstRequest?.({ - body: Buffer.concat(chunks).toString("utf8"), - contentType: request.headers["content-type"] ?? null, - }); - resolveFirstRequest = undefined; - response.statusCode = 204; - response.end(); - }); - }); - - server.on("error", reject); - server.listen(0, "127.0.0.1", () => { - const address = server.address(); - if (!address || typeof address === "string") { - reject(new Error("Expected TCP collector address")); - return; - } - - resolve({ - url: `http://127.0.0.1:${address.port}/v1/traces`, - firstRequest, - close: () => - new Promise((resolveClose, rejectClose) => { - server.close((error) => { - if (error) { - rejectClose(error); - return; - } - resolveClose(); - }); - }), - }); - }); - }); - }), - ({ close }) => Effect.promise(close), - ); - - const runtime = ManagedRuntime.make( - OtlpTracer.layer({ - url: collector.url, - exportInterval: "10 millis", - resource: { - serviceName: "t3-web", - attributes: { - "service.runtime": "t3-web", - "service.mode": "browser", - "service.version": "test", - }, - }, - }).pipe(Layer.provide(browserOtlpTracingLayer)), - ); - - try { - yield* Effect.promise(() => runtime.runPromise(Effect.void.pipe(Effect.withSpan(spanName)))); - } finally { - yield* Effect.promise(() => runtime.dispose()); - } - - const request = yield* Effect.raceFirst( - Effect.promise(() => collector.firstRequest).pipe(Effect.orDie), - Effect.sleep(Duration.seconds(1)).pipe( - Effect.andThen(Effect.die(new Error("Timed out waiting for OTLP trace export"))), - ), - ); - // @effect-diagnostics-next-line preferSchemaOverJson:off - return JSON.parse(request.body) as OtlpTracer.TraceData; - }); - const buildAppUnderTest = (options?: { config?: Partial; layers?: { @@ -1742,194 +1635,6 @@ it.layer(NodeServices.layer)("server router seam", (it) => { }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); - it.effect("proxies browser OTLP trace exports through the server", () => - Effect.gen(function* () { - const upstreamRequests: Array<{ - readonly body: string; - readonly contentType: string | null; - }> = []; - const localTraceRecords: Array = []; - const payload = { - resourceSpans: [ - { - resource: { - attributes: [ - { - key: "service.name", - value: { stringValue: "t3-web" }, - }, - ], - }, - scopeSpans: [ - { - scope: { - name: "effect", - version: "4.0.0-beta.43", - }, - spans: [ - { - traceId: "11111111111111111111111111111111", - spanId: "2222222222222222", - parentSpanId: "3333333333333333", - name: "RpcClient.server.getSettings", - kind: 3, - startTimeUnixNano: "1000000", - endTimeUnixNano: "2000000", - attributes: [ - { - key: "rpc.method", - value: { stringValue: "server.getSettings" }, - }, - ], - events: [ - { - name: "http.request", - timeUnixNano: "1500000", - attributes: [ - { - key: "http.status_code", - value: { intValue: "200" }, - }, - ], - }, - ], - links: [], - status: { - code: "STATUS_CODE_OK", - }, - flags: 1, - }, - ], - }, - ], - }, - ], - }; - - const collector = yield* Effect.acquireRelease( - Effect.promise(async () => { - const NodeHttp = await import("node:http"); - - return await new Promise<{ - readonly close: () => Promise; - readonly url: string; - }>((resolve, reject) => { - const server = NodeHttp.createServer((request, response) => { - const chunks: Buffer[] = []; - request.on("data", (chunk) => { - chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); - }); - request.on("end", () => { - upstreamRequests.push({ - body: Buffer.concat(chunks).toString("utf8"), - contentType: request.headers["content-type"] ?? null, - }); - response.statusCode = 204; - response.end(); - }); - }); - - server.on("error", reject); - server.listen(0, "127.0.0.1", () => { - const address = server.address(); - if (!address || typeof address === "string") { - reject(new Error("Expected TCP collector address")); - return; - } - - resolve({ - url: `http://127.0.0.1:${address.port}/v1/traces`, - close: () => - new Promise((resolveClose, rejectClose) => { - server.close((error) => { - if (error) { - rejectClose(error); - return; - } - resolveClose(); - }); - }), - }); - }); - }); - }), - ({ close }) => Effect.promise(close), - ); - - yield* buildAppUnderTest({ - config: { - otlpTracesUrl: collector.url, - }, - layers: { - browserTraceCollector: { - record: (records) => - Effect.sync(() => { - localTraceRecords.push(...records); - }), - }, - }, - }); - - const response = yield* HttpClient.post("/api/observability/v1/traces", { - headers: { - cookie: yield* getAuthenticatedSessionCookieHeader(), - "content-type": "application/json", - origin: "http://localhost:5733", - }, - // @effect-diagnostics-next-line preferSchemaOverJson:off - body: HttpBody.text(JSON.stringify(payload), "application/json"), - }); - - assert.equal(response.status, 204); - assert.equal(response.headers["access-control-allow-origin"], "*"); - assert.deepEqual(localTraceRecords, [ - { - type: "otlp-span", - name: "RpcClient.server.getSettings", - traceId: "11111111111111111111111111111111", - spanId: "2222222222222222", - parentSpanId: "3333333333333333", - sampled: true, - kind: "client", - startTimeUnixNano: "1000000", - endTimeUnixNano: "2000000", - durationMs: 1, - attributes: { - "rpc.method": "server.getSettings", - }, - resourceAttributes: { - "service.name": "t3-web", - }, - scope: { - name: "effect", - version: "4.0.0-beta.43", - attributes: {}, - }, - events: [ - { - name: "http.request", - timeUnixNano: "1500000", - attributes: { - "http.status_code": "200", - }, - }, - ], - links: [], - status: { - code: "STATUS_CODE_OK", - }, - }, - ]); - assert.deepEqual(upstreamRequests, [ - { - // @effect-diagnostics-next-line preferSchemaOverJson:off - body: JSON.stringify(payload), - contentType: "application/json", - }, - ]); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - it.effect("responds to browser OTLP trace preflight requests with CORS headers", () => Effect.gen(function* () { yield* buildAppUnderTest(); @@ -1962,79 +1667,6 @@ it.layer(NodeServices.layer)("server router seam", (it) => { }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); - it.effect( - "stores browser OTLP trace exports locally when no upstream collector is configured", - () => - Effect.gen(function* () { - const localTraceRecords: Array = []; - const payload = yield* makeBrowserOtlpPayload("client.test"); - const resourceSpan = payload.resourceSpans[0]; - const scopeSpan = resourceSpan?.scopeSpans[0]; - const span = scopeSpan?.spans[0]; - - assert.notEqual(resourceSpan, undefined); - assert.notEqual(scopeSpan, undefined); - assert.notEqual(span, undefined); - if (!resourceSpan || !scopeSpan || !span) { - return; - } - - yield* buildAppUnderTest({ - layers: { - browserTraceCollector: { - record: (records) => - Effect.sync(() => { - localTraceRecords.push(...records); - }), - }, - }, - }); - - const response = yield* HttpClient.post("/api/observability/v1/traces", { - headers: { - cookie: yield* getAuthenticatedSessionCookieHeader(), - "content-type": "application/json", - }, - // @effect-diagnostics-next-line preferSchemaOverJson:off - body: HttpBody.text(JSON.stringify(payload), "application/json"), - }); - - assert.equal(response.status, 204); - assert.equal(localTraceRecords.length, 1); - const record = localTraceRecords[0] as { - readonly type: string; - readonly name: string; - readonly traceId: string; - readonly spanId: string; - readonly kind: string; - readonly attributes: Readonly>; - readonly events: ReadonlyArray; - readonly links: ReadonlyArray; - readonly scope: { - readonly name?: string; - readonly attributes: Readonly>; - }; - readonly resourceAttributes: Readonly>; - readonly status?: { - readonly code?: string; - }; - }; - - assert.equal(record.type, "otlp-span"); - assert.equal(record.name, span.name); - assert.equal(record.traceId, span.traceId); - assert.equal(record.spanId, span.spanId); - assert.equal(record.kind, "internal"); - assert.deepEqual(record.attributes, {}); - assert.deepEqual(record.events, []); - assert.deepEqual(record.links, []); - assert.equal(record.scope.name, scopeSpan.scope.name); - assert.deepEqual(record.scope.attributes, {}); - assert.equal(record.resourceAttributes["service.name"], "t3-web"); - assert.equal(record.status?.code, String(span.status.code)); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - it.effect("returns 404 for missing attachment id lookups", () => Effect.gen(function* () { yield* buildAppUnderTest(); diff --git a/apps/server/src/serverRuntimeStartup.test.ts b/apps/server/src/serverRuntimeStartup.test.ts index 33b9abcb98a..345c974bf61 100644 --- a/apps/server/src/serverRuntimeStartup.test.ts +++ b/apps/server/src/serverRuntimeStartup.test.ts @@ -1,5 +1,13 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; -import { DEFAULT_MODEL, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools/contracts"; +import { + DEFAULT_MODEL, + DEFAULT_MODEL_BY_PROVIDER, + ProjectId, + ProviderDriverKind, + ProviderInstanceId, + ThreadId, + defaultInstanceIdForDriver, +} from "@t3tools/contracts"; import { assert, it } from "@effect/vitest"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; @@ -24,10 +32,11 @@ import { ServerRuntimeStartupError, } from "./serverRuntimeStartup.ts"; -it("uses the canonical Codex default for auto-bootstrapped model selection", () => { +it("uses the canonical Claude default for auto-bootstrapped model selection", () => { + const provider = ProviderDriverKind.make("claudeAgent"); assert.deepStrictEqual(getAutoBootstrapDefaultModelSelection(), { - instanceId: ProviderInstanceId.make("codex"), - model: DEFAULT_MODEL, + instanceId: defaultInstanceIdForDriver(provider), + model: DEFAULT_MODEL_BY_PROVIDER[provider] ?? DEFAULT_MODEL, }); }); diff --git a/apps/server/src/serverRuntimeStartup.ts b/apps/server/src/serverRuntimeStartup.ts index 9ec536105c8..af4460ed439 100644 --- a/apps/server/src/serverRuntimeStartup.ts +++ b/apps/server/src/serverRuntimeStartup.ts @@ -1,11 +1,14 @@ import { CommandId, DEFAULT_MODEL, + DEFAULT_MODEL_BY_PROVIDER, DEFAULT_PROVIDER_INTERACTION_MODE, type ModelSelection, ProjectId, + ProviderDriverKind, ProviderInstanceId, ThreadId, + defaultInstanceIdForDriver, } from "@t3tools/contracts"; import * as Data from "effect/Data"; import * as Deferred from "effect/Deferred"; @@ -153,10 +156,13 @@ export const launchStartupHeartbeat = recordStartupHeartbeat.pipe( Effect.asVoid, ); -export const getAutoBootstrapDefaultModelSelection = (): ModelSelection => ({ - instanceId: ProviderInstanceId.make("codex"), - model: DEFAULT_MODEL, -}); +export const getAutoBootstrapDefaultModelSelection = (): ModelSelection => { + const provider = ProviderDriverKind.make("claudeAgent"); + return { + instanceId: defaultInstanceIdForDriver(provider), + model: DEFAULT_MODEL_BY_PROVIDER[provider] ?? DEFAULT_MODEL, + }; +}; export const resolveWelcomeBase = Effect.gen(function* () { const serverConfig = yield* ServerConfig; diff --git a/apps/server/src/telemetry/Layers/AnalyticsService.ts b/apps/server/src/telemetry/Layers/AnalyticsService.ts index ec859fb18f5..7dea6a624fd 100644 --- a/apps/server/src/telemetry/Layers/AnalyticsService.ts +++ b/apps/server/src/telemetry/Layers/AnalyticsService.ts @@ -32,7 +32,7 @@ const TelemetryEnvConfig = Config.all({ posthogHost: Config.string("T3CODE_POSTHOG_HOST").pipe( Config.withDefault("https://us.i.posthog.com"), ), - enabled: Config.boolean("T3CODE_TELEMETRY_ENABLED").pipe(Config.withDefault(true)), + enabled: Config.boolean("T3CODE_TELEMETRY_ENABLED").pipe(Config.withDefault(false)), flushBatchSize: Config.number("T3CODE_TELEMETRY_FLUSH_BATCH_SIZE").pipe(Config.withDefault(20)), maxBufferedEvents: Config.number("T3CODE_TELEMETRY_MAX_BUFFERED_EVENTS").pipe( Config.withDefault(1_000), diff --git a/apps/server/src/textGeneration/PiTextGeneration.ts b/apps/server/src/textGeneration/PiTextGeneration.ts new file mode 100644 index 00000000000..1219e3ab15b --- /dev/null +++ b/apps/server/src/textGeneration/PiTextGeneration.ts @@ -0,0 +1,277 @@ +import * as Effect from "effect/Effect"; +import * as Option from "effect/Option"; +import * as Schema from "effect/Schema"; +import * as Stream from "effect/Stream"; +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; + +import { type PiSettings, type ModelSelection, TextGenerationError } from "@t3tools/contracts"; +import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git"; + +import { type TextGenerationShape } from "./TextGeneration.ts"; +import { + buildBranchNamePrompt, + buildCommitMessagePrompt, + buildPrContentPrompt, + buildThreadTitlePrompt, +} from "./TextGenerationPrompts.ts"; +import { + normalizeCliError, + sanitizeCommitSubject, + sanitizePrTitle, + sanitizeThreadTitle, + toJsonSchemaObject, +} from "./TextGenerationUtils.ts"; +import { makePiEnvironment } from "../provider/Drivers/PiHome.ts"; + +const PI_TIMEOUT_MS = 180_000; + +const encodeJsonString = Schema.encodeEffect(Schema.UnknownFromJsonString); + +export const makePiTextGeneration = Effect.fn("makePiTextGeneration")(function* ( + piSettings: PiSettings, + environment: NodeJS.ProcessEnv = process.env, +) { + const commandSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const piEnvironment = yield* makePiEnvironment(piSettings, environment); + + const readStreamAsString = ( + operation: string, + stream: Stream.Stream, + ): Effect.Effect => + stream.pipe( + Stream.decodeText(), + Stream.runFold( + () => "", + (acc, chunk) => acc + chunk, + ), + Effect.mapError((cause) => + normalizeCliError("pi", operation, cause, "Failed to collect process output"), + ), + ); + + const runPiJson = Effect.fn("runPiJson")(function* ({ + operation, + cwd, + prompt, + outputSchemaJson, + modelSelection, + }: { + operation: + | "generateCommitMessage" + | "generatePrContent" + | "generateBranchName" + | "generateThreadTitle"; + cwd: string; + prompt: string; + outputSchemaJson: S; + modelSelection: ModelSelection; + }): Effect.fn.Return { + const runPiCommand = Effect.fn("runPiJson.runPiCommand")(function* () { + const command = ChildProcess.make( + piSettings.binaryPath || "pi", + [ + "--print", + "--output-format", + "json", + ...(modelSelection.model ? ["--model", modelSelection.model] : []), + ], + { + env: piEnvironment, + cwd, + shell: true, + stdin: { + stream: Stream.encodeText( + // @effect-diagnostics-next-line preferSchemaOverJson:off + Stream.make( + `${prompt}\n\nRespond ONLY with valid JSON matching this schema:\n${JSON.stringify(toJsonSchemaObject(outputSchemaJson))}`, + ), + ), + }, + }, + ); + + const child = yield* commandSpawner + .spawn(command) + .pipe( + Effect.mapError((cause) => + normalizeCliError("pi", operation, cause, "Failed to spawn Pi Agent CLI process"), + ), + ); + + const [stdout, stderr, exitCode] = yield* Effect.all( + [ + readStreamAsString(operation, child.stdout), + readStreamAsString(operation, child.stderr), + child.exitCode.pipe( + Effect.mapError((cause) => + normalizeCliError("pi", operation, cause, "Failed to read Pi Agent CLI exit code"), + ), + ), + ], + { concurrency: "unbounded" }, + ); + + if (exitCode !== 0) { + const stderrDetail = stderr.trim(); + const stdoutDetail = stdout.trim(); + const detail = stderrDetail.length > 0 ? stderrDetail : stdoutDetail; + return yield* new TextGenerationError({ + operation, + detail: + detail.length > 0 + ? `Pi Agent CLI command failed: ${detail}` + : `Pi Agent CLI command failed with code ${exitCode}.`, + }); + } + + return stdout; + }); + + const rawStdout = yield* runPiCommand().pipe( + Effect.scoped, + Effect.timeoutOption(PI_TIMEOUT_MS), + Effect.flatMap( + Option.match({ + onNone: () => + Effect.fail( + new TextGenerationError({ operation, detail: "Pi Agent CLI request timed out." }), + ), + onSome: (value) => Effect.succeed(value), + }), + ), + ); + + const jsonMatch = rawStdout.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + return yield* new TextGenerationError({ + operation, + detail: "Pi Agent CLI did not return valid JSON output.", + }); + } + + // @effect-diagnostics-next-line preferSchemaOverJson:off + const parsed = yield* Effect.try({ + try: () => JSON.parse(jsonMatch[0]), + catch: (cause) => + new TextGenerationError({ + operation, + detail: "Pi Agent CLI returned malformed JSON.", + cause, + }), + }); + + const decodeOutput = Schema.decodeEffect(outputSchemaJson); + return yield* decodeOutput(parsed).pipe( + Effect.catchTag("SchemaError", (cause) => + Effect.fail( + new TextGenerationError({ + operation, + detail: "Pi Agent returned invalid structured output.", + cause, + }), + ), + ), + ); + }); + + const generateCommitMessage: TextGenerationShape["generateCommitMessage"] = Effect.fn( + "PiTextGeneration.generateCommitMessage", + )(function* (input) { + const { prompt, outputSchema } = buildCommitMessagePrompt({ + branch: input.branch, + stagedSummary: input.stagedSummary, + stagedPatch: input.stagedPatch, + includeBranch: input.includeBranch === true, + }); + + const generated = yield* runPiJson({ + operation: "generateCommitMessage", + cwd: input.cwd, + prompt, + outputSchemaJson: outputSchema, + modelSelection: input.modelSelection, + }); + + return { + subject: sanitizeCommitSubject(generated.subject), + body: generated.body.trim(), + ...("branch" in generated && typeof generated.branch === "string" + ? { branch: sanitizeFeatureBranchName(generated.branch) } + : {}), + }; + }); + + const generatePrContent: TextGenerationShape["generatePrContent"] = Effect.fn( + "PiTextGeneration.generatePrContent", + )(function* (input) { + const { prompt, outputSchema } = buildPrContentPrompt({ + baseBranch: input.baseBranch, + headBranch: input.headBranch, + commitSummary: input.commitSummary, + diffSummary: input.diffSummary, + diffPatch: input.diffPatch, + }); + + const generated = yield* runPiJson({ + operation: "generatePrContent", + cwd: input.cwd, + prompt, + outputSchemaJson: outputSchema, + modelSelection: input.modelSelection, + }); + + return { + title: sanitizePrTitle(generated.title), + body: generated.body.trim(), + }; + }); + + const generateBranchName: TextGenerationShape["generateBranchName"] = Effect.fn( + "PiTextGeneration.generateBranchName", + )(function* (input) { + const { prompt, outputSchema } = buildBranchNamePrompt({ + message: input.message, + attachments: input.attachments, + }); + + const generated = yield* runPiJson({ + operation: "generateBranchName", + cwd: input.cwd, + prompt, + outputSchemaJson: outputSchema, + modelSelection: input.modelSelection, + }); + + return { + branch: sanitizeBranchFragment(generated.branch), + }; + }); + + const generateThreadTitle: TextGenerationShape["generateThreadTitle"] = Effect.fn( + "PiTextGeneration.generateThreadTitle", + )(function* (input) { + const { prompt, outputSchema } = buildThreadTitlePrompt({ + message: input.message, + attachments: input.attachments, + }); + + const generated = yield* runPiJson({ + operation: "generateThreadTitle", + cwd: input.cwd, + prompt, + outputSchemaJson: outputSchema, + modelSelection: input.modelSelection, + }); + + return { + title: sanitizeThreadTitle(generated.title), + }; + }); + + return { + generateCommitMessage, + generatePrContent, + generateBranchName, + generateThreadTitle, + } satisfies TextGenerationShape; +}); diff --git a/apps/server/src/textGeneration/TextGeneration.ts b/apps/server/src/textGeneration/TextGeneration.ts index 36a23d509db..3574ae796fe 100644 --- a/apps/server/src/textGeneration/TextGeneration.ts +++ b/apps/server/src/textGeneration/TextGeneration.ts @@ -10,7 +10,7 @@ import { } from "../provider/Services/ProviderInstanceRegistry.ts"; import type { ProviderInstance } from "../provider/ProviderDriver.ts"; -export type TextGenerationProvider = "codex" | "claudeAgent" | "cursor" | "opencode"; +export type TextGenerationProvider = "codex" | "claudeAgent" | "cursor" | "opencode" | "piAgent"; export interface CommitMessageGenerationInput { cwd: string; diff --git a/apps/web/index.html b/apps/web/index.html index 88e1c8b4f23..86d6dcb5391 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -89,7 +89,7 @@ href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300..800;1,9..40,300..800&display=swap" rel="stylesheet" /> - T3 Code (Alpha) + T3 Code (Bscholar)
diff --git a/apps/web/src/branding.ts b/apps/web/src/branding.ts index 5c1309ca06b..bbd09bb33a2 100644 --- a/apps/web/src/branding.ts +++ b/apps/web/src/branding.ts @@ -19,7 +19,7 @@ export const APP_BASE_NAME = injectedDesktopAppBranding?.baseName ?? "T3 Code"; export const APP_STAGE_LABEL = injectedDesktopAppBranding?.stageLabel ?? HOSTED_APP_CHANNEL_LABEL ?? - (import.meta.env.DEV ? "Dev" : "Alpha"); + (import.meta.env.DEV ? "Dev" : "Bscholar"); export const APP_DISPLAY_NAME = injectedDesktopAppBranding?.displayName ?? `${APP_BASE_NAME} (${APP_STAGE_LABEL})`; export const APP_VERSION = import.meta.env.APP_VERSION || "0.0.0"; diff --git a/apps/web/src/components/KeybindingsToast.browser.tsx b/apps/web/src/components/KeybindingsToast.browser.tsx index 611eaf572d0..b8a1da91696 100644 --- a/apps/web/src/components/KeybindingsToast.browser.tsx +++ b/apps/web/src/components/KeybindingsToast.browser.tsx @@ -130,6 +130,13 @@ function createBaseServerConfig(): ServerConfig { serverPassword: "", customModels: [], }, + piAgent: { + enabled: true, + binaryPath: "", + homePath: "", + customModels: [], + launchArgs: "", + }, }, }, }; diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 96fc34c1013..b19e1240364 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -727,9 +727,16 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) model: selectedModel, models: selectedProviderModels, prompt, - modelOptions: composerModelOptions?.[selectedProvider], + modelOptions: composerModelOptions?.[selectedInstanceId], }), - [composerModelOptions, prompt, selectedModel, selectedProvider, selectedProviderModels], + [ + composerModelOptions, + prompt, + selectedInstanceId, + selectedModel, + selectedProvider, + selectedProviderModels, + ], ); const selectedPromptEffort = composerProviderState.promptEffort; @@ -1018,21 +1025,23 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) const providerTraitsMenuContent = renderProviderTraitsMenuContent({ provider: selectedProvider, + instanceId: selectedInstanceId, ...(routeKind === "server" ? { threadRef: routeThreadRef } : {}), ...(routeKind === "draft" && draftId ? { draftId } : {}), model: selectedModel, models: selectedProviderModels, - modelOptions: composerModelOptions?.[selectedProvider], + modelOptions: composerModelOptions?.[selectedInstanceId], prompt, onPromptChange: setPromptFromTraits, }); const providerTraitsPicker = renderProviderTraitsPicker({ provider: selectedProvider, + instanceId: selectedInstanceId, ...(routeKind === "server" ? { threadRef: routeThreadRef } : {}), ...(routeKind === "draft" && draftId ? { draftId } : {}), model: selectedModel, models: selectedProviderModels, - modelOptions: composerModelOptions?.[selectedProvider], + modelOptions: composerModelOptions?.[selectedInstanceId], prompt, onPromptChange: setPromptFromTraits, }); diff --git a/apps/web/src/components/chat/ModelPickerContent.tsx b/apps/web/src/components/chat/ModelPickerContent.tsx index c3468ef8c65..ec68d5b444a 100644 --- a/apps/web/src/components/chat/ModelPickerContent.tsx +++ b/apps/web/src/components/chat/ModelPickerContent.tsx @@ -220,9 +220,9 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { ); const showLockedInstanceSidebar = isLocked && lockedInstanceEntries.length > 1; const showSidebar = !isSearching && (!isLocked || showLockedInstanceSidebar); - const sidebarInstanceEntries = showLockedInstanceSidebar - ? lockedInstanceEntries - : instanceEntries; + const sidebarInstanceEntries = ( + showLockedInstanceSidebar ? lockedInstanceEntries : instanceEntries + ).filter((entry) => entry.isAvailable && entry.status === "ready"); const instanceOrder = useMemo( () => instanceEntries.map((entry) => entry.instanceId), [instanceEntries], @@ -538,7 +538,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { onSelectInstance={handleSelectInstance} instanceEntries={sidebarInstanceEntries} showFavorites={!isLocked} - showComingSoon={!isLocked} + showComingSoon={false} /> )} diff --git a/apps/web/src/components/chat/TraitsPicker.tsx b/apps/web/src/components/chat/TraitsPicker.tsx index 62caa323fd3..76ff055a594 100644 --- a/apps/web/src/components/chat/TraitsPicker.tsx +++ b/apps/web/src/components/chat/TraitsPicker.tsx @@ -1,5 +1,6 @@ import { type ProviderDriverKind, + type ProviderInstanceId, type ProviderOptionDescriptor, type ProviderOptionSelection, type ScopedThreadRef, @@ -196,6 +197,7 @@ export function shouldRenderTraitsControls(input: { export interface TraitsMenuContentProps { provider: ProviderDriverKind; + instanceId?: ProviderInstanceId; models: ReadonlyArray; model: string | null | undefined; prompt: string; @@ -208,6 +210,7 @@ export interface TraitsMenuContentProps { export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ provider, + instanceId, models, model, prompt, @@ -228,11 +231,12 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ return; } setProviderModelOptions(threadTarget, provider, nextOptions, { + ...(instanceId ? { instanceId } : {}), model, persistSticky: true, }); }, - [model, persistence, provider, setProviderModelOptions], + [instanceId, model, persistence, provider, setProviderModelOptions], ); const { descriptors, @@ -343,6 +347,7 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ export const TraitsPicker = memo(function TraitsPicker({ provider, + instanceId, models, model, prompt, @@ -431,6 +436,7 @@ export const TraitsPicker = memo(function TraitsPicker({ > = { @@ -7,6 +7,7 @@ export const PROVIDER_ICON_BY_PROVIDER: Partial [ProviderDriverKind.make("claudeAgent")]: ClaudeAI, [ProviderDriverKind.make("opencode")]: OpenCodeIcon, [ProviderDriverKind.make("cursor")]: CursorIcon, + [ProviderDriverKind.make("piAgent")]: PiAgentIcon, }; function isAvailableProviderOption(option: (typeof PROVIDER_OPTIONS)[number]): option is { diff --git a/apps/web/src/components/settings/AddProviderInstanceDialog.tsx b/apps/web/src/components/settings/AddProviderInstanceDialog.tsx index affa35ff260..bf3edd5dd0d 100644 --- a/apps/web/src/components/settings/AddProviderInstanceDialog.tsx +++ b/apps/web/src/components/settings/AddProviderInstanceDialog.tsx @@ -13,7 +13,7 @@ import { useSettings, useUpdateSettings } from "../../hooks/useSettings"; import { cn } from "../../lib/utils"; import { normalizeProviderAccentColor } from "../../providerInstances"; import { Button } from "../ui/button"; -import { ACPRegistryIcon, Gemini, GithubCopilotIcon, PiAgentIcon, type Icon } from "../Icons"; +import { ACPRegistryIcon, Gemini, GithubCopilotIcon, type Icon } from "../Icons"; import { Dialog, DialogDescription, @@ -86,11 +86,6 @@ const COMING_SOON_DRIVER_OPTIONS: readonly ComingSoonDriverOption[] = [ label: "ACP Registry", icon: ACPRegistryIcon, }, - { - value: ProviderDriverKind.make("piAgent"), - label: "Pi Agent", - icon: PiAgentIcon, - }, ]; /** diff --git a/apps/web/src/components/settings/providerDriverMeta.ts b/apps/web/src/components/settings/providerDriverMeta.ts index 8d3d7482f62..e10d7c48d7f 100644 --- a/apps/web/src/components/settings/providerDriverMeta.ts +++ b/apps/web/src/components/settings/providerDriverMeta.ts @@ -3,10 +3,11 @@ import { CodexSettings, CursorSettings, OpenCodeSettings, + PiSettings, ProviderDriverKind, } from "@t3tools/contracts"; import type * as Schema from "effect/Schema"; -import { ClaudeAI, CursorIcon, type Icon, OpenAI, OpenCodeIcon } from "../Icons"; +import { ClaudeAI, CursorIcon, type Icon, OpenAI, OpenCodeIcon, PiAgentIcon } from "../Icons"; type ProviderSettingsSchema = { readonly fields: Readonly>; @@ -59,6 +60,12 @@ export const PROVIDER_CLIENT_DEFINITIONS: readonly ProviderClientDefinition[] = icon: OpenCodeIcon, settingsSchema: OpenCodeSettings, }, + { + value: ProviderDriverKind.make("piAgent"), + label: "Pi Agent", + icon: PiAgentIcon, + settingsSchema: PiSettings, + }, ]; export const PROVIDER_CLIENT_DEFINITION_BY_VALUE: Partial< diff --git a/apps/web/src/composerDraftStore.test.ts b/apps/web/src/composerDraftStore.test.ts index 67f754bd715..4004567dd81 100644 --- a/apps/web/src/composerDraftStore.test.ts +++ b/apps/web/src/composerDraftStore.test.ts @@ -21,6 +21,7 @@ import { createModelSelection } from "@t3tools/shared/model"; // `stickyModelSelectionByProvider` maps are keyed by `ProviderInstanceId` // in production; these aliases keep the legacy-key migration tests concise. const CODEX_INSTANCE = ProviderInstanceId.make("codex"); +const CODEX_SECONDARY_INSTANCE = ProviderInstanceId.make("codex_secondary"); const CLAUDE_AGENT_INSTANCE = ProviderInstanceId.make("claudeAgent"); const CURSOR_INSTANCE = ProviderInstanceId.make("cursor"); const CODEX_DRIVER = ProviderDriverKind.make("codex"); @@ -1201,6 +1202,43 @@ describe("composerDraftStore modelSelection", () => { ); }); + it("stores provider option changes on a selected custom instance", () => { + const store = useComposerDraftStore.getState(); + + store.setProviderModelOptions( + threadRef, + CODEX_DRIVER, + toSelections({ reasoningEffort: "low" }), + { + instanceId: CODEX_SECONDARY_INSTANCE, + model: "gpt-5-codex", + persistSticky: true, + }, + ); + + expect( + draftFor(threadId, TEST_ENVIRONMENT_ID)?.modelSelectionByProvider[CODEX_SECONDARY_INSTANCE], + ).toEqual( + expect.objectContaining({ + instanceId: CODEX_SECONDARY_INSTANCE, + options: [{ id: "reasoningEffort", value: "low" }], + }), + ); + expect(draftFor(threadId, TEST_ENVIRONMENT_ID)?.activeProvider).toBe(CODEX_SECONDARY_INSTANCE); + expect(useComposerDraftStore.getState().stickyActiveProvider).toBe(CODEX_SECONDARY_INSTANCE); + expect(useComposerDraftStore.getState().stickyModelSelectionByProvider[CODEX_INSTANCE]).toBe( + undefined, + ); + expect( + useComposerDraftStore.getState().stickyModelSelectionByProvider[CODEX_SECONDARY_INSTANCE], + ).toEqual( + expect.objectContaining({ + instanceId: CODEX_SECONDARY_INSTANCE, + options: [{ id: "reasoningEffort", value: "low" }], + }), + ); + }); + it("updates only the draft when sticky persistence is disabled", () => { const store = useComposerDraftStore.getState(); diff --git a/apps/web/src/composerDraftStore.ts b/apps/web/src/composerDraftStore.ts index d554ad7b0d3..5c90197a406 100644 --- a/apps/web/src/composerDraftStore.ts +++ b/apps/web/src/composerDraftStore.ts @@ -371,6 +371,7 @@ interface ComposerDraftStoreState { provider: ProviderDriverKind, nextProviderOptions: ReadonlyArray | null | undefined, options?: { + instanceId?: ProviderInstanceId | null | undefined; model?: string | null | undefined; persistSticky?: boolean; }, @@ -2456,7 +2457,7 @@ const composerDraftStore = create()( if (normalizedProvider === null) { return; } - const instanceKey = defaultInstanceIdForDriver(normalizedProvider); + const instanceKey = options?.instanceId ?? defaultInstanceIdForDriver(normalizedProvider); const fallbackModel = normalizeModelSlug(options?.model, normalizedProvider) ?? DEFAULT_MODEL_BY_PROVIDER[normalizedProvider] ?? @@ -2501,7 +2502,9 @@ const composerDraftStore = create()( const { options: _, ...rest } = stickyBase; nextStickyMap[instanceKey] = rest as ModelSelection; } - nextStickyActiveProvider = base.activeProvider ?? instanceKey; + nextStickyActiveProvider = options.instanceId + ? instanceKey + : (base.activeProvider ?? instanceKey); } if ( @@ -2514,6 +2517,7 @@ const composerDraftStore = create()( const nextDraft: ComposerThreadDraftState = { ...base, + ...(options?.instanceId ? { activeProvider: instanceKey } : {}), modelSelectionByProvider: nextMap, }; const nextDraftsByThreadKey = { ...state.draftsByThreadKey }; diff --git a/apps/web/src/modelSelection.ts b/apps/web/src/modelSelection.ts index 8ba0c8fa5d9..84dc28306d7 100644 --- a/apps/web/src/modelSelection.ts +++ b/apps/web/src/modelSelection.ts @@ -25,7 +25,7 @@ import { sortModelsForProviderInstance } from "./modelOrdering"; const MAX_CUSTOM_MODEL_COUNT = 32; export const MAX_CUSTOM_MODEL_LENGTH = 256; -const DEFAULT_TEXT_GENERATION_INSTANCE_ID = ProviderInstanceId.make("codex"); +const DEFAULT_TEXT_GENERATION_INSTANCE_ID = ProviderInstanceId.make("claudeAgent"); /** * Resolve the custom-model list for a given instance, preferring the diff --git a/bun.lock b/bun.lock index ffc4a5922bd..671983dd24f 100644 --- a/bun.lock +++ b/bun.lock @@ -17,7 +17,7 @@ }, "apps/desktop": { "name": "@t3tools/desktop", - "version": "0.0.23", + "version": "0.0.24", "dependencies": { "@effect/platform-node": "catalog:", "@t3tools/contracts": "workspace:*", @@ -50,12 +50,13 @@ }, "apps/server": { "name": "t3", - "version": "0.0.23", + "version": "0.0.24", "bin": { "t3": "./dist/bin.mjs", }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.111", + "@earendil-works/pi-coding-agent": "^0.75.4", "@effect/platform-bun": "catalog:", "@effect/platform-node": "catalog:", "@effect/platform-node-shared": "catalog:", @@ -83,7 +84,7 @@ }, "apps/web": { "name": "@t3tools/web", - "version": "0.0.23", + "version": "0.0.24", "dependencies": { "@base-ui/react": "^1.4.1", "@dnd-kit/core": "^6.3.1", @@ -165,7 +166,7 @@ }, "packages/contracts": { "name": "@t3tools/contracts", - "version": "0.0.23", + "version": "0.0.24", "dependencies": { "effect": "catalog:", }, @@ -322,6 +323,56 @@ "@astrojs/yaml2ts": ["@astrojs/yaml2ts@0.2.3", "", { "dependencies": { "yaml": "^2.8.2" } }, "sha512-PJzRmgQzUxI2uwpdX2lXSHtP4G8ocp24/t+bZyf5Fy0SZLSF9f9KXZoMlFM/XCGue+B0nH/2IZ7FpBYQATBsCg=="], + "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], + + "@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="], + + "@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], + + "@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="], + + "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], + + "@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.1048.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.11", "@aws-sdk/credential-provider-node": "^3.972.42", "@aws-sdk/eventstream-handler-node": "^3.972.16", "@aws-sdk/middleware-eventstream": "^3.972.12", "@aws-sdk/middleware-websocket": "^3.972.19", "@aws-sdk/token-providers": "3.1048.0", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/fetch-http-handler": "^5.4.2", "@smithy/node-http-handler": "^4.7.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ=="], + + "@aws-sdk/core": ["@aws-sdk/core@3.974.12", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws-sdk/xml-builder": "^3.972.24", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.2", "@smithy/signature-v4": "^5.4.2", "@smithy/types": "^4.14.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-qrqgioqYFjwR6LatVNS1L2Vk++EwRIxqSQXPKNv5Ofux2D8UNgqMQ1znnMyEImXquVPTtbf71fc128pvmU6y9A=="], + + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.38", "", { "dependencies": { "@aws-sdk/core": "^3.974.12", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-m3WjZEgPtioMhPmwqUt+DhlTJ2i9ufR6DhfkyXojb9puEvfR+ur2U5shavu5/Cc9WHHsDCvALi6UFHgcqjhQ5w=="], + + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.40", "", { "dependencies": { "@aws-sdk/core": "^3.974.12", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/fetch-http-handler": "^5.4.2", "@smithy/node-http-handler": "^4.7.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-D78L/m2Dr6cJnnSvWoAudPhQmCwmJ7j6APXsPYmFpPaKfQTfCSu0rdm8j14Np+VmXF9z8Aj8HE3xFpsrwtfgeg=="], + + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.42", "", { "dependencies": { "@aws-sdk/core": "^3.974.12", "@aws-sdk/credential-provider-env": "^3.972.38", "@aws-sdk/credential-provider-http": "^3.972.40", "@aws-sdk/credential-provider-login": "^3.972.42", "@aws-sdk/credential-provider-process": "^3.972.38", "@aws-sdk/credential-provider-sso": "^3.972.42", "@aws-sdk/credential-provider-web-identity": "^3.972.42", "@aws-sdk/nested-clients": "^3.997.10", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/credential-provider-imds": "^4.3.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-Mu5ESvFXeinafVM8jTIvRqcvK2Ehj4kz3auT39yUcHwu1Vfxo6xRlmUafdKLW4tusjAJukQwK09sCSMgOm7OKg=="], + + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.42", "", { "dependencies": { "@aws-sdk/core": "^3.974.12", "@aws-sdk/nested-clients": "^3.997.10", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-O6WkZga3kf0yqyJYd1dbeJqVhEgJx/x1UaLgtbR+XuL/YP+K5y6QTxQKL7ka9z3jnQASESKGAPnRyt4D5hQrxA=="], + + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.43", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.38", "@aws-sdk/credential-provider-http": "^3.972.40", "@aws-sdk/credential-provider-ini": "^3.972.42", "@aws-sdk/credential-provider-process": "^3.972.38", "@aws-sdk/credential-provider-sso": "^3.972.42", "@aws-sdk/credential-provider-web-identity": "^3.972.42", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/credential-provider-imds": "^4.3.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-D/DJmbrWRP5BXEO3FH+ar4el+2n6OlGofiud7dQun2jES+AQEJjczenp1jBb4MBN7CpGpS8nsWGQLtuzc9tQbA=="], + + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.38", "", { "dependencies": { "@aws-sdk/core": "^3.974.12", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-EnbYVajGgbkb24s0K1eo4VNAPV5mHIET7LSvirTaFCwkfrfaOJxtSE+wY/tJdKDS21cEYkZs2ruCaAm+W4iblg=="], + + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.42", "", { "dependencies": { "@aws-sdk/core": "^3.974.12", "@aws-sdk/nested-clients": "^3.997.10", "@aws-sdk/token-providers": "3.1049.0", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-RVV/9NbFwI8ZHEH5dn39lGyFmSbSVj1+orZdr6QsOe1mW9DCglmlen0cFaNZmCcqkqc7erNRHNBduxbeZuHAnw=="], + + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.42", "", { "dependencies": { "@aws-sdk/core": "^3.974.12", "@aws-sdk/nested-clients": "^3.997.10", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-/67fXX0ddllD4u2Nujc5PvT4byHgpMUfz6+RxIKi/0nFIckeorm7JvXgzBuDyVKw0s58EbofmETDWUf9vTEuHQ=="], + + "@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.972.16", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-yedpPgKftqjU5SlPFHfqWpOw6xSCRieWRG1euWOlXn4WJxt2VX92VprCa2PpSOXjVCAeK6dTjW9eJRXVig9yGA=="], + + "@aws-sdk/middleware-eventstream": ["@aws-sdk/middleware-eventstream@3.972.12", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-tHTHHCHNrq6XklQvlzHBDJG4Iuhh7NVPRdtmvP+nHFA+5sxPlIDzlAHHgfoYHGvT3NXP1yVP/L5c3opUn6T3Qg=="], + + "@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.972.20", "", { "dependencies": { "@aws-sdk/core": "^3.974.12", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/fetch-http-handler": "^5.4.2", "@smithy/signature-v4": "^5.4.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-LM6P0i+Lu6pi25oNw2nqxjRxiEOtLgPB7xIvHfa+FxHTRLg8wcgqu3qg2COl4QaT7Es2yCxYdeRLVYazKAwL8g=="], + + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.10", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.12", "@aws-sdk/signature-v4-multi-region": "^3.996.27", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/fetch-http-handler": "^5.4.2", "@smithy/node-http-handler": "^4.7.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-FtQ/Bt327peZJuyo4WZSOLVUTw9ujRxntepiC7L65FxA2P82Xlq0g14T22BuqBUeMjDoxa9nvwiMHjLIfP3eUg=="], + + "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.27", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/signature-v4": "^5.4.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-0Phbz4t6HI3D3skxvG2uI+VWU034/nSIw1T8d+FPzzQG9EQTrw94o9mOKO2Gv3n3Oc8P7JD7RAUxkoneLWv5Eg=="], + + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1048.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/nested-clients": "^3.997.9", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA=="], + + "@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], + + "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.5", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ=="], + + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.24", "", { "dependencies": { "@nodable/entities": "2.1.0", "@smithy/types": "^4.14.1", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-V8z5YcDPfsvzrBlj0xR1vhRtocblhYbqdreCJB/voGd4Sr5zjNAeWxexbnqVtskTJe0vFb5KMqbSL++ePl+zRw=="], + + "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.4", "", {}, "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ=="], + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], @@ -384,6 +435,14 @@ "@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="], + "@earendil-works/pi-agent-core": ["@earendil-works/pi-agent-core@0.75.4", "", { "dependencies": { "@earendil-works/pi-ai": "^0.75.4", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" } }, "sha512-cGYbysb4EqUf0B28OeqFq2ppm1XF3bYBOP71q9dv38yf/UJfzMjiXBeNelrcio+QWIoVrW+xzYm7sMzYIUc9Og=="], + + "@earendil-works/pi-ai": ["@earendil-works/pi-ai@0.75.4", "", { "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", "@google/genai": "1.52.0", "@mistralai/mistralai": "2.2.1", "http-proxy-agent": "7.0.2", "https-proxy-agent": "7.0.6", "openai": "6.26.0", "partial-json": "0.1.7", "typebox": "1.1.38" }, "bin": { "pi-ai": "dist/cli.js" } }, "sha512-m/w8Hh3vQ0rAycwJiJWdzkypkn4295f4eq/966lDRy8aX5sk6bgYXH8TQmL16TO7Uwc7MbJG0QoyFHgX8RqXUQ=="], + + "@earendil-works/pi-coding-agent": ["@earendil-works/pi-coding-agent@0.75.4", "", { "dependencies": { "@earendil-works/pi-agent-core": "^0.75.4", "@earendil-works/pi-ai": "^0.75.4", "@earendil-works/pi-tui": "^0.75.4", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", "diff": "8.0.4", "glob": "13.0.6", "highlight.js": "10.7.3", "hosted-git-info": "9.0.3", "ignore": "7.0.5", "jiti": "2.7.0", "minimatch": "10.2.5", "proper-lockfile": "4.1.2", "typebox": "1.1.38", "undici": "8.3.0", "yaml": "2.9.0" }, "optionalDependencies": { "@mariozechner/clipboard": "0.3.6" }, "bin": { "pi": "dist/cli.js" } }, "sha512-Fb+FRo08b5H9pYKbQJ708/5OKL0+K/yclhfCMEhrBzSPTZZ4c85nY1YsBo4qwL20ohBMlBezHMRuHzcJ1ylEoQ=="], + + "@earendil-works/pi-tui": ["@earendil-works/pi-tui@0.75.4", "", { "dependencies": { "get-east-asian-width": "1.6.0", "marked": "15.0.12" }, "optionalDependencies": { "koffi": "2.16.2" } }, "sha512-PDhKU7u6fmEcvHUFHzrRwGc/Ytokj/hO+X4RPf+MWKEGpvg3B1vHv88Ee+Dy33004tYkQF5YeXV4btJZcp5x1g=="], + "@effect/atom-react": ["@effect/atom-react@4.0.0-beta.59", "", { "peerDependencies": { "effect": "^4.0.0-beta.59", "react": "^19.2.4", "scheduler": "*" } }, "sha512-VkznQz5c+Z/BLxX+hQNPzPOyUnLQjnbppFSNP7tbPru7HKR4ihzCDC6Xjbx87156MOrZ+JOa6shTMbmvGT5W0w=="], "@effect/language-service": ["@effect/language-service@0.84.2", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-l04qNxpiA8rY5yXWckRPJ7Mk5MNerXuNymSFf+IdflfI5i8jgL1bpBNLuP6ijg7wgjdHc/KmTnCj2kT0SCntuA=="], @@ -486,6 +545,8 @@ "@formkit/auto-animate": ["@formkit/auto-animate@0.9.0", "", {}, "sha512-VhP4zEAacXS3dfTpJpJ88QdLqMTcabMg0jwpOSxZ/VzfQVfl3GkZSCZThhGC5uhq/TxPHPzW0dzr4H9Bb1OgKA=="], + "@google/genai": ["@google/genai@1.52.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q=="], + "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], @@ -606,6 +667,30 @@ "@lexical/yjs": ["@lexical/yjs@0.41.0", "", { "dependencies": { "@lexical/offset": "0.41.0", "@lexical/selection": "0.41.0", "lexical": "0.41.0" }, "peerDependencies": { "yjs": ">=13.5.22" } }, "sha512-PaKTxSbVC4fpqUjQ7vUL9RkNF1PjL8TFl5jRe03PqoPYpE33buf3VXX6+cOUEfv9+uknSqLCPHoBS/4jN3a97w=="], + "@mariozechner/clipboard": ["@mariozechner/clipboard@0.3.6", "", { "optionalDependencies": { "@mariozechner/clipboard-darwin-arm64": "0.3.6", "@mariozechner/clipboard-darwin-universal": "0.3.6", "@mariozechner/clipboard-darwin-x64": "0.3.6", "@mariozechner/clipboard-linux-arm64-gnu": "0.3.6", "@mariozechner/clipboard-linux-arm64-musl": "0.3.6", "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.6", "@mariozechner/clipboard-linux-x64-gnu": "0.3.6", "@mariozechner/clipboard-linux-x64-musl": "0.3.6", "@mariozechner/clipboard-win32-arm64-msvc": "0.3.6", "@mariozechner/clipboard-win32-x64-msvc": "0.3.6" } }, "sha512-MXdtr+6+ntlIVHdrZYuZNQydu6o8yZswFJ2Ln81j2O/Y9B/LDHvEaIm95xWNPkjGTWriSOeLnQJRFs6dYb60bg=="], + + "@mariozechner/clipboard-darwin-arm64": ["@mariozechner/clipboard-darwin-arm64@0.3.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HjaisYCAbHi/1+N1yDAQHc8ZXGffufIUT5NSOSVR3f3AuMDusxTtnbK8tZ7JFDkShua1oNGZoNwQHsc8MPtE0Q=="], + + "@mariozechner/clipboard-darwin-universal": ["@mariozechner/clipboard-darwin-universal@0.3.6", "", { "os": "darwin" }, "sha512-8BWtPjOtJOJoykml3w0fx0zRrfWP31mXrJwfoA7xzNprkZw1uolCNfgmjDiVBseoKjp16EGITz7bN+61qn8dWA=="], + + "@mariozechner/clipboard-darwin-x64": ["@mariozechner/clipboard-darwin-x64@0.3.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-p9syiZD1kU4I+1ya7f7g+zD1GiUvR8fdlRlNmgsZNWlyjtc8rlV2EjTLd/35x1LsdBq020GVvtzp0ZmPgBI09Q=="], + + "@mariozechner/clipboard-linux-arm64-gnu": ["@mariozechner/clipboard-linux-arm64-gnu@0.3.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-5JFf5rGofrm+V29HNF+wLthXphHdQpMbKDUYJ5tML6/Z5DLlLOV/9Ak4kDPtYyZ+Dzf+kAusE0VsFg4+tfP1IA=="], + + "@mariozechner/clipboard-linux-arm64-musl": ["@mariozechner/clipboard-linux-arm64-musl@0.3.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-JlVjxxw0GbGC0djXYWRIqyteO3J1KZ/QG3udlEFaOD5TLOM1FnmXXAPDQBqr+aBVr720ef9K00dirYnJ0LDCtw=="], + + "@mariozechner/clipboard-linux-riscv64-gnu": ["@mariozechner/clipboard-linux-riscv64-gnu@0.3.6", "", { "os": "linux", "cpu": "none" }, "sha512-4t8BUi5zZ+L77otFQVnVSlaTyAX4TVk9EqQm4syMrEQp96trFEHEwwNHcNEBGzYv5+K7mxay50TthYkz47OWzQ=="], + + "@mariozechner/clipboard-linux-x64-gnu": ["@mariozechner/clipboard-linux-x64-gnu@0.3.6", "", { "os": "linux", "cpu": "x64" }, "sha512-trtPwcNLW37irwQCJLtCxLw757jjJZk3TSnY/MU9bhtWtA3K9b/eLW0e4RGhUXDoFRds9opNWWaUDuFLa8dm0w=="], + + "@mariozechner/clipboard-linux-x64-musl": ["@mariozechner/clipboard-linux-x64-musl@0.3.6", "", { "os": "linux", "cpu": "x64" }, "sha512-WfnzIvOCCWQiN0MmltCEo6cLceUDbYe+I7xyFZjaps5A+2Op/M2CY7Rey+C4ucQhrvmpoHmTSFgY9ODWk7snoA=="], + + "@mariozechner/clipboard-win32-arm64-msvc": ["@mariozechner/clipboard-win32-arm64-msvc@0.3.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-+8+1aHYsBPUjmW3otmWlg+Hijt0iJvoBBs5e0mxFeUd4gDaKMB8Bn6x7c6KVtscg7E5j5NFXnwQqNSIAO4p8zQ=="], + + "@mariozechner/clipboard-win32-x64-msvc": ["@mariozechner/clipboard-win32-x64-msvc@0.3.6", "", { "os": "win32", "cpu": "x64" }, "sha512-S4xfPmERC8ZkiLHe3vekZCjdDwNEETCuvCgQK2kP6/TnvmUkq1y2Pk+DjM4t8uh9KMX9bH4zs5ePcKa8GTXmfg=="], + + "@mistralai/mistralai": ["@mistralai/mistralai@2.2.1", "", { "dependencies": { "ws": "^8.18.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.25.0" } }, "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="], @@ -624,6 +709,8 @@ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], + "@nodable/entities": ["@nodable/entities@2.1.0", "", {}, "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA=="], + "@open-draft/deferred-promise": ["@open-draft/deferred-promise@2.2.0", "", {}, "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA=="], "@open-draft/logger": ["@open-draft/logger@0.3.0", "", { "dependencies": { "is-node-process": "^1.2.0", "outvariant": "^1.4.0" } }, "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ=="], @@ -724,6 +811,26 @@ "@preact/signals-core": ["@preact/signals-core@1.14.0", "", {}, "sha512-AowtCcCU/33lFlh1zRFf/u+12rfrhtNakj7UpaGEsmMwUKpKWMVvcktOGcwBBNiB4lWrZWc01LhiyyzVklJyaQ=="], + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/inquire": ["@protobufjs/inquire@1.1.2", "", {}, "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.1", "", {}, "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg=="], + "@quansync/fs": ["@quansync/fs@1.0.0", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="], "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.3", "", { "os": "android", "cpu": "arm64" }, "sha512-0T1k9FinuBZ/t7rZ8jN6OpUKPnUjNdYHoj/cESWrQ3ZraAJ4OMm6z7QjSfCxqj8mOp9kTKc1zHK3kGz5vMu+nQ=="], @@ -780,8 +887,28 @@ "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + "@silvia-odwyer/photon-node": ["@silvia-odwyer/photon-node@0.3.4", "", {}, "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA=="], + "@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="], + "@smithy/core": ["@smithy/core@3.24.3", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg=="], + + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.3.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w=="], + + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.4.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A=="], + + "@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA=="], + + "@smithy/signature-v4": ["@smithy/signature-v4@5.4.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g=="], + + "@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + + "@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="], @@ -910,6 +1037,8 @@ "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="], + "@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="], + "@types/statuses": ["@types/statuses@2.0.6", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], @@ -972,6 +1101,8 @@ "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="], @@ -1008,8 +1139,14 @@ "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.8", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ=="], + "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], "birpc": ["birpc@4.0.0", "", {}, "sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw=="], @@ -1020,12 +1157,18 @@ "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="], + "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], + + "brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + "builder-util-runtime": ["builder-util-runtime@9.5.1", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ=="], "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], @@ -1048,6 +1191,8 @@ "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], @@ -1110,6 +1255,8 @@ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], @@ -1158,6 +1305,8 @@ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], "effect": ["effect@4.0.0-beta.59", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-xyUDLeHSe8d6lWGOvR6Fgn2HL6gYeTZ/S4Jzk9uc4ZUxMPPsNZlNXrvk0C7/utQFzeX7uAWcVnG2BjbA0SRoAA=="], @@ -1238,10 +1387,16 @@ "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + "fast-xml-builder": ["fast-xml-builder@1.2.0", "", { "dependencies": { "path-expression-matcher": "^1.5.0", "xml-naming": "^0.1.0" } }, "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q=="], + + "fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="], + "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], @@ -1254,6 +1409,8 @@ "fontkitten": ["fontkitten@1.0.3", "", { "dependencies": { "tiny-inflate": "^1.0.3" } }, "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw=="], + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], @@ -1264,10 +1421,16 @@ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "gaxios": ["gaxios@7.1.4", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA=="], + + "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], + "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-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], @@ -1278,12 +1441,18 @@ "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], + "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "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=="], "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + "google-auth-library": ["google-auth-library@10.6.2", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw=="], + + "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], "got": ["got@11.8.6", "", { "dependencies": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", "@types/cacheable-request": "^6.0.1", "@types/responselike": "^1.0.0", "cacheable-lookup": "^5.0.3", "cacheable-request": "^7.0.2", "decompress-response": "^6.0.0", "http2-wrapper": "^1.0.0-beta.5.2", "lowercase-keys": "^2.0.0", "p-cancelable": "^2.0.0", "responselike": "^2.0.0" } }, "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g=="], @@ -1324,10 +1493,14 @@ "headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="], + "highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="], + "hono": ["hono@4.12.14", "", {}, "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w=="], "hookable": ["hookable@6.1.0", "", {}, "sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw=="], + "hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="], + "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="], "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], @@ -1338,10 +1511,16 @@ "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + "http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="], + "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=="], + "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + "import-without-cache": ["import-without-cache@0.2.5", "", {}, "sha512-B6Lc2s6yApwnD2/pMzFh/d5AVjdsDXjgkeJ766FmFuJELIGHNycKRj+l3A39yZPM4CchqNCB4RITEAYB1KUM6A=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], @@ -1404,6 +1583,8 @@ "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], "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=="], @@ -1420,10 +1601,16 @@ "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], + + "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + "koffi": ["koffi@2.16.2", "", {}, "sha512-owU0MRwv6xkrVqCd+33uw6BaYppkTRXbO/rVdJNI2dvZG0gzyRhYwW25eWtc5pauwK8TGh3AbkFONSezdykfSA=="], + "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="], "lazy-val": ["lazy-val@1.0.5", "", {}, "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="], @@ -1464,6 +1651,8 @@ "lodash.isequal": ["lodash.isequal@4.5.0", "", {}, "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="], + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="], @@ -1480,6 +1669,8 @@ "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + "marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="], + "matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], @@ -1586,6 +1777,10 @@ "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -1612,6 +1807,10 @@ "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], @@ -1648,6 +1847,8 @@ "oniguruma-to-es": ["oniguruma-to-es@4.3.5", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ=="], + "openai": ["openai@6.26.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA=="], + "outvariant": ["outvariant@1.4.3", "", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="], "oxfmt": ["oxfmt@0.40.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.40.0", "@oxfmt/binding-android-arm64": "0.40.0", "@oxfmt/binding-darwin-arm64": "0.40.0", "@oxfmt/binding-darwin-x64": "0.40.0", "@oxfmt/binding-freebsd-x64": "0.40.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.40.0", "@oxfmt/binding-linux-arm-musleabihf": "0.40.0", "@oxfmt/binding-linux-arm64-gnu": "0.40.0", "@oxfmt/binding-linux-arm64-musl": "0.40.0", "@oxfmt/binding-linux-ppc64-gnu": "0.40.0", "@oxfmt/binding-linux-riscv64-gnu": "0.40.0", "@oxfmt/binding-linux-riscv64-musl": "0.40.0", "@oxfmt/binding-linux-s390x-gnu": "0.40.0", "@oxfmt/binding-linux-x64-gnu": "0.40.0", "@oxfmt/binding-linux-x64-musl": "0.40.0", "@oxfmt/binding-openharmony-arm64": "0.40.0", "@oxfmt/binding-win32-arm64-msvc": "0.40.0", "@oxfmt/binding-win32-ia32-msvc": "0.40.0", "@oxfmt/binding-win32-x64-msvc": "0.40.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-g0C3I7xUj4b4DcagevM9kgH6+pUHytikxUcn3/VUkvzTNaaXBeyZqb7IBsHwojeXm4mTBEC/aBjBTMVUkZwWUQ=="], @@ -1660,6 +1861,8 @@ "p-queue": ["p-queue@9.1.0", "", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^7.0.0" } }, "sha512-O/ZPaXuQV29uSLbxWBGGZO1mCQXV2BLIwUr59JUU9SoH76mnYvtms7aafH/isNSNGwuEfP6W/4xD0/TJXxrizw=="], + "p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="], + "p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="], "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], @@ -1672,10 +1875,16 @@ "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + "partial-json": ["partial-json@0.1.7", "", {}, "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA=="], + "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], + "path-expression-matcher": ["path-expression-matcher@1.5.0", "", {}, "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], + "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], "path-to-regexp-updated": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], @@ -1708,8 +1917,12 @@ "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], + "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + "protobufjs": ["protobufjs@7.6.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.0", "@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-LtESOsMPTZgyYtwxhvdgdjGL0HmXEaRA/hVD6sol4zA60hVXXXP/SGmxnqDbgGE8gy7pYex7cym+5vYPcmaXBQ=="], + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], @@ -1792,6 +2005,8 @@ "retext-stringify": ["retext-stringify@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unified": "^11.0.0" } }, "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA=="], + "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], + "rettime": ["rettime@0.10.1", "", {}, "sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw=="], "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=="], @@ -1802,6 +2017,8 @@ "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "sax": ["sax@1.5.0", "", {}, "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA=="], @@ -1842,7 +2059,7 @@ "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], @@ -1874,6 +2091,8 @@ "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="], + "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], @@ -1964,6 +2183,8 @@ "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + "typebox": ["typebox@1.1.38", "", {}, "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA=="], + "typesafe-path": ["typesafe-path@0.2.2", "", {}, "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], @@ -2076,6 +2297,8 @@ "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], @@ -2090,6 +2313,8 @@ "ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], + "xml-naming": ["xml-naming@0.1.0", "", {}, "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw=="], + "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], @@ -2124,6 +2349,8 @@ "@astrojs/yaml2ts/yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], + "@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1049.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.12", "@aws-sdk/nested-clients": "^3.997.10", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-r7+d0lQMTHKypkmaF5jRTBYLYHCUHzt3gaVoN9SidLhQeWhCmHk3AKrboDTpPF5b7Pt7vKu3+oeMjznM2Eu1ow=="], + "@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], "@babel/core/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], @@ -2148,10 +2375,24 @@ "@base-ui/utils/@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], + "@earendil-works/pi-agent-core/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + + "@earendil-works/pi-ai/@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.91.1", "", { "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-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw=="], + + "@earendil-works/pi-coding-agent/diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], + + "@earendil-works/pi-coding-agent/jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], + + "@earendil-works/pi-coding-agent/undici": ["undici@8.3.0", "", {}, "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q=="], + + "@earendil-works/pi-coding-agent/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + "@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], "@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], "@pierre/diffs/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], @@ -2216,6 +2457,8 @@ "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "p-retry/retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], diff --git a/packages/contracts/src/auth.ts b/packages/contracts/src/auth.ts index 8439d12b069..f764f79f2b0 100644 --- a/packages/contracts/src/auth.ts +++ b/packages/contracts/src/auth.ts @@ -109,7 +109,7 @@ export const AuthBootstrapResult = Schema.Struct({ authenticated: Schema.Literal(true), role: AuthSessionRole, sessionMethod: ServerAuthSessionMethod, - expiresAt: Schema.DateTimeUtc, + expiresAt: Schema.DateTimeUtcFromString, }); export type AuthBootstrapResult = typeof AuthBootstrapResult.Type; @@ -117,14 +117,14 @@ export const AuthBearerBootstrapResult = Schema.Struct({ authenticated: Schema.Literal(true), role: AuthSessionRole, sessionMethod: Schema.Literal("bearer-session-token"), - expiresAt: Schema.DateTimeUtc, + expiresAt: Schema.DateTimeUtcFromString, sessionToken: TrimmedNonEmptyString, }); export type AuthBearerBootstrapResult = typeof AuthBearerBootstrapResult.Type; export const AuthWebSocketTokenResult = Schema.Struct({ token: TrimmedNonEmptyString, - expiresAt: Schema.DateTimeUtc, + expiresAt: Schema.DateTimeUtcFromString, }); export type AuthWebSocketTokenResult = typeof AuthWebSocketTokenResult.Type; @@ -132,7 +132,7 @@ export const AuthPairingCredentialResult = Schema.Struct({ id: TrimmedNonEmptyString, credential: TrimmedNonEmptyString, label: Schema.optionalKey(TrimmedNonEmptyString), - expiresAt: Schema.DateTimeUtc, + expiresAt: Schema.DateTimeUtcFromString, }); export type AuthPairingCredentialResult = typeof AuthPairingCredentialResult.Type; @@ -142,8 +142,8 @@ export const AuthPairingLink = Schema.Struct({ role: AuthSessionRole, subject: TrimmedNonEmptyString, label: Schema.optionalKey(TrimmedNonEmptyString), - createdAt: Schema.DateTimeUtc, - expiresAt: Schema.DateTimeUtc, + createdAt: Schema.DateTimeUtcFromString, + expiresAt: Schema.DateTimeUtcFromString, }); export type AuthPairingLink = typeof AuthPairingLink.Type; @@ -172,9 +172,9 @@ export const AuthClientSession = Schema.Struct({ role: AuthSessionRole, method: ServerAuthSessionMethod, client: AuthClientMetadata, - issuedAt: Schema.DateTimeUtc, - expiresAt: Schema.DateTimeUtc, - lastConnectedAt: Schema.NullOr(Schema.DateTimeUtc), + issuedAt: Schema.DateTimeUtcFromString, + expiresAt: Schema.DateTimeUtcFromString, + lastConnectedAt: Schema.NullOr(Schema.DateTimeUtcFromString), connected: Schema.Boolean, current: Schema.Boolean, }); @@ -261,6 +261,6 @@ export const AuthSessionState = Schema.Struct({ auth: ServerAuthDescriptor, role: Schema.optionalKey(AuthSessionRole), sessionMethod: Schema.optionalKey(ServerAuthSessionMethod), - expiresAt: Schema.optionalKey(Schema.DateTimeUtc), + expiresAt: Schema.optionalKey(Schema.DateTimeUtcFromString), }); export type AuthSessionState = typeof AuthSessionState.Type; diff --git a/packages/contracts/src/ipc.ts b/packages/contracts/src/ipc.ts index c0515aa9b26..b191b35851c 100644 --- a/packages/contracts/src/ipc.ts +++ b/packages/contracts/src/ipc.ts @@ -119,7 +119,7 @@ export type DesktopUpdateStatus = export type DesktopRuntimeArch = "arm64" | "x64" | "other"; export type DesktopTheme = "light" | "dark" | "system"; export type DesktopUpdateChannel = "latest" | "nightly"; -export type DesktopAppStageLabel = "Alpha" | "Dev" | "Nightly"; +export type DesktopAppStageLabel = "Bscholar" | "Dev" | "Nightly"; export const DesktopUpdateStatusSchema = Schema.Literals([ "disabled", @@ -134,7 +134,7 @@ export const DesktopUpdateStatusSchema = Schema.Literals([ export const DesktopRuntimeArchSchema = Schema.Literals(["arm64", "x64", "other"]); export const DesktopThemeSchema = Schema.Literals(["light", "dark", "system"]); export const DesktopUpdateChannelSchema = Schema.Literals(["latest", "nightly"]); -export const DesktopAppStageLabelSchema = Schema.Literals(["Alpha", "Dev", "Nightly"]); +export const DesktopAppStageLabelSchema = Schema.Literals(["Bscholar", "Dev", "Nightly"]); export interface DesktopAppBranding { baseName: string; diff --git a/packages/contracts/src/model.ts b/packages/contracts/src/model.ts index 8e7daaa0c79..748fbb3cbbd 100644 --- a/packages/contracts/src/model.ts +++ b/packages/contracts/src/model.ts @@ -131,6 +131,7 @@ const CODEX_DRIVER_KIND = ProviderDriverKind.make("codex"); const CLAUDE_DRIVER_KIND = ProviderDriverKind.make("claudeAgent"); const CURSOR_DRIVER_KIND = ProviderDriverKind.make("cursor"); const OPENCODE_DRIVER_KIND = ProviderDriverKind.make("opencode"); +const PI_AGENT_DRIVER_KIND = ProviderDriverKind.make("piAgent"); export const DEFAULT_MODEL = "gpt-5.4"; export const DEFAULT_GIT_TEXT_GENERATION_MODEL = "gpt-5.4-mini"; @@ -140,6 +141,7 @@ export const DEFAULT_MODEL_BY_PROVIDER: Partial> [CLAUDE_DRIVER_KIND]: "Claude", [CURSOR_DRIVER_KIND]: "Cursor", [OPENCODE_DRIVER_KIND]: "OpenCode", + [PI_AGENT_DRIVER_KIND]: "Pi Agent", }; diff --git a/packages/contracts/src/providerRuntime.ts b/packages/contracts/src/providerRuntime.ts index 5032dc4eb41..3d8f3c2f99a 100644 --- a/packages/contracts/src/providerRuntime.ts +++ b/packages/contracts/src/providerRuntime.ts @@ -26,6 +26,8 @@ const RuntimeEventRawSource = Schema.Union([ Schema.Literal("claude.sdk.permission"), Schema.Literal("codex.sdk.thread-event"), Schema.Literal("opencode.sdk.event"), + Schema.Literal("pi.sdk.event"), + Schema.Literal("pi.sdk.permission"), Schema.Literal("acp.jsonrpc"), Schema.TemplateLiteral(["acp.", Schema.String, ".extension"]), ]); diff --git a/packages/contracts/src/settings.test.ts b/packages/contracts/src/settings.test.ts index 39695fe3b01..6095d06f272 100644 --- a/packages/contracts/src/settings.test.ts +++ b/packages/contracts/src/settings.test.ts @@ -107,6 +107,13 @@ describe("ServerSettingsPatch string normalization", () => { binaryPath: " /opt/homebrew/bin/codex ", homePath: " ~/.codex ", }, + piAgent: { + enabled: true, + binaryPath: "", + homePath: "", + customModels: [], + launchArgs: "", + }, }, providerInstances: { codex_personal: { diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 2d115eed98e..f0cd9b2c0e3 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -3,7 +3,7 @@ import * as Duration from "effect/Duration"; import * as Schema from "effect/Schema"; import * as SchemaTransformation from "effect/SchemaTransformation"; import { TrimmedNonEmptyString, TrimmedString } from "./baseSchemas.ts"; -import { DEFAULT_GIT_TEXT_GENERATION_MODEL, ProviderOptionSelections } from "./model.ts"; +import { ProviderOptionSelections } from "./model.ts"; import { ModelSelection } from "./orchestration.ts"; import { ProviderInstanceConfig, ProviderInstanceId } from "./providerInstance.ts"; @@ -331,6 +331,49 @@ export const OpenCodeSettings = makeProviderSettingsSchema( ); export type OpenCodeSettings = typeof OpenCodeSettings.Type; +export const PiSettings = makeProviderSettingsSchema( + { + enabled: Schema.Boolean.pipe( + Schema.withDecodingDefault(Effect.succeed(true)), + Schema.annotateKey({ providerSettingsForm: { hidden: true } }), + ), + binaryPath: makeBinaryPathSetting("pi").pipe( + Schema.annotateKey({ + title: "Binary path", + description: "Path to the Pi Agent binary used by this instance.", + providerSettingsForm: { placeholder: "pi", clearWhenEmpty: "omit" }, + }), + ), + homePath: TrimmedString.pipe( + Schema.withDecodingDefault(Effect.succeed("")), + Schema.annotateKey({ + title: "Pi HOME path", + description: "Custom home directory for Pi Agent config and auth.", + providerSettingsForm: { placeholder: "~/.pi", clearWhenEmpty: "omit" }, + }), + ), + customModels: Schema.Array(Schema.String).pipe( + Schema.withDecodingDefault(Effect.succeed([])), + Schema.annotateKey({ providerSettingsForm: { hidden: true } }), + ), + launchArgs: Schema.String.pipe( + Schema.withDecodingDefault(Effect.succeed("")), + Schema.annotateKey({ + title: "Launch arguments", + description: "Additional arguments passed on session start.", + providerSettingsForm: { + placeholder: "e.g. --thinking high", + clearWhenEmpty: "omit", + }, + }), + ), + }, + { + order: ["binaryPath", "homePath", "launchArgs"], + }, +); +export type PiSettings = typeof PiSettings.Type; + export const ObservabilitySettings = Schema.Struct({ otlpTracesUrl: TrimmedString.pipe(Schema.withDecodingDefault(Effect.succeed(""))), otlpMetricsUrl: TrimmedString.pipe(Schema.withDecodingDefault(Effect.succeed(""))), @@ -353,8 +396,8 @@ export const ServerSettings = Schema.Struct({ textGenerationModelSelection: ModelSelection.pipe( Schema.withDecodingDefault( Effect.succeed({ - instanceId: ProviderInstanceId.make("codex"), - model: DEFAULT_GIT_TEXT_GENERATION_MODEL, + instanceId: ProviderInstanceId.make("claudeAgent"), + model: "claude-haiku-4-5", }), ), ), @@ -370,6 +413,7 @@ export const ServerSettings = Schema.Struct({ claudeAgent: ClaudeSettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))), cursor: CursorSettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))), opencode: OpenCodeSettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))), + piAgent: PiSettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))), }).pipe(Schema.withDecodingDefault(Effect.succeed({}))), // New driver-agnostic instance map. Keyed by `ProviderInstanceId`; values // are `ProviderInstanceConfig` envelopes. The driver-specific config blob diff --git a/packages/ssh/src/command.test.ts b/packages/ssh/src/command.test.ts index d95ffed49dc..34160de307f 100644 --- a/packages/ssh/src/command.test.ts +++ b/packages/ssh/src/command.test.ts @@ -13,6 +13,7 @@ import { ChildProcessSpawner } from "effect/unstable/process"; import { baseSshArgs, getLastNonEmptyOutputLine, + parseReleaseRepository, parseSshResolveOutput, resolveRemoteT3CliPackageSpec, runSshCommand, @@ -113,6 +114,73 @@ describe("ssh command", () => { }), ); + it.effect( + "resolves the remote t3 package spec to a GitHub Release tarball URL when releaseRepository is provided", + () => + Effect.sync(() => { + const releaseRepository = { owner: "bscholar-tt", repo: "t3code" } as const; + + assert.equal( + resolveRemoteT3CliPackageSpec({ + appVersion: "0.0.17", + updateChannel: "latest", + releaseRepository, + }), + "https://github.com/bscholar-tt/t3code/releases/download/v0.0.17/t3-0.0.17.tgz", + ); + + assert.equal( + resolveRemoteT3CliPackageSpec({ + appVersion: "0.0.17-nightly.20260415.44", + updateChannel: "nightly", + releaseRepository, + }), + "https://github.com/bscholar-tt/t3code/releases/download/v0.0.17-nightly.20260415.44/t3-0.0.17-nightly.20260415.44.tgz", + ); + + // Dev mode / channel fallback both point at the rolling latest alias + // so URLs stay stable even when no exact tag matches. + assert.equal( + resolveRemoteT3CliPackageSpec({ + appVersion: "0.0.0-dev", + updateChannel: "latest", + isDevelopment: true, + releaseRepository, + }), + "https://github.com/bscholar-tt/t3code/releases/latest/download/t3-latest.tgz", + ); + + assert.equal( + resolveRemoteT3CliPackageSpec({ + appVersion: "not-a-version", + updateChannel: "nightly", + releaseRepository, + }), + "https://github.com/bscholar-tt/t3code/releases/latest/download/t3-latest.tgz", + ); + }), + ); + + it.effect("parses release repository slugs and GitHub URLs", () => + Effect.sync(() => { + assert.deepEqual(parseReleaseRepository("bscholar-tt/t3code"), { + owner: "bscholar-tt", + repo: "t3code", + }); + assert.deepEqual(parseReleaseRepository("https://github.com/bscholar-tt/t3code"), { + owner: "bscholar-tt", + repo: "t3code", + }); + assert.deepEqual(parseReleaseRepository("https://github.com/bscholar-tt/t3code.git"), { + owner: "bscholar-tt", + repo: "t3code", + }); + assert.equal(parseReleaseRepository(""), undefined); + assert.equal(parseReleaseRepository(undefined), undefined); + assert.equal(parseReleaseRepository("not a repo"), undefined); + }), + ); + it.effect("reads the last non-empty ssh output line", () => Effect.sync(() => { assert.equal( diff --git a/packages/ssh/src/command.ts b/packages/ssh/src/command.ts index dc8839378cd..5ef68f00ca0 100644 --- a/packages/ssh/src/command.ts +++ b/packages/ssh/src/command.ts @@ -325,19 +325,73 @@ export const resolveSshTarget = Effect.fn("ssh/command.resolveSshTarget")(functi ); }); +export interface ReleaseRepository { + readonly owner: string; + readonly repo: string; +} + +/** + * Parses an `owner/repo` slug (with optional `.git` suffix) or a GitHub HTTPS + * URL (`https://github.com/owner/repo[.git]`) into a {@link ReleaseRepository}. + * Returns `undefined` for inputs that do not match those shapes so callers can + * fall back to registry-based package specs. + */ +export function parseReleaseRepository( + input: string | undefined | null, +): ReleaseRepository | undefined { + if (!input) return undefined; + const trimmed = input.trim(); + if (trimmed.length === 0) return undefined; + + const httpsMatch = /^https?:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/u.exec(trimmed); + if (httpsMatch) { + return { owner: httpsMatch[1]!, repo: httpsMatch[2]! }; + } + const slugMatch = /^([^/\s]+)\/([^/\s]+?)(?:\.git)?$/u.exec(trimmed); + if (slugMatch) { + return { owner: slugMatch[1]!, repo: slugMatch[2]! }; + } + return undefined; +} + +function releaseTarballUrl(repo: ReleaseRepository, tag: string, assetName: string): string { + return `https://github.com/${repo.owner}/${repo.repo}/releases/download/${tag}/${assetName}`; +} + +function latestReleaseTarballUrl(repo: ReleaseRepository): string { + // GitHub redirects this path to whichever asset of that name is attached to + // the release marked `latest`. release.yml uploads `t3-latest.tgz` on every + // stable release, keeping this URL stable across versions. + return `https://github.com/${repo.owner}/${repo.repo}/releases/latest/download/t3-latest.tgz`; +} + export function resolveRemoteT3CliPackageSpec(input: { readonly appVersion: string; readonly updateChannel: DesktopUpdateChannel; readonly isDevelopment?: boolean; + /** + * When provided, the remote runner will install t3 from the fork's GitHub + * Release tarball (`.tgz` attached to the matching release) rather than from + * the npm registry. This lets `npx --yes ` work on hosts without any + * registry auth (no `.npmrc` modification required). + */ + readonly releaseRepository?: ReleaseRepository; }): string { const appVersion = input.appVersion.trim(); + const repo = input.releaseRepository; + if (!input.isDevelopment && PUBLISHABLE_T3_VERSION_PATTERN.test(appVersion)) { + if (repo) { + return releaseTarballUrl(repo, `v${appVersion}`, `t3-${appVersion}.tgz`); + } return `t3@${appVersion}`; } if (input.isDevelopment) { + if (repo) return latestReleaseTarballUrl(repo); return "t3@nightly"; } + if (repo) return latestReleaseTarballUrl(repo); return input.updateChannel === "nightly" ? "t3@nightly" : "t3@latest"; } diff --git a/packages/ssh/src/tunnel.ts b/packages/ssh/src/tunnel.ts index 5ee5c684779..a3894fd2860 100644 --- a/packages/ssh/src/tunnel.ts +++ b/packages/ssh/src/tunnel.ts @@ -50,10 +50,10 @@ import { export const DEFAULT_REMOTE_PORT = 3773; const REMOTE_PORT_SCAN_WINDOW = 200; -const SSH_READY_TIMEOUT_MS = 20_000; -const SSH_READY_PROBE_TIMEOUT_MS = 1_000; +const SSH_READY_TIMEOUT_MS = 60_000; +const SSH_READY_PROBE_TIMEOUT_MS = 5_000; const TUNNEL_SHUTDOWN_TIMEOUT_MS = 2_000; -const REMOTE_READY_TIMEOUT_MS = 15_000; +const REMOTE_READY_TIMEOUT_MS = 60_000; const REMOTE_REUSE_READY_TIMEOUT_MS = 2_000; export interface RemoteT3RunnerOptions { diff --git a/scripts/build-desktop-artifact.test.ts b/scripts/build-desktop-artifact.test.ts index 743263d43b3..c609fc583cc 100644 --- a/scripts/build-desktop-artifact.test.ts +++ b/scripts/build-desktop-artifact.test.ts @@ -22,7 +22,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { }); it("switches desktop packaging product names to nightly for nightly builds", () => { - assert.equal(resolveDesktopProductName("0.0.17"), "T3 Code (Alpha)"); + assert.equal(resolveDesktopProductName("0.0.17"), "T3 Code (Bscholar)"); assert.equal(resolveDesktopProductName("0.0.17-nightly.20260413.42"), "T3 Code (Nightly)"); });