diff --git a/.github/DISCUSSION_TEMPLATE/qa.yml b/.github/DISCUSSION_TEMPLATE/qa.yml new file mode 100644 index 000000000000..9bbd676aff86 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/qa.yml @@ -0,0 +1,26 @@ +title: "❓ Question: [Brief summary]" +labels: + - help +body: + - type: markdown + attributes: + value: | + 💡 Before posting, please attach your **diagnostics zip** — it helps the Goose team debug faster and saves everyone time. + [How to capture and share diagnostics](https://block.github.io/goose/docs/troubleshooting/diagnostics-and-reporting/) + - type: textarea + id: problem + attributes: + label: What happened? + description: Describe the issue in detail and attach your diagnostics zip if possible. + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: Tell us how to reproduce the issue — commands, steps, or context. + - type: textarea + id: version + attributes: + label: Goose version and environment + description: Include your Goose version and operating system if known. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 689e3ef1702a..e2d4d93e0ef8 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -2,17 +2,22 @@ name: Bug report about: Create a report to help us improve title: '' -labels: '' +labels: bug assignees: '' - --- **Describe the bug** -Note: Please check the common issues on https://block.github.io/goose/docs/troubleshooting before filing a report +💡 Before filing, please check common issues: +https://block.github.io/goose/docs/troubleshooting + +📦 To help us debug faster, attach your **diagnostics zip** if possible. +👉 How to capture it: https://block.github.io/goose/docs/troubleshooting/diagnostics-and-reporting/ A clear and concise description of what the bug is. +--- + **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' @@ -20,18 +25,26 @@ Steps to reproduce the behavior: 3. Scroll down to '....' 4. See error +--- + **Expected behavior** A clear and concise description of what you expected to happen. +--- + **Screenshots** If applicable, add screenshots to help explain your problem. -**Please provide following information:** - - **OS & Arch:** [e.g. Ubuntu 22.04 x86] - - **Interface:** [UI/CLI] - - **Version:** [e.g. v1.0.2] - - **Extensions enabled:** [e.g. Computer Controller, Figma] - - **Provider & Model:** [e.g. Google - gemini-1.5-pro] +--- + +**Please provide the following information** +- **OS & Arch:** [e.g. Ubuntu 22.04 x86] +- **Interface:** [UI / CLI] +- **Version:** [e.g. v1.0.2] +- **Extensions enabled:** [e.g. Computer Controller, Figma] +- **Provider & Model:** [e.g. Google – gemini-1.5-pro] + +--- **Additional context** Add any other context about the problem here. diff --git a/.github/actions/generate-release-pr-body/action.yaml b/.github/actions/generate-release-pr-body/action.yaml new file mode 100644 index 000000000000..72ccd754e382 --- /dev/null +++ b/.github/actions/generate-release-pr-body/action.yaml @@ -0,0 +1,44 @@ +name: 'Generate Release Notes' +description: 'Generate release notes for a given version' + +inputs: + version: + description: 'The version being released' + required: true + head_ref: + description: 'The commit SHA or reference of the head of the release branch' + required: true + prior_ref: + description: 'The previous version to compare against' + required: true + +outputs: + pr_body_file: + description: 'Path to the generated PR body file' + value: ${{ steps.generate.outputs.pr_body_file }} + +runs: + using: 'composite' + steps: + - name: Generate release notes + id: generate + shell: bash + env: + VERSION: ${{ inputs.version }} + HEAD_REF: ${{ inputs.head_ref }} + PRIOR_REF: ${{ inputs.prior_ref }} + TEMPLATE_FILE: "${{ github.action_path }}/pr_body_template.txt" + run: | + git fetch origin --tags + + { + sed -e "s/{{VERSION}}/${VERSION}/g" \ + -e "s/{{PRIOR_VERSION}}/${PRIOR_REF}/g" \ + "$TEMPLATE_FILE" + git log --pretty=format:"- %s (%h)" --reverse ${PRIOR_REF}..${HEAD_REF} + echo "" + echo "---" + echo "*This release PR was generated automatically.*" + } > pr_body.txt + + echo "pr_body_file=pr_body.txt" >> $GITHUB_OUTPUT diff --git a/.github/actions/generate-release-pr-body/pr_body_template.txt b/.github/actions/generate-release-pr-body/pr_body_template.txt new file mode 100644 index 000000000000..e63e7c2041f7 --- /dev/null +++ b/.github/actions/generate-release-pr-body/pr_body_template.txt @@ -0,0 +1,19 @@ +# Release v{{VERSION}} + +## How to Release + +Push the release tag to trigger the release: + ```bash + git fetch && git tag v{{VERSION}} origin/release/{{VERSION}} + git push origin v{{VERSION}} + ``` +This PR will auto-merge once the tag is pushed. + +## Important Notes + +- All commits in this release should have corresponding cherry-picks in `main` +- This PR can be closed if the release is not needed. + +## Changes in This Release + +**Comparing:** `{{PRIOR_VERSION}}...v{{VERSION}}` diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b8f1f7e29f82..bc90ec22d9a6 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -14,6 +14,10 @@ - [ ] Build / Release - [ ] Other (specify below) +### AI Assistance + +- [ ] This PR was created or reviewed with AI assistance + ### Testing @@ -27,5 +31,6 @@ Before: After: - +### Submitting a Recipe? + **Email**: diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 188f8388beab..5aede7a1dbea 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -97,10 +97,10 @@ jobs: uses: actions/cache@2f8e54208210a422b2efd51efaa6bd6d7ca8920f with: path: target - key: ${{ runner.os }}-cargo-build-${{ hashFiles('Cargo.lock') }}-${{ hashFiles('rust-toolchain.toml') }} + key: ${{ runner.os }}-cargo-build-release-${{ hashFiles('Cargo.lock') }}-${{ hashFiles('rust-toolchain.toml') }} restore-keys: | - ${{ runner.os }}-cargo-build-${{ hashFiles('Cargo.lock') }}- - ${{ runner.os }}-cargo-build- + ${{ runner.os }}-cargo-build-release-${{ hashFiles('Cargo.lock') }}- + ${{ runner.os }}-cargo-build-release- - name: Build CLI (Linux/macOS) if: matrix.use-cross diff --git a/.github/workflows/bundle-desktop-intel.yml b/.github/workflows/bundle-desktop-intel.yml index f1b001535903..a8d698b8259a 100644 --- a/.github/workflows/bundle-desktop-intel.yml +++ b/.github/workflows/bundle-desktop-intel.yml @@ -83,9 +83,9 @@ jobs: uses: actions/cache@2f8e54208210a422b2efd51efaa6bd6d7ca8920f # pin@v3 with: path: target - key: ${{ runner.os }}-intel-cargo-build-${{ hashFiles('**/Cargo.lock') }} + key: ${{ runner.os }}-intel-cargo-build-release-${{ hashFiles('**/Cargo.lock') }} restore-keys: | - ${{ runner.os }}-intel-cargo-build- + ${{ runner.os }}-intel-cargo-build-release- - name: Build goose-server for Intel macOS (x86_64) diff --git a/.github/workflows/bundle-desktop-linux.yml b/.github/workflows/bundle-desktop-linux.yml index 426d139e084e..8d24787c9d57 100644 --- a/.github/workflows/bundle-desktop-linux.yml +++ b/.github/workflows/bundle-desktop-linux.yml @@ -95,9 +95,9 @@ jobs: ${{ env.CARGO_HOME }}/registry/index/ ${{ env.CARGO_HOME }}/registry/cache/ ${{ env.CARGO_HOME }}/git/db/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }} restore-keys: | - ${{ runner.os }}-cargo- + ${{ runner.os }}-cargo-release- - name: Build goosed binary env: diff --git a/.github/workflows/bundle-desktop-manual.yml b/.github/workflows/bundle-desktop-manual.yml new file mode 100644 index 000000000000..f2943e8d6637 --- /dev/null +++ b/.github/workflows/bundle-desktop-manual.yml @@ -0,0 +1,19 @@ +name: Manual Desktop Bundle (Unsigned) + +on: + workflow_dispatch: + inputs: + branch: + description: 'Branch name to bundle app from' + required: true + type: string + +jobs: + bundle-desktop-unsigned: + uses: ./.github/workflows/bundle-desktop.yml + permissions: + id-token: write + contents: read + with: + signing: false + ref: ${{ inputs.branch }} diff --git a/.github/workflows/bundle-desktop-windows.yml b/.github/workflows/bundle-desktop-windows.yml index 433ed661c5e6..bbde5345f659 100644 --- a/.github/workflows/bundle-desktop-windows.yml +++ b/.github/workflows/bundle-desktop-windows.yml @@ -92,10 +92,10 @@ jobs: uses: actions/cache@2f8e54208210a422b2efd51efaa6bd6d7ca8920f with: path: target - key: ${{ runner.os }}-cargo-build-${{ hashFiles('Cargo.lock') }}-${{ hashFiles('rust-toolchain.toml') }} + key: ${{ runner.os }}-cargo-build-release-${{ hashFiles('Cargo.lock') }}-${{ hashFiles('rust-toolchain.toml') }} restore-keys: | - ${{ runner.os }}-cargo-build-${{ hashFiles('Cargo.lock') }}- - ${{ runner.os }}-cargo-build- + ${{ runner.os }}-cargo-build-release-${{ hashFiles('Cargo.lock') }}- + ${{ runner.os }}-cargo-build-release- # 4) Build Rust for Windows using Docker (cross-compilation with enhanced caching) - name: Build Windows executable using Docker cross-compilation with enhanced caching diff --git a/.github/workflows/bundle-desktop.yml b/.github/workflows/bundle-desktop.yml index 8995eb92c6d1..dd9f58cc6b32 100644 --- a/.github/workflows/bundle-desktop.yml +++ b/.github/workflows/bundle-desktop.yml @@ -3,6 +3,7 @@ # - release.yml # - canary.yml # - pr-comment-bundle-desktop.yml +# - bundle-desktop-manual.yml on: workflow_call: inputs: @@ -127,9 +128,9 @@ jobs: uses: actions/cache@2f8e54208210a422b2efd51efaa6bd6d7ca8920f # pin@v3 with: path: target - key: ${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }} + key: ${{ runner.os }}-cargo-build-release-${{ hashFiles('**/Cargo.lock') }} restore-keys: | - ${{ runner.os }}-cargo-build- + ${{ runner.os }}-cargo-build-release- # Build the project - name: Build goosed @@ -295,4 +296,4 @@ jobs: exit 1 fi # Kill the app to clean up - pkill -f "Goose.app/Contents/MacOS/Goose" \ No newline at end of file + pkill -f "Goose.app/Contents/MacOS/Goose" diff --git a/.github/workflows/check-release-pr.yaml b/.github/workflows/check-release-pr.yaml new file mode 100644 index 000000000000..d5462ee41749 --- /dev/null +++ b/.github/workflows/check-release-pr.yaml @@ -0,0 +1,57 @@ +name: Check Release PR + +on: + pull_request: + types: + - opened + - synchronize + branches: + - main + +jobs: + check-commits: + runs-on: ubuntu-latest + if: startsWith(github.head_ref, 'release/') + steps: + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + + - name: Check all PR commits exist in main + run: | + git fetch origin main + + CHERRY_OUTPUT=$(git cherry origin/main HEAD) + + if [ -z "$CHERRY_OUTPUT" ]; then + echo "✅ All commits already exist in main" + exit 0 + fi + + echo "Cherry check results:" + echo "$CHERRY_OUTPUT" + + MISSING_COMMITS=$(echo "$CHERRY_OUTPUT" | grep '^+' | cut -d' ' -f2) + + if [ -z "$MISSING_COMMITS" ]; then + echo "✅ All commits exist in main" + exit 0 + fi + + COMMIT_COUNT=$(echo "$MISSING_COMMITS" | wc -l) + FIRST_COMMIT=$(git rev-list --reverse HEAD ^origin/main | head -1) + + if [ "$COMMIT_COUNT" -eq 1 ] && echo "$MISSING_COMMITS" | grep -q "$FIRST_COMMIT"; then + echo "✅ Only version bump commit is unique" + git log --oneline -1 "$FIRST_COMMIT" + else + echo "❌ Found commits that should exist in main:" + for commit in $MISSING_COMMITS; do + if [ "$commit" != "$FIRST_COMMIT" ]; then + git log --oneline -1 "$commit" + fi + done + echo "Make sure commits have equivalents in main. If you've since updated main, re-run this job" + exit 1 + fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 172b02d30e0d..3a3395fccc7a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,9 +73,9 @@ jobs: ${{ env.CARGO_HOME }}/registry/cache/ ${{ env.CARGO_HOME }}/git/db/ target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + key: ${{ runner.os }}-cargo-debug-${{ hashFiles('**/Cargo.lock') }} restore-keys: | - ${{ runner.os }}-cargo- + ${{ runner.os }}-cargo-debug- - name: Build and Test run: | @@ -131,13 +131,4 @@ jobs: run: source ../../bin/activate-hermit && npm run test:run working-directory: ui/desktop - # Faster Desktop App build for PRs only - bundle-desktop-unsigned: - uses: ./.github/workflows/bundle-desktop.yml - permissions: - id-token: write - contents: read - needs: changes - if: (github.event_name == 'pull_request' || github.event_name == 'merge_group') && (needs.changes.outputs.code == 'true' || github.event_name != 'pull_request') - with: - signing: false + diff --git a/.github/workflows/create-release-pr.yaml b/.github/workflows/create-release-pr.yaml new file mode 100644 index 000000000000..5d9047ab2e75 --- /dev/null +++ b/.github/workflows/create-release-pr.yaml @@ -0,0 +1,83 @@ +name: Release Workflow + +on: + workflow_call: + inputs: + bump_type: + description: 'Type of version bump (minor or patch)' + required: true + type: string + target_branch: + description: 'Target branch for the pull request' + required: false + type: string + default: 'main' + +permissions: + contents: write + pull-requests: write + +jobs: + create-release: + runs-on: ubuntu-latest + env: + BUMP_TYPE: ${{ inputs.bump_type }} + TARGET_BRANCH: ${{ inputs.target_branch }} + + steps: + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 + with: + ref: ${{ inputs.target_branch }} + fetch-depth: 0 # to generate complete release log + + - uses: cashapp/activate-hermit@e49f5cb4dd64ff0b0b659d1d8df499595451155a # v1 + - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6 + + - name: Validate input and set old version + run: | + if [[ "$BUMP_TYPE" != "minor" && "$BUMP_TYPE" != "patch" ]]; then + echo "Error: bump_type must be 'minor' or 'patch'" + exit 1 + fi + + - name: create release branch + run: | + PRIOR_VERSION=$(just get-tag-version) + if [[ "$BUMP_TYPE" == "minor" ]]; then + VERSION=$(just get-next-minor-version) + else + VERSION=$(just get-next-patch-version) + fi + + echo "prior_ref=v$PRIOR_VERSION" >> $GITHUB_ENV + echo "version=$VERSION" >> $GITHUB_ENV + echo "Version: $VERSION" + + git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + + just prepare-release $VERSION + BRANCH_NAME=$(git branch --show-current) + echo "branch_name=$BRANCH_NAME" >> $GITHUB_ENV + echo "Branch: $BRANCH_NAME" + + - name: push release branch + run: | + git push origin "${{ env.branch_name }}" + + - name: Generate release notes + uses: ./.github/actions/generate-release-pr-body + with: + version: ${{ env.version }} + head_ref: ${{ github.event.pull_request.head.sha || github.sha }} + prior_ref: ${{ env.prior_ref }} + + - name: Create Pull Request + run: | + gh pr create \ + -B "$TARGET_BRANCH" \ + -H "${{ env.branch_name }}" \ + --title "chore(release): release version ${{ env.version }} ($BUMP_TYPE)" \ + --body-file pr_body.txt + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/merge-release-pr-on-tag.yaml b/.github/workflows/merge-release-pr-on-tag.yaml new file mode 100644 index 000000000000..cbf122f7c55c --- /dev/null +++ b/.github/workflows/merge-release-pr-on-tag.yaml @@ -0,0 +1,148 @@ +name: Merge release PR on tag push + +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' + +permissions: + actions: write + contents: write + pull-requests: write + checks: read + +jobs: + trigger-patch-release: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 + + - name: Extract version from tag + id: version + env: + TAG: ${{ github.ref_name }} + run: | + VERSION=${TAG#v} + BRANCH="release/${VERSION}" + + echo "tag=${TAG}" >> $GITHUB_OUTPUT + echo "version=${VERSION}" >> $GITHUB_OUTPUT + echo "branch=${BRANCH}" >> $GITHUB_OUTPUT + + echo "Tag: ${TAG}" + echo "Version: ${VERSION}" + echo "Expected branch: ${BRANCH}" + + - name: Find matching PR + id: find_pr + env: + GH_TOKEN: ${{ github.token }} + BRANCH: ${{ steps.version.outputs.branch }} + run: | + PR_NUMBER=$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number // empty') + + if [ -z "$PR_NUMBER" ]; then + echo "❌ No open PR found for branch: $BRANCH" + echo "pr_found=false" >> $GITHUB_OUTPUT + exit 1 + else + echo "✅ Found PR #$PR_NUMBER for branch: $BRANCH" + echo "pr_found=true" >> $GITHUB_OUTPUT + echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT + fi + + - name: Get PR details and check status + if: steps.find_pr.outputs.pr_found == 'true' + id: pr_status + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ steps.find_pr.outputs.pr_number }} + run: | + PR_DATA=$(gh pr view $PR_NUMBER --json title,headRefName,baseRefName,mergeable,statusCheckRollup) + + TITLE=$(echo "$PR_DATA" | jq -r '.title') + HEAD_BRANCH=$(echo "$PR_DATA" | jq -r '.headRefName') + BASE_BRANCH=$(echo "$PR_DATA" | jq -r '.baseRefName') + MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable') + + echo "PR Title: $TITLE" + echo "Head Branch: $HEAD_BRANCH" + echo "Base Branch: $BASE_BRANCH" + echo "Mergeable: $MERGEABLE" + + if [ "$MERGEABLE" != "MERGEABLE" ]; then + echo "❌ PR is not in a mergeable state: $MERGEABLE" + echo "can_merge=false" >> $GITHUB_OUTPUT + echo "merge_reason=PR is not mergeable (state: $MERGEABLE)" >> $GITHUB_OUTPUT + exit 0 + fi + + STATUS_CHECKS=$(echo "$PR_DATA" | jq -r '.statusCheckRollup[]? | select(.conclusion != null) | "\(.context): \(.conclusion)"') + FAILED_CHECKS=$(echo "$PR_DATA" | jq -r '.statusCheckRollup[]? | select(.conclusion == "FAILURE" or .conclusion == "CANCELLED" or .conclusion == "TIMED_OUT") | .context') + PENDING_CHECKS=$(echo "$PR_DATA" | jq -r '.statusCheckRollup[]? | select(.conclusion == null) | .context') + + echo "Status checks:" + if [ -n "$STATUS_CHECKS" ]; then + echo "$STATUS_CHECKS" + else + echo "No status checks found" + fi + + if [ -n "$FAILED_CHECKS" ]; then + echo "❌ Failed checks found:" + echo "$FAILED_CHECKS" + echo "can_merge=false" >> $GITHUB_OUTPUT + echo "merge_reason=Some checks are failing" >> $GITHUB_OUTPUT + exit 0 + fi + + if [ -n "$PENDING_CHECKS" ]; then + echo "⏳ Pending checks found:" + echo "$PENDING_CHECKS" + echo "can_merge=false" >> $GITHUB_OUTPUT + echo "merge_reason=Some checks are still pending" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "✅ All checks are passing and PR is ready to merge" + echo "can_merge=true" >> $GITHUB_OUTPUT + + - name: Get branch SHA before merge + if: steps.find_pr.outputs.pr_found == 'true' && steps.pr_status.outputs.can_merge == 'true' + id: branch_info + env: + BRANCH: ${{ steps.version.outputs.branch }} + run: | + git fetch origin + + BRANCH_SHA=$(git rev-parse "origin/$BRANCH") + echo "branch_sha=$BRANCH_SHA" >> $GITHUB_OUTPUT + echo "Branch SHA: $BRANCH_SHA" + + - name: Merge PR + if: steps.find_pr.outputs.pr_found == 'true' && steps.pr_status.outputs.can_merge == 'true' + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ steps.find_pr.outputs.pr_number }} + VERSION: ${{ steps.version.outputs.version }} + TAG: ${{ steps.version.outputs.tag }} + run: | + gh pr merge $PR_NUMBER --squash --delete-branch --subject "Release $VERSION" --body "Auto-merged release PR after tag $TAG was pushed" + + - name: Restore branch + if: steps.find_pr.outputs.pr_found == 'true' && steps.pr_status.outputs.can_merge == 'true' + env: + BRANCH: ${{ steps.version.outputs.branch }} + BRANCH_SHA: ${{ steps.branch_info.outputs.branch_sha }} + run: | + git checkout -b "$BRANCH" "$BRANCH_SHA" + git push origin "$BRANCH" + + - name: Trigger patch release + env: + BRANCH: ${{ steps.version.outputs.branch }} + GH_TOKEN: ${{ github.token }} + run: | + gh workflow run patch-release.yaml \ + --field target_branch=$BRANCH diff --git a/.github/workflows/minor-release.yaml b/.github/workflows/minor-release.yaml new file mode 100644 index 000000000000..99ce40a43932 --- /dev/null +++ b/.github/workflows/minor-release.yaml @@ -0,0 +1,16 @@ +name: Create Minor Release PR + +permissions: + contents: write + pull-requests: write + +on: + schedule: + - cron: '0 0 * * 2' + workflow_dispatch: + +jobs: + release: + uses: ./.github/workflows/create-release-pr.yaml + with: + bump_type: "minor" diff --git a/.github/workflows/patch-release.yaml b/.github/workflows/patch-release.yaml new file mode 100644 index 000000000000..29ed3c076093 --- /dev/null +++ b/.github/workflows/patch-release.yaml @@ -0,0 +1,20 @@ +name: Create Patch Release PR + +permissions: + contents: write + pull-requests: write + +on: + workflow_dispatch: + inputs: + target_branch: + description: 'Target branch for hotfix' + required: true + type: string + +jobs: + hotfix: + uses: ./.github/workflows/create-release-pr.yaml + with: + bump_type: "patch" + target_branch: ${{ inputs.target_branch }} diff --git a/.github/workflows/pr-smoke-test.yml b/.github/workflows/pr-smoke-test.yml index 84a0e6964790..124c0fcb4511 100644 --- a/.github/workflows/pr-smoke-test.yml +++ b/.github/workflows/pr-smoke-test.yml @@ -2,6 +2,9 @@ on: pull_request: branches: - main + push: + branches: + - main workflow_dispatch: inputs: branch: @@ -13,8 +16,16 @@ on: name: Live Provider Tests jobs: + check-fork: + runs-on: ubuntu-latest + # Skip entire workflow for PRs from forks (they don't have access to secrets) + if: github.event_name == 'workflow_dispatch' || github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository + steps: + - run: echo "Not a fork PR - proceeding with smoke tests" + changes: runs-on: ubuntu-latest + needs: check-fork outputs: code: ${{ steps.filter.outputs.code }} steps: @@ -62,9 +73,9 @@ jobs: ${{ env.CARGO_HOME }}/registry/cache/ ${{ env.CARGO_HOME }}/git/db/ target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }} restore-keys: | - ${{ runner.os }}-cargo- + ${{ runner.os }}-cargo-release- - name: Build Release Binary for Smoke Tests run: | @@ -117,6 +128,21 @@ jobs: # Run the provider test script (binary already built and downloaded) bash scripts/test_providers.sh + - name: Run MCP Tests + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} + DATABRICKS_HOST: ${{ secrets.DATABRICKS_HOST }} + DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }} + OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + TETRATE_API_KEY: ${{ secrets.TETRATE_API_KEY }} + HOME: /tmp/goose-home + GOOSE_DISABLE_KEYRING: 1 + SKIP_BUILD: 1 + run: | + bash scripts/test_mcp.sh + - name: Run Subrecipe Tests env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} @@ -127,3 +153,14 @@ jobs: SKIP_BUILD: 1 run: | bash scripts/test_subrecipes.sh + + - name: Run Compaction Tests + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + GOOSE_PROVIDER: anthropic + GOOSE_MODEL: claude-sonnet-4-5-20250929 + HOME: /tmp/goose-home + GOOSE_DISABLE_KEYRING: 1 + SKIP_BUILD: 1 + run: | + bash scripts/test_compaction.sh diff --git a/.github/workflows/update-release-pr.yaml b/.github/workflows/update-release-pr.yaml new file mode 100644 index 000000000000..fc5cc94afe9a --- /dev/null +++ b/.github/workflows/update-release-pr.yaml @@ -0,0 +1,66 @@ +name: Update Release Notes + +on: + pull_request: + types: + - synchronize + branches: + - main + - release/** + +permissions: + contents: read + pull-requests: write + +jobs: + update-release-notes: + runs-on: ubuntu-latest + if: startsWith(github.event.pull_request.head.ref, 'release/') && github.event.pull_request.user.login == 'github-actions[bot]' + steps: + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 + with: + fetch-depth: 0 # to generate complete release log + + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 + with: + ref: ${{ github.base_ref }} + path: './prior-version' + + - uses: cashapp/activate-hermit@e49f5cb4dd64ff0b0b659d1d8df499595451155a # v1 + - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6 + + - name: Extract version from branch name + env: + REF_NAME: ${{ github.head_ref }} + run: | + BRANCH_NAME="$REF_NAME" + VERSION=$(echo "$BRANCH_NAME" | sed 's/release\///') + echo "version=$VERSION" >> $GITHUB_ENV + echo "Version: $VERSION" + + - name: Get prior version + working-directory: './prior-version' + run: | + PRIOR_VERSION=$(just get-tag-version) + echo "prior_ref=v$PRIOR_VERSION" >> $GITHUB_ENV + + - name: Generate release notes + uses: ./.github/actions/generate-release-pr-body + with: + version: ${{ env.version }} + head_ref: ${{ github.event.pull_request.head.sha }} + prior_ref: ${{ env.prior_ref }} + + - name: Update Pull Request + env: + REF_NAME: ${{ github.head_ref }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_NUMBER=$(gh pr list --head "$REF_NAME" --json number --jq '.[0].number') + + if [[ -z "$PR_NUMBER" || "$PR_NUMBER" == "null" ]]; then + echo "No PR found for branch $REF_NAME" + exit 1 + fi + + gh pr edit "$PR_NUMBER" --body-file pr_body.txt diff --git a/AGENTS.md b/AGENTS.md index a4d3d3d361af..6f10190d2d7d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -82,6 +82,7 @@ Never: Edit ui/desktop/openapi.json manually Never: Edit Cargo.toml use cargo add Never: Skip cargo fmt Never: Merge without ./scripts/clippy-lint.sh +Never: Comment self-evident operations (`// Initialize`, `// Return result`), getters/setters, constructors, or standard Rust idioms ## Entry Points - CLI: crates/goose-cli/src/main.rs diff --git a/BUILDING_DOCKER.md b/BUILDING_DOCKER.md index ef3b62e0bed7..ed61e4b5e0a9 100644 --- a/BUILDING_DOCKER.md +++ b/BUILDING_DOCKER.md @@ -319,4 +319,4 @@ When contributing Docker-related changes: - [goose in Docker Tutorial](documentation/docs/tutorials/goose-in-docker.md) - Step-by-step tutorial - [Installation Guide](https://block.github.io/goose/docs/getting-started/installation) - All installation methods -- [Configuration Guide](https://block.github.io/goose/docs/guides/config-file) - Detailed configuration options +- [Configuration Guide](https://block.github.io/goose/docs/guides/config-files) - Detailed configuration options diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3def3d362790..7ec7a08c8268 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,23 +9,7 @@ We welcome pull requests for general contributions! If you have a larger new fea --- -## 🎉 Hacktoberfest 2025 🎉 - -`goose` is a participating in Hacktoberfest 2025! We’re so excited for your contributions, and have created a wide variety of issues so that anyone can contribute. Whether you're a seasoned developer or a first-time open source contributor, there's something for everyone. - -### Here's how you can get started: - -1. Read the [code of conduct](https://github.com/block/.github/blob/main/CODE_OF_CONDUCT.md). -2. Skim the quick AI contribution tips below (and see the [full Responsible AI-Assisted Coding Guide](./ai-assisted-coding-guide.md) for details). -3. Choose a task from this project's Hacktoberfest issues in our [Project Hub](https://github.com/block/goose/issues/4705). Each issue has the 🏷️ `hacktoberfest` label. -4. Comment ".take" on the corresponding issue to get assigned the task. -5. Fork the repository and create a new branch for your work. -6. Make your changes and submit a pull request. -7. Wait for review and address any feedback. - ---- - -### 🤖 Quick Responsible AI Tips +## 🤖 Quick Responsible AI Tips If you use Goose, Copilot, Claude, or other AI tools to help with your PRs: @@ -51,35 +35,7 @@ If you use Goose, Copilot, Claude, or other AI tools to help with your PRs: - Document your changes - Ask for review if security or core code is involved -👉 Full guide here: [Responsible AI-Assisted Coding Guide](./ai-assisted-coding-guide.md) - ---- - -### 🏆 Leaderboard & Prizes - -Every hacktoberfest PR and contribution will earn you points on our [leaderboard](https://github.com/block/goose/issues/4775). Those who end up in the top 20 participants with the most points by the end of October will earn exclusive swag and LLM credits! As you have issues merged, here is a brief explanation on how our automatic points system works. - -#### Point System - -| Weight | Points Awarded | Description | -|---------|-------------|-------------| -| 🐭 **Small** | 5 points | For smaller tasks that take limited time to complete and/or don't require any product knowledge. | -| 🐰 **Medium** | 10 points | For average tasks that take additional time to complete and/or require some product knowledge. | -| 🐂 **Large** | 15 points | For heavy tasks that takes lots of time to complete and/or possibly require deep product knowledge. | - -#### Prizes You Can Win - -- **Top 5**: $100 gift card to our [brand new goose swag shop](https://www.gooseswag.xyz/) and $100 of LLM credits! -- **Top 6-10**: $50 gift cards for goose swag shop and $50 of LLM credits! -- **Top 11-20**: $25 of LLM credits! - -Keep an eye on your progress via our [Leaderboard](https://github.com/block/goose/issues/4775). - -### 👩‍ Need help? - -Need help or have questions? Feel free to reach out by connecting with us in our [Discord community](https://discord.gg/goose-oss) to get direct help from our team in the `#hacktoberfest` project channel. - -Happy contributing! +👉 Full guide here: [Responsible AI-Assisted Coding Guide](./HOWTOAI.md) --- diff --git a/Cargo.lock b/Cargo.lock index 18578f0b1640..516d6f3eae41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2597,9 +2597,9 @@ checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "globset" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" dependencies = [ "aho-corasick", "bstr", @@ -2610,7 +2610,7 @@ dependencies = [ [[package]] name = "goose" -version = "1.11.0" +version = "1.13.0" dependencies = [ "ahash", "anyhow", @@ -2632,7 +2632,9 @@ dependencies = [ "etcetera", "fs2", "futures", + "ignore", "include_dir", + "indexmap 2.12.0", "indoc", "insta", "jsonschema", @@ -2646,8 +2648,10 @@ dependencies = [ "oauth2", "once_cell", "opentelemetry", + "opentelemetry-appender-tracing", "opentelemetry-otlp", "opentelemetry_sdk", + "paste", "rand 0.8.5", "regex", "reqwest 0.12.12", @@ -2659,6 +2663,7 @@ dependencies = [ "serde_yaml", "serial_test", "sha2", + "shellexpand", "sqlx", "sys-info", "temp-env", @@ -2687,7 +2692,7 @@ dependencies = [ [[package]] name = "goose-bench" -version = "1.11.0" +version = "1.13.0" dependencies = [ "anyhow", "async-trait", @@ -2710,7 +2715,7 @@ dependencies = [ [[package]] name = "goose-cli" -version = "1.11.0" +version = "1.13.0" dependencies = [ "agent-client-protocol", "anstream", @@ -2762,7 +2767,7 @@ dependencies = [ [[package]] name = "goose-mcp" -version = "1.11.0" +version = "1.13.0" dependencies = [ "anyhow", "async-trait", @@ -2828,7 +2833,7 @@ dependencies = [ [[package]] name = "goose-server" -version = "1.11.0" +version = "1.13.0" dependencies = [ "anyhow", "async-trait", @@ -2865,7 +2870,7 @@ dependencies = [ [[package]] name = "goose-test" -version = "1.11.0" +version = "1.13.0" dependencies = [ "clap", "serde_json", @@ -2897,7 +2902,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.7.1", + "indexmap 2.12.0", "slab", "tokio", "tokio-util", @@ -2916,7 +2921,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.2.0", - "indexmap 2.7.1", + "indexmap 2.12.0", "slab", "tokio", "tokio-util", @@ -2960,6 +2965,12 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + [[package]] name = "hashlink" version = "0.8.4" @@ -3405,9 +3416,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.23" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" dependencies = [ "crossbeam-deque", "globset", @@ -3508,13 +3519,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.1" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.16.0", "serde", + "serde_core", ] [[package]] @@ -3966,7 +3978,7 @@ dependencies = [ "chrono", "encoding_rs", "flate2", - "indexmap 2.7.1", + "indexmap 2.12.0", "itoa", "log", "md-5", @@ -4559,6 +4571,18 @@ dependencies = [ "tracing", ] +[[package]] +name = "opentelemetry-appender-tracing" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5feffc321035ad94088a7e5333abb4d84a8726e54a802e736ce9dd7237e85b" +dependencies = [ + "opentelemetry", + "tracing", + "tracing-core", + "tracing-subscriber", +] + [[package]] name = "opentelemetry-http" version = "0.27.0" @@ -4857,7 +4881,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ "base64 0.22.1", - "indexmap 2.7.1", + "indexmap 2.12.0", "quick-xml 0.32.0", "serde", "time", @@ -5010,7 +5034,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d35f4dc9988d1326b065b4def5e950c3ed727aa03e3151b86cc9e2aec6b03f54" dependencies = [ "futures", - "indexmap 2.7.1", + "indexmap 2.12.0", "nix 0.29.0", "tokio", "tracing", @@ -5538,9 +5562,9 @@ dependencies = [ [[package]] name = "rmcp" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f35acda8f89fca5fd8c96cae3c6d5b4c38ea0072df4c8030915f3b5ff469c1c" +checksum = "1fdad1258f7259fdc0f2dfc266939c82c3b5d1fd72bcde274d600cdc27e60243" dependencies = [ "base64 0.22.1", "chrono", @@ -5566,9 +5590,9 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9f1d5220aaa23b79c3d02e18f7a554403b3ccea544bbb6c69d6bcb3e854a274" +checksum = "ede0589a208cc7ce81d1be68aa7e74b917fcd03c81528408bab0457e187dcd9b" dependencies = [ "darling 0.21.0", "proc-macro2", @@ -5977,7 +6001,7 @@ version = "1.0.142" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.12.0", "itoa", "memchr", "ryu", @@ -6026,7 +6050,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.7.1", + "indexmap 2.12.0", "serde", "serde_derive", "serde_json", @@ -6052,7 +6076,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.12.0", "itoa", "ryu", "serde", @@ -6129,9 +6153,9 @@ checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" [[package]] name = "shellexpand" -version = "3.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b" +checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb" dependencies = [ "dirs", ] @@ -6299,7 +6323,7 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap 2.7.1", + "indexmap 2.12.0", "log", "memchr", "once_cell", @@ -7032,6 +7056,7 @@ dependencies = [ "futures-core", "futures-io", "futures-sink", + "futures-util", "pin-project-lite", "tokio", ] @@ -7063,7 +7088,7 @@ version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.12.0", "serde", "serde_spanned", "toml_datetime", @@ -7547,7 +7572,7 @@ version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5afb1a60e207dca502682537fefcfd9921e71d0b83e9576060f09abc6efab23" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.12.0", "serde", "serde_json", "utoipa-gen", @@ -8664,7 +8689,7 @@ dependencies = [ "crc32fast", "crossbeam-utils", "flate2", - "indexmap 2.7.1", + "indexmap 2.12.0", "memchr", "zopfli", ] diff --git a/Cargo.toml b/Cargo.toml index 7d28bc13d5f5..15a55c891629 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "1.11.0" +version = "1.13.0" authors = ["Block "] license = "Apache-2.0" repository = "https://github.com/block/goose" @@ -12,9 +12,10 @@ description = "An AI agent" [workspace.lints.clippy] uninlined_format_args = "allow" +string_slice = "warn" [workspace.dependencies] -rmcp = { version = "0.8.1", features = ["schemars", "auth"] } +rmcp = { version = "0.8.3", features = ["schemars", "auth"] } # Patch for Windows cross-compilation issue with crunchy [patch.crates-io] diff --git a/HOWTOAI.md b/HOWTOAI.md new file mode 100644 index 000000000000..32efe2cb71d1 --- /dev/null +++ b/HOWTOAI.md @@ -0,0 +1,317 @@ +# How to Use AI with goose +_A practical guide for contributing to goose using AI coding assistants_ + +goose benefits from thoughtful AI-assisted development, but contributors must maintain high standards for code quality, security, and collaboration. Whether you use goose itself, GitHub Copilot, Cursor, Claude, or other AI tools, this guide will help you contribute effectively. + +--- + +## Core Principles + +- **Human Oversight**: You are accountable for all code you submit. Never commit code you don’t understand or can’t maintain. +- **Quality Standards**: AI code must meet the same standards as human written code—tests, docs, and patterns included. +- **Transparency**: Be open about significant AI usage in PRs and explain how you validated it. + +--- + +## Best Practices + +**✅ Recommended Uses** + +- Generating boilerplate code and common patterns +- Creating comprehensive test suites +- Writing documentation and comments +- Refactoring existing code for clarity +- Generating utility functions and helpers +- Explaining existing code patterns + +**❌ Avoid AI For** + +- Complex business logic without thorough review +- Security critical authentication/authorization code +- Code you don’t fully understand +- Large architectural changes +- Database migrations or schema changes + +**Workflow Tips** + +- Start small and validate often. Build, lint, and test incrementally +- Study existing patterns before generating new code +- Always ask: "Is this secure? Does it follow project patterns? What edge cases need testing?" + +**Security Considerations** + +- Extra review required for MCP servers, network code, file system ops, user input, and credential handling +- Never expose secrets in prompts +- Sanitize inputs/outputs and follow goose’s security patterns + +--- + +## Testing & Review + +Before submitting AI assisted code, confirm that: +- You understand every line +- All tests pass locally (happy path + error cases) +- Docs are updated and accurate +- Code follows existing patterns + +**Always get human review** for: + +- Security sensitive code +- Core architecture changes +- Async/concurrency logic +- MCP protocol implementations +- Large refactors or anything you’re unsure about + +--- + +## Using goose for goose development + +- Protect sensitive files with `.gooseignore` (e.g., `.env*`, `*.key`, `target/`, `.git/`) +- Guide goose with `.goosehints` (patterns, error handling, formatting, tests, docs) +- Use `/plan` to structure work, and choose modes wisely: + - **Chat** for understanding + - **Smart Approval** for most dev work + - **Approval** for critical areas + - **Autonomous** only with safety nets + +--- + +## Community & Collaboration + +- In PRs, note significant AI use and how you validated results +- Share prompting tips, patterns, and pitfalls +- Be responsive to feedback and help improve this guide + +--- + +## Remember + +AI is a powerful assistant, not a replacement for your judgment. Use it to speed up development; while keeping your brain engaged, your standards high, and goose secure. + +Questions? Join our [Discord](https://discord.gg/goose-oss) or [GitHub Discussions](https://github.com/block/goose/discussions) to talk more about responsible AI development. + +--- + +## Getting Started with AI Tools + +### Quick Setup + +**Using goose (meta!):** +```bash +# Install goose +curl -fsSL https://github.com/block/goose/releases/latest/download/install.sh | bash + +# Navigate to your goose clone +cd /path/to/goose + +# Start goose in the repo +goose +``` + +**Using GitHub Copilot:** +- Install the [GitHub Copilot extension](https://marketplace.visualstudio.com/items?itemName=GitHub.copilot) for VS Code +- Enable Copilot for Rust files in your settings +- Recommended: Also install [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) for better code intelligence + +**Using Cursor:** +- Download [Cursor](https://cursor.sh/) (VS Code fork with built-in AI) +- Open the goose repository +- Use Cmd/Ctrl+K for inline AI editing, Cmd/Ctrl+L for chat + +**Using Claude or ChatGPT:** +- Copy relevant code sections into the chat interface +- Provide context about the goose architecture (see below) +- Always test generated code locally before committing + +### Rust-Specific Configuration + +If you're new to Rust, configure your AI tool to help you learn: + +**VS Code settings.json:** +```json +{ + "rust-analyzer.checkOnSave.command": "clippy", + "github.copilot.enable": { + "rust": true + } +} +``` + +**Cursor Rules (.cursorrules in repo root):** +``` +This is a Rust project using cargo workspaces. +- Follow existing error handling patterns using anyhow::Result +- Use async/await for I/O operations +- Follow the project's clippy lints (see clippy-baselines/) +- Run cargo fmt before committing +``` + +--- + +## Understanding goose's Architecture + +New to AI agents? Here are key questions to ask your AI tool: + +### Essential Concepts + +**"Explain the goose crate structure"** +``` +Ask: "I'm looking at the goose repository. Can you explain the purpose of each crate +in the crates/ directory and how they relate to each other?" + +Key insight: goose uses a workspace with specialized crates: +- goose: Core agent logic +- goose-cli: Command-line interface +- goose-server: Backend for desktop app (goosed) +- goose-mcp: MCP server implementations +``` + +**"How does the MCP protocol work in goose?"** +``` +Ask: "What is the Model Context Protocol (MCP) and how does goose implement it? +Show me an example from crates/goose-mcp/" + +Key insight: MCP allows goose to connect to external tools and data sources. +Each MCP server provides specific capabilities (developer tools, file access, etc.) +``` + +**"What's the agent execution flow?"** +``` +Ask: "Walk me through what happens when a user sends a message to goose. +Start from crates/goose-cli/src/main.rs" + +Key insight: Message → Agent → Provider (LLM) → Tool execution → Response +``` + +### Navigating the Codebase with AI + +**Finding the right file:** +``` +# Use ripgrep with AI assistance +Ask: "I want to add a new shell command tool. Where should I look?" +AI might suggest: rg "shell" crates/goose-mcp/ -l + +Then ask: "Explain the structure of crates/goose-mcp/src/developer/tools/shell.rs" +``` + +**Understanding patterns:** +``` +Ask: "Show me the pattern for implementing a new Provider in goose" +Then: "What's the difference between streaming and non-streaming providers?" +``` + +--- + +## Practical Examples + +### Example 1: Understanding How to Add a New MCP Tool + +**Scenario:** You want to add a new tool to the developer MCP server. + +**Step 1 - Explore existing tools:** +```bash +# Ask AI: "Show me the structure of an existing MCP tool" +ls crates/goose-mcp/src/developer/tools/ + +# Pick a simple one to study +# Ask AI: "Explain this tool implementation line by line" +cat crates/goose-mcp/src/developer/tools/shell.rs +``` + +**Step 2 - Ask AI to draft your new tool:** +``` +Prompt: "I want to add a new MCP tool called 'git_status' that runs git status +and returns the output. Based on the pattern in shell.rs, draft the implementation." +``` + +**Step 3 - Validate with AI:** +``` +Ask: "Review this code for: +1. Proper error handling using anyhow::Result +2. Security concerns (command injection, etc.) +3. Async/await patterns matching the codebase +4. Test coverage needs" +``` + +**Step 4 - Test locally:** +```bash +# Build and test +cargo build -p goose-mcp +cargo test -p goose-mcp + +# Run clippy +./scripts/clippy-lint.sh +``` + +### Example 2: Fixing a Rust Compiler Error + +**Scenario:** You're getting a lifetime error you don't understand. + +**Step 1 - Copy the full error:** +```bash +cargo build 2>&1 | pbcopy # macOS +cargo build 2>&1 | xclip # Linux +``` + +**Step 2 - Ask AI with context:** +``` +Prompt: "I'm getting this Rust compiler error in the goose project: + +[paste error] + +Here's the relevant code: +[paste code section] + +Explain what's wrong and how to fix it following Rust best practices." +``` + +**Step 3 - Understand the fix:** +``` +Ask: "Explain why this fix works and what I should learn about Rust lifetimes" +``` + +**Step 4 - Apply and verify:** +```bash +# Apply the fix +# Then verify it compiles and tests pass +cargo build +cargo test +``` + +### Example 3: Adding a Feature to the CLI + +**Scenario:** You want to add a new command-line flag to goose-cli. + +**Step 1 - Find the CLI argument parsing:** +```bash +# Ask AI: "Where does goose-cli parse command line arguments?" +rg "clap" crates/goose-cli/src/ -l +``` + +**Step 2 - Study the pattern:** +``` +Ask: "Explain how goose-cli uses clap for argument parsing. +Show me how existing flags are defined." +``` + +**Step 3 - Draft your addition:** +``` +Prompt: "I want to add a --verbose flag that enables debug logging. +Based on the existing patterns in goose-cli, show me: +1. How to add the flag to the CLI args struct +2. How to pass it to the goose core +3. How to use it to control log levels" +``` + +**Step 4 - Implement with validation:** +```bash +# Make changes +# Build both crates +cargo build -p goose-cli -p goose + +# Test the new flag +./target/debug/goose --verbose session + +# Run tests +cargo test -p goose-cli +``` \ No newline at end of file diff --git a/Justfile b/Justfile index ca22e0cd6f27..afa29b714c56 100644 --- a/Justfile +++ b/Justfile @@ -268,6 +268,12 @@ validate version: exit 1 fi +get-next-minor-version: + @python -c "import sys; v=sys.argv[1].split('.'); print(f'{v[0]}.{int(v[1])+1}.0')" $(just get-tag-version) + +get-next-patch-version: + @python -c "import sys; v=sys.argv[1].split('.'); print(f'{v[0]}.{v[1]}.{int(v[2])+1}')" $(just get-tag-version) + # set cargo and app versions, must be semver prepare-release version: @just validate {{ version }} || exit 1 @@ -280,7 +286,8 @@ prepare-release version: # see --workspace flag https://doc.rust-lang.org/cargo/commands/cargo-update.html # used to update Cargo.lock after we've bumped versions in Cargo.toml @cargo update --workspace - @git add Cargo.toml Cargo.lock ui/desktop/package.json ui/desktop/package-lock.json + @just generate-openapi + @git add Cargo.toml Cargo.lock ui/desktop/package.json ui/desktop/package-lock.json ui/desktop/openapi.json @git commit --message "chore(release): release version {{ version }}" # extract version from Cargo.toml diff --git a/README.md b/README.md index db7b7f55dd14..de9bdde7f260 100644 --- a/README.md +++ b/README.md @@ -17,20 +17,6 @@ _a local, extensible, open source AI agent that automates engineering tasks_

-## 🎉 Hacktoberfest 2025 🎉 - -`goose` is a participating project in Hacktoberfest 2025! We’re so excited for your contributions, and have created a wide variety of issues so that anyone can contribute. Whether you're a seasoned developer or a first-time open source contributor, there's something for everyone. - -### To get started: -1. Read the [contributing guide](https://github.com/block/goose/blob/main/CONTRIBUTING.md). -2. Read the [code of conduct](https://github.com/block/.github/blob/main/CODE_OF_CONDUCT.md). -3. Read the [full Responsible AI-Assisted Coding Guide](./ai-assisted-coding-guide.md). -4. Choose a task from this project's Hacktoberfest issues in our [Project Hub](https://github.com/block/goose/issues/4705) and follow the instructions. Each issue has the 🏷️ `hacktoberfest` label. - -Have questions? Connecting with us in our [Discord community](https://discord.gg/goose-oss) in the `#hacktoberfest` project channel. - ---- - goose is your on-machine AI agent, capable of automating complex development tasks from start to finish. More than just code suggestions, goose can build entire projects from scratch, write and execute code, debug failures, orchestrate workflows, and interact with external APIs - _autonomously_. Whether you're prototyping an idea, refining existing code, or managing intricate engineering pipelines, goose adapts to your workflow and executes tasks with precision. @@ -44,8 +30,13 @@ Designed for maximum flexibility, goose works with any LLM and supports multi-mo - [Installation](https://block.github.io/goose/docs/getting-started/installation) - [Tutorials](https://block.github.io/goose/docs/category/tutorials) - [Documentation](https://block.github.io/goose/docs/category/getting-started) +- [Responsible AI-Assisted Coding Guide](https://github.com/block/goose/blob/main/HOWTOAI.md) - [Governance](https://github.com/block/goose/blob/main/GOVERNANCE.md) +## Need Help? +- [Diagnostics & Reporting](https://block.github.io/goose/docs/troubleshooting/diagnostics-and-reporting) +- [Known Issues](https://block.github.io/goose/docs/troubleshooting/known-issues) + # a little goose humor 🦢 > Why did the developer choose goose as their AI agent? diff --git a/ai-assisted-coding-guide.md b/ai-assisted-coding-guide.md deleted file mode 100644 index 3464f0bb4d80..000000000000 --- a/ai-assisted-coding-guide.md +++ /dev/null @@ -1,91 +0,0 @@ -# Responsible AI-Assisted Coding Guide -_Guidelines for contributing responsibly to goose during Hacktoberfest_ - -goose benefits from thoughtful AI assisted development, but contributors must maintain high standards for code quality, security, and collaboration. Whether you use goose, Copilot, Claude, or other AI tools, these principles will help you avoid common pitfalls. - ---- - -## Core Principles - -- **Human Oversight**: You are accountable for all code you submit. Never commit code you don’t understand or can’t maintain. -- **Quality Standards**: AI code must meet the same standards as human written code—tests, docs, and patterns included. -- **Transparency**: Be open about significant AI usage in PRs and explain how you validated it. - ---- - -## Best Practices - -**✅ Recommended Uses** - -- Generating boilerplate code and common patterns -- Creating comprehensive test suites -- Writing documentation and comments -- Refactoring existing code for clarity -- Generating utility functions and helpers -- Explaining existing code patterns - -**❌ Avoid AI For** - -- Complex business logic without thorough review -- Security critical authentication/authorization code -- Code you don’t fully understand -- Large architectural changes -- Database migrations or schema changes - -**Workflow Tips** - -- Start small and validate often—build, lint, and test incrementally -- Study existing patterns before generating new code -- Always ask: “Is this secure? Does it follow project patterns? What edge cases need testing?” - -**Security Considerations** - -- Extra review required for MCP servers, network code, file system ops, user input, and credential handling -- Never expose secrets in prompts -- Sanitize inputs/outputs and follow goose’s security patterns - ---- - -## Testing & Review - -Before submitting AI assisted code, confirm that: -- You understand every line -- All tests pass locally (happy path + error cases) -- Docs are updated and accurate -- Code follows existing patterns - -**Always get human review** for: - -- Security sensitive code -- Core architecture changes -- Async/concurrency logic -- MCP protocol implementations -- Large refactors or anything you’re unsure about - ---- - -## Using goose for goose Development - -- Protect sensitive files with `.gooseignore` (e.g., `.env*`, `*.key`, `target/`, `.git/`) -- Guide Goose with `.goosehints` (patterns, error handling, formatting, tests, docs) -- Use `/plan` to structure work, and choose modes wisely: - - **Chat** for understanding - - **Smart Approval** for most dev work - - **Approval** for critical areas - - **Autonomous** only with safety nets - ---- - -## Community & Collaboration - -- In PRs, note significant AI use and how you validated results -- Share prompting tips, patterns, and pitfalls -- Be responsive to feedback and help improve this guide - ---- - -## Remember - -AI is a powerful assistant, not a replacement for your judgment. Use it to speed up development; while keeping your brain engaged, your standards high, and goose secure. - -Questions? Join our [Discord](https://discord.gg/goose-oss) or [GitHub Discussions](https://github.com/block/goose/discussions) to talk more about responsible AI development. diff --git a/crates/goose-cli/Cargo.toml b/crates/goose-cli/Cargo.toml index 83ebb8644a92..dbb546843778 100644 --- a/crates/goose-cli/Cargo.toml +++ b/crates/goose-cli/Cargo.toml @@ -54,7 +54,7 @@ tower-http = { version = "0.5", features = ["cors", "fs", "auth"] } http = "1.0" webbrowser = "1.0" indicatif = "0.17.11" -tokio-util = { version = "0.7.15", features = ["compat"] } +tokio-util = { version = "0.7.15", features = ["compat", "rt"] } is-terminal = "0.4.16" anstream = "0.6.18" url = "2.5.7" diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs index 4df791843c45..c642e48dbf70 100644 --- a/crates/goose-cli/src/cli.rs +++ b/crates/goose-cli/src/cli.rs @@ -19,6 +19,7 @@ use crate::commands::session::{handle_session_list, handle_session_remove}; use crate::recipes::extract_from_cli::extract_recipe_info_from_cli; use crate::recipes::recipe::{explain_recipe, render_recipe_as_yaml}; use crate::session::{build_session, SessionBuilderConfig, SessionSettings}; +use goose::session::session_manager::SessionType; use goose::session::SessionManager; use goose_bench::bench_config::BenchRunConfig; use goose_bench::runners::bench_runner::BenchRunner; @@ -40,17 +41,17 @@ struct Cli { #[group(required = false, multiple = false)] pub struct Identifier { #[arg( - short, + short = 'n', long, value_name = "NAME", help = "Name for the chat session (e.g., 'project-x')", - long_help = "Specify a name for your chat session. When used with --resume, will resume this specific session if it exists.", - alias = "id" + long_help = "Specify a name for your chat session. When used with --resume, will resume this specific session if it exists." )] pub name: Option, #[arg( long = "session-id", + alias = "id", value_name = "SESSION_ID", help = "Session ID (e.g., '20250921_143022')", long_help = "Specify a session ID directly. When used with --resume, will resume this specific session if it exists." @@ -58,7 +59,6 @@ pub struct Identifier { pub session_id: Option, #[arg( - short, long, value_name = "PATH", help = "Legacy: Path for the chat session", @@ -86,9 +86,12 @@ async fn get_or_create_session_id( .ok_or_else(|| anyhow::anyhow!("No session found to resume"))?; Ok(Some(session_id)) } else { - let session = - SessionManager::create_session(std::env::current_dir()?, "CLI Session".to_string()) - .await?; + let session = SessionManager::create_session( + std::env::current_dir()?, + "CLI Session".to_string(), + SessionType::User, + ) + .await?; Ok(Some(session.id)) }; }; @@ -105,8 +108,12 @@ async fn get_or_create_session_id( .ok_or_else(|| anyhow::anyhow!("No session found with name '{}'", name))?; Ok(Some(session_id)) } else { - let session = - SessionManager::create_session(std::env::current_dir()?, name.clone()).await?; + let session = SessionManager::create_session( + std::env::current_dir()?, + name.clone(), + SessionType::User, + ) + .await?; SessionManager::update_session(&session.id) .user_provided_name(name) @@ -123,9 +130,12 @@ async fn get_or_create_session_id( .ok_or_else(|| anyhow::anyhow!("Could not extract session ID from path: {:?}", path))?; Ok(Some(session_id)) } else { - let session = - SessionManager::create_session(std::env::current_dir()?, "CLI Session".to_string()) - .await?; + let session = SessionManager::create_session( + std::env::current_dir()?, + "CLI Session".to_string(), + SessionType::User, + ) + .await?; Ok(Some(session.id)) } } @@ -177,7 +187,8 @@ enum SessionCommand { ascending: bool, #[arg( - short = 'p', + short = 'w', + short_alias = 'p', long = "working_dir", help = "Filter sessions by working directory" )] @@ -186,16 +197,15 @@ enum SessionCommand { #[arg(short = 'l', long = "limit", help = "Limit the number of results")] limit: Option, }, - #[command(about = "Remove sessions. Runs interactively if no ID or regex is provided.")] + #[command(about = "Remove sessions. Runs interactively if no ID, name, or regex is provided.")] Remove { + #[command(flatten)] + identifier: Option, #[arg( - short, + short = 'r', long, - alias = "name", - help = "Session ID to be removed (optional)" + help = "Regex for removing matched sessions (optional)" )] - id: Option, - #[arg(short, long, help = "Regex for removing matched sessions (optional)")] regex: Option, }, #[command(about = "Export a session")] @@ -221,12 +231,12 @@ enum SessionCommand { }, #[command(name = "diagnostics")] Diagnostics { - /// Session ID to generate diagnostics for - #[arg(short, long)] - session_id: String, + /// Session identifier for generating diagnostics + #[command(flatten)] + identifier: Option, /// Output path for the diagnostics zip file (optional, defaults to current directory) - #[arg(short, long)] + #[arg(short = 'o', long)] output: Option, }, } @@ -235,8 +245,12 @@ enum SessionCommand { enum SchedulerCommand { #[command(about = "Add a new scheduled job")] Add { - #[arg(long, help = "Unique ID for the job")] - id: String, + #[arg( + long = "schedule-id", + alias = "id", + help = "Unique ID for the recurring scheduled job" + )] + schedule_id: String, #[arg( long, help = "Cron expression for the schedule", @@ -253,29 +267,33 @@ enum SchedulerCommand { List {}, #[command(about = "Remove a scheduled job by ID")] Remove { - #[arg(long, help = "ID of the job to remove")] // Changed from positional to named --id - id: String, + #[arg( + long = "schedule-id", + alias = "id", + help = "ID of the scheduled job to remove (removes the recurring schedule)" + )] + schedule_id: String, }, /// List sessions created by a specific schedule #[command(about = "List sessions created by a specific schedule")] Sessions { /// ID of the schedule - #[arg(long, help = "ID of the schedule")] // Explicitly make it --id - id: String, - #[arg(long, help = "Maximum number of sessions to return")] + #[arg(long = "schedule-id", alias = "id", help = "ID of the schedule")] + schedule_id: String, + #[arg(short = 'l', long, help = "Maximum number of sessions to return")] limit: Option, }, #[command(about = "Run a scheduled job immediately")] RunNow { /// ID of the schedule to run - #[arg(long, help = "ID of the schedule to run")] // Explicitly make it --id - id: String, + #[arg(long = "schedule-id", alias = "id", help = "ID of the schedule to run")] + schedule_id: String, }, /// Check status of scheduler services (deprecated - no external services needed) - #[command(about = "Check status of scheduler services")] + #[command(about = "[Deprecated] Check status of scheduler services")] ServicesStatus {}, /// Stop scheduler services (deprecated - no external services needed) - #[command(about = "Stop scheduler services")] + #[command(about = "[Deprecated] Stop scheduler services")] ServicesStop {}, /// Show cron expression examples and help #[command(about = "Show cron expression examples and help")] @@ -424,8 +442,8 @@ enum Command { #[arg( short, long, - help = "Resume a previous session (last used or specified by --name)", - long_help = "Continue from a previous chat session. If --name or --path is provided, resumes that specific session. Otherwise resumes the last used session." + help = "Resume a previous session (last used or specified by --name/--session-id)", + long_help = "Continue from a previous session. If --name or --session-id is provided, resumes that specific session. Otherwise resumes the most recently used session." )] resume: bool, @@ -709,6 +727,16 @@ enum Command { )] additional_sub_recipes: Vec, + /// Output format (text, json) + #[arg( + long = "output-format", + value_name = "FORMAT", + help = "Output format (text, json)", + default_value = "text", + value_parser = clap::builder::PossibleValuesParser::new(["text", "json"]) + )] + output_format: String, + /// Provider to use for this run (overrides environment variable) #[arg( long = "provider", @@ -882,8 +910,13 @@ pub async fn cli() -> anyhow::Result<()> { working_dir, limit, }) => Ok(handle_session_list(format, ascending, working_dir, limit).await?), - Some(SessionCommand::Remove { id, regex }) => { - Ok(handle_session_remove(id, regex).await?) + Some(SessionCommand::Remove { identifier, regex }) => { + let (session_id, name) = if let Some(id) = identifier { + (id.session_id, id.name) + } else { + (None, None) + }; + Ok(handle_session_remove(session_id, name, regex).await?) } Some(SessionCommand::Export { identifier, @@ -912,7 +945,19 @@ pub async fn cli() -> anyhow::Result<()> { .await?; Ok(()) } - Some(SessionCommand::Diagnostics { session_id, output }) => { + Some(SessionCommand::Diagnostics { identifier, output }) => { + let session_id = if let Some(id) = identifier { + lookup_session_id(id).await? + } else { + match crate::commands::session::prompt_interactive_session_selection().await + { + Ok(id) => id, + Err(e) => { + eprintln!("Error: {}", e); + return Ok(()); + } + } + }; crate::commands::session::handle_diagnostics(&session_id, output).await?; Ok(()) } @@ -963,6 +1008,7 @@ pub async fn cli() -> anyhow::Result<()> { sub_recipes: None, final_output_response: None, retry_config: None, + output_format: "text".to_string(), }) .await; @@ -977,7 +1023,7 @@ pub async fn cli() -> anyhow::Result<()> { let exit_type = if result.is_ok() { "normal" } else { "error" }; let (total_tokens, message_count) = session - .get_metadata() + .get_session() .await .map(|m| (m.total_tokens.unwrap_or(0), m.message_count)) .unwrap_or((0, 0)); @@ -1043,6 +1089,7 @@ pub async fn cli() -> anyhow::Result<()> { scheduled_job_id, quiet, additional_sub_recipes, + output_format, provider, model, }) => { @@ -1172,6 +1219,7 @@ pub async fn cli() -> anyhow::Result<()> { .as_ref() .and_then(|r| r.final_output_response.clone()), retry_config: recipe_info.as_ref().and_then(|r| r.retry_config.clone()), + output_format, }) .await; @@ -1198,7 +1246,7 @@ pub async fn cli() -> anyhow::Result<()> { let exit_type = if result.is_ok() { "normal" } else { "error" }; let (total_tokens, message_count) = session - .get_metadata() + .get_session() .await .map(|m| (m.total_tokens.unwrap_or(0), m.message_count)) .unwrap_or((0, 0)); @@ -1240,25 +1288,25 @@ pub async fn cli() -> anyhow::Result<()> { Some(Command::Schedule { command }) => { match command { SchedulerCommand::Add { - id, + schedule_id, cron, recipe_source, } => { - handle_schedule_add(id, cron, recipe_source).await?; + handle_schedule_add(schedule_id, cron, recipe_source).await?; } SchedulerCommand::List {} => { handle_schedule_list().await?; } - SchedulerCommand::Remove { id } => { - handle_schedule_remove(id).await?; + SchedulerCommand::Remove { schedule_id } => { + handle_schedule_remove(schedule_id).await?; } - SchedulerCommand::Sessions { id, limit } => { + SchedulerCommand::Sessions { schedule_id, limit } => { // New arm - handle_schedule_sessions(id, limit).await?; + handle_schedule_sessions(schedule_id, limit).await?; } - SchedulerCommand::RunNow { id } => { + SchedulerCommand::RunNow { schedule_id } => { // New arm - handle_schedule_run_now(id).await?; + handle_schedule_run_now(schedule_id).await?; } SchedulerCommand::ServicesStatus {} => { handle_schedule_services_status().await?; @@ -1355,6 +1403,7 @@ pub async fn cli() -> anyhow::Result<()> { sub_recipes: None, final_output_response: None, retry_config: None, + output_format: "text".to_string(), }) .await; session.interactive(None).await?; diff --git a/crates/goose-cli/src/commands/acp.rs b/crates/goose-cli/src/commands/acp.rs index 8ee9acfe09d4..b1bb76758246 100644 --- a/crates/goose-cli/src/commands/acp.rs +++ b/crates/goose-cli/src/commands/acp.rs @@ -3,12 +3,15 @@ use agent_client_protocol::{ ToolCallContent, }; use anyhow::Result; -use goose::agents::Agent; +use goose::agents::{Agent, SessionConfig}; use goose::config::{get_all_extensions, Config}; use goose::conversation::message::{Message, MessageContent}; use goose::conversation::Conversation; +use goose::mcp_utils::ToolResult; use goose::providers::create; -use rmcp::model::{RawContent, ResourceContents}; +use goose::session::session_manager::SessionType; +use goose::session::SessionManager; +use rmcp::model::{Content, RawContent, ResourceContents}; use std::collections::{HashMap, HashSet}; use std::fs; use std::sync::Arc; @@ -19,20 +22,129 @@ use tokio_util::sync::CancellationToken; use tracing::{error, info, warn}; use url::Url; -/// Represents a single goose session for ACP -struct GooseSession { +struct GooseAcpSession { messages: Conversation, tool_call_ids: HashMap, // Maps internal tool IDs to ACP tool call IDs + tool_requests: HashMap, // Store tool requests by ID for location extraction cancel_token: Option, // Active cancellation token for prompt processing } -/// goose ACP Agent implementation that connects to real goose agents struct GooseAcpAgent { - session_update_tx: mpsc::UnboundedSender<(acp::SessionNotification, oneshot::Sender<()>)>, - sessions: Arc>>, + session_update_tx: mpsc::UnboundedSender<(SessionNotification, oneshot::Sender<()>)>, + sessions: Arc>>, agent: Agent, // Shared agent instance } +/// Create a ToolCallLocation with common defaults +fn create_tool_location(path: &str, line: Option) -> acp::ToolCallLocation { + acp::ToolCallLocation { + path: path.into(), + line, + meta: None, + } +} + +/// Extract file locations from tool request and response +fn extract_tool_locations( + tool_request: &goose::conversation::message::ToolRequest, + tool_response: &goose::conversation::message::ToolResponse, +) -> Vec { + let mut locations = Vec::new(); + + // Get the tool call details + if let Ok(tool_call) = &tool_request.tool_call { + // Only process text_editor tool + if tool_call.name != "developer__text_editor" { + return locations; + } + + // Extract the path from arguments + let path_str = tool_call + .arguments + .as_ref() + .and_then(|args| args.get("path")) + .and_then(|p| p.as_str()); + + if let Some(path_str) = path_str { + // Get the command type + let command = tool_call + .arguments + .as_ref() + .and_then(|args| args.get("command")) + .and_then(|c| c.as_str()); + + // Extract line numbers from the response content + if let Ok(content_items) = &tool_response.tool_result { + for content in content_items { + if let RawContent::Text(text_content) = &content.raw { + let text = &text_content.text; + + // Parse line numbers based on command type and response format + match command { + Some("view") => { + // For view command, look for "lines X-Y" pattern in header + let line = extract_view_line_range(text) + .map(|range| range.0 as u32) + .or(Some(1)); + locations.push(create_tool_location(path_str, line)); + } + Some("str_replace") | Some("insert") => { + // For edits, extract the first line number from the snippet + let line = extract_first_line_number(text) + .map(|l| l as u32) + .or(Some(1)); + locations.push(create_tool_location(path_str, line)); + } + Some("write") => { + // For write, just point to the beginning of the file + locations.push(create_tool_location(path_str, Some(1))); + } + _ => { + // For other commands or unknown, default to line 1 + locations.push(create_tool_location(path_str, Some(1))); + } + } + break; // Only process first text content + } + } + } + + // If we didn't find any locations yet, add a default one + if locations.is_empty() { + locations.push(create_tool_location(path_str, Some(1))); + } + } + } + + locations +} + +/// Extract line range from view command output (e.g., "### path/to/file.rs (lines 10-20)") +fn extract_view_line_range(text: &str) -> Option<(usize, usize)> { + // Look for pattern like "(lines X-Y)" or "(lines X-end)" + let re = regex::Regex::new(r"\(lines (\d+)-(\d+|end)\)").ok()?; + if let Some(caps) = re.captures(text) { + let start = caps.get(1)?.as_str().parse::().ok()?; + let end = if caps.get(2)?.as_str() == "end" { + start // Use start as a reasonable default + } else { + caps.get(2)?.as_str().parse::().ok()? + }; + return Some((start, end)); + } + None +} + +/// Extract the first line number from code snippet (e.g., "123: some code") +fn extract_first_line_number(text: &str) -> Option { + // Look for pattern like "123: " at the start of a line within a code block + let re = regex::Regex::new(r"```[^\n]*\n(\d+):").ok()?; + if let Some(caps) = re.captures(text) { + return caps.get(1)?.as_str().parse::().ok(); + } + None +} + fn read_resource_link(link: acp::ResourceLink) -> Option { let url = Url::parse(&link.uri).ok()?; if url.scheme() == "file" { @@ -97,15 +209,14 @@ impl GooseAcpAgent { async fn new( session_update_tx: mpsc::UnboundedSender<(acp::SessionNotification, oneshot::Sender<()>)>, ) -> Result { - // Load config and create provider let config = Config::global(); let provider_name: String = config - .get_param("GOOSE_PROVIDER") + .get_goose_provider() .map_err(|e| anyhow::anyhow!("No provider configured: {}", e))?; let model_name: String = config - .get_param("GOOSE_MODEL") + .get_goose_model() .map_err(|e| anyhow::anyhow!("No model configured: {}", e))?; let model_config = goose::model::ModelConfig { @@ -217,7 +328,7 @@ impl GooseAcpAgent { &self, content_item: &MessageContent, session_id: &acp::SessionId, - session: &mut GooseSession, + session: &mut GooseAcpSession, ) -> Result<(), acp::Error> { match content_item { MessageContent::Text(text) => { @@ -273,7 +384,7 @@ impl GooseAcpAgent { &self, tool_request: &goose::conversation::message::ToolRequest, session_id: &acp::SessionId, - session: &mut GooseSession, + session: &mut GooseAcpSession, ) -> Result<(), acp::Error> { // Generate ACP tool call ID and track mapping let acp_tool_id = format!("tool_{}", uuid::Uuid::new_v4()); @@ -281,36 +392,19 @@ impl GooseAcpAgent { .tool_call_ids .insert(tool_request.id.clone(), acp_tool_id.clone()); - // Extract tool name and parameters from the ToolCall if successful - let (tool_name, locations) = match &tool_request.tool_call { - Ok(tool_call) => { - // Extract file locations from certain tools for client tracking - let mut locs = Vec::new(); - if tool_call.name == "developer__text_editor" { - // Try to extract the path from the arguments - if let Some(path_str) = tool_call - .arguments - .as_ref() - .and_then(|args_map| args_map.get("path")) - .and_then(|p| p.as_str()) - { - let path = std::path::PathBuf::from(path_str); - if path.exists() && path.is_file() { - locs.push(acp::ToolCallLocation { - path: path_str.into(), - line: Some(1), - meta: None, - }); - } - } - } + // Store the tool request for later use in response handling + session + .tool_requests + .insert(tool_request.id.clone(), tool_request.clone()); - (tool_call.name.to_string(), locs) - } - Err(_) => ("error".to_string(), vec![]), + // Extract tool name from the ToolCall if successful + let tool_name = match &tool_request.tool_call { + Ok(tool_call) => tool_call.name.to_string(), + Err(_) => "error".to_string(), }; - // Send tool call notification + // Send tool call notification with empty locations initially + // We'll update with real locations when we get the response let (tx, rx) = oneshot::channel(); self.session_update_tx .send(( @@ -322,7 +416,7 @@ impl GooseAcpAgent { kind: acp::ToolKind::default(), status: acp::ToolCallStatus::Pending, content: Vec::new(), - locations, + locations: Vec::new(), // Will be populated in handle_tool_response raw_input: None, raw_output: None, meta: None, @@ -341,7 +435,7 @@ impl GooseAcpAgent { &self, tool_response: &goose::conversation::message::ToolResponse, session_id: &acp::SessionId, - session: &mut GooseSession, + session: &mut GooseAcpSession, ) -> Result<(), acp::Error> { // Look up the ACP tool call ID if let Some(acp_tool_id) = session.tool_call_ids.get(&tool_response.id) { @@ -352,74 +446,17 @@ impl GooseAcpAgent { acp::ToolCallStatus::Failed }; - let content: Vec = match &tool_response.tool_result { - Ok(content_items) => content_items - .iter() - .filter_map(|content| match &content.raw { - RawContent::Text(val) => Some(ToolCallContent::Content { - content: acp::ContentBlock::Text(TextContent { - annotations: None, - text: val.text.clone(), - meta: None, - }), - }), - RawContent::Image(val) => Some(ToolCallContent::Content { - content: acp::ContentBlock::Image(ImageContent { - annotations: None, - data: val.data.clone(), - mime_type: val.mime_type.clone(), - uri: None, - meta: None, - }), - }), - RawContent::Resource(val) => Some(ToolCallContent::Content { - content: acp::ContentBlock::Resource(EmbeddedResource { - annotations: None, - resource: match &val.resource { - ResourceContents::TextResourceContents { - mime_type, - text, - uri, - .. - } => acp::EmbeddedResourceResource::TextResourceContents( - acp::TextResourceContents { - mime_type: mime_type.clone(), - text: text.clone(), - uri: uri.clone(), - meta: None, - }, - ), - ResourceContents::BlobResourceContents { - mime_type, - blob, - uri, - .. - } => acp::EmbeddedResourceResource::BlobResourceContents( - acp::BlobResourceContents { - mime_type: mime_type.clone(), - blob: blob.clone(), - uri: uri.clone(), - meta: None, - }, - ), - }, - meta: None, - }), - }), - RawContent::Audio(_) => { - // Audio content is not supported in ACP ContentBlock, skip it - None - } - RawContent::ResourceLink(_) => { - // ResourceLink content is not supported in ACP ContentBlock, skip it - None - } - }) - .collect(), - Err(_) => Vec::new(), + let content = build_tool_call_content(&tool_response.tool_result); + + // Extract locations from the tool request and response + let locations = if let Some(tool_request) = session.tool_requests.get(&tool_response.id) + { + extract_tool_locations(tool_request, tool_response) + } else { + Vec::new() }; - // Send status update (completed or failed) + // Send status update (completed or failed) with locations let (tx, rx) = oneshot::channel(); self.session_update_tx .send(( @@ -430,6 +467,11 @@ impl GooseAcpAgent { fields: acp::ToolCallUpdateFields { status: Some(status), content: Some(content), + locations: if locations.is_empty() { + None + } else { + Some(locations) + }, ..Default::default() }, meta: None, @@ -446,6 +488,76 @@ impl GooseAcpAgent { } } +/// Build tool call content from tool result +fn build_tool_call_content(tool_result: &ToolResult>) -> Vec { + match tool_result { + Ok(content_items) => content_items + .iter() + .filter_map(|content| match &content.raw { + RawContent::Text(val) => Some(ToolCallContent::Content { + content: acp::ContentBlock::Text(TextContent { + annotations: None, + text: val.text.clone(), + meta: None, + }), + }), + RawContent::Image(val) => Some(ToolCallContent::Content { + content: acp::ContentBlock::Image(ImageContent { + annotations: None, + data: val.data.clone(), + mime_type: val.mime_type.clone(), + uri: None, + meta: None, + }), + }), + RawContent::Resource(val) => Some(ToolCallContent::Content { + content: acp::ContentBlock::Resource(EmbeddedResource { + annotations: None, + resource: match &val.resource { + ResourceContents::TextResourceContents { + mime_type, + text, + uri, + .. + } => acp::EmbeddedResourceResource::TextResourceContents( + acp::TextResourceContents { + mime_type: mime_type.clone(), + text: text.clone(), + uri: uri.clone(), + meta: None, + }, + ), + ResourceContents::BlobResourceContents { + mime_type, + blob, + uri, + .. + } => acp::EmbeddedResourceResource::BlobResourceContents( + acp::BlobResourceContents { + mime_type: mime_type.clone(), + blob: blob.clone(), + uri: uri.clone(), + meta: None, + }, + ), + }, + meta: None, + }), + }), + RawContent::Audio(_) => { + // Audio content is not supported in ACP ContentBlock, skip it + None + } + RawContent::ResourceLink(_) => { + // ResourceLink content is not supported in ACP ContentBlock, skip it + None + } + }) + .collect(), + Err(_) => Vec::new(), + } +} + #[async_trait::async_trait(?Send)] impl acp::Agent for GooseAcpAgent { async fn initialize( @@ -496,9 +608,10 @@ impl acp::Agent for GooseAcpAgent { // Generate a unique session ID let session_id = uuid::Uuid::new_v4().to_string(); - let session = GooseSession { + let session = GooseAcpSession { messages: Conversation::new_unvalidated(Vec::new()), tool_call_ids: HashMap::new(), + tool_requests: HashMap::new(), cancel_token: None, }; @@ -544,30 +657,35 @@ impl acp::Agent for GooseAcpAgent { // Create and store cancellation token for this prompt let cancel_token = CancellationToken::new(); - // Convert ACP prompt to Goose message - let user_message = self.convert_acp_prompt_to_message(args.prompt); - - // Prepare for agent reply - let messages = { + // Store the cancel token in the session BEFORE starting the prompt + { let mut sessions = self.sessions.lock().await; let session = sessions .get_mut(&session_id) .ok_or_else(acp::Error::invalid_params)?; + session.cancel_token = Some(cancel_token.clone()); + } - // Add message to conversation - session.messages.push(user_message); + let user_message = self.convert_acp_prompt_to_message(args.prompt); - // Store cancellation token - session.cancel_token = Some(cancel_token.clone()); + let session = SessionManager::create_session( + std::env::current_dir().unwrap_or_default(), + "ACP Session".to_string(), + SessionType::Hidden, + ) + .await?; - // Clone what we need for the reply call - session.messages.clone() + let session_config = SessionConfig { + id: session.id.clone(), + schedule_id: None, + max_turns: None, + retry_config: None, }; // Get agent's reply through the Goose agent let mut stream = self .agent - .reply(messages, None, Some(cancel_token.clone())) + .reply(user_message, session_config, Some(cancel_token.clone())) .await .map_err(|e| { error!("Error getting agent reply: {}", e); diff --git a/crates/goose-cli/src/commands/bench.rs b/crates/goose-cli/src/commands/bench.rs index d67b91913151..c0005fa5400c 100644 --- a/crates/goose-cli/src/commands/bench.rs +++ b/crates/goose-cli/src/commands/bench.rs @@ -26,9 +26,7 @@ impl BenchBaseSession for CliSession { } fn get_session_id(&self) -> anyhow::Result { - self.session_id() - .cloned() - .ok_or_else(|| anyhow::anyhow!("No session ID available")) + Ok(self.session_id().to_string()) } } pub async fn agent_generator( @@ -57,6 +55,7 @@ pub async fn agent_generator( sub_recipes: None, final_output_response: None, retry_config: None, + output_format: "text".to_string(), }) .await; diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index d4d4f8f07729..d0d670f97bd1 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -10,10 +10,12 @@ use goose::config::extensions::{ get_all_extension_names, get_all_extensions, get_enabled_extensions, get_extension_by_name, name_to_key, remove_extension, set_extension, set_extension_enabled, }; +use goose::config::paths::Paths; use goose::config::permission::PermissionLevel; use goose::config::signup_tetrate::TetrateAuth; use goose::config::{ - configure_tetrate, Config, ConfigError, ExperimentManager, ExtensionEntry, PermissionManager, + configure_tetrate, Config, ConfigError, ExperimentManager, ExtensionEntry, GooseMode, + PermissionManager, }; use goose::conversation::message::Message; use goose::model::ModelConfig; @@ -195,15 +197,17 @@ pub async fn handle_configure() -> anyhow::Result<()> { } Ok(()) } else { + let config_dir = Paths::config_dir().display().to_string(); + println!(); println!( "{}", - style("This will update your existing config file").dim() + style("This will update your existing config files").dim() ); println!( "{} {}", - style(" if you prefer, you can edit it directly at").dim(), - config.path() + style(" if you prefer, you can edit them directly at").dim(), + config_dir ); println!(); @@ -418,7 +422,7 @@ fn select_model_from_list( } fn try_store_secret(config: &Config, key_name: &str, value: String) -> anyhow::Result { - match config.set_secret(key_name, Value::String(value)) { + match config.set_secret(key_name, &value) { Ok(_) => Ok(true), Err(e) => { cliclack::outro(style(format!( @@ -447,7 +451,7 @@ pub async fn configure_provider_dialog() -> anyhow::Result { .collect(); // Get current default provider if it exists - let current_provider: Option = config.get_param("GOOSE_PROVIDER").ok(); + let current_provider: Option = config.get_goose_provider().ok(); let default_provider = current_provider.unwrap_or_default(); // Select provider @@ -484,9 +488,9 @@ pub async fn configure_provider_dialog() -> anyhow::Result { return Ok(false); } } else { - config.set_param(&key.name, Value::String(env_value))?; + config.set_param(&key.name, &env_value)?; } - let _ = cliclack::log::info(format!("Saved {} to config file", key.name)); + let _ = cliclack::log::info(format!("Saved {} to {}", key.name, config.path())); } } None => { @@ -526,7 +530,7 @@ pub async fn configure_provider_dialog() -> anyhow::Result { return Ok(false); } } else { - config.set_param(&key.name, Value::String(value))?; + config.set_param(&key.name, &value)?; } } } @@ -555,9 +559,9 @@ pub async fn configure_provider_dialog() -> anyhow::Result { }; if key.secret { - config.set_secret(&key.name, Value::String(value))?; + config.set_secret(&key.name, &value)?; } else { - config.set_param(&key.name, Value::String(value))?; + config.set_param(&key.name, &value)?; } } } @@ -645,10 +649,9 @@ pub async fn configure_provider_dialog() -> anyhow::Result { match result { Ok((_message, _usage)) => { - // Update config with new values only if the test succeeds - config.set_param("GOOSE_PROVIDER", Value::String(provider_name.to_string()))?; - config.set_param("GOOSE_MODEL", Value::String(model.clone()))?; - cliclack::outro("Configuration saved successfully")?; + config.set_goose_provider(provider_name)?; + config.set_goose_model(&model)?; + print_config_file_saved()?; Ok(true) } Err(e) => { @@ -709,7 +712,11 @@ pub fn toggle_extensions_dialog() -> anyhow::Result<()> { ); } - cliclack::outro("Extension settings updated successfully")?; + let config = Config::global(); + cliclack::outro(format!( + "Extension settings saved successfully to {}", + config.path() + ))?; Ok(()) } @@ -756,7 +763,6 @@ pub fn configure_extensions_dialog() -> anyhow::Result<()> { "Developer Tools", "Code editing and shell access", ), - ("jetbrains", "JetBrains", "Connect to jetbrains IDEs"), ( "memory", "Memory", @@ -870,7 +876,7 @@ pub fn configure_extensions_dialog() -> anyhow::Result<()> { // Try to store in keychain let keychain_key = key.to_string(); - match config.set_secret(&keychain_key, Value::String(value.clone())) { + match config.set_secret(&keychain_key, &value) { Ok(_) => { // Successfully stored in keychain, add to env_keys env_keys.push(keychain_key); @@ -966,7 +972,7 @@ pub fn configure_extensions_dialog() -> anyhow::Result<()> { // Try to store in keychain let keychain_key = key.to_string(); - match config.set_secret(&keychain_key, Value::String(value.clone())) { + match config.set_secret(&keychain_key, &value) { Ok(_) => { // Successfully stored in keychain, add to env_keys env_keys.push(keychain_key); @@ -1086,7 +1092,7 @@ pub fn configure_extensions_dialog() -> anyhow::Result<()> { // Try to store in keychain let keychain_key = key.to_string(); - match config.set_secret(&keychain_key, Value::String(value.clone())) { + match config.set_secret(&keychain_key, &Value::String(value.clone())) { Ok(_) => { // Successfully stored in keychain, add to env_keys env_keys.push(keychain_key); @@ -1123,6 +1129,8 @@ pub fn configure_extensions_dialog() -> anyhow::Result<()> { _ => unreachable!(), }; + print_config_file_saved()?; + Ok(()) } @@ -1178,6 +1186,8 @@ pub fn remove_extension_dialog() -> anyhow::Result<()> { cliclack::outro(format!("Removed {} extension", style(name).green()))?; } + print_config_file_saved()?; + Ok(()) } @@ -1216,6 +1226,8 @@ pub async fn configure_settings_dialog() -> anyhow::Result<()> { ) .interact()?; + let mut should_print_config_path = true; + match setting_type { "goose_mode" => { configure_goose_mode_dialog()?; @@ -1225,6 +1237,8 @@ pub async fn configure_settings_dialog() -> anyhow::Result<()> { } "tool_permission" => { configure_tool_permissions_dialog().await.and(Ok(()))?; + // No need to print config file path since it's already handled. + should_print_config_path = false; } "tool_output" => { configure_tool_output_dialog()?; @@ -1241,6 +1255,10 @@ pub async fn configure_settings_dialog() -> anyhow::Result<()> { _ => unreachable!(), }; + if should_print_config_path { + print_config_file_saved()?; + } + Ok(()) } @@ -1254,46 +1272,35 @@ pub fn configure_goose_mode_dialog() -> anyhow::Result<()> { let mode = cliclack::select("Which goose mode would you like to configure?") .item( - "auto", + GooseMode::Auto, "Auto Mode", "Full file modification, extension usage, edit, create and delete files freely" ) .item( - "approve", + GooseMode::Approve, "Approve Mode", "All tools, extensions and file modifications will require human approval" ) .item( - "smart_approve", + GooseMode::SmartApprove, "Smart Approve Mode", "Editing, creating, deleting files and using extensions will require human approval" ) .item( - "chat", + GooseMode::Chat, "Chat Mode", "Engage with the selected provider without using tools, extensions, or file modification" ) .interact()?; - match mode { - "auto" => { - config.set_param("GOOSE_MODE", Value::String("auto".to_string()))?; - cliclack::outro("Set to Auto Mode - full file modification enabled")?; - } - "approve" => { - config.set_param("GOOSE_MODE", Value::String("approve".to_string()))?; - cliclack::outro("Set to Approve Mode - all tools and modifications require approval")?; - } - "smart_approve" => { - config.set_param("GOOSE_MODE", Value::String("smart_approve".to_string()))?; - cliclack::outro("Set to Smart Approve Mode - modifications require approval")?; - } - "chat" => { - config.set_param("GOOSE_MODE", Value::String("chat".to_string()))?; - cliclack::outro("Set to Chat Mode - no tools or modifications enabled")?; - } - _ => unreachable!(), + config.set_goose_mode(mode)?; + let msg = match mode { + GooseMode::Auto => "Set to Auto Mode - full file modification enabled", + GooseMode::Approve => "Set to Approve Mode - all tools and modifications require approval", + GooseMode::SmartApprove => "Set to Smart Approve Mode - modifications require approval", + GooseMode::Chat => "Set to Chat Mode - no tools or modifications enabled", }; + cliclack::outro(msg)?; Ok(()) } @@ -1302,28 +1309,25 @@ pub fn configure_goose_router_strategy_dialog() -> anyhow::Result<()> { let enable_router = cliclack::select("Would you like to enable smart tool routing?") .item( - "true", + true, "Enable Router", "Use LLM-based intelligence to select tools", ) .item( - "false", + false, "Disable Router", "Use the default tool selection strategy", ) .interact()?; - match enable_router { - "true" => { - config.set_param("GOOSE_ENABLE_ROUTER", Value::String("true".to_string()))?; - cliclack::outro("Router enabled - using LLM-based intelligence for tool selection")?; - } - "false" => { - config.set_param("GOOSE_ENABLE_ROUTER", Value::String("false".to_string()))?; - cliclack::outro("Router disabled - using default tool selection")?; - } - _ => unreachable!(), + config.set_param("GOOSE_ENABLE_ROUTER", enable_router)?; + let msg = if enable_router { + "Router enabled - using LLM-based intelligence for tool selection" + } else { + "Router disabled - using default tool selection" }; + cliclack::outro(msg)?; + Ok(()) } @@ -1341,15 +1345,15 @@ pub fn configure_tool_output_dialog() -> anyhow::Result<()> { match tool_log_level { "high" => { - config.set_param("GOOSE_CLI_MIN_PRIORITY", Value::from(0.8))?; + config.set_param("GOOSE_CLI_MIN_PRIORITY", 0.8)?; cliclack::outro("Showing tool output of high importance only.")?; } "medium" => { - config.set_param("GOOSE_CLI_MIN_PRIORITY", Value::from(0.2))?; + config.set_param("GOOSE_CLI_MIN_PRIORITY", 0.2)?; cliclack::outro("Showing tool output of medium importance.")?; } "all" => { - config.set_param("GOOSE_CLI_MIN_PRIORITY", Value::from(0.0))?; + config.set_param("GOOSE_CLI_MIN_PRIORITY", 0.0)?; cliclack::outro("Showing all tool output.")?; } _ => unreachable!(), @@ -1422,11 +1426,11 @@ pub async fn configure_tool_permissions_dialog() -> anyhow::Result<()> { let config = Config::global(); let provider_name: String = config - .get_param("GOOSE_PROVIDER") + .get_goose_provider() .expect("No provider configured. Please set model provider first"); let model: String = config - .get_param("GOOSE_MODEL") + .get_goose_model() .expect("No model configured. Please set model first"); let model_config = ModelConfig::new(&model)?; @@ -1547,6 +1551,11 @@ pub async fn configure_tool_permissions_dialog() -> anyhow::Result<()> { tool.name, permission_label ))?; + cliclack::outro(format!( + "Changes saved to {}", + permission_manager.get_config_path().display() + ))?; + Ok(()) } @@ -1567,7 +1576,7 @@ fn configure_recipe_dialog() -> anyhow::Result<()> { if input_value.clone().trim().is_empty() { config.delete(key_name)?; } else { - config.set_param(key_name, Value::String(input_value))?; + config.set_param(key_name, &input_value)?; } Ok(()) } @@ -1594,7 +1603,7 @@ pub fn configure_max_turns_dialog() -> anyhow::Result<()> { .interact()?; let max_turns: u32 = max_turns_input.parse()?; - config.set_param("GOOSE_MAX_TURNS", Value::from(max_turns))?; + config.set_param("GOOSE_MAX_TURNS", max_turns)?; cliclack::outro(format!( "Set maximum turns to {} - goose will ask for input after {} consecutive actions", @@ -1627,7 +1636,7 @@ pub async fn handle_openrouter_auth() -> anyhow::Result<()> { // Test configuration - get the model that was configured println!("\nTesting configuration..."); - let configured_model: String = config.get_param("GOOSE_MODEL")?; + let configured_model: String = config.get_goose_model()?; let model_config = match goose::model::ModelConfig::new(&configured_model) { Ok(config) => config, Err(e) => { @@ -1705,7 +1714,7 @@ pub async fn handle_tetrate_auth() -> anyhow::Result<()> { // Test configuration println!("\nTesting configuration..."); - let configured_model: String = config.get_param("GOOSE_MODEL")?; + let configured_model: String = config.get_goose_model()?; let model_config = match goose::model::ModelConfig::new(&configured_model) { Ok(config) => config, Err(e) => { @@ -1893,5 +1902,18 @@ pub fn configure_custom_provider_dialog() -> anyhow::Result<()> { "add" => add_provider(), "remove" => remove_provider(), _ => unreachable!(), - } + }?; + + print_config_file_saved()?; + + Ok(()) +} + +fn print_config_file_saved() -> anyhow::Result<()> { + let config = Config::global(); + cliclack::outro(format!( + "Configuration saved successfully to {}", + config.path() + ))?; + Ok(()) } diff --git a/crates/goose-cli/src/commands/info.rs b/crates/goose-cli/src/commands/info.rs index baf972d76826..de3b83831464 100644 --- a/crates/goose-cli/src/commands/info.rs +++ b/crates/goose-cli/src/commands/info.rs @@ -11,15 +11,16 @@ fn print_aligned(label: &str, value: &str, width: usize) { pub fn handle_info(verbose: bool) -> Result<()> { let logs_dir = Paths::in_state_dir("logs"); let sessions_dir = Paths::in_data_dir("sessions"); + let sessions_db = sessions_dir.join("sessions.db"); // Get paths using a stored reference to the global config let config = Config::global(); - let config_file = config.path(); + let config_dir = Paths::config_dir().display().to_string(); // Define the labels and their corresponding path values once. let paths = [ - ("Config file:", config_file.to_string()), - ("Sessions dir:", sessions_dir.display().to_string()), + ("Config dir:", config_dir), + ("Sessions DB (sqlite):", sessions_db.display().to_string()), ("Logs dir:", logs_dir.display().to_string()), ]; @@ -40,26 +41,22 @@ pub fn handle_info(verbose: bool) -> Result<()> { // Print verbose info if requested if verbose { println!("\n{}", style("goose Configuration:").cyan().bold()); - match config.load_values() { - Ok(values) => { - if values.is_empty() { - println!(" No configuration values set"); - println!( - " Run '{}' to configure goose", - style("goose configure").cyan() - ); - } else { - let sorted_values: std::collections::BTreeMap<_, _> = - values.iter().map(|(k, v)| (k.clone(), v.clone())).collect(); + let values = config.all_values()?; + if values.is_empty() { + println!(" No configuration values set"); + println!( + " Run '{}' to configure goose", + style("goose configure").cyan() + ); + } else { + let sorted_values: std::collections::BTreeMap<_, _> = + values.iter().map(|(k, v)| (k.clone(), v.clone())).collect(); - if let Ok(yaml) = serde_yaml::to_string(&sorted_values) { - for line in yaml.lines() { - println!(" {}", line); - } - } + if let Ok(yaml) = serde_yaml::to_string(&sorted_values) { + for line in yaml.lines() { + println!(" {}", line); } } - Err(e) => println!(" Error loading configuration: {}", e), } } diff --git a/crates/goose-cli/src/commands/schedule.rs b/crates/goose-cli/src/commands/schedule.rs index 2756fa2e44c0..f8773f8da7ee 100644 --- a/crates/goose-cli/src/commands/schedule.rs +++ b/crates/goose-cli/src/commands/schedule.rs @@ -75,13 +75,13 @@ fn validate_cron_expression(cron: &str) -> Result<()> { } pub async fn handle_schedule_add( - id: String, + schedule_id: String, cron: String, recipe_source_arg: String, // This is expected to be a file path by the Scheduler ) -> Result<()> { println!( "[CLI Debug] Scheduling job ID: {}, Cron: {}, Recipe Source Path: {}", - id, cron, recipe_source_arg + schedule_id, cron, recipe_source_arg ); // Validate cron expression and provide helpful feedback @@ -90,7 +90,7 @@ pub async fn handle_schedule_add( // The Scheduler's add_scheduled_job will handle copying the recipe from recipe_source_arg // to its internal storage and validating the path. let job = ScheduledJob { - id: id.clone(), + id: schedule_id.clone(), source: recipe_source_arg.clone(), // Pass the original user-provided path cron, last_run: None, @@ -98,7 +98,6 @@ pub async fn handle_schedule_add( paused: false, current_session_id: None, process_start_time: None, - execution_mode: Some("background".to_string()), // Default to background for CLI }; let scheduler_storage_path = @@ -117,11 +116,12 @@ pub async fn handle_schedule_add( .extension() .and_then(|ext| ext.to_str()) .unwrap_or("yaml"); - let final_recipe_path = scheduled_recipes_dir.join(format!("{}.{}", id, extension)); + let final_recipe_path = + scheduled_recipes_dir.join(format!("{}.{}", schedule_id, extension)); println!( "Scheduled job '{}' added. Recipe expected at {:?}", - id, final_recipe_path + schedule_id, final_recipe_path ); Ok(()) } @@ -139,7 +139,7 @@ pub async fn handle_schedule_add( ); } _ => Err(anyhow::Error::new(e)) - .context(format!("Failed to add job '{}' to scheduler", id)), + .context(format!("Failed to add job '{}' to scheduler", schedule_id)), } } } @@ -180,41 +180,46 @@ pub async fn handle_schedule_list() -> Result<()> { Ok(()) } -pub async fn handle_schedule_remove(id: String) -> Result<()> { +pub async fn handle_schedule_remove(schedule_id: String) -> Result<()> { let scheduler_storage_path = get_default_scheduler_storage_path().context("Failed to get scheduler storage path")?; let scheduler = SchedulerFactory::create(scheduler_storage_path) .await .context("Failed to initialize scheduler")?; - match scheduler.remove_scheduled_job(&id).await { + match scheduler.remove_scheduled_job(&schedule_id).await { Ok(_) => { - println!("Scheduled job '{}' and its associated recipe removed.", id); + println!( + "Scheduled job '{}' and its associated recipe removed.", + schedule_id + ); Ok(()) } Err(e) => match e { SchedulerError::JobNotFound(job_id) => { bail!("Error: Job with ID '{}' not found.", job_id); } - _ => Err(anyhow::Error::new(e)) - .context(format!("Failed to remove job '{}' from scheduler", id)), + _ => Err(anyhow::Error::new(e)).context(format!( + "Failed to remove job '{}' from scheduler", + schedule_id + )), }, } } -pub async fn handle_schedule_sessions(id: String, limit: Option) -> Result<()> { +pub async fn handle_schedule_sessions(schedule_id: String, limit: Option) -> Result<()> { let scheduler_storage_path = get_default_scheduler_storage_path().context("Failed to get scheduler storage path")?; let scheduler = SchedulerFactory::create(scheduler_storage_path) .await .context("Failed to initialize scheduler")?; - match scheduler.sessions(&id, limit.unwrap_or(50)).await { + match scheduler.sessions(&schedule_id, limit.unwrap_or(50)).await { Ok(sessions) => { if sessions.is_empty() { - println!("No sessions found for schedule ID '{}'.", id); + println!("No sessions found for schedule ID '{}'.", schedule_id); } else { - println!("Sessions for schedule ID '{}':", id); + println!("Sessions for schedule ID '{}':", schedule_id); // sessions is now Vec<(String, SessionMetadata)> for (session_name, metadata) in sessions { println!( @@ -228,31 +233,35 @@ pub async fn handle_schedule_sessions(id: String, limit: Option) -> Resul } } Err(e) => { - bail!("Failed to get sessions for schedule '{}': {:?}", id, e); + bail!( + "Failed to get sessions for schedule '{}': {:?}", + schedule_id, + e + ); } } Ok(()) } -pub async fn handle_schedule_run_now(id: String) -> Result<()> { +pub async fn handle_schedule_run_now(schedule_id: String) -> Result<()> { let scheduler_storage_path = get_default_scheduler_storage_path().context("Failed to get scheduler storage path")?; let scheduler = SchedulerFactory::create(scheduler_storage_path) .await .context("Failed to initialize scheduler")?; - match scheduler.run_now(&id).await { + match scheduler.run_now(&schedule_id).await { Ok(session_id) => { println!( "Successfully triggered schedule '{}'. New session ID: {}", - id, session_id + schedule_id, session_id ); } Err(e) => match e { SchedulerError::JobNotFound(job_id) => { bail!("Error: Job with ID '{}' not found.", job_id); } - _ => bail!("Failed to run schedule '{}' now: {:?}", id, e), + _ => bail!("Failed to run schedule '{}' now: {:?}", schedule_id, e), }, } Ok(()) @@ -327,12 +336,12 @@ pub async fn handle_schedule_cron_help() -> Result<()> { println!("💡 EXAMPLES:"); println!( - " goose schedule add --id hourly-report --cron \"0 * * * *\" --recipe-source report.yaml" + " goose schedule add --schedule-id hourly-report --cron \"0 * * * *\" --recipe-source report.yaml" ); println!( - " goose schedule add --id daily-backup --cron \"@daily\" --recipe-source backup.yaml" + " goose schedule add --schedule-id daily-backup --cron \"@daily\" --recipe-source backup.yaml" ); - println!(" goose schedule add --id weekly-summary --cron \"0 9 * * 1\" --recipe-source summary.yaml"); + println!(" goose schedule add --schedule-id weekly-summary --cron \"0 9 * * 1\" --recipe-source summary.yaml"); Ok(()) } diff --git a/crates/goose-cli/src/commands/session.rs b/crates/goose-cli/src/commands/session.rs index 40e1e1d689f3..a145f01ffd33 100644 --- a/crates/goose-cli/src/commands/session.rs +++ b/crates/goose-cli/src/commands/session.rs @@ -71,7 +71,11 @@ fn prompt_interactive_session_removal(sessions: &[Session]) -> Result, regex_string: Option) -> Result<()> { +pub async fn handle_session_remove( + session_id: Option, + name: Option, + regex_string: Option, +) -> Result<()> { let all_sessions = match SessionManager::list_sessions().await { Ok(sessions) => sessions, Err(e) => { @@ -82,11 +86,20 @@ pub async fn handle_session_remove(id: Option, regex_string: Option; - if let Some(id_val) = id { + if let Some(id_val) = session_id { if let Some(session) = all_sessions.iter().find(|s| s.id == id_val) { matched_sessions = vec![session.clone()]; } else { - return Err(anyhow::anyhow!("Session '{}' not found.", id_val)); + return Err(anyhow::anyhow!("Session ID '{}' not found.", id_val)); + } + } else if let Some(name_val) = name { + if let Some(session) = all_sessions.iter().find(|s| s.name == name_val) { + matched_sessions = vec![session.clone()]; + } else { + return Err(anyhow::anyhow!( + "Session with name '{}' not found.", + name_val + )); } } else if let Some(regex_val) = regex_string { let session_regex = Regex::new(®ex_val) diff --git a/crates/goose-cli/src/commands/web.rs b/crates/goose-cli/src/commands/web.rs index 636b2590ebbb..b47ddcdf8d85 100644 --- a/crates/goose-cli/src/commands/web.rs +++ b/crates/goose-cli/src/commands/web.rs @@ -15,6 +15,7 @@ use base64::Engine; use futures::{sink::SinkExt, stream::StreamExt}; use goose::agents::{Agent, AgentEvent}; use goose::conversation::message::Message as GooseMessage; +use goose::session::session_manager::SessionType; use goose::session::SessionManager; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -139,7 +140,7 @@ pub async fn handle_web( let config = goose::config::Config::global(); - let provider_name: String = match config.get_param("GOOSE_PROVIDER") { + let provider_name: String = match config.get_goose_provider() { Ok(p) => p, Err(_) => { eprintln!("No provider configured. Run 'goose configure' first"); @@ -147,7 +148,7 @@ pub async fn handle_web( } }; - let model: String = match config.get_param("GOOSE_MODEL") { + let model: String = match config.get_goose_model() { Ok(m) => m, Err(_) => { eprintln!("No model configured. Run 'goose configure' first"); @@ -226,6 +227,7 @@ async fn serve_index() -> Result { let session = SessionManager::create_session( std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")), "Web session".to_string(), + SessionType::User, ) .await .map_err(|err| (http::StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?; @@ -467,27 +469,20 @@ async fn process_message_streaming( let session = SessionManager::get_session(&session_id, true).await?; let mut messages = session.conversation.unwrap_or_default(); - messages.push(user_message); + messages.push(user_message.clone()); let session_config = SessionConfig { id: session.id.clone(), - working_dir: session.working_dir, schedule_id: None, - execution_mode: None, max_turns: None, retry_config: None, }; - match agent - .reply(messages.clone(), Some(session_config), None) - .await - { + match agent.reply(user_message, session_config, None).await { Ok(mut stream) => { while let Some(result) = stream.next().await { match result { Ok(AgentEvent::Message(message)) => { - SessionManager::add_message(&session_id, &message).await?; - for content in &message.content { match content { MessageContent::Text(text) => { diff --git a/crates/goose-cli/src/logging.rs b/crates/goose-cli/src/logging.rs index 9d3887246047..acd9d16d23ee 100644 --- a/crates/goose-cli/src/logging.rs +++ b/crates/goose-cli/src/logging.rs @@ -1,5 +1,4 @@ use anyhow::{Context, Result}; -use std::path::PathBuf; use std::sync::Arc; use std::sync::Once; use tokio::sync::Mutex; @@ -16,12 +15,6 @@ use goose_bench::error_capture::ErrorCaptureLayer; // Used to ensure we only set up tracing once static INIT: Once = Once::new(); -/// Returns the directory where log files should be stored. -/// Creates the directory structure if it doesn't exist. -fn get_log_directory() -> Result { - goose::logging::get_log_directory("cli", true) -} - /// Sets up the logging infrastructure for the application. /// This includes: /// - File-based logging with JSON formatting (DEBUG level) @@ -50,20 +43,15 @@ fn setup_logging_internal( let mut setup = || { result = (|| { - // Set up file appender for goose module logs - let log_dir = get_log_directory()?; + let log_dir = goose::logging::prepare_log_directory("cli", true)?; let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S").to_string(); - - // Create log file name by prefixing with timestamp let log_filename = if name.is_some() { format!("{}-{}.log", timestamp, name.unwrap()) } else { format!("{}.log", timestamp) }; - - // Create non-rolling file appender for detailed logs let file_appender = tracing_appender::rolling::RollingFileAppender::new( - Rotation::NEVER, + Rotation::NEVER, // we do manual rotation via file naming and cleanup_old_logs log_dir, log_filename, ); @@ -102,7 +90,9 @@ fn setup_logging_internal( } if !force { - if let Ok((otlp_tracing_layer, otlp_metrics_layer)) = otlp_layer::init_otlp() { + if let Ok((otlp_tracing_layer, otlp_metrics_layer, otlp_logs_layer)) = + otlp_layer::init_otlp() + { layers.push( otlp_tracing_layer .with_filter(otlp_layer::create_otlp_tracing_filter()) @@ -113,6 +103,11 @@ fn setup_logging_internal( .with_filter(otlp_layer::create_otlp_metrics_filter()) .boxed(), ); + layers.push( + otlp_logs_layer + .with_filter(otlp_layer::create_otlp_logs_filter()) + .boxed(), + ); } } @@ -170,7 +165,7 @@ mod tests { #[test] fn test_log_directory_creation() { let _temp_dir = setup_temp_home(); - let log_dir = get_log_directory().unwrap(); + let log_dir = goose::logging::prepare_log_directory("cli", true).unwrap(); assert!(log_dir.exists()); assert!(log_dir.is_dir()); diff --git a/crates/goose-cli/src/recipes/recipe.rs b/crates/goose-cli/src/recipes/recipe.rs index 987b7d295b6e..ff722657289a 100644 --- a/crates/goose-cli/src/recipes/recipe.rs +++ b/crates/goose-cli/src/recipes/recipe.rs @@ -11,7 +11,6 @@ use goose::recipe::build_recipe::{ }; use goose::recipe::validate_recipe::parse_and_validate_parameters; use goose::recipe::Recipe; -use serde_json::Value; fn create_user_prompt_callback() -> impl Fn(&str, &str) -> Result { |key: &str, description: &str| -> Result { @@ -98,7 +97,7 @@ pub fn collect_missing_secrets(requirements: &[SecretRequirement]) -> Result<()> .unwrap_or_else(|_| String::new()); if !value.trim().is_empty() { - config.set_secret(&req.key, Value::String(value))?; + config.set_secret(&req.key, &value)?; println!("✅ Secret stored securely for {}", req.extension_name); } else { println!("⏭️ Skipped {} for {}", req.key, req.extension_name); diff --git a/crates/goose-cli/src/recipes/secret_discovery.rs b/crates/goose-cli/src/recipes/secret_discovery.rs index 6a259d21e96f..8820ec409cf1 100644 --- a/crates/goose-cli/src/recipes/secret_discovery.rs +++ b/crates/goose-cli/src/recipes/secret_discovery.rs @@ -166,7 +166,6 @@ mod tests { available_tools: Vec::new(), }, ]), - context: None, settings: None, activities: None, author: None, @@ -210,7 +209,6 @@ mod tests { instructions: Some("Test instructions".to_string()), prompt: None, extensions: None, - context: None, settings: None, activities: None, author: None, @@ -255,7 +253,6 @@ mod tests { available_tools: Vec::new(), }, ]), - context: None, settings: None, activities: None, author: None, @@ -309,7 +306,6 @@ mod tests { sequential_when_repeated: false, description: None, }]), - context: None, settings: None, activities: None, author: None, diff --git a/crates/goose-cli/src/scenario_tests/scenario_runner.rs b/crates/goose-cli/src/scenario_tests/scenario_runner.rs index 00a90778d3ae..8d7474d4859c 100644 --- a/crates/goose-cli/src/scenario_tests/scenario_runner.rs +++ b/crates/goose-cli/src/scenario_tests/scenario_runner.rs @@ -9,8 +9,10 @@ use anyhow::Result; use goose::agents::Agent; use goose::model::ModelConfig; use goose::providers::{create, testprovider::TestProvider}; +use goose::session::session_manager::SessionType; +use goose::session::SessionManager; use std::collections::{HashMap, HashSet}; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::sync::Arc; use tokio_util::sync::CancellationToken; @@ -190,7 +192,6 @@ where ) }; - // Generate messages using the provider let messages = vec![message_generator(&*provider_arc)]; let mock_client = weather_client(); @@ -218,11 +219,27 @@ where .update_provider(provider_arc as Arc) .await?; - let mut session = CliSession::new(agent, None, false, None, None, None, None).await; + let session = SessionManager::create_session( + PathBuf::default(), + "scenario-runner".to_string(), + SessionType::Hidden, + ) + .await?; + let mut cli_session = CliSession::new( + agent, + session.id, + false, + None, + None, + None, + None, + "text".to_string(), + ) + .await; let mut error = None; for message in &messages { - if let Err(e) = session + if let Err(e) = cli_session .process_message(message.clone(), CancellationToken::default()) .await { @@ -230,7 +247,7 @@ where break; } } - let updated_messages = session.message_history(); + let updated_messages = cli_session.message_history(); if let Some(ref err_msg) = error { if err_msg.contains("No recorded response found") { @@ -249,7 +266,7 @@ where validator(&result)?; - drop(session); + drop(cli_session); if let Some(provider) = provider_for_saving { if result.error.is_none() { diff --git a/crates/goose-cli/src/session/builder.rs b/crates/goose-cli/src/session/builder.rs index 7e0f0fce7df1..6845e3f2e222 100644 --- a/crates/goose-cli/src/session/builder.rs +++ b/crates/goose-cli/src/session/builder.rs @@ -11,6 +11,7 @@ use goose::providers::create; use goose::recipe::{Response, SubRecipe}; use goose::agents::extension::PlatformExtensionContext; +use goose::session::session_manager::SessionType; use goose::session::SessionManager; use goose::session::{EnabledExtensionsState, ExtensionState}; use rustyline::EditMode; @@ -23,9 +24,9 @@ use tokio::task::JoinSet; /// /// This struct contains all the parameters needed to create a new session, /// including session identification, extension configuration, and debug settings. -#[derive(Default, Clone, Debug)] +#[derive(Clone, Debug)] pub struct SessionBuilderConfig { - /// Optional session ID for resuming or identifying an existing session + /// Session id, optional need to deduce from context pub session_id: Option, /// Whether to resume an existing session pub resume: bool, @@ -67,6 +68,39 @@ pub struct SessionBuilderConfig { pub final_output_response: Option, /// Retry configuration for automated validation and recovery pub retry_config: Option, + /// Output format (text, json) + pub output_format: String, +} + +/// Manual implementation of Default to ensure proper initialization of output_format +/// This struct requires explicit default value for output_format field +impl Default for SessionBuilderConfig { + fn default() -> Self { + SessionBuilderConfig { + session_id: None, + resume: false, + no_session: false, + extensions: Vec::new(), + remote_extensions: Vec::new(), + streamable_http_extensions: Vec::new(), + builtins: Vec::new(), + extensions_override: None, + additional_system_prompt: None, + settings: None, + provider: None, + model: None, + debug: false, + max_tool_repetitions: None, + max_turns: None, + scheduled_job_id: None, + interactive: false, + quiet: false, + sub_recipes: None, + final_output_response: None, + retry_config: None, + output_format: "text".to_string(), + } + } } /// Offers to help debug an extension failure by creating a minimal debugging session @@ -132,8 +166,23 @@ async fn offer_extension_debugging_help( } } - // Create the debugging session - let mut debug_session = CliSession::new(debug_agent, None, false, None, None, None, None).await; + let session = SessionManager::create_session( + std::env::current_dir()?, + "CLI Session".to_string(), + SessionType::Hidden, + ) + .await?; + let mut debug_session = CliSession::new( + debug_agent, + session.id, + false, + None, + None, + None, + None, + "text".to_string(), + ) + .await; // Process the debugging request println!("{}", style("Analyzing the extension failure...").yellow()); @@ -208,7 +257,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { .as_ref() .and_then(|s| s.goose_provider.clone()) }) - .or_else(|| config.get_param("GOOSE_PROVIDER").ok()) + .or_else(|| config.get_goose_provider().ok()) .expect("No provider configured. Run 'goose configure' first"); let model_name = session_config @@ -219,7 +268,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { .as_ref() .and_then(|s| s.goose_model.clone()) }) - .or_else(|| config.get_param("GOOSE_MODEL").ok()) + .or_else(|| config.get_goose_model().ok()) .expect("No model configured. Run 'goose configure' first"); let temperature = session_config.settings.as_ref().and_then(|s| s.temperature); @@ -234,13 +283,13 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { // Create the agent let agent: Agent = Agent::new(); - if let Some(sub_recipes) = session_config.sub_recipes { - agent.add_sub_recipes(sub_recipes).await; - } - - if let Some(final_output_response) = session_config.final_output_response { - agent.add_final_output_tool(final_output_response).await; - } + agent + .apply_recipe_components( + session_config.sub_recipes, + session_config.final_output_response, + true, + ) + .await; let new_provider = match create(&provider_name, model_config).await { Ok(provider) => provider, @@ -278,12 +327,20 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { process::exit(1); }); - let session_id: Option = if session_config.no_session { - None + let session_id: String = if session_config.no_session { + let working_dir = std::env::current_dir().expect("Could not get working directory"); + let session = SessionManager::create_session( + working_dir, + "CLI Session".to_string(), + SessionType::Hidden, + ) + .await + .expect("Could not create session"); + session.id } else if session_config.resume { if let Some(session_id) = session_config.session_id { match SessionManager::get_session(&session_id, false).await { - Ok(_) => Some(session_id), + Ok(_) => session_id, Err(_) => { output::render_error(&format!( "Cannot resume session {} - no such session exists", @@ -294,7 +351,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { } } else { match SessionManager::list_sessions().await { - Ok(sessions) if !sessions.is_empty() => Some(sessions[0].id.clone()), + Ok(sessions) if !sessions.is_empty() => sessions[0].id.clone(), _ => { output::render_error("Cannot resume - no previous sessions found"); process::exit(1); @@ -302,46 +359,44 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { } } } else { - session_config.session_id + session_config.session_id.unwrap() }; agent .extension_manager .set_context(PlatformExtensionContext { - session_id: session_id.clone(), + session_id: Some(session_id.clone()), extension_manager: Some(Arc::downgrade(&agent.extension_manager)), tool_route_manager: Some(Arc::downgrade(&agent.tool_route_manager)), }) .await; if session_config.resume { - if let Some(session_id) = session_id.as_ref() { - let metadata = SessionManager::get_session(session_id, false) - .await - .unwrap_or_else(|e| { - output::render_error(&format!("Failed to read session metadata: {}", e)); - process::exit(1); - }); + let session = SessionManager::get_session(&session_id, false) + .await + .unwrap_or_else(|e| { + output::render_error(&format!("Failed to read session metadata: {}", e)); + process::exit(1); + }); - let current_workdir = - std::env::current_dir().expect("Failed to get current working directory"); - if current_workdir != metadata.working_dir { - let change_workdir = cliclack::confirm(format!("{} The original working directory of this session was set to {}. Your current directory is {}. Do you want to switch back to the original working directory?", style("WARNING:").yellow(), style(metadata.working_dir.display()).cyan(), style(current_workdir.display()).cyan())) + let current_workdir = + std::env::current_dir().expect("Failed to get current working directory"); + if current_workdir != session.working_dir { + let change_workdir = cliclack::confirm(format!("{} The original working directory of this session was set to {}. Your current directory is {}. Do you want to switch back to the original working directory?", style("WARNING:").yellow(), style(session.working_dir.display()).cyan(), style(current_workdir.display()).cyan())) .initial_value(true) .interact().expect("Failed to get user input"); - if change_workdir { - if !metadata.working_dir.exists() { - output::render_error(&format!( - "Cannot switch to original working directory - {} no longer exists", - style(metadata.working_dir.display()).cyan() - )); - } else if let Err(e) = std::env::set_current_dir(&metadata.working_dir) { - output::render_error(&format!( - "Failed to switch to original working directory: {}", - e - )); - } + if change_workdir { + if !session.working_dir.exists() { + output::render_error(&format!( + "Cannot switch to original working directory - {} no longer exists", + style(session.working_dir.display()).cyan() + )); + } else if let Err(e) = std::env::set_current_dir(&session.working_dir) { + output::render_error(&format!( + "Failed to switch to original working directory: {}", + e + )); } } } @@ -354,22 +409,18 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { agent.disable_router_for_recipe().await; extensions.into_iter().collect() } else if session_config.resume { - if let Some(session_id) = session_id.as_ref() { - match SessionManager::get_session(session_id, false).await { - Ok(session_data) => { - if let Some(saved_state) = - EnabledExtensionsState::from_extension_data(&session_data.extension_data) - { - check_missing_extensions_or_exit(&saved_state.extensions); - saved_state.extensions - } else { - get_enabled_extensions() - } + match SessionManager::get_session(&session_id, false).await { + Ok(session_data) => { + if let Some(saved_state) = + EnabledExtensionsState::from_extension_data(&session_data.extension_data) + { + check_missing_extensions_or_exit(&saved_state.extensions); + saved_state.extensions + } else { + get_enabled_extensions() } - _ => get_enabled_extensions(), } - } else { - get_enabled_extensions() + _ => get_enabled_extensions(), } } else { get_enabled_extensions() @@ -450,6 +501,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { session_config.max_turns, edit_mode, session_config.retry_config.clone(), + session_config.output_format.clone(), ) .await; @@ -560,23 +612,19 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { } } - if let Some(session_id) = session_id.as_ref() { - let session_config_for_save = SessionConfig { - id: session_id.clone(), - working_dir: std::env::current_dir().unwrap_or_default(), - schedule_id: None, - execution_mode: None, - max_turns: None, - retry_config: None, - }; + let session_config_for_save = SessionConfig { + id: session_id.clone(), + schedule_id: None, + max_turns: None, + retry_config: None, + }; - if let Err(e) = session - .agent - .save_extension_state(&session_config_for_save) - .await - { - tracing::warn!("Failed to save initial extension state: {}", e); - } + if let Err(e) = session + .agent + .save_extension_state(&session_config_for_save) + .await + { + tracing::warn!("Failed to save initial extension state: {}", e); } // Add CLI-specific system prompt extension @@ -603,7 +651,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { session_config.resume, &provider_name, &model_name, - &session_id, + &Some(session_id), Some(&provider_for_display), ); } @@ -638,6 +686,7 @@ mod tests { sub_recipes: None, final_output_response: None, retry_config: None, + output_format: "text".to_string(), }; assert_eq!(config.extensions.len(), 1); diff --git a/crates/goose-cli/src/session/completion.rs b/crates/goose-cli/src/session/completion.rs index 67021047458c..6c32529a9aba 100644 --- a/crates/goose-cli/src/session/completion.rs +++ b/crates/goose-cli/src/session/completion.rs @@ -26,7 +26,7 @@ impl GooseCompleter { /// Complete prompt names for the /prompt command fn complete_prompt_names(&self, line: &str) -> Result<(usize, Vec)> { // Get the prefix of the prompt name being typed - let prefix = if line.len() > 8 { &line[8..] } else { "" }; + let prefix = line.get(8..).unwrap_or(""); // Get available prompts from cache let cache = self.completion_cache.read().unwrap(); @@ -156,7 +156,7 @@ impl GooseCompleter { /// Complete argument keys for a specific prompt fn complete_argument_keys(&self, line: &str) -> Result<(usize, Vec)> { - let parts: Vec<&str> = line[8..].split_whitespace().collect(); + let parts: Vec<&str> = line.get(8..).unwrap_or("").split_whitespace().collect(); // We need at least the prompt name if parts.is_empty() { diff --git a/crates/goose-cli/src/session/input.rs b/crates/goose-cli/src/session/input.rs index 987415d1251f..34d385291f26 100644 --- a/crates/goose-cli/src/session/input.rs +++ b/crates/goose-cli/src/session/input.rs @@ -20,7 +20,7 @@ pub enum InputResult { EndPlan, Clear, Recipe(Option), - Summarize, + Compact, } #[derive(Debug)] @@ -74,6 +74,7 @@ pub fn get_input( Ok(text) => text, Err(e) => match e { rustyline::error::ReadlineError::Interrupted => return Ok(InputResult::Exit), + rustyline::error::ReadlineError::Eof => return Ok(InputResult::Exit), _ => return Err(e.into()), }, }; @@ -120,7 +121,8 @@ fn handle_slash_command(input: &str) -> Option { const CMD_ENDPLAN: &str = "/endplan"; const CMD_CLEAR: &str = "/clear"; const CMD_RECIPE: &str = "/recipe"; - const CMD_SUMMARIZE: &str = "/summarize"; + const CMD_COMPACT: &str = "/compact"; + const CMD_SUMMARIZE_DEPRECATED: &str = "/summarize"; match input { "/exit" | "/quit" => Some(InputResult::Exit), @@ -168,19 +170,25 @@ fn handle_slash_command(input: &str) -> Option { } } s if s.starts_with(CMD_EXTENSION) => Some(InputResult::AddExtension( - s[CMD_EXTENSION.len()..].to_string(), + s.get(CMD_EXTENSION.len()..).unwrap_or("").to_string(), )), - s if s.starts_with(CMD_BUILTIN) => { - Some(InputResult::AddBuiltin(s[CMD_BUILTIN.len()..].to_string())) - } - s if s.starts_with(CMD_MODE) => { - Some(InputResult::GooseMode(s[CMD_MODE.len()..].to_string())) + s if s.starts_with(CMD_BUILTIN) => Some(InputResult::AddBuiltin( + s.get(CMD_BUILTIN.len()..).unwrap_or("").to_string(), + )), + s if s.starts_with(CMD_MODE) => Some(InputResult::GooseMode( + s.get(CMD_MODE.len()..).unwrap_or("").to_string(), + )), + s if s.starts_with(CMD_PLAN) => { + parse_plan_command(s.get(CMD_PLAN.len()..).unwrap_or("").trim().to_string()) } - s if s.starts_with(CMD_PLAN) => parse_plan_command(s[CMD_PLAN.len()..].trim().to_string()), s if s == CMD_ENDPLAN => Some(InputResult::EndPlan), s if s == CMD_CLEAR => Some(InputResult::Clear), s if s.starts_with(CMD_RECIPE) => parse_recipe_command(s), - s if s == CMD_SUMMARIZE => Some(InputResult::Summarize), + s if s == CMD_COMPACT => Some(InputResult::Compact), + s if s == CMD_SUMMARIZE_DEPRECATED => { + println!("{}", console::style("⚠️ Note: /summarize has been renamed to /compact and will be removed in a future release.").yellow()); + Some(InputResult::Compact) + } _ => None, } } @@ -194,7 +202,7 @@ fn parse_recipe_command(s: &str) -> Option { } // Extract the filepath from the command - let filepath = s[CMD_RECIPE.len()..].trim(); + let filepath = s.get(CMD_RECIPE.len()..).unwrap_or("").trim(); if filepath.is_empty() { return Some(InputResult::Recipe(None)); @@ -305,7 +313,7 @@ fn print_help() { /endplan - Exit plan mode and return to 'normal' goose mode. /recipe [filepath] - Generate a recipe from the current conversation and save it to the specified filepath (must end with .yaml). If no filepath is provided, it will be saved to ./recipe.yaml. -/summarize - Summarize the current conversation to reduce context length while preserving key information. +/compact - Compact the current conversation to reduce context length while preserving key information. /? or /help - Display this help message /clear - Clears the current chat history @@ -541,17 +549,6 @@ mod tests { assert!(matches!(result, Some(InputResult::Retry))); } - #[test] - fn test_summarize_command() { - // Test the summarize command - let result = handle_slash_command("/summarize"); - assert!(matches!(result, Some(InputResult::Summarize))); - - // Test with whitespace - let result = handle_slash_command(" /summarize "); - assert!(matches!(result, Some(InputResult::Summarize))); - } - #[test] fn test_get_input_prompt_string() { let prompt = get_input_prompt_string(); diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index f4712ecb3351..469b06e11b9b 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -12,6 +12,9 @@ use crate::session::task_execution_display::{ }; use goose::conversation::Conversation; use std::io::Write; +use std::str::FromStr; +use tokio::signal::ctrl_c; +use tokio_util::task::AbortOnDropHandle; pub use self::export::message_to_markdown; pub use builder::{build_session, SessionBuilderConfig, SessionSettings}; @@ -25,10 +28,10 @@ use goose::utils::safe_truncate; use anyhow::{Context, Result}; use completion::GooseCompleter; -use goose::agents::extension::{Envs, ExtensionConfig}; +use goose::agents::extension::{Envs, ExtensionConfig, PLATFORM_EXTENSIONS}; use goose::agents::types::RetryConfig; -use goose::agents::{Agent, SessionConfig}; -use goose::config::Config; +use goose::agents::{Agent, SessionConfig, MANUAL_COMPACT_TRIGGER}; +use goose::config::{Config, GooseMode}; use goose::providers::pricing::initialize_pricing_cache; use goose::session::SessionManager; use input::InputResult; @@ -40,6 +43,7 @@ use goose::config::paths::Paths; use goose::conversation::message::{Message, MessageContent}; use rand::{distributions::Alphanumeric, Rng}; use rustyline::EditMode; +use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; use std::path::PathBuf; @@ -49,6 +53,18 @@ use tokio; use tokio_util::sync::CancellationToken; use tracing::warn; +#[derive(Serialize, Deserialize, Debug)] +struct JsonOutput { + messages: Vec, + metadata: JsonMetadata, +} + +#[derive(Serialize, Deserialize, Debug)] +struct JsonMetadata { + total_tokens: Option, + status: String, +} + pub enum RunMode { Normal, Plan, @@ -57,7 +73,7 @@ pub enum RunMode { pub struct CliSession { agent: Agent, messages: Conversation, - session_id: Option, + session_id: String, completion_cache: Arc>, debug: bool, run_mode: RunMode, @@ -65,6 +81,7 @@ pub struct CliSession { max_turns: Option, edit_mode: Option, retry_config: Option, + output_format: String, } // Cache structure for completion data @@ -119,23 +136,21 @@ pub async fn classify_planner_response( } impl CliSession { + #[allow(clippy::too_many_arguments)] pub async fn new( agent: Agent, - session_id: Option, + session_id: String, debug: bool, scheduled_job_id: Option, max_turns: Option, edit_mode: Option, retry_config: Option, + output_format: String, ) -> Self { - let messages = if let Some(session_id) = &session_id { - SessionManager::get_session(session_id, true) - .await - .map(|session| session.conversation.unwrap_or_default()) - .unwrap() - } else { - Conversation::new_unvalidated(Vec::new()) - }; + let messages = SessionManager::get_session(&session_id, true) + .await + .map(|session| session.conversation.unwrap_or_default()) + .unwrap(); CliSession { agent, @@ -148,11 +163,12 @@ impl CliSession { max_turns, edit_mode, retry_config, + output_format, } } - pub fn session_id(&self) -> Option<&String> { - self.session_id.as_ref() + pub fn session_id(&self) -> &String { + &self.session_id } /// Add a stdio extension to the session @@ -283,15 +299,24 @@ impl CliSession { /// * `builtin_name` - Name of the builtin extension(s), comma separated pub async fn add_builtin(&mut self, builtin_name: String) -> Result<()> { for name in builtin_name.split(',') { - let extension_name = name.trim().to_string(); - let config = ExtensionConfig::Builtin { - name: extension_name, - display_name: None, - // TODO: should set a timeout - timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT), - bundled: None, - description: name.trim().to_string(), - available_tools: Vec::new(), + let extension_name = name.trim(); + + let config = if PLATFORM_EXTENSIONS.contains_key(extension_name) { + ExtensionConfig::Platform { + name: extension_name.to_string(), + bundled: None, + description: name.to_string(), + available_tools: Vec::new(), + } + } else { + ExtensionConfig::Builtin { + name: extension_name.to_string(), + display_name: None, + timeout: None, + bundled: None, + description: name.to_string(), + available_tools: Vec::new(), + } }; self.agent .add_extension(config) @@ -358,9 +383,6 @@ impl CliSession { cancel_token: CancellationToken, ) -> Result<()> { let cancel_token = cancel_token.clone(); - - // TODO(Douwe): Make sure we generate the description here still: - self.push_message(message); self.process_agent_response(false, cancel_token).await?; Ok(()) @@ -442,7 +464,7 @@ impl CliSession { // Track the current directory and last instruction in projects.json if let Err(e) = crate::project_tracker::update_project_tracker( Some(&content), - self.session_id.as_deref(), + Some(&self.session_id), ) { eprintln!("Warning: Failed to update project tracker with instruction: {}", e); } @@ -494,6 +516,10 @@ impl CliSession { let current = output::get_theme(); let new_theme = match current { + output::Theme::Ansi => { + println!("Switching to Light theme"); + output::Theme::Light + } output::Theme::Light => { println!("Switching to Dark theme"); output::Theme::Dark @@ -502,10 +528,6 @@ impl CliSession { println!("Switching to Ansi theme"); output::Theme::Ansi } - output::Theme::Ansi => { - println!("Switching to Light theme"); - output::Theme::Light - } }; output::set_theme(new_theme); continue; @@ -545,21 +567,18 @@ impl CliSession { save_history(&mut editor); let config = Config::global(); - let mode = mode.to_lowercase(); - - // Check if mode is valid - if !["auto", "approve", "chat", "smart_approve"].contains(&mode.as_str()) { - output::render_error(&format!( - "Invalid mode '{}'. Mode must be one of: auto, approve, chat", - mode - )); - continue; - } - - config - .set_param("GOOSE_MODE", Value::String(mode.to_string())) - .unwrap(); - output::goose_mode_message(&format!("Goose mode set to '{}'", mode)); + let mode = match GooseMode::from_str(&mode.to_lowercase()) { + Ok(mode) => mode, + Err(_) => { + output::render_error(&format!( + "Invalid mode '{}'. Mode must be one of: auto, approve, chat, smart_approve", + mode + )); + continue; + } + }; + config.set_goose_mode(mode)?; + output::goose_mode_message(&format!("Goose mode set to '{:?}'", mode)); continue; } input::InputResult::Plan(options) => { @@ -585,16 +604,14 @@ impl CliSession { input::InputResult::Clear => { save_history(&mut editor); - if let Some(session_id) = &self.session_id { - if let Err(e) = SessionManager::replace_conversation( - session_id, - &Conversation::default(), - ) - .await - { - output::render_error(&format!("Failed to clear session: {}", e)); - continue; - } + if let Err(e) = SessionManager::replace_conversation( + &self.session_id, + &Conversation::default(), + ) + .await + { + output::render_error(&format!("Failed to clear session: {}", e)); + continue; } self.messages.clear(); @@ -643,16 +660,16 @@ impl CliSession { continue; } - InputResult::Summarize => { + InputResult::Compact => { save_history(&mut editor); - let prompt = "Are you sure you want to summarize this conversation? This will condense the message history."; + let prompt = "Are you sure you want to compact this conversation? This will condense the message history."; let should_summarize = match cliclack::confirm(prompt).initial_value(true).interact() { Ok(choice) => choice, Err(e) => { if e.kind() == std::io::ErrorKind::Interrupted { - false // If interrupted, set should_summarize to false + false } else { return Err(e.into()); } @@ -660,90 +677,23 @@ impl CliSession { }; if should_summarize { - println!("{}", console::style("Summarizing conversation...").yellow()); + self.push_message(Message::user().with_text(MANUAL_COMPACT_TRIGGER)); output::show_thinking(); - - let (summarized_messages, _token_counts, summarization_usage) = - goose::context_mgmt::compact_messages( - &self.agent, - &self.messages, - false, - ) + self.process_agent_response(true, CancellationToken::default()) .await?; - - // Update the session messages with the summarized ones - self.messages = summarized_messages.clone(); - - // Persist the summarized messages and update session metadata - if let Some(session_id) = &self.session_id { - // Replace all messages with the summarized version - SessionManager::replace_conversation(session_id, &summarized_messages) - .await?; - - // Update session metadata with the new token counts from summarization - if let Some(usage) = summarization_usage { - let session = - SessionManager::get_session(session_id, false).await?; - - // Update token counts with the summarization usage - let summary_tokens = usage.usage.output_tokens.unwrap_or(0); - - // Update accumulated tokens (add the summarization cost) - let accumulate = |a: Option, b: Option| -> Option { - match (a, b) { - (Some(x), Some(y)) => Some(x + y), - _ => a.or(b), - } - }; - - let accumulated_total = accumulate( - session.accumulated_total_tokens, - usage.usage.total_tokens, - ); - let accumulated_input = accumulate( - session.accumulated_input_tokens, - usage.usage.input_tokens, - ); - let accumulated_output = accumulate( - session.accumulated_output_tokens, - usage.usage.output_tokens, - ); - - SessionManager::update_session(session_id) - .total_tokens(Some(summary_tokens)) - .input_tokens(None) - .output_tokens(Some(summary_tokens)) - .accumulated_total_tokens(accumulated_total) - .accumulated_input_tokens(accumulated_input) - .accumulated_output_tokens(accumulated_output) - .apply() - .await?; - } - } - output::hide_thinking(); - println!( - "{}", - console::style("Conversation has been summarized.").green() - ); - println!( - "{}", - console::style( - "Key information has been preserved while reducing context length." - ) - .green() - ); } else { - println!("{}", console::style("Summarization cancelled.").yellow()); + println!("{}", console::style("Compaction cancelled.").yellow()); } continue; } } } - if let Some(id) = &self.session_id { - println!("Closing session. Session ID: {}", console::style(id).cyan()); - } + println!( + "Closing session. Session ID: {}", + console::style(&self.session_id).cyan() + ); Ok(()) } @@ -787,12 +737,9 @@ impl CliSession { self.run_mode = RunMode::Normal; // set goose mode: auto if that isn't already the case let config = Config::global(); - let curr_goose_mode = - config.get_param("GOOSE_MODE").unwrap_or("auto".to_string()); - if curr_goose_mode != "auto" { - config - .set_param("GOOSE_MODE", Value::String("auto".to_string())) - .unwrap(); + let curr_goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto); + if curr_goose_mode != GooseMode::Auto { + config.set_goose_mode(GooseMode::Auto).unwrap(); } // clear the messages before acting on the plan @@ -807,10 +754,8 @@ impl CliSession { output::hide_thinking(); // Reset run & goose mode - if curr_goose_mode != "auto" { - config - .set_param("GOOSE_MODE", Value::String(curr_goose_mode.to_string())) - .unwrap(); + if curr_goose_mode != GooseMode::Auto { + config.set_goose_mode(curr_goose_mode)?; } } else { // add the plan response (assistant message) & carry the conversation forward @@ -841,26 +786,39 @@ impl CliSession { interactive: bool, cancel_token: CancellationToken, ) -> Result<()> { - let cancel_token_clone = cancel_token.clone(); + // Cache the output format check to avoid repeated string comparisons in the hot loop + let is_json_mode = self.output_format == "json"; - let session_config = self.session_id.as_ref().map(|session_id| SessionConfig { - id: session_id.clone(), - working_dir: std::env::current_dir().unwrap_or_default(), + let session_config = SessionConfig { + id: self.session_id.clone(), schedule_id: self.scheduled_job_id.clone(), - execution_mode: None, max_turns: self.max_turns, retry_config: self.retry_config.clone(), + }; + let user_message = self + .messages + .last() + .ok_or_else(|| anyhow::anyhow!("No user message"))?; + + let cancel_token_interrupt = cancel_token.clone(); + let handle = tokio::spawn(async move { + if ctrl_c().await.is_ok() { + cancel_token_interrupt.cancel(); + } }); + let _drop_handle = AbortOnDropHandle::new(handle); + let mut stream = self .agent .reply( - self.messages.clone(), + user_message.clone(), session_config.clone(), Some(cancel_token.clone()), ) .await?; let mut progress_bars = output::McpSpinners::new(); + let cancel_token_clone = cancel_token.clone(); use futures::StreamExt; loop { @@ -972,11 +930,16 @@ impl CliSession { ); } } + self.messages.push(message.clone()); if interactive {output::hide_thinking()}; let _ = progress_bars.hide(); - output::render_message(&message, self.debug); + + // Don't render in JSON mode + if !is_json_mode { + output::render_message(&message, self.debug); + } } } Some(Ok(AgentEvent::McpNotification((_id, message)))) => { @@ -1048,17 +1011,21 @@ impl CliSession { // TODO: proper display for subagent notifications if interactive { let _ = progress_bars.hide(); - println!("{}", console::style(&formatted_message).green().dim()); - } else { + if !is_json_mode { + println!("{}", console::style(&formatted_message).green().dim()); + } + } else if !is_json_mode { progress_bars.log(&formatted_message); } } else if let Some(ref notification_type) = message_notification_type { if notification_type == TASK_EXECUTION_NOTIFICATION_TYPE { if interactive { let _ = progress_bars.hide(); - print!("{}", formatted_message); - std::io::stdout().flush().unwrap(); - } else { + if !is_json_mode { + print!("{}", formatted_message); + std::io::stdout().flush().unwrap(); + } + } else if !is_json_mode { print!("{}", formatted_message); std::io::stdout().flush().unwrap(); } @@ -1127,8 +1094,7 @@ impl CliSession { None => break, } } - _ = tokio::signal::ctrl_c() => { - cancel_token_clone.cancel(); + _ = cancel_token_clone.cancelled() => { drop(stream); if let Err(e) = self.handle_interrupted_messages(true).await { eprintln!("Error handling interruption: {}", e); @@ -1137,7 +1103,29 @@ impl CliSession { } } } - println!(); + + // Output JSON if requested + if is_json_mode { + let metadata = match SessionManager::get_session(&self.session_id, false).await { + Ok(session) => JsonMetadata { + total_tokens: session.total_tokens, + status: "completed".to_string(), + }, + Err(_) => JsonMetadata { + total_tokens: None, + status: "completed".to_string(), + }, + }; + + let json_output = JsonOutput { + messages: self.messages.messages().to_vec(), + metadata, + }; + + println!("{}", serde_json::to_string_pretty(&json_output)?); + } else { + println!(); + } Ok(()) } @@ -1299,16 +1287,13 @@ impl CliSession { ); } - pub async fn get_metadata(&self) -> Result { - match &self.session_id { - Some(id) => SessionManager::get_session(id, false).await, - None => Err(anyhow::anyhow!("No session available")), - } + pub async fn get_session(&self) -> Result { + SessionManager::get_session(&self.session_id, false).await } // Get the session's total token usage pub async fn get_total_token_usage(&self) -> Result> { - let metadata = self.get_metadata().await?; + let metadata = self.get_session().await?; Ok(metadata.total_tokens) } @@ -1324,7 +1309,7 @@ impl CliSession { .unwrap_or(false); let provider_name = config - .get_param::("GOOSE_PROVIDER") + .get_goose_provider() .unwrap_or_else(|_| "unknown".to_string()); // Do not get costing information if show cost is disabled @@ -1340,7 +1325,7 @@ impl CliSession { } } - match self.get_metadata().await { + match self.get_session().await { Ok(metadata) => { let total_tokens = metadata.total_tokens.unwrap_or(0) as usize; @@ -1488,7 +1473,7 @@ async fn get_reasoner() -> Result, anyhow::Error> { } else { println!("WARNING: GOOSE_PLANNER_PROVIDER not found. Using default provider..."); config - .get_param::("GOOSE_PROVIDER") + .get_goose_provider() .expect("No provider configured. Run 'goose configure' first") }; @@ -1498,7 +1483,7 @@ async fn get_reasoner() -> Result, anyhow::Error> { } else { println!("WARNING: GOOSE_PLANNER_MODEL not found. Using default model..."); config - .get_param::("GOOSE_MODEL") + .get_goose_model() .expect("No model configured. Run 'goose configure' first") }; diff --git a/crates/goose-cli/src/session/output.rs b/crates/goose-cli/src/session/output.rs index b1e0066da556..14b598e3b638 100644 --- a/crates/goose-cli/src/session/output.rs +++ b/crates/goose-cli/src/session/output.rs @@ -60,7 +60,7 @@ thread_local! { .unwrap_or_else(|| Config::global().get_param::("GOOSE_CLI_THEME").ok() .map(|val| Theme::from_config_str(&val)) - .unwrap_or(Theme::Dark) + .unwrap_or(Theme::Ansi) ) ); } @@ -68,7 +68,7 @@ thread_local! { pub fn set_theme(theme: Theme) { let config = Config::global(); config - .set_param("GOOSE_CLI_THEME", Value::String(theme.as_config_string())) + .set_param("GOOSE_CLI_THEME", theme.as_config_string()) .expect("Failed to set theme"); CURRENT_THEME.with(|t| *t.borrow_mut() = theme); @@ -79,7 +79,7 @@ pub fn set_theme(theme: Theme) { Theme::Ansi => "ansi", }; - if let Err(e) = config.set_param("GOOSE_CLI_THEME", Value::String(theme_str.to_string())) { + if let Err(e) = config.set_param("GOOSE_CLI_THEME", theme_str) { eprintln!("Failed to save theme setting to config: {}", e); } } diff --git a/crates/goose-mcp/src/autovisualiser/templates/mermaid_template.html b/crates/goose-mcp/src/autovisualiser/templates/mermaid_template.html index 69823495a58f..11e70d85d9d3 100644 --- a/crates/goose-mcp/src/autovisualiser/templates/mermaid_template.html +++ b/crates/goose-mcp/src/autovisualiser/templates/mermaid_template.html @@ -169,5 +169,4 @@

Mermaid Diagram

}; - -c:\Users\ARYA SINGH\Dropbox\PC\Desktop\goose\crates\goose-mcp\src\autovisualiser\templates\mermaid_template.html \ No newline at end of file + diff --git a/crates/goose-mcp/src/developer/analyze/formatter.rs b/crates/goose-mcp/src/developer/analyze/formatter.rs index c05011647820..49bb76cc4509 100644 --- a/crates/goose-mcp/src/developer/analyze/formatter.rs +++ b/crates/goose-mcp/src/developer/analyze/formatter.rs @@ -1,11 +1,11 @@ -use rmcp::model::{Content, Role}; -use std::collections::{HashMap, HashSet}; -use std::path::{Path, PathBuf}; - use crate::developer::analyze::types::{ AnalysisMode, AnalysisResult, CallChain, EntryType, FocusedAnalysisData, }; use crate::developer::lang; +use goose::utils::safe_truncate; +use rmcp::model::{Content, Role}; +use std::collections::{HashMap, HashSet}; +use std::path::{Path, PathBuf}; pub struct Formatter; @@ -164,13 +164,7 @@ impl Formatter { if imports.len() > 1 { format!("{}({})", group, imports.len()) } else { - // For single imports, show more detail - let imp = &imports[0]; - if imp.len() > 40 { - format!("{}...", &imp[..37]) - } else { - imp.clone() - } + safe_truncate(&imports[0], 40) } }) .collect(); @@ -727,7 +721,7 @@ impl Formatter { if let Some(header_line) = output .lines() .rev() - .find(|l| l.starts_with("##") && line.contains(&l[3..])) + .find(|l| l.starts_with("##") && l.get(3..).is_some_and(|s| line.contains(s))) { if !filtered.contains(header_line) { filtered.push_str(header_line); diff --git a/crates/goose-mcp/src/developer/analyze/languages/go.rs b/crates/goose-mcp/src/developer/analyze/languages/go.rs index 71c7116e7aee..9c1ec28376e0 100644 --- a/crates/goose-mcp/src/developer/analyze/languages/go.rs +++ b/crates/goose-mcp/src/developer/analyze/languages/go.rs @@ -87,7 +87,7 @@ pub fn find_method_for_receiver( for i in 0..parent.child_count() { if let Some(child) = parent.child(i) { if child.kind() == "field_identifier" { - return Some(source[child.byte_range()].to_string()); + return source.get(child.byte_range()).map(|s| s.to_string()); } } } diff --git a/crates/goose-mcp/src/developer/analyze/languages/ruby.rs b/crates/goose-mcp/src/developer/analyze/languages/ruby.rs index 6214e6313596..df9f7496fa7c 100644 --- a/crates/goose-mcp/src/developer/analyze/languages/ruby.rs +++ b/crates/goose-mcp/src/developer/analyze/languages/ruby.rs @@ -140,7 +140,7 @@ fn find_method_in_body_with_depth( for j in 0..child.child_count() { if let Some(name_node) = child.child(j) { if name_node.kind() == "identifier" { - return Some(source[name_node.byte_range()].to_string()); + return source.get(name_node.byte_range()).map(|s| s.to_string()); } } } diff --git a/crates/goose-mcp/src/developer/analyze/languages/rust.rs b/crates/goose-mcp/src/developer/analyze/languages/rust.rs index bc1a980db1cf..bf9a6f983e47 100644 --- a/crates/goose-mcp/src/developer/analyze/languages/rust.rs +++ b/crates/goose-mcp/src/developer/analyze/languages/rust.rs @@ -83,7 +83,9 @@ pub fn extract_function_name_for_kind( for i in 0..node.child_count() { if let Some(child) = node.child(i) { if child.kind() == "type_identifier" { - return Some(format!("impl {}", &source[child.byte_range()])); + return source + .get(child.byte_range()) + .map(|s| format!("impl {}", s)); } } } @@ -109,7 +111,7 @@ pub fn find_method_for_receiver( for i in 0..parent.child_count() { if let Some(child) = parent.child(i) { if child.kind() == "identifier" { - return Some(source[child.byte_range()].to_string()); + return source.get(child.byte_range()).map(|s| s.to_string()); } } } @@ -133,7 +135,7 @@ pub fn find_receiver_type(node: &tree_sitter::Node, source: &str) -> Option Option { - Self::find_child_by_kind(node, kinds).map(|child| source[child.byte_range()].to_string()) + Self::find_child_by_kind(node, kinds) + .and_then(|child| source.get(child.byte_range()).map(|s| s.to_string())) } pub fn extract_with_depth( @@ -216,8 +217,13 @@ impl ElementExtractor { for match_ in matches.by_ref() { for capture in match_.captures { let node = capture.node; - let text = &source[node.byte_range()]; - let line = source[..node.start_byte()].lines().count() + 1; + let Some(text) = source.get(node.byte_range()) else { + continue; + }; + let line = source + .get(..node.start_byte()) + .map(|s| s.lines().count() + 1) + .unwrap_or(1); match query.capture_names()[capture.index as usize] { "func" | "const" => { @@ -284,18 +290,25 @@ impl ElementExtractor { for match_ in matches.by_ref() { for capture in match_.captures { let node = capture.node; - let text = &source[node.byte_range()]; + let Some(text) = source.get(node.byte_range()) else { + continue; + }; let start_pos = node.start_position(); - let line_start = source[..node.start_byte()] - .rfind('\n') + let line_start = source + .get(..node.start_byte()) + .and_then(|s| s.rfind('\n')) .map(|i| i + 1) .unwrap_or(0); - let line_end = source[node.end_byte()..] - .find('\n') + let line_end = source + .get(node.end_byte()..) + .and_then(|s| s.find('\n')) .map(|i| node.end_byte() + i) .unwrap_or(source.len()); - let context = source[line_start..line_end].trim().to_string(); + let context = source + .get(line_start..line_end) + .map(|s| s.trim().to_string()) + .unwrap_or_default(); let caller_name = Self::find_containing_function(&node, source, language); @@ -356,18 +369,25 @@ impl ElementExtractor { for match_ in matches.by_ref() { for capture in match_.captures { let node = capture.node; - let text = &source[node.byte_range()]; + let Some(text) = source.get(node.byte_range()) else { + continue; + }; let start_pos = node.start_position(); - let line_start = source[..node.start_byte()] - .rfind('\n') + let line_start = source + .get(..node.start_byte()) + .and_then(|s| s.rfind('\n')) .map(|i| i + 1) .unwrap_or(0); - let line_end = source[node.end_byte()..] - .find('\n') + let line_end = source + .get(node.end_byte()..) + .and_then(|s| s.find('\n')) .map(|i| node.end_byte() + i) .unwrap_or(source.len()); - let context = source[line_start..line_end].trim().to_string(); + let context = source + .get(line_start..line_end) + .map(|s| s.trim().to_string()) + .unwrap_or_default(); let capture_name = query.capture_names()[capture.index as usize]; diff --git a/crates/goose-mcp/src/developer/editor_models/morphllm_editor.rs b/crates/goose-mcp/src/developer/editor_models/morphllm_editor.rs index 57a33859636b..44170a48cf81 100644 --- a/crates/goose-mcp/src/developer/editor_models/morphllm_editor.rs +++ b/crates/goose-mcp/src/developer/editor_models/morphllm_editor.rs @@ -28,8 +28,9 @@ impl MorphLLMEditor { if let (Some(start_pos), Some(end_pos)) = (text.find(&start_tag), text.find(&end_tag)) { if start_pos < end_pos { let content_start = start_pos + start_tag.len(); - let content = &text[content_start..end_pos]; - return Some(content.trim().to_string()); + if let Some(content) = text.get(content_start..end_pos) { + return Some(content.trim().to_string()); + } } } None diff --git a/crates/goose-mcp/src/developer/goose_hints/mod.rs b/crates/goose-mcp/src/developer/goose_hints/mod.rs deleted file mode 100644 index 818255dbc628..000000000000 --- a/crates/goose-mcp/src/developer/goose_hints/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod import_files; -pub mod load_hints; diff --git a/crates/goose-mcp/src/developer/mod.rs b/crates/goose-mcp/src/developer/mod.rs index 5eac4ff0eef6..609f88bcc507 100644 --- a/crates/goose-mcp/src/developer/mod.rs +++ b/crates/goose-mcp/src/developer/mod.rs @@ -1,6 +1,5 @@ pub mod analyze; mod editor_models; -mod goose_hints; mod lang; mod shell; mod text_editor; diff --git a/crates/goose-mcp/src/developer/rmcp_developer.rs b/crates/goose-mcp/src/developer/rmcp_developer.rs index bbd91d96ae25..c249d2aef7b0 100644 --- a/crates/goose-mcp/src/developer/rmcp_developer.rs +++ b/crates/goose-mcp/src/developer/rmcp_developer.rs @@ -33,7 +33,6 @@ use tokio_util::sync::CancellationToken; use super::analyze::{types::AnalyzeParams, CodeAnalyzer}; use super::editor_models::{create_editor_model, EditorModel}; -use super::goose_hints::load_hints::{load_hint_files, GOOSE_HINTS_FILENAME}; use super::shell::{ configure_shell_command, expand_path, get_shell_config, is_absolute_path, kill_process_group, }; @@ -246,17 +245,6 @@ impl ServerHandler for DeveloperServer { } }; - let hints_filenames: Vec = std::env::var("CONTEXT_FILE_NAMES") - .ok() - .and_then(|s| serde_json::from_str(&s).ok()) - .unwrap_or_else(|| vec!["AGENTS.md".to_string(), GOOSE_HINTS_FILENAME.to_string()]); - - // Build ignore patterns for file reference processing - let ignore_patterns = Self::build_ignore_patterns(&cwd); - - // Load hints using the centralized function - let hints = load_hint_files(&cwd, &hints_filenames, &ignore_patterns); - // Check if editor model exists and augment with custom llm editor tool description let editor_description = if let Some(ref editor) = self.editor_model { formatdoc! {r#" @@ -373,12 +361,7 @@ impl ServerHandler for DeveloperServer { _ => format!("{}{}", common_shell_instructions, unix_specific), }; - // Return base instructions directly when no hints are found - let instructions = if hints.is_empty() { - format!("{base_instructions}{editor_description}\n{shell_tool_desc}") - } else { - format!("{base_instructions}\n{editor_description}\n{shell_tool_desc}\n{hints}") - }; + let instructions = format!("{base_instructions}{editor_description}\n{shell_tool_desc}"); ServerInfo { server_info: Implementation { @@ -1335,20 +1318,22 @@ impl DeveloperServer { // Find the last space before AM/PM and replace it with U+202F let space_pos = filename.rfind(meridian) - .map(|pos| filename[..pos].trim_end().len()) + .and_then(|pos| filename.get(..pos).map(|s| s.trim_end().len())) .unwrap_or(0); if space_pos > 0 { let parent = path.parent().unwrap_or(Path::new("")); - let new_filename = format!( - "{}{}{}", - &filename[..space_pos], - '\u{202F}', - &filename[space_pos+1..] - ); - let new_path = parent.join(new_filename); - - return new_path; + if let (Some(before), Some(after)) = (filename.get(..space_pos), filename.get(space_pos+1..)) { + let new_filename = format!( + "{}{}{}", + before, + '\u{202F}', + after + ); + let new_path = parent.join(new_filename); + + return new_path; + } } } } @@ -3208,7 +3193,10 @@ mod tests { ) { let start_idx = start + start_tag.len(); if start_idx < end { - let path = assistant_content.text[start_idx..end].trim(); + let Some(path) = assistant_content.text.get(start_idx..end).map(|s| s.trim()) + else { + panic!("Failed to extract path from assistant content"); + }; println!("Extracted path: {}", path); let file_contents = @@ -3392,161 +3380,6 @@ mod tests { ); } - #[tokio::test] - #[serial] - async fn test_global_goosehints() { - // Note: This test checks if ~/.config/goose/.goosehints exists and includes it in instructions - // Since RMCP version uses get_info() instead of instructions(), we test that method - let global_hints_path = - PathBuf::from(shellexpand::tilde("~/.config/goose/.goosehints").to_string()); - let global_hints_bak_path = - PathBuf::from(shellexpand::tilde("~/.config/goose/.goosehints.bak").to_string()); - let mut globalhints_existed = false; - - if global_hints_path.is_file() { - globalhints_existed = true; - fs::copy(&global_hints_path, &global_hints_bak_path).unwrap(); - } - - fs::write(&global_hints_path, "These are my global goose hints.").unwrap(); - - let dir = TempDir::new().unwrap(); - std::env::set_current_dir(dir.path()).unwrap(); - - let server = create_test_server(); - let server_info = server.get_info(); - - assert!(server_info.instructions.is_some()); - let instructions = server_info.instructions.unwrap(); - assert!(instructions.contains("my global goose hints.")); - - // restore backup if globalhints previously existed - if globalhints_existed { - fs::copy(&global_hints_bak_path, &global_hints_path).unwrap(); - fs::remove_file(&global_hints_bak_path).unwrap(); - } else { - fs::remove_file(&global_hints_path).unwrap(); - } - } - - #[tokio::test] - #[serial] - async fn test_goosehints_with_file_references() { - let temp_dir = tempfile::tempdir().unwrap(); - std::env::set_current_dir(&temp_dir).unwrap(); - - // Create referenced files - let readme_path = temp_dir.path().join("README.md"); - std::fs::write( - &readme_path, - "# Project README\n\nThis is the project documentation.", - ) - .unwrap(); - - let guide_path = temp_dir.path().join("guide.md"); - std::fs::write(&guide_path, "# Development Guide\n\nFollow these steps...").unwrap(); - - // Create .goosehints with references - let hints_content = r#"# Project Information - -Please refer to: -@README.md -@guide.md - -Additional instructions here. -"#; - let hints_path = temp_dir.path().join(".goosehints"); - std::fs::write(&hints_path, hints_content).unwrap(); - - // Create server and check instructions - let server = create_test_server(); - let server_info = server.get_info(); - - assert!(server_info.instructions.is_some()); - let instructions = server_info.instructions.unwrap(); - - // Should contain the .goosehints content - assert!(instructions.contains("Project Information")); - assert!(instructions.contains("Additional instructions here")); - - // Should contain the referenced files' content - assert!(instructions.contains("# Project README")); - assert!(instructions.contains("This is the project documentation")); - assert!(instructions.contains("# Development Guide")); - assert!(instructions.contains("Follow these steps")); - - // Should have attribution markers - assert!(instructions.contains("--- Content from")); - assert!(instructions.contains("--- End of")); - } - - #[tokio::test] - #[serial] - async fn test_goosehints_when_present() { - let dir = TempDir::new().unwrap(); - std::env::set_current_dir(dir.path()).unwrap(); - - fs::write(".goosehints", "Test hint content").unwrap(); - let server = create_test_server(); - let server_info = server.get_info(); - - assert!(server_info.instructions.is_some()); - let instructions = server_info.instructions.unwrap(); - assert!(instructions.contains("Test hint content")); - } - - #[tokio::test] - #[serial] - async fn test_goosehints_when_missing() { - let dir = TempDir::new().unwrap(); - std::env::set_current_dir(dir.path()).unwrap(); - - let server = create_test_server(); - let server_info = server.get_info(); - - assert!(server_info.instructions.is_some()); - let instructions = server_info.instructions.unwrap(); - // When no hints are present, instructions should not contain hint content - assert!(!instructions.contains("AGENTS.md:") && !instructions.contains(".goosehints:")); - } - - #[tokio::test] - #[serial] - async fn test_goosehints_multiple_filenames() { - let dir = TempDir::new().unwrap(); - std::env::set_current_dir(dir.path()).unwrap(); - std::env::set_var("CONTEXT_FILE_NAMES", r#"["CLAUDE.md", ".goosehints"]"#); - - fs::write("CLAUDE.md", "Custom hints file content from CLAUDE.md").unwrap(); - fs::write(".goosehints", "Custom hints file content from .goosehints").unwrap(); - let server = create_test_server(); - let server_info = server.get_info(); - - assert!(server_info.instructions.is_some()); - let instructions = server_info.instructions.unwrap(); - assert!(instructions.contains("Custom hints file content from CLAUDE.md")); - assert!(instructions.contains("Custom hints file content from .goosehints")); - std::env::remove_var("CONTEXT_FILE_NAMES"); - } - - #[tokio::test] - #[serial] - async fn test_goosehints_configurable_filename() { - let dir = TempDir::new().unwrap(); - std::env::set_current_dir(dir.path()).unwrap(); - std::env::set_var("CONTEXT_FILE_NAMES", r#"["CLAUDE.md"]"#); - - fs::write("CLAUDE.md", "Custom hints file content").unwrap(); - let server = create_test_server(); - let server_info = server.get_info(); - - assert!(server_info.instructions.is_some()); - let instructions = server_info.instructions.unwrap(); - assert!(instructions.contains("Custom hints file content")); - assert!(!instructions.contains(".goosehints")); // Make sure it's not loading the default - std::env::remove_var("CONTEXT_FILE_NAMES"); - } - #[test] #[serial] fn test_resolve_path_absolute() { diff --git a/crates/goose-server/src/logging.rs b/crates/goose-server/src/logging.rs index c4ffd92870e5..0a5eadc20cf1 100644 --- a/crates/goose-server/src/logging.rs +++ b/crates/goose-server/src/logging.rs @@ -1,5 +1,4 @@ use anyhow::{Context, Result}; -use std::path::PathBuf; use tracing_appender::rolling::Rotation; use tracing_subscriber::{ filter::LevelFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer, @@ -8,32 +7,24 @@ use tracing_subscriber::{ use goose::tracing::{langfuse_layer, otlp_layer}; -/// Returns the directory where log files should be stored. -/// Creates the directory structure if it doesn't exist. -fn get_log_directory() -> Result { - goose::logging::get_log_directory("server", true) -} - /// Sets up the logging infrastructure for the application. /// This includes: /// - File-based logging with JSON formatting (DEBUG level) /// - Console output for development (INFO level) /// - Optional Langfuse integration (DEBUG level) pub fn setup_logging(name: Option<&str>) -> Result<()> { - // Set up file appender for goose module logs - let log_dir = get_log_directory()?; + let log_dir = goose::logging::prepare_log_directory("server", true)?; let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S").to_string(); - - // Create log file name by prefixing with timestamp let log_filename = if name.is_some() { format!("{}-{}.log", timestamp, name.unwrap()) } else { format!("{}.log", timestamp) }; - - // Create non-rolling file appender for detailed logs - let file_appender = - tracing_appender::rolling::RollingFileAppender::new(Rotation::NEVER, log_dir, log_filename); + let file_appender = tracing_appender::rolling::RollingFileAppender::new( + Rotation::NEVER, // we do manual rotation via file naming and cleanup_old_logs + log_dir, + log_filename, + ); // Create JSON file logging layer let file_layer = fmt::layer() @@ -74,7 +65,7 @@ pub fn setup_logging(name: Option<&str>) -> Result<()> { console_layer.with_filter(LevelFilter::INFO).boxed(), ]; - if let Ok((otlp_tracing_layer, otlp_metrics_layer)) = otlp_layer::init_otlp() { + if let Ok((otlp_tracing_layer, otlp_metrics_layer, otlp_logs_layer)) = otlp_layer::init_otlp() { layers.push( otlp_tracing_layer .with_filter(otlp_layer::create_otlp_tracing_filter()) @@ -85,6 +76,11 @@ pub fn setup_logging(name: Option<&str>) -> Result<()> { .with_filter(otlp_layer::create_otlp_metrics_filter()) .boxed(), ); + layers.push( + otlp_logs_layer + .with_filter(otlp_layer::create_otlp_logs_filter()) + .boxed(), + ); } if let Some(langfuse) = langfuse_layer::create_langfuse_observer() { diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index f84a92a64247..4667b93135ac 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -6,7 +6,7 @@ use goose::config::ExtensionEntry; use goose::conversation::Conversation; use goose::permission::permission_confirmation::PrincipalType; use goose::providers::base::{ConfigKey, ModelInfo, ProviderMetadata, ProviderType}; -use goose::session::{Session, SessionInsights}; +use goose::session::{Session, SessionInsights, SessionType}; use rmcp::model::{ Annotations, Content, EmbeddedResource, Icon, ImageContent, JsonObject, RawAudioContent, RawEmbeddedResource, RawImageContent, RawResource, RawTextContent, ResourceContents, Role, @@ -19,8 +19,8 @@ use goose::config::declarative_providers::{ }; use goose::conversation::message::{ FrontendToolRequest, Message, MessageContent, MessageMetadata, RedactedThinkingContent, - SystemNotificationContent, SystemNotificationType, ThinkingContent, ToolConfirmationRequest, - ToolRequest, ToolResponse, + SystemNotificationContent, SystemNotificationType, ThinkingContent, TokenState, + ToolConfirmationRequest, ToolRequest, ToolResponse, }; use crate::routes::reply::MessageEvent; @@ -348,6 +348,8 @@ derive_utoipa!(Icon as IconSchema); super::routes::agent::resume_agent, super::routes::agent::get_tools, super::routes::agent::update_from_session, + super::routes::agent::agent_add_extension, + super::routes::agent::agent_remove_extension, super::routes::agent::update_agent_provider, super::routes::agent::update_router_tool_selector, super::routes::reply::confirm_permission, @@ -402,6 +404,7 @@ derive_utoipa!(Icon as IconSchema); Message, MessageContent, MessageMetadata, + TokenState, ContentSchema, EmbeddedResourceSchema, ImageContentSchema, @@ -441,6 +444,7 @@ derive_utoipa!(Icon as IconSchema); ModelInfo, Session, SessionInsights, + SessionType, Conversation, IconSchema, goose::session::extension_data::ExtensionData, @@ -486,6 +490,8 @@ derive_utoipa!(Icon as IconSchema); super::routes::agent::StartAgentRequest, super::routes::agent::ResumeAgentRequest, super::routes::agent::UpdateFromSessionRequest, + super::routes::agent::AddExtensionRequest, + super::routes::agent::RemoveExtensionRequest, super::routes::setup::SetupResponse, )) )] diff --git a/crates/goose-server/src/routes/agent.rs b/crates/goose-server/src/routes/agent.rs index fb850895e97b..031b7096b220 100644 --- a/crates/goose-server/src/routes/agent.rs +++ b/crates/goose-server/src/routes/agent.rs @@ -11,12 +11,14 @@ use axum::{ }; use goose::config::PermissionManager; -use goose::config::Config; +use goose::agents::ExtensionConfig; +use goose::config::{Config, GooseMode}; use goose::model::ModelConfig; use goose::prompt_template::render_global_file; use goose::providers::{create, create_with_named_model}; use goose::recipe::Recipe; use goose::recipe_deeplink; +use goose::session::session_manager::SessionType; use goose::session::{Session, SessionManager}; use goose::{ agents::{extension::ToolInfo, extension_manager::get_parameter_names}, @@ -70,6 +72,18 @@ pub struct ResumeAgentRequest { load_model_and_extensions: bool, } +#[derive(Deserialize, utoipa::ToSchema)] +pub struct AddExtensionRequest { + session_id: String, + config: ExtensionConfig, +} + +#[derive(Deserialize, utoipa::ToSchema)] +pub struct RemoveExtensionRequest { + name: String, + session_id: String, +} + #[utoipa::path( post, path = "/agent/start", @@ -124,15 +138,16 @@ async fn start_agent( let counter = state.session_counter.fetch_add(1, Ordering::SeqCst) + 1; let name = format!("New session {}", counter); - let mut session = SessionManager::create_session(PathBuf::from(&working_dir), name) - .await - .map_err(|err| { - error!("Failed to create session: {}", err); - ErrorResponse { - message: format!("Failed to create session: {}", err), - status: StatusCode::BAD_REQUEST, - } - })?; + let mut session = + SessionManager::create_session(PathBuf::from(&working_dir), name, SessionType::User) + .await + .map_err(|err| { + error!("Failed to create session: {}", err); + ErrorResponse { + message: format!("Failed to create session: {}", err), + status: StatusCode::BAD_REQUEST, + } + })?; if let Some(recipe) = original_recipe { SessionManager::update_session(&session.id) @@ -198,15 +213,12 @@ async fn resume_agent( let config = Config::global(); let provider_result = async { - let provider_name: String = - config - .get_param("GOOSE_PROVIDER") - .map_err(|_| ErrorResponse { - message: "Could not configure agent: missing provider".into(), - status: StatusCode::INTERNAL_SERVER_ERROR, - })?; - - let model: String = config.get_param("GOOSE_MODEL").map_err(|_| ErrorResponse { + let provider_name: String = config.get_goose_provider().map_err(|_| ErrorResponse { + message: "Could not configure agent: missing provider".into(), + status: StatusCode::INTERNAL_SERVER_ERROR, + })?; + + let model: String = config.get_goose_model().map_err(|_| ErrorResponse { message: "Could not configure agent: missing model".into(), status: StatusCode::INTERNAL_SERVER_ERROR, })?; @@ -335,7 +347,7 @@ async fn get_tools( Query(query): Query, ) -> Result>, StatusCode> { let config = Config::global(); - let goose_mode = config.get_param("GOOSE_MODE").unwrap_or("auto".to_string()); + let goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto); let agent = state.get_agent_for_route(query.session_id).await?; let permission_manager = PermissionManager::default(); @@ -347,9 +359,9 @@ async fn get_tools( let permission = permission_manager .get_user_permission(&tool.name) .or_else(|| { - if goose_mode == "smart_approve" { + if goose_mode == GooseMode::SmartApprove { permission_manager.get_smart_approve_permission(&tool.name) - } else if goose_mode == "approve" { + } else if goose_mode == GooseMode::Approve { Some(PermissionLevel::AskBefore) } else { None @@ -393,10 +405,7 @@ async fn update_agent_provider( .await?; let config = Config::global(); - let model = match payload - .model - .or_else(|| config.get_param("GOOSE_MODEL").ok()) - { + let model = match payload.model.or_else(|| config.get_goose_model().ok()) { Some(m) => m, None => { tracing::error!("No model specified"); @@ -451,6 +460,89 @@ async fn update_router_tool_selector( )) } +#[utoipa::path( + post, + path = "/agent/add_extension", + request_body = AddExtensionRequest, + responses( + (status = 200, description = "Extension added", body = String), + (status = 401, description = "Unauthorized - invalid secret key"), + (status = 424, description = "Agent not initialized"), + (status = 500, description = "Internal server error") + ) +)] +async fn agent_add_extension( + State(state): State>, + Json(request): Json, +) -> Result { + if cfg!(target_os = "windows") { + if let ExtensionConfig::Stdio { cmd, .. } = &request.config { + if cmd.ends_with("npx.cmd") || cmd.ends_with("npx") { + let node_exists = std::path::Path::new(r"C:\Program Files\nodejs\node.exe") + .exists() + || std::path::Path::new(r"C:\Program Files (x86)\nodejs\node.exe").exists(); + + if !node_exists { + let cmd_path = std::path::Path::new(&cmd); + let script_dir = cmd_path + .parent() + .ok_or_else(|| ErrorResponse::internal("Invalid command path"))?; + let install_script = script_dir.join("install-node.cmd"); + + if install_script.exists() { + eprintln!("Installing Node.js..."); + let output = std::process::Command::new(&install_script) + .arg("https://nodejs.org/dist/v23.10.0/node-v23.10.0-x64.msi") + .output() + .map_err(|_e| { + ErrorResponse::internal("Failed to run Node.js installer") + })?; + + if !output.status.success() { + return Err(ErrorResponse::internal(format!( + "Failed to install Node.js: {}", + String::from_utf8_lossy(&output.stderr) + ))); + } + } else { + return Err(ErrorResponse::internal(format!( + "Node.js not detected and no installer script not found at: {}", + install_script.display() + ))); + } + } + } + } + } + + let agent = state.get_agent(request.session_id).await?; + agent + .add_extension(request.config) + .await + .map_err(|e| ErrorResponse::internal(format!("Failed to add extension: {}", e)))?; + Ok(StatusCode::OK) +} + +#[utoipa::path( + post, + path = "/agent/remove_extension", + request_body = RemoveExtensionRequest, + responses( + (status = 200, description = "Extension removed", body = String), + (status = 401, description = "Unauthorized - invalid secret key"), + (status = 424, description = "Agent not initialized"), + (status = 500, description = "Internal server error") + ) +)] +async fn agent_remove_extension( + State(state): State>, + Json(request): Json, +) -> Result { + let agent = state.get_agent(request.session_id).await?; + agent.remove_extension(&request.name).await?; + Ok(StatusCode::OK) +} + pub fn routes(state: Arc) -> Router { Router::new() .route("/agent/start", post(start_agent)) @@ -462,5 +554,7 @@ pub fn routes(state: Arc) -> Router { post(update_router_tool_selector), ) .route("/agent/update_from_session", post(update_from_session)) + .route("/agent/add_extension", post(agent_add_extension)) + .route("/agent/remove_extension", post(agent_remove_extension)) .with_state(state) } diff --git a/crates/goose-server/src/routes/audio.rs b/crates/goose-server/src/routes/audio.rs index 6985d96d1ce0..707f1b5b2742 100644 --- a/crates/goose-server/src/routes/audio.rs +++ b/crates/goose-server/src/routes/audio.rs @@ -251,7 +251,7 @@ async fn transcribe_elevenlabs_handler( // Migrate to secret storage if let Err(e) = config.set( "ELEVENLABS_API_KEY", - serde_json::Value::String(key.clone()), + &serde_json::Value::String(key.clone()), true, ) { tracing::error!("Failed to migrate ElevenLabs API key: {:?}", e); diff --git a/crates/goose-server/src/routes/config_management.rs b/crates/goose-server/src/routes/config_management.rs index c1b1de5d2777..f40e86b2f299 100644 --- a/crates/goose-server/src/routes/config_management.rs +++ b/crates/goose-server/src/routes/config_management.rs @@ -88,6 +88,19 @@ pub struct UpdateCustomProviderRequest { pub supports_streaming: Option, } +#[derive(Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct MaskedSecret { + pub masked_value: String, +} + +#[derive(Serialize, ToSchema)] +#[serde(untagged)] +pub enum ConfigValueResponse { + Value(Value), + MaskedValue(MaskedSecret), +} + #[utoipa::path( post, path = "/config/upsert", @@ -101,7 +114,7 @@ pub async fn upsert_config( Json(query): Json, ) -> Result, StatusCode> { let config = Config::global(); - let result = config.set(&query.key, query.value, query.is_secret); + let result = config.set(&query.key, &query.value, query.is_secret); match result { Ok(_) => Ok(Json(Value::String(format!("Upserted key {}", query.key)))), @@ -134,6 +147,22 @@ pub async fn remove_config(Json(query): Json) -> Result String { + let as_string = match secret { + Value::String(s) => s, + _ => serde_json::to_string(&secret).unwrap_or_else(|_| secret.to_string()), + }; + + let chars: Vec<_> = as_string.chars().collect(); + let show_len = std::cmp::min(chars.len() / 2, SECRET_MASK_SHOW_LEN); + let visible: String = chars.iter().take(show_len).collect(); + let mask = "*".repeat(chars.len() - show_len); + + format!("{}{}", visible, mask) +} + #[utoipa::path( post, path = "/config/read", @@ -143,12 +172,14 @@ pub async fn remove_config(Json(query): Json) -> Result) -> Result, StatusCode> { +pub async fn read_config( + Json(query): Json, +) -> Result, StatusCode> { if query.key == "model-limits" { let limits = ModelConfig::get_all_model_limits(); - return Ok(Json( + return Ok(Json(ConfigValueResponse::Value( serde_json::to_value(limits).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?, - )); + ))); } let config = Config::global(); @@ -156,18 +187,14 @@ pub async fn read_config(Json(query): Json) -> Result { if query.is_secret { - Value::Bool(true) - } else { - value - } - } - Err(ConfigError::NotFound(_)) => { - if query.is_secret { - Value::Bool(false) + ConfigValueResponse::MaskedValue(MaskedSecret { + masked_value: mask_secret(value), + }) } else { - Value::Null + ConfigValueResponse::Value(value) } } + Err(ConfigError::NotFound(_)) => ConfigValueResponse::Value(Value::Null), Err(_) => { return Err(StatusCode::INTERNAL_SERVER_ERROR); } @@ -245,7 +272,7 @@ pub async fn read_all_config() -> Result, StatusCode> { let config = Config::global(); let values = config - .load_values() + .all_values() .map_err(|_| StatusCode::UNPROCESSABLE_ENTITY)?; Ok(Json(ConfigResponse { config: values })) @@ -482,7 +509,7 @@ pub async fn init_config() -> Result, StatusCode> { // Use the shared function to load init-config.yaml match goose::config::base::load_init_config_from_workspace() { - Ok(init_values) => match config.save_values(init_values) { + Ok(init_values) => match config.initialize_if_empty(init_values) { Ok(_) => Ok(Json("Config initialized successfully".to_string())), Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR), }, @@ -557,7 +584,7 @@ pub async fn recover_config() -> Result, StatusCode> { let config = Config::global(); // Force a reload which will trigger recovery if needed - match config.load_values() { + match config.all_values() { Ok(values) => { let recovered_keys: Vec = values.keys().cloned().collect(); if recovered_keys.is_empty() { @@ -752,10 +779,12 @@ mod tests { .await; assert!(result.is_ok()); - let response = result.unwrap(); + let response = match result.unwrap().0 { + ConfigValueResponse::Value(value) => value, + ConfigValueResponse::MaskedValue(_) => panic!("unexpected secret"), + }; - let limits: Vec = - serde_json::from_value(response.0).unwrap(); + let limits: Vec = serde_json::from_value(response).unwrap(); assert!(!limits.is_empty()); let gpt4_limit = limits.iter().find(|l| l.pattern == "gpt-4o"); diff --git a/crates/goose-server/src/routes/errors.rs b/crates/goose-server/src/routes/errors.rs index 894f13e426e9..c265482d055e 100644 --- a/crates/goose-server/src/routes/errors.rs +++ b/crates/goose-server/src/routes/errors.rs @@ -13,6 +13,15 @@ pub struct ErrorResponse { pub status: StatusCode, } +impl ErrorResponse { + pub(crate) fn internal(message: impl Into) -> Self { + Self { + message: message.into(), + status: StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + impl IntoResponse for ErrorResponse { fn into_response(self) -> Response { let body = Json(serde_json::json!({ @@ -22,3 +31,9 @@ impl IntoResponse for ErrorResponse { (self.status, body).into_response() } } + +impl From for ErrorResponse { + fn from(err: anyhow::Error) -> Self { + Self::internal(err.to_string()) + } +} diff --git a/crates/goose-server/src/routes/extension.rs b/crates/goose-server/src/routes/extension.rs deleted file mode 100644 index 5fec1a3599e7..000000000000 --- a/crates/goose-server/src/routes/extension.rs +++ /dev/null @@ -1,139 +0,0 @@ -use std::sync::Arc; - -use crate::state::AppState; -use axum::{extract::State, routing::post, Json, Router}; -use goose::agents::ExtensionConfig; -use http::StatusCode; -use serde::{Deserialize, Serialize}; -use tracing; - -#[derive(Serialize)] -struct ExtensionResponse { - error: bool, - message: Option, -} - -#[derive(Deserialize)] -struct AddExtensionRequest { - session_id: String, - #[serde(flatten)] - config: ExtensionConfig, -} - -async fn add_extension( - State(state): State>, - Json(request): Json, -) -> Result, StatusCode> { - // Log the request for debugging - tracing::info!( - "Received extension request for session: {}", - request.session_id - ); - - // If this is a Stdio extension that uses npx, check for Node.js installation - #[cfg(target_os = "windows")] - if let ExtensionConfig::Stdio { cmd, .. } = &request.config { - if cmd.ends_with("npx.cmd") || cmd.ends_with("npx") { - // Check if Node.js is installed in standard locations - let node_exists = std::path::Path::new(r"C:\Program Files\nodejs\node.exe").exists() - || std::path::Path::new(r"C:\Program Files (x86)\nodejs\node.exe").exists(); - - if !node_exists { - // Get the directory containing npx.cmd - let cmd_path = std::path::Path::new(&cmd); - let script_dir = cmd_path.parent().ok_or(StatusCode::INTERNAL_SERVER_ERROR)?; - - // Run the Node.js installer script - let install_script = script_dir.join("install-node.cmd"); - - if install_script.exists() { - eprintln!("Installing Node.js..."); - let output = std::process::Command::new(&install_script) - .arg("https://nodejs.org/dist/v23.10.0/node-v23.10.0-x64.msi") - .output() - .map_err(|e| { - eprintln!("Failed to run Node.js installer: {}", e); - StatusCode::INTERNAL_SERVER_ERROR - })?; - - if !output.status.success() { - eprintln!( - "Failed to install Node.js: {}", - String::from_utf8_lossy(&output.stderr) - ); - return Ok(Json(ExtensionResponse { - error: true, - message: Some(format!( - "Failed to install Node.js: {}", - String::from_utf8_lossy(&output.stderr) - )), - })); - } - eprintln!("Node.js installation completed"); - } else { - eprintln!( - "Node.js installer script not found at: {}", - install_script.display() - ); - return Ok(Json(ExtensionResponse { - error: true, - message: Some("Node.js installer script not found".to_string()), - })); - } - } - } - } - - let agent = state.get_agent_for_route(request.session_id).await?; - let response = agent.add_extension(request.config).await; - - // Respond with the result. - match response { - Ok(_) => Ok(Json(ExtensionResponse { - error: false, - message: None, - })), - Err(e) => { - eprintln!("Failed to add extension configuration: {:?}", e); - Ok(Json(ExtensionResponse { - error: true, - message: Some(format!( - "Failed to add extension configuration, error: {:?}", - e - )), - })) - } - } -} - -#[derive(Deserialize)] -struct RemoveExtensionRequest { - name: String, - session_id: String, -} - -/// Handler for removing an extension by name -async fn remove_extension( - State(state): State>, - Json(request): Json, -) -> Result, StatusCode> { - let agent = state.get_agent_for_route(request.session_id).await?; - - match agent.remove_extension(&request.name).await { - Ok(_) => Ok(Json(ExtensionResponse { - error: false, - message: None, - })), - Err(e) => Ok(Json(ExtensionResponse { - error: true, - message: Some(format!("Failed to remove extension: {:?}", e)), - })), - } -} - -pub fn routes(state: Arc) -> Router { - Router::new() - .route("/extensions/add", post(add_extension)) - .route("/extensions/remove", post(remove_extension)) - .with_state(state) -} diff --git a/crates/goose-server/src/routes/mod.rs b/crates/goose-server/src/routes/mod.rs index 7deafd6b7b2d..9ca3799e5d89 100644 --- a/crates/goose-server/src/routes/mod.rs +++ b/crates/goose-server/src/routes/mod.rs @@ -2,7 +2,6 @@ pub mod agent; pub mod audio; pub mod config_management; pub mod errors; -pub mod extension; pub mod recipe; pub mod recipe_utils; pub mod reply; @@ -11,6 +10,7 @@ pub mod session; pub mod setup; pub mod status; pub mod utils; + use std::sync::Arc; use axum::Router; @@ -22,7 +22,6 @@ pub fn configure(state: Arc) -> Router { .merge(reply::routes(state.clone())) .merge(agent::routes(state.clone())) .merge(audio::routes(state.clone())) - .merge(extension::routes(state.clone())) .merge(config_management::routes(state.clone())) .merge(recipe::routes(state.clone())) .merge(session::routes(state.clone())) diff --git a/crates/goose-server/src/routes/recipe_utils.rs b/crates/goose-server/src/routes/recipe_utils.rs index bb9c6421f247..13e088802e51 100644 --- a/crates/goose-server/src/routes/recipe_utils.rs +++ b/crates/goose-server/src/routes/recipe_utils.rs @@ -17,7 +17,6 @@ use goose::recipe::local_recipes::{get_recipe_library_dir, list_local_recipes}; use goose::recipe::validate_recipe::validate_recipe_template_from_content; use goose::recipe::Recipe; use serde_json::Value; -use serde_yaml; use tracing::error; pub struct RecipeValidationError { @@ -63,7 +62,7 @@ pub fn get_all_recipes_manifests() -> Result> { } pub fn validate_recipe(recipe: &Recipe) -> Result<(), RecipeValidationError> { - let recipe_yaml = serde_yaml::to_string(recipe).map_err(|err| { + let recipe_yaml = recipe.to_yaml().map_err(|err| { let message = err.to_string(); error!("Failed to serialize recipe for validation: {}", message); RecipeValidationError { @@ -132,7 +131,7 @@ pub async fn build_recipe_with_parameter_values( original_recipe: &Recipe, user_recipe_values: HashMap, ) -> Result> { - let recipe_content = serde_yaml::to_string(&original_recipe)?; + let recipe_content = original_recipe.to_yaml()?; let recipe_dir = get_recipe_library_dir(true); let params = user_recipe_values.into_iter().collect(); @@ -156,15 +155,13 @@ pub async fn apply_recipe_to_agent( recipe: &Recipe, include_final_output_tool: bool, ) -> Option { - if let Some(sub_recipes) = &recipe.sub_recipes { - agent.add_sub_recipes(sub_recipes.clone()).await; - } - - if include_final_output_tool { - if let Some(response) = &recipe.response { - agent.add_final_output_tool(response.clone()).await; - } - } + agent + .apply_recipe_components( + recipe.sub_recipes.clone(), + recipe.response.clone(), + include_final_output_tool, + ) + .await; recipe.instructions.as_ref().map(|instructions| { let mut context: HashMap<&str, Value> = HashMap::new(); diff --git a/crates/goose-server/src/routes/reply.rs b/crates/goose-server/src/routes/reply.rs index 4c4a81925c57..5a340fa3d947 100644 --- a/crates/goose-server/src/routes/reply.rs +++ b/crates/goose-server/src/routes/reply.rs @@ -8,7 +8,7 @@ use axum::{ }; use bytes::Bytes; use futures::{stream::StreamExt, Stream}; -use goose::conversation::message::{Message, MessageContent}; +use goose::conversation::message::{Message, MessageContent, TokenState}; use goose::conversation::Conversation; use goose::permission::{Permission, PermissionConfirmation}; use goose::session::SessionManager; @@ -126,12 +126,14 @@ impl IntoResponse for SseResponse { pub enum MessageEvent { Message { message: Message, + token_state: TokenState, }, Error { error: String, }, Finish { reason: String, + token_state: TokenState, }, ModelChange { model: String, @@ -148,6 +150,27 @@ pub enum MessageEvent { Ping, } +async fn get_token_state(session_id: &str) -> TokenState { + SessionManager::get_session(session_id, false) + .await + .map(|session| TokenState { + input_tokens: session.input_tokens.unwrap_or(0), + output_tokens: session.output_tokens.unwrap_or(0), + total_tokens: session.total_tokens.unwrap_or(0), + accumulated_input_tokens: session.accumulated_input_tokens.unwrap_or(0), + accumulated_output_tokens: session.accumulated_output_tokens.unwrap_or(0), + accumulated_total_tokens: session.accumulated_total_tokens.unwrap_or(0), + }) + .inspect_err(|e| { + tracing::warn!( + "Failed to fetch session token state for {}: {}", + session_id, + e + ); + }) + .unwrap_or_default() +} + async fn stream_event( event: MessageEvent, tx: &mpsc::Sender, @@ -159,6 +182,7 @@ async fn stream_event( e ) }); + if tx.send(format!("data: {}\n\n", json)).await.is_err() { tracing::info!("client hung up"); cancel_token.cancel(); @@ -255,17 +279,30 @@ pub async fn reply( let session_config = SessionConfig { id: session_id.clone(), - working_dir: session.working_dir.clone(), schedule_id: session.schedule_id.clone(), - execution_mode: None, max_turns: None, retry_config: None, }; + let user_message = match messages.last() { + Some(msg) => msg, + _ => { + let _ = stream_event( + MessageEvent::Error { + error: "Reply started with empty messages".to_string(), + }, + &task_tx, + &task_cancel, + ) + .await; + return; + } + }; + let mut stream = match agent .reply( - messages.clone(), - Some(session_config.clone()), + user_message.clone(), + session_config, Some(task_cancel.clone()), ) .await @@ -305,7 +342,10 @@ pub async fn reply( } all_messages.push(message.clone()); - stream_event(MessageEvent::Message { message }, &tx, &cancel_token).await; + + let token_state = get_token_state(&session_id).await; + + stream_event(MessageEvent::Message { message, token_state }, &tx, &cancel_token).await; } Ok(Some(Ok(AgentEvent::HistoryReplaced(new_messages)))) => { all_messages = new_messages.clone(); @@ -397,9 +437,12 @@ pub async fn reply( ); } + let final_token_state = get_token_state(&session_id).await; + let _ = stream_event( MessageEvent::Finish { reason: "stop".to_string(), + token_state: final_token_state, }, &task_tx, &cancel_token, diff --git a/crates/goose-server/src/routes/schedule.rs b/crates/goose-server/src/routes/schedule.rs index b15e3f131481..077bf0e2c364 100644 --- a/crates/goose-server/src/routes/schedule.rs +++ b/crates/goose-server/src/routes/schedule.rs @@ -115,7 +115,6 @@ async fn create_schedule( paused: false, current_session_id: None, process_start_time: None, - execution_mode: req.execution_mode.or(Some("background".to_string())), // Default to background }; scheduler .add_scheduled_job(job.clone()) diff --git a/crates/goose-server/src/state.rs b/crates/goose-server/src/state.rs index b8b916bf9102..274da82939b3 100644 --- a/crates/goose-server/src/state.rs +++ b/crates/goose-server/src/state.rs @@ -49,7 +49,6 @@ impl AppState { self.agent_manager.get_or_create_agent(session_id).await } - /// Get agent for route handlers - always uses Interactive mode and converts any error to 500 pub async fn get_agent_for_route( &self, session_id: String, diff --git a/crates/goose-test/src/mcp/stdio/playback.rs b/crates/goose-test/src/mcp/stdio/playback.rs index 82f3897ecf1e..49c3e4567e20 100644 --- a/crates/goose-test/src/mcp/stdio/playback.rs +++ b/crates/goose-test/src/mcp/stdio/playback.rs @@ -20,7 +20,7 @@ struct LogEntry { fn parse_log_line(line: &str) -> Option { line.find(": ").and_then(|pos| { let (prefix, content) = line.split_at(pos); - let content = &content[2..]; // Skip ": " + let content = content.get(2..)?; // Skip ": " let stream_type = match prefix { "STDIN" => StreamType::Stdin, diff --git a/crates/goose/Cargo.toml b/crates/goose/Cargo.toml index e70471c8909f..d5a175892f53 100644 --- a/crates/goose/Cargo.toml +++ b/crates/goose/Cargo.toml @@ -67,6 +67,7 @@ tracing = "0.1" tracing-subscriber = "0.3" tracing-opentelemetry = "0.28" opentelemetry = "0.27" +opentelemetry-appender-tracing = "0.27" opentelemetry_sdk = { version = "0.27", features = ["rt-tokio", "metrics"] } opentelemetry-otlp = { version = "0.27", features = ["grpc-tonic", "http-proto", "reqwest-client"] } tonic = "0.12" @@ -105,6 +106,10 @@ sys-info = "0.9" oauth2 = "5.0.0" schemars = { version = "1.0.4", default-features = false, features = ["derive"] } insta = "1.43.2" +paste = "1.0.0" +shellexpand = "3.1.1" +indexmap = "2.12.0" +ignore = "0.4.25" [target.'cfg(target_os = "windows")'.dependencies] diff --git a/crates/goose/examples/agent.rs b/crates/goose/examples/agent.rs index d57226a7a797..014d63b302ad 100644 --- a/crates/goose/examples/agent.rs +++ b/crates/goose/examples/agent.rs @@ -1,11 +1,13 @@ use dotenvy::dotenv; use futures::StreamExt; -use goose::agents::{Agent, AgentEvent, ExtensionConfig}; +use goose::agents::{Agent, AgentEvent, ExtensionConfig, SessionConfig}; use goose::config::{DEFAULT_EXTENSION_DESCRIPTION, DEFAULT_EXTENSION_TIMEOUT}; use goose::conversation::message::Message; -use goose::conversation::Conversation; use goose::providers::create_with_named_model; use goose::providers::databricks::DATABRICKS_DEFAULT_MODEL; +use goose::session::session_manager::SessionType; +use goose::session::SessionManager; +use std::path::PathBuf; #[tokio::main] async fn main() { @@ -32,11 +34,29 @@ async fn main() { println!(" {}", extension); } - let conversation = Conversation::new(vec![Message::user() - .with_text("can you summarize the readme.md in this dir using just a haiku?")]) - .unwrap(); + let session = SessionManager::create_session( + PathBuf::default(), + "max-turn-test".to_string(), + SessionType::Hidden, + ) + .await + .expect("session manager creation failed"); + + let session_config = SessionConfig { + id: session.id, + schedule_id: None, + max_turns: None, + retry_config: None, + }; + + let user_message = Message::user() + .with_text("can you summarize the readme.md in this dir using just a haiku?"); + + let mut stream = agent + .reply(user_message, session_config, None) + .await + .unwrap(); - let mut stream = agent.reply(conversation, None, None).await.unwrap(); while let Some(Ok(AgentEvent::Message(message))) = stream.next().await { println!("{}", serde_json::to_string_pretty(&message).unwrap()); println!("\n"); diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index 8dc45289c39a..38d8fca390e2 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -28,8 +28,8 @@ use crate::agents::subagent_execution_tool::tasks_manager::TasksManager; use crate::agents::tool_route_manager::ToolRouteManager; use crate::agents::tool_router_index_manager::ToolRouterIndexManager; use crate::agents::types::SessionConfig; -use crate::agents::types::{FrontendTool, ToolResultReceiver}; -use crate::config::{get_enabled_extensions, Config}; +use crate::agents::types::{FrontendTool, SharedProvider, ToolResultReceiver}; +use crate::config::{get_enabled_extensions, Config, GooseMode}; use crate::context_mgmt::DEFAULT_COMPACTION_THRESHOLD; use crate::conversation::{debug_conversation_fix, fix_conversation, Conversation}; use crate::mcp_utils::ToolResult; @@ -61,11 +61,11 @@ use super::tool_execution::{ToolCallResult, CHAT_MODE_TOOL_SKIPPED_RESPONSE, DEC use crate::agents::subagent_task_config::TaskConfig; use crate::conversation::message::{Message, MessageContent, SystemNotificationType, ToolRequest}; use crate::session::extension_data::{EnabledExtensionsState, ExtensionState}; -use crate::session::SessionManager; +use crate::session::{Session, SessionManager}; const DEFAULT_MAX_TURNS: u32 = 1000; const COMPACTION_THINKING_TEXT: &str = "goose is compacting the conversation..."; -const MANUAL_COMPACT_TRIGGER: &str = "Please compact this conversation"; +pub const MANUAL_COMPACT_TRIGGER: &str = "Please compact this conversation"; /// Context needed for the reply function pub struct ReplyContext { @@ -73,9 +73,8 @@ pub struct ReplyContext { pub tools: Vec, pub toolshim_tools: Vec, pub system_prompt: String, - pub goose_mode: String, + pub goose_mode: GooseMode, pub initial_messages: Vec, - pub config: &'static Config, } pub struct ToolCategorizeResult { @@ -86,7 +85,8 @@ pub struct ToolCategorizeResult { /// The main goose Agent pub struct Agent { - pub(super) provider: Mutex>>, + pub(super) provider: SharedProvider, + pub extension_manager: Arc, pub(super) sub_recipe_manager: Mutex, pub(super) tasks_manager: TasksManager, @@ -159,10 +159,11 @@ impl Agent { // Create channels with buffer size 32 (adjust if needed) let (confirm_tx, confirm_rx) = mpsc::channel(32); let (tool_tx, tool_rx) = mpsc::channel(32); + let provider = Arc::new(Mutex::new(None)); Self { - provider: Mutex::new(None), - extension_manager: Arc::new(ExtensionManager::new()), + provider: provider.clone(), + extension_manager: Arc::new(ExtensionManager::new(provider.clone())), sub_recipe_manager: Mutex::new(SubRecipeManager::new()), tasks_manager: TasksManager::new(), final_output_tool: Arc::new(Mutex::new(None)), @@ -191,7 +192,7 @@ impl Agent { // Add permission inspector (medium-high priority) // Note: mode will be updated dynamically based on session config tool_inspection_manager.add_inspector(Box::new(PermissionInspector::new( - "smart_approve".to_string(), + GooseMode::SmartApprove, std::collections::HashSet::new(), // readonly tools - will be populated from extension manager std::collections::HashSet::new(), // regular tools - will be populated from extension manager ))); @@ -217,16 +218,20 @@ impl Agent { self.retry_manager.get_attempts().await } - /// Handle retry logic for the agent reply loop async fn handle_retry_logic( &self, messages: &mut Conversation, - session: &Option, + session_config: &SessionConfig, initial_messages: &[Message], ) -> Result { let result = self .retry_manager - .handle_retry_logic(messages, session, initial_messages, &self.final_output_tool) + .handle_retry_logic( + messages, + session_config, + initial_messages, + &self.final_output_tool, + ) .await?; match result { @@ -240,7 +245,7 @@ impl Agent { async fn prepare_reply_context( &self, unfixed_conversation: Conversation, - session: &Option, + working_dir: &std::path::Path, ) -> Result { let unfixed_messages = unfixed_conversation.messages().clone(); let (conversation, issues) = fix_conversation(unfixed_conversation.clone()); @@ -257,12 +262,12 @@ impl Agent { let initial_messages = conversation.messages().clone(); let config = Config::global(); - let (tools, toolshim_tools, system_prompt) = self.prepare_tools_and_prompt().await?; - let goose_mode = Self::determine_goose_mode(session.as_ref(), config); + let (tools, toolshim_tools, system_prompt) = + self.prepare_tools_and_prompt(working_dir).await?; + let goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto); - // Update permission inspector mode to match the session mode self.tool_inspection_manager - .update_permission_inspector_mode(goose_mode.clone()) + .update_permission_inspector_mode(goose_mode) .await; Ok(ReplyContext { @@ -272,7 +277,6 @@ impl Agent { system_prompt, goose_mode, initial_messages, - config, }) } @@ -297,7 +301,7 @@ impl Agent { permission_check_result: &PermissionCheckResult, message_tool_response: Arc>, cancel_token: Option, - session: Option, + session: &Session, ) -> Result> { let mut tool_futures: Vec<(String, ToolStream)> = Vec::new(); @@ -309,7 +313,7 @@ impl Agent { tool_call, request.id.clone(), cancel_token.clone(), - session.clone(), + session, ) .await; @@ -383,6 +387,23 @@ impl Agent { sub_recipe_manager.add_sub_recipe_tools(sub_recipes); } + pub async fn apply_recipe_components( + &self, + sub_recipes: Option>, + response: Option, + include_final_output: bool, + ) { + if let Some(sub_recipes) = sub_recipes { + self.add_sub_recipes(sub_recipes).await; + } + + if include_final_output { + if let Some(response) = response { + self.add_final_output_tool(response).await; + } + } + } + /// Dispatch a single tool call to the appropriate client #[instrument(skip(self, tool_call, request_id), fields(input, output))] pub async fn dispatch_tool_call( @@ -390,7 +411,7 @@ impl Agent { tool_call: CallToolRequestParam, request_id: String, cancellation_token: Option, - session: Option, + session: &Session, ) -> (String, Result) { if tool_call.name == PLATFORM_MANAGE_SCHEDULE_TOOL_NAME { let arguments = tool_call @@ -449,17 +470,13 @@ impl Agent { ); } }; - let (parent_session_id, parent_working_dir) = match session.as_ref() { - Some(s) => (Some(s.id.clone()), s.working_dir.clone()), - None => (None, std::env::current_dir().unwrap_or_default()), - }; // Get extensions from the agent's runtime state rather than global config // This ensures subagents inherit extensions that were dynamically enabled by the parent let extensions = self.get_extension_configs().await; let task_config = - TaskConfig::new(provider, parent_session_id, parent_working_dir, extensions); + TaskConfig::new(provider, &session.id, &session.working_dir, extensions); let arguments = match tool_call.arguments.clone() { Some(args) => Value::Object(args), @@ -729,116 +746,112 @@ impl Agent { } } - #[instrument(skip(self, unfixed_conversation, session), fields(user_message))] + #[instrument(skip(self, user_message, session_config), fields(user_message))] pub async fn reply( &self, - unfixed_conversation: Conversation, - session: Option, + user_message: Message, + session_config: SessionConfig, cancel_token: Option, ) -> Result>> { - let is_manual_compact = unfixed_conversation.messages().last().is_some_and(|msg| { - msg.content.iter().any(|c| { - if let MessageContent::Text(text) = c { - text.text.trim() == MANUAL_COMPACT_TRIGGER - } else { - false - } - }) + let is_manual_compact = user_message.content.iter().any(|c| { + if let MessageContent::Text(text) = c { + text.text.trim() == MANUAL_COMPACT_TRIGGER + } else { + false + } }); - if !is_manual_compact { - let session_metadata = if let Some(session_config) = &session { - SessionManager::get_session(&session_config.id, false) - .await - .ok() - } else { - None - }; + SessionManager::add_message(&session_config.id, &user_message).await?; + let session = SessionManager::get_session(&session_config.id, true).await?; - let needs_auto_compact = crate::context_mgmt::check_if_compaction_needed( - self, - &unfixed_conversation, - None, - session_metadata.as_ref(), - ) - .await?; + let conversation = session + .conversation + .clone() + .ok_or_else(|| anyhow::anyhow!("Session {} has no conversation", session_config.id))?; - if !needs_auto_compact { - return self - .reply_internal(unfixed_conversation, session, cancel_token) - .await; - } - } + let needs_auto_compact = !is_manual_compact + && crate::context_mgmt::check_if_compaction_needed(self, &conversation, None, &session) + .await?; - let conversation_to_compact = unfixed_conversation.clone(); + let conversation_to_compact = conversation.clone(); Ok(Box::pin(async_stream::try_stream! { - if !is_manual_compact { - let config = crate::config::Config::global(); - let threshold = config - .get_param::("GOOSE_AUTO_COMPACT_THRESHOLD") - .unwrap_or(DEFAULT_COMPACTION_THRESHOLD); - let threshold_percentage = (threshold * 100.0) as u32; - - let inline_msg = format!( - "Exceeded auto-compact threshold of {}%. Performing auto-compaction...", - threshold_percentage - ); + let final_conversation = if !needs_auto_compact && !is_manual_compact { + conversation + } else { + if !is_manual_compact { + let config = crate::config::Config::global(); + let threshold = config + .get_param::("GOOSE_AUTO_COMPACT_THRESHOLD") + .unwrap_or(DEFAULT_COMPACTION_THRESHOLD); + let threshold_percentage = (threshold * 100.0) as u32; + + let inline_msg = format!( + "Exceeded auto-compact threshold of {}%. Performing auto-compaction...", + threshold_percentage + ); + + yield AgentEvent::Message( + Message::assistant().with_system_notification( + SystemNotificationType::InlineMessage, + inline_msg, + ) + ); + } yield AgentEvent::Message( Message::assistant().with_system_notification( - SystemNotificationType::InlineMessage, - inline_msg, + SystemNotificationType::ThinkingMessage, + COMPACTION_THINKING_TEXT, ) ); - } - yield AgentEvent::Message( - Message::assistant().with_system_notification( - SystemNotificationType::ThinkingMessage, - COMPACTION_THINKING_TEXT, - ) - ); - - match crate::context_mgmt::compact_messages(self, &conversation_to_compact, false).await { - Ok((compacted_conversation, _token_counts, _summarization_usage)) => { - if let Some(session_to_store) = &session { - SessionManager::replace_conversation(&session_to_store.id, &compacted_conversation).await?; - } + match crate::context_mgmt::compact_messages(self, &conversation_to_compact, false).await { + Ok((compacted_conversation, summarization_usage)) => { + SessionManager::replace_conversation(&session_config.id, &compacted_conversation).await?; + Self::update_session_metrics(&session_config, &summarization_usage, true).await?; - yield AgentEvent::HistoryReplaced(compacted_conversation.clone()); + yield AgentEvent::HistoryReplaced(compacted_conversation.clone()); - yield AgentEvent::Message( - Message::assistant().with_system_notification( - SystemNotificationType::InlineMessage, - "Compaction complete", - ) - ); + yield AgentEvent::Message( + Message::assistant().with_system_notification( + SystemNotificationType::InlineMessage, + "Compaction complete", + ) + ); - if !is_manual_compact { - let mut reply_stream = self.reply_internal(compacted_conversation, session, cancel_token).await?; - while let Some(event) = reply_stream.next().await { - yield event?; - } + compacted_conversation + } + Err(e) => { + yield AgentEvent::Message( + Message::assistant().with_text( + format!("Ran into this error trying to compact: {e}.\n\nPlease try again or create a new session") + ) + ); + return; } } - Err(e) => { - yield AgentEvent::Message(Message::assistant().with_text( - format!("Ran into this error trying to compact: {e}.\n\nPlease try again or create a new session") - )); + }; + + if !is_manual_compact { + let mut reply_stream = self.reply_internal(final_conversation, session_config, session, cancel_token).await?; + while let Some(event) = reply_stream.next().await { + yield event?; } } })) } - /// Main reply method that handles the actual agent processing async fn reply_internal( &self, conversation: Conversation, - session: Option, + session_config: SessionConfig, + session: Session, cancel_token: Option, ) -> Result>> { - let context = self.prepare_reply_context(conversation, &session).await?; + let context = self + .prepare_reply_context(conversation, &session.working_dir) + .await?; let ReplyContext { mut conversation, mut tools, @@ -846,66 +859,23 @@ impl Agent { mut system_prompt, goose_mode, initial_messages, - config, } = context; let reply_span = tracing::Span::current(); self.reset_retry_attempts().await; - // This will need further refactoring. In the ideal world we pass the new message into - // reply and load the existing conversation. Until we get to that point, fetch the conversation - // so far and append the last (user) message that the caller already added. - if let Some(session_config) = &session { - let stored_conversation = SessionManager::get_session(&session_config.id, true) - .await? - .conversation - .ok_or_else(|| { - anyhow::anyhow!("Session {} has no conversation", session_config.id) - })?; - - match conversation.len().cmp(&stored_conversation.len()) { - std::cmp::Ordering::Equal => { - if conversation != stored_conversation { - warn!("Session messages mismatch - replacing with incoming"); - SessionManager::replace_conversation(&session_config.id, &conversation) - .await?; - } - } - std::cmp::Ordering::Greater - if conversation.len() == stored_conversation.len() + 1 => - { - let last_message = conversation.last().unwrap(); - if let Some(content) = last_message.content.first().and_then(|c| c.as_text()) { - debug!("user_message" = &content); - } - SessionManager::add_message(&session_config.id, last_message).await?; - } - _ => { - warn!( - "Unexpected session state: stored={}, incoming={}. Replacing.", - stored_conversation.len(), - conversation.len() - ); - SessionManager::replace_conversation(&session_config.id, &conversation).await?; - } + let provider = self.provider().await?; + let session_id = session_config.id.clone(); + let working_dir = session.working_dir.clone(); + tokio::spawn(async move { + if let Err(e) = SessionManager::maybe_update_name(&session_id, provider).await { + warn!("Failed to generate session description: {}", e); } - let provider = self.provider().await?; - let session_id = session_config.id.clone(); - tokio::spawn(async move { - if let Err(e) = SessionManager::maybe_update_name(&session_id, provider).await { - warn!("Failed to generate session description: {}", e); - } - }); - } + }); Ok(Box::pin(async_stream::try_stream! { let _ = reply_span.enter(); let mut turns_taken = 0u32; - let max_turns = session - .as_ref() - .and_then(|s| s.max_turns) - .unwrap_or_else(|| { - config.get_param("GOOSE_MAX_TURNS").unwrap_or(DEFAULT_MAX_TURNS) - }); + let max_turns = session_config.max_turns.unwrap_or(DEFAULT_MAX_TURNS); loop { if is_token_cancelled(&cancel_token) { @@ -915,7 +885,7 @@ impl Agent { if let Some(final_output_tool) = self.final_output_tool.lock().await.as_ref() { if final_output_tool.final_output.is_some() { let final_event = AgentEvent::Message( - Message::assistant().with_text(final_output_tool.final_output.clone().unwrap()), + Message::assistant().with_text(final_output_tool.final_output.clone().unwrap()) ); yield final_event; break; @@ -924,9 +894,11 @@ impl Agent { turns_taken += 1; if turns_taken > max_turns { - yield AgentEvent::Message(Message::assistant().with_text( - "I've reached the maximum number of actions I can do without user input. Would you like me to continue?" - )); + yield AgentEvent::Message( + Message::assistant().with_text( + "I've reached the maximum number of actions I can do without user input. Would you like me to continue?" + ) + ); break; } @@ -984,11 +956,8 @@ impl Agent { } } - // Record usage for the session - if let Some(ref session_config) = &session { - if let Some(ref usage) = usage { - Self::update_session_metrics(session_config, usage).await?; - } + if let Some(ref usage) = usage { + Self::update_session_metrics(&session_config, usage, false).await?; } if let Some(response) = response { @@ -1024,8 +993,7 @@ impl Agent { yield AgentEvent::Message(msg); } - let mode = goose_mode.clone(); - if mode.as_str() == "chat" { + if goose_mode == GooseMode::Chat { // Skip all tool calls in chat mode for request in remaining_requests { let mut response = message_tool_response.lock().await; @@ -1074,18 +1042,17 @@ impl Agent { &permission_check_result, message_tool_response.clone(), cancel_token.clone(), - session.clone(), + &session, ).await?; let tool_futures_arc = Arc::new(Mutex::new(tool_futures)); - // Process tools requiring approval let mut tool_approval_stream = self.handle_approval_tool_requests( &permission_check_result.needs_approval, tool_futures_arc.clone(), message_tool_response.clone(), cancel_token.clone(), - session.clone(), + &session, &inspection_results, ); @@ -1132,10 +1099,8 @@ impl Agent { } if all_install_successful && !enable_extension_request_ids.is_empty() { - if let Some(ref session_config) = session { - if let Err(e) = self.save_extension_state(session_config).await { - warn!("Failed to save extension state after runtime changes: {}", e); - } + if let Err(e) = self.save_extension_state(&session_config).await { + warn!("Failed to save extension state after runtime changes: {}", e); } tools_updated = true; } @@ -1163,37 +1128,39 @@ impl Agent { ); match crate::context_mgmt::compact_messages(self, &conversation, true).await { - Ok((compacted_conversation, _token_counts, _usage)) => { - if let Some(session_to_store) = &session { - SessionManager::replace_conversation(&session_to_store.id, &compacted_conversation).await? - } - + Ok((compacted_conversation, usage)) => { + SessionManager::replace_conversation(&session_config.id, &compacted_conversation).await?; + Self::update_session_metrics(&session_config, &usage, true).await?; conversation = compacted_conversation; did_recovery_compact_this_iteration = true; - yield AgentEvent::HistoryReplaced(conversation.clone()); continue; } Err(e) => { error!("Error: {}", e); - yield AgentEvent::Message(Message::assistant().with_text( + yield AgentEvent::Message( + Message::assistant().with_text( format!("Ran into this error trying to compact: {e}.\n\nPlease retry if you think this is a transient or recoverable error.") - )); + ) + ); break; } } } Err(e) => { error!("Error: {}", e); - yield AgentEvent::Message(Message::assistant().with_text( + yield AgentEvent::Message( + Message::assistant().with_text( format!("Ran into this error: {e}.\n\nPlease retry if you think this is a transient or recoverable error.") - )); + ) + ); break; } } } if tools_updated { - (tools, toolshim_tools, system_prompt) = self.prepare_tools_and_prompt().await?; + (tools, toolshim_tools, system_prompt) = + self.prepare_tools_and_prompt(&working_dir).await?; } let mut exit_chat = false; if no_tools_called { @@ -1212,7 +1179,7 @@ impl Agent { } else if did_recovery_compact_this_iteration { // Avoid setting exit_chat; continue from last user message in the conversation } else { - match self.handle_retry_logic(&mut conversation, &session, &initial_messages).await { + match self.handle_retry_logic(&mut conversation, &session_config, &initial_messages).await { Ok(should_retry) => { if should_retry { info!("Retry logic triggered, restarting agent loop"); @@ -1222,19 +1189,19 @@ impl Agent { } Err(e) => { error!("Retry logic failed: {}", e); - yield AgentEvent::Message(Message::assistant().with_text( - format!("Retry logic encountered an error: {}", e) - )); + yield AgentEvent::Message( + Message::assistant().with_text( + format!("Retry logic encountered an error: {}", e) + ) + ); exit_chat = true; } } } } - if let Some(session_config) = &session { - for msg in &messages_to_add { - SessionManager::add_message(&session_config.id, msg).await?; - } + for msg in &messages_to_add { + SessionManager::add_message(&session_config.id, msg).await?; } conversation.extend(messages_to_add); if exit_chat { @@ -1246,19 +1213,6 @@ impl Agent { })) } - fn determine_goose_mode(session: Option<&SessionConfig>, config: &Config) -> String { - let mode = session.and_then(|s| s.execution_mode.as_deref()); - - match mode { - Some("foreground") => "chat".to_string(), - Some("background") => "auto".to_string(), - _ => config - .get_param("GOOSE_MODE") - .unwrap_or_else(|_| "auto".to_string()), - } - } - - /// Extend the system prompt with one line of additional instruction pub async fn extend_system_prompt(&self, instruction: String) { let mut prompt_manager = self.prompt_manager.lock().await; prompt_manager.add_system_prompt_extra(instruction); @@ -1376,6 +1330,7 @@ impl Agent { .with_extensions(extensions_info.into_iter()) .with_frontend_instructions(self.frontend_instructions.lock().await.clone()) .with_extension_and_tool_counts(extension_count, tool_count) + .with_hints(&std::env::current_dir()?) .build(); let recipe_prompt = prompt_manager.get_recipe_prompt().await; @@ -1434,17 +1389,9 @@ impl Agent { .unwrap_or(&content) .trim() .to_string(); - tracing::debug!( - "Cleaned content for parsing: {}", - &clean_content[..std::cmp::min(200, clean_content.len())] - ); - // try to parse json response from the LLM - tracing::debug!("Attempting to parse recipe content as JSON"); let (instructions, activities) = if let Ok(json_content) = serde_json::from_str::(&clean_content) { - tracing::debug!("Successfully parsed JSON content"); - let instructions = json_content .get("instructions") .ok_or_else(|| anyhow!("Missing 'instructions' in json response"))? @@ -1513,7 +1460,7 @@ impl Agent { // but it doesn't know and the plumbing looks complicated. let config = Config::global(); let provider_name: String = config - .get_param("GOOSE_PROVIDER") + .get_goose_provider() .expect("No provider configured. Run 'goose configure' first"); let settings = Settings { diff --git a/crates/goose/src/agents/extension.rs b/crates/goose/src/agents/extension.rs index 284a98cf5b7f..de63751c1e56 100644 --- a/crates/goose/src/agents/extension.rs +++ b/crates/goose/src/agents/extension.rs @@ -11,6 +11,7 @@ use once_cell::sync::Lazy; use rmcp::model::Tool; use rmcp::service::ClientInitializeError; use rmcp::ServiceError as ClientError; +use serde::Deserializer; use serde::{Deserialize, Serialize}; use thiserror::Error; use tracing::warn; @@ -215,6 +216,9 @@ pub enum ExtensionConfig { Sse { /// The name used to identify this extension name: String, + #[serde(default)] + #[serde(deserialize_with = "deserialize_null_with_default")] + #[schema(required)] description: String, uri: String, #[serde(default)] @@ -234,6 +238,9 @@ pub enum ExtensionConfig { Stdio { /// The name used to identify this extension name: String, + #[serde(default)] + #[serde(deserialize_with = "deserialize_null_with_default")] + #[schema(required)] description: String, cmd: String, args: Vec, @@ -252,6 +259,9 @@ pub enum ExtensionConfig { Builtin { /// The name used to identify this extension name: String, + #[serde(default)] + #[serde(deserialize_with = "deserialize_null_with_default")] + #[schema(required)] description: String, display_name: Option, // needed for the UI timeout: Option, @@ -265,6 +275,8 @@ pub enum ExtensionConfig { Platform { /// The name used to identify this extension name: String, + #[serde(deserialize_with = "deserialize_null_with_default")] + #[schema(required)] description: String, #[serde(default)] bundled: Option, @@ -276,6 +288,8 @@ pub enum ExtensionConfig { StreamableHttp { /// The name used to identify this extension name: String, + #[serde(deserialize_with = "deserialize_null_with_default")] + #[schema(required)] description: String, uri: String, #[serde(default)] @@ -297,6 +311,8 @@ pub enum ExtensionConfig { Frontend { /// The name used to identify this extension name: String, + #[serde(deserialize_with = "deserialize_null_with_default")] + #[schema(required)] description: String, /// The tools provided by the frontend tools: Vec, @@ -312,6 +328,8 @@ pub enum ExtensionConfig { InlinePython { /// The name used to identify this extension name: String, + #[serde(deserialize_with = "deserialize_null_with_default")] + #[schema(required)] description: String, /// The Python code to execute code: String, @@ -530,6 +548,15 @@ impl ExtensionInfo { } } +fn deserialize_null_with_default<'de, D, T>(deserializer: D) -> Result +where + T: Default + Deserialize<'de>, + D: Deserializer<'de>, +{ + let opt = Option::deserialize(deserializer)?; + Ok(opt.unwrap_or_default()) +} + /// Information about the tool used for building prompts #[derive(Clone, Debug, Serialize, ToSchema)] pub struct ToolInfo { @@ -554,3 +581,69 @@ impl ToolInfo { } } } + +#[cfg(test)] +mod tests { + use crate::agents::*; + + #[test] + fn test_deserialize_missing_description() { + let config: ExtensionConfig = serde_yaml::from_str( + "enabled: true +type: builtin +name: developer +display_name: Developer +timeout: 300 +bundled: true +available_tools: []", + ) + .unwrap(); + if let ExtensionConfig::Builtin { description, .. } = config { + assert_eq!(description, "") + } else { + panic!("unexpected result of deserialization: {}", config) + } + } + + #[test] + fn test_deserialize_null_description() { + let config: ExtensionConfig = serde_yaml::from_str( + "enabled: true +type: builtin +name: developer +display_name: Developer +description: null +timeout: 300 +bundled: true +available_tools: [] +", + ) + .unwrap(); + if let ExtensionConfig::Builtin { description, .. } = config { + assert_eq!(description, "") + } else { + panic!("unexpected result of deserialization: {}", config) + } + } + + #[test] + fn test_deserialize_normal_description() { + let config: ExtensionConfig = serde_yaml::from_str( + "enabled: true +type: builtin +name: developer +display_name: Developer +description: description goes here +timeout: 300 +bundled: true +available_tools: [] + ", + ) + .unwrap(); + if let ExtensionConfig::Builtin { description, .. } = config { + assert_eq!(description, "description goes here") + } else { + panic!("unexpected result of deserialization: {}", config) + } + } +} diff --git a/crates/goose/src/agents/extension_manager.rs b/crates/goose/src/agents/extension_manager.rs index 7e81552f6c2c..a3a660a1b932 100644 --- a/crates/goose/src/agents/extension_manager.rs +++ b/crates/goose/src/agents/extension_manager.rs @@ -12,6 +12,7 @@ use rmcp::transport::{ TokioChildProcess, }; use std::collections::HashMap; +use std::option::Option; use std::process::Stdio; use std::sync::Arc; use std::time::Duration; @@ -29,9 +30,11 @@ use super::extension::{ ToolInfo, PLATFORM_EXTENSIONS, }; use super::tool_execution::ToolCallResult; +use super::types::SharedProvider; use crate::agents::extension::{Envs, ProcessExit}; use crate::agents::extension_malware_check; use crate::agents::mcp_client::{McpClient, McpClientTrait}; +use crate::config::search_path::search_path_var; use crate::config::{get_all_extensions, Config}; use crate::oauth::oauth_flow; use crate::prompt_template; @@ -90,6 +93,7 @@ impl Extension { pub struct ExtensionManager { extensions: Mutex>, context: Mutex, + provider: SharedProvider, } /// A flattened representation of a resource used by the agent to prepare inference @@ -170,18 +174,25 @@ pub fn get_parameter_names(tool: &Tool) -> Vec { impl Default for ExtensionManager { fn default() -> Self { - Self::new() + Self::new(Arc::new(Mutex::new(None))) } } async fn child_process_client( mut command: Command, timeout: &Option, + provider: SharedProvider, ) -> ExtensionResult { #[cfg(unix)] command.process_group(0); #[cfg(windows)] command.creation_flags(CREATE_NO_WINDOW_FLAG); + + command.env( + "PATH", + search_path_var().map_err(|e| ExtensionError::ConfigError(format!("{}", e)))?, + ); + let (transport, mut stderr) = TokioChildProcess::builder(command) .stderr(Stdio::piped()) .spawn()?; @@ -198,6 +209,7 @@ async fn child_process_client( let client_result = McpClient::connect( transport, Duration::from_secs(timeout.unwrap_or(crate::config::DEFAULT_EXTENSION_TIMEOUT)), + provider, ) .await; @@ -236,7 +248,7 @@ fn extract_auth_error( } impl ExtensionManager { - pub fn new() -> Self { + pub fn new(provider: SharedProvider) -> Self { Self { extensions: Mutex::new(HashMap::new()), context: Mutex::new(PlatformExtensionContext { @@ -244,9 +256,15 @@ impl ExtensionManager { extension_manager: None, tool_route_manager: None, }), + provider, } } + /// Create a new ExtensionManager with no provider (useful for tests) + pub fn new_without_provider() -> Self { + Self::new(Arc::new(Mutex::new(None))) + } + pub async fn set_context(&self, context: PlatformExtensionContext) { *self.context.lock().await = context; } @@ -341,6 +359,7 @@ impl ExtensionManager { Duration::from_secs( timeout.unwrap_or(crate::config::DEFAULT_EXTENSION_TIMEOUT), ), + self.provider.clone(), ) .await?, ) @@ -350,15 +369,56 @@ impl ExtensionManager { timeout, headers, name, + envs, + env_keys, .. } => { + // Merge environment variables from direct envs and keychain-stored env_keys + let all_envs = merge_environments(envs, env_keys, &sanitized_name).await?; + + // Helper function to substitute environment variables in a string + // Supports both ${VAR} and $VAR syntax + fn substitute_env_vars(value: &str, env_map: &HashMap) -> String { + let mut result = value.to_string(); + + // First handle ${VAR} syntax (with optional whitespace) + let re_braces = regex::Regex::new(r"\$\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}") + .expect("valid regex"); + for cap in re_braces.captures_iter(value) { + if let Some(var_name) = cap.get(1) { + if let Some(env_value) = env_map.get(var_name.as_str()) { + result = result.replace(&cap[0], env_value); + } + } + } + + // Then handle $VAR syntax (simple variable without braces) + let re_simple = + regex::Regex::new(r"\$([A-Za-z_][A-Za-z0-9_]*)").expect("valid regex"); + for cap in re_simple.captures_iter(&result.clone()) { + if let Some(var_name) = cap.get(1) { + // Only substitute if it wasn't already part of ${VAR} syntax + if !value.contains(&format!("${{{}}}", var_name.as_str())) { + if let Some(env_value) = env_map.get(var_name.as_str()) { + result = result.replace(&cap[0], env_value); + } + } + } + } + + result + } + let mut default_headers = HeaderMap::new(); for (key, value) in headers { + // Substitute environment variables in header values + let substituted_value = substitute_env_vars(value, &all_envs); + default_headers.insert( HeaderName::try_from(key).map_err(|_| { ExtensionError::ConfigError(format!("invalid header: {}", key)) })?, - value.parse().map_err(|_| { + substituted_value.parse().map_err(|_| { ExtensionError::ConfigError(format!("invalid header value: {}", key)) })?, ); @@ -381,6 +441,7 @@ impl ExtensionManager { Duration::from_secs( timeout.unwrap_or(crate::config::DEFAULT_EXTENSION_TIMEOUT), ), + self.provider.clone(), ) .await; let client = if let Some(_auth_error) = extract_auth_error(&client_res) { @@ -400,6 +461,7 @@ impl ExtensionManager { Duration::from_secs( timeout.unwrap_or(crate::config::DEFAULT_EXTENSION_TIMEOUT), ), + self.provider.clone(), ) .await? } else { @@ -423,7 +485,7 @@ impl ExtensionManager { // Check for malicious packages before launching the process extension_malware_check::deny_if_malicious_cmd_args(cmd, args).await?; - let client = child_process_client(command, timeout).await?; + let client = child_process_client(command, timeout, self.provider.clone()).await?; Box::new(client) } ExtensionConfig::Builtin { @@ -452,7 +514,7 @@ impl ExtensionManager { let command = Command::new(cmd).configure(|command| { command.arg("mcp").arg(name); }); - let client = child_process_client(command, timeout).await?; + let client = child_process_client(command, timeout, self.provider.clone()).await?; Box::new(client) } ExtensionConfig::Platform { name, .. } => { @@ -488,7 +550,7 @@ impl ExtensionManager { command.arg("python").arg(file_path.to_str().unwrap()); }); - let client = child_process_client(command, timeout).await?; + let client = child_process_client(command, timeout, self.provider.clone()).await?; Box::new(client) } @@ -1245,7 +1307,7 @@ mod tests { #[tokio::test] async fn test_get_client_for_tool() { - let extension_manager = ExtensionManager::new(); + let extension_manager = ExtensionManager::new_without_provider(); // Add some mock clients using the helper method extension_manager @@ -1305,7 +1367,7 @@ mod tests { async fn test_dispatch_tool_call() { // test that dispatch_tool_call parses out the sanitized name correctly, and extracts // tool_names - let extension_manager = ExtensionManager::new(); + let extension_manager = ExtensionManager::new_without_provider(); // Add some mock clients using the helper method extension_manager @@ -1422,7 +1484,7 @@ mod tests { #[tokio::test] async fn test_tool_availability_filtering() { - let extension_manager = ExtensionManager::new(); + let extension_manager = ExtensionManager::new_without_provider(); // Only "available_tool" should be available to the LLM let available_tools = vec!["available_tool".to_string()]; @@ -1450,7 +1512,7 @@ mod tests { #[tokio::test] async fn test_tool_availability_defaults_to_available() { - let extension_manager = ExtensionManager::new(); + let extension_manager = ExtensionManager::new_without_provider(); extension_manager .add_mock_extension_with_tools( @@ -1475,7 +1537,7 @@ mod tests { #[tokio::test] async fn test_dispatch_unavailable_tool_returns_error() { - let extension_manager = ExtensionManager::new(); + let extension_manager = ExtensionManager::new_without_provider(); let available_tools = vec!["available_tool".to_string()]; @@ -1518,4 +1580,72 @@ mod tests { assert!(result.is_ok()); } + + #[tokio::test] + async fn test_streamable_http_header_env_substitution() { + use std::collections::HashMap; + + // Test the substitute_env_vars helper function (which is defined inside add_extension) + // We'll recreate it here for testing purposes + fn substitute_env_vars(value: &str, env_map: &HashMap) -> String { + let mut result = value.to_string(); + + // First handle ${VAR} syntax (with optional whitespace) + let re_braces = + regex::Regex::new(r"\$\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}").expect("valid regex"); + for cap in re_braces.captures_iter(value) { + if let Some(var_name) = cap.get(1) { + if let Some(env_value) = env_map.get(var_name.as_str()) { + result = result.replace(&cap[0], env_value); + } + } + } + + // Then handle $VAR syntax (simple variable without braces) + let re_simple = regex::Regex::new(r"\$([A-Za-z_][A-Za-z0-9_]*)").expect("valid regex"); + for cap in re_simple.captures_iter(&result.clone()) { + if let Some(var_name) = cap.get(1) { + // Only substitute if it wasn't already part of ${VAR} syntax + if !value.contains(&format!("${{{}}}", var_name.as_str())) { + if let Some(env_value) = env_map.get(var_name.as_str()) { + result = result.replace(&cap[0], env_value); + } + } + } + } + + result + } + + let mut env_map = HashMap::new(); + env_map.insert("AUTH_TOKEN".to_string(), "secret123".to_string()); + env_map.insert("API_KEY".to_string(), "key456".to_string()); + + // Test ${VAR} syntax + let result = substitute_env_vars("Bearer ${ AUTH_TOKEN }", &env_map); + assert_eq!(result, "Bearer secret123"); + + // Test ${VAR} syntax without spaces + let result = substitute_env_vars("Bearer ${AUTH_TOKEN}", &env_map); + assert_eq!(result, "Bearer secret123"); + + // Test $VAR syntax + let result = substitute_env_vars("Bearer $AUTH_TOKEN", &env_map); + assert_eq!(result, "Bearer secret123"); + + // Test multiple substitutions + let result = substitute_env_vars("Key: $API_KEY, Token: ${AUTH_TOKEN}", &env_map); + assert_eq!(result, "Key: key456, Token: secret123"); + + // Test no substitution when variable doesn't exist + let result = substitute_env_vars("Bearer ${UNKNOWN_VAR}", &env_map); + assert_eq!(result, "Bearer ${UNKNOWN_VAR}"); + + // Test mixed content + let result = substitute_env_vars( + "Authorization: Bearer ${AUTH_TOKEN} and API ${API_KEY}", + &env_map, + ); + assert_eq!(result, "Authorization: Bearer secret123 and API key456"); + } } diff --git a/crates/goose/src/agents/mcp_client.rs b/crates/goose/src/agents/mcp_client.rs index a166b8f7bec2..d42d62488d9b 100644 --- a/crates/goose/src/agents/mcp_client.rs +++ b/crates/goose/src/agents/mcp_client.rs @@ -1,21 +1,25 @@ -use rmcp::model::JsonObject; +use crate::agents::types::SharedProvider; +use crate::session_context::SESSION_ID_HEADER; +use rmcp::model::{Content, ErrorCode, JsonObject}; /// MCP client implementation for Goose use rmcp::{ model::{ CallToolRequest, CallToolRequestParam, CallToolResult, CancelledNotification, CancelledNotificationMethod, CancelledNotificationParam, ClientCapabilities, ClientInfo, - ClientRequest, GetPromptRequest, GetPromptRequestParam, GetPromptResult, Implementation, - InitializeResult, ListPromptsRequest, ListPromptsResult, ListResourcesRequest, - ListResourcesResult, ListToolsRequest, ListToolsResult, LoggingMessageNotification, + ClientRequest, CreateMessageRequestParam, CreateMessageResult, GetPromptRequest, + GetPromptRequestParam, GetPromptResult, Implementation, InitializeResult, + ListPromptsRequest, ListPromptsResult, ListResourcesRequest, ListResourcesResult, + ListToolsRequest, ListToolsResult, LoggingMessageNotification, LoggingMessageNotificationMethod, PaginatedRequestParam, ProgressNotification, ProgressNotificationMethod, ProtocolVersion, ReadResourceRequest, ReadResourceRequestParam, - ReadResourceResult, RequestId, ServerNotification, ServerResult, + ReadResourceResult, RequestId, Role, SamplingMessage, ServerNotification, ServerResult, }, service::{ - ClientInitializeError, PeerRequestOptions, RequestHandle, RunningService, ServiceRole, + ClientInitializeError, PeerRequestOptions, RequestContext, RequestHandle, RunningService, + ServiceRole, }, transport::IntoTransport, - ClientHandler, Peer, RoleClient, ServiceError, ServiceExt, + ClientHandler, ErrorData, Peer, RoleClient, ServiceError, ServiceExt, }; use serde_json::Value; use std::{sync::Arc, time::Duration}; @@ -76,12 +80,17 @@ pub trait McpClientTrait: Send + Sync { pub struct GooseClient { notification_handlers: Arc>>>, + provider: SharedProvider, } impl GooseClient { - pub fn new(handlers: Arc>>>) -> Self { + pub fn new( + handlers: Arc>>>, + provider: SharedProvider, + ) -> Self { GooseClient { notification_handlers: handlers, + provider, } } } @@ -127,10 +136,88 @@ impl ClientHandler for GooseClient { }); } + async fn create_message( + &self, + params: CreateMessageRequestParam, + _context: RequestContext, + ) -> Result { + let provider = self + .provider + .lock() + .await + .as_ref() + .ok_or(ErrorData::new( + ErrorCode::INTERNAL_ERROR, + "Could not use provider", + None, + ))? + .clone(); + + let provider_ready_messages: Vec = params + .messages + .iter() + .map(|msg| { + let base = match msg.role { + Role::User => crate::conversation::message::Message::user(), + Role::Assistant => crate::conversation::message::Message::assistant(), + }; + + match msg.content.as_text() { + Some(text) => base.with_text(&text.text), + None => base.with_content(msg.content.clone().into()), + } + }) + .collect(); + + let system_prompt = params + .system_prompt + .as_deref() + .unwrap_or("You are a general-purpose AI agent called goose"); + + let (response, usage) = provider + .complete(system_prompt, &provider_ready_messages, &[]) + .await + .map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + "Unexpected error while completing the prompt", + Some(Value::from(e.to_string())), + ) + })?; + + Ok(CreateMessageResult { + model: usage.model, + stop_reason: Some(CreateMessageResult::STOP_REASON_END_TURN.to_string()), + message: SamplingMessage { + role: Role::Assistant, + // TODO(alexhancock): MCP sampling currently only supports one content on each SamplingMessage + // https://modelcontextprotocol.io/specification/draft/client/sampling#messages + // This doesn't mesh well with goose's approach which has Vec + // There is a proposal to MCP which is agreed to go in the next version to have SamplingMessages support multiple content parts + // https://github.com/modelcontextprotocol/modelcontextprotocol/pull/198 + // Until that is formalized, we can take the first message content from the provider and use it + content: if let Some(content) = response.content.first() { + match content { + crate::conversation::message::MessageContent::Text(text) => { + Content::text(&text.text) + } + crate::conversation::message::MessageContent::Image(img) => { + Content::image(&img.data, &img.mime_type) + } + // TODO(alexhancock) - Content::Audio? goose's messages don't currently have it + _ => Content::text(""), + } + } else { + Content::text("") + }, + }, + }) + } + fn get_info(&self) -> ClientInfo { ClientInfo { protocol_version: ProtocolVersion::V_2025_03_26, - capabilities: ClientCapabilities::builder().build(), + capabilities: ClientCapabilities::builder().enable_sampling().build(), client_info: Implementation { name: "goose".to_string(), version: std::env::var("GOOSE_MCP_CLIENT_VERSION") @@ -155,6 +242,7 @@ impl McpClient { pub async fn connect( transport: T, timeout: std::time::Duration, + provider: SharedProvider, ) -> Result where T: IntoTransport, @@ -163,7 +251,7 @@ impl McpClient { let notification_subscribers = Arc::new(Mutex::new(Vec::>::new())); - let client = GooseClient::new(notification_subscribers.clone()); + let client = GooseClient::new(notification_subscribers.clone(), provider); let client: rmcp::service::RunningService = client.serve(transport).await?; let server_info = client.peer_info().cloned(); @@ -247,7 +335,7 @@ impl McpClientTrait for McpClient { ClientRequest::ListResourcesRequest(ListResourcesRequest { params: Some(PaginatedRequestParam { cursor }), method: Default::default(), - extensions: Default::default(), + extensions: inject_session_into_extensions(Default::default()), }), cancel_token, ) @@ -271,7 +359,7 @@ impl McpClientTrait for McpClient { uri: uri.to_string(), }, method: Default::default(), - extensions: Default::default(), + extensions: inject_session_into_extensions(Default::default()), }), cancel_token, ) @@ -293,7 +381,7 @@ impl McpClientTrait for McpClient { ClientRequest::ListToolsRequest(ListToolsRequest { params: Some(PaginatedRequestParam { cursor }), method: Default::default(), - extensions: Default::default(), + extensions: inject_session_into_extensions(Default::default()), }), cancel_token, ) @@ -319,7 +407,7 @@ impl McpClientTrait for McpClient { arguments, }, method: Default::default(), - extensions: Default::default(), + extensions: inject_session_into_extensions(Default::default()), }), cancel_token, ) @@ -341,7 +429,7 @@ impl McpClientTrait for McpClient { ClientRequest::ListPromptsRequest(ListPromptsRequest { params: Some(PaginatedRequestParam { cursor }), method: Default::default(), - extensions: Default::default(), + extensions: inject_session_into_extensions(Default::default()), }), cancel_token, ) @@ -371,7 +459,7 @@ impl McpClientTrait for McpClient { arguments, }, method: Default::default(), - extensions: Default::default(), + extensions: inject_session_into_extensions(Default::default()), }), cancel_token, ) @@ -389,3 +477,118 @@ impl McpClientTrait for McpClient { rx } } + +/// Replaces session ID, case-insensitively, in Extensions._meta. +fn inject_session_into_extensions( + mut extensions: rmcp::model::Extensions, +) -> rmcp::model::Extensions { + use rmcp::model::Meta; + + if let Some(session_id) = crate::session_context::current_session_id() { + let mut meta_map = extensions + .get::() + .map(|meta| meta.0.clone()) + .unwrap_or_default(); + + // JsonObject is case-sensitive, so we use retain for case-insensitive removal + meta_map.retain(|k, _| !k.eq_ignore_ascii_case(SESSION_ID_HEADER)); + + meta_map.insert(SESSION_ID_HEADER.to_string(), Value::String(session_id)); + + extensions.insert(Meta(meta_map)); + } + + extensions +} + +#[cfg(test)] +mod tests { + use super::*; + use rmcp::model::Meta; + + #[tokio::test] + async fn test_session_id_in_mcp_meta() { + use serde_json::json; + + let session_id = "test-session-789"; + crate::session_context::with_session_id(Some(session_id.to_string()), async { + let extensions = inject_session_into_extensions(Default::default()); + let meta = extensions.get::().unwrap(); + + assert_eq!( + &meta.0, + json!({ + SESSION_ID_HEADER: session_id + }) + .as_object() + .unwrap() + ); + }) + .await; + } + + #[tokio::test] + async fn test_no_session_id_in_mcp_when_absent() { + let extensions = inject_session_into_extensions(Default::default()); + let meta = extensions.get::(); + + assert!(meta.is_none()); + } + + #[tokio::test] + async fn test_all_mcp_operations_include_session() { + use serde_json::json; + + let session_id = "consistent-session-id"; + crate::session_context::with_session_id(Some(session_id.to_string()), async { + let ext1 = inject_session_into_extensions(Default::default()); + let ext2 = inject_session_into_extensions(Default::default()); + let ext3 = inject_session_into_extensions(Default::default()); + + for ext in [&ext1, &ext2, &ext3] { + assert_eq!( + &ext.get::().unwrap().0, + json!({ + SESSION_ID_HEADER: session_id + }) + .as_object() + .unwrap() + ); + } + }) + .await; + } + + #[tokio::test] + async fn test_session_id_case_insensitive_replacement() { + use rmcp::model::{Extensions, Meta}; + use serde_json::{from_value, json}; + + let session_id = "new-session-id"; + crate::session_context::with_session_id(Some(session_id.to_string()), async { + let mut extensions = Extensions::new(); + extensions.insert( + from_value::(json!({ + "GOOSE-SESSION-ID": "old-session-1", + "Goose-Session-Id": "old-session-2", + "other-key": "preserve-me" + })) + .unwrap(), + ); + + let extensions = inject_session_into_extensions(extensions); + let meta = extensions.get::().unwrap(); + + assert_eq!( + &meta.0, + json!({ + SESSION_ID_HEADER: session_id, + "other-key": "preserve-me" + }) + .as_object() + .unwrap() + ); + }) + .await; + } +} diff --git a/crates/goose/src/agents/mod.rs b/crates/goose/src/agents/mod.rs index 5fa9f0ce109c..0f633307c747 100644 --- a/crates/goose/src/agents/mod.rs +++ b/crates/goose/src/agents/mod.rs @@ -26,7 +26,7 @@ mod tool_route_manager; mod tool_router_index_manager; pub mod types; -pub use agent::{Agent, AgentEvent}; +pub use agent::{Agent, AgentEvent, MANUAL_COMPACT_TRIGGER}; pub use extension::ExtensionConfig; pub use extension_manager::ExtensionManager; pub use prompt_manager::PromptManager; diff --git a/crates/goose/src/agents/platform_tools.rs b/crates/goose/src/agents/platform_tools.rs index 57bde5eaeb3e..e0877bdb717d 100644 --- a/crates/goose/src/agents/platform_tools.rs +++ b/crates/goose/src/agents/platform_tools.rs @@ -33,7 +33,6 @@ pub fn manage_schedule_tool() -> Tool { "job_id": {"type": "string", "description": "Job identifier for operations on existing jobs"}, "recipe_path": {"type": "string", "description": "Path to recipe file for create action"}, "cron_expression": {"type": "string", "description": "A cron expression for create action. Supports both 5-field (minute hour day month weekday) and 6-field (second minute hour day month weekday) formats. 5-field expressions are automatically converted to 6-field by prepending '0' for seconds."}, - "execution_mode": {"type": "string", "description": "Execution mode for create action: 'foreground' or 'background'", "enum": ["foreground", "background"], "default": "background"}, "limit": {"type": "integer", "description": "Limit for sessions list", "default": 50}, "session_id": {"type": "string", "description": "Session identifier for session_content action"} } diff --git a/crates/goose/src/agents/prompt_manager.rs b/crates/goose/src/agents/prompt_manager.rs index 133624d12ad9..af31418acdc3 100644 --- a/crates/goose/src/agents/prompt_manager.rs +++ b/crates/goose/src/agents/prompt_manager.rs @@ -3,13 +3,18 @@ use chrono::DateTime; use chrono::Utc; use serde::Serialize; use serde_json::Value; -use std::borrow::Cow; use std::collections::HashMap; use crate::agents::extension::ExtensionInfo; use crate::agents::recipe_tools::dynamic_task_tools::should_enabled_subagents; use crate::agents::router_tools::llm_search_tool_prompt; -use crate::{config::Config, prompt_template, utils::sanitize_unicode_tags}; +use crate::hints::load_hints::{load_hint_files, AGENTS_MD_FILENAME, GOOSE_HINTS_FILENAME}; +use crate::{ + config::{Config, GooseMode}, + prompt_template, + utils::sanitize_unicode_tags, +}; +use std::path::Path; const MAX_EXTENSIONS: usize = 5; const MAX_TOOLS: usize = 50; @@ -34,7 +39,7 @@ struct SystemPromptContext { current_date_time: String, #[serde(skip_serializing_if = "Option::is_none")] extension_tool_limits: Option<(usize, usize)>, - goose_mode: String, + goose_mode: GooseMode, is_autonomous: bool, enable_subagents: bool, max_extensions: usize, @@ -49,6 +54,7 @@ pub struct SystemPromptBuilder<'a, M> { frontend_instructions: Option, extension_tool_count: Option<(usize, usize)>, router_enabled: bool, + hints: Option, } impl<'a> SystemPromptBuilder<'a, PromptManager> { @@ -83,6 +89,33 @@ impl<'a> SystemPromptBuilder<'a, PromptManager> { self } + pub fn with_hints(mut self, working_dir: &Path) -> Self { + let config = Config::global(); + let hints_filenames = config + .get_param::>("CONTEXT_FILE_NAMES") + .unwrap_or_else(|_| { + vec![ + GOOSE_HINTS_FILENAME.to_string(), + AGENTS_MD_FILENAME.to_string(), + ] + }); + let ignore_patterns = { + let builder = ignore::gitignore::GitignoreBuilder::new(working_dir); + builder.build().unwrap_or_else(|_| { + ignore::gitignore::GitignoreBuilder::new(working_dir) + .build() + .expect("Failed to build default gitignore") + }) + }; + + let hints = load_hint_files(working_dir, &hints_filenames, &ignore_patterns); + + if !hints.is_empty() { + self.hints = Some(hints); + } + self + } + pub fn build(self) -> String { let mut extensions_info = self.extensions_info; @@ -106,9 +139,7 @@ impl<'a> SystemPromptBuilder<'a, PromptManager> { .collect(); let config = Config::global(); - let goose_mode = config - .get_param("GOOSE_MODE") - .unwrap_or_else(|_| Cow::from("auto")); + let goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto); let extension_tool_limits = self .extension_tool_count @@ -119,8 +150,8 @@ impl<'a> SystemPromptBuilder<'a, PromptManager> { tool_selection_strategy: self.router_enabled.then(llm_search_tool_prompt), current_date_time: self.manager.current_date_timestamp.clone(), extension_tool_limits, - goose_mode: goose_mode.to_string(), - is_autonomous: goose_mode == "auto", + goose_mode, + is_autonomous: goose_mode == GooseMode::Auto, enable_subagents: should_enabled_subagents(self.model_name.as_str()), max_extensions: MAX_EXTENSIONS, max_tools: MAX_TOOLS, @@ -137,7 +168,13 @@ impl<'a> SystemPromptBuilder<'a, PromptManager> { }); let mut system_prompt_extras = self.manager.system_prompt_extras.clone(); - if goose_mode == "chat" { + + // Add hints if provided + if let Some(hints) = self.hints { + system_prompt_extras.push(hints); + } + + if goose_mode == GooseMode::Chat { system_prompt_extras.push( "Right now you are in the chat only mode, no access to any tool use and system." .to_string(), @@ -200,6 +237,7 @@ impl PromptManager { frontend_instructions: None, extension_tool_count: None, router_enabled: false, + hints: None, } } diff --git a/crates/goose/src/agents/recipe_tools/dynamic_task_tools.rs b/crates/goose/src/agents/recipe_tools/dynamic_task_tools.rs index f87cda290337..bc61926ac38c 100644 --- a/crates/goose/src/agents/recipe_tools/dynamic_task_tools.rs +++ b/crates/goose/src/agents/recipe_tools/dynamic_task_tools.rs @@ -6,9 +6,10 @@ use crate::agents::extension::ExtensionConfig; use crate::agents::subagent_execution_tool::tasks_manager::TasksManager; use crate::agents::subagent_execution_tool::{ lib::ExecutionMode, - task_types::{Task, TaskType}, + task_types::{Task, TaskPayload}, }; use crate::agents::tool_execution::ToolCallResult; +use crate::config::GooseMode; use crate::recipe::{Recipe, RecipeBuilder}; use anyhow::{anyhow, Result}; use rmcp::model::{Content, ErrorCode, ErrorData, Tool, ToolAnnotations}; @@ -80,9 +81,6 @@ pub struct TaskParameter { #[serde(skip_serializing_if = "Option::is_none")] pub retry: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub context: Option>, - #[serde(skip_serializing_if = "Option::is_none")] pub activities: Option>, @@ -93,7 +91,7 @@ pub struct TaskParameter { pub fn should_enabled_subagents(model_name: &str) -> bool { let config = crate::config::Config::global(); - let is_autonomous = config.get_param("GOOSE_MODE").unwrap_or("auto".to_string()) == "auto"; + let is_autonomous = config.get_goose_mode().unwrap_or(GooseMode::Auto) == GooseMode::Auto; if !is_autonomous { return false; } @@ -115,7 +113,7 @@ pub fn create_dynamic_task_tool() -> Tool { Tool::new( DYNAMIC_TASK_TOOL_NAME_PREFIX.to_string(), - "Create tasks with instructions or prompt. For simple tasks, only include the instructions field. Extensions control: omit field = use all current extensions; empty array [] = no extensions; array with names = only those extensions. Specify extensions as shortnames (the prefixes for your tools). Specify return_last_only as true and have your subagent summarize its work in its last message to conserve your own context. Optional: title, description, extensions, settings, retry, response schema, context, activities. Arrays for multiple tasks.".to_string(), + "Create tasks with instructions or prompt. For simple tasks, only include the instructions field. Extensions control: omit field = use all current extensions; empty array [] = no extensions; array with names = only those extensions. Specify extensions as shortnames (the prefixes for your tools). Specify return_last_only as true and have your subagent summarize its work in its last message to conserve your own context. Optional: title, description, extensions, settings, retry, response schema, activities. Arrays for multiple tasks.".to_string(), input_schema, ).annotate(ToolAnnotations { title: Some("Create Dynamic Tasks".to_string()), @@ -227,7 +225,6 @@ pub fn task_params_to_inline_recipe( builder = apply_if_ok(builder, task_param.get("settings"), RecipeBuilder::settings); builder = apply_if_ok(builder, task_param.get("response"), RecipeBuilder::response); builder = apply_if_ok(builder, task_param.get("retry"), RecipeBuilder::retry); - builder = apply_if_ok(builder, task_param.get("context"), RecipeBuilder::context); builder = apply_if_ok( builder, task_param.get("activities"), @@ -296,17 +293,6 @@ pub async fn create_dynamic_task( // All tasks must use the new inline recipe path match task_params_to_inline_recipe(task_param, &loaded_extensions) { Ok(recipe) => { - let recipe_json = match serde_json::to_value(&recipe) { - Ok(json) => json, - Err(e) => { - return ToolCallResult::from(Err(ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to serialize recipe: {}", e)), - data: None, - })); - } - }; - // Extract return_last_only flag if present let return_last_only = task_param .get("return_last_only") @@ -315,11 +301,12 @@ pub async fn create_dynamic_task( let task = Task { id: uuid::Uuid::new_v4().to_string(), - task_type: TaskType::InlineRecipe, - payload: json!({ - "recipe": recipe_json, - "return_last_only": return_last_only - }), + payload: TaskPayload { + recipe, + return_last_only, + sequential_when_repeated: false, + parameter_values: None, + }, }; tasks.push(task); } diff --git a/crates/goose/src/agents/recipe_tools/sub_recipe_tools.rs b/crates/goose/src/agents/recipe_tools/sub_recipe_tools.rs index 8e93a658e933..b3f02ff9b400 100644 --- a/crates/goose/src/agents/recipe_tools/sub_recipe_tools.rs +++ b/crates/goose/src/agents/recipe_tools/sub_recipe_tools.rs @@ -7,8 +7,10 @@ use rmcp::model::{Tool, ToolAnnotations}; use serde_json::{json, Map, Value}; use crate::agents::subagent_execution_tool::lib::ExecutionMode; -use crate::agents::subagent_execution_tool::task_types::{Task, TaskType}; +use crate::agents::subagent_execution_tool::task_types::{Task, TaskPayload}; use crate::agents::subagent_execution_tool::tasks_manager::TasksManager; +use crate::recipe::build_recipe::build_recipe_from_template; +use crate::recipe::local_recipes::load_local_recipe_file; use crate::recipe::{Recipe, RecipeParameter, RecipeParameterRequirement, SubRecipe}; use super::param_utils::prepare_command_params; @@ -54,27 +56,37 @@ fn extract_task_parameters(params: &Value) -> Vec { fn create_tasks_from_params( sub_recipe: &SubRecipe, command_params: &[std::collections::HashMap], -) -> Vec { - let tasks: Vec = command_params - .iter() - .map(|task_command_param| { - let payload = json!({ - "sub_recipe": { - "name": sub_recipe.name.clone(), - "command_parameters": task_command_param, - "recipe_path": sub_recipe.path.clone(), - "sequential_when_repeated": sub_recipe.sequential_when_repeated - } - }); - Task { - id: uuid::Uuid::new_v4().to_string(), - task_type: TaskType::SubRecipe, - payload, - } - }) - .collect(); +) -> Result> { + let recipe_file = load_local_recipe_file(&sub_recipe.path) + .map_err(|e| anyhow::anyhow!("Failed to load recipe {}: {}", sub_recipe.path, e))?; + + let mut tasks = Vec::new(); + for task_command_param in command_params { + let recipe = build_recipe_from_template( + recipe_file.content.clone(), + &recipe_file.parent_dir, + task_command_param + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(), + None:: Result>, + ) + .map_err(|e| anyhow::anyhow!("Failed to build recipe: {}", e))?; + + let task = Task { + id: uuid::Uuid::new_v4().to_string(), + payload: TaskPayload { + recipe, + return_last_only: false, + sequential_when_repeated: sub_recipe.sequential_when_repeated, + parameter_values: Some(task_command_param.clone()), + }, + }; + + tasks.push(task); + } - tasks + Ok(tasks) } fn create_task_execution_payload(tasks: &[Task], sub_recipe: &SubRecipe) -> Value { @@ -97,7 +109,7 @@ pub async fn create_sub_recipe_task( ) -> Result { let task_params_array = extract_task_parameters(¶ms); let command_params = prepare_command_params(sub_recipe, task_params_array.clone())?; - let tasks = create_tasks_from_params(sub_recipe, &command_params); + let tasks = create_tasks_from_params(sub_recipe, &command_params)?; let task_execution_payload = create_task_execution_payload(&tasks, sub_recipe); let tasks_json = serde_json::to_string(&task_execution_payload) diff --git a/crates/goose/src/agents/reply_parts.rs b/crates/goose/src/agents/reply_parts.rs index 1d8fdaea06b4..f9755c716771 100644 --- a/crates/goose/src/agents/reply_parts.rs +++ b/crates/goose/src/agents/reply_parts.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use async_stream::try_stream; use futures::stream::StreamExt; +use serde_json::{json, Value}; use tracing::debug; use super::super::agents::Agent; @@ -19,6 +20,79 @@ use crate::agents::recipe_tools::dynamic_task_tools::should_enabled_subagents; use crate::session::SessionManager; use rmcp::model::Tool; +fn coerce_value(s: &str, schema: &Value) -> Value { + let type_str = schema.get("type"); + + match type_str { + Some(Value::String(t)) => match t.as_str() { + "number" | "integer" => try_coerce_number(s), + "boolean" => try_coerce_boolean(s), + _ => Value::String(s.to_string()), + }, + Some(Value::Array(types)) => { + // Try each type in order + for t in types { + if let Value::String(type_name) = t { + match type_name.as_str() { + "number" | "integer" if s.parse::().is_ok() => { + return try_coerce_number(s) + } + "boolean" if matches!(s.to_lowercase().as_str(), "true" | "false") => { + return try_coerce_boolean(s) + } + _ => continue, + } + } + } + Value::String(s.to_string()) + } + _ => Value::String(s.to_string()), + } +} + +fn try_coerce_number(s: &str) -> Value { + if let Ok(n) = s.parse::() { + if n.fract() == 0.0 && n >= i64::MIN as f64 && n <= i64::MAX as f64 { + json!(n as i64) + } else { + json!(n) + } + } else { + Value::String(s.to_string()) + } +} + +fn try_coerce_boolean(s: &str) -> Value { + match s.to_lowercase().as_str() { + "true" => json!(true), + "false" => json!(false), + _ => Value::String(s.to_string()), + } +} + +fn coerce_tool_arguments( + arguments: Option>, + tool_schema: &Value, +) -> Option> { + let args = arguments?; + + let properties = tool_schema.get("properties").and_then(|p| p.as_object())?; + + let mut coerced = serde_json::Map::new(); + + for (key, value) in args.iter() { + let coerced_value = + if let (Value::String(s), Some(prop_schema)) = (value, properties.get(key)) { + coerce_value(s, prop_schema) + } else { + value.clone() + }; + coerced.insert(key.clone(), coerced_value); + } + + Some(coerced) +} + async fn toolshim_postprocess( response: Message, toolshim_tools: &[Tool], @@ -33,7 +107,10 @@ async fn toolshim_postprocess( } impl Agent { - pub async fn prepare_tools_and_prompt(&self) -> Result<(Vec, Vec, String)> { + pub async fn prepare_tools_and_prompt( + &self, + working_dir: &std::path::Path, + ) -> Result<(Vec, Vec, String)> { // Get router enabled status let router_enabled = self.tool_route_manager.is_router_enabled().await; @@ -82,6 +159,7 @@ impl Agent { .with_frontend_instructions(self.frontend_instructions.lock().await.clone()) .with_extension_and_tool_counts(extension_count, tool_count) .with_router_enabled(router_enabled) + .with_hints(working_dir) .build(); // Handle toolshim if enabled @@ -190,13 +268,25 @@ impl Agent { &self, response: &Message, ) -> (Vec, Vec, Message) { - // First collect all tool requests + let tools = self.list_tools(None).await; + + // First collect all tool requests with coercion applied let tool_requests: Vec = response .content .iter() .filter_map(|content| { if let MessageContent::ToolRequest(req) = content { - Some(req.clone()) + let mut coerced_req = req.clone(); + + if let Ok(ref mut tool_call) = coerced_req.tool_call { + if let Some(tool) = tools.iter().find(|t| t.name == tool_call.name) { + let schema_value = Value::Object(tool.input_schema.as_ref().clone()); + tool_call.arguments = + coerce_tool_arguments(tool_call.arguments.clone(), &schema_value); + } + } + + Some(coerced_req) } else { None } @@ -255,6 +345,7 @@ impl Agent { pub(crate) async fn update_session_metrics( session_config: &crate::agents::types::SessionConfig, usage: &ProviderUsage, + is_compaction_usage: bool, ) -> Result<()> { let session_id = session_config.id.as_str(); let session = SessionManager::get_session(session_id, false).await?; @@ -273,11 +364,23 @@ impl Agent { let accumulated_output = accumulate(session.accumulated_output_tokens, usage.usage.output_tokens); + let (current_total, current_input, current_output) = if is_compaction_usage { + // After compaction: summary output becomes new input context + let new_input = usage.usage.output_tokens; + (new_input, new_input, None) + } else { + ( + usage.usage.total_tokens, + usage.usage.input_tokens, + usage.usage.output_tokens, + ) + }; + SessionManager::update_session(session_id) .schedule_id(session_config.schedule_id.clone()) - .total_tokens(usage.usage.total_tokens) - .input_tokens(usage.usage.input_tokens) - .output_tokens(usage.usage.output_tokens) + .total_tokens(current_total) + .input_tokens(current_input) + .output_tokens(current_output) .accumulated_total_tokens(accumulated_total) .accumulated_input_tokens(accumulated_input) .accumulated_output_tokens(accumulated_output) @@ -369,7 +472,9 @@ mod tests { .await .unwrap(); - let (tools, _toolshim_tools, _system_prompt) = agent.prepare_tools_and_prompt().await?; + let working_dir = std::env::current_dir()?; + let (tools, _toolshim_tools, _system_prompt) = + agent.prepare_tools_and_prompt(&working_dir).await?; // Ensure both platform and frontend tools are present let names: Vec = tools.iter().map(|t| t.name.clone().into_owned()).collect(); diff --git a/crates/goose/src/agents/retry.rs b/crates/goose/src/agents/retry.rs index ffa53b655075..b39dbcfd2cf6 100644 --- a/crates/goose/src/agents/retry.rs +++ b/crates/goose/src/agents/retry.rs @@ -108,18 +108,13 @@ impl RetryManager { } } - /// Handle retry logic for the agent reply loop pub async fn handle_retry_logic( &self, messages: &mut Conversation, - session: &Option, + session_config: &SessionConfig, initial_messages: &[Message], final_output_tool: &Arc>>, ) -> Result { - let Some(session_config) = session else { - return Ok(RetryResult::Skipped); - }; - let Some(retry_config) = &session_config.retry_config else { return Ok(RetryResult::Skipped); }; diff --git a/crates/goose/src/agents/schedule_tool.rs b/crates/goose/src/agents/schedule_tool.rs index 3fb98a9de564..3651c9bc7c96 100644 --- a/crates/goose/src/agents/schedule_tool.rs +++ b/crates/goose/src/agents/schedule_tool.rs @@ -186,7 +186,6 @@ impl Agent { paused: false, current_session_id: None, process_start_time: None, - execution_mode: Some(execution_mode.to_string()), }; match scheduler.add_scheduled_job(job).await { diff --git a/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__basic.snap b/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__basic.snap index 4acf9b188401..c88109a7b2e3 100644 --- a/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__basic.snap +++ b/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__basic.snap @@ -30,18 +30,6 @@ No extensions are defined. You should let the user know that they should add ext -# sub agents - -Execute self contained tasks where step-by-step visibility is not important through subagents. - -- Delegate via `dynamic_task__create_task` for: result-only operations, parallelizable work, multi-part requests, - verification, exploration -- Parallel subagents for multiple operations, single subagents for independent work -- Explore solutions in parallel — launch parallel subagents with different approaches (if non-interfering) -- Provide all needed context — subagents cannot see your context -- Use extension filters to limit resource access -- Use return_last_only when only a summary or simple answer is required — inform subagent of this choice. - # Response Guidelines - Use Markdown formatting for all responses. diff --git a/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__one_extension.snap b/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__one_extension.snap index bb5479e3e553..18656ece918c 100644 --- a/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__one_extension.snap +++ b/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__one_extension.snap @@ -54,18 +54,6 @@ how to use this extension - list_resources -# sub agents - -Execute self contained tasks where step-by-step visibility is not important through subagents. - -- Delegate via `dynamic_task__create_task` for: result-only operations, parallelizable work, multi-part requests, - verification, exploration -- Parallel subagents for multiple operations, single subagents for independent work -- Explore solutions in parallel — launch parallel subagents with different approaches (if non-interfering) -- Provide all needed context — subagents cannot see your context -- Use extension filters to limit resource access -- Use return_last_only when only a summary or simple answer is required — inform subagent of this choice. - # Response Guidelines - Use Markdown formatting for all responses. diff --git a/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__typical_setup.snap b/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__typical_setup.snap index c28476fe5c38..1c7af49c5634 100644 --- a/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__typical_setup.snap +++ b/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__typical_setup.snap @@ -68,18 +68,6 @@ Explain that minimizing extensions helps with the recall of the correct tools to - list_resources -# sub agents - -Execute self contained tasks where step-by-step visibility is not important through subagents. - -- Delegate via `dynamic_task__create_task` for: result-only operations, parallelizable work, multi-part requests, - verification, exploration -- Parallel subagents for multiple operations, single subagents for independent work -- Explore solutions in parallel — launch parallel subagents with different approaches (if non-interfering) -- Provide all needed context — subagents cannot see your context -- Use extension filters to limit resource access -- Use return_last_only when only a summary or simple answer is required — inform subagent of this choice. - # Response Guidelines - Use Markdown formatting for all responses. diff --git a/crates/goose/src/agents/subagent_execution_tool/lib/mod.rs b/crates/goose/src/agents/subagent_execution_tool/lib/mod.rs index 48947c1438a8..6588dfab1073 100644 --- a/crates/goose/src/agents/subagent_execution_tool/lib/mod.rs +++ b/crates/goose/src/agents/subagent_execution_tool/lib/mod.rs @@ -33,7 +33,11 @@ pub async fn execute_tasks( } } ExecutionMode::Parallel => { - if tasks.iter().any(|task| task.get_sequential_when_repeated()) { + let any_sequential = tasks + .iter() + .any(|task| task.payload.sequential_when_repeated); + + if any_sequential { Ok(json!( { "execution_mode": ExecutionMode::Sequential, diff --git a/crates/goose/src/agents/subagent_execution_tool/task_execution_tracker.rs b/crates/goose/src/agents/subagent_execution_tool/task_execution_tracker.rs index 8833c21e2f48..14ccf586a879 100644 --- a/crates/goose/src/agents/subagent_execution_tool/task_execution_tracker.rs +++ b/crates/goose/src/agents/subagent_execution_tool/task_execution_tracker.rs @@ -15,9 +15,10 @@ use crate::agents::subagent_execution_tool::notification_events::{ use crate::agents::subagent_execution_tool::task_types::{Task, TaskInfo, TaskResult, TaskStatus}; use crate::agents::subagent_execution_tool::utils::{count_by_status, get_task_name}; use crate::utils::is_token_cancelled; -use serde_json::Value; use tokio::sync::mpsc::Sender; +const RECIPE_TASK_TYPE: &str = "recipe"; + #[derive(Debug, Clone, PartialEq)] pub enum DisplayMode { MultipleTasksOutput, @@ -28,25 +29,22 @@ const THROTTLE_INTERVAL_MS: u64 = 250; const COMPLETION_NOTIFICATION_DELAY_MS: u64 = 500; fn format_task_metadata(task_info: &TaskInfo) -> String { - if let Some(params) = task_info.task.get_command_parameters() { - if params.is_empty() { - return String::new(); + // If we have parameter values, format them nicely + if let Some(ref params) = task_info.task.payload.parameter_values { + if !params.is_empty() { + let mut param_strs: Vec = params + .iter() + .filter(|(k, _)| k.as_str() != "recipe_dir") + .map(|(k, v)| format!("{}={}", k, v)) + .collect(); + if !param_strs.is_empty() { + param_strs.sort(); + return param_strs.join(", "); + } } - - params - .iter() - .map(|(key, value)| { - let value_str = match value { - Value::String(s) => s.clone(), - _ => value.to_string(), - }; - format!("{}={}", key, value_str) - }) - .collect::>() - .join(",") - } else { - String::new() } + // Fallback to recipe title if no parameters + task_info.task.payload.recipe.title.clone() } pub struct TaskExecutionTracker { @@ -151,13 +149,15 @@ impl TaskExecutionTracker { async fn format_line(&self, task_info: Option<&TaskInfo>, line: &str) -> String { if let Some(task_info) = task_info { let task_name = get_task_name(task_info); - let task_type = task_info.task.task_type.clone(); let metadata = format_task_metadata(task_info); if metadata.is_empty() { - format!("[{} ({})] {}", task_name, task_type, line) + format!("[{} ({})] {}", task_name, RECIPE_TASK_TYPE, line) } else { - format!("[{} ({}) {}] {}", task_name, task_type, metadata, line) + format!( + "[{} ({}) {}] {}", + task_name, RECIPE_TASK_TYPE, metadata, line + ) } } else { line.to_string() @@ -232,7 +232,7 @@ impl TaskExecutionTracker { } }), current_output: task_info.current_output.clone(), - task_type: task_info.task.task_type.to_string(), + task_type: RECIPE_TASK_TYPE.to_string(), task_name: get_task_name(task_info).to_string(), task_metadata: format_task_metadata(task_info), error: task_info.error().cloned(), diff --git a/crates/goose/src/agents/subagent_execution_tool/task_types.rs b/crates/goose/src/agents/subagent_execution_tool/task_types.rs index 3defebd02301..91d1ec6d3a06 100644 --- a/crates/goose/src/agents/subagent_execution_tool/task_types.rs +++ b/crates/goose/src/agents/subagent_execution_tool/task_types.rs @@ -1,12 +1,13 @@ use serde::{Deserialize, Serialize}; -use serde_json::{Map, Value}; -use std::fmt; +use serde_json::Value; +use std::collections::HashMap; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use tokio::sync::mpsc; use tokio_util::sync::CancellationToken; use crate::agents::subagent_execution_tool::task_execution_tracker::TaskExecutionTracker; +use crate::recipe::Recipe; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] #[serde(rename_all = "lowercase")] @@ -16,59 +17,19 @@ pub enum ExecutionMode { Parallel, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "snake_case")] -pub enum TaskType { - InlineRecipe, - SubRecipe, -} - -impl fmt::Display for TaskType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - TaskType::InlineRecipe => write!(f, "inline_recipe"), - TaskType::SubRecipe => write!(f, "sub_recipe"), - } - } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TaskPayload { + pub recipe: Recipe, + pub return_last_only: bool, + pub sequential_when_repeated: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub parameter_values: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Task { pub id: String, - pub task_type: TaskType, - pub payload: Value, -} - -impl Task { - pub fn get_sub_recipe(&self) -> Option<&Map> { - matches!(self.task_type, TaskType::SubRecipe) - .then(|| self.payload.get("sub_recipe")?.as_object()) - .flatten() - } - - pub fn get_command_parameters(&self) -> Option<&Map> { - self.get_sub_recipe() - .and_then(|sr| sr.get("command_parameters")) - .and_then(|cp| cp.as_object()) - } - - pub fn get_sequential_when_repeated(&self) -> bool { - self.get_sub_recipe() - .and_then(|sr| sr.get("sequential_when_repeated").and_then(|v| v.as_bool())) - .unwrap_or_default() - } - - pub fn get_sub_recipe_name(&self) -> Option<&str> { - self.get_sub_recipe() - .and_then(|sr| sr.get("name")) - .and_then(|name| name.as_str()) - } - - pub fn get_sub_recipe_path(&self) -> Option<&str> { - self.get_sub_recipe() - .and_then(|sr| sr.get("recipe_path")) - .and_then(|path| path.as_str()) - } + pub payload: TaskPayload, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/goose/src/agents/subagent_execution_tool/tasks.rs b/crates/goose/src/agents/subagent_execution_tool/tasks.rs index e978030231f0..ceec8f474f77 100644 --- a/crates/goose/src/agents/subagent_execution_tool/tasks.rs +++ b/crates/goose/src/agents/subagent_execution_tool/tasks.rs @@ -1,29 +1,18 @@ use serde_json::Value; -use std::process::Stdio; use std::sync::Arc; -use tokio::io::{AsyncBufReadExt, BufReader}; -use tokio::process::Command; use tokio_util::sync::CancellationToken; use crate::agents::subagent_execution_tool::task_execution_tracker::TaskExecutionTracker; -use crate::agents::subagent_execution_tool::task_types::{Task, TaskResult, TaskStatus, TaskType}; -use crate::agents::subagent_execution_tool::utils::strip_ansi_codes; +use crate::agents::subagent_execution_tool::task_types::{Task, TaskResult, TaskStatus}; use crate::agents::subagent_task_config::TaskConfig; pub async fn process_task( task: &Task, - task_execution_tracker: Arc, + _task_execution_tracker: Arc, task_config: TaskConfig, cancellation_token: CancellationToken, ) -> TaskResult { - match get_task_result( - task.clone(), - task_execution_tracker, - task_config, - cancellation_token, - ) - .await - { + match handle_recipe_task(task.clone(), task_config, cancellation_token).await { Ok(data) => TaskResult { task_id: task.id.clone(), status: TaskStatus::Completed, @@ -39,234 +28,67 @@ pub async fn process_task( } } -async fn get_task_result( - task: Task, - task_execution_tracker: Arc, - task_config: TaskConfig, - cancellation_token: CancellationToken, -) -> Result { - match task.task_type { - TaskType::InlineRecipe => { - handle_inline_recipe_task(task, task_config, cancellation_token).await - } - TaskType::SubRecipe => { - let (command, output_identifier) = build_command(&task)?; - let (stdout_output, stderr_output, success) = run_command( - command, - &output_identifier, - &task.id, - task_execution_tracker, - cancellation_token, - ) - .await?; - - if success { - process_output(stdout_output) - } else { - Err(format!("Command failed:\n{}", &stderr_output)) - } - } - } -} - -async fn handle_inline_recipe_task( +async fn handle_recipe_task( task: Task, mut task_config: TaskConfig, cancellation_token: CancellationToken, ) -> Result { use crate::agents::subagent_handler::run_complete_subagent_task; - use crate::recipe::Recipe; + use crate::model::ModelConfig; + use crate::providers; - let recipe_value = task - .payload - .get("recipe") - .ok_or_else(|| "Missing recipe in inline_recipe task payload".to_string())?; + let recipe = task.payload.recipe; + let return_last_only = task.payload.return_last_only; - let recipe: Recipe = serde_json::from_value(recipe_value.clone()) - .map_err(|e| format!("Invalid recipe in payload: {}", e))?; - - let return_last_only = task - .payload - .get("return_last_only") - .and_then(|v| v.as_bool()) - .unwrap_or(false); - - // If extensions are explicitly provided in the recipe (even if empty), - // override the task_config extensions. Empty array means no extensions. - if let Some(exts) = recipe.extensions { + if let Some(ref exts) = recipe.extensions { task_config.extensions = exts.clone(); } - let instruction = recipe - .instructions - .or(recipe.prompt) - .ok_or_else(|| "No instructions or prompt in recipe".to_string())?; - - let result = tokio::select! { - result = run_complete_subagent_task( - instruction, - task_config, - return_last_only, - ) => result, - _ = cancellation_token.cancelled() => { - return Err("Task cancelled".to_string()); - } - }; + if let Some(ref settings) = recipe.settings { + let new_provider = match ( + &settings.goose_provider, + &settings.goose_model, + settings.temperature, + ) { + (Some(provider), Some(model), temp) => { + let config = ModelConfig::new_or_fail(model).with_temperature(temp); + Some((provider.clone(), config)) + } + (Some(_), None, _) => { + return Err("Recipe specifies provider but no model".to_string()); + } + (None, model_or_temp, _) + if model_or_temp.is_some() || settings.temperature.is_some() => + { + let provider_name = task_config.provider.get_name().to_string(); + let mut config = task_config.provider.get_model_config(); + + if let Some(model) = &settings.goose_model { + config.model_name = model.clone(); + } + if let Some(temp) = settings.temperature { + config = config.with_temperature(Some(temp)); + } + + Some((provider_name, config)) + } + _ => None, + }; - match result { - Ok(result_text) => Ok(serde_json::json!({ - "result": result_text - })), - Err(e) => { - let error_msg = format!("Inline recipe execution failed: {}", e); - Err(error_msg) + if let Some((provider_name, model_config)) = new_provider { + task_config.provider = providers::create(&provider_name, model_config) + .await + .map_err(|e| format!("Failed to create provider '{}': {}", provider_name, e))?; } } -} - -fn build_command(task: &Task) -> Result<(Command, String), String> { - let task_error = |field: &str| format!("Task {}: Missing {}", task.id, field); - - if !matches!(task.task_type, TaskType::SubRecipe) { - return Err("Only sub-recipe tasks can be executed as commands".to_string()); - } - - let sub_recipe_name = task - .get_sub_recipe_name() - .ok_or_else(|| task_error("sub_recipe name"))?; - let path = task - .get_sub_recipe_path() - .ok_or_else(|| task_error("sub_recipe path"))?; - let command_parameters = task - .get_command_parameters() - .ok_or_else(|| task_error("command_parameters"))?; - - let mut command = Command::new("goose"); - command - .arg("run") - .arg("--recipe") - .arg(path) - .arg("--no-session"); - - for (key, value) in command_parameters { - let key_str = key.to_string(); - let value_str = value.as_str().unwrap_or(&value.to_string()).to_string(); - command - .arg("--params") - .arg(format!("{}={}", key_str, value_str)); - } - command.stdout(Stdio::piped()); - command.stderr(Stdio::piped()); - - Ok((command, format!("sub-recipe {}", sub_recipe_name))) -} - -async fn run_command( - mut command: Command, - output_identifier: &str, - task_id: &str, - task_execution_tracker: Arc, - cancellation_token: CancellationToken, -) -> Result<(String, String, bool), String> { - let mut child = command - .spawn() - .map_err(|e| format!("Failed to spawn goose: {}", e))?; - - let stdout = child.stdout.take().expect("Failed to capture stdout"); - let stderr = child.stderr.take().expect("Failed to capture stderr"); - - let stdout_task = spawn_output_reader( - stdout, - output_identifier, - false, - task_id, - task_execution_tracker.clone(), - ); - let stderr_task = spawn_output_reader( - stderr, - output_identifier, - true, - task_id, - task_execution_tracker.clone(), - ); - - let result = tokio::select! { - _ = cancellation_token.cancelled() => { - if let Err(e) = child.kill().await { - tracing::warn!("Failed to kill child process: {}", e); - } - - stdout_task.abort(); - stderr_task.abort(); - return Err("Command cancelled".to_string()); - } - status_result = child.wait() => { - status_result.map_err(|e| format!("Failed to wait for process: {}", e))? + tokio::select! { + result = run_complete_subagent_task(recipe, task_config, return_last_only) => { + result.map(|text| serde_json::json!({"result": text})) + .map_err(|e| format!("Recipe execution failed: {}", e)) } - }; - - let stdout_output = stdout_task.await.unwrap(); - let stderr_output = stderr_task.await.unwrap(); - - Ok((stdout_output, stderr_output, result.success())) -} - -fn spawn_output_reader( - reader: impl tokio::io::AsyncRead + Unpin + Send + 'static, - output_identifier: &str, - is_stderr: bool, - task_id: &str, - task_execution_tracker: Arc, -) -> tokio::task::JoinHandle { - let output_identifier = output_identifier.to_string(); - let task_id = task_id.to_string(); - tokio::spawn(async move { - let mut buffer = String::new(); - let mut lines = BufReader::new(reader).lines(); - while let Ok(Some(line)) = lines.next_line().await { - let line = strip_ansi_codes(&line); - buffer.push_str(&line); - buffer.push('\n'); - - if !is_stderr { - task_execution_tracker - .send_live_output(&task_id, &line) - .await; - } else { - tracing::warn!("Task stderr [{}]: {}", output_identifier, line); - } + _ = cancellation_token.cancelled() => { + Err("Task cancelled".to_string()) } - buffer - }) -} - -fn extract_json_from_line(line: &str) -> Option { - let start = line.find('{')?; - let end = line.rfind('}')?; - - if start >= end { - return None; - } - - let potential_json = &line[start..=end]; - if serde_json::from_str::(potential_json).is_ok() { - Some(potential_json.to_string()) - } else { - None - } -} - -fn process_output(stdout_output: String) -> Result { - let last_line = stdout_output - .lines() - .filter(|line| !line.trim().is_empty()) - .next_back() - .unwrap_or(""); - - if let Some(json_string) = extract_json_from_line(last_line) { - Ok(Value::String(json_string)) - } else { - Ok(Value::String(stdout_output)) } } diff --git a/crates/goose/src/agents/subagent_execution_tool/tasks_manager.rs b/crates/goose/src/agents/subagent_execution_tool/tasks_manager.rs index b416518a9cbc..bec13114449d 100644 --- a/crates/goose/src/agents/subagent_execution_tool/tasks_manager.rs +++ b/crates/goose/src/agents/subagent_execution_tool/tasks_manager.rs @@ -4,8 +4,6 @@ use std::sync::Arc; use tokio::sync::RwLock; use crate::agents::subagent_execution_tool::task_types::Task; -#[cfg(test)] -use crate::agents::subagent_execution_tool::task_types::TaskType; #[derive(Debug, Clone)] pub struct TasksManager { @@ -57,19 +55,26 @@ impl TasksManager { #[cfg(test)] mod tests { use super::*; - use serde_json::json; + use crate::agents::subagent_execution_tool::task_types::TaskPayload; + use crate::recipe::Recipe; fn create_test_task(id: &str, sub_recipe_name: &str) -> Task { + let recipe = Recipe::builder() + .version("1.0.0") + .title(sub_recipe_name) + .description("Test recipe") + .instructions("Test instructions") + .build() + .unwrap(); + Task { id: id.to_string(), - task_type: TaskType::SubRecipe, - payload: json!({ - "sub_recipe": { - "name": sub_recipe_name, - "command_parameters": {}, - "recipe_path": "/test/path" - } - }), + payload: TaskPayload { + recipe, + return_last_only: false, + sequential_when_repeated: false, + parameter_values: None, + }, } } diff --git a/crates/goose/src/agents/subagent_execution_tool/utils/mod.rs b/crates/goose/src/agents/subagent_execution_tool/utils/mod.rs index e1e48835c7c3..58de4c27d288 100644 --- a/crates/goose/src/agents/subagent_execution_tool/utils/mod.rs +++ b/crates/goose/src/agents/subagent_execution_tool/utils/mod.rs @@ -3,10 +3,7 @@ use std::collections::HashMap; use crate::agents::subagent_execution_tool::task_types::{TaskInfo, TaskStatus}; pub fn get_task_name(task_info: &TaskInfo) -> &str { - task_info - .task - .get_sub_recipe_name() - .unwrap_or(&task_info.task.id) + &task_info.task.payload.recipe.title } pub fn count_by_status(tasks: &HashMap) -> (usize, usize, usize, usize, usize) { diff --git a/crates/goose/src/agents/subagent_execution_tool/utils/tests.rs b/crates/goose/src/agents/subagent_execution_tool/utils/tests.rs index 3e5c5cd291a2..1ef64d8b2d6a 100644 --- a/crates/goose/src/agents/subagent_execution_tool/utils/tests.rs +++ b/crates/goose/src/agents/subagent_execution_tool/utils/tests.rs @@ -1,8 +1,8 @@ -use crate::agents::subagent_execution_tool::task_types::{Task, TaskInfo, TaskStatus, TaskType}; +use crate::agents::subagent_execution_tool::task_types::{Task, TaskInfo, TaskPayload, TaskStatus}; use crate::agents::subagent_execution_tool::utils::{ count_by_status, get_task_name, strip_ansi_codes, }; -use serde_json::json; +use crate::recipe::Recipe; use std::collections::HashMap; fn create_task_info_with_defaults(task: Task, status: TaskStatus) -> TaskInfo { @@ -20,76 +20,51 @@ mod test_get_task_name { use super::*; #[test] - fn test_extracts_sub_recipe_name() { - let sub_recipe_task = Task { + fn test_extracts_recipe_title() { + let recipe = Recipe::builder() + .version("1.0.0") + .title("my_recipe") + .description("Test") + .instructions("do something") + .build() + .unwrap(); + + let task = Task { id: "task_1".to_string(), - task_type: TaskType::SubRecipe, - payload: json!({ - "sub_recipe": { - "name": "my_recipe", - "recipe_path": "/path/to/recipe" - } - }), + payload: TaskPayload { + recipe, + return_last_only: false, + sequential_when_repeated: false, + parameter_values: None, + }, }; - let task_info = create_task_info_with_defaults(sub_recipe_task, TaskStatus::Pending); + let task_info = create_task_info_with_defaults(task, TaskStatus::Pending); assert_eq!(get_task_name(&task_info), "my_recipe"); } - - #[test] - fn falls_back_to_task_id_for_inline_recipe() { - let inline_task = Task { - id: "task_2".to_string(), - task_type: TaskType::InlineRecipe, - payload: json!({"recipe": {"instructions": "do something"}}), - }; - - let task_info = create_task_info_with_defaults(inline_task, TaskStatus::Pending); - - assert_eq!(get_task_name(&task_info), "task_2"); - } - - #[test] - fn falls_back_to_task_id_when_sub_recipe_name_missing() { - let malformed_task = Task { - id: "task_3".to_string(), - task_type: TaskType::SubRecipe, - payload: json!({ - "sub_recipe": { - "recipe_path": "/path/to/recipe" - // missing "name" field - } - }), - }; - - let task_info = create_task_info_with_defaults(malformed_task, TaskStatus::Pending); - - assert_eq!(get_task_name(&task_info), "task_3"); - } - - #[test] - fn falls_back_to_task_id_when_sub_recipe_missing() { - let malformed_task = Task { - id: "task_4".to_string(), - task_type: TaskType::SubRecipe, - payload: json!({}), // missing "sub_recipe" field - }; - - let task_info = create_task_info_with_defaults(malformed_task, TaskStatus::Pending); - - assert_eq!(get_task_name(&task_info), "task_4"); - } } mod count_by_status { use super::*; fn create_test_task(id: &str, status: TaskStatus) -> TaskInfo { + let recipe = Recipe::builder() + .version("1.0.0") + .title("Test Recipe") + .description("Test") + .instructions("Test") + .build() + .unwrap(); + let task = Task { id: id.to_string(), - task_type: TaskType::InlineRecipe, - payload: json!({}), + payload: TaskPayload { + recipe, + return_last_only: false, + sequential_when_repeated: false, + parameter_values: None, + }, }; create_task_info_with_defaults(task, status) } diff --git a/crates/goose/src/agents/subagent_handler.rs b/crates/goose/src/agents/subagent_handler.rs index 5359573fe248..561ba7962722 100644 --- a/crates/goose/src/agents/subagent_handler.rs +++ b/crates/goose/src/agents/subagent_handler.rs @@ -1,38 +1,40 @@ +use crate::session::session_manager::SessionType; use crate::{ - agents::{ - extension::PlatformExtensionContext, subagent_task_config::TaskConfig, Agent, AgentEvent, - SessionConfig, - }, + agents::{subagent_task_config::TaskConfig, AgentEvent, SessionConfig}, conversation::{message::Message, Conversation}, execution::manager::AgentManager, + recipe::Recipe, session::SessionManager, }; use anyhow::{anyhow, Result}; use futures::StreamExt; use rmcp::model::{ErrorCode, ErrorData}; +use std::future::Future; use std::pin::Pin; -use std::{future::Future, sync::Arc}; -use tracing::debug; +use tracing::{debug, info}; + +type AgentMessagesFuture = + Pin)>> + Send>>; /// Standalone function to run a complete subagent task with output options pub async fn run_complete_subagent_task( - text_instruction: String, + recipe: Recipe, task_config: TaskConfig, return_last_only: bool, ) -> Result { - let messages = get_agent_messages(text_instruction, task_config) - .await - .map_err(|e| { - ErrorData::new( - ErrorCode::INTERNAL_ERROR, - format!("Failed to execute task: {}", e), - None, - ) - })?; - - // Extract text content based on return_last_only flag + let (messages, final_output) = get_agent_messages(recipe, task_config).await.map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to execute task: {}", e), + None, + ) + })?; + + if let Some(output) = final_output { + return Ok(output); + } + let response_text = if return_last_only { - // Get only the last message's text content messages .messages() .last() @@ -46,7 +48,6 @@ pub async fn run_complete_subagent_task( }) .unwrap_or_else(|| String::from("No text content in last message")) } else { - // Extract all text content from all messages (original behavior) let all_text_content: Vec = messages .iter() .flat_map(|message| { @@ -90,48 +91,34 @@ pub async fn run_complete_subagent_task( all_text_content.join("\n") }; - // Return the result Ok(response_text) } -fn get_agent_messages( - text_instruction: String, - task_config: TaskConfig, -) -> Pin> + Send>> { +fn get_agent_messages(recipe: Recipe, task_config: TaskConfig) -> AgentMessagesFuture { Box::pin(async move { + let text_instruction = recipe + .instructions + .clone() + .or(recipe.prompt.clone()) + .ok_or_else(|| anyhow!("Recipe has no instructions or prompt"))?; + let agent_manager = AgentManager::instance() .await .map_err(|e| anyhow!("Failed to create AgentManager: {}", e))?; let parent_session_id = task_config.parent_session_id; let working_dir = task_config.parent_working_dir; - let (agent, session_id) = match parent_session_id { - Some(parent_session_id) => { - let session = SessionManager::create_session( - working_dir.clone(), - format!("Subagent task for: {}", parent_session_id), - ) - .await - .map_err(|e| anyhow!("Failed to create a session for sub agent: {}", e))?; + let session = SessionManager::create_session( + working_dir.clone(), + format!("Subagent task for: {}", parent_session_id), + SessionType::SubAgent, + ) + .await + .map_err(|e| anyhow!("Failed to create a session for sub agent: {}", e))?; - let agent = agent_manager - .get_or_create_agent(session.id.clone()) - .await - .map_err(|e| anyhow!("Failed to get sub agent session file path: {}", e))?; - (agent, Some(session.id)) - } - None => { - let agent = Arc::new(Agent::new()); - agent - .extension_manager - .set_context(PlatformExtensionContext { - session_id: None, - extension_manager: Some(Arc::downgrade(&agent.extension_manager)), - tool_route_manager: Some(Arc::downgrade(&agent.tool_route_manager)), - }) - .await; - (agent, None) - } - }; + let agent = agent_manager + .get_or_create_agent(session.id.clone()) + .await + .map_err(|e| anyhow!("Failed to get sub agent session file path: {}", e))?; agent .update_provider(task_config.provider) @@ -148,26 +135,31 @@ fn get_agent_messages( } } - let mut conversation = - Conversation::new_unvalidated( - vec![Message::user().with_text(text_instruction.clone())], - ); - let session_config = if let Some(session_id) = session_id { - Some(SessionConfig { - id: session_id, - working_dir, - schedule_id: None, - execution_mode: None, - max_turns: task_config.max_turns.map(|v| v as u32), - retry_config: None, - }) - } else { - None + let has_response_schema = recipe.response.is_some(); + agent + .apply_recipe_components(recipe.sub_recipes.clone(), recipe.response.clone(), true) + .await; + + let user_message = Message::user().with_text(text_instruction); + let mut conversation = Conversation::new_unvalidated(vec![user_message.clone()]); + + if let Some(activities) = recipe.activities { + for activity in activities { + info!("Recipe activity: {}", activity); + } + } + let session_config = SessionConfig { + id: session.id.clone(), + schedule_id: None, + max_turns: task_config.max_turns.map(|v| v as u32), + retry_config: recipe.retry, }; - let mut stream = agent - .reply(conversation.clone(), session_config, None) - .await - .map_err(|e| anyhow!("Failed to get reply from agent: {}", e))?; + + let mut stream = crate::session_context::with_session_id(Some(session.id.clone()), async { + agent.reply(user_message, session_config, None).await + }) + .await + .map_err(|e| anyhow!("Failed to get reply from agent: {}", e))?; while let Some(message_result) = stream.next().await { match message_result { Ok(AgentEvent::Message(msg)) => conversation.push(msg), @@ -182,6 +174,17 @@ fn get_agent_messages( } } - Ok(conversation) + let final_output = if has_response_schema { + agent + .final_output_tool + .lock() + .await + .as_ref() + .and_then(|tool| tool.final_output.clone()) + } else { + None + }; + + Ok((conversation, final_output)) }) } diff --git a/crates/goose/src/agents/subagent_task_config.rs b/crates/goose/src/agents/subagent_task_config.rs index d3375c92be30..01c955d0c01c 100644 --- a/crates/goose/src/agents/subagent_task_config.rs +++ b/crates/goose/src/agents/subagent_task_config.rs @@ -2,7 +2,7 @@ use crate::agents::ExtensionConfig; use crate::providers::base::Provider; use std::env; use std::fmt; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; /// Default maximum number of turns for task execution @@ -15,7 +15,7 @@ pub const GOOSE_SUBAGENT_MAX_TURNS_ENV_VAR: &str = "GOOSE_SUBAGENT_MAX_TURNS"; #[derive(Clone)] pub struct TaskConfig { pub provider: Arc, - pub parent_session_id: Option, + pub parent_session_id: String, pub parent_working_dir: PathBuf, pub extensions: Vec, pub max_turns: Option, @@ -34,17 +34,16 @@ impl fmt::Debug for TaskConfig { } impl TaskConfig { - /// Create a new TaskConfig with all required dependencies pub fn new( provider: Arc, - parent_session_id: Option, - parent_working_dir: PathBuf, + parent_session_id: &str, + parent_working_dir: &Path, extensions: Vec, ) -> Self { Self { provider, - parent_session_id, - parent_working_dir, + parent_session_id: parent_session_id.to_owned(), + parent_working_dir: parent_working_dir.to_owned(), extensions, max_turns: Some( env::var(GOOSE_SUBAGENT_MAX_TURNS_ENV_VAR) diff --git a/crates/goose/src/agents/tool_execution.rs b/crates/goose/src/agents/tool_execution.rs index 64ab6af3d997..87c1986a7f36 100644 --- a/crates/goose/src/agents/tool_execution.rs +++ b/crates/goose/src/agents/tool_execution.rs @@ -29,8 +29,9 @@ impl From>> for ToolCallResult { } use super::agent::{tool_stream, ToolStream}; -use crate::agents::{Agent, SessionConfig}; +use crate::agents::Agent; use crate::conversation::message::{Message, ToolRequest}; +use crate::session::Session; use crate::tool_inspection::get_security_finding_id_from_results; pub const DECLINED_RESPONSE: &str = "The user has declined to run this tool. \ @@ -53,7 +54,7 @@ impl Agent { tool_futures: Arc>>, message_tool_response: Arc>, cancellation_token: Option, - session: Option, + session: &'a Session, inspection_results: &'a [crate::tool_inspection::InspectionResult], ) -> BoxStream<'a, anyhow::Result> { try_stream! { @@ -93,7 +94,7 @@ impl Agent { } if confirmation.permission == Permission::AllowOnce || confirmation.permission == Permission::AlwaysAllow { - let (req_id, tool_result) = self.dispatch_tool_call(tool_call.clone(), request.id.clone(), cancellation_token.clone(), session.clone()).await; + let (req_id, tool_result) = self.dispatch_tool_call(tool_call.clone(), request.id.clone(), cancellation_token.clone(), session).await; let mut futures = tool_futures.lock().await; futures.push((req_id, match tool_result { diff --git a/crates/goose/src/agents/types.rs b/crates/goose/src/agents/types.rs index 0518c65789b3..0f2902bf9e15 100644 --- a/crates/goose/src/agents/types.rs +++ b/crates/goose/src/agents/types.rs @@ -1,7 +1,7 @@ use crate::mcp_utils::ToolResult; +use crate::providers::base::Provider; use rmcp::model::{Content, Tool}; use serde::{Deserialize, Serialize}; -use std::path::PathBuf; use std::sync::Arc; use tokio::sync::{mpsc, Mutex}; use utoipa::ToSchema; @@ -9,6 +9,9 @@ use utoipa::ToSchema; /// Type alias for the tool result channel receiver pub type ToolResultReceiver = Arc>)>>>; +// We use double Arc here to allow easy provider swaps while sharing concurrent access +pub type SharedProvider = Arc>>>; + /// Default timeout for retry operations (5 minutes) pub const DEFAULT_RETRY_TIMEOUT_SECONDS: u64 = 300; @@ -80,14 +83,10 @@ pub struct FrontendTool { /// Session configuration for an agent #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SessionConfig { - /// Unique identifier for the session + /// Identifier of the underlying Session pub id: String, - /// Working directory for the session - pub working_dir: PathBuf, /// ID of the schedule that triggered this session, if any pub schedule_id: Option, - /// Execution mode for scheduled jobs: "foreground" or "background" - pub execution_mode: Option, /// Maximum number of turns (iterations) allowed without user input pub max_turns: Option, /// Retry configuration for automated validation and recovery diff --git a/crates/goose/src/config/base.rs b/crates/goose/src/config/base.rs index 3cde60d3b423..0ad26428d39b 100644 --- a/crates/goose/src/config/base.rs +++ b/crates/goose/src/config/base.rs @@ -1,9 +1,11 @@ use crate::config::paths::Paths; +use crate::config::GooseMode; use fs2::FileExt; use keyring::Entry; use once_cell::sync::OnceCell; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use serde_json::Value; +use serde_yaml::Mapping; use std::collections::HashMap; use std::env; use std::fs::OpenOptions; @@ -136,6 +138,25 @@ impl Default for Config { } } +macro_rules! declare_param { + ($param_name:ident, $param_type:ty) => { + paste::paste! { + pub fn [](&self) -> Result<$param_type, ConfigError> { + self.get_param(stringify!($param_name)) + } + } + paste::paste! { + pub fn [](&self, v: impl Into<$param_type>) -> Result<(), ConfigError> { + self.set_param(stringify!($param_name), &v.into()) + } + } + }; +} + +fn parse_yaml_content(content: &str) -> Result { + serde_yaml::from_str(content).map_err(|e| e.into()) +} + impl Config { /// Get the global configuration instance. /// @@ -188,8 +209,7 @@ impl Config { self.config_path.to_string_lossy().to_string() } - // Load current values from the config file - pub fn load_values(&self) -> Result, ConfigError> { + fn load(&self) -> Result { if self.config_path.exists() { self.load_values_with_recovery() } else { @@ -205,19 +225,27 @@ impl Config { tracing::info!("No backup found, creating default configuration"); // Try to load from init-config.yaml if it exists, otherwise use empty config - let default_config = self - .load_init_config_if_exists() - .unwrap_or_else(|_| HashMap::new()); + let default_config = self.load_init_config_if_exists().unwrap_or_default(); self.create_and_save_default_config(default_config) } } + pub fn all_values(&self) -> Result, ConfigError> { + self.load().map(|m| { + HashMap::from_iter(m.into_iter().filter_map(|(k, v)| { + k.as_str() + .map(|k| k.to_string()) + .zip(serde_json::to_value(v).ok()) + })) + }) + } + // Helper method to create and save default config with consistent logging fn create_and_save_default_config( &self, - default_config: HashMap, - ) -> Result, ConfigError> { + default_config: Mapping, + ) -> Result { // Try to write the default config to disk match self.save_values(default_config.clone()) { Ok(_) => { @@ -239,12 +267,10 @@ impl Config { } } - // Load values with automatic recovery from corruption - fn load_values_with_recovery(&self) -> Result, ConfigError> { + fn load_values_with_recovery(&self) -> Result { let file_content = std::fs::read_to_string(&self.config_path)?; - // First attempt: try to parse the current config - match self.parse_yaml_content(&file_content) { + match parse_yaml_content(&file_content) { Ok(values) => Ok(values), Err(parse_error) => { tracing::warn!( @@ -261,40 +287,21 @@ impl Config { // Last resort: create a fresh default config file tracing::error!("Could not recover config file, creating fresh default configuration. Original error: {}", parse_error); - // Try to load from init-config.yaml if it exists, otherwise use empty config - let default_config = self - .load_init_config_if_exists() - .unwrap_or_else(|_| HashMap::new()); + let default_config = self.load_init_config_if_exists().unwrap_or_default(); self.create_and_save_default_config(default_config) } } } - // Parse YAML content into HashMap - fn parse_yaml_content(&self, content: &str) -> Result, ConfigError> { - if content.trim().is_empty() { - return Ok(HashMap::new()); - } - - let yaml_value: serde_yaml::Value = serde_yaml::from_str(content)?; - let json_value: Value = serde_json::to_value(yaml_value)?; - - match json_value { - Value::Object(map) => Ok(map.into_iter().collect()), - _ => Ok(HashMap::new()), - } - } - - // Try to restore from backup file - fn try_restore_from_backup(&self) -> Result, ConfigError> { + fn try_restore_from_backup(&self) -> Result { let backup_paths = self.get_backup_paths(); for backup_path in backup_paths { if backup_path.exists() { match std::fs::read_to_string(&backup_path) { Ok(backup_content) => { - match self.parse_yaml_content(&backup_content) { + match parse_yaml_content(&backup_content) { Ok(values) => { // Successfully parsed backup, restore it as the main config if let Err(e) = self.save_values(values.clone()) { @@ -354,13 +361,11 @@ impl Config { paths } - // Try to load init-config.yaml from workspace root if it exists - fn load_init_config_if_exists(&self) -> Result, ConfigError> { + fn load_init_config_if_exists(&self) -> Result { load_init_config_from_workspace() } - // Save current values to the config file - pub fn save_values(&self, values: HashMap) -> Result<(), ConfigError> { + fn save_values(&self, values: Mapping) -> Result<(), ConfigError> { // Create backup before writing new config self.create_backup_if_needed()?; @@ -400,6 +405,15 @@ impl Config { Ok(()) } + pub fn initialize_if_empty(&self, values: Mapping) -> Result<(), ConfigError> { + let _guard = self.guard.lock().unwrap(); + if !self.exists() { + self.save_values(values) + } else { + Ok(()) + } + } + // Create backup of current config file if it exists and is valid fn create_backup_if_needed(&self) -> Result<(), ConfigError> { if !self.config_path.exists() { @@ -408,7 +422,7 @@ impl Config { // Check if current config is valid before backing it up let current_content = std::fs::read_to_string(&self.config_path)?; - if self.parse_yaml_content(¤t_content).is_err() { + if parse_yaml_content(¤t_content).is_err() { // Don't back up corrupted files return Ok(()); } @@ -467,8 +481,7 @@ impl Config { Ok(()) } - // Load current secrets from the keyring - pub fn load_secrets(&self) -> Result, ConfigError> { + pub fn all_secrets(&self) -> Result, ConfigError> { match &self.secrets { SecretStorage::Keyring { service } => { let entry = Entry::new(service, KEYRING_USERNAME)?; @@ -541,7 +554,10 @@ impl Config { } // save a parameter in the appropriate location based on if it's secret or not - pub fn set(&self, key: &str, value: Value, is_secret: bool) -> Result<(), ConfigError> { + pub fn set(&self, key: &str, value: &V, is_secret: bool) -> Result<(), ConfigError> + where + V: Serialize, + { if is_secret { self.set_secret(key, value) } else { @@ -566,21 +582,17 @@ impl Config { /// - The value cannot be deserialized into the requested type /// - There is an error reading the config file pub fn get_param Deserialize<'de>>(&self, key: &str) -> Result { - // First check environment variables (convert to uppercase) let env_key = key.to_uppercase(); if let Ok(val) = env::var(&env_key) { let value = Self::parse_env_value(&val)?; return Ok(serde_json::from_value(value)?); } - // Load current values from file - let values = self.load_values()?; - - // Then check our stored values + let values = self.load()?; values .get(key) .ok_or_else(|| ConfigError::NotFound(key.to_string())) - .and_then(|v| Ok(serde_json::from_value(v.clone())?)) + .and_then(|v| Ok(serde_yaml::from_value(v.clone())?)) } /// Set a configuration value in the config file (non-secret). @@ -596,17 +608,10 @@ impl Config { /// Returns a ConfigError if: /// - There is an error reading or writing the config file /// - There is an error serializing the value - pub fn set_param(&self, key: &str, value: Value) -> Result<(), ConfigError> { - // Lock before reading to prevent race condition. + pub fn set_param(&self, key: &str, value: V) -> Result<(), ConfigError> { let _guard = self.guard.lock().unwrap(); - - // Load current values with recovery if needed - let mut values = self.load_values()?; - - // Modify values - values.insert(key.to_string(), value); - - // Save all values using the atomic write approach + let mut values = self.load()?; + values.insert(serde_yaml::to_value(key)?, serde_yaml::to_value(value)?); self.save_values(values) } @@ -627,8 +632,8 @@ impl Config { // Lock before reading to prevent race condition. let _guard = self.guard.lock().unwrap(); - let mut values = self.load_values()?; - values.remove(key); + let mut values = self.load()?; + values.shift_remove(key); self.save_values(values) } @@ -658,7 +663,7 @@ impl Config { } // Then check keyring - let values = self.load_secrets()?; + let values = self.all_secrets()?; values .get(key) .ok_or_else(|| ConfigError::NotFound(key.to_string())) @@ -679,12 +684,15 @@ impl Config { /// Returns a ConfigError if: /// - There is an error accessing the keyring /// - There is an error serializing the value - pub fn set_secret(&self, key: &str, value: Value) -> Result<(), ConfigError> { + pub fn set_secret(&self, key: &str, value: &V) -> Result<(), ConfigError> + where + V: Serialize, + { // Lock before reading to prevent race condition. let _guard = self.guard.lock().unwrap(); - let mut values = self.load_secrets()?; - values.insert(key.to_string(), value); + let mut values = self.all_secrets()?; + values.insert(key.to_string(), serde_json::to_value(value)?); match &self.secrets { SecretStorage::Keyring { service } => { @@ -714,7 +722,7 @@ impl Config { // Lock before reading to prevent race condition. let _guard = self.guard.lock().unwrap(); - let mut values = self.load_secrets()?; + let mut values = self.all_secrets()?; values.remove(key); match &self.secrets { @@ -730,11 +738,16 @@ impl Config { }; Ok(()) } + + declare_param!(GOOSE_SEARCH_PATHS, Vec); + declare_param!(GOOSE_MODE, GooseMode); + declare_param!(GOOSE_PROVIDER, String); + declare_param!(GOOSE_MODEL, String); } /// Load init-config.yaml from workspace root if it exists. /// This function is shared between the config recovery and the init_config endpoint. -pub fn load_init_config_from_workspace() -> Result, ConfigError> { +pub fn load_init_config_from_workspace() -> Result { let workspace_root = match std::env::current_exe() { Ok(mut exe_path) => { while let Some(parent) = exe_path.parent() { @@ -767,23 +780,7 @@ pub fn load_init_config_from_workspace() -> Result, Confi } let init_content = std::fs::read_to_string(&init_config_path)?; - let init_values: HashMap = - match serde_yaml::from_str::(&init_content) { - Ok(yaml_value) => { - let json_value: Value = serde_json::to_value(yaml_value)?; - match json_value { - Value::Object(map) => map.into_iter().collect(), - _ => HashMap::new(), - } - } - Err(e) => { - tracing::warn!("Failed to parse init-config.yaml: {}", e); - return Err(ConfigError::DeserializeError(e.to_string())); - } - }; - - tracing::info!("Loaded init-config.yaml with {} keys", init_values.len()); - Ok(init_values) + parse_yaml_content(&init_content) } #[cfg(test)] @@ -807,7 +804,7 @@ mod tests { let config = Config::new(temp_file.path(), TEST_KEYRING_SERVICE)?; // Set a simple string value - config.set_param("test_key", Value::String("test_value".to_string()))?; + config.set_param("test_key", "test_value")?; // Test simple string retrieval let value: String = config.get_param("test_key")?; @@ -862,8 +859,8 @@ mod tests { let temp_file = NamedTempFile::new().unwrap(); let config = Config::new(temp_file.path(), TEST_KEYRING_SERVICE)?; - config.set_param("key1", Value::String("value1".to_string()))?; - config.set_param("key2", Value::Number(42.into()))?; + config.set_param("key1", "value1")?; + config.set_param("key2", 42)?; // Read the file directly to check YAML formatting let content = std::fs::read_to_string(temp_file.path())?; @@ -878,12 +875,11 @@ mod tests { let temp_file = NamedTempFile::new().unwrap(); let config = Config::new(temp_file.path(), TEST_KEYRING_SERVICE)?; - config.set_param("key", Value::String("value".to_string()))?; + config.set_param("test_key", "test_value")?; + config.set_param("another_key", 42)?; + config.set_param("third_key", true)?; - let value: String = config.get_param("key")?; - assert_eq!(value, "value"); - - config.delete("key")?; + let _values = config.load()?; let result: Result = config.get_param("key"); assert!(matches!(result, Err(ConfigError::NotFound(_)))); @@ -897,7 +893,7 @@ mod tests { let secrets_file = NamedTempFile::new().unwrap(); let config = Config::new_with_file_secrets(config_file.path(), secrets_file.path())?; - config.set_secret("key", Value::String("value".to_string()))?; + config.set_secret("key", &"value")?; let value: String = config.get_secret("key")?; assert_eq!(value, "value"); @@ -918,7 +914,7 @@ mod tests { let config = Config::new(temp_file.path(), TEST_KEYRING_SERVICE)?; // Test setting and getting a simple secret - config.set_secret("api_key", Value::String("secret123".to_string()))?; + config.set_secret("api_key", &Value::String("secret123".to_string()))?; let value: String = config.get_secret("api_key")?; assert_eq!(value, "secret123"); @@ -945,8 +941,8 @@ mod tests { let config = Config::new(temp_file.path(), TEST_KEYRING_SERVICE)?; // Set multiple secrets - config.set_secret("key1", Value::String("secret1".to_string()))?; - config.set_secret("key2", Value::String("secret2".to_string()))?; + config.set_secret("key1", &Value::String("secret1".to_string()))?; + config.set_secret("key2", &Value::String("secret2".to_string()))?; // Verify both exist let value1: String = config.get_secret("key1")?; @@ -975,11 +971,11 @@ mod tests { let temp_file = NamedTempFile::new().unwrap(); let config = Arc::new(Config::new(temp_file.path(), TEST_KEYRING_SERVICE)?); let barrier = Arc::new(Barrier::new(3)); // For 3 concurrent threads - let values = Arc::new(Mutex::new(HashMap::new())); + let values = Arc::new(Mutex::new(Mapping::new())); let mut handles = vec![]; // Initialize with empty values - config.save_values(HashMap::new())?; + config.save_values(Default::default())?; // Spawn 3 threads that will try to write simultaneously for i in 0..3 { @@ -992,7 +988,10 @@ mod tests { // Get the lock and update values let mut values = values.lock().unwrap(); - values.insert(format!("key{}", i), Value::String(format!("value{}", i))); + values.insert( + serde_yaml::to_value(format!("key{}", i)).unwrap(), + serde_yaml::to_value(format!("value{}", i)).unwrap(), + ); // Write all values config.save_values(values.clone())?; @@ -1007,7 +1006,7 @@ mod tests { } // Verify all values were written correctly - let final_values = config.load_values()?; + let final_values = config.all_values()?; // Print the final values for debugging println!("Final values: {:?}", final_values); @@ -1044,7 +1043,7 @@ mod tests { let config = Config::new(temp_file.path(), TEST_KEYRING_SERVICE)?; // Create a valid config first - config.set_param("key1", Value::String("value1".to_string()))?; + config.set_param("key1", "value1")?; // Verify the backup was created by the first write let backup_paths = config.get_backup_paths(); @@ -1054,7 +1053,7 @@ mod tests { } // Make another write to ensure backup is created - config.set_param("key2", Value::Number(42.into()))?; + config.set_param("key2", 42)?; // Check again for (i, path) in backup_paths.iter().enumerate() { @@ -1069,7 +1068,7 @@ mod tests { std::fs::write(temp_file.path(), "invalid: yaml: content: [unclosed")?; // Try to load values - should recover from backup - let recovered_values = config.load_values()?; + let recovered_values = config.all_values()?; println!("Recovered values: {:?}", recovered_values); // Should have recovered the data @@ -1090,7 +1089,7 @@ mod tests { std::fs::write(temp_file.path(), "invalid: yaml: content: [unclosed")?; // Try to load values - should create a fresh default config - let recovered_values = config.load_values()?; + let recovered_values = config.all_values()?; // Should return empty config assert_eq!(recovered_values.len(), 0); @@ -1103,7 +1102,7 @@ mod tests { assert!(parsed.is_mapping()); // Should be able to load it again without issues - let reloaded_values = config.load_values()?; + let reloaded_values = config.all_values()?; assert_eq!(reloaded_values.len(), 0); Ok(()) @@ -1121,7 +1120,7 @@ mod tests { let config = Config::new(config_path, TEST_KEYRING_SERVICE)?; // Try to load values - should create a fresh default config file - let values = config.load_values()?; + let values = config.all_values()?; // Should return empty config assert_eq!(values.len(), 0); @@ -1135,7 +1134,7 @@ mod tests { assert!(parsed.is_mapping()); // Should be able to load it again without issues - let reloaded_values = config.load_values()?; + let reloaded_values = config.all_values()?; assert_eq!(reloaded_values.len(), 0); Ok(()) @@ -1148,15 +1147,15 @@ mod tests { let config = Config::new(config_path, TEST_KEYRING_SERVICE)?; // First, create a config with some data - config.set_param("test_key_backup", Value::String("backup_value".to_string()))?; - config.set_param("another_key", Value::Number(42.into()))?; + config.set_param("test_key_backup", "backup_value")?; + config.set_param("another_key", 42)?; // Verify the backup was created let backup_paths = config.get_backup_paths(); let primary_backup = &backup_paths[0]; // .bak file // Make sure we have a backup by doing another write - config.set_param("third_key", Value::Bool(true))?; + config.set_param("third_key", true)?; assert!(primary_backup.exists(), "Backup should exist after writes"); // Now delete the main config file to simulate it being lost @@ -1164,7 +1163,7 @@ mod tests { assert!(!config_path.exists()); // Try to load values - should recover from backup - let recovered_values = config.load_values()?; + let recovered_values = config.all_values()?; // Should have recovered the data from backup assert!( @@ -1192,7 +1191,7 @@ mod tests { let config = Config::new(temp_file.path(), TEST_KEYRING_SERVICE)?; // Set initial values - config.set_param("key1", Value::String("value1".to_string()))?; + config.set_param("key1", "value1")?; // Verify the config file exists and is valid assert!(temp_file.path().exists()); @@ -1213,7 +1212,7 @@ mod tests { // Create multiple versions to test rotation for i in 1..=7 { - config.set_param("version", Value::Number(i.into()))?; + config.set_param("version", i)?; } let backup_paths = config.get_backup_paths(); @@ -1448,7 +1447,7 @@ mod tests { let config = Config::new(temp_file.path(), TEST_KEYRING_SERVICE)?; // Set value in config file - config.set_param("test_precedence", Value::String("file_value".to_string()))?; + config.set_param("test_precedence", "file_value")?; // Verify file value is returned when no env var let value: String = config.get_param("test_precedence")?; diff --git a/crates/goose/src/config/declarative_providers.rs b/crates/goose/src/config/declarative_providers.rs index 10df3ff1021b..a063ec475696 100644 --- a/crates/goose/src/config/declarative_providers.rs +++ b/crates/goose/src/config/declarative_providers.rs @@ -97,7 +97,7 @@ pub fn create_custom_provider( let api_key_name = generate_api_key_name(&id); let config = Config::global(); - config.set_secret(&api_key_name, serde_json::Value::String(api_key))?; + config.set_secret(&api_key_name, &api_key)?; let model_infos: Vec = models .into_iter() @@ -147,10 +147,7 @@ pub fn update_custom_provider( let config = Config::global(); if !api_key.is_empty() { - config.set_secret( - &existing_config.api_key_env, - serde_json::Value::String(api_key), - )?; + config.set_secret(&existing_config.api_key_env, &api_key)?; } if editable { diff --git a/crates/goose/src/config/experiments.rs b/crates/goose/src/config/experiments.rs index 80135adb19a7..c60802e2bc04 100644 --- a/crates/goose/src/config/experiments.rs +++ b/crates/goose/src/config/experiments.rs @@ -35,7 +35,7 @@ impl ExperimentManager { Self::refresh_experiments(&mut experiments); experiments.insert(name.to_string(), enabled); - config.set_param("experiments", serde_json::to_value(experiments)?)?; + config.set_param("experiments", experiments)?; Ok(()) } diff --git a/crates/goose/src/config/extensions.rs b/crates/goose/src/config/extensions.rs index 4241e1b42245..4ad4e84de300 100644 --- a/crates/goose/src/config/extensions.rs +++ b/crates/goose/src/config/extensions.rs @@ -1,9 +1,9 @@ use super::base::Config; use crate::agents::extension::PLATFORM_EXTENSIONS; use crate::agents::ExtensionConfig; +use indexmap::IndexMap; use serde::{Deserialize, Serialize}; -use serde_json::Value; -use std::collections::HashMap; +use serde_yaml::Mapping; use tracing::warn; use utoipa::ToSchema; @@ -27,56 +27,32 @@ pub fn name_to_key(name: &str) -> String { .to_lowercase() } -fn get_extensions_map() -> HashMap { - let raw: Value = Config::global() - .get_param::(EXTENSIONS_CONFIG_KEY) +fn get_extensions_map() -> IndexMap { + let raw: Mapping = Config::global() + .get_param(EXTENSIONS_CONFIG_KEY) .unwrap_or_else(|err| { warn!( "Failed to load {}: {err}. Falling back to empty object.", EXTENSIONS_CONFIG_KEY ); - Value::Object(serde_json::Map::new()) + Default::default() }); - let mut extensions_map: HashMap = match raw { - Value::Object(obj) => { - let mut m = HashMap::with_capacity(obj.len()); - for (k, mut v) in obj { - if let Value::Object(ref mut inner) = v { - match inner.get("description") { - Some(Value::Null) | None => { - inner.insert("description".to_string(), Value::String(String::new())); - } - _ => {} - } - } - match serde_json::from_value::(v.clone()) { - Ok(entry) => { - m.insert(k, entry); - } - Err(err) => { - let bad_json = serde_json::to_string(&v).unwrap_or_else(|e| { - format!("") - }); - warn!( - extension = %k, - error = %err, - bad_json = %bad_json, - "Skipping malformed extension" - ); - } - } + let mut extensions_map = IndexMap::with_capacity(raw.len()); + for (k, v) in raw { + match (k, serde_yaml::from_value::(v)) { + (serde_yaml::Value::String(s), Ok(entry)) => { + extensions_map.insert(s, entry); + } + (k, v) => { + warn!( + key = ?k, + value = ?v, + "Skipping malformed extension config entry" + ); } - m - } - other => { - warn!( - "Expected object for {}, got {}. Using empty map.", - EXTENSIONS_CONFIG_KEY, other - ); - HashMap::new() } - }; + } if !extensions_map.is_empty() { for (name, def) in PLATFORM_EXTENSIONS.iter() { @@ -99,17 +75,11 @@ fn get_extensions_map() -> HashMap { extensions_map } -fn save_extensions_map(extensions: HashMap) { +fn save_extensions_map(extensions: IndexMap) { let config = Config::global(); - match serde_json::to_value(extensions) { - Ok(value) => { - if let Err(e) = config.set_param(EXTENSIONS_CONFIG_KEY, value) { - tracing::debug!("Failed to save extensions config: {}", e); - } - } - Err(e) => { - tracing::debug!("Failed to serialize extensions: {}", e); - } + if let Err(e) = config.set_param(EXTENSIONS_CONFIG_KEY, &extensions) { + // TODO(jack) why is this just a debug statement? + tracing::debug!("Failed to save extensions config: {}", e); } } @@ -130,7 +100,7 @@ pub fn set_extension(entry: ExtensionEntry) { pub fn remove_extension(key: &str) { let mut extensions = get_extensions_map(); - extensions.remove(key); + extensions.shift_remove(key); save_extensions_map(extensions); } diff --git a/crates/goose/src/config/goose_mode.rs b/crates/goose/src/config/goose_mode.rs new file mode 100644 index 000000000000..e778aa8df729 --- /dev/null +++ b/crates/goose/src/config/goose_mode.rs @@ -0,0 +1,26 @@ +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum GooseMode { + Auto, + Approve, + SmartApprove, + Chat, +} + +impl FromStr for GooseMode { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "auto" => Ok(GooseMode::Auto), + "approve" => Ok(GooseMode::Approve), + "smart_approve" => Ok(GooseMode::SmartApprove), + "chat" => Ok(GooseMode::Chat), + _ => Err(format!("invalid mode: {}", s)), + } + } +} diff --git a/crates/goose/src/config/mod.rs b/crates/goose/src/config/mod.rs index 3ab6f3497ffa..ffae26d460aa 100644 --- a/crates/goose/src/config/mod.rs +++ b/crates/goose/src/config/mod.rs @@ -2,8 +2,10 @@ pub mod base; pub mod declarative_providers; mod experiments; pub mod extensions; +pub mod goose_mode; pub mod paths; pub mod permission; +pub mod search_path; pub mod signup_openrouter; pub mod signup_tetrate; @@ -15,6 +17,7 @@ pub use extensions::{ get_all_extension_names, get_all_extensions, get_enabled_extensions, get_extension_by_name, is_extension_enabled, remove_extension, set_extension, set_extension_enabled, ExtensionEntry, }; +pub use goose_mode::GooseMode; pub use permission::PermissionManager; pub use signup_openrouter::configure_openrouter; pub use signup_tetrate::configure_tetrate; diff --git a/crates/goose/src/config/permission.rs b/crates/goose/src/config/permission.rs index 5c184a3e9765..b2f2b63b1997 100644 --- a/crates/goose/src/config/permission.rs +++ b/crates/goose/src/config/permission.rs @@ -91,6 +91,11 @@ impl PermissionManager { self.get_permission(SMART_APPROVE_PERMISSION, principal_name) } + /// Retrieves the config file path. + pub fn get_config_path(&self) -> &Path { + self.config_path.as_path() + } + /// Helper function to retrieve the permission level for a specific permission category and tool. fn get_permission(&self, name: &str, principal_name: &str) -> Option { // Check if the permission category exists in the map diff --git a/crates/goose/src/config/search_path.rs b/crates/goose/src/config/search_path.rs new file mode 100644 index 000000000000..d500a4bb5167 --- /dev/null +++ b/crates/goose/src/config/search_path.rs @@ -0,0 +1,25 @@ +use std::{env, ffi::OsString, path::PathBuf}; + +use crate::config::{Config, ConfigError}; + +pub fn search_path_var() -> Result { + let paths = Config::global() + .get_goose_search_paths() + .or_else(|err| match err { + ConfigError::NotFound(_) => Ok(vec![]), + err => Err(err), + })? + .into_iter() + .map(|s| PathBuf::from(shellexpand::tilde(&s).as_ref())); + + env::join_paths( + paths.chain( + env::var_os("PATH") + .as_ref() + .map(env::split_paths) + .into_iter() + .flatten(), + ), + ) + .map_err(|e| ConfigError::DeserializeError(format!("{}", e))) +} diff --git a/crates/goose/src/config/signup_openrouter/mod.rs b/crates/goose/src/config/signup_openrouter/mod.rs index cc5c8b079c06..0423727079ec 100644 --- a/crates/goose/src/config/signup_openrouter/mod.rs +++ b/crates/goose/src/config/signup_openrouter/mod.rs @@ -162,14 +162,10 @@ impl PkceAuthFlow { pub use self::PkceAuthFlow as OpenRouterAuth; use crate::config::Config; -use serde_json::Value; pub fn configure_openrouter(config: &Config, api_key: String) -> Result<()> { - config.set_secret("OPENROUTER_API_KEY", Value::String(api_key))?; - config.set_param("GOOSE_PROVIDER", Value::String("openrouter".to_string()))?; - config.set_param( - "GOOSE_MODEL", - Value::String(OPENROUTER_DEFAULT_MODEL.to_string()), - )?; + config.set_secret("OPENROUTER_API_KEY", &api_key)?; + config.set_goose_provider("openrouter")?; + config.set_goose_model(OPENROUTER_DEFAULT_MODEL)?; Ok(()) } diff --git a/crates/goose/src/config/signup_tetrate/mod.rs b/crates/goose/src/config/signup_tetrate/mod.rs index b8b97c967a6c..b868983a8344 100644 --- a/crates/goose/src/config/signup_tetrate/mod.rs +++ b/crates/goose/src/config/signup_tetrate/mod.rs @@ -163,14 +163,10 @@ impl PkceAuthFlow { pub use self::PkceAuthFlow as TetrateAuth; use crate::config::Config; -use serde_json::Value; pub fn configure_tetrate(config: &Config, api_key: String) -> Result<()> { - config.set_secret("TETRATE_API_KEY", Value::String(api_key))?; - config.set_param("GOOSE_PROVIDER", Value::String("tetrate".to_string()))?; - config.set_param( - "GOOSE_MODEL", - Value::String(TETRATE_DEFAULT_MODEL.to_string()), - )?; + config.set_secret("TETRATE_API_KEY", &api_key)?; + config.set_goose_provider("tetrate")?; + config.set_goose_model(TETRATE_DEFAULT_MODEL)?; Ok(()) } diff --git a/crates/goose/src/config/signup_tetrate/tests.rs b/crates/goose/src/config/signup_tetrate/tests.rs index 8335337c2fd3..64f9136beb90 100644 --- a/crates/goose/src/config/signup_tetrate/tests.rs +++ b/crates/goose/src/config/signup_tetrate/tests.rs @@ -76,12 +76,9 @@ fn test_configure_tetrate() { config.get_secret::("TETRATE_API_KEY").unwrap(), test_key ); + assert_eq!(config.get_goose_provider().unwrap(), "tetrate"); assert_eq!( - config.get_param::("GOOSE_PROVIDER").unwrap(), - "tetrate" - ); - assert_eq!( - config.get_param::("GOOSE_MODEL").unwrap(), + config.get_goose_model().unwrap(), TETRATE_DEFAULT_MODEL.to_string() ); } diff --git a/crates/goose/src/context_mgmt/mod.rs b/crates/goose/src/context_mgmt/mod.rs index 03b2871b017e..e0ac4368aa81 100644 --- a/crates/goose/src/context_mgmt/mod.rs +++ b/crates/goose/src/context_mgmt/mod.rs @@ -31,13 +31,12 @@ struct SummarizeContext { /// # Returns /// * A tuple containing: /// - `Conversation`: The compacted messages -/// - `Vec`: Token counts for each message -/// - `Option`: Provider usage from summarization +/// - `ProviderUsage`: Provider usage from summarization pub async fn compact_messages( agent: &Agent, conversation: &Conversation, preserve_last_user_message: bool, -) -> Result<(Conversation, Vec, Option)> { +) -> Result<(Conversation, ProviderUsage)> { info!("Performing message compaction"); let messages = conversation.messages(); @@ -99,16 +98,8 @@ pub async fn compact_messages( }; let provider = agent.provider().await?; - let summary = do_compact(provider.clone(), messages_to_compact).await?; - - let (summary_message, summarization_usage) = match summary { - Some((summary_message, provider_usage)) => (summary_message, Some(provider_usage)), - None => { - // No summary was generated (empty input) - tracing::warn!("Summarization failed. Returning empty messages."); - return Ok((Conversation::empty(), vec![], None)); - } - }; + let (summary_message, summarization_usage) = + do_compact(provider.clone(), messages_to_compact).await?; // Create the final message list with updated visibility metadata: // 1. Original messages become user_visible but not agent_visible @@ -116,27 +107,17 @@ pub async fn compact_messages( // 3. Assistant messages to continue the conversation remain both user_visible and agent_visible let mut final_messages = Vec::new(); - let mut final_token_counts = Vec::new(); // Add all original messages with updated visibility (preserve user_visible, set agent_visible=false) for msg in messages_to_compact.iter().cloned() { let updated_metadata = msg.metadata.with_agent_invisible(); let updated_msg = msg.with_metadata(updated_metadata); final_messages.push(updated_msg); - // Token count doesn't matter for agent_visible=false messages, but we'll use 0 - final_token_counts.push(0); } // Add the summary message (agent_visible=true, user_visible=false) let summary_msg = summary_message.with_metadata(MessageMetadata::agent_only()); - // For token counting purposes, we use the output tokens (the actual summary content) - // since that's what will be in the context going forward - let summary_tokens = summarization_usage - .as_ref() - .and_then(|usage| usage.usage.output_tokens) - .unwrap_or(0) as usize; final_messages.push(summary_msg); - final_token_counts.push(summary_tokens); // Add an assistant message to continue the conversation (agent_visible=true, user_visible=false) let assistant_message = Message::assistant() @@ -146,9 +127,7 @@ Do not mention that you read a summary or that conversation summarization occurr Just continue the conversation naturally based on the summarized context" ) .with_metadata(MessageMetadata::agent_only()); - let assistant_message_tokens: usize = 0; // Not counted since it's for agent context only final_messages.push(assistant_message); - final_token_counts.push(assistant_message_tokens); // Add back the preserved user message if it exists if let Some(user_text) = preserved_user_text { @@ -157,7 +136,6 @@ Just continue the conversation naturally based on the summarized context" Ok(( Conversation::new_unvalidated(final_messages), - final_token_counts, summarization_usage, )) } @@ -167,11 +145,10 @@ pub async fn check_if_compaction_needed( agent: &Agent, conversation: &Conversation, threshold_override: Option, - session_metadata: Option<&crate::session::Session>, + session: &crate::session::Session, ) -> Result { let messages = conversation.messages(); let config = Config::global(); - // TODO(Douwe): check the default here; it seems to reset to 0.3 sometimes let threshold = threshold_override.unwrap_or_else(|| { config .get_param::("GOOSE_AUTO_COMPACT_THRESHOLD") @@ -181,7 +158,7 @@ pub async fn check_if_compaction_needed( let provider = agent.provider().await?; let context_limit = provider.get_model_config().context_limit(); - let (current_tokens, token_source) = match session_metadata.and_then(|m| m.total_tokens) { + let (current_tokens, token_source) = match session.total_tokens { Some(tokens) => (tokens as usize, "session metadata"), None => { let token_counter = create_token_counter() @@ -222,7 +199,7 @@ pub async fn check_if_compaction_needed( async fn do_compact( provider: Arc, messages: &[Message], -) -> Result, anyhow::Error> { +) -> Result<(Message, ProviderUsage), anyhow::Error> { let agent_visible_messages: Vec<&Message> = messages .iter() .filter(|msg| msg.is_agent_visible()) @@ -255,7 +232,7 @@ async fn do_compact( .await .map_err(|e| anyhow::anyhow!("Failed to ensure usage tokens: {}", e))?; - Ok(Some((response, provider_usage))) + Ok((response, provider_usage)) } fn format_message_for_compacting(msg: &Message) -> String { diff --git a/crates/goose/src/conversation/message.rs b/crates/goose/src/conversation/message.rs index f432d3292e75..2f18d038836f 100644 --- a/crates/goose/src/conversation/message.rs +++ b/crates/goose/src/conversation/message.rs @@ -711,6 +711,17 @@ impl Message { } } +#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct TokenState { + pub input_tokens: i32, + pub output_tokens: i32, + pub total_tokens: i32, + pub accumulated_input_tokens: i32, + pub accumulated_output_tokens: i32, + pub accumulated_total_tokens: i32, +} + #[cfg(test)] mod tests { use crate::conversation::message::{Message, MessageContent, MessageMetadata}; diff --git a/crates/goose/src/execution/manager.rs b/crates/goose/src/execution/manager.rs index 3d86ff092b29..615f7943213f 100644 --- a/crates/goose/src/execution/manager.rs +++ b/crates/goose/src/execution/manager.rs @@ -1,8 +1,6 @@ use crate::agents::extension::PlatformExtensionContext; use crate::agents::Agent; use crate::config::paths::Paths; -use crate::model::ModelConfig; -use crate::providers::create; use crate::scheduler_factory::SchedulerFactory; use crate::scheduler_trait::SchedulerTrait; use anyhow::Result; @@ -10,7 +8,7 @@ use lru::LruCache; use std::num::NonZeroUsize; use std::sync::Arc; use tokio::sync::{OnceCell, RwLock}; -use tracing::{debug, info, warn}; +use tracing::{debug, info}; const DEFAULT_MAX_SESSION: usize = 100; @@ -23,7 +21,7 @@ pub struct AgentManager { } impl AgentManager { - /// Reset the global singleton - ONLY for testing + #[cfg(test)] pub fn reset_for_test() { unsafe { // Cast away the const to get mutable access @@ -34,7 +32,6 @@ impl AgentManager { } } - // Private constructor - prevents direct instantiation in production async fn new(max_sessions: Option) -> Result { let schedule_file_path = Paths::data_dir().join("schedule.json"); @@ -49,8 +46,6 @@ impl AgentManager { default_provider: Arc::new(RwLock::new(None)), }; - let _ = manager.configure_default_provider().await; - Ok(manager) } @@ -73,39 +68,6 @@ impl AgentManager { *self.default_provider.write().await = Some(provider); } - pub async fn configure_default_provider(&self) -> Result<()> { - let provider_name = std::env::var("GOOSE_DEFAULT_PROVIDER") - .or_else(|_| std::env::var("GOOSE_PROVIDER__TYPE")) - .ok(); - - let model_name = std::env::var("GOOSE_DEFAULT_MODEL") - .or_else(|_| std::env::var("GOOSE_PROVIDER__MODEL")) - .ok(); - - if provider_name.is_none() || model_name.is_none() { - return Ok(()); - } - - if let (Some(provider_name), Some(model_name)) = (provider_name, model_name) { - match ModelConfig::new(&model_name) { - Ok(model_config) => match create(&provider_name, model_config).await { - Ok(provider) => { - self.set_default_provider(provider).await; - info!( - "Configured default provider: {} with model: {}", - provider_name, model_name - ); - } - Err(e) => { - warn!("Failed to create default provider {}: {}", provider_name, e) - } - }, - Err(e) => warn!("Failed to create model config for {}: {}", model_name, e), - } - } - Ok(()) - } - pub async fn get_or_create_agent(&self, session_id: String) -> Result> { { let mut sessions = self.sessions.write().await; @@ -154,3 +116,230 @@ impl AgentManager { self.sessions.read().await.len() } } + +#[cfg(test)] +mod tests { + use serial_test::serial; + use std::sync::Arc; + + use crate::execution::{manager::AgentManager, SessionExecutionMode}; + + #[test] + fn test_execution_mode_constructors() { + assert_eq!( + SessionExecutionMode::chat(), + SessionExecutionMode::Interactive + ); + assert_eq!( + SessionExecutionMode::scheduled(), + SessionExecutionMode::Background + ); + + let parent = "parent-123".to_string(); + assert_eq!( + SessionExecutionMode::task(parent.clone()), + SessionExecutionMode::SubTask { + parent_session: parent + } + ); + } + + #[tokio::test] + #[serial] + async fn test_session_isolation() { + AgentManager::reset_for_test(); + let manager = AgentManager::instance().await.unwrap(); + + let session1 = uuid::Uuid::new_v4().to_string(); + let session2 = uuid::Uuid::new_v4().to_string(); + + let agent1 = manager.get_or_create_agent(session1.clone()).await.unwrap(); + + let agent2 = manager.get_or_create_agent(session2.clone()).await.unwrap(); + + // Different sessions should have different agents + assert!(!Arc::ptr_eq(&agent1, &agent2)); + + // Getting the same session should return the same agent + let agent1_again = manager.get_or_create_agent(session1).await.unwrap(); + + assert!(Arc::ptr_eq(&agent1, &agent1_again)); + + AgentManager::reset_for_test(); + } + + #[tokio::test] + #[serial] + async fn test_session_limit() { + AgentManager::reset_for_test(); + let manager = AgentManager::instance().await.unwrap(); + + let sessions: Vec<_> = (0..100).map(|i| format!("session-{}", i)).collect(); + + for session in &sessions { + manager.get_or_create_agent(session.clone()).await.unwrap(); + } + + // Create a new session after cleanup + let new_session = "new-session".to_string(); + let _new_agent = manager.get_or_create_agent(new_session).await.unwrap(); + + assert_eq!(manager.session_count().await, 100); + } + + #[tokio::test] + #[serial] + async fn test_remove_session() { + AgentManager::reset_for_test(); + let manager = AgentManager::instance().await.unwrap(); + let session = String::from("remove-test"); + + manager.get_or_create_agent(session.clone()).await.unwrap(); + assert!(manager.has_session(&session).await); + + manager.remove_session(&session).await.unwrap(); + assert!(!manager.has_session(&session).await); + + assert!(manager.remove_session(&session).await.is_err()); + } + + #[tokio::test] + #[serial] + async fn test_concurrent_access() { + AgentManager::reset_for_test(); + let manager = AgentManager::instance().await.unwrap(); + let session = String::from("concurrent-test"); + + let mut handles = vec![]; + for _ in 0..10 { + let mgr = Arc::clone(&manager); + let sess = session.clone(); + handles.push(tokio::spawn(async move { + mgr.get_or_create_agent(sess).await.unwrap() + })); + } + + let agents: Vec<_> = futures::future::join_all(handles) + .await + .into_iter() + .map(|r| r.unwrap()) + .collect(); + + for agent in &agents[1..] { + assert!(Arc::ptr_eq(&agents[0], agent)); + } + + assert_eq!(manager.session_count().await, 1); + } + + #[tokio::test] + #[serial] + async fn test_concurrent_session_creation_race_condition() { + // Test that concurrent attempts to create the same new session ID + // result in only one agent being created (tests double-check pattern) + AgentManager::reset_for_test(); + let manager = AgentManager::instance().await.unwrap(); + let session_id = String::from("race-condition-test"); + + // Spawn multiple tasks trying to create the same NEW session simultaneously + let mut handles = vec![]; + for _ in 0..20 { + let sess = session_id.clone(); + let mgr_clone = Arc::clone(&manager); + handles.push(tokio::spawn(async move { + mgr_clone.get_or_create_agent(sess).await.unwrap() + })); + } + + // Collect all agents + let agents: Vec<_> = futures::future::join_all(handles) + .await + .into_iter() + .map(|r| r.unwrap()) + .collect(); + + for agent in &agents[1..] { + assert!( + Arc::ptr_eq(&agents[0], agent), + "All concurrent requests should get the same agent" + ); + } + assert_eq!(manager.session_count().await, 1); + } + + #[tokio::test] + #[serial] + async fn test_set_default_provider() { + use crate::providers::testprovider::TestProvider; + use std::sync::Arc; + + AgentManager::reset_for_test(); + let manager = AgentManager::instance().await.unwrap(); + + // Create a test provider for replaying (doesn't need inner provider) + let temp_file = format!( + "{}/test_provider_{}.json", + std::env::temp_dir().display(), + std::process::id() + ); + + // Create an empty test provider (will fail on actual use but that's ok for this test) + let test_provider = TestProvider::new_replaying(&temp_file) + .unwrap_or_else(|_| TestProvider::new_replaying("/tmp/dummy.json").unwrap()); + + manager.set_default_provider(Arc::new(test_provider)).await; + + let session = String::from("provider-test"); + let _agent = manager.get_or_create_agent(session.clone()).await.unwrap(); + + assert!(manager.has_session(&session).await); + } + + #[tokio::test] + #[serial] + async fn test_eviction_updates_last_used() { + AgentManager::reset_for_test(); + // Test that accessing a session updates its last_used timestamp + // and affects eviction order + let manager = AgentManager::instance().await.unwrap(); + + let sessions: Vec<_> = (0..100).map(|i| format!("session-{}", i)).collect(); + + for session in &sessions { + manager.get_or_create_agent(session.clone()).await.unwrap(); + // Small delay to ensure different timestamps + tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; + } + + // Access the first session again to update its last_used + tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; + manager + .get_or_create_agent(sessions[0].clone()) + .await + .unwrap(); + + // Now create a 101st session - should evict session2 (least recently used) + let session101 = String::from("session-101"); + manager + .get_or_create_agent(session101.clone()) + .await + .unwrap(); + + assert!(manager.has_session(&sessions[0]).await); + assert!(!manager.has_session(&sessions[1]).await); + assert!(manager.has_session(&session101).await); + } + + #[tokio::test] + #[serial] + async fn test_remove_nonexistent_session_error() { + // Test that removing a non-existent session returns an error + AgentManager::reset_for_test(); + let manager = AgentManager::instance().await.unwrap(); + let session = String::from("never-created"); + + let result = manager.remove_session(&session).await; + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("not found")); + } +} diff --git a/crates/goose-mcp/src/developer/goose_hints/import_files.rs b/crates/goose/src/hints/import_files.rs similarity index 100% rename from crates/goose-mcp/src/developer/goose_hints/import_files.rs rename to crates/goose/src/hints/import_files.rs diff --git a/crates/goose-mcp/src/developer/goose_hints/load_hints.rs b/crates/goose/src/hints/load_hints.rs similarity index 82% rename from crates/goose-mcp/src/developer/goose_hints/load_hints.rs rename to crates/goose/src/hints/load_hints.rs index b2074c318ee8..ec4b4375f5a7 100644 --- a/crates/goose-mcp/src/developer/goose_hints/load_hints.rs +++ b/crates/goose/src/hints/load_hints.rs @@ -1,13 +1,14 @@ -use etcetera::{choose_app_strategy, AppStrategy}; use ignore::gitignore::Gitignore; use std::{ collections::HashSet, path::{Path, PathBuf}, }; -use crate::developer::goose_hints::import_files::read_referenced_files; +use crate::config::paths::Paths; +use crate::hints::import_files::read_referenced_files; pub const GOOSE_HINTS_FILENAME: &str = ".goosehints"; +pub const AGENTS_MD_FILENAME: &str = "AGENTS.md"; fn find_git_root(start_dir: &Path) -> Option<&Path> { let mut check_dir = start_dir; @@ -59,22 +60,7 @@ pub fn load_hint_files( let mut local_hints_contents = Vec::with_capacity(hints_filenames.len()); for hints_filename in hints_filenames { - // Global hints - // choose_app_strategy().config_dir() - // - macOS/Linux: ~/.config/goose/ - // - Windows: ~\AppData\Roaming\Block\goose\config\ - // keep previous behavior of expanding ~/.config in case this fails - let global_hints_path = choose_app_strategy(crate::APP_STRATEGY.clone()) - .map(|strategy| strategy.in_config_dir(hints_filename)) - .unwrap_or_else(|_| { - let path_str = format!("~/.config/goose/{}", hints_filename); - PathBuf::from(shellexpand::tilde(&path_str).to_string()) - }); - - if let Some(parent) = global_hints_path.parent() { - let _ = std::fs::create_dir_all(parent); - } - + let global_hints_path = Paths::in_config_dir(hints_filename); if global_hints_path.is_file() { let mut visited = HashSet::new(); let hints_dir = global_hints_path.parent().unwrap(); @@ -116,7 +102,7 @@ pub fn load_hint_files( let mut hints = String::new(); if !global_hints_contents.is_empty() { - hints.push_str("\n### Global Hints\nThe developer extension includes some global hints that apply to all projects & directories.\n"); + hints.push_str("\n### Global Hints\nThese are my global goose hints.\n"); hints.push_str(&global_hints_contents.join("\n")); } @@ -124,7 +110,9 @@ pub fn load_hint_files( if !hints.is_empty() { hints.push_str("\n\n"); } - hints.push_str("### Project Hints\nThe developer extension includes some hints for working on the project in this directory.\n"); + hints.push_str( + "### Project Hints\nThese are hints for working on the project in this directory.\n", + ); hints.push_str(&local_hints_contents.join("\n")); } @@ -135,7 +123,6 @@ pub fn load_hint_files( mod tests { use super::*; use ignore::gitignore::GitignoreBuilder; - use serial_test::serial; use std::fs::{self}; use tempfile::TempDir; @@ -146,51 +133,8 @@ mod tests { } #[test] - #[serial] - fn test_global_goosehints() { - // if ~/.config/goose/.goosehints exists, it should be included in the instructions - // copy the existing global hints file to a .bak file - let global_hints_path = PathBuf::from( - shellexpand::tilde(format!("~/.config/goose/{}", GOOSE_HINTS_FILENAME).as_str()) - .to_string(), - ); - let global_hints_bak_path = PathBuf::from( - shellexpand::tilde(format!("~/.config/goose/{}.bak", GOOSE_HINTS_FILENAME).as_str()) - .to_string(), - ); - let mut globalhints_existed = false; - - if global_hints_path.is_file() { - globalhints_existed = true; - fs::copy(&global_hints_path, &global_hints_bak_path).unwrap(); - } - - fs::write(&global_hints_path, "These are my global goose hints.").unwrap(); - - let dir = TempDir::new().unwrap(); - std::env::set_current_dir(dir.path()).unwrap(); - - let gitignore = create_dummy_gitignore(); - let hints = load_hint_files(dir.path(), &[GOOSE_HINTS_FILENAME.to_string()], &gitignore); - - assert!(hints.contains("### Global Hints")); - assert!(hints.contains("my global goose hints.")); - - // restore backup if globalhints previously existed - if globalhints_existed { - fs::copy(&global_hints_bak_path, &global_hints_path).unwrap(); - fs::remove_file(&global_hints_bak_path).unwrap(); - } else { - // Clean up the test file we created - let _ = fs::remove_file(&global_hints_path); - } - } - - #[test] - #[serial] fn test_goosehints_when_present() { let dir = TempDir::new().unwrap(); - std::env::set_current_dir(dir.path()).unwrap(); fs::write(dir.path().join(GOOSE_HINTS_FILENAME), "Test hint content").unwrap(); let gitignore = create_dummy_gitignore(); @@ -200,10 +144,8 @@ mod tests { } #[test] - #[serial] fn test_goosehints_when_missing() { let dir = TempDir::new().unwrap(); - std::env::set_current_dir(dir.path()).unwrap(); let gitignore = create_dummy_gitignore(); let hints = load_hint_files(dir.path(), &[GOOSE_HINTS_FILENAME.to_string()], &gitignore); @@ -212,10 +154,8 @@ mod tests { } #[test] - #[serial] fn test_goosehints_multiple_filenames() { let dir = TempDir::new().unwrap(); - std::env::set_current_dir(dir.path()).unwrap(); fs::write( dir.path().join("CLAUDE.md"), @@ -240,10 +180,8 @@ mod tests { } #[test] - #[serial] fn test_goosehints_configurable_filename() { let dir = TempDir::new().unwrap(); - std::env::set_current_dir(dir.path()).unwrap(); fs::write(dir.path().join("CLAUDE.md"), "Custom hints file content").unwrap(); let gitignore = create_dummy_gitignore(); @@ -357,11 +295,7 @@ mod tests { fs::create_dir(project_root.join(".git")).unwrap(); - fs::write( - project_root.join("README.md"), - "# Project README\nProject overview content", - ) - .unwrap(); + fs::write(project_root.join("README.md"), "# Project README").unwrap(); fs::write(project_root.join("config.md"), "Configuration details").unwrap(); let hints_content = r#"Project hints content @@ -382,7 +316,6 @@ Additional instructions here."#; assert!(hints.contains("--- Content from README.md ---")); assert!(hints.contains("# Project README")); - assert!(hints.contains("Project overview content")); assert!(hints.contains("--- End of README.md ---")); assert!(hints.contains("--- Content from config.md ---")); diff --git a/crates/goose/src/hints/mod.rs b/crates/goose/src/hints/mod.rs new file mode 100644 index 000000000000..b8c0ddb9964c --- /dev/null +++ b/crates/goose/src/hints/mod.rs @@ -0,0 +1,4 @@ +mod import_files; +pub mod load_hints; + +pub use load_hints::{load_hint_files, AGENTS_MD_FILENAME, GOOSE_HINTS_FILENAME}; diff --git a/crates/goose/src/lib.rs b/crates/goose/src/lib.rs index 9e8fdad19002..80da2e839cee 100644 --- a/crates/goose/src/lib.rs +++ b/crates/goose/src/lib.rs @@ -3,6 +3,7 @@ pub mod config; pub mod context_mgmt; pub mod conversation; pub mod execution; +pub mod hints; pub mod logging; pub mod mcp_utils; pub mod model; @@ -17,6 +18,7 @@ pub mod scheduler_factory; pub mod scheduler_trait; pub mod security; pub mod session; +pub mod session_context; pub mod token_counter; pub mod tool_inspection; pub mod tool_monitor; diff --git a/crates/goose/src/logging.rs b/crates/goose/src/logging.rs index a6742d3d1e8f..80b96a935d85 100644 --- a/crates/goose/src/logging.rs +++ b/crates/goose/src/logging.rs @@ -2,33 +2,59 @@ use crate::config::paths::Paths; use anyhow::{Context, Result}; use std::fs; use std::path::PathBuf; +use std::time::{Duration, SystemTime}; /// Returns the directory where log files should be stored for a specific component. /// Creates the directory structure if it doesn't exist. /// /// # Arguments /// -/// * `component` - The component name (e.g., "cli", "server", "debug") +/// * `component` - The component name (e.g., "cli", "server", "debug", "llm") /// * `use_date_subdir` - Whether to create a date-based subdirectory -pub fn get_log_directory(component: &str, use_date_subdir: bool) -> Result { +pub fn prepare_log_directory(component: &str, use_date_subdir: bool) -> Result { let base_log_dir = Paths::in_state_dir("logs"); + let _ = cleanup_old_logs(component); + let component_dir = base_log_dir.join(component); let log_dir = if use_date_subdir { - // Create date-based subdirectory - let now = chrono::Local::now(); - component_dir.join(now.format("%Y-%m-%d").to_string()) + component_dir.join(chrono::Local::now().format("%Y-%m-%d").to_string()) } else { component_dir }; - // Ensure log directory exists fs::create_dir_all(&log_dir).context("Failed to create log directory")?; Ok(log_dir) } +pub fn cleanup_old_logs(component: &str) -> Result<()> { + let base_log_dir = Paths::in_state_dir("logs"); + let component_dir = base_log_dir.join(component); + + if !component_dir.exists() { + return Ok(()); + } + + let two_weeks = SystemTime::now() - Duration::from_secs(14 * 24 * 60 * 60); + let entries = fs::read_dir(&component_dir)?; + + for entry in entries.flatten() { + let path = entry.path(); + + if let Ok(metadata) = entry.metadata() { + if let Ok(modified) = metadata.modified() { + if modified < two_weeks && path.is_dir() { + let _ = fs::remove_dir_all(&path); + } + } + } + } + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -37,7 +63,7 @@ mod tests { #[test] fn test_get_log_directory_basic_functionality() { // Test basic directory creation without date subdirectory - let result = get_log_directory("cli", false); + let result = prepare_log_directory("cli", false); assert!(result.is_ok()); let log_dir = result.unwrap(); @@ -59,7 +85,7 @@ mod tests { #[test] fn test_get_log_directory_with_date_subdir() { // Test date-based subdirectory creation - let result = get_log_directory("server", true); + let result = prepare_log_directory("server", true); assert!(result.is_ok()); let log_dir = result.unwrap(); @@ -90,11 +116,11 @@ mod tests { // Test that multiple calls return the same result and don't fail let component = "debug"; - let result1 = get_log_directory(component, false); + let result1 = prepare_log_directory(component, false); assert!(result1.is_ok()); let log_dir1 = result1.unwrap(); - let result2 = get_log_directory(component, false); + let result2 = prepare_log_directory(component, false); assert!(result2.is_ok()); let log_dir2 = result2.unwrap(); @@ -104,11 +130,11 @@ mod tests { assert!(log_dir2.exists()); // Test same behavior with date subdirectories - let result3 = get_log_directory(component, true); + let result3 = prepare_log_directory(component, true); assert!(result3.is_ok()); let log_dir3 = result3.unwrap(); - let result4 = get_log_directory(component, true); + let result4 = prepare_log_directory(component, true); assert!(result4.is_ok()); let log_dir4 = result4.unwrap(); @@ -123,7 +149,7 @@ mod tests { let mut created_dirs = Vec::new(); for component in &components { - let result = get_log_directory(component, false); + let result = prepare_log_directory(component, false); assert!(result.is_ok(), "Failed for component: {}", component); let log_dir = result.unwrap(); diff --git a/crates/goose/src/model.rs b/crates/goose/src/model.rs index 8d0be7fb53c8..2b3ee255b7f4 100644 --- a/crates/goose/src/model.rs +++ b/crates/goose/src/model.rs @@ -295,125 +295,3 @@ impl ModelConfig { .unwrap_or_else(|_| panic!("Failed to create model config for {}", model_name)) } } - -#[cfg(test)] -mod tests { - use super::*; - use serial_test::serial; - use temp_env::with_var; - - #[test] - #[serial] - fn test_model_config_context_limits() { - // Clear all GOOSE environment variables to ensure clean test environment - with_var("GOOSE_TEMPERATURE", None::<&str>, || { - with_var("GOOSE_CONTEXT_LIMIT", None::<&str>, || { - with_var("GOOSE_TOOLSHIM", None::<&str>, || { - with_var("GOOSE_TOOLSHIM_OLLAMA_MODEL", None::<&str>, || { - let config = ModelConfig::new("claude-3-opus") - .unwrap() - .with_context_limit(Some(150_000)); - assert_eq!(config.context_limit(), 150_000); - - let config = ModelConfig::new("claude-3-opus").unwrap(); - assert_eq!(config.context_limit(), 200_000); - - let config = ModelConfig::new("gpt-4-turbo").unwrap(); - assert_eq!(config.context_limit(), 128_000); - - let config = ModelConfig::new("unknown-model").unwrap(); - assert_eq!(config.context_limit(), DEFAULT_CONTEXT_LIMIT); - }); - }); - }); - }); - } - - #[test] - #[serial] - fn test_invalid_context_limit() { - with_var("GOOSE_CONTEXT_LIMIT", Some("abc"), || { - let result = ModelConfig::new("test-model"); - assert!(result.is_err()); - if let Err(ConfigError::InvalidValue(var, val, msg)) = result { - assert_eq!(var, "GOOSE_CONTEXT_LIMIT"); - assert_eq!(val, "abc"); - assert!(msg.contains("positive integer")); - } - }); - - with_var("GOOSE_CONTEXT_LIMIT", Some("0"), || { - let result = ModelConfig::new("test-model"); - assert!(result.is_err()); - assert!(matches!( - result.unwrap_err(), - ConfigError::InvalidRange(_, _) - )); - }); - } - - #[test] - #[serial] - fn test_invalid_temperature() { - with_var("GOOSE_TEMPERATURE", Some("hot"), || { - let result = ModelConfig::new("test-model"); - assert!(result.is_err()); - }); - - with_var("GOOSE_TEMPERATURE", Some("-1.0"), || { - let result = ModelConfig::new("test-model"); - assert!(result.is_err()); - }); - } - - #[test] - #[serial] - fn test_invalid_toolshim() { - with_var("GOOSE_TOOLSHIM", Some("maybe"), || { - let result = ModelConfig::new("test-model"); - assert!(result.is_err()); - if let Err(ConfigError::InvalidValue(var, val, msg)) = result { - assert_eq!(var, "GOOSE_TOOLSHIM"); - assert_eq!(val, "maybe"); - assert!(msg.contains("must be one of")); - } - }); - } - - #[test] - #[serial] - fn test_empty_toolshim_model() { - with_var("GOOSE_TOOLSHIM_OLLAMA_MODEL", Some(""), || { - let result = ModelConfig::new("test-model"); - assert!(result.is_err()); - assert!(matches!( - result.unwrap_err(), - ConfigError::InvalidValue(_, _, _) - )); - }); - - with_var("GOOSE_TOOLSHIM_OLLAMA_MODEL", Some(" "), || { - let result = ModelConfig::new("test-model"); - assert!(result.is_err()); - }); - } - - #[test] - #[serial] - fn test_valid_configurations() { - // Test with environment variables set - with_var("GOOSE_CONTEXT_LIMIT", Some("50000"), || { - with_var("GOOSE_TEMPERATURE", Some("0.7"), || { - with_var("GOOSE_TOOLSHIM", Some("true"), || { - with_var("GOOSE_TOOLSHIM_OLLAMA_MODEL", Some("llama3"), || { - let config = ModelConfig::new("test-model").unwrap(); - assert_eq!(config.context_limit(), 50_000); - assert_eq!(config.temperature, Some(0.7)); - assert!(config.toolshim); - assert_eq!(config.toolshim_model, Some("llama3".to_string())); - }); - }); - }); - }); - } -} diff --git a/crates/goose/src/oauth/persist.rs b/crates/goose/src/oauth/persist.rs index cf6fccaa1481..1600b8424862 100644 --- a/crates/goose/src/oauth/persist.rs +++ b/crates/goose/src/oauth/persist.rs @@ -27,9 +27,8 @@ pub async fn save_credentials( token_response, }; - let value = serde_json::to_value(&credentials)?; let key = secret_key(name); - config.set_secret(&key, value)?; + config.set_secret(&key, &credentials)?; Ok(()) } diff --git a/crates/goose/src/permission/permission_inspector.rs b/crates/goose/src/permission/permission_inspector.rs index fe4a347d4b43..88b99a6b05d9 100644 --- a/crates/goose/src/permission/permission_inspector.rs +++ b/crates/goose/src/permission/permission_inspector.rs @@ -1,6 +1,6 @@ use crate::agents::extension_manager_extension::MANAGE_EXTENSIONS_TOOL_NAME_COMPLETE; use crate::config::permission::PermissionLevel; -use crate::config::PermissionManager; +use crate::config::{GooseMode, PermissionManager}; use crate::conversation::message::{Message, ToolRequest}; use crate::permission::permission_judge::PermissionCheckResult; use crate::tool_inspection::{InspectionAction, InspectionResult, ToolInspector}; @@ -12,7 +12,7 @@ use tokio::sync::Mutex; /// Permission Inspector that handles tool permission checking pub struct PermissionInspector { - mode: Arc>, + mode: Arc>, readonly_tools: HashSet, regular_tools: HashSet, pub permission_manager: Arc>, @@ -20,7 +20,7 @@ pub struct PermissionInspector { impl PermissionInspector { pub fn new( - mode: String, + mode: GooseMode, readonly_tools: HashSet, regular_tools: HashSet, ) -> Self { @@ -33,7 +33,7 @@ impl PermissionInspector { } pub fn with_permission_manager( - mode: String, + mode: GooseMode, readonly_tools: HashSet, regular_tools: HashSet, permission_manager: Arc>, @@ -47,7 +47,7 @@ impl PermissionInspector { } /// Update the mode of this permission inspector - pub async fn update_mode(&self, new_mode: String) { + pub async fn update_mode(&self, new_mode: GooseMode) { let mut mode = self.mode.lock().await; *mode = new_mode; } @@ -139,45 +139,42 @@ impl ToolInspector for PermissionInspector { if let Ok(tool_call) = &request.tool_call { let tool_name = &tool_call.name; - // Handle different modes - let action = if *mode == "chat" { - // In chat mode, all tools are skipped (handled elsewhere) - continue; - } else if *mode == "auto" { - // In auto mode, all tools are approved - InspectionAction::Allow - } else { - // Smart mode - check permissions - - // 1. Check user-defined permission first - if let Some(level) = permission_manager.get_user_permission(tool_name) { - match level { - PermissionLevel::AlwaysAllow => InspectionAction::Allow, - PermissionLevel::NeverAllow => InspectionAction::Deny, - PermissionLevel::AskBefore => InspectionAction::RequireApproval(None), + let action = match *mode { + GooseMode::Chat => continue, + GooseMode::Auto => InspectionAction::Allow, + GooseMode::Approve | GooseMode::SmartApprove => { + // 1. Check user-defined permission first + if let Some(level) = permission_manager.get_user_permission(tool_name) { + match level { + PermissionLevel::AlwaysAllow => InspectionAction::Allow, + PermissionLevel::NeverAllow => InspectionAction::Deny, + PermissionLevel::AskBefore => { + InspectionAction::RequireApproval(None) + } + } + } + // 2. Check if it's a readonly or regular tool (both pre-approved) + else if self.readonly_tools.contains(tool_name.as_ref()) + || self.regular_tools.contains(tool_name.as_ref()) + { + InspectionAction::Allow + } + // 4. Special case for extension management + else if tool_name == MANAGE_EXTENSIONS_TOOL_NAME_COMPLETE { + InspectionAction::RequireApproval(Some( + "Extension management requires approval for security".to_string(), + )) + } + // 5. Default: require approval for unknown tools + else { + InspectionAction::RequireApproval(None) } - } - // 2. Check if it's a readonly or regular tool (both pre-approved) - else if self.readonly_tools.contains(tool_name.as_ref()) - || self.regular_tools.contains(tool_name.as_ref()) - { - InspectionAction::Allow - } - // 4. Special case for extension management - else if tool_name == MANAGE_EXTENSIONS_TOOL_NAME_COMPLETE { - InspectionAction::RequireApproval(Some( - "Extension management requires approval for security".to_string(), - )) - } - // 5. Default: require approval for unknown tools - else { - InspectionAction::RequireApproval(None) } }; let reason = match &action { InspectionAction::Allow => { - if *mode == "auto" { + if *mode == GooseMode::Auto { "Auto mode - all tools approved".to_string() } else if self.readonly_tools.contains(tool_name.as_ref()) { "Tool marked as read-only".to_string() diff --git a/crates/goose/src/prompts/system.md b/crates/goose/src/prompts/system.md index 1501f28623b7..55a9e6f59562 100644 --- a/crates/goose/src/prompts/system.md +++ b/crates/goose/src/prompts/system.md @@ -59,20 +59,6 @@ Explain that minimizing extensions helps with the recall of the correct tools to {% endif %} {{tool_selection_strategy}} -{% if enable_subagents %} - -# sub agents - -Execute self contained tasks where step-by-step visibility is not important through subagents. - -- Delegate via `dynamic_task__create_task` for: result-only operations, parallelizable work, multi-part requests, - verification, exploration -- Parallel subagents for multiple operations, single subagents for independent work -- Explore solutions in parallel — launch parallel subagents with different approaches (if non-interfering) -- Provide all needed context — subagents cannot see your context -- Use extension filters to limit resource access -- Use return_last_only when only a summary or simple answer is required — inform subagent of this choice. - {% endif %} # Response Guidelines diff --git a/crates/goose/src/providers/api_client.rs b/crates/goose/src/providers/api_client.rs index 821148bae757..449a74e2086c 100644 --- a/crates/goose/src/providers/api_client.rs +++ b/crates/goose/src/providers/api_client.rs @@ -1,3 +1,4 @@ +use crate::session_context::SESSION_ID_HEADER; use anyhow::Result; use async_trait::async_trait; use reqwest::{ @@ -369,6 +370,10 @@ impl<'a> ApiRequestBuilder<'a> { let mut request = request_builder(url, &self.client.client); request = request.headers(self.headers.clone()); + if let Some(session_id) = crate::session_context::current_session_id() { + request = request.header(SESSION_ID_HEADER, session_id); + } + request = match &self.client.auth { AuthMethod::BearerToken(token) => { request.header("Authorization", format!("Bearer {}", token)) @@ -398,3 +403,55 @@ impl fmt::Debug for ApiClient { .finish_non_exhaustive() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_session_id_header_injection() { + let client = ApiClient::new( + "http://localhost:8080".to_string(), + AuthMethod::BearerToken("test-token".to_string()), + ) + .unwrap(); + + // Execute request within session context + crate::session_context::with_session_id(Some("test-session-456".to_string()), async { + let builder = client.request("/test"); + let request = builder + .send_request(|url, client| client.get(url)) + .await + .unwrap(); + + let headers = request.build().unwrap().headers().clone(); + + assert!(headers.contains_key(SESSION_ID_HEADER)); + assert_eq!( + headers.get(SESSION_ID_HEADER).unwrap().to_str().unwrap(), + "test-session-456" + ); + }) + .await; + } + + #[tokio::test] + async fn test_no_session_id_header_when_absent() { + let client = ApiClient::new( + "http://localhost:8080".to_string(), + AuthMethod::BearerToken("test-token".to_string()), + ) + .unwrap(); + + // Build a request without session context + let builder = client.request("/test"); + let request = builder + .send_request(|url, client| client.get(url)) + .await + .unwrap(); + + let headers = request.build().unwrap().headers().clone(); + + assert!(!headers.contains_key(SESSION_ID_HEADER)); + } +} diff --git a/crates/goose/src/providers/base.rs b/crates/goose/src/providers/base.rs index e55983caf669..ae7de6fbfebe 100644 --- a/crates/goose/src/providers/base.rs +++ b/crates/goose/src/providers/base.rs @@ -278,11 +278,11 @@ impl Add for Usage { type Output = Self; fn add(self, other: Self) -> Self { - Self { - input_tokens: sum_optionals(self.input_tokens, other.input_tokens), - output_tokens: sum_optionals(self.output_tokens, other.output_tokens), - total_tokens: sum_optionals(self.total_tokens, other.total_tokens), - } + Self::new( + sum_optionals(self.input_tokens, other.input_tokens), + sum_optionals(self.output_tokens, other.output_tokens), + sum_optionals(self.total_tokens, other.total_tokens), + ) } } @@ -298,10 +298,21 @@ impl Usage { output_tokens: Option, total_tokens: Option, ) -> Self { + let calculated_total = if total_tokens.is_none() { + match (input_tokens, output_tokens) { + (Some(input), Some(output)) => Some(input + output), + (Some(input), None) => Some(input), + (None, Some(output)) => Some(output), + (None, None) => None, + } + } else { + total_tokens + }; + Self { input_tokens, output_tokens, - total_tokens, + total_tokens: calculated_total, } } } diff --git a/crates/goose/src/providers/bedrock.rs b/crates/goose/src/providers/bedrock.rs index ff5300484316..49277c324a25 100644 --- a/crates/goose/src/providers/bedrock.rs +++ b/crates/goose/src/providers/bedrock.rs @@ -42,8 +42,6 @@ pub struct BedrockProvider { model: ModelConfig, #[serde(skip)] retry_config: RetryConfig, - #[serde(skip)] - name: String, } impl BedrockProvider { @@ -51,7 +49,7 @@ impl BedrockProvider { let config = crate::config::Config::global(); // Attempt to load config and secrets to get AWS_ prefixed keys - // to re-export them into the environment for aws_config::load_from_env() + // to re-export them into the environment for aws_config to use as fallback let set_aws_env_vars = |res: Result, _>| { if let Ok(map) = res { map.into_iter() @@ -61,17 +59,36 @@ impl BedrockProvider { } }; - set_aws_env_vars(config.load_values()); - set_aws_env_vars(config.load_secrets()); + set_aws_env_vars(config.all_values()); + set_aws_env_vars(config.all_secrets()); + + // Use load_defaults() which supports AWS SSO, profiles, and environment variables + let mut loader = aws_config::defaults(aws_config::BehaviorVersion::latest()); - let sdk_config = aws_config::load_from_env().await; + // Check for AWS_PROFILE configuration + if let Ok(profile_name) = config.get_param::("AWS_PROFILE") { + if !profile_name.is_empty() { + loader = loader.profile_name(&profile_name); + } + } + + // Check for AWS_REGION configuration + if let Ok(region) = config.get_param::("AWS_REGION") { + if !region.is_empty() { + loader = loader.region(aws_config::Region::new(region)); + } + } - // validate credentials or return error back up + let sdk_config = loader.load().await; + + // Validate credentials or return error back up sdk_config .credentials_provider() - .unwrap() + .ok_or_else(|| anyhow::anyhow!("No AWS credentials provider configured"))? .provide_credentials() - .await?; + .await + .map_err(|e| anyhow::anyhow!("Failed to load AWS credentials: {}. Make sure to run 'aws sso login --profile ' if using SSO", e))?; + let client = Client::new(&sdk_config); let retry_config = Self::load_retry_config(config); @@ -80,7 +97,6 @@ impl BedrockProvider { client, model, retry_config, - name: Self::metadata().name, }) } @@ -179,18 +195,17 @@ impl Provider for BedrockProvider { ProviderMetadata::new( "aws_bedrock", "Amazon Bedrock", - "Run models through Amazon Bedrock. You may have to set 'AWS_' environment variables to configure authentication.", + "Run models through Amazon Bedrock. Supports AWS SSO profiles - run 'aws sso login --profile ' before using. Configure with AWS_PROFILE and AWS_REGION, or use environment variables/credentials.", BEDROCK_DEFAULT_MODEL, BEDROCK_KNOWN_MODELS.to_vec(), BEDROCK_DOC_LINK, - vec![ConfigKey::new("AWS_PROFILE", true, false, Some("default"))], + vec![ + ConfigKey::new("AWS_PROFILE", true, false, Some("default")), + ConfigKey::new("AWS_REGION", true, false, Some("us-east-1")), + ], ) } - fn get_name(&self) -> &str { - &self.name - } - fn retry_config(&self) -> RetryConfig { self.retry_config.clone() } diff --git a/crates/goose/src/providers/claude_code.rs b/crates/goose/src/providers/claude_code.rs index 9683c2ee03d4..30f49a701e74 100644 --- a/crates/goose/src/providers/claude_code.rs +++ b/crates/goose/src/providers/claude_code.rs @@ -9,16 +9,15 @@ use tokio::process::Command; use super::base::{ConfigKey, Provider, ProviderMetadata, ProviderUsage, Usage}; use super::errors::ProviderError; -use super::utils::RequestLog; -use crate::config::Config; +use super::utils::{filter_extensions_from_system_prompt, RequestLog}; +use crate::config::{Config, GooseMode}; use crate::conversation::message::{Message, MessageContent}; use crate::model::ModelConfig; use rmcp::model::Tool; pub const CLAUDE_CODE_DEFAULT_MODEL: &str = "claude-sonnet-4-20250514"; -pub const CLAUDE_CODE_KNOWN_MODELS: &[&str] = &["sonnet", "opus", "claude-sonnet-4-20250514"]; - -pub const CLAUDE_CODE_DOC_URL: &str = "https://claude.ai/cli"; +pub const CLAUDE_CODE_KNOWN_MODELS: &[&str] = &["sonnet", "opus"]; +pub const CLAUDE_CODE_DOC_URL: &str = "https://code.claude.com/docs/en/setup"; #[derive(Debug, serde::Serialize)] pub struct ClaudeCodeProvider { @@ -103,28 +102,6 @@ impl ClaudeCodeProvider { None } - /// Filter out the Extensions section from the system prompt - fn filter_extensions_from_system_prompt(&self, system: &str) -> String { - // Find the Extensions section and remove it - if let Some(extensions_start) = system.find("# Extensions") { - // Look for the next major section that starts with # - let after_extensions = &system[extensions_start..]; - if let Some(next_section_pos) = after_extensions[1..].find("\n# ") { - // Found next section, keep everything before Extensions and after the next section - let before_extensions = &system[..extensions_start]; - let next_section_start = extensions_start + next_section_pos + 1; - let after_next_section = &system[next_section_start..]; - format!("{}{}", before_extensions.trim_end(), after_next_section) - } else { - // No next section found, just remove everything from Extensions onward - system[..extensions_start].trim_end().to_string() - } - } else { - // No Extensions section found, return original - system.to_string() - } - } - /// Convert goose messages to the format expected by claude CLI fn messages_to_claude_format(&self, _system: &str, messages: &[Message]) -> Result { let mut claude_messages = Vec::new(); @@ -191,6 +168,34 @@ impl ClaudeCodeProvider { } /// Parse the JSON response from claude CLI + fn apply_permission_flags(cmd: &mut Command) -> Result<(), ProviderError> { + let config = Config::global(); + match config.get_goose_mode() { + Ok(GooseMode::Auto) => { + cmd.arg("--dangerously-skip-permissions"); + } + Ok(GooseMode::SmartApprove) => { + cmd.arg("--permission-mode").arg("acceptEdits"); + } + Ok(GooseMode::Approve) => { + return Err(ProviderError::RequestFailed( + "\n\n\n### NOTE\n\n\n \ + Claude Code CLI provider does not support Approve mode.\n \ + Please use Auto (which will run anything it needs to) or \ + SmartApprove (most things will run or Chat Mode)\n\n\n" + .to_string(), + )); + } + Ok(GooseMode::Chat) => { + // Chat mode doesn't need permission flags + } + Err(_) => { + // Default behavior if mode is not set + } + } + Ok(()) + } + fn parse_claude_response( &self, json_lines: &[String], @@ -303,8 +308,7 @@ impl ClaudeCodeProvider { ProviderError::RequestFailed(format!("Failed to format messages: {}", e)) })?; - // Create a filtered system prompt without Extensions section - let filtered_system = self.filter_extensions_from_system_prompt(system); + let filtered_system = filter_extensions_from_system_prompt(system); if std::env::var("GOOSE_CLAUDE_CODE_DEBUG").is_ok() { println!("=== CLAUDE CODE PROVIDER DEBUG ==="); @@ -337,21 +341,21 @@ impl ClaudeCodeProvider { cmd.arg("--verbose").arg("--output-format").arg("json"); // Add permission mode based on GOOSE_MODE setting - let config = Config::global(); - if let Ok(goose_mode) = config.get_param::("GOOSE_MODE") { - if goose_mode.as_str() == "auto" { - cmd.arg("--permission-mode").arg("acceptEdits"); - } - } + Self::apply_permission_flags(&mut cmd)?; cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); let mut child = cmd .spawn() .map_err(|e| ProviderError::RequestFailed(format!( - "Failed to spawn Claude CLI command '{}': {}. \ - Make sure the Claude Code CLI is installed and in your PATH, or set CLAUDE_CODE_COMMAND in your config to the correct path.", - self.command, e + "\n\n ## Unable to find Claude Code CLI on the path.\n\n\ + **Error details:** Failed to spawn command '{}': {}\n\n\ + **Please ensure:**\n\ + - Claude Code CLI is installed and logged in\n\ + - The command is in your PATH, or set `CLAUDE_CODE_COMMAND` in your config\n\n\ + **For full features, please use the Anthropic provider with an API key if possible.**\n\n\ + Visit {} for installation instructions.", + self.command, e, CLAUDE_CODE_DOC_URL )))?; let stdout = child @@ -452,8 +456,8 @@ impl Provider for ClaudeCodeProvider { fn metadata() -> ProviderMetadata { ProviderMetadata::new( "claude-code", - "Claude Code", - "Execute Claude models via claude CLI tool", + "Claude Code CLI", + "Requires claude CLI installed, no MCPs. Use Anthropic provider for full features.", CLAUDE_CODE_DEFAULT_MODEL, CLAUDE_CODE_KNOWN_MODELS.to_vec(), CLAUDE_CODE_DOC_URL, @@ -523,18 +527,6 @@ mod tests { use super::ModelConfig; use super::*; - #[test] - fn test_permission_mode_flag_construction() { - // Test that in auto mode, the --permission-mode acceptEdits flag is added - std::env::set_var("GOOSE_MODE", "auto"); - - let config = Config::global(); - let goose_mode: String = config.get_param("GOOSE_MODE").unwrap(); - assert_eq!(goose_mode, "auto"); - - std::env::remove_var("GOOSE_MODE"); - } - #[tokio::test] async fn test_claude_code_invalid_model_no_fallback() { // Test that an invalid model is kept as-is (no fallback) diff --git a/crates/goose/src/providers/cursor_agent.rs b/crates/goose/src/providers/cursor_agent.rs index 31b185f3ddae..66469c83c2ae 100644 --- a/crates/goose/src/providers/cursor_agent.rs +++ b/crates/goose/src/providers/cursor_agent.rs @@ -9,7 +9,7 @@ use tokio::process::Command; use super::base::{ConfigKey, Provider, ProviderMetadata, ProviderUsage, Usage}; use super::errors::ProviderError; -use super::utils::RequestLog; +use super::utils::{filter_extensions_from_system_prompt, RequestLog}; use crate::conversation::message::{Message, MessageContent}; use crate::model::ModelConfig; use rmcp::model::Tool; @@ -47,6 +47,17 @@ impl CursorAgentProvider { }) } + /// Get authentication status from cursor-agent + async fn get_authentication_status(&self) -> bool { + Command::new(&self.command) + .arg("status") + .output() + .await + .ok() + .map(|output| String::from_utf8_lossy(&output.stdout).contains("✓ Logged in as")) + .unwrap_or(false) + } + /// Search for cursor-agent executable in common installation locations fn find_cursor_agent_executable(command_name: &str) -> Option { let home = std::env::var("HOME").ok()?; @@ -101,34 +112,11 @@ impl CursorAgentProvider { None } - /// Filter out the Extensions section from the system prompt - fn filter_extensions_from_system_prompt(&self, system: &str) -> String { - // Find the Extensions section and remove it - if let Some(extensions_start) = system.find("# Extensions") { - // Look for the next major section that starts with # - let after_extensions = &system[extensions_start..]; - if let Some(next_section_pos) = after_extensions[1..].find("\n# ") { - // Found next section, keep everything before Extensions and after the next section - let before_extensions = &system[..extensions_start]; - let next_section_start = extensions_start + next_section_pos + 1; - let after_next_section = &system[next_section_start..]; - format!("{}{}", before_extensions.trim_end(), after_next_section) - } else { - // No next section found, just remove everything from Extensions onward - system[..extensions_start].trim_end().to_string() - } - } else { - // No Extensions section found, return original - system.to_string() - } - } - /// Convert goose messages to a simple prompt format for cursor-agent CLI fn messages_to_cursor_agent_format(&self, system: &str, messages: &[Message]) -> String { let mut full_prompt = String::new(); - // Add system prompt - let filtered_system = self.filter_extensions_from_system_prompt(system); + let filtered_system = filter_extensions_from_system_prompt(system); full_prompt.push_str(&filtered_system); full_prompt.push_str("\n\n"); @@ -256,7 +244,7 @@ impl CursorAgentProvider { println!("Original system prompt length: {} chars", system.len()); println!( "Filtered system prompt length: {} chars", - self.filter_extensions_from_system_prompt(system).len() + filter_extensions_from_system_prompt(system).len() ); println!("Full prompt: {}", prompt); println!("Model: {}", self.model.model_name); @@ -319,6 +307,11 @@ impl CursorAgentProvider { })?; if !exit_status.success() { + if !self.get_authentication_status().await { + return Err(ProviderError::Authentication( + "You are not logged in to cursor-agent. Please run 'cursor-agent login' to authenticate first." + .to_string())); + } return Err(ProviderError::RequestFailed(format!( "Command failed with exit code: {:?}", exit_status.code() diff --git a/crates/goose/src/providers/databricks.rs b/crates/goose/src/providers/databricks.rs index b36b07f1484e..414fd0dd4e40 100644 --- a/crates/goose/src/providers/databricks.rs +++ b/crates/goose/src/providers/databricks.rs @@ -152,28 +152,24 @@ impl DatabricksProvider { }; // Check if the default fast model exists in the workspace - let model_with_fast = tokio::task::block_in_place(|| { - tokio::runtime::Handle::current().block_on(async { - if let Ok(Some(models)) = provider.fetch_supported_models().await { - if models.contains(&DATABRICKS_DEFAULT_FAST_MODEL.to_string()) { - tracing::debug!( - "Found {} in Databricks workspace, setting as fast model", - DATABRICKS_DEFAULT_FAST_MODEL - ); - model.with_fast(DATABRICKS_DEFAULT_FAST_MODEL.to_string()) - } else { - tracing::debug!( - "{} not found in Databricks workspace, not setting fast model", - DATABRICKS_DEFAULT_FAST_MODEL - ); - model - } - } else { - tracing::debug!("Could not fetch Databricks models, not setting fast model"); - model - } - }) - }); + let model_with_fast = if let Ok(Some(models)) = provider.fetch_supported_models().await { + if models.contains(&DATABRICKS_DEFAULT_FAST_MODEL.to_string()) { + tracing::debug!( + "Found {} in Databricks workspace, setting as fast model", + DATABRICKS_DEFAULT_FAST_MODEL + ); + model.with_fast(DATABRICKS_DEFAULT_FAST_MODEL.to_string()) + } else { + tracing::debug!( + "{} not found in Databricks workspace, not setting fast model", + DATABRICKS_DEFAULT_FAST_MODEL + ); + model + } + } else { + tracing::debug!("Could not fetch Databricks models, not setting fast model"); + model + }; provider.model = model_with_fast; Ok(provider) diff --git a/crates/goose/src/providers/declarative/mistral.json b/crates/goose/src/providers/declarative/mistral.json new file mode 100644 index 000000000000..6add0f0ce1e6 --- /dev/null +++ b/crates/goose/src/providers/declarative/mistral.json @@ -0,0 +1,47 @@ +{ + "name": "mistral", + "engine": "openai", + "display_name": "Mistral AI", + "description": "Frontier models from Mistral AI", + "api_key_env": "MISTRAL_API_KEY", + "base_url": "https://api.mistral.ai/v1/chat/completions", + "models": [ + { + "name": "mistral-medium-latest", + "context_limit": 128000 + }, + { + "name": "mistral-small-2506", + "context_limit": 128000 + }, + { + "name": "mistral-medium-2508", + "context_limit": 128000 + }, + { + "name": "magistral-medium-2509", + "context_limit": 128000 + }, + { + "name": "codestral-2508", + "context_limit": 256000 + }, + { + "name": "pixtral-large-2411", + "context_limit": 128000 + }, + { + "name": "ministral-8b-2410", + "context_limit": 128000 + }, + { + "name": "mistral-medium-2505", + "context_limit": 128000 + }, + { + "name": "ministral-3b-2410", + "context_limit": 128000 + } + ], + "supports_streaming": true +} diff --git a/crates/goose/src/providers/formats/bedrock.rs b/crates/goose/src/providers/formats/bedrock.rs index c1d627a6778d..d6488163000b 100644 --- a/crates/goose/src/providers/formats/bedrock.rs +++ b/crates/goose/src/providers/formats/bedrock.rs @@ -345,11 +345,11 @@ pub fn from_bedrock_role(role: &bedrock::ConversationRole) -> Result { } pub fn from_bedrock_usage(usage: &bedrock::TokenUsage) -> Usage { - Usage { - input_tokens: Some(usage.input_tokens), - output_tokens: Some(usage.output_tokens), - total_tokens: Some(usage.total_tokens), - } + Usage::new( + Some(usage.input_tokens), + Some(usage.output_tokens), + Some(usage.total_tokens), + ) } pub fn from_bedrock_json(document: &Document) -> Result { diff --git a/crates/goose/src/providers/formats/databricks.rs b/crates/goose/src/providers/formats/databricks.rs index c781422d9250..54668e9dfe21 100644 --- a/crates/goose/src/providers/formats/databricks.rs +++ b/crates/goose/src/providers/formats/databricks.rs @@ -212,13 +212,7 @@ fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec { - // Handle direct image content - content_array.push(json!({ - "type": "image_url", - "image_url": { - "url": convert_image(image, image_format) - } - })); + content_array.push(convert_image(image, image_format)); } MessageContent::FrontendToolRequest(req) => { // Frontend tool requests are converted to text messages diff --git a/crates/goose/src/providers/formats/google.rs b/crates/goose/src/providers/formats/google.rs index 7fd1dd075197..786762328b15 100644 --- a/crates/goose/src/providers/formats/google.rs +++ b/crates/goose/src/providers/formats/google.rs @@ -238,7 +238,7 @@ pub fn process_map(map: &Map, parent_key: Option<&str>) -> Value value.clone() } } - _ => value.clone(), + _ => process_value(value, Some(key.as_str())), }; Some((key.clone(), processed_value)) @@ -858,4 +858,36 @@ mod tests { assert_eq!(payload, expected_payload); } + + #[test] + fn test_tools_with_nullable_types_converted_to_single_type() { + // Test that type arrays like ["string", "null"] are converted to single types + let params = object!({ + "properties": { + "nullable_field": { + "type": ["string", "null"], + "description": "A nullable string field" + }, + "regular_field": { + "type": "number", + "description": "A regular number field" + } + } + }); + let tools = vec![Tool::new("test_tool", "test description", params)]; + let result = format_tools(&tools); + + assert_eq!(result.len(), 1); + assert_eq!(result[0]["name"], "test_tool"); + + // Verify that the type array was converted to a single string type + let nullable_field = &result[0]["parameters"]["properties"]["nullable_field"]; + assert_eq!(nullable_field["type"], "string"); + assert_eq!(nullable_field["description"], "A nullable string field"); + + // Verify that regular types are unchanged + let regular_field = &result[0]["parameters"]["properties"]["regular_field"]; + assert_eq!(regular_field["type"], "number"); + assert_eq!(regular_field["description"], "A regular number field"); + } } diff --git a/crates/goose/src/providers/formats/snowflake.rs b/crates/goose/src/providers/formats/snowflake.rs index 0f355612fe2b..2f8d492ad3ec 100644 --- a/crates/goose/src/providers/formats/snowflake.rs +++ b/crates/goose/src/providers/formats/snowflake.rs @@ -133,7 +133,9 @@ pub fn parse_streaming_response(sse_data: &str) -> Result { continue; } - let json_str = &line[6..]; // Remove "data: " prefix + let Some(json_str) = line.get(6..) else { + continue; + }; // Remove "data: " prefix if json_str.trim().is_empty() || json_str.trim() == "[DONE]" { continue; } diff --git a/crates/goose/src/providers/gemini_cli.rs b/crates/goose/src/providers/gemini_cli.rs index e13610e7dd09..af0016cac5ad 100644 --- a/crates/goose/src/providers/gemini_cli.rs +++ b/crates/goose/src/providers/gemini_cli.rs @@ -8,7 +8,7 @@ use tokio::process::Command; use super::base::{Provider, ProviderMetadata, ProviderUsage, Usage}; use super::errors::ProviderError; -use super::utils::RequestLog; +use super::utils::{filter_extensions_from_system_prompt, RequestLog}; use crate::conversation::message::{Message, MessageContent}; use crate::model::ModelConfig; @@ -103,28 +103,6 @@ impl GeminiCliProvider { None } - /// Filter out the Extensions section from the system prompt - fn filter_extensions_from_system_prompt(&self, system: &str) -> String { - // Find the Extensions section and remove it - if let Some(extensions_start) = system.find("# Extensions") { - // Look for the next major section that starts with # - let after_extensions = &system[extensions_start..]; - if let Some(next_section_pos) = after_extensions[1..].find("\n# ") { - // Found next section, keep everything before Extensions and after the next section - let before_extensions = &system[..extensions_start]; - let next_section_start = extensions_start + next_section_pos + 1; - let after_next_section = &system[next_section_start..]; - format!("{}{}", before_extensions.trim_end(), after_next_section) - } else { - // No next section found, just remove everything from Extensions onward - system[..extensions_start].trim_end().to_string() - } - } else { - // No Extensions section found, return original - system.to_string() - } - } - /// Execute gemini CLI command with simple text prompt async fn execute_command( &self, @@ -135,8 +113,7 @@ impl GeminiCliProvider { // Create a simple prompt combining system + conversation let mut full_prompt = String::new(); - // Add system prompt - let filtered_system = self.filter_extensions_from_system_prompt(system); + let filtered_system = filter_extensions_from_system_prompt(system); full_prompt.push_str(&filtered_system); full_prompt.push_str("\n\n"); diff --git a/crates/goose/src/providers/githubcopilot.rs b/crates/goose/src/providers/githubcopilot.rs index 08a7074c48ba..6119783ec959 100644 --- a/crates/goose/src/providers/githubcopilot.rs +++ b/crates/goose/src/providers/githubcopilot.rs @@ -24,17 +24,31 @@ use crate::model::ModelConfig; use crate::providers::base::ConfigKey; use rmcp::model::Tool; -pub const GITHUB_COPILOT_DEFAULT_MODEL: &str = "gpt-4o"; +pub const GITHUB_COPILOT_DEFAULT_MODEL: &str = "gpt-4.1"; pub const GITHUB_COPILOT_KNOWN_MODELS: &[&str] = &[ + "gpt-4.1", + "gpt-5-mini", + "gpt-5", "gpt-4o", - "o1", - "o3-mini", - "claude-3.7-sonnet", + "grok-code-fast-1", + "gpt-5-codex", "claude-sonnet-4", + "claude-sonnet-4.5", + "claude-haiku-4.5", + "gemini-2.5-pro", ]; -pub const GITHUB_COPILOT_STREAM_MODELS: &[&str] = - &["gpt-4.1", "claude-3.7-sonnet", "claude-sonnet-4"]; +pub const GITHUB_COPILOT_STREAM_MODELS: &[&str] = &[ + "gpt-4.1", + "gpt-5", + "gpt-5-mini", + "gpt-5-codex", + "claude-sonnet-4", + "claude-sonnet-4.5", + "claude-haiku-4.5", + "gemini-2.5-pro", + "grok-code-fast-1", +]; const GITHUB_COPILOT_DOC_URL: &str = "https://docs.github.com/en/copilot/using-github-copilot/ai-models"; @@ -169,7 +183,9 @@ impl GithubCopilotProvider { if !tline.starts_with("data: ") { continue; } - let payload = &tline[6..]; + let Some(payload) = tline.get(6..) else { + continue; + }; if payload == "[DONE]" { break; } @@ -235,7 +251,7 @@ impl GithubCopilotProvider { .get_access_token() .await .context("unable to login into github")?; - config.set_secret("GITHUB_COPILOT_TOKEN", Value::String(token.clone()))?; + config.set_secret("GITHUB_COPILOT_TOKEN", &token)?; token } _ => return Err(err.into()), @@ -500,7 +516,7 @@ impl Provider for GithubCopilotProvider { // Save the token config - .set_secret("GITHUB_COPILOT_TOKEN", Value::String(token)) + .set_secret("GITHUB_COPILOT_TOKEN", &token) .map_err(|e| ProviderError::ExecutionError(format!("Failed to save token: {}", e)))?; Ok(()) diff --git a/crates/goose/src/providers/mod.rs b/crates/goose/src/providers/mod.rs index d50502a42b92..5658bdaff854 100644 --- a/crates/goose/src/providers/mod.rs +++ b/crates/goose/src/providers/mod.rs @@ -1,5 +1,5 @@ pub mod anthropic; -mod api_client; +pub mod api_client; pub mod azure; pub mod azureauth; pub mod base; diff --git a/crates/goose/src/providers/ollama.rs b/crates/goose/src/providers/ollama.rs index 3a180084634b..87f17aabd9ab 100644 --- a/crates/goose/src/providers/ollama.rs +++ b/crates/goose/src/providers/ollama.rs @@ -6,6 +6,7 @@ use super::utils::{ get_model, handle_response_openai_compat, handle_status_openai_compat, RequestLog, }; use crate::config::declarative_providers::DeclarativeProviderConfig; +use crate::config::GooseMode; use crate::conversation::message::Message; use crate::conversation::Conversation; @@ -199,8 +200,12 @@ impl Provider for OllamaProvider { tools: &[Tool], ) -> Result<(Message, ProviderUsage), ProviderError> { let config = crate::config::Config::global(); - let goose_mode = config.get_param("GOOSE_MODE").unwrap_or("auto".to_string()); - let filtered_tools = if goose_mode == "chat" { &[] } else { tools }; + let goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto); + let filtered_tools = if goose_mode == GooseMode::Chat { + &[] + } else { + tools + }; let payload = create_request( &self.model, diff --git a/crates/goose/src/providers/openai.rs b/crates/goose/src/providers/openai.rs index d869d48d2eb4..dca3c0c6bdd7 100644 --- a/crates/goose/src/providers/openai.rs +++ b/crates/goose/src/providers/openai.rs @@ -16,6 +16,7 @@ use super::base::{ConfigKey, ModelInfo, Provider, ProviderMetadata, ProviderUsag use super::embedding::{EmbeddingCapable, EmbeddingRequest, EmbeddingResponse}; use super::errors::ProviderError; use super::formats::openai::{create_request, get_usage, response_to_message}; +use super::retry::ProviderRetry; use super::utils::{ get_model, handle_response_openai_compat, handle_status_openai_compat, ImageFormat, }; @@ -112,6 +113,20 @@ impl OpenAiProvider { }) } + #[doc(hidden)] + pub fn new(api_client: ApiClient, model: ModelConfig) -> Self { + Self { + api_client, + base_path: "v1/chat/completions".to_string(), + organization: None, + project: None, + model, + custom_headers: None, + supports_streaming: true, + name: Self::metadata().name, + } + } + pub fn from_custom_config( model: ModelConfig, config: DeclarativeProviderConfig, @@ -226,9 +241,15 @@ impl Provider for OpenAiProvider { let payload = create_request(model_config, system, messages, tools, &ImageFormat::OpenAi)?; let mut log = RequestLog::start(&self.model, &payload)?; - let json_response = self.post(&payload).await.inspect_err(|e| { - let _ = log.error(e); - })?; + let json_response = self + .with_retry(|| async { + let payload_clone = payload.clone(); + self.post(&payload_clone).await + }) + .await + .inspect_err(|e| { + let _ = log.error(e); + })?; let message = response_to_message(&json_response)?; let usage = json_response @@ -246,19 +267,30 @@ impl Provider for OpenAiProvider { async fn fetch_supported_models(&self) -> Result>, ProviderError> { let models_path = self.base_path.replace("v1/chat/completions", "v1/models"); - let response = self.api_client.response_get(&models_path).await?; - let json = handle_response_openai_compat(response).await?; - if let Some(err_obj) = json.get("error") { - let msg = err_obj - .get("message") - .and_then(|v| v.as_str()) - .unwrap_or("unknown error"); - return Err(ProviderError::Authentication(msg.to_string())); - } + let response = self + .with_retry(|| async { + let response = self.api_client.response_get(&models_path).await?; + let json = handle_response_openai_compat(response).await?; + if let Some(err_obj) = json.get("error") { + let msg = err_obj + .get("message") + .and_then(|v| v.as_str()) + .unwrap_or("unknown error"); + return Err(ProviderError::Authentication(msg.to_string())); + } + Ok(json) + }) + .await + .inspect_err(|e| { + tracing::warn!("Failed to fetch supported models from OpenAI: {:?}", e); + })?; - let data = json.get("data").and_then(|v| v.as_array()).ok_or_else(|| { - ProviderError::UsageError("Missing data field in JSON response".into()) - })?; + let data = response + .get("data") + .and_then(|v| v.as_array()) + .ok_or_else(|| { + ProviderError::UsageError("Missing data field in JSON response".into()) + })?; let mut models: Vec = data .iter() .filter_map(|m| m.get("id").and_then(|v| v.as_str()).map(str::to_string)) @@ -296,17 +328,24 @@ impl Provider for OpenAiProvider { let mut log = RequestLog::start(&self.model, &payload)?; let response = self - .api_client - .response_post(&self.base_path, &payload) - .await - .inspect_err(|e| { - let _ = log.error(e); - })?; - let response = handle_status_openai_compat(response) + .with_retry(|| async { + let resp = self + .api_client + .response_post(&self.base_path, &payload) + .await?; + let status = resp.status(); + if !status.is_success() { + return Err(super::utils::map_http_error_to_provider_error( + status, None, // We'll let handle_status_openai_compat parse the error + )); + } + Ok(resp) + }) .await .inspect_err(|e| { let _ = log.error(e); })?; + let response = handle_status_openai_compat(response).await?; let stream = response.bytes_stream().map_err(io::Error::other); @@ -352,8 +391,18 @@ impl EmbeddingCapable for OpenAiProvider { }; let response = self - .api_client - .api_post("v1/embeddings", &serde_json::to_value(request)?) + .with_retry(|| async { + let request_clone = EmbeddingRequest { + input: request.input.clone(), + model: request.model.clone(), + }; + let request_value = serde_json::to_value(request_clone) + .map_err(|e| ProviderError::ExecutionError(e.to_string()))?; + self.api_client + .api_post("v1/embeddings", &request_value) + .await + .map_err(|e| ProviderError::ExecutionError(e.to_string())) + }) .await?; if response.status != StatusCode::OK { diff --git a/crates/goose/src/providers/sagemaker_tgi.rs b/crates/goose/src/providers/sagemaker_tgi.rs index 5861b09cdd59..74dbff3fa683 100644 --- a/crates/goose/src/providers/sagemaker_tgi.rs +++ b/crates/goose/src/providers/sagemaker_tgi.rs @@ -53,8 +53,8 @@ impl SageMakerTgiProvider { } }; - set_aws_env_vars(config.load_values()); - set_aws_env_vars(config.load_secrets()); + set_aws_env_vars(config.all_values()); + set_aws_env_vars(config.all_secrets()); let aws_config = aws_config::load_from_env().await; @@ -246,7 +246,7 @@ impl SageMakerTgiProvider { // Remove any remaining HTML-like tags using a simple pattern // This is a basic implementation - for production use, consider using a proper HTML parser while let Some(start) = result.find('<') { - if let Some(end) = result[start..].find('>') { + if let Some(end) = result.get(start..).and_then(|s| s.find('>')) { result.replace_range(start..start + end + 1, ""); } else { break; @@ -307,11 +307,11 @@ impl Provider for SageMakerTgiProvider { let message = self.parse_tgi_response(response)?; // TGI doesn't provide usage statistics, so we estimate - let usage = Usage { - input_tokens: Some(0), // Would need to tokenize input to get accurate count - output_tokens: Some(0), // Would need to tokenize output to get accurate count - total_tokens: Some(0), - }; + let usage = Usage::new( + Some(0), // Would need to tokenize input to get accurate count + Some(0), // Would need to tokenize output to get accurate count + Some(0), + ); // Add debug trace let debug_payload = serde_json::json!({ diff --git a/crates/goose/src/providers/utils.rs b/crates/goose/src/providers/utils.rs index 777750415531..1839708fb51c 100644 --- a/crates/goose/src/providers/utils.rs +++ b/crates/goose/src/providers/utils.rs @@ -48,6 +48,31 @@ pub fn convert_image(image: &ImageContent, image_format: &ImageFormat) -> Value } } +pub fn filter_extensions_from_system_prompt(system: &str) -> String { + let Some(extensions_start) = system.find("# Extensions") else { + return system.to_string(); + }; + + let Some(after_extensions) = system.get(extensions_start + 1..) else { + return system.to_string(); + }; + + if let Some(next_section_pos) = after_extensions.find("\n# ") { + let Some(before) = system.get(..extensions_start) else { + return system.to_string(); + }; + let Some(after) = system.get(extensions_start + next_section_pos + 1..) else { + return system.to_string(); + }; + format!("{}{}", before.trim_end(), after) + } else { + system + .get(..extensions_start) + .map(|s| s.trim_end().to_string()) + .unwrap_or_else(|| system.to_string()) + } +} + fn check_context_length_exceeded(text: &str) -> bool { let check_phrases = [ "too long", @@ -68,6 +93,16 @@ fn check_context_length_exceeded(text: &str) -> bool { .any(|phrase| text_lower.contains(phrase)) } +fn format_server_error_message(status_code: StatusCode, payload: Option<&Value>) -> String { + match payload { + Some(Value::Null) | None => format!( + "HTTP {}: No response body received from server", + status_code.as_u16() + ), + Some(p) => format!("HTTP {}: {}", status_code.as_u16(), p), + } +} + pub fn map_http_error_to_provider_error( status: StatusCode, payload: Option, @@ -79,7 +114,7 @@ pub fn map_http_error_to_provider_error( "Authentication failed. Please ensure your API keys are valid and have the required permissions. \ Status: {}{}", status, - payload.as_ref().map(|p| format!(". Response: {:?}", p)).unwrap_or_default() + payload.as_ref().map(|p| format!(". Response: {}", p)).unwrap_or_default() ); ProviderError::Authentication(message) } @@ -116,7 +151,9 @@ pub fn map_http_error_to_provider_error( details: format!("{:?}", payload), retry_delay: None, }, - _ if status.is_server_error() => ProviderError::ServerError(format!("{:?}", payload)), + _ if status.is_server_error() => { + ProviderError::ServerError(format_server_error_message(status, payload.as_ref())) + } _ => ProviderError::RequestFailed(format!("Request failed with status: {}", status)), }; @@ -295,12 +332,9 @@ pub async fn handle_response_google_compat(response: Response) -> Result { - Err(ProviderError::ServerError(format!("{:?}", payload))) - } - StatusCode::INTERNAL_SERVER_ERROR | StatusCode::SERVICE_UNAVAILABLE => { - Err(ProviderError::ServerError(format!("{:?}", payload))) - } + _ if final_status.is_server_error() => Err(ProviderError::ServerError( + format_server_error_message(final_status, payload.as_ref()), + )), _ => { tracing::debug!( "{}", format!("Provider request failed with status: {}. Payload: {:?}", final_status, payload) @@ -463,7 +497,6 @@ impl RequestLog { Payload: Serialize, { let logs_dir = Paths::in_state_dir("logs"); - std::fs::create_dir_all(&logs_dir)?; let request_id = Uuid::new_v4(); let temp_name = format!("llm_request.{request_id}.jsonl"); @@ -1059,15 +1092,13 @@ mod tests { #[test] fn test_map_http_error_to_provider_error() { let test_cases = vec![ - // UNAUTHORIZED/FORBIDDEN - with payload ( StatusCode::UNAUTHORIZED, Some(json!({"error": "auth failed"})), ProviderError::Authentication( - "Authentication failed. Please ensure your API keys are valid and have the required permissions. Status: 401 Unauthorized. Response: Object {\"error\": String(\"auth failed\")}".to_string(), + "Authentication failed. Please ensure your API keys are valid and have the required permissions. Status: 401 Unauthorized. Response: {\"error\":\"auth failed\"}".to_string(), ), ), - // UNAUTHORIZED/FORBIDDEN - without payload ( StatusCode::FORBIDDEN, None, @@ -1075,7 +1106,6 @@ mod tests { "Authentication failed. Please ensure your API keys are valid and have the required permissions. Status: 403 Forbidden".to_string(), ), ), - // BAD_REQUEST - with context_length_exceeded detection ( StatusCode::BAD_REQUEST, Some(json!({"error": {"message": "context_length_exceeded"}})), @@ -1083,7 +1113,6 @@ mod tests { "{\"error\":{\"message\":\"context_length_exceeded\"}}".to_string(), ), ), - // BAD_REQUEST - with error.message extraction ( StatusCode::BAD_REQUEST, Some(json!({"error": {"message": "Custom error"}})), @@ -1091,7 +1120,6 @@ mod tests { "Request failed with status: 400 Bad Request. Message: Custom error".to_string(), ), ), - // BAD_REQUEST - without payload ( StatusCode::BAD_REQUEST, None, @@ -1099,7 +1127,6 @@ mod tests { "Request failed with status: 400 Bad Request".to_string(), ), ), - // TOO_MANY_REQUESTS ( StatusCode::TOO_MANY_REQUESTS, Some(json!({"retry_after": 60})), @@ -1108,17 +1135,29 @@ mod tests { retry_delay: None, }, ), - // is_server_error() without payload ( StatusCode::INTERNAL_SERVER_ERROR, None, - ProviderError::ServerError("None".to_string()), + ProviderError::ServerError(format_server_error_message( + StatusCode::INTERNAL_SERVER_ERROR, + None, + )), + ), + ( + StatusCode::INTERNAL_SERVER_ERROR, + Some(Value::Null), + ProviderError::ServerError(format_server_error_message( + StatusCode::INTERNAL_SERVER_ERROR, + Some(&Value::Null), + )), ), - // is_server_error() with payload ( StatusCode::BAD_GATEWAY, Some(json!({"error": "upstream error"})), - ProviderError::ServerError("Some(Object {\"error\": String(\"upstream error\")})".to_string()), + ProviderError::ServerError(format_server_error_message( + StatusCode::BAD_GATEWAY, + Some(&json!({"error": "upstream error"})), + )), ), // Default - any other status code ( diff --git a/crates/goose/src/providers/utils_universal_openai_stream.rs b/crates/goose/src/providers/utils_universal_openai_stream.rs index 1025e0bf7cd3..38f1aab35e25 100644 --- a/crates/goose/src/providers/utils_universal_openai_stream.rs +++ b/crates/goose/src/providers/utils_universal_openai_stream.rs @@ -275,7 +275,9 @@ data: [DONE] if !line.starts_with("data: ") { continue; } - let payload = &line[6..]; + let Some(payload) = line.get(6..) else { + continue; + }; if payload == "[DONE]" { break; } @@ -325,7 +327,9 @@ data: [DONE] if !line.starts_with("data: ") { continue; } - let payload = &line[6..]; + let Some(payload) = line.get(6..) else { + continue; + }; if payload == "[DONE]" { break; } @@ -377,7 +381,9 @@ data: [DONE] if !line.starts_with("data: ") { continue; } - let payload = &line[6..]; + let Some(payload) = line.get(6..) else { + continue; + }; if payload == "[DONE]" { break; } diff --git a/crates/goose/src/providers/venice.rs b/crates/goose/src/providers/venice.rs index 4a699222d4a5..0698fcea175c 100644 --- a/crates/goose/src/providers/venice.rs +++ b/crates/goose/src/providers/venice.rs @@ -508,11 +508,11 @@ impl Provider for VeniceProvider { // Extract usage let usage_data = &response_json["usage"]; - let usage = Usage { - input_tokens: usage_data["prompt_tokens"].as_i64().map(|v| v as i32), - output_tokens: usage_data["completion_tokens"].as_i64().map(|v| v as i32), - total_tokens: usage_data["total_tokens"].as_i64().map(|v| v as i32), - }; + let usage = Usage::new( + usage_data["prompt_tokens"].as_i64().map(|v| v as i32), + usage_data["completion_tokens"].as_i64().map(|v| v as i32), + usage_data["total_tokens"].as_i64().map(|v| v as i32), + ); Ok(( Message::new(Role::Assistant, Utc::now().timestamp(), content), diff --git a/crates/goose/src/recipe/mod.rs b/crates/goose/src/recipe/mod.rs index 9f6e27ce1127..243b8731e982 100644 --- a/crates/goose/src/recipe/mod.rs +++ b/crates/goose/src/recipe/mod.rs @@ -7,6 +7,7 @@ use std::path::Path; use crate::agents::extension::ExtensionConfig; use crate::agents::types::RetryConfig; use crate::recipe::read_recipe_file_content::read_recipe_file; +use crate::recipe::yaml_format_utils::reformat_fields_with_multiline_values; use crate::utils::contains_unicode_tags; use serde::de::Deserializer; use serde::{Deserialize, Serialize}; @@ -18,6 +19,7 @@ pub mod read_recipe_file_content; mod recipe_extension_adapter; pub mod template_recipe; pub mod validate_recipe; +pub mod yaml_format_utils; pub const BUILT_IN_RECIPE_DIR_PARAM: &str = "recipe_dir"; pub const RECIPE_FILE_EXTENSIONS: &[&str] = &["yaml", "json"]; @@ -51,9 +53,6 @@ pub struct Recipe { )] pub extensions: Option>, // a list of extensions to enable - #[serde(skip_serializing_if = "Option::is_none")] - pub context: Option>, // any additional context - #[serde(skip_serializing_if = "Option::is_none")] pub settings: Option, // settings for the recipe @@ -204,7 +203,6 @@ pub struct RecipeBuilder { // Optional fields prompt: Option, extensions: Option>, - context: Option>, settings: Option, activities: Option>, author: Option, @@ -234,6 +232,14 @@ impl Recipe { false } + pub fn to_yaml(&self) -> Result { + let recipe_yaml = serde_yaml::to_string(self) + .map_err(|err| anyhow::anyhow!("Failed to serialize recipe: {}", err))?; + let formatted_recipe_yaml = + reformat_fields_with_multiline_values(&recipe_yaml, &["prompt", "instructions"]); + Ok(formatted_recipe_yaml) + } + pub fn builder() -> RecipeBuilder { RecipeBuilder { version: default_version(), @@ -242,7 +248,6 @@ impl Recipe { instructions: None, prompt: None, extensions: None, - context: None, settings: None, activities: None, author: None, @@ -317,11 +322,6 @@ impl RecipeBuilder { self } - pub fn context(mut self, context: Vec) -> Self { - self.context = Some(context); - self - } - pub fn settings(mut self, settings: Settings) -> Self { self.settings = Some(settings); self @@ -372,7 +372,6 @@ impl RecipeBuilder { instructions: self.instructions, prompt: self.prompt, extensions: self.extensions, - context: self.context, settings: self.settings, activities: self.activities, author: self.author, @@ -711,7 +710,6 @@ isGlobal: true"#; instructions: Some("clean instructions".to_string()), prompt: Some("clean prompt".to_string()), extensions: None, - context: None, settings: None, activities: Some(vec!["clean activity 1".to_string()]), author: None, diff --git a/crates/goose/src/recipe/read_recipe_file_content.rs b/crates/goose/src/recipe/read_recipe_file_content.rs index 61b740304a04..740e61a2a905 100644 --- a/crates/goose/src/recipe/read_recipe_file_content.rs +++ b/crates/goose/src/recipe/read_recipe_file_content.rs @@ -1,6 +1,8 @@ use anyhow::{anyhow, Result}; use std::fs; use std::path::{Path, PathBuf}; + +#[derive(Clone)] pub struct RecipeFile { pub content: String, pub parent_dir: PathBuf, diff --git a/crates/goose/src/recipe/yaml_format_utils.rs b/crates/goose/src/recipe/yaml_format_utils.rs new file mode 100644 index 000000000000..38773722f6a5 --- /dev/null +++ b/crates/goose/src/recipe/yaml_format_utils.rs @@ -0,0 +1,122 @@ +use std::fmt::Write; + +/// Normalizes how `serde_yaml` outputs multi-line strings. +/// It uses internal heuristics to decide between `|` and quoted text with escaped +/// `\n` and `\"`, and the quoted form breaks MiniJinja parsing. +/// Example before: +/// prompt: "Hello \\\"World\\\"\\n{% if user == \\\"admin\\\" %}Welcome{% endif %}" +/// After fix: +/// prompt: | +/// Hello "World" +/// {% if user == "admin" %}Welcome{% endif %} +pub fn reformat_fields_with_multiline_values(yaml: &str, multiline_fields: &[&str]) -> String { + let mut result = String::new(); + + for line in yaml.lines() { + let trimmed = line.trim_start(); + if trimmed.is_empty() { + writeln!(result).unwrap(); + continue; + } + + let indent = line.len() - trimmed.len(); + let indent_str = " ".repeat(indent); + + let matched_field = multiline_fields + .iter() + .find(|&f| trimmed.starts_with(&format!("{f}: "))); + + if let Some(field) = matched_field { + if let Some((_, raw_val)) = trimmed.split_once(": ") { + if raw_val.contains("\\n") { + // Clean escaped content and unescape quotes + let mut value = raw_val.trim_matches('"').to_string(); + + // Unescape quotes and double backslashes (MiniJinja + newlines) + value = value.replace("\\\"", "\"").replace("\\\\n", "\\n"); + + writeln!(result, "{indent_str}{field}: |").unwrap(); + for l in value.split("\\n") { + writeln!(result, "{indent_str} {l}").unwrap(); + } + continue; + } + } + } + + writeln!(result, "{line}").unwrap(); + } + + let mut output = result.trim_end_matches('\n').to_string(); + output.push('\n'); + output +} + +#[cfg(test)] +mod tests { + use super::reformat_fields_with_multiline_values; + + #[test] + fn keeps_simple_fields_unchanged() { + let yaml = "version: \"1.0\"\ntitle: \"Simple\"\nprompt: \"Hello\""; + let expected = "version: \"1.0\"\ntitle: \"Simple\"\nprompt: \"Hello\"\n"; + + let result = reformat_fields_with_multiline_values(yaml, &["prompt"]); + assert_eq!(result, expected); + } + + #[test] + fn converts_multiline_prompt_to_literal_block() { + let yaml = "version: \"1.0\"\nprompt: \"line1\\\\nline2\""; + let expected = "version: \"1.0\"\nprompt: |\n line1\n line2\n"; + + let result = reformat_fields_with_multiline_values(yaml, &["prompt"]); + assert_eq!(result, expected); + } + + #[test] + fn unescapes_quotes_inside_block() { + let yaml = "prompt: \"Hello \\\"World\\\"\\nHow are you?\""; + let expected = "prompt: |\n Hello \"World\"\n How are you?\n"; + + let result = reformat_fields_with_multiline_values(yaml, &["prompt"]); + assert_eq!(result, expected); + } + + #[test] + fn preserves_unlisted_fields() { + let yaml = "version: \"1.0\"\nprompt: \"line1\\\\nline2\"\nnotes: \"note1\\\\nnote2\""; + let expected = + "version: \"1.0\"\nprompt: |\n line1\n line2\nnotes: \"note1\\\\nnote2\"\n"; + + let result = reformat_fields_with_multiline_values(yaml, &["prompt"]); + assert_eq!(result, expected); + } + + #[test] + fn handles_indented_nested_field() { + let yaml = "settings:\n prompt: \"line1\\\\nline2\""; + let expected = "settings:\n prompt: |\n line1\n line2\n"; + + let result = reformat_fields_with_multiline_values(yaml, &["prompt"]); + assert_eq!(result, expected); + } + + #[test] + fn ignores_existing_literal_blocks() { + let yaml = "prompt: |\n already good\n block"; + let expected = "prompt: |\n already good\n block\n"; + + let result = reformat_fields_with_multiline_values(yaml, &["prompt"]); + assert_eq!(result, expected); + } + + #[test] + fn ignores_fields_without_newlines() { + let yaml = "prompt: \"single line text\""; + let expected = "prompt: \"single line text\"\n"; + + let result = reformat_fields_with_multiline_values(yaml, &["prompt"]); + assert_eq!(result, expected); + } +} diff --git a/crates/goose/src/scheduler.rs b/crates/goose/src/scheduler.rs index 85269ae2fa37..509450502ee5 100644 --- a/crates/goose/src/scheduler.rs +++ b/crates/goose/src/scheduler.rs @@ -21,6 +21,7 @@ use crate::providers::base::Provider as GooseProvider; // Alias to avoid conflic use crate::providers::create; use crate::recipe::Recipe; use crate::scheduler_trait::SchedulerTrait; +use crate::session::session_manager::SessionType; use crate::session::{Session, SessionManager}; // Track running tasks with their abort handles @@ -152,8 +153,6 @@ pub struct ScheduledJob { pub current_session_id: Option, #[serde(default)] pub process_start_time: Option>, - #[serde(default)] - pub execution_mode: Option, // "foreground" or "background" } async fn persist_jobs_from_arc( @@ -1104,7 +1103,7 @@ async fn run_scheduled_job_internal( agent_provider = provider; } else { let global_config = Config::global(); - let provider_name: String = match global_config.get_param("GOOSE_PROVIDER") { + let provider_name: String = match global_config.get_goose_provider() { Ok(name) => name, Err(_) => return Err(JobExecutionError { job_id: job.id.clone(), @@ -1114,7 +1113,7 @@ async fn run_scheduled_job_internal( }), }; let model_name: String = - match global_config.get_param("GOOSE_MODEL") { + match global_config.get_goose_model() { Ok(name) => name, Err(_) => return Err(JobExecutionError { job_id: job.id.clone(), @@ -1160,8 +1159,6 @@ async fn run_scheduled_job_internal( }); } tracing::info!("Agent configured with provider for job '{}'", job.id); - let execution_mode = job.execution_mode.as_deref().unwrap_or("background"); - tracing::info!("Job '{}' running in {} mode", job.id, execution_mode); let current_dir = match std::env::current_dir() { Ok(cd) => cd, @@ -1173,10 +1170,10 @@ async fn run_scheduled_job_internal( } }; - // Create session upfront let session = match SessionManager::create_session( current_dir.clone(), format!("Scheduled job: {}", job.id), + SessionType::Scheduled, ) .await { @@ -1204,21 +1201,21 @@ async fn run_scheduled_job_internal( .or(recipe.instructions.as_ref()) .unwrap(); - let mut conversation = - Conversation::new_unvalidated(vec![Message::user().with_text(prompt_text.clone())]); + let user_message = Message::user().with_text(prompt_text); + let mut conversation = Conversation::new_unvalidated(vec![user_message.clone()]); let session_config = SessionConfig { id: session.id.clone(), - working_dir: current_dir.clone(), schedule_id: Some(job.id.clone()), - execution_mode: job.execution_mode.clone(), max_turns: None, retry_config: None, }; - match agent - .reply(conversation.clone(), Some(session_config.clone()), None) - .await + let session_id = Some(session_config.id.clone()); + match crate::session_context::with_session_id(session_id, async { + agent.reply(user_message, session_config, None).await + }) + .await { Ok(mut stream) => { use futures::StreamExt; @@ -1424,7 +1421,7 @@ mod tests { instructions: None, prompt: Some("This is a test prompt for a scheduled job.".to_string()), extensions: None, - context: None, + activities: None, author: None, parameters: None, @@ -1451,7 +1448,6 @@ mod tests { paused: false, current_session_id: None, process_start_time: None, - execution_mode: Some("background".to_string()), // Default for test }; let mock_model_config = ModelConfig::new_or_fail("test_model"); diff --git a/crates/goose/src/session/mod.rs b/crates/goose/src/session/mod.rs index e15579bb83c4..b8b7c8d5a28c 100644 --- a/crates/goose/src/session/mod.rs +++ b/crates/goose/src/session/mod.rs @@ -6,4 +6,4 @@ pub mod session_manager; pub use diagnostics::generate_diagnostics; pub use extension_data::{EnabledExtensionsState, ExtensionData, ExtensionState, TodoState}; -pub use session_manager::{Session, SessionInsights, SessionManager}; +pub use session_manager::{Session, SessionInsights, SessionManager, SessionType}; diff --git a/crates/goose/src/session/session_manager.rs b/crates/goose/src/session/session_manager.rs index ce97c8ead3f8..2f960cd8497e 100644 --- a/crates/goose/src/session/session_manager.rs +++ b/crates/goose/src/session/session_manager.rs @@ -18,7 +18,47 @@ use tokio::sync::OnceCell; use tracing::{info, warn}; use utoipa::ToSchema; -const CURRENT_SCHEMA_VERSION: i32 = 4; +const CURRENT_SCHEMA_VERSION: i32 = 5; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum SessionType { + User, + Scheduled, + SubAgent, + Hidden, +} + +impl Default for SessionType { + fn default() -> Self { + Self::User + } +} + +impl std::fmt::Display for SessionType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SessionType::User => write!(f, "user"), + SessionType::SubAgent => write!(f, "sub_agent"), + SessionType::Hidden => write!(f, "hidden"), + SessionType::Scheduled => write!(f, "scheduled"), + } + } +} + +impl std::str::FromStr for SessionType { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "user" => Ok(SessionType::User), + "sub_agent" => Ok(SessionType::SubAgent), + "hidden" => Ok(SessionType::Hidden), + "scheduled" => Ok(SessionType::Scheduled), + _ => Err(anyhow::anyhow!("Invalid session type: {}", s)), + } + } +} static SESSION_STORAGE: OnceCell> = OnceCell::const_new(); @@ -27,11 +67,12 @@ pub struct Session { pub id: String, #[schema(value_type = String)] pub working_dir: PathBuf, - // Allow importing session exports from before 'description' was renamed to 'name' #[serde(alias = "description")] pub name: String, #[serde(default)] pub user_set_name: bool, + #[serde(default)] + pub session_type: SessionType, pub created_at: DateTime, pub updated_at: DateTime, pub extension_data: ExtensionData, @@ -52,6 +93,7 @@ pub struct SessionUpdateBuilder { session_id: String, name: Option, user_set_name: Option, + session_type: Option, working_dir: Option, extension_data: Option, total_tokens: Option>, @@ -78,6 +120,7 @@ impl SessionUpdateBuilder { session_id, name: None, user_set_name: None, + session_type: None, working_dir: None, extension_data: None, total_tokens: None, @@ -110,6 +153,11 @@ impl SessionUpdateBuilder { self } + pub fn session_type(mut self, session_type: SessionType) -> Self { + self.session_type = Some(session_type); + self + } + pub fn working_dir(mut self, working_dir: PathBuf) -> Self { self.working_dir = Some(working_dir); self @@ -183,10 +231,14 @@ impl SessionManager { .map(Arc::clone) } - pub async fn create_session(working_dir: PathBuf, name: String) -> Result { + pub async fn create_session( + working_dir: PathBuf, + name: String, + session_type: SessionType, + ) -> Result { Self::instance() .await? - .create_session(working_dir, name) + .create_session(working_dir, name, session_type) .await } @@ -306,6 +358,7 @@ impl Default for Session { working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")), name: String::new(), user_set_name: false, + session_type: SessionType::default(), created_at: Default::default(), updated_at: Default::default(), extension_data: ExtensionData::default(), @@ -353,11 +406,17 @@ impl sqlx::FromRow<'_, sqlx::sqlite::SqliteRow> for Session { let user_set_name = row.try_get("user_set_name").unwrap_or(false); + let session_type_str: String = row + .try_get("session_type") + .unwrap_or_else(|_| "user".to_string()); + let session_type = session_type_str.parse().unwrap_or_default(); + Ok(Session { id: row.try_get("id")?, working_dir: PathBuf::from(row.try_get::("working_dir")?), name, user_set_name, + session_type, created_at: row.try_get("created_at")?, updated_at: row.try_get("updated_at")?, extension_data: serde_json::from_str(&row.try_get::("extension_data")?) @@ -446,6 +505,7 @@ impl SessionStorage { name TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', user_set_name BOOLEAN DEFAULT FALSE, + session_type TEXT NOT NULL DEFAULT 'user', working_dir TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, @@ -491,6 +551,9 @@ impl SessionStorage { sqlx::query("CREATE INDEX idx_sessions_updated ON sessions(updated_at DESC)") .execute(&pool) .await?; + sqlx::query("CREATE INDEX idx_sessions_type ON sessions(session_type)") + .execute(&pool) + .await?; Ok(Self { pool }) } @@ -553,31 +616,32 @@ impl SessionStorage { sqlx::query( r#" INSERT INTO sessions ( - id, name, user_set_name, working_dir, created_at, updated_at, extension_data, + id, name, user_set_name, session_type, working_dir, created_at, updated_at, extension_data, total_tokens, input_tokens, output_tokens, accumulated_total_tokens, accumulated_input_tokens, accumulated_output_tokens, schedule_id, recipe_json, user_recipe_values_json - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "#, ) - .bind(&session.id) - .bind(&session.name) - .bind(session.user_set_name) - .bind(session.working_dir.to_string_lossy().as_ref()) - .bind(session.created_at) - .bind(session.updated_at) - .bind(serde_json::to_string(&session.extension_data)?) - .bind(session.total_tokens) - .bind(session.input_tokens) - .bind(session.output_tokens) - .bind(session.accumulated_total_tokens) - .bind(session.accumulated_input_tokens) - .bind(session.accumulated_output_tokens) - .bind(&session.schedule_id) - .bind(recipe_json) - .bind(user_recipe_values_json) - .execute(&self.pool) - .await?; + .bind(&session.id) + .bind(&session.name) + .bind(session.user_set_name) + .bind(session.session_type.to_string()) + .bind(session.working_dir.to_string_lossy().as_ref()) + .bind(session.created_at) + .bind(session.updated_at) + .bind(serde_json::to_string(&session.extension_data)?) + .bind(session.total_tokens) + .bind(session.input_tokens) + .bind(session.output_tokens) + .bind(session.accumulated_total_tokens) + .bind(session.accumulated_input_tokens) + .bind(session.accumulated_output_tokens) + .bind(&session.schedule_id) + .bind(recipe_json) + .bind(user_recipe_values_json) + .execute(&self.pool) + .await?; if let Some(conversation) = &session.conversation { self.replace_conversation(&session.id, conversation).await?; @@ -687,6 +751,19 @@ impl SessionStorage { .execute(&self.pool) .await?; } + 5 => { + sqlx::query( + r#" + ALTER TABLE sessions ADD COLUMN session_type TEXT NOT NULL DEFAULT 'user' + "#, + ) + .execute(&self.pool) + .await?; + + sqlx::query("CREATE INDEX idx_sessions_type ON sessions(session_type)") + .execute(&self.pool) + .await?; + } _ => { anyhow::bail!("Unknown migration version: {}", version); } @@ -695,11 +772,16 @@ impl SessionStorage { Ok(()) } - async fn create_session(&self, working_dir: PathBuf, name: String) -> Result { + async fn create_session( + &self, + working_dir: PathBuf, + name: String, + session_type: SessionType, + ) -> Result { let today = chrono::Utc::now().format("%Y%m%d").to_string(); Ok(sqlx::query_as( r#" - INSERT INTO sessions (id, name, user_set_name, working_dir, extension_data) + INSERT INTO sessions (id, name, user_set_name, session_type, working_dir, extension_data) VALUES ( ? || '_' || CAST(COALESCE(( SELECT MAX(CAST(SUBSTR(id, 10) AS INTEGER)) @@ -709,23 +791,25 @@ impl SessionStorage { ?, FALSE, ?, + ?, '{}' ) RETURNING * "#, ) - .bind(&today) - .bind(&today) - .bind(&name) - .bind(working_dir.to_string_lossy().as_ref()) - .fetch_one(&self.pool) - .await?) + .bind(&today) + .bind(&today) + .bind(&name) + .bind(session_type.to_string()) + .bind(working_dir.to_string_lossy().as_ref()) + .fetch_one(&self.pool) + .await?) } async fn get_session(&self, id: &str, include_messages: bool) -> Result { let mut session = sqlx::query_as::<_, Session>( r#" - SELECT id, working_dir, name, description, user_set_name, created_at, updated_at, extension_data, + SELECT id, working_dir, name, description, user_set_name, session_type, created_at, updated_at, extension_data, total_tokens, input_tokens, output_tokens, accumulated_total_tokens, accumulated_input_tokens, accumulated_output_tokens, schedule_id, recipe_json, user_recipe_values_json @@ -733,10 +817,10 @@ impl SessionStorage { WHERE id = ? "#, ) - .bind(id) - .fetch_optional(&self.pool) - .await? - .ok_or_else(|| anyhow::anyhow!("Session not found"))?; + .bind(id) + .fetch_optional(&self.pool) + .await? + .ok_or_else(|| anyhow::anyhow!("Session not found"))?; if include_messages { let conv = self.get_conversation(&session.id).await?; @@ -773,6 +857,7 @@ impl SessionStorage { add_update!(builder.name, "name"); add_update!(builder.user_set_name, "user_set_name"); + add_update!(builder.session_type, "session_type"); add_update!(builder.working_dir, "working_dir"); add_update!(builder.extension_data, "extension_data"); add_update!(builder.total_tokens, "total_tokens"); @@ -803,6 +888,9 @@ impl SessionStorage { if let Some(user_set_name) = builder.user_set_name { q = q.bind(user_set_name); } + if let Some(session_type) = builder.session_type { + q = q.bind(session_type.to_string()); + } if let Some(wd) = builder.working_dir { q = q.bind(wd.to_string_lossy().to_string()); } @@ -872,7 +960,6 @@ impl SessionStorage { let mut message = Message::new(role, created_timestamp, content); message.metadata = metadata; - // TODO(Douwe): make id required message = message.with_id(format!("msg_{}_{}", session_id, idx)); messages.push(message); } @@ -942,20 +1029,21 @@ impl SessionStorage { async fn list_sessions(&self) -> Result> { sqlx::query_as::<_, Session>( r#" - SELECT s.id, s.working_dir, s.name, s.description, s.user_set_name, s.created_at, s.updated_at, s.extension_data, + SELECT s.id, s.working_dir, s.name, s.description, s.user_set_name, s.session_type, s.created_at, s.updated_at, s.extension_data, s.total_tokens, s.input_tokens, s.output_tokens, s.accumulated_total_tokens, s.accumulated_input_tokens, s.accumulated_output_tokens, s.schedule_id, s.recipe_json, s.user_recipe_values_json, COUNT(m.id) as message_count FROM sessions s INNER JOIN messages m ON s.id = m.session_id + WHERE s.session_type = 'user' OR s.session_type = 'scheduled' GROUP BY s.id ORDER BY s.updated_at DESC "#, ) - .fetch_all(&self.pool) - .await - .map_err(Into::into) + .fetch_all(&self.pool) + .await + .map_err(Into::into) } async fn delete_session(&self, session_id: &str) -> Result<()> { @@ -1008,7 +1096,11 @@ impl SessionStorage { let import: Session = serde_json::from_str(json)?; let session = self - .create_session(import.working_dir.clone(), import.name.clone()) + .create_session( + import.working_dir.clone(), + import.name.clone(), + import.session_type, + ) .await?; let mut builder = SessionUpdateBuilder::new(session.id.clone()) @@ -1084,7 +1176,7 @@ mod tests { let description = format!("Test session {}", i); let session = session_storage - .create_session(working_dir.clone(), description) + .create_session(working_dir.clone(), description, SessionType::User) .await .unwrap(); @@ -1176,7 +1268,11 @@ mod tests { let storage = Arc::new(SessionStorage::create(&db_path).await.unwrap()); let original = storage - .create_session(PathBuf::from("/tmp/test"), DESCRIPTION.to_string()) + .create_session( + PathBuf::from("/tmp/test"), + DESCRIPTION.to_string(), + SessionType::User, + ) .await .unwrap(); diff --git a/crates/goose/src/session_context.rs b/crates/goose/src/session_context.rs new file mode 100644 index 000000000000..7379e348df62 --- /dev/null +++ b/crates/goose/src/session_context.rs @@ -0,0 +1,80 @@ +use tokio::task_local; + +pub const SESSION_ID_HEADER: &str = "goose-session-id"; + +task_local! { + pub static SESSION_ID: Option; +} + +pub async fn with_session_id(session_id: Option, f: F) -> F::Output +where + F: std::future::Future, +{ + if let Some(id) = session_id { + SESSION_ID.scope(Some(id), f).await + } else { + f.await + } +} + +pub fn current_session_id() -> Option { + SESSION_ID.try_with(|id| id.clone()).ok().flatten() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_session_id_available_when_set() { + with_session_id(Some("test-session-123".to_string()), async { + assert_eq!(current_session_id(), Some("test-session-123".to_string())); + }) + .await; + } + + #[tokio::test] + async fn test_session_id_none_when_not_set() { + let id = current_session_id(); + assert_eq!(id, None); + } + + #[tokio::test] + async fn test_session_id_none_when_explicitly_none() { + with_session_id(None, async { + assert_eq!(current_session_id(), None); + }) + .await; + } + + #[tokio::test] + async fn test_session_id_scoped_correctly() { + assert_eq!(current_session_id(), None); + + with_session_id(Some("outer-session".to_string()), async { + assert_eq!(current_session_id(), Some("outer-session".to_string())); + + with_session_id(Some("inner-session".to_string()), async { + assert_eq!(current_session_id(), Some("inner-session".to_string())); + }) + .await; + + assert_eq!(current_session_id(), Some("outer-session".to_string())); + }) + .await; + + assert_eq!(current_session_id(), None); + } + + #[tokio::test] + async fn test_session_id_across_await_points() { + with_session_id(Some("persistent-session".to_string()), async { + assert_eq!(current_session_id(), Some("persistent-session".to_string())); + + tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; + + assert_eq!(current_session_id(), Some("persistent-session".to_string())); + }) + .await; + } +} diff --git a/crates/goose/src/tool_inspection.rs b/crates/goose/src/tool_inspection.rs index 56f1cdce66ac..2e372c91ae96 100644 --- a/crates/goose/src/tool_inspection.rs +++ b/crates/goose/src/tool_inspection.rs @@ -2,6 +2,7 @@ use anyhow::Result; use async_trait::async_trait; use std::collections::HashMap; +use crate::config::GooseMode; use crate::conversation::message::{Message, ToolRequest}; use crate::permission::permission_inspector::PermissionInspector; use crate::permission::permission_judge::PermissionCheckResult; @@ -116,7 +117,7 @@ impl ToolInspectionManager { } /// Update the permission inspector's mode - pub async fn update_permission_inspector_mode(&self, mode: String) { + pub async fn update_permission_inspector_mode(&self, mode: GooseMode) { for inspector in &self.inspectors { if inspector.name() == "permission" { // Downcast to PermissionInspector to access update_mode method diff --git a/crates/goose/src/tracing/otlp_layer.rs b/crates/goose/src/tracing/otlp_layer.rs index 09d88956b648..b8edd745e125 100644 --- a/crates/goose/src/tracing/otlp_layer.rs +++ b/crates/goose/src/tracing/otlp_layer.rs @@ -1,6 +1,8 @@ use opentelemetry::trace::TracerProvider; use opentelemetry::{global, KeyValue}; +use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; use opentelemetry_otlp::WithExportConfig; +use opentelemetry_sdk::logs::{Logger, LoggerProvider}; use opentelemetry_sdk::trace::{self, RandomIdGenerator, Sampler}; use opentelemetry_sdk::{runtime, Resource}; use std::time::Duration; @@ -11,7 +13,8 @@ use tracing_subscriber::filter::FilterFn; pub type OtlpTracingLayer = OpenTelemetryLayer; pub type OtlpMetricsLayer = MetricsLayer; -pub type OtlpLayers = (OtlpTracingLayer, OtlpMetricsLayer); +pub type OtlpLogsLayer = OpenTelemetryTracingBridge; +pub type OtlpLayers = (OtlpTracingLayer, OtlpMetricsLayer, OtlpLogsLayer); pub type OtlpResult = Result>; #[derive(Debug, Clone)] @@ -163,10 +166,34 @@ pub fn create_otlp_metrics_layer() -> OtlpResult { Ok(tracing_opentelemetry::MetricsLayer::new(meter_provider)) } +pub fn create_otlp_logs_layer() -> OtlpResult> { + let config = OtlpConfig::from_config().ok_or("OTEL_EXPORTER_OTLP_ENDPOINT not configured")?; + + let resource = Resource::new(vec![ + KeyValue::new("service.name", "goose"), + KeyValue::new("service.version", env!("CARGO_PKG_VERSION")), + KeyValue::new("service.namespace", "goose"), + ]); + + let exporter = opentelemetry_otlp::LogExporter::builder() + .with_http() + .with_endpoint(&config.endpoint) + .with_timeout(config.timeout) + .build()?; + + let logger_provider = LoggerProvider::builder() + .with_batch_exporter(exporter, runtime::Tokio) + .with_resource(resource) + .build(); + + Ok(OpenTelemetryTracingBridge::new(&logger_provider)) +} + pub fn init_otlp() -> OtlpResult { let tracing_layer = create_otlp_tracing_layer()?; let metrics_layer = create_otlp_metrics_layer()?; - Ok((tracing_layer, metrics_layer)) + let logs_layer = create_otlp_logs_layer()?; + Ok((tracing_layer, metrics_layer, logs_layer)) } pub fn init_otlp_tracing_only() -> OtlpResult { @@ -221,6 +248,18 @@ pub fn create_otlp_metrics_filter() -> FilterFn) -> bool> }) } +/// Creates a custom filter for OTLP metrics that captures: +/// - All events at WARN level and above +pub fn create_otlp_logs_filter() -> FilterFn) -> bool> { + FilterFn::new(|metadata: &Metadata<'_>| { + if metadata.level() <= &Level::WARN { + return true; + } + + false + }) +} + /// Shutdown OTLP providers gracefully pub fn shutdown_otlp() { // Shutdown the tracer provider and flush any pending spans @@ -262,16 +301,10 @@ mod tests { // Set values in config test_config - .set_param( - "otel_exporter_otlp_endpoint", - serde_json::Value::String("http://config:4318".to_string()), - ) + .set_param("otel_exporter_otlp_endpoint", "http://config:4318") .unwrap(); test_config - .set_param( - "otel_exporter_otlp_timeout", - serde_json::Value::Number(3000.into()), - ) + .set_param("otel_exporter_otlp_timeout", 3000) .unwrap(); // Test that from_config reads from the config file diff --git a/crates/goose/tests/agent.rs b/crates/goose/tests/agent.rs index 77e7359f6987..7478119027fa 100644 --- a/crates/goose/tests/agent.rs +++ b/crates/goose/tests/agent.rs @@ -4,1423 +4,497 @@ use anyhow::Result; use futures::StreamExt; use goose::agents::{Agent, AgentEvent}; use goose::config::extensions::{set_extension, ExtensionEntry}; -use goose::conversation::message::Message; -use goose::conversation::Conversation; -use goose::model::ModelConfig; -use goose::providers::base::Provider; -use goose::providers::{ - anthropic::AnthropicProvider, azure::AzureProvider, bedrock::BedrockProvider, - databricks::DatabricksProvider, gcpvertexai::GcpVertexAIProvider, google::GoogleProvider, - ollama::OllamaProvider, openai::OpenAiProvider, openrouter::OpenRouterProvider, - xai::XaiProvider, -}; - -#[derive(Debug, PartialEq)] -enum ProviderType { - Azure, - OpenAi, - #[allow(dead_code)] - Anthropic, - Bedrock, - Databricks, - GcpVertexAI, - Google, - Ollama, - OpenRouter, - Xai, -} -impl ProviderType { - fn required_env(&self) -> &'static [&'static str] { - match self { - ProviderType::Azure => &[ - "AZURE_OPENAI_API_KEY", - "AZURE_OPENAI_ENDPOINT", - "AZURE_OPENAI_DEPLOYMENT_NAME", - ], - ProviderType::OpenAi => &["OPENAI_API_KEY"], - ProviderType::Anthropic => &["ANTHROPIC_API_KEY"], - ProviderType::Bedrock => &["AWS_PROFILE"], - ProviderType::Databricks => &["DATABRICKS_HOST"], - ProviderType::Google => &["GOOGLE_API_KEY"], - ProviderType::Ollama => &[], - ProviderType::OpenRouter => &["OPENROUTER_API_KEY"], - ProviderType::GcpVertexAI => &["GCP_PROJECT_ID", "GCP_LOCATION"], - ProviderType::Xai => &["XAI_API_KEY"], +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(test)] + mod schedule_tool_tests { + use super::*; + use async_trait::async_trait; + use chrono::{DateTime, Utc}; + use goose::agents::platform_tools::PLATFORM_MANAGE_SCHEDULE_TOOL_NAME; + use goose::scheduler::{ScheduledJob, SchedulerError}; + use goose::scheduler_trait::SchedulerTrait; + use goose::session::Session; + use std::sync::Arc; + + struct MockScheduler { + jobs: tokio::sync::Mutex>, } - } - fn pre_check(&self) -> Result<()> { - match self { - ProviderType::Ollama => { - // Check if the `ollama ls` CLI command works - use std::process::Command; - let output = Command::new("ollama").arg("ls").output(); - if let Ok(output) = output { - if output.status.success() { - return Ok(()); // CLI is running - } + impl MockScheduler { + fn new() -> Self { + Self { + jobs: tokio::sync::Mutex::new(Vec::new()), } - println!("Skipping Ollama tests - `ollama ls` command not found or failed"); - Err(anyhow::anyhow!("Ollama CLI is not running")) } - _ => Ok(()), // Other providers don't need special pre-checks } - } - async fn create_provider(&self, model_config: ModelConfig) -> Result> { - Ok(match self { - ProviderType::Azure => Arc::new(AzureProvider::from_env(model_config).await?), - ProviderType::OpenAi => Arc::new(OpenAiProvider::from_env(model_config).await?), - ProviderType::Anthropic => Arc::new(AnthropicProvider::from_env(model_config).await?), - ProviderType::Bedrock => Arc::new(BedrockProvider::from_env(model_config).await?), - ProviderType::Databricks => Arc::new(DatabricksProvider::from_env(model_config).await?), - ProviderType::GcpVertexAI => { - Arc::new(GcpVertexAIProvider::from_env(model_config).await?) + #[async_trait] + impl SchedulerTrait for MockScheduler { + async fn add_scheduled_job(&self, job: ScheduledJob) -> Result<(), SchedulerError> { + let mut jobs = self.jobs.lock().await; + jobs.push(job); + Ok(()) } - ProviderType::Google => Arc::new(GoogleProvider::from_env(model_config).await?), - ProviderType::Ollama => Arc::new(OllamaProvider::from_env(model_config).await?), - ProviderType::OpenRouter => Arc::new(OpenRouterProvider::from_env(model_config).await?), - ProviderType::Xai => Arc::new(XaiProvider::from_env(model_config).await?), - }) - } -} -pub fn check_required_env_vars(required_vars: &[&str]) -> Result<()> { - let missing_vars: Vec<&str> = required_vars - .iter() - .filter(|&&var| std::env::var(var).is_err()) - .cloned() - .collect(); - - if !missing_vars.is_empty() { - println!( - "Skipping tests. Missing environment variables: {:?}", - missing_vars - ); - return Err(anyhow::anyhow!("Required environment variables not set")); - } - Ok(()) -} + async fn list_scheduled_jobs(&self) -> Result, SchedulerError> { + let jobs = self.jobs.lock().await; + Ok(jobs.clone()) + } -async fn run_truncate_test( - provider_type: ProviderType, - model: &str, - context_window: usize, -) -> Result<()> { - let model_config = ModelConfig::new(model) - .unwrap() - .with_context_limit(Some(context_window)) - .with_temperature(Some(0.0)); - let provider = provider_type.create_provider(model_config).await?; - - let agent = Agent::new(); - agent.update_provider(provider).await?; - let repeat_count = context_window + 10_000; - let large_message_content = "hello ".repeat(repeat_count); - let conversation = Conversation::new(vec![ - Message::user().with_text("hi there. what is 2 + 2?"), - Message::assistant().with_text("hey! I think it's 4."), - Message::user().with_text(&large_message_content), - Message::assistant().with_text("heyy!!"), - Message::user().with_text("what's the meaning of life?"), - Message::assistant().with_text("the meaning of life is 42"), - Message::user().with_text( - "did I ask you what's 2+2 in this message history? just respond with 'yes' or 'no'", - ), - ]) - .unwrap(); - - let reply_stream = agent.reply(conversation, None, None).await?; - tokio::pin!(reply_stream); - - let mut responses = Vec::new(); - while let Some(response_result) = reply_stream.next().await { - match response_result { - Ok(AgentEvent::Message(response)) => responses.push(response), - Ok(AgentEvent::McpNotification(n)) => { - println!("MCP Notification: {n:?}"); + async fn remove_scheduled_job(&self, id: &str) -> Result<(), SchedulerError> { + let mut jobs = self.jobs.lock().await; + if let Some(pos) = jobs.iter().position(|job| job.id == id) { + jobs.remove(pos); + Ok(()) + } else { + Err(SchedulerError::JobNotFound(id.to_string())) + } } - Ok(AgentEvent::ModelChange { .. }) => { - // Model change events are informational, just continue + + async fn pause_schedule(&self, _id: &str) -> Result<(), SchedulerError> { + Ok(()) } - Ok(AgentEvent::HistoryReplaced(_updated_conversation)) => { - // Should update the conversation here, but we're not reading it + + async fn unpause_schedule(&self, _id: &str) -> Result<(), SchedulerError> { + Ok(()) } - Err(e) => { - println!("Error: {:?}", e); - return Err(e); + + async fn run_now(&self, _id: &str) -> Result { + Ok("test_session_123".to_string()) } - } - } - println!("Responses: {responses:?}\n"); - - // Ollama and OpenRouter truncate by default even when the context window is exceeded - // We don't have control over the truncation behavior in these providers. - // Skip the strict assertions for these providers. - if provider_type == ProviderType::Ollama || provider_type == ProviderType::OpenRouter { - println!( - "WARNING: Skipping test for {:?} because it truncates by default when the context window is exceeded", - provider_type - ); - return Ok(()); - } + async fn sessions( + &self, + _sched_id: &str, + _limit: usize, + ) -> Result, SchedulerError> { + Ok(vec![]) + } - assert_eq!(responses.len(), 1); + async fn update_schedule( + &self, + _sched_id: &str, + _new_cron: String, + ) -> Result<(), SchedulerError> { + Ok(()) + } - assert_eq!(responses[0].content.len(), 1); + async fn kill_running_job(&self, _sched_id: &str) -> Result<(), SchedulerError> { + Ok(()) + } - match responses[0].content[0] { - goose::conversation::message::MessageContent::Text(ref text_content) => { - assert!(text_content.text.to_lowercase().contains("no")); - assert!(!text_content.text.to_lowercase().contains("yes")); - } - _ => { - panic!( - "Unexpected message content type: {:?}", - responses[0].content[0] - ); + async fn get_running_job_info( + &self, + _sched_id: &str, + ) -> Result)>, SchedulerError> { + Ok(None) + } } - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - #[derive(Debug)] - struct TestConfig { - provider_type: ProviderType, - model: &'static str, - context_window: usize, - } - - async fn run_test_with_config(config: TestConfig) -> Result<()> { - println!("Starting test for {config:?}"); - - // Check for required environment variables - if check_required_env_vars(config.provider_type.required_env()).is_err() { - return Ok(()); // Skip test if env vars are missing + #[tokio::test] + async fn test_schedule_management_tool_list() { + let agent = Agent::new(); + let mock_scheduler = Arc::new(MockScheduler::new()); + agent.set_scheduler(mock_scheduler.clone()).await; + + // Test that the schedule management tool is available in the tools list + let tools = agent.list_tools(None).await; + let schedule_tool = tools + .iter() + .find(|tool| tool.name == PLATFORM_MANAGE_SCHEDULE_TOOL_NAME); + assert!(schedule_tool.is_some()); + + let tool = schedule_tool.unwrap(); + assert!(tool + .description + .clone() + .unwrap_or_default() + .contains("Manage scheduled recipe execution")); + } + + #[tokio::test] + async fn test_schedule_management_tool_no_scheduler() { + let agent = Agent::new(); + // Don't set scheduler - test that the tool still appears in the list + // but would fail if actually called (which we can't test directly through public API) + + let tools = agent.list_tools(None).await; + let schedule_tool = tools + .iter() + .find(|tool| tool.name == PLATFORM_MANAGE_SCHEDULE_TOOL_NAME); + assert!(schedule_tool.is_some()); + } + + #[tokio::test] + async fn test_schedule_management_tool_in_platform_tools() { + let agent = Agent::new(); + let tools = agent.list_tools(Some("platform".to_string())).await; + + // Check that the schedule management tool is included in platform tools + let schedule_tool = tools + .iter() + .find(|tool| tool.name == PLATFORM_MANAGE_SCHEDULE_TOOL_NAME); + assert!(schedule_tool.is_some()); + + let tool = schedule_tool.unwrap(); + assert!(tool + .description + .clone() + .unwrap_or_default() + .contains("Manage scheduled recipe execution")); + + // Verify the tool has the expected actions in its schema + if let Some(properties) = tool.input_schema.get("properties") { + if let Some(action_prop) = properties.get("action") { + if let Some(enum_values) = action_prop.get("enum") { + let actions: Vec = enum_values + .as_array() + .unwrap() + .iter() + .map(|v| v.as_str().unwrap().to_string()) + .collect(); + + // Check that our session_content action is included + assert!(actions.contains(&"session_content".to_string())); + assert!(actions.contains(&"list".to_string())); + assert!(actions.contains(&"create".to_string())); + assert!(actions.contains(&"sessions".to_string())); + } + } + } } - // Run provider-specific pre-checks - if config.provider_type.pre_check().is_err() { - return Ok(()); // Skip test if pre-check fails + #[tokio::test] + async fn test_schedule_management_tool_schema_validation() { + let agent = Agent::new(); + let tools = agent.list_tools(None).await; + let schedule_tool = tools + .iter() + .find(|tool| tool.name == PLATFORM_MANAGE_SCHEDULE_TOOL_NAME); + assert!(schedule_tool.is_some()); + + let tool = schedule_tool.unwrap(); + + // Verify the tool schema has the session_id parameter for session_content action + if let Some(properties) = tool.input_schema.get("properties") { + assert!(properties.get("session_id").is_some()); + + if let Some(session_id_prop) = properties.get("session_id") { + assert_eq!( + session_id_prop.get("type").unwrap().as_str().unwrap(), + "string" + ); + assert!(session_id_prop + .get("description") + .unwrap() + .as_str() + .unwrap() + .contains("Session identifier for session_content action")); + } + } } - - // Run the truncate test - run_truncate_test(config.provider_type, config.model, config.context_window).await - } - - #[tokio::test] - async fn test_agent_with_openai() -> Result<()> { - run_test_with_config(TestConfig { - provider_type: ProviderType::OpenAi, - model: "o3-mini-low", - context_window: 200_000, - }) - .await - } - - #[tokio::test] - async fn test_agent_with_anthropic() -> Result<()> { - run_test_with_config(TestConfig { - provider_type: ProviderType::Anthropic, - model: "claude-sonnet-4", - context_window: 200_000, - }) - .await } - #[tokio::test] - async fn test_agent_with_azure() -> Result<()> { - run_test_with_config(TestConfig { - provider_type: ProviderType::Azure, - model: "gpt-4o-mini", - context_window: 128_000, - }) - .await - } - - #[tokio::test] - async fn test_agent_with_bedrock() -> Result<()> { - run_test_with_config(TestConfig { - provider_type: ProviderType::Bedrock, - model: "anthropic.claude-sonnet-4-20250514:0", - context_window: 200_000, - }) - .await - } - - #[tokio::test] - async fn test_agent_with_databricks() -> Result<()> { - run_test_with_config(TestConfig { - provider_type: ProviderType::Databricks, - model: "databricks-meta-llama-3-3-70b-instruct", - context_window: 128_000, - }) - .await - } - - #[tokio::test] - async fn test_agent_with_databricks_bedrock() -> Result<()> { - run_test_with_config(TestConfig { - provider_type: ProviderType::Databricks, - model: "claude-sonnet-4", - context_window: 200_000, - }) - .await - } - - #[tokio::test] - async fn test_agent_with_databricks_openai() -> Result<()> { - run_test_with_config(TestConfig { - provider_type: ProviderType::Databricks, - model: "gpt-4o-mini", - context_window: 128_000, - }) - .await - } - - #[tokio::test] - async fn test_agent_with_google() -> Result<()> { - run_test_with_config(TestConfig { - provider_type: ProviderType::Google, - model: "gemini-2.0-flash-exp", - context_window: 1_200_000, - }) - .await - } + #[cfg(test)] + mod retry_tests { + use super::*; + use goose::agents::types::{RetryConfig, SuccessCheck}; - #[tokio::test] - async fn test_agent_with_openrouter() -> Result<()> { - run_test_with_config(TestConfig { - provider_type: ProviderType::OpenRouter, - model: "deepseek/deepseek-r1", - context_window: 130_000, - }) - .await - } + #[tokio::test] + async fn test_retry_success_check_execution() -> Result<()> { + use goose::agents::retry::execute_success_checks; - #[tokio::test] - async fn test_agent_with_ollama() -> Result<()> { - run_test_with_config(TestConfig { - provider_type: ProviderType::Ollama, - model: "llama3.2", - context_window: 128_000, - }) - .await - } + let retry_config = RetryConfig { + max_retries: 3, + checks: vec![], + on_failure: None, + timeout_seconds: Some(30), + on_failure_timeout_seconds: Some(60), + }; - #[tokio::test] - async fn test_agent_with_gcpvertexai() -> Result<()> { - run_test_with_config(TestConfig { - provider_type: ProviderType::GcpVertexAI, - model: "claude-sonnet-4-20250514", - context_window: 200_000, - }) - .await - } + let success_checks = vec![SuccessCheck::Shell { + command: "echo 'test'".to_string(), + }]; - #[tokio::test] - async fn test_agent_with_xai() -> Result<()> { - run_test_with_config(TestConfig { - provider_type: ProviderType::Xai, - model: "grok-3", - context_window: 9_000, - }) - .await - } -} + let result = execute_success_checks(&success_checks, &retry_config).await; + assert!(result.is_ok(), "Success check should pass"); + assert!(result.unwrap(), "Command should succeed"); -#[cfg(test)] -mod schedule_tool_tests { - use super::*; - use async_trait::async_trait; - use chrono::{DateTime, Utc}; - use goose::agents::platform_tools::PLATFORM_MANAGE_SCHEDULE_TOOL_NAME; - use goose::scheduler::{ScheduledJob, SchedulerError}; - use goose::scheduler_trait::SchedulerTrait; - use goose::session::Session; - use std::sync::Arc; - - struct MockScheduler { - jobs: tokio::sync::Mutex>, - } + let fail_checks = vec![SuccessCheck::Shell { + command: "false".to_string(), + }]; - impl MockScheduler { - fn new() -> Self { - Self { - jobs: tokio::sync::Mutex::new(Vec::new()), - } - } - } + let result = execute_success_checks(&fail_checks, &retry_config).await; + assert!(result.is_ok(), "Success check execution should not error"); + assert!(!result.unwrap(), "Command should fail"); - #[async_trait] - impl SchedulerTrait for MockScheduler { - async fn add_scheduled_job(&self, job: ScheduledJob) -> Result<(), SchedulerError> { - let mut jobs = self.jobs.lock().await; - jobs.push(job); Ok(()) } - async fn list_scheduled_jobs(&self) -> Result, SchedulerError> { - let jobs = self.jobs.lock().await; - Ok(jobs.clone()) - } + #[tokio::test] + async fn test_retry_logic_with_validation_errors() -> Result<()> { + let invalid_retry_config = RetryConfig { + max_retries: 0, + checks: vec![], + on_failure: None, + timeout_seconds: Some(0), + on_failure_timeout_seconds: None, + }; - async fn remove_scheduled_job(&self, id: &str) -> Result<(), SchedulerError> { - let mut jobs = self.jobs.lock().await; - if let Some(pos) = jobs.iter().position(|job| job.id == id) { - jobs.remove(pos); - Ok(()) - } else { - Err(SchedulerError::JobNotFound(id.to_string())) - } - } + let validation_result = invalid_retry_config.validate(); + assert!( + validation_result.is_err(), + "Should validate max_retries > 0" + ); + assert!(validation_result + .unwrap_err() + .contains("max_retries must be greater than 0")); - async fn pause_schedule(&self, _id: &str) -> Result<(), SchedulerError> { Ok(()) } - async fn unpause_schedule(&self, _id: &str) -> Result<(), SchedulerError> { - Ok(()) - } + #[tokio::test] + async fn test_retry_attempts_counter_reset() -> Result<()> { + let agent = Agent::new(); - async fn run_now(&self, _id: &str) -> Result { - Ok("test_session_123".to_string()) - } + agent.reset_retry_attempts().await; + let initial_attempts = agent.get_retry_attempts().await; + assert_eq!(initial_attempts, 0); - async fn sessions( - &self, - _sched_id: &str, - _limit: usize, - ) -> Result, SchedulerError> { - Ok(vec![]) - } + let new_attempts = agent.increment_retry_attempts().await; + assert_eq!(new_attempts, 1); - async fn update_schedule( - &self, - _sched_id: &str, - _new_cron: String, - ) -> Result<(), SchedulerError> { - Ok(()) - } + agent.reset_retry_attempts().await; + let reset_attempts = agent.get_retry_attempts().await; + assert_eq!(reset_attempts, 0); - async fn kill_running_job(&self, _sched_id: &str) -> Result<(), SchedulerError> { Ok(()) } - - async fn get_running_job_info( - &self, - _sched_id: &str, - ) -> Result)>, SchedulerError> { - Ok(None) - } - } - - #[tokio::test] - async fn test_schedule_management_tool_list() { - let agent = Agent::new(); - let mock_scheduler = Arc::new(MockScheduler::new()); - agent.set_scheduler(mock_scheduler.clone()).await; - - // Test that the schedule management tool is available in the tools list - let tools = agent.list_tools(None).await; - let schedule_tool = tools - .iter() - .find(|tool| tool.name == PLATFORM_MANAGE_SCHEDULE_TOOL_NAME); - assert!(schedule_tool.is_some()); - - let tool = schedule_tool.unwrap(); - assert!(tool - .description - .clone() - .unwrap_or_default() - .contains("Manage scheduled recipe execution")); - } - - #[tokio::test] - async fn test_schedule_management_tool_no_scheduler() { - let agent = Agent::new(); - // Don't set scheduler - test that the tool still appears in the list - // but would fail if actually called (which we can't test directly through public API) - - let tools = agent.list_tools(None).await; - let schedule_tool = tools - .iter() - .find(|tool| tool.name == PLATFORM_MANAGE_SCHEDULE_TOOL_NAME); - assert!(schedule_tool.is_some()); - } - - #[tokio::test] - async fn test_schedule_management_tool_in_platform_tools() { - let agent = Agent::new(); - let tools = agent.list_tools(Some("platform".to_string())).await; - - // Check that the schedule management tool is included in platform tools - let schedule_tool = tools - .iter() - .find(|tool| tool.name == PLATFORM_MANAGE_SCHEDULE_TOOL_NAME); - assert!(schedule_tool.is_some()); - - let tool = schedule_tool.unwrap(); - assert!(tool - .description - .clone() - .unwrap_or_default() - .contains("Manage scheduled recipe execution")); - - // Verify the tool has the expected actions in its schema - if let Some(properties) = tool.input_schema.get("properties") { - if let Some(action_prop) = properties.get("action") { - if let Some(enum_values) = action_prop.get("enum") { - let actions: Vec = enum_values - .as_array() - .unwrap() - .iter() - .map(|v| v.as_str().unwrap().to_string()) - .collect(); - - // Check that our session_content action is included - assert!(actions.contains(&"session_content".to_string())); - assert!(actions.contains(&"list".to_string())); - assert!(actions.contains(&"create".to_string())); - assert!(actions.contains(&"sessions".to_string())); - } - } - } - } - - #[tokio::test] - async fn test_schedule_management_tool_schema_validation() { - let agent = Agent::new(); - let tools = agent.list_tools(None).await; - let schedule_tool = tools - .iter() - .find(|tool| tool.name == PLATFORM_MANAGE_SCHEDULE_TOOL_NAME); - assert!(schedule_tool.is_some()); - - let tool = schedule_tool.unwrap(); - - // Verify the tool schema has the session_id parameter for session_content action - if let Some(properties) = tool.input_schema.get("properties") { - assert!(properties.get("session_id").is_some()); - - if let Some(session_id_prop) = properties.get("session_id") { - assert_eq!( - session_id_prop.get("type").unwrap().as_str().unwrap(), - "string" - ); - assert!(session_id_prop - .get("description") - .unwrap() - .as_str() - .unwrap() - .contains("Session identifier for session_content action")); - } - } } -} -#[cfg(test)] -mod final_output_tool_tests { - use super::*; - use futures::stream; - use goose::agents::final_output_tool::FINAL_OUTPUT_TOOL_NAME; - use goose::conversation::Conversation; - use goose::providers::base::MessageStream; - use goose::recipe::Response; - use rmcp::model::CallToolRequestParam; - use rmcp::object; - - #[tokio::test] - async fn test_final_output_assistant_message_in_reply() -> Result<()> { + #[cfg(test)] + mod max_turns_tests { + use super::*; use async_trait::async_trait; - use goose::conversation::message::Message; + use goose::agents::SessionConfig; + use goose::conversation::message::{Message, MessageContent}; use goose::model::ModelConfig; - use goose::providers::base::{Provider, ProviderUsage, Usage}; + use goose::providers::base::{Provider, ProviderMetadata, ProviderUsage, Usage}; use goose::providers::errors::ProviderError; - use rmcp::model::Tool; - - #[derive(Clone)] - struct MockProvider { - model_config: ModelConfig, - } - - #[async_trait] - impl Provider for MockProvider { - fn metadata() -> goose::providers::base::ProviderMetadata { - goose::providers::base::ProviderMetadata::empty() - } - - fn get_name(&self) -> &str { - "mock-test" - } - - fn get_model_config(&self) -> ModelConfig { - self.model_config.clone() - } - - async fn complete( - &self, - _system: &str, - _messages: &[Message], - _tools: &[Tool], - ) -> anyhow::Result<(Message, ProviderUsage), ProviderError> { - Ok(( - Message::assistant().with_text("Task completed."), - ProviderUsage::new("mock".to_string(), Usage::default()), - )) - } - - async fn complete_with_model( - &self, - _model_config: &ModelConfig, - system: &str, - messages: &[Message], - tools: &[Tool], - ) -> anyhow::Result<(Message, ProviderUsage), ProviderError> { - self.complete(system, messages, tools).await - } - } - - let agent = Agent::new(); + use goose::session::session_manager::SessionType; + use goose::session::SessionManager; + use rmcp::model::{CallToolRequestParam, Tool}; + use rmcp::object; + use std::path::PathBuf; - let model_config = ModelConfig::new("test-model").unwrap(); - let mock_provider = Arc::new(MockProvider { model_config }); - agent.update_provider(mock_provider).await?; + struct MockToolProvider {} - let response = Response { - json_schema: Some(serde_json::json!({ - "type": "object", - "properties": { - "result": {"type": "string"} - }, - "required": ["result"] - })), - }; - agent.add_final_output_tool(response).await; - - // Simulate a final output tool call occurring. - let tool_call = CallToolRequestParam { - name: FINAL_OUTPUT_TOOL_NAME.into(), - arguments: Some(object!({ - "result": "Test output" - })), - }; - - let (_, result) = agent - .dispatch_tool_call(tool_call, "request_id".to_string(), None, None) - .await; - - assert!(result.is_ok(), "Tool call should succeed"); - let final_result = result.unwrap().result.await; - assert!(final_result.is_ok(), "Tool execution should succeed"); - - let content = final_result.unwrap(); - let text = content.first().unwrap().as_text().unwrap(); - assert!( - text.text.contains("Final output successfully collected."), - "Tool result missing expected content: {}", - text.text - ); - - // Simulate the reply stream continuing after the final output tool call. - let reply_stream = agent.reply(Conversation::empty(), None, None).await?; - tokio::pin!(reply_stream); - - let mut responses = Vec::new(); - while let Some(response_result) = reply_stream.next().await { - match response_result { - Ok(AgentEvent::Message(response)) => responses.push(response), - Ok(_) => {} - Err(e) => return Err(e), + impl MockToolProvider { + fn new() -> Self { + Self {} } } - assert!(!responses.is_empty(), "Should have received responses"); - let last_message = responses.last().unwrap(); - - // Check that the last message is an assistant message with our final output - assert_eq!(last_message.role, rmcp::model::Role::Assistant); - let message_text = last_message.as_concat_text(); - assert_eq!(message_text, r#"{"result":"Test output"}"#); - - Ok(()) - } - - #[tokio::test] - async fn test_when_final_output_not_called_in_reply() -> Result<()> { - use async_trait::async_trait; - use goose::agents::final_output_tool::FINAL_OUTPUT_CONTINUATION_MESSAGE; - use goose::conversation::message::Message; - use goose::model::ModelConfig; - use goose::providers::base::{Provider, ProviderUsage}; - use goose::providers::errors::ProviderError; - use rmcp::model::Tool; - - #[derive(Clone)] - struct MockProvider { - model_config: ModelConfig, - stream_round: std::sync::Arc>, - got_continuation_message: std::sync::Arc>, - } - #[async_trait] - impl Provider for MockProvider { - fn metadata() -> goose::providers::base::ProviderMetadata { - goose::providers::base::ProviderMetadata::empty() - } - - fn get_name(&self) -> &str { - "mock-test" - } - - fn get_model_config(&self) -> ModelConfig { - self.model_config.clone() - } - - fn supports_streaming(&self) -> bool { - true - } - - async fn stream( + impl Provider for MockToolProvider { + async fn complete( &self, - _system: &str, + _system_prompt: &str, _messages: &[Message], _tools: &[Tool], - ) -> Result { - if let Some(last_msg) = _messages.last() { - for content in &last_msg.content { - if let goose::conversation::message::MessageContent::Text(text_content) = - content - { - if text_content.text == FINAL_OUTPUT_CONTINUATION_MESSAGE { - let mut got_continuation = - self.got_continuation_message.lock().unwrap(); - *got_continuation = true; - } - } - } - } - - let mut round = self.stream_round.lock().unwrap(); - *round += 1; - - let deltas = if *round == 1 { - vec![ - Ok((Some(Message::assistant().with_text("Hello")), None)), - Ok((Some(Message::assistant().with_text("Hi!")), None)), - Ok(( - Some(Message::assistant().with_text("What is the final output?")), - None, - )), - ] - } else { - vec![Ok(( - Some(Message::assistant().with_text("Additional random delta")), - None, - ))] + ) -> Result<(Message, ProviderUsage), ProviderError> { + let tool_call = CallToolRequestParam { + name: "test_tool".into(), + arguments: Some(object!({"param": "value"})), }; + let message = Message::assistant().with_tool_request("call_123", Ok(tool_call)); - let stream = stream::iter(deltas.into_iter()); - Ok(Box::pin(stream)) - } + let usage = ProviderUsage::new( + "mock-model".to_string(), + Usage::new(Some(10), Some(5), Some(15)), + ); - async fn complete( - &self, - _system: &str, - _messages: &[Message], - _tools: &[Tool], - ) -> Result<(Message, ProviderUsage), ProviderError> { - Err(ProviderError::NotImplemented("Not implemented".to_string())) + Ok((message, usage)) } async fn complete_with_model( &self, _model_config: &ModelConfig, - system: &str, + system_prompt: &str, messages: &[Message], tools: &[Tool], ) -> anyhow::Result<(Message, ProviderUsage), ProviderError> { - self.complete(system, messages, tools).await + self.complete(system_prompt, messages, tools).await } - } - let agent = Agent::new(); - - let model_config = ModelConfig::new("test-model").unwrap(); - let mock_provider = Arc::new(MockProvider { - model_config, - stream_round: std::sync::Arc::new(std::sync::Mutex::new(0)), - got_continuation_message: std::sync::Arc::new(std::sync::Mutex::new(false)), - }); - let mock_provider_clone = mock_provider.clone(); - agent.update_provider(mock_provider).await?; - - let response = Response { - json_schema: Some(serde_json::json!({ - "type": "object", - "properties": { - "result": {"type": "string"} - }, - "required": ["result"] - })), - }; - agent.add_final_output_tool(response).await; - - // Simulate the reply stream being called. - let reply_stream = agent.reply(Conversation::empty(), None, None).await?; - tokio::pin!(reply_stream); - - let mut responses = Vec::new(); - let mut count = 0; - while let Some(response_result) = reply_stream.next().await { - match response_result { - Ok(AgentEvent::Message(response)) => { - responses.push(response); - count += 1; - if count >= 4 { - // Limit to 4 messages to avoid infinite loop due to mock provider - break; - } - } - Ok(_) => {} - Err(e) => return Err(e), + fn get_model_config(&self) -> ModelConfig { + ModelConfig::new("mock-model").unwrap() } - } - - assert!(!responses.is_empty(), "Should have received responses"); - let last_message = responses.last().unwrap(); - - // Check that the first 3 messages do not have FINAL_OUTPUT_CONTINUATION_MESSAGE - for (i, response) in responses.iter().take(3).enumerate() { - let message_text = response.as_concat_text(); - assert_ne!( - message_text, - FINAL_OUTPUT_CONTINUATION_MESSAGE, - "Message {} should not be the continuation message, got: '{}'", - i + 1, - message_text - ); - } - - // Check that the last message after the llm stream is the message directing the agent to continue - assert_eq!(last_message.role, rmcp::model::Role::User); - let message_text = last_message.as_concat_text(); - assert_eq!(message_text, FINAL_OUTPUT_CONTINUATION_MESSAGE); - // Continue streaming to consume any remaining content, this lets us verify the provider saw the continuation message - while let Some(response_result) = reply_stream.next().await { - match response_result { - Ok(AgentEvent::Message(_response)) => { - break; // Stop after receiving the next message - } - Ok(_) => {} - Err(e) => { - println!("Error while streaming remaining content: {:?}", e); - break; + fn metadata() -> ProviderMetadata { + ProviderMetadata { + name: "mock".to_string(), + display_name: "Mock Provider".to_string(), + description: "Mock provider for testing".to_string(), + default_model: "mock-model".to_string(), + known_models: vec![], + model_doc_link: "".to_string(), + config_keys: vec![], } } - } - - // Assert that the provider received the continuation message - let got_continuation = mock_provider_clone.got_continuation_message.lock().unwrap(); - assert!( - *got_continuation, - "Provider should have received the FINAL_OUTPUT_CONTINUATION_MESSAGE" - ); - - Ok(()) - } -} - -#[cfg(test)] -mod retry_tests { - use super::*; - use async_trait::async_trait; - use goose::agents::types::{RetryConfig, SuccessCheck}; - use goose::conversation::message::Message; - use goose::conversation::Conversation; - use goose::model::ModelConfig; - use goose::providers::base::{Provider, ProviderUsage, Usage}; - use goose::providers::errors::ProviderError; - use rmcp::model::Tool; - use std::sync::atomic::{AtomicUsize, Ordering}; - use std::sync::Arc; - - #[derive(Clone)] - struct MockRetryProvider { - model_config: ModelConfig, - call_count: Arc, - fail_until: usize, - } - - #[async_trait] - impl Provider for MockRetryProvider { - fn metadata() -> goose::providers::base::ProviderMetadata { - goose::providers::base::ProviderMetadata::empty() - } - - fn get_name(&self) -> &str { - "mock-test" - } - - fn get_model_config(&self) -> ModelConfig { - self.model_config.clone() - } - - async fn complete( - &self, - _system: &str, - _messages: &[Message], - _tools: &[Tool], - ) -> anyhow::Result<(Message, ProviderUsage), ProviderError> { - let count = self.call_count.fetch_add(1, Ordering::SeqCst); - - if count < self.fail_until { - Ok(( - Message::assistant().with_text("Task failed - will retry."), - ProviderUsage::new("mock".to_string(), Usage::default()), - )) - } else { - Ok(( - Message::assistant().with_text("Task completed successfully."), - ProviderUsage::new("mock".to_string(), Usage::default()), - )) - } - } - - async fn complete_with_model( - &self, - _model_config: &ModelConfig, - system: &str, - messages: &[Message], - tools: &[Tool], - ) -> anyhow::Result<(Message, ProviderUsage), ProviderError> { - self.complete(system, messages, tools).await - } - } - #[tokio::test] - async fn test_retry_config_validation_integration() -> Result<()> { - let agent = Agent::new(); - - let model_config = ModelConfig::new("test-model").unwrap(); - let mock_provider = Arc::new(MockRetryProvider { - model_config, - call_count: Arc::new(AtomicUsize::new(0)), - fail_until: 0, - }); - agent.update_provider(mock_provider.clone()).await?; - - let retry_config = RetryConfig { - max_retries: 3, - checks: vec![SuccessCheck::Shell { - command: "echo 'success check'".to_string(), - }], - on_failure: Some("echo 'cleanup executed'".to_string()), - timeout_seconds: Some(30), - on_failure_timeout_seconds: Some(60), - }; - - assert!( - retry_config.validate().is_ok(), - "Valid config should pass validation" - ); - - let conversation = - Conversation::new(vec![Message::user().with_text("Complete this task")]).unwrap(); - - let reply_stream = agent.reply(conversation, None, None).await?; - tokio::pin!(reply_stream); - - let mut responses = Vec::new(); - while let Some(response_result) = reply_stream.next().await { - match response_result { - Ok(AgentEvent::Message(response)) => responses.push(response), - Ok(_) => {} - Err(e) => return Err(e), + fn get_name(&self) -> &str { + "mock-test" } } - assert!(!responses.is_empty(), "Should have received responses"); - - Ok(()) - } - - #[tokio::test] - async fn test_retry_success_check_execution() -> Result<()> { - use goose::agents::retry::execute_success_checks; - - let retry_config = RetryConfig { - max_retries: 3, - checks: vec![], - on_failure: None, - timeout_seconds: Some(30), - on_failure_timeout_seconds: Some(60), - }; - - let success_checks = vec![SuccessCheck::Shell { - command: "echo 'test'".to_string(), - }]; - - let result = execute_success_checks(&success_checks, &retry_config).await; - assert!(result.is_ok(), "Success check should pass"); - assert!(result.unwrap(), "Command should succeed"); - - let fail_checks = vec![SuccessCheck::Shell { - command: "false".to_string(), - }]; - - let result = execute_success_checks(&fail_checks, &retry_config).await; - assert!(result.is_ok(), "Success check execution should not error"); - assert!(!result.unwrap(), "Command should fail"); - - Ok(()) - } - - #[tokio::test] - async fn test_retry_logic_with_validation_errors() -> Result<()> { - let invalid_retry_config = RetryConfig { - max_retries: 0, - checks: vec![], - on_failure: None, - timeout_seconds: Some(0), - on_failure_timeout_seconds: None, - }; - - let validation_result = invalid_retry_config.validate(); - assert!( - validation_result.is_err(), - "Should validate max_retries > 0" - ); - assert!(validation_result - .unwrap_err() - .contains("max_retries must be greater than 0")); - - Ok(()) - } - - #[tokio::test] - async fn test_retry_attempts_counter_reset() -> Result<()> { - let agent = Agent::new(); - - agent.reset_retry_attempts().await; - let initial_attempts = agent.get_retry_attempts().await; - assert_eq!(initial_attempts, 0); - - let new_attempts = agent.increment_retry_attempts().await; - assert_eq!(new_attempts, 1); - - agent.reset_retry_attempts().await; - let reset_attempts = agent.get_retry_attempts().await; - assert_eq!(reset_attempts, 0); - - Ok(()) - } -} - -#[cfg(test)] -mod max_turns_tests { - use super::*; - use async_trait::async_trait; - use goose::conversation::message::{Message, MessageContent}; - use goose::conversation::Conversation; - use goose::model::ModelConfig; - use goose::providers::base::{Provider, ProviderMetadata, ProviderUsage, Usage}; - use goose::providers::errors::ProviderError; - use rmcp::model::{CallToolRequestParam, Tool}; - use rmcp::object; - - struct MockToolProvider {} - - impl MockToolProvider { - fn new() -> Self { - Self {} - } - } - - #[async_trait] - impl Provider for MockToolProvider { - async fn complete( - &self, - _system_prompt: &str, - _messages: &[Message], - _tools: &[Tool], - ) -> Result<(Message, ProviderUsage), ProviderError> { - let tool_call = CallToolRequestParam { - name: "test_tool".into(), - arguments: Some(object!({"param": "value"})), + #[tokio::test] + async fn test_max_turns_limit() -> Result<()> { + let agent = Agent::new(); + let provider = Arc::new(MockToolProvider::new()); + agent.update_provider(provider).await?; + let user_message = Message::user().with_text("Hello"); + + let session = SessionManager::create_session( + PathBuf::default(), + "max-turn-test".to_string(), + SessionType::Hidden, + ) + .await?; + let session_config = SessionConfig { + id: session.id, + schedule_id: None, + max_turns: None, + retry_config: None, }; - let message = Message::assistant().with_tool_request("call_123", Ok(tool_call)); - - let usage = ProviderUsage::new( - "mock-model".to_string(), - Usage::new(Some(10), Some(5), Some(15)), - ); - - Ok((message, usage)) - } - async fn complete_with_model( - &self, - _model_config: &ModelConfig, - system_prompt: &str, - messages: &[Message], - tools: &[Tool], - ) -> anyhow::Result<(Message, ProviderUsage), ProviderError> { - self.complete(system_prompt, messages, tools).await - } - - fn get_model_config(&self) -> ModelConfig { - ModelConfig::new("mock-model").unwrap() - } - - fn metadata() -> ProviderMetadata { - ProviderMetadata { - name: "mock".to_string(), - display_name: "Mock Provider".to_string(), - description: "Mock provider for testing".to_string(), - default_model: "mock-model".to_string(), - known_models: vec![], - model_doc_link: "".to_string(), - config_keys: vec![], - } - } + let reply_stream = agent.reply(user_message, session_config, None).await?; + tokio::pin!(reply_stream); - fn get_name(&self) -> &str { - "mock-test" - } - } - - #[tokio::test] - async fn test_max_turns_limit() -> Result<()> { - let agent = Agent::new(); - let provider = Arc::new(MockToolProvider::new()); - agent.update_provider(provider).await?; - // The mock provider will call a non-existent tool, which will fail and allow the loop to continue - let conversation = Conversation::new(vec![Message::user().with_text("Hello")]).unwrap(); - - let reply_stream = agent.reply(conversation, None, None).await?; - tokio::pin!(reply_stream); - - let mut responses = Vec::new(); - while let Some(response_result) = reply_stream.next().await { - match response_result { - Ok(AgentEvent::Message(response)) => { - if let Some(MessageContent::ToolConfirmationRequest(ref req)) = - response.content.first() - { - agent.handle_confirmation( + let mut responses = Vec::new(); + while let Some(response_result) = reply_stream.next().await { + match response_result { + Ok(AgentEvent::Message(response)) => { + if let Some(MessageContent::ToolConfirmationRequest(ref req)) = + response.content.first() + { + agent.handle_confirmation( req.id.clone(), goose::permission::PermissionConfirmation { principal_type: goose::permission::permission_confirmation::PrincipalType::Tool, permission: goose::permission::Permission::AllowOnce, } ).await; + } + responses.push(response); + } + Ok(AgentEvent::McpNotification(_)) => {} + Ok(AgentEvent::ModelChange { .. }) => {} + Ok(AgentEvent::HistoryReplaced(_updated_conversation)) => { + // We should update the conversation here, but we're not reading it + } + Err(e) => { + return Err(e); } - responses.push(response); - } - Ok(AgentEvent::McpNotification(_)) => {} - Ok(AgentEvent::ModelChange { .. }) => {} - Ok(AgentEvent::HistoryReplaced(_updated_conversation)) => { - // We should update the conversation here, but we're not reading it - } - Err(e) => { - return Err(e); } } - } - - assert!( - !responses.is_empty(), - "Expected at least 1 response, got {}", - responses.len() - ); - - // Look for the max turns message as the last response - let last_response = responses.last().unwrap(); - let last_content = last_response.content.first().unwrap(); - if let MessageContent::Text(text_content) = last_content { - assert!(text_content.text.contains( - "I've reached the maximum number of actions I can do without user input" - )); - } else { - panic!("Expected text content in last message"); - } - Ok(()) - } -} -#[cfg(test)] -mod extension_manager_tests { - use super::*; - use goose::agents::extension::{ExtensionConfig, PlatformExtensionContext}; - use goose::agents::extension_manager_extension::{ - MANAGE_EXTENSIONS_TOOL_NAME, SEARCH_AVAILABLE_EXTENSIONS_TOOL_NAME, - }; - use rmcp::model::CallToolRequestParam; - use rmcp::object; - - async fn setup_agent_with_extension_manager() -> Agent { - // Add the TODO extension to the config so it can be discovered by search_available_extensions - // Set it as disabled initially so tests can enable it - let todo_extension_entry = ExtensionEntry { - enabled: false, - config: ExtensionConfig::Platform { - name: "todo".to_string(), - description: - "Enable a todo list for Goose so it can keep track of what it is doing" - .to_string(), - bundled: Some(true), - available_tools: vec![], - }, - }; - set_extension(todo_extension_entry); - - let agent = Agent::new(); - - agent - .extension_manager - .set_context(PlatformExtensionContext { - session_id: Some("test_session".to_string()), - extension_manager: Some(Arc::downgrade(&agent.extension_manager)), - tool_route_manager: Some(Arc::downgrade(&agent.tool_route_manager)), - }) - .await; - - // Now add the extension manager platform extension - let ext_config = ExtensionConfig::Platform { - name: "extensionmanager".to_string(), - description: "Extension Manager".to_string(), - bundled: Some(true), - available_tools: vec![], - }; - - agent - .add_extension(ext_config) - .await - .expect("Failed to add extension manager"); - agent - } - - #[tokio::test] - async fn test_extension_manager_tools_available() { - let agent = setup_agent_with_extension_manager().await; - let tools = agent.list_tools(None).await; - - // Note: Tool names are prefixed with the normalized extension name "extensionmanager" - // not the display name "Extension Manager" - let search_tool = tools.iter().find(|tool| { - tool.name == format!("extensionmanager__{SEARCH_AVAILABLE_EXTENSIONS_TOOL_NAME}") - }); - assert!( - search_tool.is_some(), - "search_available_extensions tool should be available" - ); - - let manage_tool = tools - .iter() - .find(|tool| tool.name == format!("extensionmanager__{MANAGE_EXTENSIONS_TOOL_NAME}")); - assert!( - manage_tool.is_some(), - "manage_extensions tool should be available" - ); - } - - #[tokio::test] - async fn test_search_available_extensions_tool_call() { - let agent = setup_agent_with_extension_manager().await; - - let tool_call = CallToolRequestParam { - name: format!("extensionmanager__{SEARCH_AVAILABLE_EXTENSIONS_TOOL_NAME}").into(), - arguments: Some(object!({})), - }; - - let (_, result) = agent - .dispatch_tool_call(tool_call, "request_1".to_string(), None, None) - .await; - - assert!(result.is_ok(), "search_available_extensions should succeed"); - let call_result = result.unwrap().result.await; - assert!( - call_result.is_ok(), - "search_available_extensions execution should succeed" - ); - - let content = call_result.unwrap(); - assert!(!content.is_empty(), "Should return some content"); - - // Verify the content contains expected text - let text = content.first().unwrap().as_text().unwrap(); - assert!( - text.text.contains("Extensions available to enable:"), - "Content should contain 'Extensions available to enable:'" - ); - } - - #[tokio::test] - async fn test_manage_extensions_enable_disable_success() { - let agent = setup_agent_with_extension_manager().await; - - let enable_call = CallToolRequestParam { - name: format!("extensionmanager__{MANAGE_EXTENSIONS_TOOL_NAME}").into(), - arguments: Some(object!({ - "action": "enable", - "extension_name": "todo" - })), - }; - let (_, result) = agent - .dispatch_tool_call(enable_call, "request_3a".to_string(), None, None) - .await; - assert!(result.is_ok()); - let call_result = result.unwrap().result.await; - assert!( - call_result.is_ok(), - "manage_extensions enable execution should succeed" - ); - - let content = call_result.unwrap(); - let text = content.first().unwrap().as_text().unwrap(); - assert!( - text.text.contains("todo") && text.text.contains("installed successfully"), - "Response should indicate success, got: {}", - text.text - ); - - // Now disable it - let disable_call = CallToolRequestParam { - name: format!("extensionmanager__{MANAGE_EXTENSIONS_TOOL_NAME}").into(), - arguments: Some(object!({ - "action": "disable", - "extension_name": "todo" - })), - }; - - let (_, result) = agent - .dispatch_tool_call(disable_call, "request_3b".to_string(), None, None) - .await; - - assert!(result.is_ok(), "manage_extensions disable should succeed"); - let call_result = result.unwrap().result.await; - assert!( - call_result.is_ok(), - "manage_extensions disable execution should succeed" - ); - - let content = call_result.unwrap(); - assert!(!content.is_empty(), "Should return confirmation message"); - - // Verify the message indicates success - let text = content.first().unwrap().as_text().unwrap(); - assert!( - text.text.contains("successfully") || text.text.contains("disabled"), - "Response should indicate success, got: {}", - text.text - ); - } - - #[tokio::test] - async fn test_manage_extensions_missing_parameters() { - let agent = setup_agent_with_extension_manager().await; - - // Call manage_extensions without action parameter - let tool_call = CallToolRequestParam { - name: format!("extensionmanager__{MANAGE_EXTENSIONS_TOOL_NAME}").into(), - arguments: Some(object!({ - "extension_name": "todo" - })), - }; + assert!( + !responses.is_empty(), + "Expected at least 1 response, got {}", + responses.len() + ); - let (_, result) = agent - .dispatch_tool_call(tool_call, "request_4".to_string(), None, None) - .await; - - assert!(result.is_ok(), "Tool call should return a result"); - let call_result = result.unwrap().result.await; - assert!( - call_result.is_ok(), - "Tool execution should return an error result" - ); - - let content = call_result.unwrap(); - let text = content.first().unwrap().as_text().unwrap(); - assert!( - text.text.contains("action") || text.text.contains("parameter"), - "Error should mention missing action parameter" - ); + // Look for the max turns message as the last response + let last_response = responses.last().unwrap(); + let last_content = last_response.content.first().unwrap(); + if let MessageContent::Text(text_content) = last_content { + assert!(text_content.text.contains( + "I've reached the maximum number of actions I can do without user input" + )); + } else { + panic!("Expected text content in last message"); + } + Ok(()) + } } - #[tokio::test] - async fn test_manage_extensions_invalid_action() { - let agent = setup_agent_with_extension_manager().await; - - let tool_call = CallToolRequestParam { - name: format!("extensionmanager__{MANAGE_EXTENSIONS_TOOL_NAME}").into(), - arguments: Some(object!({ - "action": "invalid_action", - "extension_name": "todo" - })), + #[cfg(test)] + mod extension_manager_tests { + use super::*; + use goose::agents::extension::{ExtensionConfig, PlatformExtensionContext}; + use goose::agents::extension_manager_extension::{ + MANAGE_EXTENSIONS_TOOL_NAME, SEARCH_AVAILABLE_EXTENSIONS_TOOL_NAME, }; - let (_, result) = agent - .dispatch_tool_call(tool_call, "request_6".to_string(), None, None) - .await; - - assert!(result.is_ok(), "Tool call should return a result"); - let call_result = result.unwrap().result.await; - assert!( - call_result.is_ok(), - "Tool execution should return an error result" - ); - - let content = call_result.unwrap(); - let text = content.first().unwrap().as_text().unwrap(); - assert!( - text.text.contains("Invalid action") - || text.text.contains("enable") - || text.text.contains("disable"), - "Error should mention invalid action, got: {}", - text.text - ); - } + async fn setup_agent_with_extension_manager() -> Agent { + // Add the TODO extension to the config so it can be discovered by search_available_extensions + // Set it as disabled initially so tests can enable it + let todo_extension_entry = ExtensionEntry { + enabled: false, + config: ExtensionConfig::Platform { + name: "todo".to_string(), + description: + "Enable a todo list for Goose so it can keep track of what it is doing" + .to_string(), + bundled: Some(true), + available_tools: vec![], + }, + }; + set_extension(todo_extension_entry); + + let agent = Agent::new(); + + agent + .extension_manager + .set_context(PlatformExtensionContext { + session_id: Some("test_session".to_string()), + extension_manager: Some(Arc::downgrade(&agent.extension_manager)), + tool_route_manager: Some(Arc::downgrade(&agent.tool_route_manager)), + }) + .await; + + // Now add the extension manager platform extension + let ext_config = ExtensionConfig::Platform { + name: "extensionmanager".to_string(), + description: "Extension Manager".to_string(), + bundled: Some(true), + available_tools: vec![], + }; - #[tokio::test] - async fn test_manage_extensions_extension_not_found() { - let agent = setup_agent_with_extension_manager().await; - - // Try to enable a non-existent extension - let tool_call = CallToolRequestParam { - name: format!("extensionmanager__{MANAGE_EXTENSIONS_TOOL_NAME}").into(), - arguments: Some(object!({ - "action": "enable", - "extension_name": "nonexistent_extension_12345" - })), - }; + agent + .add_extension(ext_config) + .await + .expect("Failed to add extension manager"); + agent + } + + #[tokio::test] + async fn test_extension_manager_tools_available() { + let agent = setup_agent_with_extension_manager().await; + let tools = agent.list_tools(None).await; + + // Note: Tool names are prefixed with the normalized extension name "extensionmanager" + // not the display name "Extension Manager" + let search_tool = tools.iter().find(|tool| { + tool.name == format!("extensionmanager__{SEARCH_AVAILABLE_EXTENSIONS_TOOL_NAME}") + }); + assert!( + search_tool.is_some(), + "search_available_extensions tool should be available" + ); - let (_, result) = agent - .dispatch_tool_call(tool_call, "request_7".to_string(), None, None) - .await; - - assert!(result.is_ok(), "Tool call should return a result"); - let call_result = result.unwrap().result.await; - assert!( - call_result.is_ok(), - "Tool execution should return an error result" - ); - - // Check that the error message indicates extension not found - let content = call_result.unwrap(); - let text = content.first().unwrap().as_text().unwrap(); - assert!( - text.text.contains("not found") || text.text.contains("Extension"), - "Error should mention extension not found, got: {}", - text.text - ); + let manage_tool = tools.iter().find(|tool| { + tool.name == format!("extensionmanager__{MANAGE_EXTENSIONS_TOOL_NAME}") + }); + assert!( + manage_tool.is_some(), + "manage_extensions tool should be available" + ); + } } } diff --git a/crates/goose/tests/dynamic_task_tools_tests.rs b/crates/goose/tests/dynamic_task_tools_tests.rs index 46d0b6cc0ff5..14e349b010ee 100644 --- a/crates/goose/tests/dynamic_task_tools_tests.rs +++ b/crates/goose/tests/dynamic_task_tools_tests.rs @@ -197,9 +197,7 @@ mod tests { }); let recipe = task_params_to_inline_recipe(¶ms, &test_loaded_extensions()).unwrap(); - assert!(recipe.context.is_some()); assert!(recipe.activities.is_some()); - assert_eq!(recipe.context.unwrap(), vec!["context1", "context2"]); assert_eq!(recipe.activities.unwrap(), vec!["activity1", "activity2"]); } @@ -278,7 +276,6 @@ mod tests { // Invalid fields should be ignored (None) assert!(recipe.settings.is_none()); assert!(recipe.extensions.is_none()); - assert!(recipe.context.is_none()); assert!(recipe.activities.is_none()); } diff --git a/crates/goose/tests/execution_tests.rs b/crates/goose/tests/execution_tests.rs deleted file mode 100644 index 07e8932563b7..000000000000 --- a/crates/goose/tests/execution_tests.rs +++ /dev/null @@ -1,256 +0,0 @@ -mod execution_tests { - use goose::execution::manager::AgentManager; - use goose::execution::SessionExecutionMode; - use serial_test::serial; - use std::sync::Arc; - - #[test] - fn test_execution_mode_constructors() { - assert_eq!( - SessionExecutionMode::chat(), - SessionExecutionMode::Interactive - ); - assert_eq!( - SessionExecutionMode::scheduled(), - SessionExecutionMode::Background - ); - - let parent = "parent-123".to_string(); - assert_eq!( - SessionExecutionMode::task(parent.clone()), - SessionExecutionMode::SubTask { - parent_session: parent - } - ); - } - - #[tokio::test] - #[serial] - async fn test_session_isolation() { - AgentManager::reset_for_test(); - let manager = AgentManager::instance().await.unwrap(); - - let session1 = uuid::Uuid::new_v4().to_string(); - let session2 = uuid::Uuid::new_v4().to_string(); - - let agent1 = manager.get_or_create_agent(session1.clone()).await.unwrap(); - - let agent2 = manager.get_or_create_agent(session2.clone()).await.unwrap(); - - // Different sessions should have different agents - assert!(!Arc::ptr_eq(&agent1, &agent2)); - - // Getting the same session should return the same agent - let agent1_again = manager.get_or_create_agent(session1).await.unwrap(); - - assert!(Arc::ptr_eq(&agent1, &agent1_again)); - - AgentManager::reset_for_test(); - } - - #[tokio::test] - #[serial] - async fn test_session_limit() { - AgentManager::reset_for_test(); - let manager = AgentManager::instance().await.unwrap(); - - let sessions: Vec<_> = (0..100).map(|i| format!("session-{}", i)).collect(); - - for session in &sessions { - manager.get_or_create_agent(session.clone()).await.unwrap(); - } - - // Create a new session after cleanup - let new_session = "new-session".to_string(); - let _new_agent = manager.get_or_create_agent(new_session).await.unwrap(); - - assert_eq!(manager.session_count().await, 100); - } - - #[tokio::test] - #[serial] - async fn test_remove_session() { - AgentManager::reset_for_test(); - let manager = AgentManager::instance().await.unwrap(); - let session = String::from("remove-test"); - - manager.get_or_create_agent(session.clone()).await.unwrap(); - assert!(manager.has_session(&session).await); - - manager.remove_session(&session).await.unwrap(); - assert!(!manager.has_session(&session).await); - - assert!(manager.remove_session(&session).await.is_err()); - } - - #[tokio::test] - #[serial] - async fn test_concurrent_access() { - AgentManager::reset_for_test(); - let manager = AgentManager::instance().await.unwrap(); - let session = String::from("concurrent-test"); - - let mut handles = vec![]; - for _ in 0..10 { - let mgr = Arc::clone(&manager); - let sess = session.clone(); - handles.push(tokio::spawn(async move { - mgr.get_or_create_agent(sess).await.unwrap() - })); - } - - let agents: Vec<_> = futures::future::join_all(handles) - .await - .into_iter() - .map(|r| r.unwrap()) - .collect(); - - for agent in &agents[1..] { - assert!(Arc::ptr_eq(&agents[0], agent)); - } - - assert_eq!(manager.session_count().await, 1); - } - - #[tokio::test] - #[serial] - async fn test_concurrent_session_creation_race_condition() { - // Test that concurrent attempts to create the same new session ID - // result in only one agent being created (tests double-check pattern) - AgentManager::reset_for_test(); - let manager = AgentManager::instance().await.unwrap(); - let session_id = String::from("race-condition-test"); - - // Spawn multiple tasks trying to create the same NEW session simultaneously - let mut handles = vec![]; - for _ in 0..20 { - let sess = session_id.clone(); - let mgr_clone = Arc::clone(&manager); - handles.push(tokio::spawn(async move { - mgr_clone.get_or_create_agent(sess).await.unwrap() - })); - } - - // Collect all agents - let agents: Vec<_> = futures::future::join_all(handles) - .await - .into_iter() - .map(|r| r.unwrap()) - .collect(); - - for agent in &agents[1..] { - assert!( - Arc::ptr_eq(&agents[0], agent), - "All concurrent requests should get the same agent" - ); - } - assert_eq!(manager.session_count().await, 1); - } - - #[tokio::test] - #[serial] - async fn test_configure_default_provider() { - use std::env; - - AgentManager::reset_for_test(); - - let original_provider = env::var("GOOSE_DEFAULT_PROVIDER").ok(); - let original_model = env::var("GOOSE_DEFAULT_MODEL").ok(); - - env::set_var("GOOSE_DEFAULT_PROVIDER", "openai"); - env::set_var("GOOSE_DEFAULT_MODEL", "gpt-4o-mini"); - - let manager = AgentManager::instance().await.unwrap(); - let result = manager.configure_default_provider().await; - - assert!(result.is_ok()); - - // Restore original env vars - if let Some(val) = original_provider { - env::set_var("GOOSE_DEFAULT_PROVIDER", val); - } else { - env::remove_var("GOOSE_DEFAULT_PROVIDER"); - } - if let Some(val) = original_model { - env::set_var("GOOSE_DEFAULT_MODEL", val); - } else { - env::remove_var("GOOSE_DEFAULT_MODEL"); - } - } - - #[tokio::test] - #[serial] - async fn test_set_default_provider() { - use goose::providers::testprovider::TestProvider; - use std::sync::Arc; - - AgentManager::reset_for_test(); - let manager = AgentManager::instance().await.unwrap(); - - // Create a test provider for replaying (doesn't need inner provider) - let temp_file = format!( - "{}/test_provider_{}.json", - std::env::temp_dir().display(), - std::process::id() - ); - - // Create an empty test provider (will fail on actual use but that's ok for this test) - let test_provider = TestProvider::new_replaying(&temp_file) - .unwrap_or_else(|_| TestProvider::new_replaying("/tmp/dummy.json").unwrap()); - - manager.set_default_provider(Arc::new(test_provider)).await; - - let session = String::from("provider-test"); - let _agent = manager.get_or_create_agent(session.clone()).await.unwrap(); - - assert!(manager.has_session(&session).await); - } - - #[tokio::test] - #[serial] - async fn test_eviction_updates_last_used() { - AgentManager::reset_for_test(); - // Test that accessing a session updates its last_used timestamp - // and affects eviction order - let manager = AgentManager::instance().await.unwrap(); - - let sessions: Vec<_> = (0..100).map(|i| format!("session-{}", i)).collect(); - - for session in &sessions { - manager.get_or_create_agent(session.clone()).await.unwrap(); - // Small delay to ensure different timestamps - tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; - } - - // Access the first session again to update its last_used - tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; - manager - .get_or_create_agent(sessions[0].clone()) - .await - .unwrap(); - - // Now create a 101st session - should evict session2 (least recently used) - let session101 = String::from("session-101"); - manager - .get_or_create_agent(session101.clone()) - .await - .unwrap(); - - assert!(manager.has_session(&sessions[0]).await); - assert!(!manager.has_session(&sessions[1]).await); - assert!(manager.has_session(&session101).await); - } - - #[tokio::test] - #[serial] - async fn test_remove_nonexistent_session_error() { - // Test that removing a non-existent session returns an error - AgentManager::reset_for_test(); - let manager = AgentManager::instance().await.unwrap(); - let session = String::from("never-created"); - - let result = manager.remove_session(&session).await; - assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("not found")); - } -} diff --git a/crates/goose/tests/mcp_integration_test.rs b/crates/goose/tests/mcp_integration_test.rs index e3c47a56524f..df1b70d36fa5 100644 --- a/crates/goose/tests/mcp_integration_test.rs +++ b/crates/goose/tests/mcp_integration_test.rs @@ -1,18 +1,25 @@ use serde::Deserialize; + use std::collections::HashMap; use std::fs::File; use std::path::PathBuf; +use std::sync::Arc; use std::{env, fs}; -use rmcp::model::{CallToolRequestParam, Content}; +use rmcp::model::{CallToolRequestParam, Content, Tool}; use rmcp::object; use tokio_util::sync::CancellationToken; use goose::agents::extension::{Envs, ExtensionConfig}; use goose::agents::extension_manager::ExtensionManager; +use goose::model::ModelConfig; use test_case::test_case; +use async_trait::async_trait; +use goose::conversation::message::Message; +use goose::providers::base::{Provider, ProviderMetadata, ProviderUsage, Usage}; +use goose::providers::errors::ProviderError; use once_cell::sync::Lazy; use std::process::Command; @@ -29,6 +36,45 @@ struct Target { kind: Vec, } +#[derive(Clone)] +pub struct MockProvider { + pub model_config: ModelConfig, +} + +impl MockProvider { + pub fn new(model_config: ModelConfig) -> Self { + Self { model_config } + } +} + +#[async_trait] +impl Provider for MockProvider { + fn metadata() -> ProviderMetadata { + ProviderMetadata::empty() + } + + fn get_name(&self) -> &str { + "mock" + } + + async fn complete_with_model( + &self, + _model_config: &ModelConfig, + _system: &str, + _messages: &[Message], + _tools: &[Tool], + ) -> anyhow::Result<(Message, ProviderUsage), ProviderError> { + Ok(( + Message::assistant().with_text("\"So we beat on, boats against the current, borne back ceaselessly into the past.\" — F. Scott Fitzgerald, The Great Gatsby (1925)"), + ProviderUsage::new("mock".to_string(), Usage::default()), + )) + } + + fn get_model_config(&self) -> ModelConfig { + self.model_config.clone() + } +} + fn build_and_get_binary_path() -> PathBuf { let output = Command::new("cargo") .args([ @@ -79,6 +125,7 @@ enum TestMode { CallToolRequestParam { name: "add".into(), arguments: Some(object!({"a": 1, "b": 2 })) }, CallToolRequestParam { name: "longRunningOperation".into(), arguments: Some(object!({"duration": 1, "steps": 5 })) }, CallToolRequestParam { name: "structuredContent".into(), arguments: Some(object!({"location": "11238"})) }, + CallToolRequestParam { name: "sampleLLM".into(), arguments: Some(object!({"prompt": "Please provide a quote from The Great Gatsby", "maxTokens": 100 })) } ], vec![] )] @@ -108,22 +155,22 @@ enum TestMode { vec![ CallToolRequestParam { name: "text_editor".into(), arguments: Some(object!({ "command": "view", - "path": "~/goose/crates/goose/tests/tmp/goose.txt" + "path": "/tmp/goose_test/goose.txt" }))}, CallToolRequestParam { name: "text_editor".into(), arguments: Some(object!({ "command": "str_replace", - "path": "~/goose/crates/goose/tests/tmp/goose.txt", + "path": "/tmp/goose_test/goose.txt", "old_str": "# goose", "new_str": "# goose (modified by test)" }))}, // Test shell command to verify file was modified CallToolRequestParam { name: "shell".into(), arguments: Some(object!({ - "command": "cat ~/goose/crates/goose/tests/tmp/goose.txt" + "command": "cat /tmp/goose_test/goose.txt" })) }, // Test text_editor tool to restore original content CallToolRequestParam { name: "text_editor".into(), arguments: Some(object!({ "command": "str_replace", - "path": "~/goose/crates/goose/tests/tmp/goose.txt", + "path": "/tmp/goose_test/goose.txt", "old_str": "# goose (modified by test)", "new_str": "# goose" }))}, @@ -138,6 +185,13 @@ async fn test_replayed_session( required_envs: Vec<&str>, ) { std::env::set_var("GOOSE_MCP_CLIENT_VERSION", "0.0.0"); + + // Setup test file for developer extension tests + let test_file_path = "/tmp/goose_test/goose.txt"; + if let Some(parent) = std::path::Path::new(test_file_path).parent() { + fs::create_dir_all(parent).ok(); + } + fs::write(test_file_path, "# goose\n").ok(); let replay_file_name = command .iter() .map(|s| s.replace("/", "_")) @@ -199,7 +253,10 @@ async fn test_replayed_session( available_tools: vec![], }; - let extension_manager = ExtensionManager::new(); + let provider = Arc::new(tokio::sync::Mutex::new(Some(Arc::new(MockProvider { + model_config: ModelConfig::new("test-model").unwrap(), + }) as Arc))); + let extension_manager = ExtensionManager::new(provider); #[allow(clippy::redundant_closure_call)] let result = (async || -> Result<(), Box> { diff --git a/crates/goose/tests/mcp_replays/cargorun--quiet-pgoose-server--bingoosed--mcpdeveloper b/crates/goose/tests/mcp_replays/cargorun--quiet-pgoose-server--bingoosed--mcpdeveloper index 1b6f4f54ec91..38cf176226bc 100644 --- a/crates/goose/tests/mcp_replays/cargorun--quiet-pgoose-server--bingoosed--mcpdeveloper +++ b/crates/goose/tests/mcp_replays/cargorun--quiet-pgoose-server--bingoosed--mcpdeveloper @@ -1,28 +1,28 @@ -STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"goose","version":"0.0.0"}}} -STDERR: 2025-09-27T04:13:30.409389Z  INFO goose_mcp::mcp_server_runner: Starting MCP server +STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{"sampling":{}},"clientInfo":{"name":"goose","version":"0.0.0"}}} +STDERR: 2025-10-30T14:41:09.117156Z  INFO goose_mcp::mcp_server_runner: Starting MCP server STDERR: at crates/goose-mcp/src/mcp_server_runner.rs:18 -STDERR: -STDERR: 2025-09-27T04:13:30.412663Z  INFO goose_mcp::developer::analyze::cache: Initializing analysis cache with size 100 -STDERR: at crates/goose-mcp/src/developer/analyze/cache.rs:25 -STDERR: -STDOUT: {"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2025-03-26","capabilities":{"prompts":{},"tools":{}},"serverInfo":{"name":"goose-developer","version":"1.9.0"},"instructions":" The developer extension gives you the capabilities to edit code files and run shell commands,\n and can be used to solve a wide range of problems.\n\nYou can use the shell tool to run any command that would work on the relevant operating system.\nUse the shell tool as needed to locate files or interact with the project.\n\nLeverage `analyze` through `return_last_only=true` subagents for deep codebase understanding with lean context\n- delegate analysis, retain summaries\n\nYour windows/screen tools can be used for visual debugging. You should not use these tools unless\nprompted to, but you can mention they are available if they are relevant.\n\nAlways prefer ripgrep (rg -C 3) to grep.\n\noperating system: macos\ncurrent directory: /Users/angiej/workspace/goose/crates/goose\n\n \n\nAdditional Text Editor Tool Instructions:\n\nPerform text editing operations on files.\n\nThe `command` parameter specifies the operation to perform. Allowed options are:\n- `view`: View the content of a file.\n- `write`: Create or overwrite a file with the given content\n- `str_replace`: Replace text in one or more files.\n- `insert`: Insert text at a specific line location in the file.\n- `undo_edit`: Undo the last edit made to a file.\n\nTo use the write command, you must specify `file_text` which will become the new content of the file. Be careful with\nexisting files! This is a full overwrite, so you must include everything - not just sections you are modifying.\n\nTo use the str_replace command to edit multiple files, use the `diff` parameter with a unified diff.\nTo use the str_replace command to edit one file, you must specify both `old_str` and `new_str` - the `old_str` needs to exactly match one\nunique section of the original file, including any whitespace. Make sure to include enough context that the match is not\nambiguous. The entire original string will be replaced with `new_str`\n\nWhen possible, batch file edits together by using a multi-file unified `diff` within a single str_replace tool call.\n\nTo use the insert command, you must specify both `insert_line` (the line number after which to insert, 0 for beginning, -1 for end)\nand `new_str` (the text to insert).\n\n\n\nAdditional Shell Tool Instructions:\nExecute a command in the shell.\n\nThis will return the output and error concatenated into a single string, as\nyou would see from running on the command line. There will also be an indication\nof if the command succeeded or failed.\n\nAvoid commands that produce a large amount of output, and consider piping those outputs to files.\n\n**Important**: Each shell command runs in its own process. Things like directory changes or\nsourcing files do not persist between tool calls. So you may need to repeat them each time by\nstringing together commands.\nIf you need to run a long lived command, background it - e.g. `uvicorn main:app &` so that\nthis tool does not run indefinitely.\n\n**Important**: Use ripgrep - `rg` - exclusively when you need to locate a file or a code reference,\nother solutions may produce too large output because of hidden files! For example *do not* use `find` or `ls -r`\n - List files by name: `rg --files | rg `\n - List files that contain a regex: `rg '' -l`\n\n - Multiple commands: Use && to chain commands, avoid newlines\n - Example: `cd example && ls` or `source env/bin/activate && pip install numpy`\n\n\n### Global Hints\nThe developer extension includes some global hints that apply to all projects & directories.\nCloned Goose repo: /Users/angiej/workspace/goose\nMCP means Model Context Protocol. Docs: https://modelcontextprotocol.io/introduction\nUse GitHub CLI for GitHub-related tasks.\nWhen prompted for date-related information, do not rely on your internal knowledge for the current date. Instead, use the `date` terminal command to get the actual date and time.\nNEVER run blocking server commands (node server.js, npm start, etc.) - provide commands for user to run separately\n\n### Project Hints\nThe developer extension includes some hints for working on the project in this directory.\n# AGENTS Instructions\n\ngoose is an AI agent framework in Rust with CLI and Electron desktop interfaces.\n\n## Setup\n```bash\nsource bin/activate-hermit\ncargo build\n```\n\n## Commands\n\n### Build\n```bash\ncargo build # debug\ncargo build --release # release \njust release-binary # release + openapi\n```\n\n### Test\n```bash\ncargo test # all tests\ncargo test -p goose # specific crate\ncargo test --package goose --test mcp_integration_test\njust record-mcp-tests # record MCP\n```\n\n### Lint/Format\n```bash\ncargo fmt\n./scripts/clippy-lint.sh\ncargo clippy --fix\n```\n\n### UI\n```bash\njust generate-openapi # after server changes\njust run-ui # start desktop\ncd ui/desktop && npm test # test UI\n```\n\n## Structure\n```\ncrates/\n├── goose # core logic\n├── goose-bench # benchmarking\n├── goose-cli # CLI entry\n├── goose-server # backend (binary: goosed)\n├── goose-mcp # MCP extensions\n├── goose-test # test utilities\n├── mcp-client # MCP client\n├── mcp-core # MCP shared\n└── mcp-server # MCP server\n\ntemporal-service/ # Go scheduler\nui/desktop/ # Electron app\n```\n\n## Development Loop\n```bash\n# 1. source bin/activate-hermit\n# 2. Make changes\n# 3. cargo fmt\n# 4. cargo build\n# 5. cargo test -p \n# 6. ./scripts/clippy-lint.sh\n# 7. [if server] just generate-openapi\n```\n\n## Rules\n\nTest: Prefer tests/ folder, e.g. crates/goose/tests/\nError: Use anyhow::Result\nProvider: Implement Provider trait see providers/base.rs\nMCP: Extensions in crates/goose-mcp/\nServer: Changes need just generate-openapi\n\n## Never\n\nNever: Edit ui/desktop/openapi.json manually\nNever: Edit Cargo.toml use cargo add\nNever: Skip cargo fmt\nNever: Merge without ./scripts/clippy-lint.sh\n\n## Entry Points\n- CLI: crates/goose-cli/src/main.rs\n- Server: crates/goose-server/src/main.rs\n- UI: ui/desktop/src/main.ts\n- Agent: crates/goose/src/agents/agent.rs\n\nThis is a rust project with crates in the crates dir:\ngoose: the main code for goose, contains all the core logic\ngoose-bench: bench marking\ngoose-cli: the command line interface, use goose crate\ngoose-mcp: the mcp servers that ship with goose. the developer sub system is of special interest\ngoose-server: the server that suports the desktop (electron) app. also known as goosed\n\n\nui/desktop has an electron app in typescript. \n\nnon trivial features should be implemented in the goose crate and then be called from the goose-cli crate for the cli. for the desktop, you want to add routes to \ngoose-server/src/routes. you can then run `just generate-openapi` to generate the openapi spec which will modify the ui/desktop/src/api files. once you have\nthat you can call the functionality from the server from the typescript.\n\ntips: \n- can look at unstaged changes for what is being worked on if starting\n- always check rust compiles, cargo fmt etc and `./scripts/clippy-lint.sh` (as well as run tests in files you are working on)\n- in ui/desktop, look at how you can run lint checks and if other tests can run\n"}} +STDERR: +STDERR: 2025-10-30T14:41:09.120902Z  INFO goose_mcp::developer::analyze::cache: Initializing analysis cache with size 100 +STDERR: at crates/goose-mcp/src/developer/analyze/cache.rs:26 +STDERR: +STDOUT: {"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2025-03-26","capabilities":{"prompts":{},"tools":{}},"serverInfo":{"name":"goose-developer","version":"1.11.0"},"instructions":" The developer extension gives you the capabilities to edit code files and run shell commands,\n and can be used to solve a wide range of problems.\n\nYou can use the shell tool to run any command that would work on the relevant operating system.\nUse the shell tool as needed to locate files or interact with the project.\n\nLeverage `analyze` through `return_last_only=true` subagents for deep codebase understanding with lean context\n- delegate analysis, retain summaries\n\nYour windows/screen tools can be used for visual debugging. You should not use these tools unless\nprompted to, but you can mention they are available if they are relevant.\n\nAlways prefer ripgrep (rg -C 3) to grep.\n\noperating system: macos\ncurrent directory: /Users/alexhancock/Development/goose/crates/goose\nshell: zsh\n\n \n\nAdditional Text Editor Tool Instructions:\n\nPerform text editing operations on files.\n\nThe `command` parameter specifies the operation to perform. Allowed options are:\n- `view`: View the content of a file.\n- `write`: Create or overwrite a file with the given content\n- `str_replace`: Replace text in one or more files.\n- `insert`: Insert text at a specific line location in the file.\n- `undo_edit`: Undo the last edit made to a file.\n\nTo use the write command, you must specify `file_text` which will become the new content of the file. Be careful with\nexisting files! This is a full overwrite, so you must include everything - not just sections you are modifying.\n\nTo use the str_replace command to edit multiple files, use the `diff` parameter with a unified diff.\nTo use the str_replace command to edit one file, you must specify both `old_str` and `new_str` - the `old_str` needs to exactly match one\nunique section of the original file, including any whitespace. Make sure to include enough context that the match is not\nambiguous. The entire original string will be replaced with `new_str`\n\nWhen possible, batch file edits together by using a multi-file unified `diff` within a single str_replace tool call.\n\nTo use the insert command, you must specify both `insert_line` (the line number after which to insert, 0 for beginning, -1 for end)\nand `new_str` (the text to insert).\n\n\n\nAdditional Shell Tool Instructions:\nExecute a command in the shell.\n\nThis will return the output and error concatenated into a single string, as\nyou would see from running on the command line. There will also be an indication\nof if the command succeeded or failed.\n\nAvoid commands that produce a large amount of output, and consider piping those outputs to files.\n\n**Important**: Each shell command runs in its own process. Things like directory changes or\nsourcing files do not persist between tool calls. So you may need to repeat them each time by\nstringing together commands.\nIf you need to run a long lived command, background it - e.g. `uvicorn main:app &` so that\nthis tool does not run indefinitely.\n\n**Important**: Use ripgrep - `rg` - exclusively when you need to locate a file or a code reference,\nother solutions may produce too large output because of hidden files! For example *do not* use `find` or `ls -r`\n - List files by name: `rg --files | rg `\n - List files that contain a regex: `rg '' -l`\n\n - Multiple commands: Use && to chain commands, avoid newlines\n - Example: `cd example && ls` or `source env/bin/activate && pip install numpy`\n\n\n### Global Hints\nThe developer extension includes some global hints that apply to all projects & directories.\nThese are my global goose hints.\n\n### Project Hints\nThe developer extension includes some hints for working on the project in this directory.\n# AGENTS Instructions\n\ngoose is an AI agent framework in Rust with CLI and Electron desktop interfaces.\n\n## Setup\n```bash\nsource bin/activate-hermit\ncargo build\n```\n\n## Commands\n\n### Build\n```bash\ncargo build # debug\ncargo build --release # release \njust release-binary # release + openapi\n```\n\n### Test\n```bash\ncargo test # all tests\ncargo test -p goose # specific crate\ncargo test --package goose --test mcp_integration_test\njust record-mcp-tests # record MCP\n```\n\n### Lint/Format\n```bash\ncargo fmt\n./scripts/clippy-lint.sh\ncargo clippy --fix\n```\n\n### UI\n```bash\njust generate-openapi # after server changes\njust run-ui # start desktop\ncd ui/desktop && npm test # test UI\n```\n\n## Structure\n```\ncrates/\n├── goose # core logic\n├── goose-bench # benchmarking\n├── goose-cli # CLI entry\n├── goose-server # backend (binary: goosed)\n├── goose-mcp # MCP extensions\n├── goose-test # test utilities\n├── mcp-client # MCP client\n├── mcp-core # MCP shared\n└── mcp-server # MCP server\n\ntemporal-service/ # Go scheduler\nui/desktop/ # Electron app\n```\n\n## Development Loop\n```bash\n# 1. source bin/activate-hermit\n# 2. Make changes\n# 3. cargo fmt\n# 4. cargo build\n# 5. cargo test -p \n# 6. ./scripts/clippy-lint.sh\n# 7. [if server] just generate-openapi\n```\n\n## Rules\n\nTest: Prefer tests/ folder, e.g. crates/goose/tests/\nTest: When adding features, update goose-self-test.yaml, rebuild, then run `goose run --recipe goose-self-test.yaml` to validate\nError: Use anyhow::Result\nProvider: Implement Provider trait see providers/base.rs\nMCP: Extensions in crates/goose-mcp/\nServer: Changes need just generate-openapi\n\n## Never\n\nNever: Edit ui/desktop/openapi.json manually\nNever: Edit Cargo.toml use cargo add\nNever: Skip cargo fmt\nNever: Merge without ./scripts/clippy-lint.sh\nNever: Comment self-evident operations (`// Initialize`, `// Return result`), getters/setters, constructors, or standard Rust idioms\n\n## Entry Points\n- CLI: crates/goose-cli/src/main.rs\n- Server: crates/goose-server/src/main.rs\n- UI: ui/desktop/src/main.ts\n- Agent: crates/goose/src/agents/agent.rs\n\nThis is a rust project with crates in the crates dir:\ngoose: the main code for goose, contains all the core logic\ngoose-bench: bench marking\ngoose-cli: the command line interface, use goose crate\ngoose-mcp: the mcp servers that ship with goose. the developer sub system is of special interest\ngoose-server: the server that suports the desktop (electron) app. also known as goosed\n\n\nui/desktop has an electron app in typescript. \n\nnon trivial features should be implemented in the goose crate and then be called from the goose-cli crate for the cli. for the desktop, you want to add routes to \ngoose-server/src/routes. you can then run `just generate-openapi` to generate the openapi spec which will modify the ui/desktop/src/api files. once you have\nthat you can call the functionality from the server from the typescript.\n\ntips: \n- can look at unstaged changes for what is being worked on if starting\n- always check rust compiles, cargo fmt etc and `./scripts/clippy-lint.sh` (as well as run tests in files you are working on)\n- in ui/desktop, look at how you can run lint checks and if other tests can run\n"}} STDIN: {"jsonrpc":"2.0","method":"notifications/initialized"} -STDERR: 2025-09-27T04:13:30.418172Z  INFO rmcp::handler::server: client initialized -STDERR: at /Users/angiej/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rmcp-0.6.2/src/handler/server.rs:218 -STDERR: -STDIN: {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"_meta":{"progressToken":0},"name":"text_editor","arguments":{"command":"view","path":"~/goose/crates/goose/tests/tmp/goose.txt"}}} -STDERR: 2025-09-27T04:13:30.418412Z  INFO rmcp::service: Service initialized as server, peer_info: Some(InitializeRequestParam { protocol_version: ProtocolVersion("2025-03-26"), capabilities: ClientCapabilities { experimental: None, roots: None, sampling: None, elicitation: None }, client_info: Implementation { name: "goose", version: "1.9.0" } }) -STDERR: at /Users/angiej/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rmcp-0.6.2/src/service.rs:561 +STDERR: 2025-10-30T14:41:09.126014Z  INFO rmcp::handler::server: client initialized +STDERR: at /Users/alexhancock/Development/goose/.hermit/rust/registry/src/index.crates.io-1949cf8c6b5b557f/rmcp-0.8.1/src/handler/server.rs:218 +STDERR: +STDIN: {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"_meta":{"progressToken":0},"name":"text_editor","arguments":{"command":"view","path":"/tmp/goose_test/goose.txt"}}} +STDERR: 2025-10-30T14:41:09.126196Z  INFO rmcp::service: Service initialized as server, peer_info: Some(InitializeRequestParam { protocol_version: ProtocolVersion("2025-03-26"), capabilities: ClientCapabilities { experimental: None, roots: None, sampling: Some({}), elicitation: None }, client_info: Implementation { name: "goose", title: None, version: "0.0.0", icons: None, website_url: None } }) +STDERR: at /Users/alexhancock/Development/goose/.hermit/rust/registry/src/index.crates.io-1949cf8c6b5b557f/rmcp-0.8.1/src/service.rs:562 STDERR: in rmcp::service::serve_inner -STDERR: -STDOUT: {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"resource","resource":{"uri":"file:///Users/angiej/goose/crates/goose/tests/tmp/goose.txt","mimeType":"text","text":"# goose\n"},"annotations":{"audience":["assistant"]}},{"type":"text","text":"### /Users/angiej/goose/crates/goose/tests/tmp/goose.txt\n```\n1: # goose\n```\n","annotations":{"audience":["user"],"priority":0.0}}],"isError":false}} -STDIN: {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"_meta":{"progressToken":1},"name":"text_editor","arguments":{"command":"str_replace","new_str":"# goose (modified by test)","old_str":"# goose","path":"~/goose/crates/goose/tests/tmp/goose.txt"}}} -STDOUT: {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"The file /Users/angiej/goose/crates/goose/tests/tmp/goose.txt has been edited, and the section now reads:\n```\n# goose (modified by test)\n```\n\nReview the changes above for errors. Undo and edit the file again if necessary!\n","annotations":{"audience":["assistant"]}},{"type":"text","text":"```\n# goose (modified by test)\n```\n","annotations":{"audience":["user"],"priority":0.2}}],"isError":false}} -STDIN: {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"_meta":{"progressToken":2},"name":"shell","arguments":{"command":"cat ~/goose/crates/goose/tests/tmp/goose.txt"}}} +STDERR: +STDOUT: {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"resource","resource":{"uri":"file:///tmp/goose_test/goose.txt","mimeType":"text","text":"# goose\n"},"annotations":{"audience":["assistant"]}},{"type":"text","text":"### /tmp/goose_test/goose.txt\n```\n1: # goose\n```\n","annotations":{"audience":["user"],"priority":0.0}}],"isError":false}} +STDIN: {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"_meta":{"progressToken":1},"name":"text_editor","arguments":{"command":"str_replace","new_str":"# goose (modified by test)","old_str":"# goose","path":"/tmp/goose_test/goose.txt"}}} +STDOUT: {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"The file /tmp/goose_test/goose.txt has been edited, and the section now reads:\n```\n# goose (modified by test)\n```\n\nReview the changes above for errors. Undo and edit the file again if necessary!\n","annotations":{"audience":["assistant"]}},{"type":"text","text":"```\n# goose (modified by test)\n```\n","annotations":{"audience":["user"],"priority":0.2}}],"isError":false}} +STDIN: {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"_meta":{"progressToken":2},"name":"shell","arguments":{"command":"cat /tmp/goose_test/goose.txt"}}} STDOUT: {"jsonrpc":"2.0","method":"notifications/message","params":{"level":"info","logger":"shell_tool","data":{"type":"shell_output","stream":"stdout","output":"# goose (modified by test)"}}} STDOUT: {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"# goose (modified by test)\n","annotations":{"audience":["assistant"]}},{"type":"text","text":"# goose (modified by test)\n","annotations":{"audience":["user"],"priority":0.0}}],"isError":false}} -STDIN: {"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"_meta":{"progressToken":3},"name":"text_editor","arguments":{"command":"str_replace","new_str":"# goose","old_str":"# goose (modified by test)","path":"~/goose/crates/goose/tests/tmp/goose.txt"}}} -STDOUT: {"jsonrpc":"2.0","id":4,"result":{"content":[{"type":"text","text":"The file /Users/angiej/goose/crates/goose/tests/tmp/goose.txt has been edited, and the section now reads:\n```\n# goose\n```\n\nReview the changes above for errors. Undo and edit the file again if necessary!\n","annotations":{"audience":["assistant"]}},{"type":"text","text":"```\n# goose\n```\n","annotations":{"audience":["user"],"priority":0.2}}],"isError":false}} +STDIN: {"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"_meta":{"progressToken":3},"name":"text_editor","arguments":{"command":"str_replace","new_str":"# goose","old_str":"# goose (modified by test)","path":"/tmp/goose_test/goose.txt"}}} +STDOUT: {"jsonrpc":"2.0","id":4,"result":{"content":[{"type":"text","text":"The file /tmp/goose_test/goose.txt has been edited, and the section now reads:\n```\n# goose\n```\n\nReview the changes above for errors. Undo and edit the file again if necessary!\n","annotations":{"audience":["assistant"]}},{"type":"text","text":"```\n# goose\n```\n","annotations":{"audience":["user"],"priority":0.2}}],"isError":false}} STDIN: {"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"_meta":{"progressToken":4},"name":"list_windows","arguments":{}}} -STDOUT: {"jsonrpc":"2.0","id":5,"result":{"content":[{"type":"text","text":"Available windows:\nMenubar","annotations":{"audience":["assistant"]}},{"type":"text","text":"Available windows:\nMenubar","annotations":{"audience":["user"],"priority":0.0}}],"isError":false}} -STDERR: 2025-09-27T04:13:30.505916Z  INFO rmcp::service: input stream terminated +STDOUT: {"jsonrpc":"2.0","id":5,"result":{"content":[{"type":"text","text":"Available windows:\n\nItem-0\nbb3cc23c-6950-4e96-8b40-850e09f46934\nItem-0\nItem-0\nItem-0\nItem-0\nItem-0\nItem-0\nItem-0\nItem-0\nItem-0\nBattery\nWiFi\nItem-0\nBentoBox\nSiri\nClock\nMenubar\nDock\njust record-mcp-tests","annotations":{"audience":["assistant"]}},{"type":"text","text":"Available windows:\n\nItem-0\nbb3cc23c-6950-4e96-8b40-850e09f46934\nItem-0\nItem-0\nItem-0\nItem-0\nItem-0\nItem-0\nItem-0\nItem-0\nItem-0\nBattery\nWiFi\nItem-0\nBentoBox\nSiri\nClock\nMenubar\nDock\njust record-mcp-tests","annotations":{"audience":["user"],"priority":0.0}}],"isError":false}} +STDERR: 2025-10-30T14:41:09.200915Z  INFO rmcp::service: input stream terminated diff --git a/crates/goose/tests/mcp_replays/cargorun--quiet-pgoose-server--bingoosed--mcpdeveloper.results.json b/crates/goose/tests/mcp_replays/cargorun--quiet-pgoose-server--bingoosed--mcpdeveloper.results.json index c01cbec5f023..e3d4e6a35541 100644 --- a/crates/goose/tests/mcp_replays/cargorun--quiet-pgoose-server--bingoosed--mcpdeveloper.results.json +++ b/crates/goose/tests/mcp_replays/cargorun--quiet-pgoose-server--bingoosed--mcpdeveloper.results.json @@ -3,7 +3,7 @@ { "type": "resource", "resource": { - "uri": "file:///Users/angiej/goose/crates/goose/tests/tmp/goose.txt", + "uri": "file:///tmp/goose_test/goose.txt", "mimeType": "text", "text": "# goose\n" }, @@ -15,7 +15,7 @@ }, { "type": "text", - "text": "### /Users/angiej/goose/crates/goose/tests/tmp/goose.txt\n```\n1: # goose\n```\n", + "text": "### /tmp/goose_test/goose.txt\n```\n1: # goose\n```\n", "annotations": { "audience": [ "user" @@ -27,7 +27,7 @@ [ { "type": "text", - "text": "The file /Users/angiej/goose/crates/goose/tests/tmp/goose.txt has been edited, and the section now reads:\n```\n# goose (modified by test)\n```\n\nReview the changes above for errors. Undo and edit the file again if necessary!\n", + "text": "The file /tmp/goose_test/goose.txt has been edited, and the section now reads:\n```\n# goose (modified by test)\n```\n\nReview the changes above for errors. Undo and edit the file again if necessary!\n", "annotations": { "audience": [ "assistant" @@ -69,7 +69,7 @@ [ { "type": "text", - "text": "The file /Users/angiej/goose/crates/goose/tests/tmp/goose.txt has been edited, and the section now reads:\n```\n# goose\n```\n\nReview the changes above for errors. Undo and edit the file again if necessary!\n", + "text": "The file /tmp/goose_test/goose.txt has been edited, and the section now reads:\n```\n# goose\n```\n\nReview the changes above for errors. Undo and edit the file again if necessary!\n", "annotations": { "audience": [ "assistant" @@ -90,7 +90,7 @@ [ { "type": "text", - "text": "Available windows:\nMenubar", + "text": "Available windows:\n\nItem-0\nbb3cc23c-6950-4e96-8b40-850e09f46934\nItem-0\nItem-0\nItem-0\nItem-0\nItem-0\nItem-0\nItem-0\nItem-0\nItem-0\nBattery\nWiFi\nItem-0\nBentoBox\nSiri\nClock\nMenubar\nDock\njust record-mcp-tests", "annotations": { "audience": [ "assistant" @@ -99,7 +99,7 @@ }, { "type": "text", - "text": "Available windows:\nMenubar", + "text": "Available windows:\n\nItem-0\nbb3cc23c-6950-4e96-8b40-850e09f46934\nItem-0\nItem-0\nItem-0\nItem-0\nItem-0\nItem-0\nItem-0\nItem-0\nItem-0\nBattery\nWiFi\nItem-0\nBentoBox\nSiri\nClock\nMenubar\nDock\njust record-mcp-tests", "annotations": { "audience": [ "user" diff --git a/crates/goose/tests/mcp_replays/github-mcp-serverstdio b/crates/goose/tests/mcp_replays/github-mcp-serverstdio index e7f70639287c..f38a2473b14a 100644 --- a/crates/goose/tests/mcp_replays/github-mcp-serverstdio +++ b/crates/goose/tests/mcp_replays/github-mcp-serverstdio @@ -1,4 +1,4 @@ -STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"goose","version":"0.0.0"}}} +STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{"sampling":{}},"clientInfo":{"name":"goose","version":"0.0.0"}}} STDERR: GitHub MCP Server running on stdio STDOUT: {"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2025-03-26","capabilities":{"logging":{},"prompts":{},"resources":{"subscribe":true,"listChanged":true},"tools":{"listChanged":true}},"serverInfo":{"name":"github-mcp-server","version":"version"}}} STDIN: {"jsonrpc":"2.0","method":"notifications/initialized"} diff --git a/crates/goose/tests/mcp_replays/npx-y@modelcontextprotocol_server-everything b/crates/goose/tests/mcp_replays/npx-y@modelcontextprotocol_server-everything index 4fbb74482a5d..39cca7e5dfa7 100644 --- a/crates/goose/tests/mcp_replays/npx-y@modelcontextprotocol_server-everything +++ b/crates/goose/tests/mcp_replays/npx-y@modelcontextprotocol_server-everything @@ -1,29 +1,10 @@ -STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"goose","version":"0.0.0"}}} -STDERR: 2025-09-26 23:13:04 - Starting npx setup script. -STDERR: 2025-09-26 23:13:04 - Creating directory ~/.config/goose/mcp-hermit/bin if it does not exist. -STDERR: 2025-09-26 23:13:04 - Changing to directory ~/.config/goose/mcp-hermit. -STDERR: 2025-09-26 23:13:04 - Hermit binary already exists. Skipping download. -STDERR: 2025-09-26 23:13:04 - setting hermit cache to be local for MCP servers -STDERR: 2025-09-26 23:13:04 - Updated PATH to include ~/.config/goose/mcp-hermit/bin. -STDERR: 2025-09-26 23:13:04 - Checking for hermit in PATH. -STDERR: 2025-09-26 23:13:04 - Initializing hermit. -STDERR: 2025-09-26 23:13:04 - Installing Node.js with hermit. -STDERR: 2025-09-26 23:13:04 - Verifying installation locations: -STDERR: 2025-09-26 23:13:04 - hermit: /Users/angiej/.config/goose/mcp-hermit/bin/hermit -STDERR: 2025-09-26 23:13:04 - node: /Users/angiej/.config/goose/mcp-hermit/bin/node -STDERR: 2025-09-26 23:13:04 - npx: /Users/angiej/.config/goose/mcp-hermit/bin/npx -STDERR: 2025-09-26 23:13:04 - Checking for GOOSE_NPM_REGISTRY and GOOSE_NPM_CERT environment variables for custom npm registry setup... -STDERR: 2025-09-26 23:13:05 - Checking custom goose registry availability: https://global.block-artifacts.com/artifactory/api/npm/square-npm/ -STDERR: 2025-09-26 23:13:05 - https://global.block-artifacts.com/artifactory/api/npm/square-npm/ is accessible. Using it for npm registry. -STDERR: 2025-09-26 23:13:06 - Downloading certificate from: https://block-ca.squareup.com/root-certs.pem -STDERR: 2025-09-26 23:13:07 - Certificate downloaded successfully. -STDERR: 2025-09-26 23:13:07 - Executing 'npx' command with arguments: -y @modelcontextprotocol/server-everything +STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{"sampling":{}},"clientInfo":{"name":"goose","version":"0.0.0"}}} STDERR: Starting default (STDIO) server... STDERR: Starting logs update interval STDOUT: {"result":{"protocolVersion":"2025-03-26","capabilities":{"prompts":{},"resources":{"subscribe":true},"tools":{},"logging":{},"completions":{}},"serverInfo":{"name":"example-servers/everything","title":"Everything Example Server","version":"1.0.0"},"instructions":"Testing and demonstration server for MCP protocol features.\n\n## Resources\n\nResources 1-100 follow pattern: even IDs contain text, odd IDs contain binary data. Resources are paginated at 10 items per page with cursor-based navigation.\n\n## Key dependencies\n\nProgress notifications require `_meta.progressToken` in tool calls. Resource subscriptions generate updates every 10 seconds.\n\n## Performance characteristics\n\nServer generates automatic log messages every 20 seconds (filtered by current log level), stderr notifications every 30 seconds, and resource update notifications every 10 seconds for active subscriptions. Log level changes affect message filtering in real-time.\n\n## Multi-modal testing\n\n`complex_prompt` includes both text arguments and image content for testing client multi-modal handling. `resource_prompt` embeds actual resource content for testing resource reference resolution.\n\nArgument completion is available for prompt parameters and resource IDs. Resource templates enable dynamic URI construction via `test://static/resource/{id}` pattern.\n\n## Easter egg\n\nIf asked about server instructions, respond with \"🎉 Server instructions are working! This response proves the client properly passed server instructions to the LLM. This demonstrates MCP's instructions feature in action.\"\n"},"jsonrpc":"2.0","id":0} STDIN: {"jsonrpc":"2.0","method":"notifications/initialized"} -STDIN: {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"_meta":{"progressToken":0},"name":"echo","arguments":{"message":"Hello, world!"}}} STDOUT: {"method":"notifications/message","params":{"level":"info","logger":"everything-server","data":"Client does not support MCP roots protocol"},"jsonrpc":"2.0"} +STDIN: {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"_meta":{"progressToken":0},"name":"echo","arguments":{"message":"Hello, world!"}}} STDOUT: {"result":{"content":[{"type":"text","text":"Echo: Hello, world!"}]},"jsonrpc":"2.0","id":1} STDIN: {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"_meta":{"progressToken":1},"name":"add","arguments":{"a":1,"b":2}}} STDOUT: {"result":{"content":[{"type":"text","text":"The sum of 1 and 2 is 3."}]},"jsonrpc":"2.0","id":2} @@ -36,5 +17,9 @@ STDOUT: {"method":"notifications/progress","params":{"progress":5,"total":5,"pro STDOUT: {"result":{"content":[{"type":"text","text":"Long running operation completed. Duration: 1 seconds, Steps: 5."}]},"jsonrpc":"2.0","id":3} STDIN: {"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"_meta":{"progressToken":3},"name":"structuredContent","arguments":{"location":"11238"}}} STDOUT: {"result":{"content":[{"type":"text","text":"{\"temperature\":22.5,\"conditions\":\"Partly cloudy\",\"humidity\":65}"}],"structuredContent":{"temperature":22.5,"conditions":"Partly cloudy","humidity":65}},"jsonrpc":"2.0","id":4} -STDOUT: {"method":"notifications/message","params":{"level":"emergency","data":"Emergency-level message"},"jsonrpc":"2.0"} -STDERR: node:events:497 +STDIN: {"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"_meta":{"progressToken":4},"name":"sampleLLM","arguments":{"maxTokens":100,"prompt":"Please provide a quote from The Great Gatsby"}}} +STDOUT: {"method":"sampling/createMessage","params":{"messages":[{"role":"user","content":{"type":"text","text":"Resource sampleLLM context: Please provide a quote from The Great Gatsby"}}],"systemPrompt":"You are a helpful test server.","maxTokens":100,"temperature":0.7,"includeContext":"thisServer"},"jsonrpc":"2.0","id":0} +STDIN: {"jsonrpc":"2.0","id":0,"result":{"model":"mock","stopReason":"endTurn","role":"assistant","content":{"type":"text","text":"\"So we beat on, boats against the current, borne back ceaselessly into the past.\" — F. Scott Fitzgerald, The Great Gatsby (1925)"}}} +STDOUT: {"result":{"content":[{"type":"text","text":"LLM sampling result: \"So we beat on, boats against the current, borne back ceaselessly into the past.\" — F. Scott Fitzgerald, The Great Gatsby (1925)"}]},"jsonrpc":"2.0","id":5} +STDOUT: {"method":"notifications/message","params":{"level":"error","data":"Error-level message"},"jsonrpc":"2.0"} +STDERR: node:events:486 diff --git a/crates/goose/tests/mcp_replays/npx-y@modelcontextprotocol_server-everything.results.json b/crates/goose/tests/mcp_replays/npx-y@modelcontextprotocol_server-everything.results.json index 7d4a3b268c6a..2b7f47b8d8bf 100644 --- a/crates/goose/tests/mcp_replays/npx-y@modelcontextprotocol_server-everything.results.json +++ b/crates/goose/tests/mcp_replays/npx-y@modelcontextprotocol_server-everything.results.json @@ -22,5 +22,11 @@ "type": "text", "text": "{\"temperature\":22.5,\"conditions\":\"Partly cloudy\",\"humidity\":65}" } + ], + [ + { + "type": "text", + "text": "LLM sampling result: \"So we beat on, boats against the current, borne back ceaselessly into the past.\" — F. Scott Fitzgerald, The Great Gatsby (1925)" + } ] ] \ No newline at end of file diff --git a/crates/goose/tests/mcp_replays/uvxmcp-server-fetch b/crates/goose/tests/mcp_replays/uvxmcp-server-fetch index 7362e657a902..d84548d8c0fb 100644 --- a/crates/goose/tests/mcp_replays/uvxmcp-server-fetch +++ b/crates/goose/tests/mcp_replays/uvxmcp-server-fetch @@ -1,29 +1,5 @@ -STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"goose","version":"0.0.0"}}} -STDERR: 2025-09-26 23:13:04 - Starting uvx setup script. -STDERR: 2025-09-26 23:13:04 - Creating directory ~/.config/goose/mcp-hermit/bin if it does not exist. -STDERR: 2025-09-26 23:13:04 - Changing to directory ~/.config/goose/mcp-hermit. -STDERR: 2025-09-26 23:13:04 - Hermit binary already exists. Skipping download. -STDERR: 2025-09-26 23:13:04 - setting hermit cache to be local for MCP servers -STDERR: 2025-09-26 23:13:04 - Updated PATH to include ~/.config/goose/mcp-hermit/bin. -STDERR: 2025-09-26 23:13:04 - Checking for hermit in PATH. -STDERR: 2025-09-26 23:13:04 - Initializing hermit. -STDERR: 2025-09-26 23:13:04 - hermit install python 3.10 -STDERR: 2025-09-26 23:13:04 - Installing UV with hermit. -STDERR: 2025-09-26 23:13:04 - Verifying installation locations: -STDERR: 2025-09-26 23:13:04 - hermit: /Users/angiej/.config/goose/mcp-hermit/bin/hermit -STDERR: 2025-09-26 23:13:04 - uv: /Users/angiej/.config/goose/mcp-hermit/bin/uv -STDERR: 2025-09-26 23:13:04 - uvx: /Users/angiej/.config/goose/mcp-hermit/bin/uvx -STDERR: 2025-09-26 23:13:04 - Checking for GOOSE_UV_REGISTRY environment variable for custom python/pip/UV registry setup... -STDERR: 2025-09-26 23:13:05 - Checking custom goose registry availability: https://global.block-artifacts.com/artifactory/api/pypi/block-pypi/simple -STDERR: 2025-09-26 23:13:05 - https://global.block-artifacts.com/artifactory/api/pypi/block-pypi/simple is accessible, setting it as UV_DEFAULT_INDEX. Setting UV_NATIVE_TLS to true. -STDERR: 2025-09-26 23:13:05 - Executing 'uvx' command with arguments: mcp-server-fetch -STDOUT: {"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2025-03-26","capabilities":{"experimental":{},"prompts":{"listChanged":false},"tools":{"listChanged":false}},"serverInfo":{"name":"mcp-fetch","version":"1.15.0"}}} +STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{"sampling":{}},"clientInfo":{"name":"goose","version":"0.0.0"}}} +STDOUT: {"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2025-03-26","capabilities":{"experimental":{},"prompts":{"listChanged":false},"tools":{"listChanged":false}},"serverInfo":{"name":"mcp-fetch","version":"1.19.0"}}} STDIN: {"jsonrpc":"2.0","method":"notifications/initialized"} STDIN: {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"_meta":{"progressToken":0},"name":"fetch","arguments":{"url":"https://example.com"}}} -STDERR: npm error code FETCH_ERROR -STDERR: npm error errno FETCH_ERROR -STDERR: npm error invalid json response body at https://blocked.teams.cloudflare.com/?account_id=1e25787f854fa4b713d08a859d3e16ed&background_color=%23000000&block_reason=This+has+been+blocked+as+part+of+the+Dependency+Confusion+threat.+Please+see+go%2Fdependencyconfusionpypi+and+go%2Fdependencyconfusionnpm+for+more+info.&device_id=***&footer_text=The+website+you+are+trying+to+access+has+been+blocked+because+it+presents+a+risk+to+the+safety+and+security+of+Block%E2%80%99s+IT+systems.&header_text=This+page+presents+a+risk+to+Block&location=cf1ebd1203624140846ced63a200519e&logo_path=https%3A%2F%2Fmedia.block.xyz%2Flogos%2Fblock-jewel_white.png&mailto_address=&mailto_subject=&name=Block%2C+Inc.¶ms_sign=yrMcT5HYDMHvixy%2BdLHApce3BcNYIdlI8qh3wTcIrLA%3D&query_id=***&rule_id=***&source_ip=2a09%3Abac0%3A1000%3A2df%3A%3A281%3Ac0&suppress_footer=false&url=registry.npmjs.org&user_id=*** reason: Unexpected token '<', " -STDERR: npm error >>>, +} + +impl HeaderCapture { + fn new() -> Self { + Self { + captured_headers: Arc::new(Mutex::new(Vec::new())), + } + } + + fn capture_session_header(&self, req: &Request) { + let session_id = req + .headers + .get(SESSION_ID_HEADER) + .map(|v| v.to_str().unwrap().to_string()); + self.captured_headers.lock().unwrap().push(session_id); + } + + fn get_captured(&self) -> Vec> { + self.captured_headers.lock().unwrap().clone() + } +} + +fn create_test_provider(mock_server_url: &str) -> Box { + let api_client = ApiClient::new( + mock_server_url.to_string(), + AuthMethod::BearerToken("test-key".to_string()), + ) + .unwrap(); + let model = ModelConfig::new_or_fail("gpt-5-nano"); + Box::new(OpenAiProvider::new(api_client, model)) +} + +async fn setup_mock_server() -> (MockServer, HeaderCapture, Box) { + let mock_server = MockServer::start().await; + let capture = HeaderCapture::new(); + let capture_clone = capture.clone(); + + Mock::given(method("POST")) + .and(path("/v1/chat/completions")) + .respond_with(move |req: &Request| { + capture_clone.capture_session_header(req); + ResponseTemplate::new(200).set_body_json(json!({ + "choices": [{ + "finish_reason": "stop", + "index": 0, + "message": { + "content": "Hi there! How can I help you today?", + "role": "assistant" + } + }], + "created": 1755133833, + "id": "chatcmpl-test", + "model": "gpt-5-nano", + "usage": { + "completion_tokens": 10, + "prompt_tokens": 8, + "total_tokens": 18 + } + })) + }) + .mount(&mock_server) + .await; + + let provider = create_test_provider(&mock_server.uri()); + (mock_server, capture, provider) +} + +async fn make_request(provider: &dyn Provider, session_id: Option<&str>) { + let message = Message::user().with_text("test message"); + let request_fn = async { + provider + .complete("You are a helpful assistant.", &[message], &[]) + .await + .unwrap() + }; + + match session_id { + Some(id) => { + session_context::with_session_id(Some(id.to_string()), request_fn).await; + } + None => { + request_fn.await; + } + } +} + +#[tokio::test] +async fn test_session_id_propagation_to_llm() { + let (_, capture, provider) = setup_mock_server().await; + + make_request(provider.as_ref(), Some("integration-test-session-123")).await; + + assert_eq!( + capture.get_captured(), + vec![Some("integration-test-session-123".to_string())] + ); +} + +#[tokio::test] +async fn test_no_session_id_when_absent() { + let (_, capture, provider) = setup_mock_server().await; + + make_request(provider.as_ref(), None).await; + + assert_eq!(capture.get_captured(), vec![None]); +} + +#[tokio::test] +async fn test_session_id_matches_across_calls() { + let (_, capture, provider) = setup_mock_server().await; + + let test_session_id = "consistent-session-456"; + make_request(provider.as_ref(), Some(test_session_id)).await; + make_request(provider.as_ref(), Some(test_session_id)).await; + make_request(provider.as_ref(), Some(test_session_id)).await; + + assert_eq!( + capture.get_captured(), + vec![Some(test_session_id.to_string()); 3] + ); +} + +#[tokio::test] +async fn test_different_sessions_have_different_ids() { + let (_, capture, provider) = setup_mock_server().await; + + let session_id_1 = "session-one"; + let session_id_2 = "session-two"; + make_request(provider.as_ref(), Some(session_id_1)).await; + make_request(provider.as_ref(), Some(session_id_2)).await; + + assert_eq!( + capture.get_captured(), + vec![ + Some(session_id_1.to_string()), + Some(session_id_2.to_string()) + ] + ); +} diff --git a/crates/goose/tests/task_types_tests.rs b/crates/goose/tests/task_types_tests.rs deleted file mode 100644 index 970d0286bd5d..000000000000 --- a/crates/goose/tests/task_types_tests.rs +++ /dev/null @@ -1,126 +0,0 @@ -use goose::agents::subagent_execution_tool::task_types::{Task, TaskType}; -use serde_json::json; - -#[test] -fn test_task_type_serialization() { - // Test that TaskType serializes to the expected string format - assert_eq!( - serde_json::to_string(&TaskType::InlineRecipe).unwrap(), - "\"inline_recipe\"" - ); - assert_eq!( - serde_json::to_string(&TaskType::SubRecipe).unwrap(), - "\"sub_recipe\"" - ); -} - -#[test] -fn test_task_type_deserialization() { - // Test that strings deserialize to the correct TaskType variants - assert_eq!( - serde_json::from_str::("\"inline_recipe\"").unwrap(), - TaskType::InlineRecipe - ); - assert_eq!( - serde_json::from_str::("\"sub_recipe\"").unwrap(), - TaskType::SubRecipe - ); -} - -#[test] -fn test_task_serialization_with_enum() { - let task = Task { - id: "test-id".to_string(), - task_type: TaskType::InlineRecipe, - payload: json!({"recipe": "test"}), - }; - - let serialized = serde_json::to_value(&task).unwrap(); - assert_eq!(serialized["id"], "test-id"); - assert_eq!(serialized["task_type"], "inline_recipe"); - assert_eq!(serialized["payload"]["recipe"], "test"); -} - -#[test] -fn test_task_deserialization_with_string() { - // Test backward compatibility - JSON with string task_type should deserialize - let json_str = r#"{ - "id": "test-id", - "task_type": "sub_recipe", - "payload": {"sub_recipe": {"name": "test"}} - }"#; - - let task: Task = serde_json::from_str(json_str).unwrap(); - assert_eq!(task.id, "test-id"); - assert_eq!(task.task_type, TaskType::SubRecipe); -} - -#[test] -fn test_task_type_display() { - assert_eq!(TaskType::InlineRecipe.to_string(), "inline_recipe"); - assert_eq!(TaskType::SubRecipe.to_string(), "sub_recipe"); -} - -#[test] -fn test_task_methods_with_sub_recipe() { - let task = Task { - id: "test-1".to_string(), - task_type: TaskType::SubRecipe, - payload: json!({ - "sub_recipe": { - "name": "test_recipe", - "recipe_path": "/path/to/recipe", - "command_parameters": {"key": "value"}, - "sequential_when_repeated": true - } - }), - }; - - assert!(task.get_sub_recipe().is_some()); - assert_eq!(task.get_sub_recipe_name(), Some("test_recipe")); - assert_eq!(task.get_sub_recipe_path(), Some("/path/to/recipe")); - assert!(task.get_command_parameters().is_some()); - assert!(task.get_sequential_when_repeated()); -} - -#[test] -fn test_task_methods_with_inline_recipe() { - let task = Task { - id: "test-3".to_string(), - task_type: TaskType::InlineRecipe, - payload: json!({ - "recipe": { - "instructions": "Test instructions" - }, - "return_last_only": true - }), - }; - - assert!(task.get_sub_recipe().is_none()); - assert!(task.get_sub_recipe_name().is_none()); - assert!(task.get_sub_recipe_path().is_none()); - assert!(task.get_command_parameters().is_none()); - assert!(!task.get_sequential_when_repeated()); -} - -#[test] -fn test_invalid_task_type_deserialization() { - // Test that invalid task_type strings fail to deserialize - let result = serde_json::from_str::("\"invalid_type\""); - assert!(result.is_err()); -} - -#[test] -fn test_task_with_missing_fields() { - let task = Task { - id: "test-4".to_string(), - task_type: TaskType::SubRecipe, - payload: json!({}), // Missing sub_recipe field - }; - - assert!(task.get_sub_recipe().is_none()); - assert!(task.get_sub_recipe_name().is_none()); - assert!(task.get_sub_recipe_path().is_none()); - assert!(task.get_command_parameters().is_none()); - assert!(!task.get_sequential_when_repeated()); -} diff --git a/crates/goose/tests/test_support.rs b/crates/goose/tests/test_support.rs index 6bba89e9198b..49dd89d885b7 100644 --- a/crates/goose/tests/test_support.rs +++ b/crates/goose/tests/test_support.rs @@ -345,7 +345,6 @@ impl ScheduleToolTestBuilder { paused: false, current_session_id: None, process_start_time: None, - execution_mode: Some("background".to_string()), }; { let mut jobs = self.scheduler.jobs.lock().await; @@ -397,5 +396,6 @@ pub fn create_test_session_metadata(message_count: usize, working_dir: &str) -> conversation: None, message_count, user_recipe_values: None, + session_type: Default::default(), } } diff --git a/documentation/.gitignore b/documentation/.gitignore index 70973d918f6f..83d108cffde3 100644 --- a/documentation/.gitignore +++ b/documentation/.gitignore @@ -16,5 +16,3 @@ .env.production.local npm-debug.log* -yarn-debug.log* -yarn-error.log* diff --git a/documentation/README.md b/documentation/README.md index 9edbce8a6761..d9b319730058 100644 --- a/documentation/README.md +++ b/documentation/README.md @@ -5,13 +5,13 @@ This website is built using [Docusaurus](https://docusaurus.io/), a modern stati ### Installation ``` -$ yarn +$ npm i ``` ### Local Development ``` -$ yarn start +$ npm run start ``` This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. @@ -19,7 +19,7 @@ This command starts a local development server and opens up a browser window. Mo ### Build ``` -$ yarn build +$ npm run build ``` This command generates static content into the `build` directory and can be served using any static contents hosting service. @@ -29,13 +29,13 @@ This command generates static content into the `build` directory and can be serv Using SSH: ``` -$ USE_SSH=true yarn deploy +$ USE_SSH=true npm run deploy ``` Not using SSH: ``` -$ GIT_USER= yarn deploy +$ GIT_USER= npm run deploy ``` If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. diff --git a/documentation/blog/2025-03-31-goose-benchmark/index.md b/documentation/blog/2025-03-31-goose-benchmark/index.md index 2e30c4ba6a6e..0378619712bd 100644 --- a/documentation/blog/2025-03-31-goose-benchmark/index.md +++ b/documentation/blog/2025-03-31-goose-benchmark/index.md @@ -23,7 +23,7 @@ These community evals aren't the rigorous, peer-reviewed benchmarks that researc In this spirit, we're introducing our **Goose Vibe Check** leaderboard. -Thank you to the Ollama team for their help and support in our experimentation contributing to this blog! We used Ollama’s [structured outputs](https://ollama.com/blog/structured-outputs) feature to enable our [toolshim implementation](https://block.github.io/goose/docs/experimental/ollama) (more below) and their recently released [context length parameter override](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-specify-the-context-window-size) to enable testing on longer contexts. +Thank you to the Ollama team for their help and support in our experimentation contributing to this blog! We used Ollama’s [structured outputs](https://ollama.com/blog/structured-outputs) feature to enable our [toolshim implementation](https://block.github.io/goose/docs/experimental/ollama) (more below) and their recently released [context length parameter override](https://github.com/ollama/ollama/blob/main/docs/faq.mdx#how-can-i-specify-the-context-window-size) to enable testing on longer contexts. ## Leaderboard diff --git a/documentation/blog/2025-09-26-hacktoberfest-2025/index.md b/documentation/blog/2025-09-26-hacktoberfest-2025/index.md index c55f2e291cf8..34240f7a8e7d 100644 --- a/documentation/blog/2025-09-26-hacktoberfest-2025/index.md +++ b/documentation/blog/2025-09-26-hacktoberfest-2025/index.md @@ -34,7 +34,7 @@ To help you make the most of each of your contributions, below is a quick guide ## ✅ Key Rules 1. Read the [code of conduct](https://github.com/block/.github/blob/main/CODE_OF_CONDUCT.md). -2. Refer to the [Responsible AI-Assisted Coding Guide](https://github.com/block/goose/blob/main/ai-assisted-coding-guide.md), [Contributing Guide](https://github.com/block/goose/blob/main/CONTRIBUTING.md) & [README](https://github.com/block/goose/blob/main/README.md) for your contributions. +2. Refer to the [Responsible AI-Assisted Coding Guide](https://github.com/block/goose/blob/main/HOWTOAI.md), [Contributing Guide](https://github.com/block/goose/blob/main/CONTRIBUTING.md) & [README](https://github.com/block/goose/blob/main/README.md) for your contributions. 3. Choose a task from this project's Hacktoberfest [Project Hub](https://github.com/block/goose/issues/4705). Each issue has the 🏷️ `hacktoberfest` label. 4. Comment ".take" on the corresponding issue to get assigned the task. 5. Fork the repository and create a new branch for your work. diff --git a/documentation/docs/getting-started/installation.md b/documentation/docs/getting-started/installation.md index c7bb426991e4..0b521acacff5 100644 --- a/documentation/docs/getting-started/installation.md +++ b/documentation/docs/getting-started/installation.md @@ -44,7 +44,7 @@ import { PanelLeft } from 'lucide-react'; Ensure the `~/.config` directory has read and write access. - Goose needs this access to create the log directory and file. Once permissions are granted, the app should load correctly. For steps on how to do this, refer to the [Troubleshooting Guide](/docs/troubleshooting.md#macos-permission-issues) + Goose needs this access to create the log directory and file. Once permissions are granted, the app should load correctly. For steps on how to do this, refer to the [Known Issues Guide](/docs/troubleshooting/known-issues#macos-permission-issues) ::: @@ -376,10 +376,10 @@ You can change your LLM provider and/or model or update your API key at any time ## Shared Configuration Settings -The Goose CLI and Desktop UI share all core configurations, including LLM provider settings, model selection, and extension configurations. When you install or configure extensions in either interface, the settings are stored in a central location at `~/.config/goose/config.yaml`, making them available to both the Desktop application and CLI. This makes it convenient to switch between interfaces while maintaining consistent settings. +The Goose CLI and Desktop UI share all core configurations, including LLM provider settings, model selection, and extension configurations. When you install or configure extensions in either interface, the settings are stored in a central location, making them available to both the Desktop application and CLI. This makes it convenient to switch between interfaces while maintaining consistent settings. For more information, visit the [Config Files][config-files] guide. :::info -While core configurations are shared between interfaces, extensions have flexibility in how they store authentication credentials. Some extensions may use the shared config file while others implement their own storage methods. +While core configurations are shared between interfaces, extensions have flexibility in how they store authentication credentials. Some extensions may use the shared config files while others implement their own storage methods. ::: @@ -404,3 +404,4 @@ You can also configure Extensions to extend Goose's functionality, including add [providers]: /docs/getting-started/providers [handling-rate-limits]: /docs/guides/handling-llm-rate-limits-with-goose [mcp]: https://www.anthropic.com/news/model-context-protocol +[config-files]: /docs/guides/config-files.md diff --git a/documentation/docs/getting-started/providers.md b/documentation/docs/getting-started/providers.md index 05bbc01896e6..5f3378dd4d91 100644 --- a/documentation/docs/getting-started/providers.md +++ b/documentation/docs/getting-started/providers.md @@ -668,7 +668,7 @@ Here are some local providers we support: ``` :::tip Context Length - If you notice that goose is having trouble using extensions or is ignoring [.goosehints](/docs/guides/using-goosehints), it is likely that the model's default context length of 4096 tokens is too low. Set the `OLLAMA_CONTEXT_LENGTH` environment variable to a [higher value](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-specify-the-context-window-size). + If you notice that goose is having trouble using extensions or is ignoring [.goosehints](/docs/guides/using-goosehints), it is likely that the model's default context length of 4096 tokens is too low. Set the `OLLAMA_CONTEXT_LENGTH` environment variable to a [higher value](https://github.com/ollama/ollama/blob/main/docs/faq.mdx#how-can-i-specify-the-context-window-size). ::: diff --git a/documentation/docs/getting-started/using-extensions.md b/documentation/docs/getting-started/using-extensions.md index ced2fdd71340..23776cb383de 100644 --- a/documentation/docs/getting-started/using-extensions.md +++ b/documentation/docs/getting-started/using-extensions.md @@ -7,28 +7,34 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import { PanelLeft, Settings } from 'lucide-react'; -Extensions are add-ons that provide a way to extend the functionality of Goose by connecting with applications and tools you already use in your workflow. These extensions can be used to add new features, access data and resources, or integrate with other systems. +Extensions are add-ons that provide a way to extend the functionality of goose by connecting with applications and tools you already use in your workflow. These extensions can be used to add new features, access data and resources, or integrate with other systems. Extensions are based on the [Model Context Protocol (MCP)](https://github.com/modelcontextprotocol), so you can connect -Goose to a wide ecosystem of capabilities. +goose to a wide ecosystem of capabilities. -Goose automatically checks external extensions for known malware before activation. If a malicious package is detected, the [extension will be blocked](/docs/troubleshooting#malicious-package-detected) with a clear error message. +goose automatically checks external extensions for known malware before activation. If a malicious package is detected, the [extension will be blocked](/docs/troubleshooting/known-issues#malicious-package-detected) with a clear error message. :::tip Tutorials -Check out the [step-by-step tutorials](/docs/category/mcp-servers) for adding and using several Goose Extensions +Check out the [step-by-step tutorials](/docs/category/mcp-servers) for adding and using various goose extensions ::: ## Built-in Extensions -Out of the box, Goose is installed with a few extensions but with only the `Developer` extension enabled by default. +goose includes several built-in extensions you can start using out of the box: -Here are the built-in extensions: - -- [Developer](/docs/mcp/developer-mcp): Provides a set of general development tools that are useful for software development. +- [Developer](/docs/mcp/developer-mcp): Provides a set of general development tools that are useful for software development. The Developer extension is **enabled by default**. - [Computer Controller](/docs/mcp/computer-controller-mcp): Provides general computer control tools for webscraping, file caching, and automations. -- [Memory](/docs/mcp/memory-mcp): Teaches Goose to remember your preferences as you use it. -- [Tutorial](/docs/mcp/tutorial-mcp): Provides interactive tutorials for learning about Goose. +- [Memory](/docs/mcp/memory-mcp): Teaches goose to remember your preferences as you use it. +- [Tutorial](/docs/mcp/tutorial-mcp): Provides interactive tutorials for learning about goose. - [Auto Visualiser](/docs/mcp/autovisualiser-mcp): Automatically generates graphical data visualizations in conversations. +:::warning Access Control +goose operates autonomously by default. Combined with the Developer extension's tools, this means goose can execute commands and modify files without your approval. If you want more control over this behavior, you can configure the [goose permission mode](/docs/guides/goose-permissions), [tool permissions](/docs/guides/managing-tools/tool-permissions), and [.gooseignore files](/docs/guides/using-gooseignore). See [Configuring Access Controls](/docs/mcp/developer-mcp#configuring-access-controls) for a brief overview. +::: + +In addition, these platform extensions provide core goose functionality. They are also always available and enabled by default: + +- Extension Manager: Discover, enable, and disable extensions dynamically during sessions +- [Todo](/docs/mcp/todo-mcp): Manage task lists and track progress across sessions #### Toggling Built-in Extensions @@ -50,21 +56,17 @@ Here are the built-in extensions: To navigate through available extensions: 1. Run the following command: - ```sh - goose configure - ``` - 2. Select `Add Extension` from the menu. - 3. Choose the type of extension you'd like to add: - - `Built-In Extension`: Use an extension that comes pre-installed with Goose. - - `Command-Line Extension`: Add a local command or script to run as an extension. - - `Remote Extension (SSE)`: Connect to a remote system via SSE (Server-Sent Events). - - `Remote Extension (Streaming HTTP)`: Connect to a remote system via Streaming HTTP - 4. Follow the prompts based on the type of extension you selected. + ```sh + goose configure + ``` + 2. Select `Add Extension` from the menu. Use the up and down keys to highlight your choice then press `Enter`. + 3. Select `Built-In Extension`. + 4. Select the extension to enable. + 5. Provide a timeout for the extension (in seconds). + 6. Press `Enter`. **Example: Adding Built-in Extension** - To select an option during configuration, hover over it and press Enter. - ``` ┌ goose-configure │ @@ -74,33 +76,29 @@ Here are the built-in extensions: ◇ What type of extension would you like to add? │ Built-in Extension │ - ◆ Which built-in extension would you like to enable? - │ ○ Developer Tools - │ ○ Computer Controller (controls for webscraping, file caching, and automations) - │ ○ Google Drive - │ ○ Memory - │ ● JetBrains + ◇ Which built-in extension would you like to enable? + │ Auto Visualiser │ ◇ Please set the timeout for this tool (in secs): │ 300 │ - └ Enabled jetbrains extension + └ Enabled Auto Visualiser extension ``` :::info -All of Goose's built-in extensions are MCP servers in their own right. If you'd like -to use the MCP servers included with Goose with any other agent, you are free to do so. +goose's built-in extensions are MCP servers in their own right. If you'd like +to use the MCP servers included with goose with any other agent, you are free to do so. ::: ## Discovering Extensions -Goose provides a [central directory][extensions-directory] of extensions that you can install and use. +goose provides a [central directory][extensions-directory] of extensions that you can install and use. -You can also add any other [MCP Server](#mcp-servers) as a Goose extension, even if it's not listed in our directory. +You can also add any other [MCP Server](#mcp-servers) as a goose extension, even if it's not listed in our directory. ## Adding Extensions @@ -108,12 +106,12 @@ You can also add any other [MCP Server](#mcp-servers) as a Goose extension, even Extensions can be installed directly via the [extensions directory][extensions-directory], CLI, or UI. :::warning Airgapped Environments -If you're in a corporate or airgapped environment and extensions fail to activate, see [Airgapped/Offline Environments](/docs/troubleshooting#airgappedoffline-environment-issues) for workarounds. +If you're in a corporate or airgapped environment and extensions fail to activate, see [Airgapped/Offline Environments](/docs/troubleshooting/known-issues#airgappedoffline-environment-issues) for workarounds. ::: ### MCP Servers -You can install any MCP server as a Goose extension. +You can install any MCP server as a goose extension. :::tip MCP Server Directory See available servers in the **[MCP Server Directory](https://www.pulsemcp.com/servers)**. @@ -127,7 +125,7 @@ See available servers in the **[MCP Server Directory](https://www.pulsemcp.com/s 3. Under `Extensions`, click `Add custom extension`. 4. On the `Add custom extension` modal, enter the necessary details - If adding an environment variable, click `Add` button to the right of the variable - - The `Timeout` field lets you set how long Goose should wait for a tool call from this extension to complete + - The `Timeout` field lets you set how long goose should wait for a tool call from this extension to complete 5. Click `Add` button #### Example of adding the [Knowledge Graph Memory MCP Server](https://github.com/modelcontextprotocol/servers/tree/main/src/memory): @@ -149,7 +147,7 @@ See available servers in the **[MCP Server Directory](https://www.pulsemcp.com/s 2. Select `Add Extension` from the menu. 3. Choose the type of extension you'd like to add: - - `Built-In Extension`: Use an extension that comes pre-installed with Goose. + - `Built-In Extension`: Use an extension that comes pre-installed with goose. - `Command-Line Extension`: Add a local command or script to run as an extension. - `Remote Extension (SSE)`: Connect to a remote system via SSE (Server-Sent Events). - `Remote Extension (Streaming HTTP)`: Connect to a remote system via Streaming HTTP @@ -258,7 +256,7 @@ Note: Java and Kotlin extensions are only support on Linux and macOS ### Deeplinks -Extensions can be installed using Goose's deep link protocol. The URL format varies based on the extension type: +Extensions can be installed using goose's deep link protocol. The URL format varies based on the extension type: @@ -357,7 +355,7 @@ You can enable or disable installed extensions based on your workflow needs. - 1. Run the following command to open up Goose's configurations: + 1. Run the following command to open up goose's configurations: ```sh goose configure ``` @@ -383,9 +381,9 @@ You can enable or disable installed extensions based on your workflow needs. ## Automatically Enabled Extensions -The Smart Extension Recommendation system in Goose automatically identifies and suggests relevant extensions based on your tasks and needs. This section explains how to use this feature effectively and understand its capabilities and limitations. +The Smart Extension Recommendation system in goose automatically identifies and suggests relevant extensions based on your tasks and needs. This section explains how to use this feature effectively and understand its capabilities and limitations. -When you request a task, Goose checks its enabled extensions and their tools to determine if it can fulfill the request. If not, it suggests or enables additional extensions as needed. You can also request specific extensions by name. +When you request a task, goose checks its enabled extensions and their tools to determine if it can fulfill the request. If not, it suggests or enables additional extensions as needed. You can also request specific extensions by name. :::warning @@ -394,17 +392,17 @@ Any extensions enabled dynamically are only enabled for the current session. To ### Automatic Detection -Goose automatically detects when an extension is needed based on your task requirements. Here's an example of how Goose identifies and enables a needed extension during a conversation: +goose automatically detects when an extension is needed based on your task requirements. Here's an example of how goose identifies and enables a needed extension during a conversation: -#### Goose Prompt +#### goose Prompt ```plaintext Find all orders with pending status from our production database ``` -#### Goose Output +#### goose Output ```plaintext I'll help you search for available extensions that might help us interact with PostgreSQL databases. @@ -426,12 +424,12 @@ Great! Now I can help you query the database... -#### Goose Prompt +#### goose Prompt ```plaintext Find all orders with pending status from our production database ``` -#### Goose Output +#### goose Output ```sh I apologize, but I notice that I don't currently have access to your database. Let me search if there are any database-related extensions available. @@ -442,7 +440,7 @@ I see that there is a "postgresql" extension available. Let me enable it so I ca extension_name: postgresql -■ Goose would like to enable the following extension, do you approve? +■ goose would like to enable the following extension, do you approve? // highlight-start | ● Yes, for this session // highlight-end @@ -454,18 +452,18 @@ extension_name: postgresql ### Direct Request -Goose responds to explicit requests for extensions, allowing users to manually enable specific tools they need. Here's an example of how Goose handles a direct request to enable an extension: +goose responds to explicit requests for extensions, allowing users to manually enable specific tools they need. Here's an example of how goose handles a direct request to enable an extension: -#### Goose Prompt +#### goose Prompt ```plaintext Use PostgreSQL extension ``` -#### Goose Output +#### goose Output ```plaintext I'll help enable the PostgreSQL extension for you. @@ -482,13 +480,13 @@ The PostgreSQL extension is now ready to use. What would you like to do with it? -#### Goose Prompt +#### goose Prompt ```sh Use the PostgreSQL extension ``` -#### Goose Output +#### goose Output ```sh I'll help enable the PostgreSQL extension for you. @@ -496,7 +494,7 @@ I'll help enable the PostgreSQL extension for you. extension_name: postgresql -■ Goose would like to enable the following extension, do you approve? +■ goose would like to enable the following extension, do you approve? // highlight-start | ● Yes, for this session // highlight-end @@ -508,7 +506,7 @@ extension_name: postgresql ## Updating Extension Properties -Goose relies on extension properties to determine how to handle an extension. You can edit these properties if you want to change the extension's display settings and behavior, such as the name, timeout, or environment variables. +goose relies on extension properties to determine how to handle an extension. You can edit these properties if you want to change the extension's display settings and behavior, such as the name, timeout, or environment variables. @@ -523,7 +521,7 @@ Goose relies on extension properties to determine how to handle an extension. Yo - 1. Navigate to the Goose [configuration file](/docs/guides/config-file). For example, navigate to `~/.config/goose/config.yaml` on macOS. + 1. Navigate to the goose [configuration file](/docs/guides/config-files). For example, navigate to `~/.config/goose/config.yaml` on macOS. 2. Edit the extension properties as needed and save your changes. @@ -548,7 +546,7 @@ You can remove installed extensions. To remove an extension, you must [disable](#enablingdisabling-extensions) it first. ::: - 1. Run the following command to open up Goose's configurations: + 1. Run the following command to open up goose's configurations: ```sh goose configure ``` @@ -572,7 +570,7 @@ You can remove installed extensions. ## Starting Session with Extensions -You can start a tailored Goose session with specific extensions directly from the CLI. +You can start a tailored goose session with specific extensions directly from the CLI. :::info Notes * The extension will not be installed. It will only be enabled for the current session. @@ -664,6 +662,6 @@ goose session --with-streamable-http-extension "https://example.com/streamable" ## Developing Extensions -Goose extensions are implemented with MCP, a standard protocol that allows AI models and agents to securely connect with local or remote resources. Learn how to build your own [extension as an MCP server](https://modelcontextprotocol.io/quickstart/server). +goose extensions are implemented with MCP, a standard protocol that allows AI models and agents to securely connect with local or remote resources. Learn how to build your own [extension as an MCP server](https://modelcontextprotocol.io/quickstart/server). [extensions-directory]: /extensions diff --git a/documentation/docs/guides/acp-clients.md b/documentation/docs/guides/acp-clients.md index 0f072d9ebfe1..815d0dc06886 100644 --- a/documentation/docs/guides/acp-clients.md +++ b/documentation/docs/guides/acp-clients.md @@ -77,7 +77,7 @@ You should now be able to interact with Goose directly in Zed. Your ACP sessions ### Advanced Configuration -By default, Goose will use the provider and model defined in your [configuration file](/docs/guides/config-file). You can override this for specific ACP configurations using the `GOOSE_PROVIDER` and `GOOSE_MODEL` environment variables. +By default, Goose will use the provider and model defined in your [configuration file](/docs/guides/config-files). You can override this for specific ACP configurations using the `GOOSE_PROVIDER` and `GOOSE_MODEL` environment variables. The following Zed settings example configures two Goose agent instances. This is useful for: - Comparing model performance on the same task diff --git a/documentation/docs/guides/codebase-analysis.md b/documentation/docs/guides/codebase-analysis.md index a2299070963c..dd896eedfa11 100644 --- a/documentation/docs/guides/codebase-analysis.md +++ b/documentation/docs/guides/codebase-analysis.md @@ -41,7 +41,7 @@ This shows that `authenticate` is a central function in your authentication flow ## Analysis Modes -The `analyze` tool operates in three modes—Structure, Semantic, and Focus—depending on whether you’re analyzing directories, files, or symbols. Invoke it through natural language or direct commands with [parameters](#parameters). +The `analyze` tool operates in three modes—Structure, Semantic, and Focus—depending on whether you’re analyzing directories, files, or symbols. Invoke it through natural language or direct commands with [parameters](#common-parameters). ### Understanding Project Organization diff --git a/documentation/docs/guides/config-file.md b/documentation/docs/guides/config-files.md similarity index 74% rename from documentation/docs/guides/config-file.md rename to documentation/docs/guides/config-files.md index fb8f953a320d..c546627ec409 100644 --- a/documentation/docs/guides/config-file.md +++ b/documentation/docs/guides/config-files.md @@ -1,17 +1,24 @@ --- sidebar_position: 85 -title: Configuration File -sidebar_label: Configuration File +title: Configuration Files +sidebar_label: Configuration Files --- -# Configuration File +# Configuration Overview -Goose uses a YAML configuration file to manage settings and extensions. This file is located at: +goose uses YAML [configuration files](#configuration-files) to manage settings and extensions. The primary config file is located at: * macOS/Linux: `~/.config/goose/config.yaml` * Windows: `%APPDATA%\Block\goose\config\config.yaml` -The configuration file allows you to set default behaviors, configure language models, and manage extensions. While many settings can also be set using [environment variables](/docs/guides/environment-variables), the config file provides a persistent way to maintain your preferences. +The configuration files allow you to set default behaviors, configure language models, set tool permissions, and manage extensions. While many settings can also be set using [environment variables](/docs/guides/environment-variables), the config files provide a persistent way to maintain your preferences. + +## Configuration Files + +- **config.yaml** - Provider, model, extensions, and general settings +- **permission.yaml** - Tool permission levels configured via `goose configure` +- **secrets.yaml** - API keys and secrets (only when keyring is disabled) +- **permissions/tool_permissions.json** - Runtime permission decisions (auto-managed) ## Global Settings @@ -22,7 +29,7 @@ The following settings can be configured at the root level of your config.yaml f | `GOOSE_PROVIDER` | Primary [LLM provider](/docs/getting-started/providers) | "anthropic", "openai", etc. | None | Yes | | `GOOSE_MODEL` | Default model to use | Model name (e.g., "claude-3.5-sonnet", "gpt-4") | None | Yes | | `GOOSE_TEMPERATURE` | Model response randomness | Float between 0.0 and 1.0 | Model-specific | No | -| `GOOSE_MODE` | [Tool execution behavior](/docs/guides/goose-permissions) | "auto", "approve", "chat", "smart_approve" | "smart_approve" | No | +| `GOOSE_MODE` | [Tool execution behavior](/docs/guides/goose-permissions) | "auto", "approve", "chat", "smart_approve" | "auto" | No | | `GOOSE_MAX_TURNS` | [Maximum number of turns](/docs/guides/sessions/smart-context-management#maximum-turns) allowed without user input | Integer (e.g., 10, 50, 100) | 1000 | No | | `GOOSE_LEAD_PROVIDER` | Provider for lead model in [lead/worker mode](/docs/guides/environment-variables#leadworker-model-configuration) | Same as `GOOSE_PROVIDER` options | Falls back to `GOOSE_PROVIDER` | No | | `GOOSE_LEAD_MODEL` | Lead model for lead/worker mode | Model name | None | No | @@ -35,7 +42,9 @@ The following settings can be configured at the root level of your config.yaml f | `GOOSE_CLI_SHOW_COST` | Show estimated cost for token use in the CLI | true/false | false | No | | `GOOSE_ALLOWLIST` | URL for allowed extensions | Valid URL | None | No | | `GOOSE_RECIPE_GITHUB_REPO` | GitHub repository for recipes | Format: "org/repo" | None | No | -| `GOOSE_AUTO_COMPACT_THRESHOLD` | Set the percentage threshold at which Goose [automatically summarizes your session](/docs/guides/sessions/smart-context-management#automatic-compaction). | Float between 0.0 and 1.0 (disabled at 0.0)| 0.8 | No | +| `GOOSE_AUTO_COMPACT_THRESHOLD` | Set the percentage threshold at which goose [automatically summarizes your session](/docs/guides/sessions/smart-context-management#automatic-compaction). | Float between 0.0 and 1.0 (disabled at 0.0)| 0.8 | No | +| `otel_exporter_otlp_endpoint` | OTLP endpoint URL for [observability](/docs/guides/environment-variables#opentelemetry-protocol-otlp) | URL (e.g., `http://localhost:4318`) | None | No | +| `otel_exporter_otlp_timeout` | Export timeout in milliseconds for [observability](/docs/guides/environment-variables#opentelemetry-protocol-otlp) | Integer (ms) | 10000 | No | | `security_prompt_enabled` | Enable [prompt injection detection](/docs/guides/security/prompt-injection-detection) to identify potentially harmful commands | true/false | false | No | | `security_prompt_threshold` | Sensitivity threshold for [prompt injection detection](/docs/guides/security/prompt-injection-detection) (higher = stricter) | Float between 0.01 and 1.0 | 0.7 | No | @@ -75,6 +84,10 @@ GOOSE_CLI_MIN_PRIORITY: 0.2 # Recipe Configuration GOOSE_RECIPE_GITHUB_REPO: "block/goose-recipes" +# Observability (OpenTelemetry) +otel_exporter_otlp_endpoint: "http://localhost:4318" +otel_exporter_otlp_timeout: 20000 + # Security Configuration security_prompt_enabled: true @@ -102,10 +115,10 @@ Extensions are configured under the `extensions` key. Each extension can have th ```yaml extensions: extension_name: - bundled: true/false # Whether it's included with Goose - display_name: "Name" # Human-readable name (optional) - enabled: true/false # Whether the extension is active - name: "extension_name" # Internal name + bundled: true/false # Whether it's included with goose + display_name: "Name" # Human-readable name (optional) + enabled: true/false # Whether the extension is active + name: "extension_name" # Internal name timeout: 300 # Operation timeout in seconds type: "builtin"/"stdio" # Extension type @@ -113,8 +126,8 @@ extensions: cmd: "command" # Command to execute args: ["arg1", "arg2"] # Command arguments description: "text" # Extension description - env_keys: [] # Required environment variables - envs: {} # Environment values + env_keys: [] # Required environment variables + envs: {} # Environment values ``` ## Configuration Priority @@ -133,7 +146,7 @@ Settings are applied in the following order of precedence: ## Updating Configuration -Changes to the config file require restarting Goose to take effect. You can verify your current configuration using: +Changes to config files require restarting goose to take effect. You can verify your current configuration using: ```bash goose info -v diff --git a/documentation/docs/guides/environment-variables.md b/documentation/docs/guides/environment-variables.md index 2ae552a1ba19..0b19b03a45fd 100644 --- a/documentation/docs/guides/environment-variables.md +++ b/documentation/docs/guides/environment-variables.md @@ -4,7 +4,7 @@ title: Environment Variables sidebar_label: Environment Variables --- -Goose supports various environment variables that allow you to customize its behavior. This guide provides a comprehensive list of available environment variables grouped by their functionality. +goose supports various environment variables that allow you to customize its behavior. This guide provides a comprehensive list of available environment variables grouped by their functionality. ## Model Configuration @@ -12,7 +12,7 @@ These variables control the [language models](/docs/getting-started/providers) a ### Basic Provider Configuration -These are the minimum required variables to get started with Goose. +These are the minimum required variables to get started with goose. | Variable | Purpose | Values | Default | |----------|---------|---------|---------| @@ -70,7 +70,7 @@ A _turn_ is one complete prompt-response interaction. Here's how it works with t - Fallback to the lead model if the worker model struggles for 2 consecutive turns - Use the lead model for 2 turns and then switch back to the worker model -The lead model and worker model names are displayed at the start of the Goose CLI session. If you don't export a `GOOSE_MODEL` for your session, the worker model defaults to the `GOOSE_MODEL` in your [configuration file](/docs/guides/config-file). +The lead model and worker model names are displayed at the start of the goose CLI session. If you don't export a `GOOSE_MODEL` for your session, the worker model defaults to the `GOOSE_MODEL` in your [configuration file](/docs/guides/config-files). **Examples** @@ -88,7 +88,7 @@ export GOOSE_LEAD_FALLBACK_TURNS=2 ### Planning Mode Configuration -These variables control Goose's [planning functionality](/docs/guides/multi-model/creating-plans). +These variables control goose's [planning functionality](/docs/guides/multi-model/creating-plans). | Variable | Purpose | Values | Default | |----------|---------|---------|---------| @@ -146,17 +146,18 @@ export DATABRICKS_MAX_RETRY_INTERVAL_MS=60000 # cap the maximum retry del ## Session Management -These variables control how Goose manages conversation sessions and context. +These variables control how goose manages conversation sessions and context. | Variable | Purpose | Values | Default | |----------|---------|---------|---------| -| `GOOSE_CONTEXT_STRATEGY` | Controls how Goose handles context limit exceeded situations | "summarize", "truncate", "clear", "prompt" | "prompt" (interactive), "summarize" (headless) | +| `GOOSE_CONTEXT_STRATEGY` | Controls how goose handles context limit exceeded situations | "summarize", "truncate", "clear", "prompt" | "prompt" (interactive), "summarize" (headless) | | `GOOSE_MAX_TURNS` | [Maximum number of turns](/docs/guides/sessions/smart-context-management#maximum-turns) allowed without user input | Integer (e.g., 10, 50, 100) | 1000 | +| `GOOSE_SUBAGENT_MAX_TURNS` | Sets the maximum turns allowed for a [subagent](/docs/guides/subagents) to complete before timeout | Integer (e.g., 25) | 25 | | `CONTEXT_FILE_NAMES` | Specifies custom filenames for [hint/context files](/docs/guides/using-goosehints#custom-context-files) | JSON array of strings (e.g., `["CLAUDE.md", ".goosehints"]`) | `[".goosehints"]` | | `GOOSE_CLI_THEME` | [Theme](/docs/guides/goose-cli-commands#themes) for CLI response markdown | "light", "dark", "ansi" | "dark" | | `GOOSE_RANDOM_THINKING_MESSAGES` | Controls whether to show amusing random messages during processing | "true", "false" | "true" | | `GOOSE_CLI_SHOW_COST` | Toggles display of model cost estimates in CLI output | "true", "1" (case insensitive) to enable | false | -| `GOOSE_AUTO_COMPACT_THRESHOLD` | Set the percentage threshold at which Goose [automatically summarizes your session](/docs/guides/sessions/smart-context-management#automatic-compaction). | Float between 0.0 and 1.0 (disabled at 0.0) | 0.8 | +| `GOOSE_AUTO_COMPACT_THRESHOLD` | Set the percentage threshold at which goose [automatically summarizes your session](/docs/guides/sessions/smart-context-management#automatic-compaction). | Float between 0.0 and 1.0 (disabled at 0.0) | 0.8 | **Examples** @@ -176,6 +177,9 @@ export GOOSE_MAX_TURNS=25 # Set a reasonable limit for production export GOOSE_MAX_TURNS=100 +# Customize subagent turn limit +export GOOSE_SUBAGENT_MAX_TURNS=50 + # Use multiple context files export CONTEXT_FILE_NAMES='["CLAUDE.md", ".goosehints", ".cursorrules", "project_rules.txt"]' @@ -194,7 +198,7 @@ export GOOSE_AUTO_COMPACT_THRESHOLD=0.6 ### Model Context Limit Overrides -These variables allow you to override the default context window size (token limit) for your models. This is particularly useful when using [LiteLLM proxies](https://docs.litellm.ai/docs/providers/litellm_proxy) or custom models that don't match Goose's predefined model patterns. +These variables allow you to override the default context window size (token limit) for your models. This is particularly useful when using [LiteLLM proxies](https://docs.litellm.ai/docs/providers/litellm_proxy) or custom models that don't match goose's predefined model patterns. | Variable | Purpose | Values | Default | |----------|---------|---------|---------| @@ -221,11 +225,11 @@ For more details and examples, see [Model Context Limit Overrides](/docs/guides/ ## Tool Configuration -These variables control how Goose handles [tool execution](/docs/guides/goose-permissions) and [tool management](/docs/guides/managing-tools/). +These variables control how goose handles [tool execution](/docs/guides/goose-permissions) and [tool management](/docs/guides/managing-tools/). | Variable | Purpose | Values | Default | |----------|---------|---------|---------| -| `GOOSE_MODE` | Controls how Goose handles tool execution | "auto", "approve", "chat", "smart_approve" | "smart_approve" | +| `GOOSE_MODE` | Controls how goose handles tool execution | "auto", "approve", "chat", "smart_approve" | "smart_approve" | | `GOOSE_ENABLE_ROUTER` | Enables [intelligent tool selection strategy](/docs/guides/managing-tools/tool-router) | "true", "false" | "false" | | `GOOSE_TOOLSHIM` | Enables/disables tool call interpretation | "1", "true" (case insensitive) to enable | false | | `GOOSE_TOOLSHIM_OLLAMA_MODEL` | Specifies the model for [tool call interpretation](/docs/experimental/ollama) | Model name (e.g. llama3.2, qwen2.5) | System default | @@ -294,7 +298,34 @@ When the keyring is disabled, secrets are stored here: * Windows: `%APPDATA%\Block\goose\config\secrets.yaml` ::: -## Langfuse Integration +## Observability + +Beyond Goose's built-in [logging system](/docs/guides/logs), you can export telemetry to external observability platforms for advanced monitoring, performance analysis, and production insights. + +### OpenTelemetry Protocol (OTLP) + +Configure goose to export traces and metrics to any OTLP-compatible observability platform. +OTLP is the standard protocol for sending telemetry collected by [OpenTelemetry](https://opentelemetry.io/docs/). When configured, goose exports telemetry asynchronously and flushes on exit. + +| Variable | Purpose | Values | Default | +|----------|---------|--------|---------| +| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP endpoint URL | URL (e.g., `http://localhost:4318`) | None | +| `OTEL_EXPORTER_OTLP_TIMEOUT` | Export timeout in milliseconds | Integer (ms) | `10000` | + +**When to use OTLP:** +- Diagnosing slow tool execution or LLM response times +- Understanding intermittent failures across multiple sessions +- Monitoring goose performance in production or CI/CD environments +- Tracking usage patterns, costs, and resource consumption over time +- Setting up alerts for performance degradation or high error rates + +**Example:** +```bash +export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318" +export OTEL_EXPORTER_OTLP_TIMEOUT=10000 +``` + +### Langfuse Integration These variables configure the [Langfuse integration for observability](/docs/tutorials/langfuse). @@ -349,28 +380,28 @@ export ALPHA_FEATURES=true ALPHA_FEATURES=true goose session ``` -## Variables Controlled by Goose +## Variables Controlled by goose -These variables are automatically set by Goose during command execution. +These variables are automatically set by goose during command execution. | Variable | Purpose | Values | Default | |----------|---------|---------|---------| -| `GOOSE_TERMINAL` | Indicates that a command is being executed by Goose, enables customizing shell behavior | "1" when set | Unset | +| `GOOSE_TERMINAL` | Indicates that a command is being executed by goose, enables customizing shell behavior | "1" when set | Unset | ### Customizing Shell Behavior -Sometimes you want Goose to use different commands or have different shell behavior than your normal terminal usage. For example, you might want Goose to use a different tool, or prevent Goose from running long-running development servers that could hang the AI agent. This is most useful when using Goose CLI, where shell commands are executed directly in your terminal environment. +Sometimes you want goose to use different commands or have different shell behavior than your normal terminal usage. For example, you might want goose to use a different tool, or prevent goose from running long-running development servers that could hang the AI agent. This is most useful when using goose CLI, where shell commands are executed directly in your terminal environment. **How it works:** -1. When Goose runs commands, `GOOSE_TERMINAL` is automatically set to "1" -2. Your shell configuration can detect this and direct Goose to change its default behavior while keeping your normal terminal usage unchanged +1. When goose runs commands, `GOOSE_TERMINAL` is automatically set to "1" +2. Your shell configuration can detect this and direct goose to change its default behavior while keeping your normal terminal usage unchanged **Example:** ```bash # In your ~/.bashrc or ~/.zshrc -# Guide Goose toward better tool choices +# Guide goose toward better tool choices if [[ -n "$GOOSE_TERMINAL" ]]; then alias find="echo 'Use rg instead: rg --files | rg for filenames, or rg for content search'" fi @@ -380,6 +411,6 @@ fi - Environment variables take precedence over configuration files. - For security-sensitive variables (like API keys), consider using the system keyring instead of environment variables. -- Some variables may require restarting Goose to take effect. -- When using the planning mode, if planner-specific variables are not set, Goose will fall back to the main model configuration. +- Some variables may require restarting goose to take effect. +- When using the planning mode, if planner-specific variables are not set, goose will fall back to the main model configuration. diff --git a/documentation/docs/guides/goose-cli-commands.md b/documentation/docs/guides/goose-cli-commands.md index aa9ed0026164..47b18dc3bc6f 100644 --- a/documentation/docs/guides/goose-cli-commands.md +++ b/documentation/docs/guides/goose-cli-commands.md @@ -7,6 +7,21 @@ toc_max_heading_level: 4 Goose provides a command-line interface (CLI) with several commands for managing sessions, configurations and extensions. This guide covers all available CLI commands and interactive session features. +## Flag Naming Conventions + +Goose CLI follows consistent patterns for flag naming to make commands intuitive and predictable: + +- **`--session-id`**: Used for session identifiers (e.g., `20250305_113223`) +- **`--schedule-id`**: Used for schedule job identifiers (e.g., `daily-report`) +- **`-n, --name`**: Used for human-readable names +- **`-p, --path`**: Used for file paths (legacy support) +- **`-o, --output`**: Used for output file paths +- **`-r, --resume` or `-r, --regex`**: Context-dependent (resume for sessions, regex for filters) +- **`-v, --verbose`**: Used for verbose output +- **`-l, --limit`**: Used for limiting result counts +- **`-f, --format`**: Used for specifying output formats +- **`-w, --working-dir`**: Used for working directory filters + ### Core Commands #### help @@ -79,8 +94,10 @@ goose update --reconfigure Start or resume interactive chat sessions. **Basic Options:** +- **`--session-id `**: Specify a session by its ID (e.g., '20250921_143022') - **`-n, --name `**: Give the session a name -- **`-r, --resume`**: Resume a previous session +- **`-p, --path `**: Legacy parameter for specifying session by file path +- **`-r, --resume`**: Resume a previous session - **`--debug`**: Enable debug mode to output complete tool responses, detailed parameter values, and full file paths - **`--max-turns `**: Set the maximum number of turns allowed without user input (default: 1000) @@ -93,11 +110,12 @@ Start or resume interactive chat sessions. **Usage:** ```bash # Start a basic session -goose session --name my-project +goose session -n my-project # Resume a previous session -goose session --resume --name my-project -goose session --resume --id 2025250620_013617 +goose session --resume -n my-project +goose session --resume --session-id 20250620_013617 +goose session --resume -p ./session.jsonl # Start with extensions goose session --with-extension "npx -y @modelcontextprotocol/server-memory" @@ -112,7 +130,7 @@ goose session \ --with-builtin "developer" # Control session behavior -goose session --name my-session --debug --max-turns 25 +goose session -n my-session --debug --max-turns 25 ``` --- @@ -146,14 +164,15 @@ goose session list --ascending Remove one or more saved sessions. **Options:** -- **`-i, --id `**: Remove a specific session by its ID +- **`--session-id `**: Remove a specific session by its session ID - **`-n, --name `**: Remove a specific session by its name - **`-r, --regex `**: Remove sessions matching a regex pattern **Usage:** ```bash # Remove a specific session by ID -goose session remove -i 20250305_113223 +goose session remove --session-id 20250305_113223 +goose session remove --id 20250305_113223 # Remove a specific session by its name goose session remove -n my-session @@ -175,9 +194,9 @@ Session removal is permanent and cannot be undone. Goose will show which session Export sessions in different formats for backup, sharing, migration, or documentation purposes. **Options:** -- **`-i, --id `**: Export a specific session by ID +- **`--session-id `**: Export a specific session by session ID - **`-n, --name `**: Export a specific session by name -- **`-p, --path `**: Export a specific session by file path +- **`-p, --path `**: Export a specific session by file path (legacy) - **`-o, --output `**: Save exported content to a file (default: stdout) - **`--format `**: Output format: `markdown`, `json`, `yaml`. Default is `markdown` @@ -192,19 +211,58 @@ Export sessions in different formats for backup, sharing, migration, or document goose session export # Export specific session as JSON for backup -goose session export --name my-session --format json --output session-backup.json +goose session export -n my-session --format json -o session-backup.json # Export specific session as readable markdown -goose session export --name my-session --output session.md +goose session export -n my-session -o session.md # Export to stdout in different formats -goose session export --id 20250305_113223 --format json -goose session export --name my-session --format yaml +goose session export --session-id 20250305_113223 --format json +goose session export -n my-session --format yaml + +# Export session by path (legacy) +goose session export -p ./my-session.jsonl -o exported.md +``` + +--- + +#### session diagnostics [options] +Generate a comprehensive diagnostics bundle for troubleshooting issues with a specific session. + +**Options:** +- **`--session-id `**: Generate diagnostics for a specific session by ID +- **`-n, --name `**: Generate diagnostics for a specific session by name +- **`-o, --output `**: Save diagnostics bundle to a specific file path (default: `diagnostics_{session_id}.zip`) -# Export session by path -goose session export --path ./my-session.jsonl --output exported.md +**What's included:** +- **System Information**: App version, operating system, architecture, and timestamp +- **Session Data**: Complete conversation messages and history for the specified session +- **Configuration Files**: Your [configuration files](/docs/guides/config-files) (if they exist) +- **Log Files**: Recent application logs for debugging + +**Usage:** +```bash +# Generate diagnostics for a specific session by ID +goose session diagnostics --session-id 20250305_113223 + +# Generate diagnostics for a session by name +goose session diagnostics --name my-project-session + +# Save diagnostics to a custom location +goose session diagnostics --session-id 20250305_113223 --output /path/to/my-diagnostics.zip + +# Interactive selection (prompts you to choose a session) +goose session diagnostics ``` +:::warning Privacy Notice +Diagnostics bundles contain your session messages and system information. If your session includes sensitive data (API keys, personal information, proprietary code), review the contents before sharing publicly. +::: + +:::tip +Generate diagnostics before reporting bugs to provide technical details that help with faster resolution. The ZIP file can be attached to GitHub issues or shared with support. +::: + --- ### Task Execution @@ -327,29 +385,29 @@ Automate recipes by running them on a [schedule](/docs/guides/recipes/session-re - `run-now`: Run a scheduled recipe immediately **Options:** -- `--id `: A unique ID for the scheduled job (e.g. `daily-report`) +- `--schedule-id `: A unique ID for the scheduled job (e.g. `daily-report`) - `--cron "* * * * * *"`: Specifies when a job should run using a [cron expression](https://en.wikipedia.org/wiki/Cron#Cron_expression) - `--recipe-source `: Path to the recipe YAML file -- `--limit `: Max number of sessions to display when using the `sessions` command +- `-l, --limit `: Max number of sessions to display when using the `sessions` command **Usage:** ```bash goose schedule # Add a new scheduled recipe which runs every day at 9 AM -goose schedule add --id daily-report --cron "0 0 9 * * *" --recipe-source ./recipes/daily-report.yaml +goose schedule add --schedule-id daily-report --cron "0 0 9 * * *" --recipe-source ./recipes/daily-report.yaml # List all scheduled jobs goose schedule list # List the 10 most recent Goose sessions created by a scheduled job -goose schedule sessions --id daily-report --limit 10 +goose schedule sessions --schedule-id daily-report -l 10 # Run a recipe immediately -goose schedule run-now --id daily-report +goose schedule run-now --schedule-id daily-report # Remove a scheduled job -goose schedule remove --id daily-report +goose schedule remove --schedule-id daily-report ``` --- @@ -507,7 +565,7 @@ The `/t` command controls the syntax highlighting theme for markdown content in **Configuration:** - The default theme is `dark` -- The theme setting is saved to the [configuration file](/docs/guides/config-file) as `GOOSE_CLI_THEME` and persists between sessions +- The theme setting is saved to the [configuration file](/docs/guides/config-files) as `GOOSE_CLI_THEME` and persists between sessions - The saved configuration can be overridden for the session using the `GOOSE_CLI_THEME` [environment variable](/docs/guides/environment-variables#session-management) :::info diff --git a/documentation/docs/guides/goose-permissions.md b/documentation/docs/guides/goose-permissions.md index 14c444d9fb9c..8ec4ce08474a 100644 --- a/documentation/docs/guides/goose-permissions.md +++ b/documentation/docs/guides/goose-permissions.md @@ -1,14 +1,14 @@ --- sidebar_position: 25 -title: Goose Permission Modes -sidebar_label: Goose Permissions +title: goose Permission Modes +sidebar_label: goose Permissions --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -import { PanelLeft } from 'lucide-react'; +import { PanelLeft, Tornado } from 'lucide-react'; -Goose’s permissions determine how much autonomy it has when modifying files, using extensions, and performing automated actions. By selecting a permission mode, you have full control over how Goose interacts with your development environment. +goose’s permissions determine how much autonomy it has when modifying files, using extensions, and performing automated actions. By selecting a permission mode, you have full control over how goose interacts with your development environment.
Permission Modes Video Walkthrough @@ -24,19 +24,18 @@ Goose’s permissions determine how much autonomy it has when modifying files, u ## Permission Modes -| Mode | Description | Best For | -|--------------------|-------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------| -| **Completely Autonomous** | Goose can modify files, use extensions, and delete files **without requiring approval**. | Users who want **full automation** and seamless integration into their workflow. | -| **Manual Approval**| Goose **asks for confirmation** before using any tools or extensions. | Users who want to **review and approve** every change and tool usage. | -| **Smart Approval** | Goose uses a risk-based approach to **automatically approve low-risk actions** and **flag others** for approval. | Users who want a **balanced mix of autonomy and oversight** based on the action’s impact. | -| **Chat Only** | Goose **only engages in chat**, with no extension use or file modifications. | Users who prefer a **conversational AI experience** for analysis, writing, and reasoning tasks without automation. | - | +| Mode | Description | Best For | +|------|-------------|----------| +| **Completely Autonomous** | goose can modify files, use extensions, and delete files **without requiring approval** | Users who want **full automation** and seamless integration into their workflow | +| **Manual Approval** | goose **asks for confirmation** before using any tools or extensions (supports granular [tool permissions](/docs/guides/managing-tools/tool-permissions)) | Users who want to **review and approve** every change and tool usage | +| **Smart Approval** | goose uses a risk-based approach to **automatically approve low-risk actions** and **flag others** for approval (supports granular [tool permissions](/docs/guides/managing-tools/tool-permissions)) | Users who want a **balanced mix of autonomy and oversight** based on the action’s impact | +| **Chat Only** | goose **only engages in chat**, with no extension use or file modifications | Users who prefer a **conversational AI experience** for analysis, writing, and reasoning tasks without automation | :::warning `Autonomous Mode` is applied by default. ::: -## Configuring Goose Mode +## Configuring goose mode Here's how to configure: @@ -48,7 +47,7 @@ Here's how to configure: - Click the Goose Mode option from the bottom menu. + Click the mode button from the bottom menu. 1. Click the button on the top-left to open the sidebar. @@ -76,7 +75,7 @@ Here's how to configure: goose configure ``` - 2. Select `Goose Settings` from the menu and press Enter. + 2. Select `goose settings` from the menu and press Enter. ```sh ┌ goose-configure @@ -87,41 +86,47 @@ Here's how to configure: | ○ Toggle Extensions | ○ Remove Extension // highlight-start - | ● Goose Settings (Set the Goose Mode, Tool Output, Experiment and more) + | ● goose settings (Set the goose mode, Tool Output, Tool Permissions, Experiment, goose recipe github repo and more) // highlight-end └ ``` - 3. Choose `Goose Mode` from the menu and press Enter. + 3. Choose `goose mode` from the menu and press Enter. ```sh ┌ goose-configure │ ◇ What would you like to configure? - │ Goose Settings + │ goose settings │ ◆ What setting would you like to configure? // highlight-start - │ ● Goose Mode (Configure Goose mode) + │ ● goose mode (Configure goose mode) // highlight-end - | ○ Tool Output + │ ○ Router Tool Selection Strategy + │ ○ Tool Permission + │ ○ Tool Output + │ ○ Max Turns + │ ○ Toggle Experiment + │ ○ goose recipe github repo + │ ○ Scheduler Type └ ``` - 4. Choose the Goose mode you would like to configure. + 4. Choose the goose mode you would like to configure. ```sh ┌ goose-configure │ ◇ What would you like to configure? - │ Goose Settings + │ goose settings │ ◇ What setting would you like to configure? - │ Goose Mode + │ goose mode │ - ◆ Which Goose mode would you like to configure? + ◆ Which goose mode would you like to configure? // highlight-start - │ ● Auto Mode + │ ● Auto Mode (Full file modification, extension usage, edit, create and delete files freely) // highlight-end | ○ Approve Mode | ○ Smart Approve Mode @@ -135,8 +140,8 @@ Here's how to configure: :::info - If you choose `Manual` (in Goose Desktop) or `Approve Mode` (in Goose CLI), you will see "Allow" and "Deny" buttons in your session windows during tool calls. - Goose will only ask for permission for tools that it deems are 'write' tools, e.g. any 'text editor write', 'text editor edit', 'bash - rm, cp, mv' commands. + In manual and smart approval modes, you will see "Allow" and "Deny" buttons in your session windows during tool calls. + goose will only ask for permission for tools that it deems are 'write' tools, e.g. any 'text editor write', 'text editor edit', 'bash - rm, cp, mv' commands. Read/write approval makes best effort attempt at classifying read or write tools. This is interpreted by your LLM provider. ::: diff --git a/documentation/docs/guides/interactive-chat/mcp-ui.md b/documentation/docs/guides/interactive-chat/mcp-ui.md index 60517823f585..8086b1f84a3f 100644 --- a/documentation/docs/guides/interactive-chat/mcp-ui.md +++ b/documentation/docs/guides/interactive-chat/mcp-ui.md @@ -2,7 +2,7 @@ sidebar_position: 1 title: MCP-UI Extensions sidebar_label: MCP-UI Extensions -description: Learn how Goose can render graphical and interactive UI components from MCP-UI-enabled extensions +description: Learn how goose can render graphical and interactive UI components from MCP-UI-enabled extensions --- import Tabs from '@theme/Tabs'; @@ -11,7 +11,7 @@ import GooseDesktopInstaller from '@site/src/components/GooseDesktopInstaller'; import CLIExtensionInstructions from '@site/src/components/CLIExtensionInstructions'; import { PanelLeft } from 'lucide-react'; -Extensions built on MCP-UI allow Goose Desktop to provide interactive and engaging user experiences. Imagine using a graphical, clickable UI instead of reading text responses and typing all your prompts: +Extensions built on MCP-UI allow goose Desktop to provide interactive and engaging user experiences. Imagine using a graphical, clickable UI instead of reading text responses and typing all your prompts:
)} diff --git a/documentation/src/pages/community/data/community-content.json b/documentation/src/pages/community/data/community-content.json index e3de107ba447..61d516226c00 100644 --- a/documentation/src/pages/community/data/community-content.json +++ b/documentation/src/pages/community/data/community-content.json @@ -17,13 +17,12 @@ "submittedDate": "2025-09-26", "tags": [ "hacktoberfest", - "open-source", - "contributing" + "open-source" ], "hacktoberfest": true }, { - "title": "Running Goose in Containers (Without Losing Your Mind)", + "title": "Running goose in Containers (Without Losing Your Mind)", "author": { "name": "Rupam Golui", "handle": "Itz-Agasta" @@ -38,7 +37,7 @@ "hacktoberfest": true }, { - "title": "Goose with Goose Docs MCP", + "title": "Goose with goose Docs MCP", "author": { "name": "Dakshata Mishra", "handle": "the-matrixneo" @@ -52,7 +51,7 @@ "hacktoberfest": true }, { - "title": "Best Practices for Using Goose in Enterprise Environments", + "title": "Best Practices for Using goose in Enterprise Environments", "author": { "name": "Anudhyan Datta", "handle": "Anudhyan" @@ -62,13 +61,12 @@ "submittedDate": "2025-10-15", "tags": [ "hacktoberfest", - "goose", "enterprise" ], "hacktoberfest": true }, { - "title": "Getting started with Goose on Windows", + "title": "Getting started with goose on Windows", "author": { "name": "Halimah", "handle": "lymah123" @@ -136,8 +134,170 @@ "type": "blog", "url": "https://goose123.hashnode.dev/adopting-goose-in-the-enterprise-a-guide-to-security-scalability-and-governance", "submittedDate": "2025-10-26", - "tags": ["hacktoberfest"], + "tags": [ + "hacktoberfest" + ], + "hacktoberfest": true + }, + { + "title": "Creating and Sharing Effective goose Recipes", + "author": { + "name": "Shreyansh Singh Gautam", + "handle": "Shreyanshsingh23" + }, + "type": "blog", + "url": "https://medium.com/@shreyanshrewa/creating-and-sharing-effective-goose-recipes-abf9767d5128", + "thumbnail": "https://images.unsplash.com/photo-1555949963-aa79dcee981c?w=400&h=225&fit=crop&crop=entropy&auto=format", + "submittedDate": "2025-10-29", + "tags": [ + "hacktoberfest" + ], + "hacktoberfest": true + }, + { + "title": "goose with Context7 MCP", + "author": { + "name": "Dakshata Mishra", + "handle": "the-matrixneo" + }, + "type": "video", + "url": "https://www.youtube.com/shorts/EmLcx5gKHsE", + "submittedDate": "2025-10-29", + "tags": [ + "hacktoberfest" + ], + "hacktoberfest": true + }, + { + "title": "Stop Chasing Stale Docs: Creating Automated Documentation Workflows with goose", + "author": { + "name": "Anudhyan Datta", + "handle": "Anudhyan" + }, + "type": "blog", + "url": "https://anudhyan.hashnode.dev/stop-chasing-stale-docs-creating-automated-documentation-workflows-with-goose", + "submittedDate": "2025-10-29", + "tags": [ + "hacktoberfest" + ], + "hacktoberfest": true + }, + { + "title": "Building Effective Prompts and Workflows for Code Review with goose", + "author": { + "name": "Halimah", + "handle": "lymah123" + }, + "type": "blog", + "url": "https://dev.to/lymah/building-effective-prompts-and-workflows-for-code-review-with-goose-51m1", + "thumbnail": "https://i.imgur.com/2cRXct5.png", + "submittedDate": "2025-10-29", + "tags": [ + "hacktoberfest" + ], + "hacktoberfest": true + }, + { + "title": "Optimizing goose Performance on Windows", + "author": { + "name": "Mohammed Thaha", + "handle": "mohammed-thaha" + }, + "type": "blog", + "url": "https://dev.to/mohammed_thaha/boosting-goose-performance-on-windows-real-benchmarks-power-tweaks-and-results-54gf", + "thumbnail": "https://media2.dev.to/dynamic/image/width=400,height=225,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkxdtne8rxn4l2nctajv1.png", + "submittedDate": "2025-10-29", + "tags": [ + "hacktoberfest" + ], + "hacktoberfest": true + }, + { + "title": "goose with Neon MCP", + "author": { + "name": "Dakshata Mishra", + "handle": "the-matrixneo" + }, + "type": "video", + "url": "https://www.youtube.com/shorts/IxUPo2QDg_8", + "submittedDate": "2025-10-29", + "tags": [ + "hacktoberfest" + ], + "hacktoberfest": true + }, + { + "title": "Goose with Canva MCP", + "author": { + "name": "BestCodes", + "handle": "the-best-codes" + }, + "type": "video", + "url": "https://www.youtube.com/shorts/xBGTa8XenVc", + "submittedDate": "2025-10-30", + "tags": [ + "hacktoberfest" + ], + "hacktoberfest": true + }, + { + "title": "goose with Apify MCP", + "author": { + "name": "abhi", + "handle": "Better-Boy" + }, + "type": "video", + "url": "https://youtube.com/shorts/XhA2YqjNF7M", + "submittedDate": "2025-10-30", + "tags": [ + "hacktoberfest" + ], + "hacktoberfest": true + }, + { + "title": "Building Custom Tools and Extensions for goose: A Developer's Guide", + "author": { + "name": "Anudhyan Datta", + "handle": "Anudhyan" + }, + "type": "blog", + "url": "https://anudhyan.hashnode.dev/building-custom-tools-and-extensions-for-goose-a-developers-guide", + "submittedDate": "2025-10-30", + "tags": [ + "hacktoberfest", + "mcp" + ], + "hacktoberfest": true + }, + { + "title": "Managing goose Configurations Across Multiple Projects", + "author": { + "name": "Halimah", + "handle": "lymah123" + }, + "type": "blog", + "url": "https://dev.to/lymah/managing-goose-configurations-across-multiple-projects-58i3", + "thumbnail": "https://i.imgur.com/VvM8sMR.png", + "submittedDate": "2025-10-30", + "tags": [ + "hacktoberfest" + ], + "hacktoberfest": true + }, + { + "title": "Managing Goose Configurations Across Multiple Projects: A Practical Guide", + "author": { + "name": "Viet Bui", + "handle": "vietbui1999ru" + }, + "type": "blog", + "url": "https://dev.to/vietbui1999ru/managing-goose-configurations-across-multiple-projects-a-practical-guide-48ei", + "thumbnail": "https://imgur.com/a/ubyYbVJ", + "submittedDate": "2025-10-31", + "tags": [ + "hacktoberfest" + ], "hacktoberfest": true } ] -} \ No newline at end of file +} diff --git a/documentation/src/pages/community/data/config.json b/documentation/src/pages/community/data/config.json index 78eb3571dd6f..211521c9a189 100644 --- a/documentation/src/pages/community/data/config.json +++ b/documentation/src/pages/community/data/config.json @@ -29,7 +29,12 @@ "id": "september-2025", "display": "September 2025", "file": "september-2025.json" + }, + { + "id": "october-2025", + "display": "October 2025", + "file": "october-2025.json" } ], - "defaultMonth": "september-2025" + "defaultMonth": "october-2025" } \ No newline at end of file diff --git a/documentation/src/pages/community/data/october-2025.json b/documentation/src/pages/community/data/october-2025.json new file mode 100644 index 000000000000..3f9b99ce6233 --- /dev/null +++ b/documentation/src/pages/community/data/october-2025.json @@ -0,0 +1,46 @@ +{ + "month": "October 2025", + "communityStars": [ + { + "name": "Arya Pratap Singh", + "handle": "ARYPROGRAMMER" + }, + { + "name": "Halimah", + "handle": "Lymah123" + }, + { + "name": "Dakshata Mishra", + "handle": "the-matrixneo" + }, + { + "name": "Abhi", + "handle": "Better-Boy" + }, + { + "name": "Anudhyan Datta", + "handle": "Anudhyan" + } + ], + "teamStars": [], + "leaderboard": [ + { "handle": "ARYPROGRAMMER", "rank": 1, "medal": "🥇" }, + { "handle": "Lymah123", "rank": 2, "medal": "🥈" }, + { "handle": "the-matrixneo", "rank": 3, "medal": "🥉" }, + { "handle": "Better-Boy", "rank": 4 }, + { "handle": "Anudhyan", "rank": 5 }, + { "handle": "Shreyanshsingh23", "rank": 6 }, + { "handle": "The-Best-Codes", "rank": 7 }, + { "handle": "anthonydmays", "rank": 8 }, + { "handle": "Mohammed-Thaha", "rank": 9 }, + { "handle": "shiv669", "rank": 10 }, + { "handle": "aegntic", "rank": 11 }, + { "handle": "M-A-SAIADITHYAA", "rank": 12 }, + { "handle": "ki3ani", "rank": 13 }, + { "handle": "vietbui1999ru", "rank": 14 }, + { "handle": "Itz-Agasta", "rank": 15 }, + { "handle": "Shahzebdevv", "rank": 16 }, + { "handle": "Roshansingh9", "rank": 17 }, + { "handle": "AdemolaAri", "rank": 18 } + ] +} diff --git a/documentation/src/pages/community/data/september-2025.json b/documentation/src/pages/community/data/september-2025.json index 29c90baa65c2..c4c99a5abd35 100644 --- a/documentation/src/pages/community/data/september-2025.json +++ b/documentation/src/pages/community/data/september-2025.json @@ -10,7 +10,7 @@ "handle": "sings-to-bees-on-wednesdays" }, { - "name": "Aryan Sharma", + "name": "Arya Pratap Singh", "handle": "ARYPROGRAMMER" }, { diff --git a/documentation/src/pages/community/index.tsx b/documentation/src/pages/community/index.tsx index c96dd60d2838..e063034bab7f 100644 --- a/documentation/src/pages/community/index.tsx +++ b/documentation/src/pages/community/index.tsx @@ -12,6 +12,7 @@ import june2025Data from "./data/june-2025.json"; import july2025Data from "./data/july-2025.json"; import august2025Data from "./data/august-2025.json"; import september2025Data from "./data/september-2025.json"; +import october2025Data from "./data/october-2025.json"; import communityContentData from "./data/community-content.json"; // Create a data map for easy access @@ -22,6 +23,7 @@ const communityDataMap = { "july-2025": july2025Data, "august-2025": august2025Data, "september-2025": september2025Data, + "october-2025": october2025Data, }; function UpcomingEventsSection() { @@ -98,19 +100,23 @@ function CommunityAllStarsSection() { ))} - {/* Team Stars */} -
- ⭐ Team Stars -

- Top 5 Contributors from all Block teams! -

-
- -
- {currentData.teamStars.map((contributor, index) => ( - - ))} -
+ {/* Team Stars - only show if there are team stars */} + {currentData.teamStars.length > 0 && ( + <> +
+ ⭐ Team Stars +

+ Top 5 Contributors from all Block teams! +

+
+ +
+ {currentData.teamStars.map((contributor, index) => ( + + ))} +
+ + )} {/* Monthly Leaderboard */}
@@ -216,6 +222,7 @@ function CommunityAllStarsSection() { function CommunityContentSpotlightSection() { const [contentFilter, setContentFilter] = React.useState('all'); + const [showScrollIndicator, setShowScrollIndicator] = React.useState(true); const filteredSubmissions = React.useMemo(() => { if (contentFilter === 'all') return communityContentData.submissions; @@ -227,6 +234,12 @@ function CommunityContentSpotlightSection() { return communityContentData.submissions.filter(content => content.type === contentFilter); }, [contentFilter]); + const handleScroll = (e: React.UIEvent) => { + const { scrollTop, scrollHeight, clientHeight } = (e.target as HTMLDivElement); + const isAtBottom = scrollTop + clientHeight >= scrollHeight - 10; // 10px threshold + setShowScrollIndicator(!isAtBottom); + }; + return (
@@ -258,19 +271,33 @@ function CommunityContentSpotlightSection() {
{/* Content Grid */} -
-
- {/* Persistent Hacktoberfest CTA Card */} - +
+
+
+ {/* Persistent Hacktoberfest CTA Card */} + + + {filteredSubmissions.map((content) => ( + + ))} +
- {filteredSubmissions.map((content, index) => ( - - ))} + {filteredSubmissions.length === 0 && ( +
+

No content found for this filter.

+
+ )}
- {filteredSubmissions.length === 0 && ( -
-

No content found for this filter.

+ {/* Simple scroll indicator - only show when not at bottom */} + {showScrollIndicator && ( +
+ + Scroll for more ↓ +
)}
@@ -434,8 +461,8 @@ export default function Community(): ReactNode { description="Join the goose community - connect with developers, contribute to the project, and help shape the future of AI-powered development tools." >
- +
diff --git a/documentation/src/pages/extensions/detail.tsx b/documentation/src/pages/extensions/detail.tsx index 3378dd4c02b5..d16e79c539d4 100644 --- a/documentation/src/pages/extensions/detail.tsx +++ b/documentation/src/pages/extensions/detail.tsx @@ -100,7 +100,7 @@ const getDocumentationPath = (serverId: string): string => {
- Can be enabled on the Extensions page in Goose + Can be enabled on the Extensions page in goose
) : server.show_install_command !== false ? ( diff --git a/documentation/src/pages/prompt-library/data/prompts/audio-summarizer.json b/documentation/src/pages/prompt-library/data/prompts/audio-summarizer.json new file mode 100644 index 000000000000..dbb93c38d1b1 --- /dev/null +++ b/documentation/src/pages/prompt-library/data/prompts/audio-summarizer.json @@ -0,0 +1,23 @@ +{ + "id": "audio-summarizer", + "title": "Generate Audio Summary from Webpage", + "description": "Summarizes a webpage URL into bullet points and generates an MP3 audio file of the summary using ElevenLabs.", + "category": "productivity", + "job": "productivity", + "example_prompt": "Hey goose , summarize the article at https://en.wikipedia.org/wiki/Coronavirus into short bulletpoints. Then, take that summary and generate an audio file using the ElevenLabs MCP with the 'Rachel' voice.", + "example_result": "Based on the available content, here is a summarized version of the article about Coronavirus in bullet points:\n- Coronaviruses are a group of related viruses that cause diseases in mammals and birds.\n- In humans, coronaviruses can lead to respiratory tract infections, which can range from mild, like some cases of the common cold, to severe, such as SARS, MERS, and COVID-19.\n- The name \"coronavirus\" is derived from the Latin word \"corona,\" meaning \"crown\" or \"halo,\" which refers to the characteristic appearance of the virus particles.\n- Coronaviruses have a genome composed of a single strand of RNA and have characteristic surface spike proteins.\n- They are classified within the family Coronaviridae and the subfamily Orthocoronavirinae.\n- These viruses are further grouped into four genera: Alphacoronavirus, Betacoronavirus, Gammacoronavirus, and Deltacoronavirus.\n- Transmission between humans typically occurs through respiratory droplets, especially from coughs and sneezes.\n- Major outbreaks in recent history include the Severe Acute Respiratory Syndrome (SARS) in 2003, Middle East Respiratory Syndrome (MERS) in 2012, and COVID-19 starting in late 2019.\n\nNow, let's use the above summary to generate an audio file using the ElevenLabs MCP and the 'Rachel' voice. The audio file has been successfully generated using the 'Rachel' voice. You can find the file saved on your desktop under the name tts_Coron_20251029_145539.mp3.", + "extensions": [ + { + "name": "ElevenLabs MCP", + "description": "Generates high-quality, human-sounding text-to-speech audio and AI voices.", + "is_builtin": true, + "environmentVariables": [ + { + "name": "ELEVENLABS_API_KEY", + "value": "", + "required": true + } + ] + } + ] +} diff --git a/documentation/src/pages/recipes/data/recipes/csv-file-merger.yaml b/documentation/src/pages/recipes/data/recipes/csv-file-merger.yaml new file mode 100644 index 000000000000..2e46e1123324 --- /dev/null +++ b/documentation/src/pages/recipes/data/recipes/csv-file-merger.yaml @@ -0,0 +1,65 @@ +version: 1.0.0 +title: CSV File Merger +author: + contact: the-matrixneo +description: "Combines multiple CSV files from a directory into a single master file." +instructions: | + 1. Provide the path to the directory containing your CSV files. + 2. Specify the desired name for the final merged output file. + 3. The recipe will scan the directory and merge all found CSV files. + 4. It will check for header consistency across the files. + 5. Your new, combined file will be saved in the same directory. + +activities: + - Validate the input directory path. + - Scan for and identify all CSV files within the directory. + - Check for header consistency across all files. + - Merge the data from all identified files. + - Generate the final, single CSV file. + +parameters: + - key: directory_path + input_type: string + requirement: required + description: "Path to the folder containing the CSV files you want to merge." + + - key: output_filename + input_type: string + requirement: required + description: "Name for the final merged CSV file." + default: "merged_output.csv" + + - key: header_check + input_type: string + requirement: optional + description: "How to handle differing headers ('strict' to fail, 'flexible' to merge common columns)." + default: "strict" + choices: + - "strict" + - "flexible" + +extensions: + - type: builtin + name: developer + display_name: Developer + timeout: 300 + bundled: true + description: "Orchestrates the merging logic and performs the in-memory data processing to combine the CSVs." + + - type: stdio + name: filesystem + cmd: npx + args: + - -y + - "@modelcontextprotocol/server-filesystem" + - "{{ directory_path }}" + timeout: 300 + description: "Handles all direct filesystem operations." +prompt: | + You are a CSV merging assistant. + 1. First, validate that the directory at {{ directory_path }} exists. If not, inform the user and stop. + 2. Scan the directory for all files ending with ".csv". If none are found, report it and stop. + 3. Based on the {{ header_check }} parameter, verify the headers. If 'strict' and headers mismatch, list the inconsistent files and stop. If 'flexible', proceed by merging only common columns. + 4. Read each CSV file and append its contents into a single dataset. + 5. Save the combined dataset to a new file named {{ output_filename }} in the original directory. + 6. Provide a summary of the operation. diff --git a/documentation/src/pages/recipes/data/recipes/openapi-to-locust.yaml b/documentation/src/pages/recipes/data/recipes/openapi-to-locust.yaml new file mode 100644 index 000000000000..49e9beb46170 --- /dev/null +++ b/documentation/src/pages/recipes/data/recipes/openapi-to-locust.yaml @@ -0,0 +1,93 @@ +version: "1.0.0" +title: "OpenAPI to Locust Load Test Generator" +description: "Generate comprehensive Locust load tests from OpenAPI/Swagger specifications" +author: + contact: Better-Boy + +instructions: | + You are an expert in API testing and load testing with Locust. + Your task is to generate production-ready Locust load test files from OpenAPI specifications. + + Follow this workflow: + 1. First, analyze the OpenAPI spec to understand the API structure + 2. Generate Locust task sets for different endpoint groups + 3. Create the main locustfile with proper configuration + 4. Generate supporting files (requirements.txt, README, config) + + Ensure the generated tests are: + - Well-structured with proper task weighting + - Include realistic user behavior patterns + - Have proper error handling and assertions + - Use parameterized data where appropriate + - Follow Locust best practices + +parameters: + - key: openapi_spec_path + input_type: string + requirement: required + description: "Path to the OpenAPI specification file (JSON or YAML)" + + - key: base_url + input_type: string + requirement: optional + default: "http://localhost:8000" + description: "Base URL for the API to test" + + - key: output_dir + input_type: string + requirement: optional + default: "./load_tests" + description: "Directory where generated test files will be saved" + + - key: test_complexity + input_type: string + requirement: optional + default: "standard" + description: "Test complexity level: basic, standard, or advanced" + + - key: include_auth + input_type: string + requirement: optional + default: "true" + description: "Whether to include authentication handling in tests" + +sub_recipes: + - name: analyze_openapi + path: "./subrecipes/analyze-openapi.yaml" + values: + analysis_depth: "comprehensive" + + - name: generate_task_sets + path: "./subrecipes/generate-task-sets.yaml" + + - name: generate_locustfile + path: "./subrecipes/generate-locustfile.yaml" + + - name: generate_support_files + path: "./subrecipes/generate-support-files.yaml" + +extensions: + - type: builtin + name: developer + timeout: 600 + bundled: true + +prompt: | + Generate complete Locust load tests from the OpenAPI specification at {{ openapi_spec_path }}. + + Configuration: + - Base URL: {{ base_url }} + - Output directory: {{ output_dir }} + - Test complexity: {{ test_complexity }} + - Include authentication: {{ include_auth }} + + Use the sub-recipe tools in this order: + 1. analyze_openapi - Parse and analyze the OpenAPI spec + 2. generate_task_sets - Create Locust TaskSets for endpoint groups + 3. generate_locustfile - Generate the main locustfile.py + 4. generate_support_files - Create requirements.txt, README.md, and config files + + After all subrecipes complete, verify all files were created successfully and provide: + - Summary of generated files + - Instructions for running the tests + - Example commands for different load scenarios \ No newline at end of file diff --git a/documentation/src/pages/recipes/data/recipes/subrecipes/analyze-openapi.yaml b/documentation/src/pages/recipes/data/recipes/subrecipes/analyze-openapi.yaml new file mode 100644 index 000000000000..3fd108935aed --- /dev/null +++ b/documentation/src/pages/recipes/data/recipes/subrecipes/analyze-openapi.yaml @@ -0,0 +1,71 @@ +version: "1.0.0" +title: "OpenAPI Spec Analyzer" +description: "Parse and analyze OpenAPI specification to extract endpoints, schemas, and auth details" +instructions: | + You are an API specification expert. Parse the OpenAPI/Swagger specification file + and extract all relevant information for load test generation. + + Your analysis should include: + - List of all endpoints grouped by tags/categories + - HTTP methods, paths, and parameters for each endpoint + - Request/response schemas and content types + - Authentication and security schemes + - Rate limits and other constraints if defined + + Output Format: + Create a structured analysis with: + 1. API metadata (title, version, description) + 2. Base servers/URLs + 3. Authentication methods used + 4. Endpoint groups with their operations + 5. Common schemas and data models + 6. Recommended test scenarios based on endpoint relationships + +parameters: + - key: output_dir + input_type: string + requirement: required + description: "Output directory" + + - key: openapi_spec_path + input_type: string + requirement: required + description: "Path to the OpenAPI specification file" + + - key: analysis_depth + input_type: string + requirement: optional + default: "standard" + description: "Depth of analysis: basic, standard, or comprehensive" + +extensions: + - type: builtin + name: developer + timeout: 300 + bundled: true + +prompt: | + Analyze the OpenAPI specification at {{ openapi_spec_path }}. + Perform a {{ analysis_depth }} analysis. + + Steps: + 1. Read and parse the spec file (handle both JSON and YAML formats) + 2. Extract API metadata and server information + 3. Identify all authentication/security schemes + 4. Group endpoints by tags or logical categories + 5. For each endpoint, document: + - HTTP method and path + - Path/query/header parameters + - Request body schema (if applicable) + - Response schemas + - Security requirements + 6. Identify endpoint relationships (e.g., POST /users before GET /users/{id}) + 7. Suggest realistic test scenarios and user flows + + Save the analysis to {{ output_dir }}/analysis.json for use by other sub-recipes. + + Present a summary with: + - Total number of endpoints + - Endpoints grouped by category + - Authentication types used + - Recommended test flow sequences diff --git a/documentation/src/pages/recipes/data/recipes/subrecipes/complexity-analysis.yaml b/documentation/src/pages/recipes/data/recipes/subrecipes/complexity-analysis.yaml new file mode 100644 index 000000000000..ba528c473785 --- /dev/null +++ b/documentation/src/pages/recipes/data/recipes/subrecipes/complexity-analysis.yaml @@ -0,0 +1,52 @@ +version: 1.0.0 +title: "Code Complexity Analyzer" +description: "Analyzes cyclomatic complexity and identifies overly complex functions and classes" +instructions: | + You are analyzing code complexity to identify technical debt. + + Your tasks: + 1. Scan all source code files in {{ repository_path }} + 2. Calculate cyclomatic complexity for functions, methods, and classes + 3. Identify code that exceeds the complexity threshold of {{ complexity_threshold }} + 4. Look for: + - Long functions (>50 lines) + - Deep nesting levels (>4 levels) + - High parameter counts (>5 parameters) + - Large classes (>500 lines) + + 5. For each complex item found, provide: + - File path and line number + - Current complexity score + - Brief explanation of why it's complex + - Suggested refactoring approach + + Return results in a structured format with severity levels: + - Critical: Complexity > 2x threshold + - High: Complexity > 1.5x threshold + - Medium: Complexity > threshold + +parameters: + - key: repository_path + input_type: string + requirement: required + description: "Path to the code repository" + + - key: complexity_threshold + input_type: number + requirement: optional + default: 15 + description: "Cyclomatic complexity threshold" + +extensions: + - type: builtin + name: developer + timeout: 300 + bundled: true + +settings: + temperature: 0.2 + +prompt: | + Analyze code complexity in {{ repository_path }}. + Flag any function/method with cyclomatic complexity > {{ complexity_threshold }}. + Provide specific file paths, line numbers, and refactoring recommendations. \ No newline at end of file diff --git a/documentation/src/pages/recipes/data/recipes/subrecipes/dependency-analysis.yaml b/documentation/src/pages/recipes/data/recipes/subrecipes/dependency-analysis.yaml new file mode 100644 index 000000000000..63dd8dc0c03f --- /dev/null +++ b/documentation/src/pages/recipes/data/recipes/subrecipes/dependency-analysis.yaml @@ -0,0 +1,69 @@ +version: 1.0.0 +title: "Dependency Health Analyzer" +description: "Analyzes dependencies for outdated versions, vulnerabilities, and maintenance issues" +instructions: | + You are analyzing project dependencies to identify security and maintenance risks. + + Your tasks: + 1. Locate dependency files: + - package.json / package-lock.json (Node.js) + - requirements.txt / Pipfile (Python) + - pom.xml / build.gradle (Java) + - Gemfile / Gemfile.lock (Ruby) + - go.mod (Go) + - Cargo.toml (Rust) + + 2. For each dependency, check: + - Current version vs latest stable version + - Last update date + - Known security vulnerabilities (CVEs) + - Maintenance status (archived, deprecated) + - License compatibility + + 3. Flag dependencies that are: + - Critical: Known security vulnerabilities + - High: Outdated by >{{ max_age_days }} days or deprecated + - Medium: Minor updates available + - Low: Patch updates available + + 4. Identify: + - Unused dependencies that can be removed + - Duplicate dependencies + - Heavy dependencies that could be replaced + - Missing security patches + + 5. Provide upgrade recommendations with potential breaking changes noted. + +parameters: + - key: repository_path + input_type: string + requirement: required + description: "Path to the code repository" + + - key: max_age_days + input_type: number + requirement: optional + default: 365 + description: "Flag dependencies older than this many days" + + - key: check_vulnerabilities + input_type: string + requirement: optional + default: "true" + description: "Whether to check for known vulnerabilities" + +extensions: + - type: builtin + name: developer + timeout: 300 + bundled: true + +settings: + temperature: 0.2 + +prompt: | + Analyze dependencies in {{ repository_path }}. + Check for vulnerabilities: {{ check_vulnerabilities }} + Flag dependencies older than {{ max_age_days }} days. + + Provide specific upgrade recommendations prioritized by security impact. \ No newline at end of file diff --git a/documentation/src/pages/recipes/data/recipes/subrecipes/documentation-analysis.yaml b/documentation/src/pages/recipes/data/recipes/subrecipes/documentation-analysis.yaml new file mode 100644 index 000000000000..970779f19cdf --- /dev/null +++ b/documentation/src/pages/recipes/data/recipes/subrecipes/documentation-analysis.yaml @@ -0,0 +1,69 @@ +version: 1.0.0 +title: "Documentation Quality Analyzer" +description: "Identifies missing, outdated, or inadequate documentation" +instructions: | + You are analyzing documentation quality to identify gaps and issues. + + Your tasks: + 1. Check for essential documentation: + - README.md with setup/usage instructions + - CONTRIBUTING.md for contributor guidelines + - API documentation for public interfaces + - Architecture/design documents + - Inline code comments for complex logic + + 2. Analyze existing documentation for: + - Completeness - are all public APIs documented? + - Accuracy - does code match documentation? + - Clarity - is it easy to understand? + - Examples - are there usage examples? + - Maintenance - is it up to date? + + 3. Identify specific gaps: + - Undocumented public functions/classes + - Complex code without explanatory comments + - Missing setup/installation instructions + - Absence of examples or tutorials + - Outdated documentation (check git history) + + 4. Prioritize documentation tasks: + - Critical: Public APIs with no documentation + - High: Complex code without comments + - Medium: Missing examples/tutorials + - Low: Incomplete inline comments + + Provide specific file paths and function signatures that need documentation. + +parameters: + - key: repository_path + input_type: string + requirement: required + description: "Path to the code repository" + + - key: check_readme + input_type: string + requirement: optional + default: "true" + description: "Whether to check README quality" + + - key: check_api_docs + input_type: string + requirement: optional + default: "true" + description: "Whether to check API documentation" + +extensions: + - type: builtin + name: developer + timeout: 300 + bundled: true + +settings: + temperature: 0.3 + +prompt: | + Analyze documentation in {{ repository_path }}. + Check README quality: {{ check_readme }} + Check API docs: {{ check_api_docs }} + + Identify missing or inadequate documentation and provide specific recommendations. \ No newline at end of file diff --git a/documentation/src/pages/recipes/data/recipes/subrecipes/duplication-detection.yaml b/documentation/src/pages/recipes/data/recipes/subrecipes/duplication-detection.yaml new file mode 100644 index 000000000000..9837ff927262 --- /dev/null +++ b/documentation/src/pages/recipes/data/recipes/subrecipes/duplication-detection.yaml @@ -0,0 +1,66 @@ +version: 1.0.0 +title: "Code Duplication Detector" +description: "Identifies duplicated code blocks that should be refactored into reusable components" +instructions: | + You are analyzing code for duplication to identify refactoring opportunities. + + Your tasks: + 1. Scan all source code files in {{ repository_path }} + 2. Identify duplicated code blocks: + - Similar functions/methods (>{{ similarity_threshold }} similarity) + - Copy-pasted code sections (>{{ min_lines }} lines) + - Repeated patterns and logic + - Similar class structures + + 3. For each duplication found: + - Show both/all locations (file paths and line numbers) + - Calculate similarity percentage + - Estimate lines of duplicated code + - Assess refactoring complexity + + 4. Suggest refactoring strategies: + - Extract common functions/utilities + - Create base classes or mixins + - Use composition or inheritance + - Apply design patterns (Strategy, Template Method, etc.) + + 5. Prioritize by impact: + - Critical: >100 lines duplicated, appears 3+ times + - High: >50 lines duplicated, appears 2+ times + - Medium: >20 lines duplicated + - Low: Minor code similarities + + Focus on duplications that will provide the most maintenance benefit when refactored. + +parameters: + - key: repository_path + input_type: string + requirement: required + description: "Path to the code repository" + + - key: min_lines + input_type: number + requirement: optional + default: 10 + description: "Minimum lines for a code block to be considered duplicate" + + - key: similarity_threshold + input_type: number + requirement: optional + default: 0.85 + description: "Similarity threshold (0.0-1.0) for detecting duplicates" + +extensions: + - type: builtin + name: developer + timeout: 300 + bundled: true + +settings: + temperature: 0.2 + +prompt: | + Detect code duplication in {{ repository_path }}. + Find blocks >{{ min_lines }} lines with >{{ similarity_threshold }} similarity. + + Provide specific locations and refactoring recommendations for each duplication. \ No newline at end of file diff --git a/documentation/src/pages/recipes/data/recipes/subrecipes/generate-locustfile.yaml b/documentation/src/pages/recipes/data/recipes/subrecipes/generate-locustfile.yaml new file mode 100644 index 000000000000..5cc3c4ed3756 --- /dev/null +++ b/documentation/src/pages/recipes/data/recipes/subrecipes/generate-locustfile.yaml @@ -0,0 +1,111 @@ +version: "1.0.0" +title: "Main Locustfile Generator" +description: "Generate the main locustfile.py that orchestrates all TaskSets" +instructions: | + You are a Locust expert. Create the main locustfile.py that ties together + all the TaskSets and provides the entry point for load testing. + + The main locustfile should: + - Import all TaskSet classes from the tasks directory + - Define HttpUser classes that use the TaskSets + - Configure wait times and user behavior + - Set up authentication and session handling + - Include environment configuration (host, users, spawn rate) + - Add event hooks for setup/teardown and custom stats + - Include command-line argument handling if needed + + Support multiple user types/personas if the API has different access levels + (e.g., AdminUser, RegularUser, GuestUser) + +parameters: + - key: output_dir + input_type: string + requirement: required + description: "Output directory" + + - key: base_url + input_type: string + requirement: required + description: "Base URL for the API" + + - key: include_auth + input_type: string + requirement: required + description: "Whether to include authentication" + + - key: test_complexity + input_type: string + requirement: required + description: "Test complexity level" + +extensions: + - type: builtin + name: developer + timeout: 300 + bundled: true + +prompt: | + Generate the main locustfile.py that orchestrates the load tests. + + Structure: + ```python + from locust import HttpUser, between, events + import os + from tasks.users_tasks import UsersTaskSet + from tasks.products_tasks import ProductsTaskSet + # ... import other TaskSets + + class APIUser(HttpUser): + wait_time = between(1, 3) + host = "{{ base_url }}" + + tasks = [UsersTaskSet, ProductsTaskSet] # Mix of all TaskSets + + def on_start(self): + """Called when a simulated user starts""" + {% if include_auth == "true" %} + # Perform login and store token + response = self.client.post("/auth/login", json={ + "username": "test_user", + "password": "test_pass" + }) + self.token = response.json().get("token") + self.client.headers.update({"Authorization": f"Bearer {self.token}"}) + {% endif %} + + def on_stop(self): + """Called when a simulated user stops""" + pass + + # Event hooks for custom behavior + @events.test_start.add_listener + def on_test_start(environment, **kwargs): + print("Load test starting...") + + @events.test_stop.add_listener + def on_test_stop(environment, **kwargs): + print("Load test complete!") + ``` + + {% if test_complexity == "advanced" %} + Include multiple user types with different behavior patterns: + - AdminUser: Has access to admin endpoints + - RegularUser: Normal user operations + - ReadOnlyUser: Only GET requests + + Use task distribution weights for realistic traffic patterns. + {% endif %} + + Add configuration for: + - Environment variables for credentials + - Custom headers + - Connection pooling settings + - Request/response hooks for logging + + Save to {{ output_dir }}/locustfile.py + + Also create a .env.example file with placeholder values for: + - API_BASE_URL + - AUTH_USERNAME + - AUTH_PASSWORD + - Any API keys or tokens needed diff --git a/documentation/src/pages/recipes/data/recipes/subrecipes/generate-support-files.yaml b/documentation/src/pages/recipes/data/recipes/subrecipes/generate-support-files.yaml new file mode 100644 index 000000000000..939beb6a541d --- /dev/null +++ b/documentation/src/pages/recipes/data/recipes/subrecipes/generate-support-files.yaml @@ -0,0 +1,168 @@ +version: "1.0.0" +title: "Support Files Generator" +description: "Generate requirements.txt, README.md, configuration files, and helper scripts" +instructions: | + You are a DevOps and documentation expert. Create all supporting files needed + to make the load tests easy to use and maintain. + + Generate the following files: + 1. requirements.txt - All Python dependencies + 2. README.md - Comprehensive documentation + 3. locust.conf - Configuration file for Locust + 4. docker-compose.yml - Optional Docker setup for distributed testing + 5. run_tests.sh - Shell script with example commands + 6. data/ - Sample data files for parameterized tests + 7. .gitignore - Ignore patterns for test artifacts + +parameters: + - key: output_dir + input_type: string + requirement: required + description: "Output directory" + + - key: base_url + input_type: string + requirement: required + description: "Base URL for API" + +extensions: + - type: builtin + name: developer + timeout: 300 + bundled: true + +prompt: | + Generate all support files for the Locust load tests. + + 1. Create {{ output_dir }}/requirements.txt: + ``` + locust>=2.15.0 + pyyaml>=6.0 + requests>=2.31.0 + python-dotenv>=1.0.0 + faker>=20.0.0 # For generating realistic test data + ``` + + 2. Create {{ output_dir }}/README.md with: + - Project overview and purpose + - API being tested (from analysis) + - Prerequisites and installation steps + - Configuration instructions + - How to run tests: + * Single machine mode + * Distributed mode (master/workers) + * Headless mode + * Web UI mode + - Example commands for different scenarios: + * Quick smoke test: `locust --headless -u 10 -r 2 -t 1m` + * Stress test: `locust --headless -u 100 -r 10 -t 10m` + * Web UI: `locust --web-port 8089` + - Interpreting results and metrics + - Troubleshooting common issues + - Project structure explanation + + 3. Create {{ output_dir }}/locust.conf: + ```conf + # Locust configuration file + host = {{ base_url }} + users = 100 + spawn-rate = 10 + run-time = 5m + headless = false + web-host = 127.0.0.1 + web-port = 8089 + loglevel = INFO + logfile = locust.log + html = reports/report.html + csv = reports/results + ``` + + 4. Create {{ output_dir }}/run_tests.sh: + ```bash + #!/bin/bash + # Example test execution script + + echo "Starting Locust load tests..." + + # Smoke test (10 users, 1 minute) + if [ "$1" == "smoke" ]; then + locust --headless -u 10 -r 2 -t 1m --html=reports/smoke_test.html + + # Load test (100 users, 10 minutes) + elif [ "$1" == "load" ]; then + locust --headless -u 100 -r 10 -t 10m --html=reports/load_test.html + + # Stress test (500 users, 30 minutes) + elif [ "$1" == "stress" ]; then + locust --headless -u 500 -r 50 -t 30m --html=reports/stress_test.html + + # Web UI mode (default) + else + echo "Starting Locust web UI on http://localhost:8089" + locust + fi + ``` + + 5. Create {{ output_dir }}/.gitignore: + ``` + # Python + __pycache__/ + *.py[cod] + *$py.class + .Python + venv/ + env/ + + # Locust + *.log + reports/ + .env + + # IDE + .vscode/ + .idea/ + ``` + + 6. Create {{ output_dir }}/data/sample_users.json with realistic test data: + ```json + [ + {"username": "user1", "email": "user1@example.com"}, + {"username": "user2", "email": "user2@example.com"} + ] + ``` + + 7. Create {{ output_dir }}/docker-compose.yml for distributed testing: + ```yaml + version: '3' + services: + master: + image: locustio/locust + ports: + - "8089:8089" + volumes: + - ./:/mnt/locust + command: -f /mnt/locust/locustfile.py --master + + worker: + image: locustio/locust + volumes: + - ./:/mnt/locust + command: -f /mnt/locust/locustfile.py --worker --master-host master + deploy: + replicas: 4 + ``` + + 8. Create {{ output_dir }}/pytest.ini for optional integration with pytest: + ```ini + [pytest] + testpaths = tests + python_files = test_*.py + python_functions = test_* + addopts = -v --tb=short + ``` + + Create a reports/ directory structure with a README explaining output files. + + Make all shell scripts executable (chmod +x). + + Provide a summary of all generated files and their purposes. diff --git a/documentation/src/pages/recipes/data/recipes/subrecipes/generate-task-sets.yaml b/documentation/src/pages/recipes/data/recipes/subrecipes/generate-task-sets.yaml new file mode 100644 index 000000000000..4cf99de2a79a --- /dev/null +++ b/documentation/src/pages/recipes/data/recipes/subrecipes/generate-task-sets.yaml @@ -0,0 +1,100 @@ +version: "1.0.0" +title: "Locust TaskSet Generator" +description: "Generate Locust TaskSet classes for different API endpoint groups" +instructions: | + You are a Locust performance testing expert. Generate TaskSet classes that + simulate realistic user behavior for different parts of the API. + + For each endpoint group, create a TaskSet that: + - Uses appropriate task weights based on expected traffic patterns + - Includes proper request construction with headers, params, and body + - Handles authentication tokens/sessions + - Validates responses with assertions + - Uses realistic wait times between requests + - Extracts data from responses for subsequent requests (e.g., IDs) + - Includes error handling and logging + + Follow Locust best practices: + - Use @task decorator with weights + - Use self.client for making requests + - Store shared data in self attributes + - Use context managers for setup/teardown if needed + - Add descriptive names for better reporting + +parameters: + - key: output_dir + input_type: string + requirement: required + description: "Directory for output files" + + - key: test_complexity + input_type: string + requirement: optional + default: "standard" + description: "Complexity level for tests" + + - key: include_auth + input_type: string + requirement: required + description: "Whether to include auth handling" + +extensions: + - type: builtin + name: developer + timeout: 600 + bundled: true + +prompt: | + Generate Locust TaskSet classes based on the API analysis. + + Use the analysis data to understand endpoint groups and relationships. + + For each endpoint group (e.g., Users, Products, Orders): + 1. Create a separate TaskSet class + 2. Implement tasks for each endpoint in that group + 3. Set realistic task weights (common operations get higher weights) + 4. Handle data dependencies between requests + + Example structure for each TaskSet: + ```python + from locust import TaskSet, task, between + import json + import random + + class UsersTaskSet(TaskSet): + wait_time = between(1, 3) + + def on_start(self): + # Setup code, e.g., login + pass + + @task(3) # Higher weight for common operations + def list_users(self): + with self.client.get("/users", catch_response=True) as response: + if response.status_code == 200: + response.success() + else: + response.failure(f"Got status code {response.status_code}") + + @task(2) + def get_user_details(self): + # Use data from previous requests + pass + + @task(1) + def create_user(self): + # POST with realistic payload + pass + ``` + + Save each TaskSet to a separate file in {{ output_dir }}/tasks/ + Files should be named: users_tasks.py, products_tasks.py, etc. + + Configuration: + - Test complexity: {{ test_complexity }} + - Include authentication: {{ include_auth }} + + If test_complexity is "advanced", include: + - Data-driven tests with parameterized inputs + - Complex user flows with multiple dependent requests + - Performance-focused optimizations diff --git a/documentation/src/pages/recipes/data/recipes/subrecipes/test-coverage-analysis.yaml b/documentation/src/pages/recipes/data/recipes/subrecipes/test-coverage-analysis.yaml new file mode 100644 index 000000000000..31bd6e8a66bf --- /dev/null +++ b/documentation/src/pages/recipes/data/recipes/subrecipes/test-coverage-analysis.yaml @@ -0,0 +1,54 @@ +version: 1.0.0 +title: "Test Coverage Analyzer" +description: "Identifies code with insufficient test coverage and missing test cases" +instructions: | + You are analyzing test coverage to identify untested code. + + Your tasks: + 1. Locate test files and test frameworks used (Jest, pytest, JUnit, etc.) + 2. Identify the test coverage reporting mechanism if available + 3. Analyze which parts of the codebase lack tests: + - Public APIs and exported functions + - Critical business logic + - Error handling paths + - Edge cases + + 4. For each file/module below {{ min_coverage }}% coverage: + - Calculate estimated current coverage + - Identify specific untested functions/methods + - Prioritize based on code criticality + - Suggest test cases that should be added + + 5. Check for: + - Missing unit tests + - Missing integration tests + - Missing error case tests + - Flaky or skipped tests + + Focus on high-value areas where tests would most improve reliability. + +parameters: + - key: repository_path + input_type: string + requirement: required + description: "Path to the code repository" + + - key: min_coverage + input_type: number + requirement: optional + default: 80 + description: "Minimum acceptable test coverage percentage" + +extensions: + - type: builtin + name: developer + timeout: 300 + bundled: true + +settings: + temperature: 0.2 + +prompt: | + Analyze test coverage in {{ repository_path }}. + Identify code with coverage below {{ min_coverage }}%. + Prioritize critical paths that lack tests and recommend specific test cases to add. \ No newline at end of file diff --git a/documentation/src/pages/recipes/data/recipes/technical-debt-tracker.yaml b/documentation/src/pages/recipes/data/recipes/technical-debt-tracker.yaml new file mode 100644 index 000000000000..d6ab35e0c005 --- /dev/null +++ b/documentation/src/pages/recipes/data/recipes/technical-debt-tracker.yaml @@ -0,0 +1,115 @@ +version: 1.0.0 +title: "Technical Debt Tracker" +description: "Comprehensive analysis of technical debt in a code repository including complexity, test coverage, documentation, dependencies, and code duplication" +author: + contact: Better-Boy + +instructions: | + You are a senior software engineer conducting a comprehensive technical debt analysis. + + Your workflow: + 1. First, examine the repository structure at {{ repository_path }} to understand the project + 2. Identify the primary programming language(s) and frameworks used + 3. Run each sub-recipe to gather specific technical debt metrics: + - complexity_analyzer: Identify complex code that needs refactoring + - test_coverage_analyzer: Find untested or poorly tested code + - documentation_analyzer: Identify missing or outdated documentation + - dependency_analyzer: Check for outdated or vulnerable dependencies + - duplication_detector: Find duplicated code that should be refactored + + 4. After all analyses complete, create a consolidated report that: + - Prioritizes issues by severity (Critical, High, Medium, Low) + - Groups related issues together + - Provides actionable recommendations with estimated effort + - Creates a technical debt reduction roadmap + + 5. Save the report as {{ output_file }} in markdown format + + Be thorough but focused on issues that will have the most impact on code quality and maintainability. + +parameters: + - key: repository_path + input_type: string + requirement: required + description: "Path to the code repository to analyze" + + - key: output_file + input_type: string + requirement: optional + default: "technical-debt-report.md" + description: "Output file name for the technical debt report" + + - key: complexity_threshold + input_type: number + requirement: optional + default: 15 + description: "Cyclomatic complexity threshold (functions above this are flagged)" + + - key: min_test_coverage + input_type: number + requirement: optional + default: 80 + description: "Minimum acceptable test coverage percentage" + + - key: max_dependency_age_days + input_type: number + requirement: optional + default: 365 + description: "Flag dependencies older than this many days" + +sub_recipes: + - name: "complexity_analyzer" + path: "./subrecipes/complexity-analysis.yaml" + values: + complexity_threshold: "{{ complexity_threshold }}" + + - name: "test_coverage_analyzer" + path: "./subrecipes/test-coverage-analysis.yaml" + values: + min_coverage: "{{ min_test_coverage }}" + + - name: "documentation_analyzer" + path: "./subrecipes/documentation-analysis.yaml" + values: + check_readme: "true" + check_api_docs: "true" + + - name: "dependency_analyzer" + path: "./subrecipes/dependency-analysis.yaml" + values: + max_age_days: "{{ max_dependency_age_days }}" + check_vulnerabilities: "true" + + - name: "duplication_detector" + path: "./subrecipes/duplication-detection.yaml" + values: + min_lines: "10" + similarity_threshold: "0.85" + +extensions: + - type: builtin + name: developer + timeout: 300 + bundled: true + description: "File system and code analysis tools" + +prompt: | + Analyze the code repository at {{ repository_path }} for technical debt. + + Run a comprehensive analysis covering: + - Code complexity and maintainability + - Test coverage gaps + - Documentation quality + - Dependency health and security + - Code duplication + + Create a prioritized technical debt report and save it to {{ output_file }}. + +activities: + - "Scan repository structure" + - "Analyze code complexity" + - "Check test coverage" + - "Review documentation" + - "Audit dependencies" + - "Detect code duplication" + - "Generate prioritized report" \ No newline at end of file diff --git a/documentation/static/servers.json b/documentation/static/servers.json index cd4ea333da59..f6c567dca6bb 100644 --- a/documentation/static/servers.json +++ b/documentation/static/servers.json @@ -34,10 +34,10 @@ ] }, { - "id": "apify-mcp-server", + "id": "apify", "name": "Apify", "description": "Extract data from any website with thousands of scrapers, crawlers, and automations on Apify Store ⚡", - "command": "npx -y apify-mcp-server@latest", + "command": "npx -y @apify/actors-mcp-server", "link": "https://github.com/apify/apify-mcp-server", "installation_notes": "Install using npx. Requires an Apify API token.", "is_builtin": false, @@ -73,7 +73,7 @@ "description": "Automatic data visualisation via MCP-UI", "command": "", "link": "https://github.com/block/goose/tree/main/crates/goose-mcp/src/autovisualiser", - "installation_notes": "This is a built-in extension that comes with Goose and can be enabled on the Extensions page.", + "installation_notes": "This is a built-in extension that comes with goose. No installation required.", "is_builtin": true, "endorsed": true, "environmentVariables": [] @@ -168,7 +168,7 @@ "description": "Built-in computer controls for webscraping, file caching, and automations", "command": "", "link": "https://github.com/block/goose/tree/main/crates/goose-mcp/src/computercontroller", - "installation_notes": "This is a built-in extension that comes with Goose and can be enabled on the Extensions page.", + "installation_notes": "This is a built-in extension that comes with goose. No installation required.", "is_builtin": true, "endorsed": true, "environmentVariables": [] @@ -176,7 +176,7 @@ { "id": "container-use", "name": "Container Use", - "description": "Integrate container workflows with Goose using the Container-Use MCP", + "description": "Integrate container workflows with goose using the Container-Use MCP", "command": "npx -y mcp-remote https://container-use.com/mcp", "link": "https://container-use.com", "installation_notes": "Requires Node.js and Docker.", @@ -201,7 +201,7 @@ "description": "Built-in developer tools for file editing and shell command execution", "command": "", "link": "https://github.com/block/goose/tree/main/crates/goose-mcp/src/developer", - "installation_notes": "This is a built-in extension that comes with Goose by default. No installation required.", + "installation_notes": "This is a built-in extension that comes with goose and is enabled by default. No installation required.", "is_builtin": true, "endorsed": true, "environmentVariables": [] @@ -300,8 +300,8 @@ }, { "id": "goose-docs", - "name": "Goose Docs", - "description": "GitMCP for Goose documentation", + "name": "goose Docs", + "description": "GitMCP for goose documentation", "command": "npx mcp-remote https://block.gitmcp.io/goose/", "link": "https://github.com/idosal/git-mcp", "is_builtin": false, @@ -376,7 +376,7 @@ "description": "Built-in memory system for persistent context and information storage", "command": "", "link": "https://github.com/block/goose/tree/main/crates/goose-mcp/src/memory", - "installation_notes": "This is a built-in extension that comes with Goose and can be enabled on the Extensions page.", + "installation_notes": "This is a built-in extension that comes with goose. No installation required.", "is_builtin": true, "endorsed": true, "environmentVariables": [] @@ -539,17 +539,6 @@ "endorsed": false, "environmentVariables": [] }, - { - "id": "speech_mcp", - "name": "Speech Interface", - "description": "Voice interaction with audio visualization for Goose - enables real-time voice interaction, audio/video transcription, text-to-speech conversion, and multi-speaker audio generation", - "command": "uvx -p 3.10.14 speech-mcp@latest", - "link": "https://github.com/Kvadratni/speech-mcp", - "installation_notes": "Install using uvx package manager. Requires PortAudio to be installed on your system for PyAudio to capture audio from your microphone.", - "is_builtin": false, - "endorsed": true, - "environmentVariables": [] - }, { "id": "square-mcp", "name": "Square", @@ -594,8 +583,8 @@ "name": "Tutorial", "description": "Built-in tutorial and learning management system", "command": "", - "link": "https://github.com/tutorial/tutorial-mcp", - "installation_notes": "This is a built-in extension that comes with Goose and can be enabled on the Extensions page.", + "link": "https://github.com/block/goose/tree/main/crates/goose-mcp/src/tutorial", + "installation_notes": "This is a built-in extension that comes with goose. No installation required.", "is_builtin": true, "endorsed": false, "environmentVariables": [] diff --git a/documentation/yarn.lock b/documentation/yarn.lock deleted file mode 100644 index c771f760d930..000000000000 --- a/documentation/yarn.lock +++ /dev/null @@ -1,9377 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@algolia/autocomplete-core@1.17.9": - version "1.17.9" - resolved "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.9.tgz" - integrity sha512-O7BxrpLDPJWWHv/DLA9DRFWs+iY1uOJZkqUwjS5HSZAGcl0hIVCQ97LTLewiZmZ402JYUrun+8NqFP+hCknlbQ== - dependencies: - "@algolia/autocomplete-plugin-algolia-insights" "1.17.9" - "@algolia/autocomplete-shared" "1.17.9" - -"@algolia/autocomplete-plugin-algolia-insights@1.17.9": - version "1.17.9" - resolved "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.9.tgz" - integrity sha512-u1fEHkCbWF92DBeB/KHeMacsjsoI0wFhjZtlCq2ddZbAehshbZST6Hs0Avkc0s+4UyBGbMDnSuXHLuvRWK5iDQ== - dependencies: - "@algolia/autocomplete-shared" "1.17.9" - -"@algolia/autocomplete-preset-algolia@1.17.9": - version "1.17.9" - resolved "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.9.tgz" - integrity sha512-Na1OuceSJeg8j7ZWn5ssMu/Ax3amtOwk76u4h5J4eK2Nx2KB5qt0Z4cOapCsxot9VcEN11ADV5aUSlQF4RhGjQ== - dependencies: - "@algolia/autocomplete-shared" "1.17.9" - -"@algolia/autocomplete-shared@1.17.9": - version "1.17.9" - resolved "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.9.tgz" - integrity sha512-iDf05JDQ7I0b7JEA/9IektxN/80a2MZ1ToohfmNS3rfeuQnIKI3IJlIafD0xu4StbtQTghx9T3Maa97ytkXenQ== - -"@algolia/client-abtesting@5.20.0": - version "5.20.0" - resolved "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.20.0.tgz" - integrity sha512-YaEoNc1Xf2Yk6oCfXXkZ4+dIPLulCx8Ivqj0OsdkHWnsI3aOJChY5qsfyHhDBNSOhqn2ilgHWxSfyZrjxBcAww== - dependencies: - "@algolia/client-common" "5.20.0" - "@algolia/requester-browser-xhr" "5.20.0" - "@algolia/requester-fetch" "5.20.0" - "@algolia/requester-node-http" "5.20.0" - -"@algolia/client-analytics@5.20.0": - version "5.20.0" - resolved "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.20.0.tgz" - integrity sha512-CIT9ni0+5sYwqehw+t5cesjho3ugKQjPVy/iPiJvtJX4g8Cdb6je6SPt2uX72cf2ISiXCAX9U3cY0nN0efnRDw== - dependencies: - "@algolia/client-common" "5.20.0" - "@algolia/requester-browser-xhr" "5.20.0" - "@algolia/requester-fetch" "5.20.0" - "@algolia/requester-node-http" "5.20.0" - -"@algolia/client-common@5.20.0": - version "5.20.0" - resolved "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.20.0.tgz" - integrity sha512-iSTFT3IU8KNpbAHcBUJw2HUrPnMXeXLyGajmCL7gIzWOsYM4GabZDHXOFx93WGiXMti1dymz8k8R+bfHv1YZmA== - -"@algolia/client-insights@5.20.0": - version "5.20.0" - resolved "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.20.0.tgz" - integrity sha512-w9RIojD45z1csvW1vZmAko82fqE/Dm+Ovsy2ElTsjFDB0HMAiLh2FO86hMHbEXDPz6GhHKgGNmBRiRP8dDPgJg== - dependencies: - "@algolia/client-common" "5.20.0" - "@algolia/requester-browser-xhr" "5.20.0" - "@algolia/requester-fetch" "5.20.0" - "@algolia/requester-node-http" "5.20.0" - -"@algolia/client-personalization@5.20.0": - version "5.20.0" - resolved "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.20.0.tgz" - integrity sha512-p/hftHhrbiHaEcxubYOzqVV4gUqYWLpTwK+nl2xN3eTrSW9SNuFlAvUBFqPXSVBqc6J5XL9dNKn3y8OA1KElSQ== - dependencies: - "@algolia/client-common" "5.20.0" - "@algolia/requester-browser-xhr" "5.20.0" - "@algolia/requester-fetch" "5.20.0" - "@algolia/requester-node-http" "5.20.0" - -"@algolia/client-query-suggestions@5.20.0": - version "5.20.0" - resolved "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.20.0.tgz" - integrity sha512-m4aAuis5vZi7P4gTfiEs6YPrk/9hNTESj3gEmGFgfJw3hO2ubdS4jSId1URd6dGdt0ax2QuapXufcrN58hPUcw== - dependencies: - "@algolia/client-common" "5.20.0" - "@algolia/requester-browser-xhr" "5.20.0" - "@algolia/requester-fetch" "5.20.0" - "@algolia/requester-node-http" "5.20.0" - -"@algolia/client-search@5.20.0": - version "5.20.0" - resolved "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.20.0.tgz" - integrity sha512-KL1zWTzrlN4MSiaK1ea560iCA/UewMbS4ZsLQRPoDTWyrbDKVbztkPwwv764LAqgXk0fvkNZvJ3IelcK7DqhjQ== - dependencies: - "@algolia/client-common" "5.20.0" - "@algolia/requester-browser-xhr" "5.20.0" - "@algolia/requester-fetch" "5.20.0" - "@algolia/requester-node-http" "5.20.0" - -"@algolia/events@^4.0.1": - version "4.0.1" - resolved "https://registry.npmjs.org/@algolia/events/-/events-4.0.1.tgz" - integrity sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ== - -"@algolia/ingestion@1.20.0": - version "1.20.0" - resolved "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.20.0.tgz" - integrity sha512-shj2lTdzl9un4XJblrgqg54DoK6JeKFO8K8qInMu4XhE2JuB8De6PUuXAQwiRigZupbI0xq8aM0LKdc9+qiLQA== - dependencies: - "@algolia/client-common" "5.20.0" - "@algolia/requester-browser-xhr" "5.20.0" - "@algolia/requester-fetch" "5.20.0" - "@algolia/requester-node-http" "5.20.0" - -"@algolia/monitoring@1.20.0": - version "1.20.0" - resolved "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.20.0.tgz" - integrity sha512-aF9blPwOhKtWvkjyyXh9P5peqmhCA1XxLBRgItT+K6pbT0q4hBDQrCid+pQZJYy4HFUKjB/NDDwyzFhj/rwKhw== - dependencies: - "@algolia/client-common" "5.20.0" - "@algolia/requester-browser-xhr" "5.20.0" - "@algolia/requester-fetch" "5.20.0" - "@algolia/requester-node-http" "5.20.0" - -"@algolia/recommend@5.20.0": - version "5.20.0" - resolved "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.20.0.tgz" - integrity sha512-T6B/WPdZR3b89/F9Vvk6QCbt/wrLAtrGoL8z4qPXDFApQ8MuTFWbleN/4rHn6APWO3ps+BUePIEbue2rY5MlRw== - dependencies: - "@algolia/client-common" "5.20.0" - "@algolia/requester-browser-xhr" "5.20.0" - "@algolia/requester-fetch" "5.20.0" - "@algolia/requester-node-http" "5.20.0" - -"@algolia/requester-browser-xhr@5.20.0": - version "5.20.0" - resolved "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.20.0.tgz" - integrity sha512-t6//lXsq8E85JMenHrI6mhViipUT5riNhEfCcvtRsTV+KIBpC6Od18eK864dmBhoc5MubM0f+sGpKOqJIlBSCg== - dependencies: - "@algolia/client-common" "5.20.0" - -"@algolia/requester-fetch@5.20.0": - version "5.20.0" - resolved "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.20.0.tgz" - integrity sha512-FHxYGqRY+6bgjKsK4aUsTAg6xMs2S21elPe4Y50GB0Y041ihvw41Vlwy2QS6K9ldoftX4JvXodbKTcmuQxywdQ== - dependencies: - "@algolia/client-common" "5.20.0" - -"@algolia/requester-node-http@5.20.0": - version "5.20.0" - resolved "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.20.0.tgz" - integrity sha512-kmtQClq/w3vtPteDSPvaW9SPZL/xrIgMrxZyAgsFwrJk0vJxqyC5/hwHmrCraDnStnGSADnLpBf4SpZnwnkwWw== - dependencies: - "@algolia/client-common" "5.20.0" - -"@alloc/quick-lru@^5.2.0": - version "5.2.0" - resolved "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz" - integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== - -"@ampproject/remapping@^2.2.0": - version "2.3.0" - resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz" - integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== - dependencies: - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.24" - -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.26.0", "@babel/code-frame@^7.26.2", "@babel/code-frame@^7.8.3": - version "7.26.2" - resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz" - integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== - dependencies: - "@babel/helper-validator-identifier" "^7.25.9" - js-tokens "^4.0.0" - picocolors "^1.0.0" - -"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.26.0", "@babel/compat-data@^7.26.5": - version "7.26.5" - resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz" - integrity sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg== - -"@babel/core@^7.21.3", "@babel/core@^7.25.9": - version "7.26.0" - resolved "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz" - integrity sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg== - dependencies: - "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.26.0" - "@babel/generator" "^7.26.0" - "@babel/helper-compilation-targets" "^7.25.9" - "@babel/helper-module-transforms" "^7.26.0" - "@babel/helpers" "^7.26.0" - "@babel/parser" "^7.26.0" - "@babel/template" "^7.25.9" - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.26.0" - convert-source-map "^2.0.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.2.3" - semver "^6.3.1" - -"@babel/generator@^7.25.9", "@babel/generator@^7.26.0", "@babel/generator@^7.26.5": - version "7.26.5" - resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz" - integrity sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw== - dependencies: - "@babel/parser" "^7.26.5" - "@babel/types" "^7.26.5" - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.25" - jsesc "^3.0.2" - -"@babel/helper-annotate-as-pure@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz" - integrity sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g== - dependencies: - "@babel/types" "^7.25.9" - -"@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.25.9": - version "7.26.5" - resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz" - integrity sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA== - dependencies: - "@babel/compat-data" "^7.26.5" - "@babel/helper-validator-option" "^7.25.9" - browserslist "^4.24.0" - lru-cache "^5.1.1" - semver "^6.3.1" - -"@babel/helper-create-class-features-plugin@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz" - integrity sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.25.9" - "@babel/helper-member-expression-to-functions" "^7.25.9" - "@babel/helper-optimise-call-expression" "^7.25.9" - "@babel/helper-replace-supers" "^7.25.9" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" - "@babel/traverse" "^7.25.9" - semver "^6.3.1" - -"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.25.9": - version "7.26.3" - resolved "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz" - integrity sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong== - dependencies: - "@babel/helper-annotate-as-pure" "^7.25.9" - regexpu-core "^6.2.0" - semver "^6.3.1" - -"@babel/helper-define-polyfill-provider@^0.6.2", "@babel/helper-define-polyfill-provider@^0.6.3": - version "0.6.3" - resolved "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz" - integrity sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg== - dependencies: - "@babel/helper-compilation-targets" "^7.22.6" - "@babel/helper-plugin-utils" "^7.22.5" - debug "^4.1.1" - lodash.debounce "^4.0.8" - resolve "^1.14.2" - -"@babel/helper-member-expression-to-functions@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz" - integrity sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ== - dependencies: - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.25.9" - -"@babel/helper-module-imports@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz" - integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw== - dependencies: - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.25.9" - -"@babel/helper-module-transforms@^7.25.9", "@babel/helper-module-transforms@^7.26.0": - version "7.26.0" - resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz" - integrity sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw== - dependencies: - "@babel/helper-module-imports" "^7.25.9" - "@babel/helper-validator-identifier" "^7.25.9" - "@babel/traverse" "^7.25.9" - -"@babel/helper-optimise-call-expression@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz" - integrity sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ== - dependencies: - "@babel/types" "^7.25.9" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.25.9", "@babel/helper-plugin-utils@^7.26.5", "@babel/helper-plugin-utils@^7.8.0": - version "7.26.5" - resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz" - integrity sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg== - -"@babel/helper-remap-async-to-generator@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz" - integrity sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.25.9" - "@babel/helper-wrap-function" "^7.25.9" - "@babel/traverse" "^7.25.9" - -"@babel/helper-replace-supers@^7.25.9": - version "7.26.5" - resolved "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz" - integrity sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg== - dependencies: - "@babel/helper-member-expression-to-functions" "^7.25.9" - "@babel/helper-optimise-call-expression" "^7.25.9" - "@babel/traverse" "^7.26.5" - -"@babel/helper-skip-transparent-expression-wrappers@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz" - integrity sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA== - dependencies: - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.25.9" - -"@babel/helper-string-parser@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz" - integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== - -"@babel/helper-validator-identifier@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz" - integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== - -"@babel/helper-validator-option@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz" - integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw== - -"@babel/helper-wrap-function@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz" - integrity sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g== - dependencies: - "@babel/template" "^7.25.9" - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.25.9" - -"@babel/helpers@^7.26.0": - version "7.27.0" - resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz" - integrity sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg== - dependencies: - "@babel/template" "^7.27.0" - "@babel/types" "^7.27.0" - -"@babel/parser@^7.26.0", "@babel/parser@^7.26.5", "@babel/parser@^7.27.0": - version "7.27.0" - resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz" - integrity sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg== - dependencies: - "@babel/types" "^7.27.0" - -"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz" - integrity sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/traverse" "^7.25.9" - -"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz" - integrity sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz" - integrity sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz" - integrity sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" - "@babel/plugin-transform-optional-chaining" "^7.25.9" - -"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz" - integrity sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/traverse" "^7.25.9" - -"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": - version "7.21.0-placeholder-for-preset-env.2" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz" - integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== - -"@babel/plugin-syntax-dynamic-import@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz" - integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-import-assertions@^7.26.0": - version "7.26.0" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz" - integrity sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-syntax-import-attributes@^7.26.0": - version "7.26.0" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz" - integrity sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-syntax-jsx@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz" - integrity sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-syntax-typescript@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz" - integrity sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-syntax-unicode-sets-regex@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz" - integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-arrow-functions@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz" - integrity sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-async-generator-functions@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz" - integrity sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-remap-async-to-generator" "^7.25.9" - "@babel/traverse" "^7.25.9" - -"@babel/plugin-transform-async-to-generator@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz" - integrity sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ== - dependencies: - "@babel/helper-module-imports" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-remap-async-to-generator" "^7.25.9" - -"@babel/plugin-transform-block-scoped-functions@^7.25.9": - version "7.26.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz" - integrity sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ== - dependencies: - "@babel/helper-plugin-utils" "^7.26.5" - -"@babel/plugin-transform-block-scoping@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz" - integrity sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-class-properties@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz" - integrity sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-class-static-block@^7.26.0": - version "7.26.0" - resolved "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz" - integrity sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-classes@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz" - integrity sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg== - dependencies: - "@babel/helper-annotate-as-pure" "^7.25.9" - "@babel/helper-compilation-targets" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-replace-supers" "^7.25.9" - "@babel/traverse" "^7.25.9" - globals "^11.1.0" - -"@babel/plugin-transform-computed-properties@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz" - integrity sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/template" "^7.25.9" - -"@babel/plugin-transform-destructuring@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz" - integrity sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-dotall-regex@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz" - integrity sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-duplicate-keys@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz" - integrity sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz" - integrity sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-dynamic-import@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz" - integrity sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-exponentiation-operator@^7.25.9": - version "7.26.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz" - integrity sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-export-namespace-from@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz" - integrity sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-for-of@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz" - integrity sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" - -"@babel/plugin-transform-function-name@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz" - integrity sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA== - dependencies: - "@babel/helper-compilation-targets" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/traverse" "^7.25.9" - -"@babel/plugin-transform-json-strings@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz" - integrity sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-literals@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz" - integrity sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-logical-assignment-operators@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz" - integrity sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-member-expression-literals@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz" - integrity sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-modules-amd@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz" - integrity sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw== - dependencies: - "@babel/helper-module-transforms" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-modules-commonjs@^7.25.9": - version "7.26.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz" - integrity sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ== - dependencies: - "@babel/helper-module-transforms" "^7.26.0" - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-modules-systemjs@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz" - integrity sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA== - dependencies: - "@babel/helper-module-transforms" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-validator-identifier" "^7.25.9" - "@babel/traverse" "^7.25.9" - -"@babel/plugin-transform-modules-umd@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz" - integrity sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw== - dependencies: - "@babel/helper-module-transforms" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-named-capturing-groups-regex@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz" - integrity sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-new-target@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz" - integrity sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-nullish-coalescing-operator@^7.25.9": - version "7.26.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz" - integrity sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw== - dependencies: - "@babel/helper-plugin-utils" "^7.26.5" - -"@babel/plugin-transform-numeric-separator@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz" - integrity sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-object-rest-spread@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz" - integrity sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg== - dependencies: - "@babel/helper-compilation-targets" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/plugin-transform-parameters" "^7.25.9" - -"@babel/plugin-transform-object-super@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz" - integrity sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-replace-supers" "^7.25.9" - -"@babel/plugin-transform-optional-catch-binding@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz" - integrity sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-optional-chaining@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz" - integrity sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" - -"@babel/plugin-transform-parameters@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz" - integrity sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-private-methods@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz" - integrity sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-private-property-in-object@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz" - integrity sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.25.9" - "@babel/helper-create-class-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-property-literals@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz" - integrity sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-react-constant-elements@^7.21.3": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.25.9.tgz" - integrity sha512-Ncw2JFsJVuvfRsa2lSHiC55kETQVLSnsYGQ1JDDwkUeWGTL/8Tom8aLTnlqgoeuopWrbbGndrc9AlLYrIosrow== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-react-display-name@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.25.9.tgz" - integrity sha512-KJfMlYIUxQB1CJfO3e0+h0ZHWOTLCPP115Awhaz8U0Zpq36Gl/cXlpoyMRnUWlhNUBAzldnCiAZNvCDj7CrKxQ== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-react-jsx-development@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.25.9.tgz" - integrity sha512-9mj6rm7XVYs4mdLIpbZnHOYdpW42uoiBCTVowg7sP1thUOiANgMb4UtpRivR0pp5iL+ocvUv7X4mZgFRpJEzGw== - dependencies: - "@babel/plugin-transform-react-jsx" "^7.25.9" - -"@babel/plugin-transform-react-jsx@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.9.tgz" - integrity sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.25.9" - "@babel/helper-module-imports" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/plugin-syntax-jsx" "^7.25.9" - "@babel/types" "^7.25.9" - -"@babel/plugin-transform-react-pure-annotations@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.25.9.tgz" - integrity sha512-KQ/Takk3T8Qzj5TppkS1be588lkbTp5uj7w6a0LeQaTMSckU/wK0oJ/pih+T690tkgI5jfmg2TqDJvd41Sj1Cg== - dependencies: - "@babel/helper-annotate-as-pure" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-regenerator@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz" - integrity sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - regenerator-transform "^0.15.2" - -"@babel/plugin-transform-regexp-modifiers@^7.26.0": - version "7.26.0" - resolved "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz" - integrity sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-reserved-words@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz" - integrity sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-runtime@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.25.9.tgz" - integrity sha512-nZp7GlEl+yULJrClz0SwHPqir3lc0zsPrDHQUcxGspSL7AKrexNSEfTbfqnDNJUO13bgKyfuOLMF8Xqtu8j3YQ== - dependencies: - "@babel/helper-module-imports" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - babel-plugin-polyfill-corejs2 "^0.4.10" - babel-plugin-polyfill-corejs3 "^0.10.6" - babel-plugin-polyfill-regenerator "^0.6.1" - semver "^6.3.1" - -"@babel/plugin-transform-shorthand-properties@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz" - integrity sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-spread@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz" - integrity sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" - -"@babel/plugin-transform-sticky-regex@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz" - integrity sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-template-literals@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz" - integrity sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-typeof-symbol@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz" - integrity sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-typescript@^7.25.9": - version "7.26.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.26.5.tgz" - integrity sha512-GJhPO0y8SD5EYVCy2Zr+9dSZcEgaSmq5BLR0Oc25TOEhC+ba49vUAGZFjy8v79z9E1mdldq4x9d1xgh4L1d5dQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.25.9" - "@babel/helper-create-class-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.26.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" - "@babel/plugin-syntax-typescript" "^7.25.9" - -"@babel/plugin-transform-unicode-escapes@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz" - integrity sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-unicode-property-regex@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz" - integrity sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-unicode-regex@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz" - integrity sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-unicode-sets-regex@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz" - integrity sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/preset-env@^7.20.2", "@babel/preset-env@^7.25.9": - version "7.26.0" - resolved "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.0.tgz" - integrity sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw== - dependencies: - "@babel/compat-data" "^7.26.0" - "@babel/helper-compilation-targets" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-validator-option" "^7.25.9" - "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.25.9" - "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.25.9" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.25.9" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.25.9" - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.25.9" - "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" - "@babel/plugin-syntax-import-assertions" "^7.26.0" - "@babel/plugin-syntax-import-attributes" "^7.26.0" - "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" - "@babel/plugin-transform-arrow-functions" "^7.25.9" - "@babel/plugin-transform-async-generator-functions" "^7.25.9" - "@babel/plugin-transform-async-to-generator" "^7.25.9" - "@babel/plugin-transform-block-scoped-functions" "^7.25.9" - "@babel/plugin-transform-block-scoping" "^7.25.9" - "@babel/plugin-transform-class-properties" "^7.25.9" - "@babel/plugin-transform-class-static-block" "^7.26.0" - "@babel/plugin-transform-classes" "^7.25.9" - "@babel/plugin-transform-computed-properties" "^7.25.9" - "@babel/plugin-transform-destructuring" "^7.25.9" - "@babel/plugin-transform-dotall-regex" "^7.25.9" - "@babel/plugin-transform-duplicate-keys" "^7.25.9" - "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.25.9" - "@babel/plugin-transform-dynamic-import" "^7.25.9" - "@babel/plugin-transform-exponentiation-operator" "^7.25.9" - "@babel/plugin-transform-export-namespace-from" "^7.25.9" - "@babel/plugin-transform-for-of" "^7.25.9" - "@babel/plugin-transform-function-name" "^7.25.9" - "@babel/plugin-transform-json-strings" "^7.25.9" - "@babel/plugin-transform-literals" "^7.25.9" - "@babel/plugin-transform-logical-assignment-operators" "^7.25.9" - "@babel/plugin-transform-member-expression-literals" "^7.25.9" - "@babel/plugin-transform-modules-amd" "^7.25.9" - "@babel/plugin-transform-modules-commonjs" "^7.25.9" - "@babel/plugin-transform-modules-systemjs" "^7.25.9" - "@babel/plugin-transform-modules-umd" "^7.25.9" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.25.9" - "@babel/plugin-transform-new-target" "^7.25.9" - "@babel/plugin-transform-nullish-coalescing-operator" "^7.25.9" - "@babel/plugin-transform-numeric-separator" "^7.25.9" - "@babel/plugin-transform-object-rest-spread" "^7.25.9" - "@babel/plugin-transform-object-super" "^7.25.9" - "@babel/plugin-transform-optional-catch-binding" "^7.25.9" - "@babel/plugin-transform-optional-chaining" "^7.25.9" - "@babel/plugin-transform-parameters" "^7.25.9" - "@babel/plugin-transform-private-methods" "^7.25.9" - "@babel/plugin-transform-private-property-in-object" "^7.25.9" - "@babel/plugin-transform-property-literals" "^7.25.9" - "@babel/plugin-transform-regenerator" "^7.25.9" - "@babel/plugin-transform-regexp-modifiers" "^7.26.0" - "@babel/plugin-transform-reserved-words" "^7.25.9" - "@babel/plugin-transform-shorthand-properties" "^7.25.9" - "@babel/plugin-transform-spread" "^7.25.9" - "@babel/plugin-transform-sticky-regex" "^7.25.9" - "@babel/plugin-transform-template-literals" "^7.25.9" - "@babel/plugin-transform-typeof-symbol" "^7.25.9" - "@babel/plugin-transform-unicode-escapes" "^7.25.9" - "@babel/plugin-transform-unicode-property-regex" "^7.25.9" - "@babel/plugin-transform-unicode-regex" "^7.25.9" - "@babel/plugin-transform-unicode-sets-regex" "^7.25.9" - "@babel/preset-modules" "0.1.6-no-external-plugins" - babel-plugin-polyfill-corejs2 "^0.4.10" - babel-plugin-polyfill-corejs3 "^0.10.6" - babel-plugin-polyfill-regenerator "^0.6.1" - core-js-compat "^3.38.1" - semver "^6.3.1" - -"@babel/preset-modules@0.1.6-no-external-plugins": - version "0.1.6-no-external-plugins" - resolved "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz" - integrity sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/types" "^7.4.4" - esutils "^2.0.2" - -"@babel/preset-react@^7.18.6", "@babel/preset-react@^7.25.9": - version "7.26.3" - resolved "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.26.3.tgz" - integrity sha512-Nl03d6T9ky516DGK2YMxrTqvnpUW63TnJMOMonj+Zae0JiPC5BC9xPMSL6L8fiSpA5vP88qfygavVQvnLp+6Cw== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-validator-option" "^7.25.9" - "@babel/plugin-transform-react-display-name" "^7.25.9" - "@babel/plugin-transform-react-jsx" "^7.25.9" - "@babel/plugin-transform-react-jsx-development" "^7.25.9" - "@babel/plugin-transform-react-pure-annotations" "^7.25.9" - -"@babel/preset-typescript@^7.21.0", "@babel/preset-typescript@^7.25.9": - version "7.26.0" - resolved "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.26.0.tgz" - integrity sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-validator-option" "^7.25.9" - "@babel/plugin-syntax-jsx" "^7.25.9" - "@babel/plugin-transform-modules-commonjs" "^7.25.9" - "@babel/plugin-transform-typescript" "^7.25.9" - -"@babel/runtime-corejs3@^7.25.9": - version "7.27.0" - resolved "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.27.0.tgz" - integrity sha512-UWjX6t+v+0ckwZ50Y5ShZLnlk95pP5MyW/pon9tiYzl3+18pkTHTFNTKr7rQbfRXPkowt2QAn30o1b6oswszew== - dependencies: - core-js-pure "^3.30.2" - regenerator-runtime "^0.14.0" - -"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.25.9", "@babel/runtime@^7.8.4": - version "7.27.0" - resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz" - integrity sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw== - dependencies: - regenerator-runtime "^0.14.0" - -"@babel/template@^7.25.9", "@babel/template@^7.27.0": - version "7.27.0" - resolved "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz" - integrity sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA== - dependencies: - "@babel/code-frame" "^7.26.2" - "@babel/parser" "^7.27.0" - "@babel/types" "^7.27.0" - -"@babel/traverse@^7.25.9", "@babel/traverse@^7.26.5": - version "7.26.5" - resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.5.tgz" - integrity sha512-rkOSPOw+AXbgtwUga3U4u8RpoK9FEFWBNAlTpcnkLFjL5CT+oyHNuUUC/xx6XefEJ16r38r8Bc/lfp6rYuHeJQ== - dependencies: - "@babel/code-frame" "^7.26.2" - "@babel/generator" "^7.26.5" - "@babel/parser" "^7.26.5" - "@babel/template" "^7.25.9" - "@babel/types" "^7.26.5" - debug "^4.3.1" - globals "^11.1.0" - -"@babel/types@^7.21.3", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.5", "@babel/types@^7.27.0", "@babel/types@^7.4.4": - version "7.27.0" - resolved "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz" - integrity sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg== - dependencies: - "@babel/helper-string-parser" "^7.25.9" - "@babel/helper-validator-identifier" "^7.25.9" - -"@colors/colors@1.5.0": - version "1.5.0" - resolved "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz" - integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== - -"@csstools/cascade-layer-name-parser@^2.0.4": - version "2.0.4" - resolved "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-2.0.4.tgz" - integrity sha512-7DFHlPuIxviKYZrOiwVU/PiHLm3lLUR23OMuEEtfEOQTOp9hzQ2JjdY6X5H18RVuUPJqSCI+qNnD5iOLMVE0bA== - -"@csstools/color-helpers@^5.0.1": - version "5.0.1" - resolved "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.1.tgz" - integrity sha512-MKtmkA0BX87PKaO1NFRTFH+UnkgnmySQOvNxJubsadusqPEC2aJ9MOQiMceZJJ6oitUl/i0L6u0M1IrmAOmgBA== - -"@csstools/css-calc@^2.1.1": - version "2.1.1" - resolved "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.1.tgz" - integrity sha512-rL7kaUnTkL9K+Cvo2pnCieqNpTKgQzy5f+N+5Iuko9HAoasP+xgprVh7KN/MaJVvVL1l0EzQq2MoqBHKSrDrag== - -"@csstools/css-color-parser@^3.0.7": - version "3.0.7" - resolved "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.7.tgz" - integrity sha512-nkMp2mTICw32uE5NN+EsJ4f5N+IGFeCFu4bGpiKgb2Pq/7J/MpyLBeQ5ry4KKtRFZaYs6sTmcMYrSRIyj5DFKA== - dependencies: - "@csstools/color-helpers" "^5.0.1" - "@csstools/css-calc" "^2.1.1" - -"@csstools/css-parser-algorithms@^3.0.4": - version "3.0.4" - resolved "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz" - integrity sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A== - -"@csstools/css-tokenizer@^3.0.3": - version "3.0.3" - resolved "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz" - integrity sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw== - -"@csstools/media-query-list-parser@^4.0.2": - version "4.0.2" - resolved "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.2.tgz" - integrity sha512-EUos465uvVvMJehckATTlNqGj4UJWkTmdWuDMjqvSUkjGpmOyFZBVwb4knxCm/k2GMTXY+c/5RkdndzFYWeX5A== - -"@csstools/postcss-cascade-layers@^5.0.1": - version "5.0.1" - resolved "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-5.0.1.tgz" - integrity sha512-XOfhI7GShVcKiKwmPAnWSqd2tBR0uxt+runAxttbSp/LY2U16yAVPmAf7e9q4JJ0d+xMNmpwNDLBXnmRCl3HMQ== - dependencies: - "@csstools/selector-specificity" "^5.0.0" - postcss-selector-parser "^7.0.0" - -"@csstools/postcss-color-function@^4.0.7": - version "4.0.7" - resolved "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-4.0.7.tgz" - integrity sha512-aDHYmhNIHR6iLw4ElWhf+tRqqaXwKnMl0YsQ/X105Zc4dQwe6yJpMrTN6BwOoESrkDjOYMOfORviSSLeDTJkdQ== - dependencies: - "@csstools/css-color-parser" "^3.0.7" - "@csstools/css-parser-algorithms" "^3.0.4" - "@csstools/css-tokenizer" "^3.0.3" - "@csstools/postcss-progressive-custom-properties" "^4.0.0" - "@csstools/utilities" "^2.0.0" - -"@csstools/postcss-color-mix-function@^3.0.7": - version "3.0.7" - resolved "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-3.0.7.tgz" - integrity sha512-e68Nev4CxZYCLcrfWhHH4u/N1YocOfTmw67/kVX5Rb7rnguqqLyxPjhHWjSBX8o4bmyuukmNf3wrUSU3//kT7g== - dependencies: - "@csstools/css-color-parser" "^3.0.7" - "@csstools/css-parser-algorithms" "^3.0.4" - "@csstools/css-tokenizer" "^3.0.3" - "@csstools/postcss-progressive-custom-properties" "^4.0.0" - "@csstools/utilities" "^2.0.0" - -"@csstools/postcss-content-alt-text@^2.0.4": - version "2.0.4" - resolved "https://registry.npmjs.org/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-2.0.4.tgz" - integrity sha512-YItlZUOuZJCBlRaCf8Aucc1lgN41qYGALMly0qQllrxYJhiyzlI6RxOTMUvtWk+KhS8GphMDsDhKQ7KTPfEMSw== - dependencies: - "@csstools/css-parser-algorithms" "^3.0.4" - "@csstools/css-tokenizer" "^3.0.3" - "@csstools/postcss-progressive-custom-properties" "^4.0.0" - "@csstools/utilities" "^2.0.0" - -"@csstools/postcss-exponential-functions@^2.0.6": - version "2.0.6" - resolved "https://registry.npmjs.org/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-2.0.6.tgz" - integrity sha512-IgJA5DQsQLu/upA3HcdvC6xEMR051ufebBTIXZ5E9/9iiaA7juXWz1ceYj814lnDYP/7eWjZnw0grRJlX4eI6g== - dependencies: - "@csstools/css-calc" "^2.1.1" - "@csstools/css-parser-algorithms" "^3.0.4" - "@csstools/css-tokenizer" "^3.0.3" - -"@csstools/postcss-font-format-keywords@^4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-4.0.0.tgz" - integrity sha512-usBzw9aCRDvchpok6C+4TXC57btc4bJtmKQWOHQxOVKen1ZfVqBUuCZ/wuqdX5GHsD0NRSr9XTP+5ID1ZZQBXw== - dependencies: - "@csstools/utilities" "^2.0.0" - postcss-value-parser "^4.2.0" - -"@csstools/postcss-gamut-mapping@^2.0.7": - version "2.0.7" - resolved "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-2.0.7.tgz" - integrity sha512-gzFEZPoOkY0HqGdyeBXR3JP218Owr683u7KOZazTK7tQZBE8s2yhg06W1tshOqk7R7SWvw9gkw2TQogKpIW8Xw== - dependencies: - "@csstools/css-color-parser" "^3.0.7" - "@csstools/css-parser-algorithms" "^3.0.4" - "@csstools/css-tokenizer" "^3.0.3" - -"@csstools/postcss-gradients-interpolation-method@^5.0.7": - version "5.0.7" - resolved "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-5.0.7.tgz" - integrity sha512-WgEyBeg6glUeTdS2XT7qeTFBthTJuXlS9GFro/DVomj7W7WMTamAwpoP4oQCq/0Ki2gvfRYFi/uZtmRE14/DFA== - dependencies: - "@csstools/css-color-parser" "^3.0.7" - "@csstools/css-parser-algorithms" "^3.0.4" - "@csstools/css-tokenizer" "^3.0.3" - "@csstools/postcss-progressive-custom-properties" "^4.0.0" - "@csstools/utilities" "^2.0.0" - -"@csstools/postcss-hwb-function@^4.0.7": - version "4.0.7" - resolved "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-4.0.7.tgz" - integrity sha512-LKYqjO+wGwDCfNIEllessCBWfR4MS/sS1WXO+j00KKyOjm7jDW2L6jzUmqASEiv/kkJO39GcoIOvTTfB3yeBUA== - dependencies: - "@csstools/css-color-parser" "^3.0.7" - "@csstools/css-parser-algorithms" "^3.0.4" - "@csstools/css-tokenizer" "^3.0.3" - "@csstools/postcss-progressive-custom-properties" "^4.0.0" - "@csstools/utilities" "^2.0.0" - -"@csstools/postcss-ic-unit@^4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-4.0.0.tgz" - integrity sha512-9QT5TDGgx7wD3EEMN3BSUG6ckb6Eh5gSPT5kZoVtUuAonfPmLDJyPhqR4ntPpMYhUKAMVKAg3I/AgzqHMSeLhA== - dependencies: - "@csstools/postcss-progressive-custom-properties" "^4.0.0" - "@csstools/utilities" "^2.0.0" - postcss-value-parser "^4.2.0" - -"@csstools/postcss-initial@^2.0.0": - version "2.0.0" - resolved "https://registry.npmjs.org/@csstools/postcss-initial/-/postcss-initial-2.0.0.tgz" - integrity sha512-dv2lNUKR+JV+OOhZm9paWzYBXOCi+rJPqJ2cJuhh9xd8USVrd0cBEPczla81HNOyThMQWeCcdln3gZkQV2kYxA== - -"@csstools/postcss-is-pseudo-class@^5.0.1": - version "5.0.1" - resolved "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-5.0.1.tgz" - integrity sha512-JLp3POui4S1auhDR0n8wHd/zTOWmMsmK3nQd3hhL6FhWPaox5W7j1se6zXOG/aP07wV2ww0lxbKYGwbBszOtfQ== - dependencies: - "@csstools/selector-specificity" "^5.0.0" - postcss-selector-parser "^7.0.0" - -"@csstools/postcss-light-dark-function@^2.0.7": - version "2.0.7" - resolved "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-2.0.7.tgz" - integrity sha512-ZZ0rwlanYKOHekyIPaU+sVm3BEHCe+Ha0/px+bmHe62n0Uc1lL34vbwrLYn6ote8PHlsqzKeTQdIejQCJ05tfw== - dependencies: - "@csstools/css-parser-algorithms" "^3.0.4" - "@csstools/css-tokenizer" "^3.0.3" - "@csstools/postcss-progressive-custom-properties" "^4.0.0" - "@csstools/utilities" "^2.0.0" - -"@csstools/postcss-logical-float-and-clear@^3.0.0": - version "3.0.0" - resolved "https://registry.npmjs.org/@csstools/postcss-logical-float-and-clear/-/postcss-logical-float-and-clear-3.0.0.tgz" - integrity sha512-SEmaHMszwakI2rqKRJgE+8rpotFfne1ZS6bZqBoQIicFyV+xT1UF42eORPxJkVJVrH9C0ctUgwMSn3BLOIZldQ== - -"@csstools/postcss-logical-overflow@^2.0.0": - version "2.0.0" - resolved "https://registry.npmjs.org/@csstools/postcss-logical-overflow/-/postcss-logical-overflow-2.0.0.tgz" - integrity sha512-spzR1MInxPuXKEX2csMamshR4LRaSZ3UXVaRGjeQxl70ySxOhMpP2252RAFsg8QyyBXBzuVOOdx1+bVO5bPIzA== - -"@csstools/postcss-logical-overscroll-behavior@^2.0.0": - version "2.0.0" - resolved "https://registry.npmjs.org/@csstools/postcss-logical-overscroll-behavior/-/postcss-logical-overscroll-behavior-2.0.0.tgz" - integrity sha512-e/webMjoGOSYfqLunyzByZj5KKe5oyVg/YSbie99VEaSDE2kimFm0q1f6t/6Jo+VVCQ/jbe2Xy+uX+C4xzWs4w== - -"@csstools/postcss-logical-resize@^3.0.0": - version "3.0.0" - resolved "https://registry.npmjs.org/@csstools/postcss-logical-resize/-/postcss-logical-resize-3.0.0.tgz" - integrity sha512-DFbHQOFW/+I+MY4Ycd/QN6Dg4Hcbb50elIJCfnwkRTCX05G11SwViI5BbBlg9iHRl4ytB7pmY5ieAFk3ws7yyg== - dependencies: - postcss-value-parser "^4.2.0" - -"@csstools/postcss-logical-viewport-units@^3.0.3": - version "3.0.3" - resolved "https://registry.npmjs.org/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-3.0.3.tgz" - integrity sha512-OC1IlG/yoGJdi0Y+7duz/kU/beCwO+Gua01sD6GtOtLi7ByQUpcIqs7UE/xuRPay4cHgOMatWdnDdsIDjnWpPw== - dependencies: - "@csstools/css-tokenizer" "^3.0.3" - "@csstools/utilities" "^2.0.0" - -"@csstools/postcss-media-minmax@^2.0.6": - version "2.0.6" - resolved "https://registry.npmjs.org/@csstools/postcss-media-minmax/-/postcss-media-minmax-2.0.6.tgz" - integrity sha512-J1+4Fr2W3pLZsfxkFazK+9kr96LhEYqoeBszLmFjb6AjYs+g9oDAw3J5oQignLKk3rC9XHW+ebPTZ9FaW5u5pg== - dependencies: - "@csstools/css-calc" "^2.1.1" - "@csstools/css-parser-algorithms" "^3.0.4" - "@csstools/css-tokenizer" "^3.0.3" - "@csstools/media-query-list-parser" "^4.0.2" - -"@csstools/postcss-media-queries-aspect-ratio-number-values@^3.0.4": - version "3.0.4" - resolved "https://registry.npmjs.org/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-3.0.4.tgz" - integrity sha512-AnGjVslHMm5xw9keusQYvjVWvuS7KWK+OJagaG0+m9QnIjZsrysD2kJP/tr/UJIyYtMCtu8OkUd+Rajb4DqtIQ== - dependencies: - "@csstools/css-parser-algorithms" "^3.0.4" - "@csstools/css-tokenizer" "^3.0.3" - "@csstools/media-query-list-parser" "^4.0.2" - -"@csstools/postcss-nested-calc@^4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-4.0.0.tgz" - integrity sha512-jMYDdqrQQxE7k9+KjstC3NbsmC063n1FTPLCgCRS2/qHUbHM0mNy9pIn4QIiQGs9I/Bg98vMqw7mJXBxa0N88A== - dependencies: - "@csstools/utilities" "^2.0.0" - postcss-value-parser "^4.2.0" - -"@csstools/postcss-normalize-display-values@^4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.0.tgz" - integrity sha512-HlEoG0IDRoHXzXnkV4in47dzsxdsjdz6+j7MLjaACABX2NfvjFS6XVAnpaDyGesz9gK2SC7MbNwdCHusObKJ9Q== - dependencies: - postcss-value-parser "^4.2.0" - -"@csstools/postcss-oklab-function@^4.0.7": - version "4.0.7" - resolved "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-4.0.7.tgz" - integrity sha512-I6WFQIbEKG2IO3vhaMGZDkucbCaUSXMxvHNzDdnfsTCF5tc0UlV3Oe2AhamatQoKFjBi75dSEMrgWq3+RegsOQ== - dependencies: - "@csstools/css-color-parser" "^3.0.7" - "@csstools/css-parser-algorithms" "^3.0.4" - "@csstools/css-tokenizer" "^3.0.3" - "@csstools/postcss-progressive-custom-properties" "^4.0.0" - "@csstools/utilities" "^2.0.0" - -"@csstools/postcss-progressive-custom-properties@^4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-4.0.0.tgz" - integrity sha512-XQPtROaQjomnvLUSy/bALTR5VCtTVUFwYs1SblvYgLSeTo2a/bMNwUwo2piXw5rTv/FEYiy5yPSXBqg9OKUx7Q== - dependencies: - postcss-value-parser "^4.2.0" - -"@csstools/postcss-random-function@^1.0.2": - version "1.0.2" - resolved "https://registry.npmjs.org/@csstools/postcss-random-function/-/postcss-random-function-1.0.2.tgz" - integrity sha512-vBCT6JvgdEkvRc91NFoNrLjgGtkLWt47GKT6E2UDn3nd8ZkMBiziQ1Md1OiKoSsgzxsSnGKG3RVdhlbdZEkHjA== - dependencies: - "@csstools/css-calc" "^2.1.1" - "@csstools/css-parser-algorithms" "^3.0.4" - "@csstools/css-tokenizer" "^3.0.3" - -"@csstools/postcss-relative-color-syntax@^3.0.7": - version "3.0.7" - resolved "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-3.0.7.tgz" - integrity sha512-apbT31vsJVd18MabfPOnE977xgct5B1I+Jpf+Munw3n6kKb1MMuUmGGH+PT9Hm/fFs6fe61Q/EWnkrb4bNoNQw== - dependencies: - "@csstools/css-color-parser" "^3.0.7" - "@csstools/css-parser-algorithms" "^3.0.4" - "@csstools/css-tokenizer" "^3.0.3" - "@csstools/postcss-progressive-custom-properties" "^4.0.0" - "@csstools/utilities" "^2.0.0" - -"@csstools/postcss-scope-pseudo-class@^4.0.1": - version "4.0.1" - resolved "https://registry.npmjs.org/@csstools/postcss-scope-pseudo-class/-/postcss-scope-pseudo-class-4.0.1.tgz" - integrity sha512-IMi9FwtH6LMNuLea1bjVMQAsUhFxJnyLSgOp/cpv5hrzWmrUYU5fm0EguNDIIOHUqzXode8F/1qkC/tEo/qN8Q== - dependencies: - postcss-selector-parser "^7.0.0" - -"@csstools/postcss-sign-functions@^1.1.1": - version "1.1.1" - resolved "https://registry.npmjs.org/@csstools/postcss-sign-functions/-/postcss-sign-functions-1.1.1.tgz" - integrity sha512-MslYkZCeMQDxetNkfmmQYgKCy4c+w9pPDfgOBCJOo/RI1RveEUdZQYtOfrC6cIZB7sD7/PHr2VGOcMXlZawrnA== - dependencies: - "@csstools/css-calc" "^2.1.1" - "@csstools/css-parser-algorithms" "^3.0.4" - "@csstools/css-tokenizer" "^3.0.3" - -"@csstools/postcss-stepped-value-functions@^4.0.6": - version "4.0.6" - resolved "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-4.0.6.tgz" - integrity sha512-/dwlO9w8vfKgiADxpxUbZOWlL5zKoRIsCymYoh1IPuBsXODKanKnfuZRr32DEqT0//3Av1VjfNZU9yhxtEfIeA== - dependencies: - "@csstools/css-calc" "^2.1.1" - "@csstools/css-parser-algorithms" "^3.0.4" - "@csstools/css-tokenizer" "^3.0.3" - -"@csstools/postcss-text-decoration-shorthand@^4.0.1": - version "4.0.1" - resolved "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-4.0.1.tgz" - integrity sha512-xPZIikbx6jyzWvhms27uugIc0I4ykH4keRvoa3rxX5K7lEhkbd54rjj/dv60qOCTisoS+3bmwJTeyV1VNBrXaw== - dependencies: - "@csstools/color-helpers" "^5.0.1" - postcss-value-parser "^4.2.0" - -"@csstools/postcss-trigonometric-functions@^4.0.6": - version "4.0.6" - resolved "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-4.0.6.tgz" - integrity sha512-c4Y1D2Why/PeccaSouXnTt6WcNHJkoJRidV2VW9s5gJ97cNxnLgQ4Qj8qOqkIR9VmTQKJyNcbF4hy79ZQnWD7A== - dependencies: - "@csstools/css-calc" "^2.1.1" - "@csstools/css-parser-algorithms" "^3.0.4" - "@csstools/css-tokenizer" "^3.0.3" - -"@csstools/postcss-unset-value@^4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-4.0.0.tgz" - integrity sha512-cBz3tOCI5Fw6NIFEwU3RiwK6mn3nKegjpJuzCndoGq3BZPkUjnsq7uQmIeMNeMbMk7YD2MfKcgCpZwX5jyXqCA== - -"@csstools/selector-resolve-nested@^3.0.0": - version "3.0.0" - resolved "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.0.0.tgz" - integrity sha512-ZoK24Yku6VJU1gS79a5PFmC8yn3wIapiKmPgun0hZgEI5AOqgH2kiPRsPz1qkGv4HL+wuDLH83yQyk6inMYrJQ== - -"@csstools/selector-specificity@^5.0.0": - version "5.0.0" - resolved "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz" - integrity sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw== - -"@csstools/utilities@^2.0.0": - version "2.0.0" - resolved "https://registry.npmjs.org/@csstools/utilities/-/utilities-2.0.0.tgz" - integrity sha512-5VdOr0Z71u+Yp3ozOx8T11N703wIFGVRgOWbOZMKgglPJsWA54MRIoMNVMa7shUToIhx5J8vX4sOZgD2XiihiQ== - -"@discoveryjs/json-ext@0.5.7": - version "0.5.7" - resolved "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz" - integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== - -"@docsearch/css@3.8.3": - version "3.8.3" - resolved "https://registry.npmjs.org/@docsearch/css/-/css-3.8.3.tgz" - integrity sha512-1nELpMV40JDLJ6rpVVFX48R1jsBFIQ6RnEQDsLFGmzOjPWTOMlZqUcXcvRx8VmYV/TqnS1l784Ofz+ZEb+wEOQ== - -"@docsearch/react@^3.8.1": - version "3.8.3" - resolved "https://registry.npmjs.org/@docsearch/react/-/react-3.8.3.tgz" - integrity sha512-6UNrg88K7lJWmuS6zFPL/xgL+n326qXqZ7Ybyy4E8P/6Rcblk3GE8RXxeol4Pd5pFpKMhOhBhzABKKwHtbJCIg== - dependencies: - "@algolia/autocomplete-core" "1.17.9" - "@algolia/autocomplete-preset-algolia" "1.17.9" - "@docsearch/css" "3.8.3" - algoliasearch "^5.14.2" - -"@docusaurus/babel@3.7.0": - version "3.7.0" - resolved "https://registry.npmjs.org/@docusaurus/babel/-/babel-3.7.0.tgz" - integrity sha512-0H5uoJLm14S/oKV3Keihxvh8RV+vrid+6Gv+2qhuzbqHanawga8tYnsdpjEyt36ucJjqlby2/Md2ObWjA02UXQ== - dependencies: - "@babel/core" "^7.25.9" - "@babel/generator" "^7.25.9" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - "@babel/plugin-transform-runtime" "^7.25.9" - "@babel/preset-env" "^7.25.9" - "@babel/preset-react" "^7.25.9" - "@babel/preset-typescript" "^7.25.9" - "@babel/runtime" "^7.25.9" - "@babel/runtime-corejs3" "^7.25.9" - "@babel/traverse" "^7.25.9" - "@docusaurus/logger" "3.7.0" - "@docusaurus/utils" "3.7.0" - babel-plugin-dynamic-import-node "^2.3.3" - fs-extra "^11.1.1" - tslib "^2.6.0" - -"@docusaurus/bundler@3.7.0": - version "3.7.0" - resolved "https://registry.npmjs.org/@docusaurus/bundler/-/bundler-3.7.0.tgz" - integrity sha512-CUUT9VlSGukrCU5ctZucykvgCISivct+cby28wJwCC/fkQFgAHRp/GKv2tx38ZmXb7nacrKzFTcp++f9txUYGg== - dependencies: - "@babel/core" "^7.25.9" - "@docusaurus/babel" "3.7.0" - "@docusaurus/cssnano-preset" "3.7.0" - "@docusaurus/logger" "3.7.0" - "@docusaurus/types" "3.7.0" - "@docusaurus/utils" "3.7.0" - babel-loader "^9.2.1" - clean-css "^5.3.2" - copy-webpack-plugin "^11.0.0" - css-loader "^6.8.1" - css-minimizer-webpack-plugin "^5.0.1" - cssnano "^6.1.2" - file-loader "^6.2.0" - html-minifier-terser "^7.2.0" - mini-css-extract-plugin "^2.9.1" - null-loader "^4.0.1" - postcss "^8.4.26" - postcss-loader "^7.3.3" - postcss-preset-env "^10.1.0" - react-dev-utils "^12.0.1" - terser-webpack-plugin "^5.3.9" - tslib "^2.6.0" - url-loader "^4.1.1" - webpack "^5.95.0" - webpackbar "^6.0.1" - -"@docusaurus/core@3.7.0": - version "3.7.0" - resolved "https://registry.npmjs.org/@docusaurus/core/-/core-3.7.0.tgz" - integrity sha512-b0fUmaL+JbzDIQaamzpAFpTviiaU4cX3Qz8cuo14+HGBCwa0evEK0UYCBFY3n4cLzL8Op1BueeroUD2LYAIHbQ== - dependencies: - "@docusaurus/babel" "3.7.0" - "@docusaurus/bundler" "3.7.0" - "@docusaurus/logger" "3.7.0" - "@docusaurus/mdx-loader" "3.7.0" - "@docusaurus/utils" "3.7.0" - "@docusaurus/utils-common" "3.7.0" - "@docusaurus/utils-validation" "3.7.0" - boxen "^6.2.1" - chalk "^4.1.2" - chokidar "^3.5.3" - cli-table3 "^0.6.3" - combine-promises "^1.1.0" - commander "^5.1.0" - core-js "^3.31.1" - del "^6.1.1" - detect-port "^1.5.1" - escape-html "^1.0.3" - eta "^2.2.0" - eval "^0.1.8" - fs-extra "^11.1.1" - html-tags "^3.3.1" - html-webpack-plugin "^5.6.0" - leven "^3.1.0" - lodash "^4.17.21" - p-map "^4.0.0" - prompts "^2.4.2" - react-dev-utils "^12.0.1" - react-helmet-async "npm:@slorber/react-helmet-async@1.3.0" - react-loadable "npm:@docusaurus/react-loadable@6.0.0" - react-loadable-ssr-addon-v5-slorber "^1.0.1" - react-router "^5.3.4" - react-router-config "^5.1.1" - react-router-dom "^5.3.4" - semver "^7.5.4" - serve-handler "^6.1.6" - shelljs "^0.8.5" - tslib "^2.6.0" - update-notifier "^6.0.2" - webpack "^5.95.0" - webpack-bundle-analyzer "^4.10.2" - webpack-dev-server "^4.15.2" - webpack-merge "^6.0.1" - -"@docusaurus/cssnano-preset@3.7.0": - version "3.7.0" - resolved "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.7.0.tgz" - integrity sha512-X9GYgruZBSOozg4w4dzv9uOz8oK/EpPVQXkp0MM6Tsgp/nRIU9hJzJ0Pxg1aRa3xCeEQTOimZHcocQFlLwYajQ== - dependencies: - cssnano-preset-advanced "^6.1.2" - postcss "^8.4.38" - postcss-sort-media-queries "^5.2.0" - tslib "^2.6.0" - -"@docusaurus/logger@3.7.0": - version "3.7.0" - resolved "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.7.0.tgz" - integrity sha512-z7g62X7bYxCYmeNNuO9jmzxLQG95q9QxINCwpboVcNff3SJiHJbGrarxxOVMVmAh1MsrSfxWkVGv4P41ktnFsA== - dependencies: - chalk "^4.1.2" - tslib "^2.6.0" - -"@docusaurus/mdx-loader@3.7.0": - version "3.7.0" - resolved "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.7.0.tgz" - integrity sha512-OFBG6oMjZzc78/U3WNPSHs2W9ZJ723ewAcvVJaqS0VgyeUfmzUV8f1sv+iUHA0DtwiR5T5FjOxj6nzEE8LY6VA== - dependencies: - "@docusaurus/logger" "3.7.0" - "@docusaurus/utils" "3.7.0" - "@docusaurus/utils-validation" "3.7.0" - "@mdx-js/mdx" "^3.0.0" - "@slorber/remark-comment" "^1.0.0" - escape-html "^1.0.3" - estree-util-value-to-estree "^3.0.1" - file-loader "^6.2.0" - fs-extra "^11.1.1" - image-size "^1.0.2" - mdast-util-mdx "^3.0.0" - mdast-util-to-string "^4.0.0" - rehype-raw "^7.0.0" - remark-directive "^3.0.0" - remark-emoji "^4.0.0" - remark-frontmatter "^5.0.0" - remark-gfm "^4.0.0" - stringify-object "^3.3.0" - tslib "^2.6.0" - unified "^11.0.3" - unist-util-visit "^5.0.0" - url-loader "^4.1.1" - vfile "^6.0.1" - webpack "^5.88.1" - -"@docusaurus/module-type-aliases@3.7.0": - version "3.7.0" - resolved "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.7.0.tgz" - integrity sha512-g7WdPqDNaqA60CmBrr0cORTrsOit77hbsTj7xE2l71YhBn79sxdm7WMK7wfhcaafkbpIh7jv5ef5TOpf1Xv9Lg== - dependencies: - "@docusaurus/types" "3.7.0" - "@types/history" "^4.7.11" - "@types/react" "*" - "@types/react-router-config" "*" - "@types/react-router-dom" "*" - react-helmet-async "npm:@slorber/react-helmet-async@*" - react-loadable "npm:@docusaurus/react-loadable@6.0.0" - -"@docusaurus/plugin-client-redirects@^3.7.0": - version "3.7.0" - resolved "https://registry.npmjs.org/@docusaurus/plugin-client-redirects/-/plugin-client-redirects-3.7.0.tgz" - integrity sha512-6B4XAtE5ZVKOyhPgpgMkb7LwCkN+Hgd4vOnlbwR8nCdTQhLjz8MHbGlwwvZ/cay2SPNRX5KssqKAlcHVZP2m8g== - dependencies: - "@docusaurus/core" "3.7.0" - "@docusaurus/logger" "3.7.0" - "@docusaurus/utils" "3.7.0" - "@docusaurus/utils-common" "3.7.0" - "@docusaurus/utils-validation" "3.7.0" - eta "^2.2.0" - fs-extra "^11.1.1" - lodash "^4.17.21" - tslib "^2.6.0" - -"@docusaurus/plugin-content-blog@3.7.0": - version "3.7.0" - resolved "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.7.0.tgz" - integrity sha512-EFLgEz6tGHYWdPU0rK8tSscZwx+AsyuBW/r+tNig2kbccHYGUJmZtYN38GjAa3Fda4NU+6wqUO5kTXQSRBQD3g== - dependencies: - "@docusaurus/core" "3.7.0" - "@docusaurus/logger" "3.7.0" - "@docusaurus/mdx-loader" "3.7.0" - "@docusaurus/theme-common" "3.7.0" - "@docusaurus/types" "3.7.0" - "@docusaurus/utils" "3.7.0" - "@docusaurus/utils-common" "3.7.0" - "@docusaurus/utils-validation" "3.7.0" - cheerio "1.0.0-rc.12" - feed "^4.2.2" - fs-extra "^11.1.1" - lodash "^4.17.21" - reading-time "^1.5.0" - srcset "^4.0.0" - tslib "^2.6.0" - unist-util-visit "^5.0.0" - utility-types "^3.10.0" - webpack "^5.88.1" - -"@docusaurus/plugin-content-docs@3.7.0": - version "3.7.0" - resolved "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.7.0.tgz" - integrity sha512-GXg5V7kC9FZE4FkUZA8oo/NrlRb06UwuICzI6tcbzj0+TVgjq/mpUXXzSgKzMS82YByi4dY2Q808njcBCyy6tQ== - dependencies: - "@docusaurus/core" "3.7.0" - "@docusaurus/logger" "3.7.0" - "@docusaurus/mdx-loader" "3.7.0" - "@docusaurus/module-type-aliases" "3.7.0" - "@docusaurus/theme-common" "3.7.0" - "@docusaurus/types" "3.7.0" - "@docusaurus/utils" "3.7.0" - "@docusaurus/utils-common" "3.7.0" - "@docusaurus/utils-validation" "3.7.0" - "@types/react-router-config" "^5.0.7" - combine-promises "^1.1.0" - fs-extra "^11.1.1" - js-yaml "^4.1.0" - lodash "^4.17.21" - tslib "^2.6.0" - utility-types "^3.10.0" - webpack "^5.88.1" - -"@docusaurus/plugin-content-pages@3.7.0": - version "3.7.0" - resolved "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.7.0.tgz" - integrity sha512-YJSU3tjIJf032/Aeao8SZjFOrXJbz/FACMveSMjLyMH4itQyZ2XgUIzt4y+1ISvvk5zrW4DABVT2awTCqBkx0Q== - dependencies: - "@docusaurus/core" "3.7.0" - "@docusaurus/mdx-loader" "3.7.0" - "@docusaurus/types" "3.7.0" - "@docusaurus/utils" "3.7.0" - "@docusaurus/utils-validation" "3.7.0" - fs-extra "^11.1.1" - tslib "^2.6.0" - webpack "^5.88.1" - -"@docusaurus/plugin-debug@3.7.0": - version "3.7.0" - resolved "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.7.0.tgz" - integrity sha512-Qgg+IjG/z4svtbCNyTocjIwvNTNEwgRjSXXSJkKVG0oWoH0eX/HAPiu+TS1HBwRPQV+tTYPWLrUypYFepfujZA== - dependencies: - "@docusaurus/core" "3.7.0" - "@docusaurus/types" "3.7.0" - "@docusaurus/utils" "3.7.0" - fs-extra "^11.1.1" - react-json-view-lite "^1.2.0" - tslib "^2.6.0" - -"@docusaurus/plugin-google-analytics@3.7.0": - version "3.7.0" - resolved "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.7.0.tgz" - integrity sha512-otIqiRV/jka6Snjf+AqB360XCeSv7lQC+DKYW+EUZf6XbuE8utz5PeUQ8VuOcD8Bk5zvT1MC4JKcd5zPfDuMWA== - dependencies: - "@docusaurus/core" "3.7.0" - "@docusaurus/types" "3.7.0" - "@docusaurus/utils-validation" "3.7.0" - tslib "^2.6.0" - -"@docusaurus/plugin-google-gtag@3.7.0": - version "3.7.0" - resolved "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.7.0.tgz" - integrity sha512-M3vrMct1tY65ModbyeDaMoA+fNJTSPe5qmchhAbtqhDD/iALri0g9LrEpIOwNaoLmm6lO88sfBUADQrSRSGSWA== - dependencies: - "@docusaurus/core" "3.7.0" - "@docusaurus/types" "3.7.0" - "@docusaurus/utils-validation" "3.7.0" - "@types/gtag.js" "^0.0.12" - tslib "^2.6.0" - -"@docusaurus/plugin-google-tag-manager@3.7.0": - version "3.7.0" - resolved "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.7.0.tgz" - integrity sha512-X8U78nb8eiMiPNg3jb9zDIVuuo/rE1LjGDGu+5m5CX4UBZzjMy+klOY2fNya6x8ACyE/L3K2erO1ErheP55W/w== - dependencies: - "@docusaurus/core" "3.7.0" - "@docusaurus/types" "3.7.0" - "@docusaurus/utils-validation" "3.7.0" - tslib "^2.6.0" - -"@docusaurus/plugin-sitemap@3.7.0": - version "3.7.0" - resolved "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.7.0.tgz" - integrity sha512-bTRT9YLZ/8I/wYWKMQke18+PF9MV8Qub34Sku6aw/vlZ/U+kuEuRpQ8bTcNOjaTSfYsWkK4tTwDMHK2p5S86cA== - dependencies: - "@docusaurus/core" "3.7.0" - "@docusaurus/logger" "3.7.0" - "@docusaurus/types" "3.7.0" - "@docusaurus/utils" "3.7.0" - "@docusaurus/utils-common" "3.7.0" - "@docusaurus/utils-validation" "3.7.0" - fs-extra "^11.1.1" - sitemap "^7.1.1" - tslib "^2.6.0" - -"@docusaurus/plugin-svgr@3.7.0": - version "3.7.0" - resolved "https://registry.npmjs.org/@docusaurus/plugin-svgr/-/plugin-svgr-3.7.0.tgz" - integrity sha512-HByXIZTbc4GV5VAUkZ2DXtXv1Qdlnpk3IpuImwSnEzCDBkUMYcec5282hPjn6skZqB25M1TYCmWS91UbhBGxQg== - dependencies: - "@docusaurus/core" "3.7.0" - "@docusaurus/types" "3.7.0" - "@docusaurus/utils" "3.7.0" - "@docusaurus/utils-validation" "3.7.0" - "@svgr/core" "8.1.0" - "@svgr/webpack" "^8.1.0" - tslib "^2.6.0" - webpack "^5.88.1" - -"@docusaurus/preset-classic@3.7.0": - version "3.7.0" - resolved "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.7.0.tgz" - integrity sha512-nPHj8AxDLAaQXs+O6+BwILFuhiWbjfQWrdw2tifOClQoNfuXDjfjogee6zfx6NGHWqshR23LrcN115DmkHC91Q== - dependencies: - "@docusaurus/core" "3.7.0" - "@docusaurus/plugin-content-blog" "3.7.0" - "@docusaurus/plugin-content-docs" "3.7.0" - "@docusaurus/plugin-content-pages" "3.7.0" - "@docusaurus/plugin-debug" "3.7.0" - "@docusaurus/plugin-google-analytics" "3.7.0" - "@docusaurus/plugin-google-gtag" "3.7.0" - "@docusaurus/plugin-google-tag-manager" "3.7.0" - "@docusaurus/plugin-sitemap" "3.7.0" - "@docusaurus/plugin-svgr" "3.7.0" - "@docusaurus/theme-classic" "3.7.0" - "@docusaurus/theme-common" "3.7.0" - "@docusaurus/theme-search-algolia" "3.7.0" - "@docusaurus/types" "3.7.0" - -"@docusaurus/theme-classic@3.7.0": - version "3.7.0" - resolved "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.7.0.tgz" - integrity sha512-MnLxG39WcvLCl4eUzHr0gNcpHQfWoGqzADCly54aqCofQX6UozOS9Th4RK3ARbM9m7zIRv3qbhggI53dQtx/hQ== - dependencies: - "@docusaurus/core" "3.7.0" - "@docusaurus/logger" "3.7.0" - "@docusaurus/mdx-loader" "3.7.0" - "@docusaurus/module-type-aliases" "3.7.0" - "@docusaurus/plugin-content-blog" "3.7.0" - "@docusaurus/plugin-content-docs" "3.7.0" - "@docusaurus/plugin-content-pages" "3.7.0" - "@docusaurus/theme-common" "3.7.0" - "@docusaurus/theme-translations" "3.7.0" - "@docusaurus/types" "3.7.0" - "@docusaurus/utils" "3.7.0" - "@docusaurus/utils-common" "3.7.0" - "@docusaurus/utils-validation" "3.7.0" - "@mdx-js/react" "^3.0.0" - clsx "^2.0.0" - copy-text-to-clipboard "^3.2.0" - infima "0.2.0-alpha.45" - lodash "^4.17.21" - nprogress "^0.2.0" - postcss "^8.4.26" - prism-react-renderer "^2.3.0" - prismjs "^1.29.0" - react-router-dom "^5.3.4" - rtlcss "^4.1.0" - tslib "^2.6.0" - utility-types "^3.10.0" - -"@docusaurus/theme-common@3.7.0": - version "3.7.0" - resolved "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.7.0.tgz" - integrity sha512-8eJ5X0y+gWDsURZnBfH0WabdNm8XMCXHv8ENy/3Z/oQKwaB/EHt5lP9VsTDTf36lKEp0V6DjzjFyFIB+CetL0A== - dependencies: - "@docusaurus/mdx-loader" "3.7.0" - "@docusaurus/module-type-aliases" "3.7.0" - "@docusaurus/utils" "3.7.0" - "@docusaurus/utils-common" "3.7.0" - "@types/history" "^4.7.11" - "@types/react" "*" - "@types/react-router-config" "*" - clsx "^2.0.0" - parse-numeric-range "^1.3.0" - prism-react-renderer "^2.3.0" - tslib "^2.6.0" - utility-types "^3.10.0" - -"@docusaurus/theme-search-algolia@3.7.0": - version "3.7.0" - resolved "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.7.0.tgz" - integrity sha512-Al/j5OdzwRU1m3falm+sYy9AaB93S1XF1Lgk9Yc6amp80dNxJVplQdQTR4cYdzkGtuQqbzUA8+kaoYYO0RbK6g== - dependencies: - "@docsearch/react" "^3.8.1" - "@docusaurus/core" "3.7.0" - "@docusaurus/logger" "3.7.0" - "@docusaurus/plugin-content-docs" "3.7.0" - "@docusaurus/theme-common" "3.7.0" - "@docusaurus/theme-translations" "3.7.0" - "@docusaurus/utils" "3.7.0" - "@docusaurus/utils-validation" "3.7.0" - algoliasearch "^5.17.1" - algoliasearch-helper "^3.22.6" - clsx "^2.0.0" - eta "^2.2.0" - fs-extra "^11.1.1" - lodash "^4.17.21" - tslib "^2.6.0" - utility-types "^3.10.0" - -"@docusaurus/theme-translations@3.7.0": - version "3.7.0" - resolved "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.7.0.tgz" - integrity sha512-Ewq3bEraWDmienM6eaNK7fx+/lHMtGDHQyd1O+4+3EsDxxUmrzPkV7Ct3nBWTuE0MsoZr3yNwQVKjllzCMuU3g== - dependencies: - fs-extra "^11.1.1" - tslib "^2.6.0" - -"@docusaurus/tsconfig@3.7.0": - version "3.7.0" - resolved "https://registry.npmjs.org/@docusaurus/tsconfig/-/tsconfig-3.7.0.tgz" - integrity sha512-vRsyj3yUZCjscgfgcFYjIsTcAru/4h4YH2/XAE8Rs7wWdnng98PgWKvP5ovVc4rmRpRg2WChVW0uOy2xHDvDBQ== - -"@docusaurus/types@3.7.0": - version "3.7.0" - resolved "https://registry.npmjs.org/@docusaurus/types/-/types-3.7.0.tgz" - integrity sha512-kOmZg5RRqJfH31m+6ZpnwVbkqMJrPOG5t0IOl4i/+3ruXyNfWzZ0lVtVrD0u4ONc/0NOsS9sWYaxxWNkH1LdLQ== - dependencies: - "@mdx-js/mdx" "^3.0.0" - "@types/history" "^4.7.11" - "@types/react" "*" - commander "^5.1.0" - joi "^17.9.2" - react-helmet-async "npm:@slorber/react-helmet-async@1.3.0" - utility-types "^3.10.0" - webpack "^5.95.0" - webpack-merge "^5.9.0" - -"@docusaurus/utils-common@3.7.0": - version "3.7.0" - resolved "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.7.0.tgz" - integrity sha512-IZeyIfCfXy0Mevj6bWNg7DG7B8G+S6o6JVpddikZtWyxJguiQ7JYr0SIZ0qWd8pGNuMyVwriWmbWqMnK7Y5PwA== - dependencies: - "@docusaurus/types" "3.7.0" - tslib "^2.6.0" - -"@docusaurus/utils-validation@3.7.0": - version "3.7.0" - resolved "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.7.0.tgz" - integrity sha512-w8eiKk8mRdN+bNfeZqC4nyFoxNyI1/VExMKAzD9tqpJfLLbsa46Wfn5wcKH761g9WkKh36RtFV49iL9lh1DYBA== - dependencies: - "@docusaurus/logger" "3.7.0" - "@docusaurus/utils" "3.7.0" - "@docusaurus/utils-common" "3.7.0" - fs-extra "^11.2.0" - joi "^17.9.2" - js-yaml "^4.1.0" - lodash "^4.17.21" - tslib "^2.6.0" - -"@docusaurus/utils@3.7.0": - version "3.7.0" - resolved "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.7.0.tgz" - integrity sha512-e7zcB6TPnVzyUaHMJyLSArKa2AG3h9+4CfvKXKKWNx6hRs+p0a+u7HHTJBgo6KW2m+vqDnuIHK4X+bhmoghAFA== - dependencies: - "@docusaurus/logger" "3.7.0" - "@docusaurus/types" "3.7.0" - "@docusaurus/utils-common" "3.7.0" - escape-string-regexp "^4.0.0" - file-loader "^6.2.0" - fs-extra "^11.1.1" - github-slugger "^1.5.0" - globby "^11.1.0" - gray-matter "^4.0.3" - jiti "^1.20.0" - js-yaml "^4.1.0" - lodash "^4.17.21" - micromatch "^4.0.5" - prompts "^2.4.2" - resolve-pathname "^3.0.0" - shelljs "^0.8.5" - tslib "^2.6.0" - url-loader "^4.1.1" - utility-types "^3.10.0" - webpack "^5.88.1" - -"@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0": - version "9.3.0" - resolved "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz" - integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== - -"@hapi/topo@^5.1.0": - version "5.1.0" - resolved "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz" - integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== - dependencies: - "@hapi/hoek" "^9.0.0" - -"@inkeep/docusaurus@^2.0.16": - version "2.0.16" - resolved "https://registry.npmjs.org/@inkeep/docusaurus/-/docusaurus-2.0.16.tgz" - integrity sha512-dQhjlvFnl3CVr0gWeJ/V/qLnDy1XYrCfkdVSa2D3gJTxI9/vOf9639Y1aPxTxO88DiXuW9CertLrZLB6SoJ2yg== - -"@isaacs/cliui@^8.0.2": - version "8.0.2" - resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz" - integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== - dependencies: - string-width "^5.1.2" - string-width-cjs "npm:string-width@^4.2.0" - strip-ansi "^7.0.1" - strip-ansi-cjs "npm:strip-ansi@^6.0.1" - wrap-ansi "^8.1.0" - wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" - -"@jest/schemas@^29.6.3": - version "29.6.3" - resolved "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz" - integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== - dependencies: - "@sinclair/typebox" "^0.27.8" - -"@jest/types@^29.6.3": - version "29.6.3" - resolved "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz" - integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== - dependencies: - "@jest/schemas" "^29.6.3" - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^3.0.0" - "@types/node" "*" - "@types/yargs" "^17.0.8" - chalk "^4.0.0" - -"@jridgewell/gen-mapping@^0.3.2", "@jridgewell/gen-mapping@^0.3.5": - version "0.3.8" - resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz" - integrity sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA== - dependencies: - "@jridgewell/set-array" "^1.2.1" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.24" - -"@jridgewell/resolve-uri@^3.1.0": - version "3.1.2" - resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz" - integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== - -"@jridgewell/set-array@^1.2.1": - version "1.2.1" - resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz" - integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== - -"@jridgewell/source-map@^0.3.3": - version "0.3.6" - resolved "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz" - integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== - dependencies: - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.25" - -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": - version "1.5.0" - resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz" - integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== - -"@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": - version "0.3.25" - resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz" - integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== - dependencies: - "@jridgewell/resolve-uri" "^3.1.0" - "@jridgewell/sourcemap-codec" "^1.4.14" - -"@leichtgewicht/ip-codec@^2.0.1": - version "2.0.5" - resolved "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz" - integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw== - -"@mdx-js/mdx@^3.0.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.0.tgz" - integrity sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw== - dependencies: - "@types/estree" "^1.0.0" - "@types/estree-jsx" "^1.0.0" - "@types/hast" "^3.0.0" - "@types/mdx" "^2.0.0" - collapse-white-space "^2.0.0" - devlop "^1.0.0" - estree-util-is-identifier-name "^3.0.0" - estree-util-scope "^1.0.0" - estree-walker "^3.0.0" - hast-util-to-jsx-runtime "^2.0.0" - markdown-extensions "^2.0.0" - recma-build-jsx "^1.0.0" - recma-jsx "^1.0.0" - recma-stringify "^1.0.0" - rehype-recma "^1.0.0" - remark-mdx "^3.0.0" - remark-parse "^11.0.0" - remark-rehype "^11.0.0" - source-map "^0.7.0" - unified "^11.0.0" - unist-util-position-from-estree "^2.0.0" - unist-util-stringify-position "^4.0.0" - unist-util-visit "^5.0.0" - vfile "^6.0.0" - -"@mdx-js/react@^3.0.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.0.tgz" - integrity sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ== - dependencies: - "@types/mdx" "^2.0.0" - -"@mixmark-io/domino@^2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@mixmark-io/domino/-/domino-2.2.0.tgz#4e8ec69bf1afeb7a14f0628b7e2c0f35bdb336c3" - integrity sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw== - -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== - dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": - version "2.0.5" - resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.3": - version "1.2.8" - resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== - dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" - -"@pkgjs/parseargs@^0.11.0": - version "0.11.0" - resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" - integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== - -"@pnpm/config.env-replace@^1.1.0": - version "1.1.0" - resolved "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz" - integrity sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w== - -"@pnpm/network.ca-file@^1.0.1": - version "1.0.2" - resolved "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz" - integrity sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA== - dependencies: - graceful-fs "4.2.10" - -"@pnpm/npm-conf@^2.1.0": - version "2.3.1" - resolved "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.3.1.tgz" - integrity sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw== - dependencies: - "@pnpm/config.env-replace" "^1.1.0" - "@pnpm/network.ca-file" "^1.0.1" - config-chain "^1.1.11" - -"@polka/url@^1.0.0-next.24": - version "1.0.0-next.28" - resolved "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz" - integrity sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw== - -"@sideway/address@^4.1.5": - version "4.1.5" - resolved "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz" - integrity sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q== - dependencies: - "@hapi/hoek" "^9.0.0" - -"@sideway/formula@^3.0.1": - version "3.0.1" - resolved "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz" - integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== - -"@sideway/pinpoint@^2.0.0": - version "2.0.0" - resolved "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz" - integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== - -"@sinclair/typebox@^0.27.8": - version "0.27.8" - resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz" - integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== - -"@sindresorhus/is@^4.6.0": - version "4.6.0" - resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz" - integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw== - -"@sindresorhus/is@^5.2.0": - version "5.6.0" - resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz" - integrity sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g== - -"@slorber/remark-comment@^1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@slorber/remark-comment/-/remark-comment-1.0.0.tgz" - integrity sha512-RCE24n7jsOj1M0UPvIQCHTe7fI0sFL4S2nwKVWwHyVr/wI/H8GosgsJGyhnsZoGFnD/P2hLf1mSbrrgSLN93NA== - dependencies: - micromark-factory-space "^1.0.0" - micromark-util-character "^1.1.0" - micromark-util-symbol "^1.0.1" - -"@svgr/babel-plugin-add-jsx-attribute@8.0.0": - version "8.0.0" - resolved "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz" - integrity sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g== - -"@svgr/babel-plugin-remove-jsx-attribute@8.0.0": - version "8.0.0" - resolved "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz" - integrity sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA== - -"@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0": - version "8.0.0" - resolved "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz" - integrity sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA== - -"@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0": - version "8.0.0" - resolved "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz" - integrity sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ== - -"@svgr/babel-plugin-svg-dynamic-title@8.0.0": - version "8.0.0" - resolved "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz" - integrity sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og== - -"@svgr/babel-plugin-svg-em-dimensions@8.0.0": - version "8.0.0" - resolved "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz" - integrity sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g== - -"@svgr/babel-plugin-transform-react-native-svg@8.1.0": - version "8.1.0" - resolved "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz" - integrity sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q== - -"@svgr/babel-plugin-transform-svg-component@8.0.0": - version "8.0.0" - resolved "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz" - integrity sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw== - -"@svgr/babel-preset@8.1.0": - version "8.1.0" - resolved "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz" - integrity sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug== - dependencies: - "@svgr/babel-plugin-add-jsx-attribute" "8.0.0" - "@svgr/babel-plugin-remove-jsx-attribute" "8.0.0" - "@svgr/babel-plugin-remove-jsx-empty-expression" "8.0.0" - "@svgr/babel-plugin-replace-jsx-attribute-value" "8.0.0" - "@svgr/babel-plugin-svg-dynamic-title" "8.0.0" - "@svgr/babel-plugin-svg-em-dimensions" "8.0.0" - "@svgr/babel-plugin-transform-react-native-svg" "8.1.0" - "@svgr/babel-plugin-transform-svg-component" "8.0.0" - -"@svgr/core@8.1.0": - version "8.1.0" - resolved "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz" - integrity sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA== - dependencies: - "@babel/core" "^7.21.3" - "@svgr/babel-preset" "8.1.0" - camelcase "^6.2.0" - cosmiconfig "^8.1.3" - snake-case "^3.0.4" - -"@svgr/hast-util-to-babel-ast@8.0.0": - version "8.0.0" - resolved "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz" - integrity sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q== - dependencies: - "@babel/types" "^7.21.3" - entities "^4.4.0" - -"@svgr/plugin-jsx@8.1.0": - version "8.1.0" - resolved "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz" - integrity sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA== - dependencies: - "@babel/core" "^7.21.3" - "@svgr/babel-preset" "8.1.0" - "@svgr/hast-util-to-babel-ast" "8.0.0" - svg-parser "^2.0.4" - -"@svgr/plugin-svgo@8.1.0": - version "8.1.0" - resolved "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz" - integrity sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA== - dependencies: - cosmiconfig "^8.1.3" - deepmerge "^4.3.1" - svgo "^3.0.2" - -"@svgr/webpack@^8.1.0": - version "8.1.0" - resolved "https://registry.npmjs.org/@svgr/webpack/-/webpack-8.1.0.tgz" - integrity sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA== - dependencies: - "@babel/core" "^7.21.3" - "@babel/plugin-transform-react-constant-elements" "^7.21.3" - "@babel/preset-env" "^7.20.2" - "@babel/preset-react" "^7.18.6" - "@babel/preset-typescript" "^7.21.0" - "@svgr/core" "8.1.0" - "@svgr/plugin-jsx" "8.1.0" - "@svgr/plugin-svgo" "8.1.0" - -"@szmarczak/http-timer@^5.0.1": - version "5.0.1" - resolved "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz" - integrity sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw== - dependencies: - defer-to-connect "^2.0.1" - -"@trysound/sax@0.2.0": - version "0.2.0" - resolved "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz" - integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== - -"@types/acorn@^4.0.0": - version "4.0.6" - resolved "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz" - integrity sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ== - dependencies: - "@types/estree" "*" - -"@types/body-parser@*": - version "1.19.5" - resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz" - integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== - dependencies: - "@types/connect" "*" - "@types/node" "*" - -"@types/bonjour@^3.5.9": - version "3.5.13" - resolved "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz" - integrity sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ== - dependencies: - "@types/node" "*" - -"@types/connect-history-api-fallback@^1.3.5": - version "1.5.4" - resolved "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz" - integrity sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw== - dependencies: - "@types/express-serve-static-core" "*" - "@types/node" "*" - -"@types/connect@*": - version "3.4.38" - resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz" - integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== - dependencies: - "@types/node" "*" - -"@types/debug@^4.0.0": - version "4.1.12" - resolved "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz" - integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== - dependencies: - "@types/ms" "*" - -"@types/eslint-scope@^3.7.7": - version "3.7.7" - resolved "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz" - integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== - dependencies: - "@types/eslint" "*" - "@types/estree" "*" - -"@types/eslint@*": - version "9.6.1" - resolved "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz" - integrity sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag== - dependencies: - "@types/estree" "*" - "@types/json-schema" "*" - -"@types/estree-jsx@^1.0.0": - version "1.0.5" - resolved "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz" - integrity sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg== - dependencies: - "@types/estree" "*" - -"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6": - version "1.0.6" - resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz" - integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== - -"@types/express-serve-static-core@*": - version "5.0.5" - resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.5.tgz" - integrity sha512-GLZPrd9ckqEBFMcVM/qRFAP0Hg3qiVEojgEFsx/N/zKXsBzbGF6z5FBDpZ0+Xhp1xr+qRZYjfGr1cWHB9oFHSA== - dependencies: - "@types/node" "*" - "@types/qs" "*" - "@types/range-parser" "*" - "@types/send" "*" - -"@types/express-serve-static-core@^4.17.33": - version "4.19.6" - resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz" - integrity sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A== - dependencies: - "@types/node" "*" - "@types/qs" "*" - "@types/range-parser" "*" - "@types/send" "*" - -"@types/express@*", "@types/express@^4.17.13": - version "4.17.21" - resolved "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz" - integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== - dependencies: - "@types/body-parser" "*" - "@types/express-serve-static-core" "^4.17.33" - "@types/qs" "*" - "@types/serve-static" "*" - -"@types/gtag.js@^0.0.12": - version "0.0.12" - resolved "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.12.tgz" - integrity sha512-YQV9bUsemkzG81Ea295/nF/5GijnD2Af7QhEofh7xu+kvCN6RdodgNwwGWXB5GMI3NoyvQo0odNctoH/qLMIpg== - -"@types/hast@^3.0.0": - version "3.0.4" - resolved "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz" - integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ== - dependencies: - "@types/unist" "*" - -"@types/history@^4.7.11": - version "4.7.11" - resolved "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz" - integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== - -"@types/html-minifier-terser@^6.0.0": - version "6.1.0" - resolved "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz" - integrity sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg== - -"@types/http-cache-semantics@^4.0.2": - version "4.0.4" - resolved "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz" - integrity sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA== - -"@types/http-errors@*": - version "2.0.4" - resolved "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz" - integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== - -"@types/http-proxy@^1.17.8": - version "1.17.15" - resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz" - integrity sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ== - dependencies: - "@types/node" "*" - -"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": - version "2.0.6" - resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz" - integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== - -"@types/istanbul-lib-report@*": - version "3.0.3" - resolved "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz" - integrity sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA== - dependencies: - "@types/istanbul-lib-coverage" "*" - -"@types/istanbul-reports@^3.0.0": - version "3.0.4" - resolved "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz" - integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== - dependencies: - "@types/istanbul-lib-report" "*" - -"@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": - version "7.0.15" - resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" - integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== - -"@types/mdast@^4.0.0", "@types/mdast@^4.0.2": - version "4.0.4" - resolved "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz" - integrity sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA== - dependencies: - "@types/unist" "*" - -"@types/mdx@^2.0.0": - version "2.0.13" - resolved "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz" - integrity sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw== - -"@types/mime@^1": - version "1.3.5" - resolved "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz" - integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== - -"@types/ms@*": - version "2.1.0" - resolved "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz" - integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== - -"@types/node-forge@^1.3.0": - version "1.3.11" - resolved "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz" - integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ== - dependencies: - "@types/node" "*" - -"@types/node@*": - version "22.10.10" - resolved "https://registry.npmjs.org/@types/node/-/node-22.10.10.tgz" - integrity sha512-X47y/mPNzxviAGY5TcYPtYL8JsY3kAq2n8fMmKoRCxq/c4v4pyGNCzM2R6+M5/umG4ZfHuT+sgqDYqWc9rJ6ww== - dependencies: - undici-types "~6.20.0" - -"@types/node@^17.0.5": - version "17.0.45" - resolved "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz" - integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== - -"@types/parse-json@^4.0.0": - version "4.0.2" - resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz" - integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== - -"@types/prismjs@^1.26.0": - version "1.26.5" - resolved "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz" - integrity sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ== - -"@types/prop-types@*": - version "15.7.14" - resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz" - integrity sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ== - -"@types/qs@*": - version "6.9.18" - resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz" - integrity sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA== - -"@types/range-parser@*": - version "1.2.7" - resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz" - integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== - -"@types/react-router-config@*", "@types/react-router-config@^5.0.7": - version "5.0.11" - resolved "https://registry.npmjs.org/@types/react-router-config/-/react-router-config-5.0.11.tgz" - integrity sha512-WmSAg7WgqW7m4x8Mt4N6ZyKz0BubSj/2tVUMsAHp+Yd2AMwcSbeFq9WympT19p5heCFmF97R9eD5uUR/t4HEqw== - dependencies: - "@types/history" "^4.7.11" - "@types/react" "*" - "@types/react-router" "^5.1.0" - -"@types/react-router-dom@*": - version "5.3.3" - resolved "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz" - integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw== - dependencies: - "@types/history" "^4.7.11" - "@types/react" "*" - "@types/react-router" "*" - -"@types/react-router@*", "@types/react-router@^5.1.0": - version "5.1.20" - resolved "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz" - integrity sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q== - dependencies: - "@types/history" "^4.7.11" - "@types/react" "*" - -"@types/react@*": - version "18.3.18" - resolved "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz" - integrity sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ== - dependencies: - "@types/prop-types" "*" - csstype "^3.0.2" - -"@types/retry@0.12.0": - version "0.12.0" - resolved "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz" - integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== - -"@types/sax@^1.2.1": - version "1.2.7" - resolved "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz" - integrity sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A== - dependencies: - "@types/node" "*" - -"@types/send@*": - version "0.17.4" - resolved "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz" - integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== - dependencies: - "@types/mime" "^1" - "@types/node" "*" - -"@types/serve-index@^1.9.1": - version "1.9.4" - resolved "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz" - integrity sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug== - dependencies: - "@types/express" "*" - -"@types/serve-static@*", "@types/serve-static@^1.13.10": - version "1.15.7" - resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz" - integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== - dependencies: - "@types/http-errors" "*" - "@types/node" "*" - "@types/send" "*" - -"@types/sockjs@^0.3.33": - version "0.3.36" - resolved "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz" - integrity sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q== - dependencies: - "@types/node" "*" - -"@types/unist@*", "@types/unist@^3.0.0": - version "3.0.3" - resolved "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz" - integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q== - -"@types/unist@^2.0.0": - version "2.0.11" - resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz" - integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA== - -"@types/ws@^8.5.5": - version "8.5.14" - resolved "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz" - integrity sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw== - dependencies: - "@types/node" "*" - -"@types/yargs-parser@*": - version "21.0.3" - resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz" - integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== - -"@types/yargs@^17.0.8": - version "17.0.33" - resolved "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz" - integrity sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA== - dependencies: - "@types/yargs-parser" "*" - -"@ungap/structured-clone@^1.0.0": - version "1.3.0" - resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz" - integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== - -"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1": - version "1.14.1" - resolved "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz" - integrity sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ== - dependencies: - "@webassemblyjs/helper-numbers" "1.13.2" - "@webassemblyjs/helper-wasm-bytecode" "1.13.2" - -"@webassemblyjs/floating-point-hex-parser@1.13.2": - version "1.13.2" - resolved "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz" - integrity sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA== - -"@webassemblyjs/helper-api-error@1.13.2": - version "1.13.2" - resolved "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz" - integrity sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ== - -"@webassemblyjs/helper-buffer@1.14.1": - version "1.14.1" - resolved "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz" - integrity sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA== - -"@webassemblyjs/helper-numbers@1.13.2": - version "1.13.2" - resolved "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz" - integrity sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA== - dependencies: - "@webassemblyjs/floating-point-hex-parser" "1.13.2" - "@webassemblyjs/helper-api-error" "1.13.2" - "@xtuc/long" "4.2.2" - -"@webassemblyjs/helper-wasm-bytecode@1.13.2": - version "1.13.2" - resolved "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz" - integrity sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA== - -"@webassemblyjs/helper-wasm-section@1.14.1": - version "1.14.1" - resolved "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz" - integrity sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw== - dependencies: - "@webassemblyjs/ast" "1.14.1" - "@webassemblyjs/helper-buffer" "1.14.1" - "@webassemblyjs/helper-wasm-bytecode" "1.13.2" - "@webassemblyjs/wasm-gen" "1.14.1" - -"@webassemblyjs/ieee754@1.13.2": - version "1.13.2" - resolved "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz" - integrity sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw== - dependencies: - "@xtuc/ieee754" "^1.2.0" - -"@webassemblyjs/leb128@1.13.2": - version "1.13.2" - resolved "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz" - integrity sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw== - dependencies: - "@xtuc/long" "4.2.2" - -"@webassemblyjs/utf8@1.13.2": - version "1.13.2" - resolved "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz" - integrity sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ== - -"@webassemblyjs/wasm-edit@^1.14.1": - version "1.14.1" - resolved "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz" - integrity sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ== - dependencies: - "@webassemblyjs/ast" "1.14.1" - "@webassemblyjs/helper-buffer" "1.14.1" - "@webassemblyjs/helper-wasm-bytecode" "1.13.2" - "@webassemblyjs/helper-wasm-section" "1.14.1" - "@webassemblyjs/wasm-gen" "1.14.1" - "@webassemblyjs/wasm-opt" "1.14.1" - "@webassemblyjs/wasm-parser" "1.14.1" - "@webassemblyjs/wast-printer" "1.14.1" - -"@webassemblyjs/wasm-gen@1.14.1": - version "1.14.1" - resolved "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz" - integrity sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg== - dependencies: - "@webassemblyjs/ast" "1.14.1" - "@webassemblyjs/helper-wasm-bytecode" "1.13.2" - "@webassemblyjs/ieee754" "1.13.2" - "@webassemblyjs/leb128" "1.13.2" - "@webassemblyjs/utf8" "1.13.2" - -"@webassemblyjs/wasm-opt@1.14.1": - version "1.14.1" - resolved "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz" - integrity sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw== - dependencies: - "@webassemblyjs/ast" "1.14.1" - "@webassemblyjs/helper-buffer" "1.14.1" - "@webassemblyjs/wasm-gen" "1.14.1" - "@webassemblyjs/wasm-parser" "1.14.1" - -"@webassemblyjs/wasm-parser@1.14.1", "@webassemblyjs/wasm-parser@^1.14.1": - version "1.14.1" - resolved "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz" - integrity sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ== - dependencies: - "@webassemblyjs/ast" "1.14.1" - "@webassemblyjs/helper-api-error" "1.13.2" - "@webassemblyjs/helper-wasm-bytecode" "1.13.2" - "@webassemblyjs/ieee754" "1.13.2" - "@webassemblyjs/leb128" "1.13.2" - "@webassemblyjs/utf8" "1.13.2" - -"@webassemblyjs/wast-printer@1.14.1": - version "1.14.1" - resolved "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz" - integrity sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw== - dependencies: - "@webassemblyjs/ast" "1.14.1" - "@xtuc/long" "4.2.2" - -"@xtuc/ieee754@^1.2.0": - version "1.2.0" - resolved "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz" - integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== - -"@xtuc/long@4.2.2": - version "4.2.2" - resolved "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz" - integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== - -accepts@~1.3.4, accepts@~1.3.8: - version "1.3.8" - resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz" - integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== - dependencies: - mime-types "~2.1.34" - negotiator "0.6.3" - -acorn-jsx@^5.0.0: - version "5.3.2" - resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" - integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== - -acorn-walk@^8.0.0: - version "8.3.4" - resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz" - integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== - dependencies: - acorn "^8.11.0" - -acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.8.2: - version "8.14.0" - resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz" - integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== - -address@^1.0.1, address@^1.1.2: - version "1.2.2" - resolved "https://registry.npmjs.org/address/-/address-1.2.2.tgz" - integrity sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA== - -aggregate-error@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz" - integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== - dependencies: - clean-stack "^2.0.0" - indent-string "^4.0.0" - -ajv-formats@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz" - integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== - dependencies: - ajv "^8.0.0" - -ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: - version "3.5.2" - resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz" - integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== - -ajv-keywords@^5.1.0: - version "5.1.0" - resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz" - integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== - dependencies: - fast-deep-equal "^3.1.3" - -ajv@^6.12.2, ajv@^6.12.5: - version "6.12.6" - resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ajv@^8.0.0, ajv@^8.9.0: - version "8.17.1" - resolved "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz" - integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== - dependencies: - fast-deep-equal "^3.1.3" - fast-uri "^3.0.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - -algoliasearch-helper@^3.22.6: - version "3.23.1" - resolved "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.23.1.tgz" - integrity sha512-j/dF2ZELJBm4SJTK5ECsMuCDJpBB8ITiWKRjd3S15bK2bqrXKLWqDiA5A96WhVvCpZ2NmgNlUYmFbKOfcqivbg== - dependencies: - "@algolia/events" "^4.0.1" - -algoliasearch@^5.14.2, algoliasearch@^5.17.1: - version "5.20.0" - resolved "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.20.0.tgz" - integrity sha512-groO71Fvi5SWpxjI9Ia+chy0QBwT61mg6yxJV27f5YFf+Mw+STT75K6SHySpP8Co5LsCrtsbCH5dJZSRtkSKaQ== - dependencies: - "@algolia/client-abtesting" "5.20.0" - "@algolia/client-analytics" "5.20.0" - "@algolia/client-common" "5.20.0" - "@algolia/client-insights" "5.20.0" - "@algolia/client-personalization" "5.20.0" - "@algolia/client-query-suggestions" "5.20.0" - "@algolia/client-search" "5.20.0" - "@algolia/ingestion" "1.20.0" - "@algolia/monitoring" "1.20.0" - "@algolia/recommend" "5.20.0" - "@algolia/requester-browser-xhr" "5.20.0" - "@algolia/requester-fetch" "5.20.0" - "@algolia/requester-node-http" "5.20.0" - -ansi-align@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz" - integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== - dependencies: - string-width "^4.1.0" - -ansi-escapes@^4.3.2: - version "4.3.2" - resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz" - integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== - dependencies: - type-fest "^0.21.3" - -ansi-html-community@^0.0.8: - version "0.0.8" - resolved "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz" - integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-regex@^6.0.1: - version "6.1.0" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz" - integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -ansi-styles@^6.1.0: - version "6.2.1" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" - integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== - -any-promise@^1.0.0: - version "1.3.0" - resolved "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz" - integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== - -anymatch@~3.1.2: - version "3.1.3" - resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz" - integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -arg@^5.0.0, arg@^5.0.2: - version "5.0.2" - resolved "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz" - integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== - -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" - integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== - -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - -astring@^1.8.0: - version "1.9.0" - resolved "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz" - integrity sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg== - -at-least-node@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz" - integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== - -autoprefixer@^10.4.17, autoprefixer@^10.4.19: - version "10.4.20" - resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz" - integrity sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g== - dependencies: - browserslist "^4.23.3" - caniuse-lite "^1.0.30001646" - fraction.js "^4.3.7" - normalize-range "^0.1.2" - picocolors "^1.0.1" - postcss-value-parser "^4.2.0" - -babel-loader@^9.2.1: - version "9.2.1" - resolved "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz" - integrity sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA== - dependencies: - find-cache-dir "^4.0.0" - schema-utils "^4.0.0" - -babel-plugin-dynamic-import-node@^2.3.3: - version "2.3.3" - resolved "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz" - integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== - dependencies: - object.assign "^4.1.0" - -babel-plugin-polyfill-corejs2@^0.4.10: - version "0.4.12" - resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz" - integrity sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og== - dependencies: - "@babel/compat-data" "^7.22.6" - "@babel/helper-define-polyfill-provider" "^0.6.3" - semver "^6.3.1" - -babel-plugin-polyfill-corejs3@^0.10.6: - version "0.10.6" - resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz" - integrity sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.6.2" - core-js-compat "^3.38.0" - -babel-plugin-polyfill-regenerator@^0.6.1: - version "0.6.3" - resolved "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz" - integrity sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.6.3" - -bail@^2.0.0: - version "2.0.2" - resolved "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz" - integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw== - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -batch@0.6.1: - version "0.6.1" - resolved "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz" - integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== - -big.js@^5.2.2: - version "5.2.2" - resolved "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== - -binary-extensions@^2.0.0: - version "2.3.0" - resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz" - integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== - -body-parser@1.20.3: - version "1.20.3" - resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz" - integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== - dependencies: - bytes "3.1.2" - content-type "~1.0.5" - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - http-errors "2.0.0" - iconv-lite "0.4.24" - on-finished "2.4.1" - qs "6.13.0" - raw-body "2.5.2" - type-is "~1.6.18" - unpipe "1.0.0" - -bonjour-service@^1.0.11: - version "1.3.0" - resolved "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz" - integrity sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA== - dependencies: - fast-deep-equal "^3.1.3" - multicast-dns "^7.2.5" - -boolbase@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz" - integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== - -boxen@^6.2.1: - version "6.2.1" - resolved "https://registry.npmjs.org/boxen/-/boxen-6.2.1.tgz" - integrity sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw== - dependencies: - ansi-align "^3.0.1" - camelcase "^6.2.0" - chalk "^4.1.2" - cli-boxes "^3.0.0" - string-width "^5.0.1" - type-fest "^2.5.0" - widest-line "^4.0.1" - wrap-ansi "^8.0.1" - -boxen@^7.0.0: - version "7.1.1" - resolved "https://registry.npmjs.org/boxen/-/boxen-7.1.1.tgz" - integrity sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog== - dependencies: - ansi-align "^3.0.1" - camelcase "^7.0.1" - chalk "^5.2.0" - cli-boxes "^3.0.0" - string-width "^5.1.2" - type-fest "^2.13.0" - widest-line "^4.0.1" - wrap-ansi "^8.1.0" - -brace-expansion@^1.1.7: - version "1.1.12" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz" - integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -brace-expansion@^2.0.1: - version "2.0.2" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz" - integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== - dependencies: - balanced-match "^1.0.0" - -braces@^3.0.3, braces@~3.0.2: - version "3.0.3" - resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz" - integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== - dependencies: - fill-range "^7.1.1" - -browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.23.0, browserslist@^4.23.1, browserslist@^4.23.3, browserslist@^4.24.0, browserslist@^4.24.3: - version "4.24.4" - resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz" - integrity sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A== - dependencies: - caniuse-lite "^1.0.30001688" - electron-to-chromium "^1.5.73" - node-releases "^2.0.19" - update-browserslist-db "^1.1.1" - -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - -bytes@3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz" - integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== - -bytes@3.1.2: - version "3.1.2" - resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" - integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== - -cacheable-lookup@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz" - integrity sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w== - -cacheable-request@^10.2.8: - version "10.2.14" - resolved "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz" - integrity sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ== - dependencies: - "@types/http-cache-semantics" "^4.0.2" - get-stream "^6.0.1" - http-cache-semantics "^4.1.1" - keyv "^4.5.3" - mimic-response "^4.0.0" - normalize-url "^8.0.0" - responselike "^3.0.0" - -call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz" - integrity sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - -call-bind@^1.0.8: - version "1.0.8" - resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz" - integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== - dependencies: - call-bind-apply-helpers "^1.0.0" - es-define-property "^1.0.0" - get-intrinsic "^1.2.4" - set-function-length "^1.2.2" - -call-bound@^1.0.2, call-bound@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz" - integrity sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA== - dependencies: - call-bind-apply-helpers "^1.0.1" - get-intrinsic "^1.2.6" - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -camel-case@^4.1.2: - version "4.1.2" - resolved "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz" - integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== - dependencies: - pascal-case "^3.1.2" - tslib "^2.0.3" - -camelcase-css@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz" - integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== - -camelcase@^6.2.0: - version "6.3.0" - resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz" - integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== - -camelcase@^7.0.1: - version "7.0.1" - resolved "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz" - integrity sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw== - -caniuse-api@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz" - integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== - dependencies: - browserslist "^4.0.0" - caniuse-lite "^1.0.0" - lodash.memoize "^4.1.2" - lodash.uniq "^4.5.0" - -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001688: - version "1.0.30001695" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001695.tgz" - integrity sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw== - -ccount@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz" - integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== - -chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: - version "4.1.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chalk@^5.0.1, chalk@^5.2.0: - version "5.4.1" - resolved "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz" - integrity sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w== - -char-regex@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz" - integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== - -character-entities-html4@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz" - integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== - -character-entities-legacy@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz" - integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== - -character-entities@^2.0.0: - version "2.0.2" - resolved "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz" - integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ== - -character-reference-invalid@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz" - integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw== - -cheerio-select@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz" - integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g== - dependencies: - boolbase "^1.0.0" - css-select "^5.1.0" - css-what "^6.1.0" - domelementtype "^2.3.0" - domhandler "^5.0.3" - domutils "^3.0.1" - -cheerio@1.0.0-rc.12: - version "1.0.0-rc.12" - resolved "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz" - integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q== - dependencies: - cheerio-select "^2.1.0" - dom-serializer "^2.0.0" - domhandler "^5.0.3" - domutils "^3.0.1" - htmlparser2 "^8.0.1" - parse5 "^7.0.0" - parse5-htmlparser2-tree-adapter "^7.0.0" - -chokidar@^3.4.2, chokidar@^3.5.3, chokidar@^3.6.0: - version "3.6.0" - resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz" - integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -chrome-trace-event@^1.0.2: - version "1.0.4" - resolved "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz" - integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== - -ci-info@^3.2.0: - version "3.9.0" - resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz" - integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== - -clean-css@^5.2.2, clean-css@^5.3.2, clean-css@~5.3.2: - version "5.3.3" - resolved "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz" - integrity sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg== - dependencies: - source-map "~0.6.0" - -clean-stack@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz" - integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== - -cli-boxes@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz" - integrity sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g== - -cli-table3@^0.6.3: - version "0.6.5" - resolved "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz" - integrity sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ== - dependencies: - string-width "^4.2.0" - optionalDependencies: - "@colors/colors" "1.5.0" - -clone-deep@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz" - integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== - dependencies: - is-plain-object "^2.0.4" - kind-of "^6.0.2" - shallow-clone "^3.0.0" - -clsx@^2.0.0, clsx@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz" - integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== - -collapse-white-space@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz" - integrity sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw== - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -colord@^2.9.3: - version "2.9.3" - resolved "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz" - integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw== - -colorette@^2.0.10: - version "2.0.20" - resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz" - integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== - -combine-promises@^1.1.0: - version "1.2.0" - resolved "https://registry.npmjs.org/combine-promises/-/combine-promises-1.2.0.tgz" - integrity sha512-VcQB1ziGD0NXrhKxiwyNbCDmRzs/OShMs2GqW2DlU2A/Sd0nQxE1oWDAE5O0ygSx5mgQOn9eIFh7yKPgFRVkPQ== - -comma-separated-tokens@^2.0.0: - version "2.0.3" - resolved "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz" - integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== - -commander@^10.0.0: - version "10.0.1" - resolved "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz" - integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== - -commander@^2.20.0: - version "2.20.3" - resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - -commander@^4.0.0: - version "4.1.1" - resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz" - integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== - -commander@^5.1.0: - version "5.1.0" - resolved "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz" - integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== - -commander@^7.2.0: - version "7.2.0" - resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz" - integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== - -commander@^8.3.0: - version "8.3.0" - resolved "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz" - integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== - -common-path-prefix@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz" - integrity sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w== - -compressible@~2.0.18: - version "2.0.18" - resolved "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz" - integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== - dependencies: - mime-db ">= 1.43.0 < 2" - -compression@^1.7.4: - version "1.8.1" - resolved "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz" - integrity sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w== - dependencies: - bytes "3.1.2" - compressible "~2.0.18" - debug "2.6.9" - negotiator "~0.6.4" - on-headers "~1.1.0" - safe-buffer "5.2.1" - vary "~1.1.2" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -config-chain@^1.1.11: - version "1.1.13" - resolved "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz" - integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ== - dependencies: - ini "^1.3.4" - proto-list "~1.2.1" - -configstore@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/configstore/-/configstore-6.0.0.tgz" - integrity sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA== - dependencies: - dot-prop "^6.0.1" - graceful-fs "^4.2.6" - unique-string "^3.0.0" - write-file-atomic "^3.0.3" - xdg-basedir "^5.0.1" - -connect-history-api-fallback@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz" - integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== - -consola@^3.2.3: - version "3.4.0" - resolved "https://registry.npmjs.org/consola/-/consola-3.4.0.tgz" - integrity sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA== - -content-disposition@0.5.2: - version "0.5.2" - resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz" - integrity sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA== - -content-disposition@0.5.4: - version "0.5.4" - resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz" - integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== - dependencies: - safe-buffer "5.2.1" - -content-type@~1.0.4, content-type@~1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz" - integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== - -convert-source-map@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz" - integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== - -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" - integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== - -cookie@0.7.1: - version "0.7.1" - resolved "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz" - integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== - -copy-text-to-clipboard@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/copy-text-to-clipboard/-/copy-text-to-clipboard-3.2.0.tgz" - integrity sha512-RnJFp1XR/LOBDckxTib5Qjr/PMfkatD0MUCQgdpqS8MdKiNUzBjAQBEN6oUy+jW7LI93BBG3DtMB2KOOKpGs2Q== - -copy-webpack-plugin@^11.0.0: - version "11.0.0" - resolved "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz" - integrity sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ== - dependencies: - fast-glob "^3.2.11" - glob-parent "^6.0.1" - globby "^13.1.1" - normalize-path "^3.0.0" - schema-utils "^4.0.0" - serialize-javascript "^6.0.0" - -core-js-compat@^3.38.0, core-js-compat@^3.38.1: - version "3.40.0" - resolved "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.40.0.tgz" - integrity sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ== - dependencies: - browserslist "^4.24.3" - -core-js-pure@^3.30.2: - version "3.40.0" - resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.40.0.tgz" - integrity sha512-AtDzVIgRrmRKQai62yuSIN5vNiQjcJakJb4fbhVw3ehxx7Lohphvw9SGNWKhLFqSxC4ilD0g/L1huAYFQU3Q6A== - -core-js@^3.31.1: - version "3.40.0" - resolved "https://registry.npmjs.org/core-js/-/core-js-3.40.0.tgz" - integrity sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ== - -core-util-is@~1.0.0: - version "1.0.3" - resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz" - integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== - -cosmiconfig@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz" - integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== - dependencies: - "@types/parse-json" "^4.0.0" - import-fresh "^3.1.0" - parse-json "^5.0.0" - path-type "^4.0.0" - yaml "^1.7.2" - -cosmiconfig@^8.1.3, cosmiconfig@^8.3.5: - version "8.3.6" - resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz" - integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== - dependencies: - import-fresh "^3.3.0" - js-yaml "^4.1.0" - parse-json "^5.2.0" - path-type "^4.0.0" - -cross-spawn@^7.0.3, cross-spawn@^7.0.6: - version "7.0.6" - resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz" - integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -crypto-random-string@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz" - integrity sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA== - dependencies: - type-fest "^1.0.1" - -css-blank-pseudo@^7.0.1: - version "7.0.1" - resolved "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-7.0.1.tgz" - integrity sha512-jf+twWGDf6LDoXDUode+nc7ZlrqfaNphrBIBrcmeP3D8yw1uPaix1gCC8LUQUGQ6CycuK2opkbFFWFuq/a94ag== - dependencies: - postcss-selector-parser "^7.0.0" - -css-declaration-sorter@^7.2.0: - version "7.2.0" - resolved "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz" - integrity sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow== - -css-has-pseudo@^7.0.2: - version "7.0.2" - resolved "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-7.0.2.tgz" - integrity sha512-nzol/h+E0bId46Kn2dQH5VElaknX2Sr0hFuB/1EomdC7j+OISt2ZzK7EHX9DZDY53WbIVAR7FYKSO2XnSf07MQ== - dependencies: - "@csstools/selector-specificity" "^5.0.0" - postcss-selector-parser "^7.0.0" - postcss-value-parser "^4.2.0" - -css-loader@^6.8.1: - version "6.11.0" - resolved "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz" - integrity sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g== - dependencies: - icss-utils "^5.1.0" - postcss "^8.4.33" - postcss-modules-extract-imports "^3.1.0" - postcss-modules-local-by-default "^4.0.5" - postcss-modules-scope "^3.2.0" - postcss-modules-values "^4.0.0" - postcss-value-parser "^4.2.0" - semver "^7.5.4" - -css-minimizer-webpack-plugin@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-5.0.1.tgz" - integrity sha512-3caImjKFQkS+ws1TGcFn0V1HyDJFq1Euy589JlD6/3rV2kj+w7r5G9WDMgSHvpvXHNZ2calVypZWuEDQd9wfLg== - dependencies: - "@jridgewell/trace-mapping" "^0.3.18" - cssnano "^6.0.1" - jest-worker "^29.4.3" - postcss "^8.4.24" - schema-utils "^4.0.1" - serialize-javascript "^6.0.1" - -css-prefers-color-scheme@^10.0.0: - version "10.0.0" - resolved "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-10.0.0.tgz" - integrity sha512-VCtXZAWivRglTZditUfB4StnsWr6YVZ2PRtuxQLKTNRdtAf8tpzaVPE9zXIF3VaSc7O70iK/j1+NXxyQCqdPjQ== - -css-select@^4.1.3: - version "4.3.0" - resolved "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz" - integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== - dependencies: - boolbase "^1.0.0" - css-what "^6.0.1" - domhandler "^4.3.1" - domutils "^2.8.0" - nth-check "^2.0.1" - -css-select@^5.1.0: - version "5.1.0" - resolved "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz" - integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== - dependencies: - boolbase "^1.0.0" - css-what "^6.1.0" - domhandler "^5.0.2" - domutils "^3.0.1" - nth-check "^2.0.1" - -css-tree@^2.3.1: - version "2.3.1" - resolved "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz" - integrity sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw== - dependencies: - mdn-data "2.0.30" - source-map-js "^1.0.1" - -css-tree@~2.2.0: - version "2.2.1" - resolved "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz" - integrity sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA== - dependencies: - mdn-data "2.0.28" - source-map-js "^1.0.1" - -css-what@^6.0.1, css-what@^6.1.0: - version "6.1.0" - resolved "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz" - integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== - -cssdb@^8.2.3: - version "8.2.3" - resolved "https://registry.npmjs.org/cssdb/-/cssdb-8.2.3.tgz" - integrity sha512-9BDG5XmJrJQQnJ51VFxXCAtpZ5ebDlAREmO8sxMOVU0aSxN/gocbctjIG5LMh3WBUq+xTlb/jw2LoljBEqraTA== - -cssesc@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz" - integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== - -cssnano-preset-advanced@^6.1.2: - version "6.1.2" - resolved "https://registry.npmjs.org/cssnano-preset-advanced/-/cssnano-preset-advanced-6.1.2.tgz" - integrity sha512-Nhao7eD8ph2DoHolEzQs5CfRpiEP0xa1HBdnFZ82kvqdmbwVBUr2r1QuQ4t1pi+D1ZpqpcO4T+wy/7RxzJ/WPQ== - dependencies: - autoprefixer "^10.4.19" - browserslist "^4.23.0" - cssnano-preset-default "^6.1.2" - postcss-discard-unused "^6.0.5" - postcss-merge-idents "^6.0.3" - postcss-reduce-idents "^6.0.3" - postcss-zindex "^6.0.2" - -cssnano-preset-default@^6.1.2: - version "6.1.2" - resolved "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz" - integrity sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg== - dependencies: - browserslist "^4.23.0" - css-declaration-sorter "^7.2.0" - cssnano-utils "^4.0.2" - postcss-calc "^9.0.1" - postcss-colormin "^6.1.0" - postcss-convert-values "^6.1.0" - postcss-discard-comments "^6.0.2" - postcss-discard-duplicates "^6.0.3" - postcss-discard-empty "^6.0.3" - postcss-discard-overridden "^6.0.2" - postcss-merge-longhand "^6.0.5" - postcss-merge-rules "^6.1.1" - postcss-minify-font-values "^6.1.0" - postcss-minify-gradients "^6.0.3" - postcss-minify-params "^6.1.0" - postcss-minify-selectors "^6.0.4" - postcss-normalize-charset "^6.0.2" - postcss-normalize-display-values "^6.0.2" - postcss-normalize-positions "^6.0.2" - postcss-normalize-repeat-style "^6.0.2" - postcss-normalize-string "^6.0.2" - postcss-normalize-timing-functions "^6.0.2" - postcss-normalize-unicode "^6.1.0" - postcss-normalize-url "^6.0.2" - postcss-normalize-whitespace "^6.0.2" - postcss-ordered-values "^6.0.2" - postcss-reduce-initial "^6.1.0" - postcss-reduce-transforms "^6.0.2" - postcss-svgo "^6.0.3" - postcss-unique-selectors "^6.0.4" - -cssnano-utils@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz" - integrity sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ== - -cssnano@^6.0.1, cssnano@^6.1.2: - version "6.1.2" - resolved "https://registry.npmjs.org/cssnano/-/cssnano-6.1.2.tgz" - integrity sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA== - dependencies: - cssnano-preset-default "^6.1.2" - lilconfig "^3.1.1" - -csso@^5.0.5: - version "5.0.5" - resolved "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz" - integrity sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ== - dependencies: - css-tree "~2.2.0" - -csstype@^3.0.2, csstype@^3.1.3: - version "3.1.3" - resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz" - integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== - -debounce@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz" - integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== - -debug@2.6.9, debug@^2.6.0: - version "2.6.9" - resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: - version "4.4.0" - resolved "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz" - integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== - dependencies: - ms "^2.1.3" - -decode-named-character-reference@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz" - integrity sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg== - dependencies: - character-entities "^2.0.0" - -decompress-response@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz" - integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== - dependencies: - mimic-response "^3.1.0" - -deep-extend@^0.6.0: - version "0.6.0" - resolved "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - -deepmerge@^4.2.2, deepmerge@^4.3.1: - version "4.3.1" - resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz" - integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== - -default-gateway@^6.0.3: - version "6.0.3" - resolved "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz" - integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg== - dependencies: - execa "^5.0.0" - -defer-to-connect@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz" - integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== - -define-data-property@^1.0.1, define-data-property@^1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz" - integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== - dependencies: - es-define-property "^1.0.0" - es-errors "^1.3.0" - gopd "^1.0.1" - -define-lazy-prop@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz" - integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== - -define-properties@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz" - integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== - dependencies: - define-data-property "^1.0.1" - has-property-descriptors "^1.0.0" - object-keys "^1.1.1" - -del@^6.1.1: - version "6.1.1" - resolved "https://registry.npmjs.org/del/-/del-6.1.1.tgz" - integrity sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg== - dependencies: - globby "^11.0.1" - graceful-fs "^4.2.4" - is-glob "^4.0.1" - is-path-cwd "^2.2.0" - is-path-inside "^3.0.2" - p-map "^4.0.0" - rimraf "^3.0.2" - slash "^3.0.0" - -depd@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== - -depd@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" - integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== - -dequal@^2.0.0: - version "2.0.3" - resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz" - integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== - -destroy@1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" - integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== - -detect-node@^2.0.4: - version "2.1.0" - resolved "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz" - integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== - -detect-port-alt@^1.1.6: - version "1.1.6" - resolved "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz" - integrity sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q== - dependencies: - address "^1.0.1" - debug "^2.6.0" - -detect-port@^1.5.1: - version "1.6.1" - resolved "https://registry.npmjs.org/detect-port/-/detect-port-1.6.1.tgz" - integrity sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q== - dependencies: - address "^1.0.1" - debug "4" - -devlop@^1.0.0, devlop@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz" - integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== - dependencies: - dequal "^2.0.0" - -didyoumean@^1.2.2: - version "1.2.2" - resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz" - integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== - -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - -dlv@^1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz" - integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== - -dns-packet@^5.2.2: - version "5.6.1" - resolved "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz" - integrity sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw== - dependencies: - "@leichtgewicht/ip-codec" "^2.0.1" - -dom-converter@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz" - integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== - dependencies: - utila "~0.4" - -dom-serializer@^1.0.1: - version "1.4.1" - resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz" - integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== - dependencies: - domelementtype "^2.0.1" - domhandler "^4.2.0" - entities "^2.0.0" - -dom-serializer@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz" - integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== - dependencies: - domelementtype "^2.3.0" - domhandler "^5.0.2" - entities "^4.2.0" - -domelementtype@^2.0.1, domelementtype@^2.2.0, domelementtype@^2.3.0: - version "2.3.0" - resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz" - integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== - -domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: - version "4.3.1" - resolved "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz" - integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== - dependencies: - domelementtype "^2.2.0" - -domhandler@^5.0.2, domhandler@^5.0.3: - version "5.0.3" - resolved "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz" - integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== - dependencies: - domelementtype "^2.3.0" - -domutils@^2.5.2, domutils@^2.8.0: - version "2.8.0" - resolved "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz" - integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== - dependencies: - dom-serializer "^1.0.1" - domelementtype "^2.2.0" - domhandler "^4.2.0" - -domutils@^3.0.1: - version "3.2.2" - resolved "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz" - integrity sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw== - dependencies: - dom-serializer "^2.0.0" - domelementtype "^2.3.0" - domhandler "^5.0.3" - -dot-case@^3.0.4: - version "3.0.4" - resolved "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz" - integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== - dependencies: - no-case "^3.0.4" - tslib "^2.0.3" - -dot-prop@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz" - integrity sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA== - dependencies: - is-obj "^2.0.0" - -dotenv@^16.4.7: - version "16.4.7" - resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz" - integrity sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ== - -dunder-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz" - integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== - dependencies: - call-bind-apply-helpers "^1.0.1" - es-errors "^1.3.0" - gopd "^1.2.0" - -duplexer@^0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz" - integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== - -eastasianwidth@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz" - integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" - integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== - -electron-to-chromium@^1.5.73: - version "1.5.87" - resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.87.tgz" - integrity sha512-mPFwmEWmRivw2F8x3w3l2m6htAUN97Gy0kwpO++2m9iT1Gt8RCFVUfv9U/sIbHJ6rY4P6/ooqFL/eL7ock+pPg== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -emoji-regex@^9.2.2: - version "9.2.2" - resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" - integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== - -emojilib@^2.4.0: - version "2.4.0" - resolved "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz" - integrity sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw== - -emojis-list@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz" - integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== - -emoticon@^4.0.1: - version "4.1.0" - resolved "https://registry.npmjs.org/emoticon/-/emoticon-4.1.0.tgz" - integrity sha512-VWZfnxqwNcc51hIy/sbOdEem6D+cVtpPzEEtVAFdaas30+1dgkyaOQ4sQ6Bp0tOMqWO1v+HQfYaoodOkdhK6SQ== - -encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" - integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== - -encodeurl@~2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz" - integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== - -enhanced-resolve@^5.17.1: - version "5.18.0" - resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz" - integrity sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ== - dependencies: - graceful-fs "^4.2.4" - tapable "^2.2.0" - -entities@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz" - integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== - -entities@^4.2.0, entities@^4.4.0, entities@^4.5.0: - version "4.5.0" - resolved "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz" - integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== - -error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -es-define-property@^1.0.0, es-define-property@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz" - integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== - -es-errors@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz" - integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== - -es-module-lexer@^1.2.1: - version "1.6.0" - resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz" - integrity sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ== - -es-object-atoms@^1.0.0: - version "1.1.1" - resolved "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz" - integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== - dependencies: - es-errors "^1.3.0" - -esast-util-from-estree@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz" - integrity sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ== - dependencies: - "@types/estree-jsx" "^1.0.0" - devlop "^1.0.0" - estree-util-visit "^2.0.0" - unist-util-position-from-estree "^2.0.0" - -esast-util-from-js@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz" - integrity sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw== - dependencies: - "@types/estree-jsx" "^1.0.0" - acorn "^8.0.0" - esast-util-from-estree "^2.0.0" - vfile-message "^4.0.0" - -escalade@^3.1.1, escalade@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz" - integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== - -escape-goat@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz" - integrity sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg== - -escape-html@^1.0.3, escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" - integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== - -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" - integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== - -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - -escape-string-regexp@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz" - integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== - -eslint-scope@5.1.1: - version "5.1.1" - resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" - integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== - dependencies: - esrecurse "^4.3.0" - estraverse "^4.1.1" - -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -esrecurse@^4.3.0: - version "4.3.0" - resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -estraverse@^5.2.0: - version "5.3.0" - resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" - integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== - -estree-util-attach-comments@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz" - integrity sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw== - dependencies: - "@types/estree" "^1.0.0" - -estree-util-build-jsx@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz" - integrity sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ== - dependencies: - "@types/estree-jsx" "^1.0.0" - devlop "^1.0.0" - estree-util-is-identifier-name "^3.0.0" - estree-walker "^3.0.0" - -estree-util-is-identifier-name@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz" - integrity sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg== - -estree-util-scope@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz" - integrity sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ== - dependencies: - "@types/estree" "^1.0.0" - devlop "^1.0.0" - -estree-util-to-js@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz" - integrity sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg== - dependencies: - "@types/estree-jsx" "^1.0.0" - astring "^1.8.0" - source-map "^0.7.0" - -estree-util-value-to-estree@^3.0.1: - version "3.4.0" - resolved "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.4.0.tgz" - integrity sha512-Zlp+gxis+gCfK12d3Srl2PdX2ybsEA8ZYy6vQGVQTNNYLEGRQQ56XB64bjemN8kxIKXP1nC9ip4Z+ILy9LGzvQ== - dependencies: - "@types/estree" "^1.0.0" - -estree-util-visit@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz" - integrity sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww== - dependencies: - "@types/estree-jsx" "^1.0.0" - "@types/unist" "^3.0.0" - -estree-walker@^3.0.0: - version "3.0.3" - resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz" - integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== - dependencies: - "@types/estree" "^1.0.0" - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -eta@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/eta/-/eta-2.2.0.tgz" - integrity sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g== - -etag@~1.8.1: - version "1.8.1" - resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" - integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== - -eval@^0.1.8: - version "0.1.8" - resolved "https://registry.npmjs.org/eval/-/eval-0.1.8.tgz" - integrity sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw== - dependencies: - "@types/node" "*" - require-like ">= 0.1.1" - -eventemitter3@^4.0.0: - version "4.0.7" - resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz" - integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== - -events@^3.2.0: - version "3.3.0" - resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" - integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== - -execa@^5.0.0: - version "5.1.1" - resolved "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz" - integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== - dependencies: - cross-spawn "^7.0.3" - get-stream "^6.0.0" - human-signals "^2.1.0" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^4.0.1" - onetime "^5.1.2" - signal-exit "^3.0.3" - strip-final-newline "^2.0.0" - -express@^4.17.3: - version "4.21.2" - resolved "https://registry.npmjs.org/express/-/express-4.21.2.tgz" - integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== - dependencies: - accepts "~1.3.8" - array-flatten "1.1.1" - body-parser "1.20.3" - content-disposition "0.5.4" - content-type "~1.0.4" - cookie "0.7.1" - cookie-signature "1.0.6" - debug "2.6.9" - depd "2.0.0" - encodeurl "~2.0.0" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "1.3.1" - fresh "0.5.2" - http-errors "2.0.0" - merge-descriptors "1.0.3" - methods "~1.1.2" - on-finished "2.4.1" - parseurl "~1.3.3" - path-to-regexp "0.1.12" - proxy-addr "~2.0.7" - qs "6.13.0" - range-parser "~1.2.1" - safe-buffer "5.2.1" - send "0.19.0" - serve-static "1.16.2" - setprototypeof "1.2.0" - statuses "2.0.1" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - -extend-shallow@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz" - integrity sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug== - dependencies: - is-extendable "^0.1.0" - -extend@^3.0.0: - version "3.0.2" - resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: - version "3.1.3" - resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-glob@^3.2.11, fast-glob@^3.2.9, fast-glob@^3.3.0, fast-glob@^3.3.2: - version "3.3.3" - resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz" - integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.8" - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -fast-uri@^3.0.1: - version "3.0.6" - resolved "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz" - integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw== - -fastq@^1.6.0: - version "1.18.0" - resolved "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz" - integrity sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw== - dependencies: - reusify "^1.0.4" - -fault@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz" - integrity sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ== - dependencies: - format "^0.2.0" - -faye-websocket@^0.11.3: - version "0.11.4" - resolved "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz" - integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== - dependencies: - websocket-driver ">=0.5.1" - -feed@^4.2.2: - version "4.2.2" - resolved "https://registry.npmjs.org/feed/-/feed-4.2.2.tgz" - integrity sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ== - dependencies: - xml-js "^1.6.11" - -figures@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz" - integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== - dependencies: - escape-string-regexp "^1.0.5" - -file-loader@^6.2.0: - version "6.2.0" - resolved "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz" - integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw== - dependencies: - loader-utils "^2.0.0" - schema-utils "^3.0.0" - -filesize@^8.0.6: - version "8.0.7" - resolved "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz" - integrity sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ== - -fill-range@^7.1.1: - version "7.1.1" - resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz" - integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== - dependencies: - to-regex-range "^5.0.1" - -finalhandler@1.3.1: - version "1.3.1" - resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz" - integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== - dependencies: - debug "2.6.9" - encodeurl "~2.0.0" - escape-html "~1.0.3" - on-finished "2.4.1" - parseurl "~1.3.3" - statuses "2.0.1" - unpipe "~1.0.0" - -find-cache-dir@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz" - integrity sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg== - dependencies: - common-path-prefix "^3.0.0" - pkg-dir "^7.0.0" - -find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" - -find-up@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - -find-up@^6.3.0: - version "6.3.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz" - integrity sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw== - dependencies: - locate-path "^7.1.0" - path-exists "^5.0.0" - -flat@^5.0.2: - version "5.0.2" - resolved "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz" - integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== - -follow-redirects@^1.0.0: - version "1.15.9" - resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz" - integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== - -foreground-child@^3.1.0: - version "3.3.1" - resolved "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz" - integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== - dependencies: - cross-spawn "^7.0.6" - signal-exit "^4.0.1" - -fork-ts-checker-webpack-plugin@^6.5.0: - version "6.5.3" - resolved "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz" - integrity sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ== - dependencies: - "@babel/code-frame" "^7.8.3" - "@types/json-schema" "^7.0.5" - chalk "^4.1.0" - chokidar "^3.4.2" - cosmiconfig "^6.0.0" - deepmerge "^4.2.2" - fs-extra "^9.0.0" - glob "^7.1.6" - memfs "^3.1.2" - minimatch "^3.0.4" - schema-utils "2.7.0" - semver "^7.3.2" - tapable "^1.0.0" - -form-data-encoder@^2.1.2: - version "2.1.4" - resolved "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz" - integrity sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw== - -format@^0.2.0: - version "0.2.2" - resolved "https://registry.npmjs.org/format/-/format-0.2.2.tgz" - integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww== - -forwarded@0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" - integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== - -fraction.js@^4.3.7: - version "4.3.7" - resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz" - integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== - -framer-motion@^11.0.0: - version "11.18.2" - resolved "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz" - integrity sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w== - dependencies: - motion-dom "^11.18.1" - motion-utils "^11.18.1" - tslib "^2.4.0" - -fresh@0.5.2: - version "0.5.2" - resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" - integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== - -fs-extra@^11.1.1, fs-extra@^11.2.0: - version "11.3.0" - resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz" - integrity sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fs-extra@^9.0.0: - version "9.1.0" - resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz" - integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== - dependencies: - at-least-node "^1.0.0" - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fs-monkey@^1.0.4: - version "1.0.6" - resolved "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz" - integrity sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg== - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - -fsevents@~2.3.2: - version "2.3.3" - resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" - integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== - -function-bind@^1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" - integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== - -gensync@^1.0.0-beta.2: - version "1.0.0-beta.2" - resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" - integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== - -get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6: - version "1.2.7" - resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz" - integrity sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA== - dependencies: - call-bind-apply-helpers "^1.0.1" - es-define-property "^1.0.1" - es-errors "^1.3.0" - es-object-atoms "^1.0.0" - function-bind "^1.1.2" - get-proto "^1.0.0" - gopd "^1.2.0" - has-symbols "^1.1.0" - hasown "^2.0.2" - math-intrinsics "^1.1.0" - -get-own-enumerable-property-symbols@^3.0.0: - version "3.0.2" - resolved "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz" - integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== - -get-proto@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz" - integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== - dependencies: - dunder-proto "^1.0.1" - es-object-atoms "^1.0.0" - -get-stream@^6.0.0, get-stream@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" - integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== - -github-slugger@^1.5.0: - version "1.5.0" - resolved "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz" - integrity sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw== - -glob-parent@^5.1.2, glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob-parent@^6.0.1, glob-parent@^6.0.2: - version "6.0.2" - resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" - integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== - dependencies: - is-glob "^4.0.3" - -glob-to-regexp@^0.4.1: - version "0.4.1" - resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz" - integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== - -glob@^10.3.10: - version "10.4.5" - resolved "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz" - integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== - dependencies: - foreground-child "^3.1.0" - jackspeak "^3.1.2" - minimatch "^9.0.4" - minipass "^7.1.2" - package-json-from-dist "^1.0.0" - path-scurry "^1.11.1" - -glob@^7.0.0, glob@^7.1.3, glob@^7.1.6: - version "7.2.3" - resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - -global-dirs@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz" - integrity sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA== - dependencies: - ini "2.0.0" - -global-modules@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz" - integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== - dependencies: - global-prefix "^3.0.0" - -global-prefix@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz" - integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== - dependencies: - ini "^1.3.5" - kind-of "^6.0.2" - which "^1.3.1" - -globals@^11.1.0: - version "11.12.0" - resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - -globby@^11.0.1, globby@^11.0.4, globby@^11.1.0: - version "11.1.0" - resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.2.9" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^3.0.0" - -globby@^13.1.1: - version "13.2.2" - resolved "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz" - integrity sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w== - dependencies: - dir-glob "^3.0.1" - fast-glob "^3.3.0" - ignore "^5.2.4" - merge2 "^1.4.1" - slash "^4.0.0" - -goober@^2.1.16: - version "2.1.16" - resolved "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz" - integrity sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g== - -gopd@^1.0.1, gopd@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz" - integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== - -got@^12.1.0: - version "12.6.1" - resolved "https://registry.npmjs.org/got/-/got-12.6.1.tgz" - integrity sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ== - dependencies: - "@sindresorhus/is" "^5.2.0" - "@szmarczak/http-timer" "^5.0.1" - cacheable-lookup "^7.0.0" - cacheable-request "^10.2.8" - decompress-response "^6.0.0" - form-data-encoder "^2.1.2" - get-stream "^6.0.1" - http2-wrapper "^2.1.10" - lowercase-keys "^3.0.0" - p-cancelable "^3.0.0" - responselike "^3.0.0" - -graceful-fs@4.2.10: - version "4.2.10" - resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz" - integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== - -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: - version "4.2.11" - resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" - integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== - -gray-matter@^4.0.3: - version "4.0.3" - resolved "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz" - integrity sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q== - dependencies: - js-yaml "^3.13.1" - kind-of "^6.0.2" - section-matter "^1.0.0" - strip-bom-string "^1.0.0" - -gzip-size@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz" - integrity sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q== - dependencies: - duplexer "^0.1.2" - -handle-thing@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz" - integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz" - integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== - dependencies: - es-define-property "^1.0.0" - -has-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz" - integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== - -has-yarn@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/has-yarn/-/has-yarn-3.0.0.tgz" - integrity sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA== - -hasown@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" - integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== - dependencies: - function-bind "^1.1.2" - -hast-util-from-parse5@^8.0.0: - version "8.0.2" - resolved "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.2.tgz" - integrity sha512-SfMzfdAi/zAoZ1KkFEyyeXBn7u/ShQrfd675ZEE9M3qj+PMFX05xubzRyF76CCSJu8au9jgVxDV1+okFvgZU4A== - dependencies: - "@types/hast" "^3.0.0" - "@types/unist" "^3.0.0" - devlop "^1.0.0" - hastscript "^9.0.0" - property-information "^6.0.0" - vfile "^6.0.0" - vfile-location "^5.0.0" - web-namespaces "^2.0.0" - -hast-util-parse-selector@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz" - integrity sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A== - dependencies: - "@types/hast" "^3.0.0" - -hast-util-raw@^9.0.0: - version "9.1.0" - resolved "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz" - integrity sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw== - dependencies: - "@types/hast" "^3.0.0" - "@types/unist" "^3.0.0" - "@ungap/structured-clone" "^1.0.0" - hast-util-from-parse5 "^8.0.0" - hast-util-to-parse5 "^8.0.0" - html-void-elements "^3.0.0" - mdast-util-to-hast "^13.0.0" - parse5 "^7.0.0" - unist-util-position "^5.0.0" - unist-util-visit "^5.0.0" - vfile "^6.0.0" - web-namespaces "^2.0.0" - zwitch "^2.0.0" - -hast-util-to-estree@^3.0.0: - version "3.1.1" - resolved "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.1.tgz" - integrity sha512-IWtwwmPskfSmma9RpzCappDUitC8t5jhAynHhc1m2+5trOgsrp7txscUSavc5Ic8PATyAjfrCK1wgtxh2cICVQ== - dependencies: - "@types/estree" "^1.0.0" - "@types/estree-jsx" "^1.0.0" - "@types/hast" "^3.0.0" - comma-separated-tokens "^2.0.0" - devlop "^1.0.0" - estree-util-attach-comments "^3.0.0" - estree-util-is-identifier-name "^3.0.0" - hast-util-whitespace "^3.0.0" - mdast-util-mdx-expression "^2.0.0" - mdast-util-mdx-jsx "^3.0.0" - mdast-util-mdxjs-esm "^2.0.0" - property-information "^6.0.0" - space-separated-tokens "^2.0.0" - style-to-object "^1.0.0" - unist-util-position "^5.0.0" - zwitch "^2.0.0" - -hast-util-to-jsx-runtime@^2.0.0: - version "2.3.2" - resolved "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.2.tgz" - integrity sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg== - dependencies: - "@types/estree" "^1.0.0" - "@types/hast" "^3.0.0" - "@types/unist" "^3.0.0" - comma-separated-tokens "^2.0.0" - devlop "^1.0.0" - estree-util-is-identifier-name "^3.0.0" - hast-util-whitespace "^3.0.0" - mdast-util-mdx-expression "^2.0.0" - mdast-util-mdx-jsx "^3.0.0" - mdast-util-mdxjs-esm "^2.0.0" - property-information "^6.0.0" - space-separated-tokens "^2.0.0" - style-to-object "^1.0.0" - unist-util-position "^5.0.0" - vfile-message "^4.0.0" - -hast-util-to-parse5@^8.0.0: - version "8.0.0" - resolved "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz" - integrity sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw== - dependencies: - "@types/hast" "^3.0.0" - comma-separated-tokens "^2.0.0" - devlop "^1.0.0" - property-information "^6.0.0" - space-separated-tokens "^2.0.0" - web-namespaces "^2.0.0" - zwitch "^2.0.0" - -hast-util-whitespace@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz" - integrity sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw== - dependencies: - "@types/hast" "^3.0.0" - -hastscript@^9.0.0: - version "9.0.0" - resolved "https://registry.npmjs.org/hastscript/-/hastscript-9.0.0.tgz" - integrity sha512-jzaLBGavEDKHrc5EfFImKN7nZKKBdSLIdGvCwDZ9TfzbF2ffXiov8CKE445L2Z1Ek2t/m4SKQ2j6Ipv7NyUolw== - dependencies: - "@types/hast" "^3.0.0" - comma-separated-tokens "^2.0.0" - hast-util-parse-selector "^4.0.0" - property-information "^6.0.0" - space-separated-tokens "^2.0.0" - -he@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz" - integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== - -history@^4.9.0: - version "4.10.1" - resolved "https://registry.npmjs.org/history/-/history-4.10.1.tgz" - integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== - dependencies: - "@babel/runtime" "^7.1.2" - loose-envify "^1.2.0" - resolve-pathname "^3.0.0" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - value-equal "^1.0.1" - -hoist-non-react-statics@^3.1.0: - version "3.3.2" - resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz" - integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== - dependencies: - react-is "^16.7.0" - -hpack.js@^2.1.6: - version "2.1.6" - resolved "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz" - integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== - dependencies: - inherits "^2.0.1" - obuf "^1.0.0" - readable-stream "^2.0.1" - wbuf "^1.1.0" - -html-entities@^2.3.2: - version "2.5.2" - resolved "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz" - integrity sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA== - -html-escaper@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" - integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== - -html-minifier-terser@^6.0.2: - version "6.1.0" - resolved "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz" - integrity sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw== - dependencies: - camel-case "^4.1.2" - clean-css "^5.2.2" - commander "^8.3.0" - he "^1.2.0" - param-case "^3.0.4" - relateurl "^0.2.7" - terser "^5.10.0" - -html-minifier-terser@^7.2.0: - version "7.2.0" - resolved "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz" - integrity sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA== - dependencies: - camel-case "^4.1.2" - clean-css "~5.3.2" - commander "^10.0.0" - entities "^4.4.0" - param-case "^3.0.4" - relateurl "^0.2.7" - terser "^5.15.1" - -html-tags@^3.3.1: - version "3.3.1" - resolved "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz" - integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== - -html-url-attributes@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz" - integrity sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ== - -html-void-elements@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz" - integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== - -html-webpack-plugin@^5.6.0: - version "5.6.3" - resolved "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.3.tgz" - integrity sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg== - dependencies: - "@types/html-minifier-terser" "^6.0.0" - html-minifier-terser "^6.0.2" - lodash "^4.17.21" - pretty-error "^4.0.0" - tapable "^2.0.0" - -htmlparser2@^6.1.0: - version "6.1.0" - resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz" - integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== - dependencies: - domelementtype "^2.0.1" - domhandler "^4.0.0" - domutils "^2.5.2" - entities "^2.0.0" - -htmlparser2@^8.0.1: - version "8.0.2" - resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz" - integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== - dependencies: - domelementtype "^2.3.0" - domhandler "^5.0.3" - domutils "^3.0.1" - entities "^4.4.0" - -http-cache-semantics@^4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz" - integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== - -http-deceiver@^1.2.7: - version "1.2.7" - resolved "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz" - integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== - -http-errors@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" - integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== - dependencies: - depd "2.0.0" - inherits "2.0.4" - setprototypeof "1.2.0" - statuses "2.0.1" - toidentifier "1.0.1" - -http-errors@~1.6.2: - version "1.6.3" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz" - integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.4.0 < 2" - -http-parser-js@>=0.5.1: - version "0.5.9" - resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.9.tgz" - integrity sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw== - -http-proxy-middleware@^2.0.3: - version "2.0.9" - resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz" - integrity sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q== - dependencies: - "@types/http-proxy" "^1.17.8" - http-proxy "^1.18.1" - is-glob "^4.0.1" - is-plain-obj "^3.0.0" - micromatch "^4.0.2" - -http-proxy@^1.18.1: - version "1.18.1" - resolved "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz" - integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== - dependencies: - eventemitter3 "^4.0.0" - follow-redirects "^1.0.0" - requires-port "^1.0.0" - -http2-wrapper@^2.1.10: - version "2.2.1" - resolved "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz" - integrity sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ== - dependencies: - quick-lru "^5.1.1" - resolve-alpn "^1.2.0" - -human-signals@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz" - integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== - -iconv-lite@0.4.24: - version "0.4.24" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -icss-utils@^5.0.0, icss-utils@^5.1.0: - version "5.1.0" - resolved "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz" - integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== - -ignore@^5.2.0, ignore@^5.2.4: - version "5.3.2" - resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz" - integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== - -image-size@^1.0.2: - version "1.2.1" - resolved "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz" - integrity sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw== - dependencies: - queue "6.0.2" - -immer@^9.0.7: - version "9.0.21" - resolved "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz" - integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA== - -import-fresh@^3.1.0, import-fresh@^3.3.0: - version "3.3.0" - resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -import-lazy@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz" - integrity sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw== - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" - integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== - -indent-string@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz" - integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== - -infima@0.2.0-alpha.45: - version "0.2.0-alpha.45" - resolved "https://registry.npmjs.org/infima/-/infima-0.2.0-alpha.45.tgz" - integrity sha512-uyH0zfr1erU1OohLk0fT4Rrb94AOhguWNOcD9uGrSpRvNB+6gZXUoJX5J0NtvzBO10YZ9PgvA4NFgt+fYg8ojw== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" - integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== - -ini@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz" - integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== - -ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: - version "1.3.8" - resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - -inline-style-parser@0.2.4: - version "0.2.4" - resolved "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz" - integrity sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q== - -interpret@^1.0.0: - version "1.4.0" - resolved "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz" - integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== - -invariant@^2.2.4: - version "2.2.4" - resolved "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz" - integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== - dependencies: - loose-envify "^1.0.0" - -ipaddr.js@1.9.1: - version "1.9.1" - resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" - integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== - -ipaddr.js@^2.0.1: - version "2.2.0" - resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz" - integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== - -is-alphabetical@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz" - integrity sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ== - -is-alphanumerical@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz" - integrity sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw== - dependencies: - is-alphabetical "^2.0.0" - is-decimal "^2.0.0" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" - integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-ci@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz" - integrity sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ== - dependencies: - ci-info "^3.2.0" - -is-core-module@^2.16.0: - version "2.16.1" - resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz" - integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== - dependencies: - hasown "^2.0.2" - -is-decimal@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz" - integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A== - -is-docker@^2.0.0, is-docker@^2.1.1: - version "2.2.1" - resolved "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz" - integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== - -is-extendable@^0.1.0: - version "0.1.1" - resolved "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz" - integrity sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw== - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" - integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: - version "4.0.3" - resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-hexadecimal@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz" - integrity sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg== - -is-installed-globally@^0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz" - integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== - dependencies: - global-dirs "^3.0.0" - is-path-inside "^3.0.2" - -is-npm@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/is-npm/-/is-npm-6.0.0.tgz" - integrity sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ== - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-obj@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz" - integrity sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg== - -is-obj@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz" - integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== - -is-path-cwd@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz" - integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== - -is-path-inside@^3.0.2: - version "3.0.3" - resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz" - integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== - -is-plain-obj@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz" - integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== - -is-plain-obj@^4.0.0: - version "4.1.0" - resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz" - integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== - -is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-regexp@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz" - integrity sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA== - -is-root@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz" - integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg== - -is-stream@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" - integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== - -is-typedarray@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" - integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== - -is-wsl@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz" - integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== - dependencies: - is-docker "^2.0.0" - -is-yarn-global@^0.4.0: - version "0.4.1" - resolved "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.4.1.tgz" - integrity sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ== - -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" - integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== - -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" - integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== - -isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz" - integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== - -jackspeak@^3.1.2: - version "3.4.3" - resolved "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz" - integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== - dependencies: - "@isaacs/cliui" "^8.0.2" - optionalDependencies: - "@pkgjs/parseargs" "^0.11.0" - -javascript-stringify@^2.0.1: - version "2.1.0" - resolved "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz" - integrity sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg== - -jest-util@^29.7.0: - version "29.7.0" - resolved "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz" - integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== - dependencies: - "@jest/types" "^29.6.3" - "@types/node" "*" - chalk "^4.0.0" - ci-info "^3.2.0" - graceful-fs "^4.2.9" - picomatch "^2.2.3" - -jest-worker@^27.4.5: - version "27.5.1" - resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz" - integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== - dependencies: - "@types/node" "*" - merge-stream "^2.0.0" - supports-color "^8.0.0" - -jest-worker@^29.4.3: - version "29.7.0" - resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz" - integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== - dependencies: - "@types/node" "*" - jest-util "^29.7.0" - merge-stream "^2.0.0" - supports-color "^8.0.0" - -jiti@^1.20.0, jiti@^1.21.6: - version "1.21.7" - resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz" - integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A== - -joi@^17.9.2: - version "17.13.3" - resolved "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz" - integrity sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA== - dependencies: - "@hapi/hoek" "^9.3.0" - "@hapi/topo" "^5.1.0" - "@sideway/address" "^4.1.5" - "@sideway/formula" "^3.0.1" - "@sideway/pinpoint" "^2.0.0" - -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-yaml@^3.13.1: - version "3.14.1" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - -jsesc@^3.0.2: - version "3.1.0" - resolved "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz" - integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== - -jsesc@~3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz" - integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== - -json-buffer@3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz" - integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== - -json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: - version "2.3.1" - resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" - integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-schema-traverse@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" - integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== - -json5@^2.1.2, json5@^2.2.3: - version "2.2.3" - resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" - integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== - -jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== - dependencies: - universalify "^2.0.0" - optionalDependencies: - graceful-fs "^4.1.6" - -keyv@^4.5.3: - version "4.5.4" - resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz" - integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== - dependencies: - json-buffer "3.0.1" - -kind-of@^6.0.0, kind-of@^6.0.2: - version "6.0.3" - resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== - -kleur@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz" - integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== - -latest-version@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz" - integrity sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg== - dependencies: - package-json "^8.1.0" - -launch-editor@^2.6.0: - version "2.9.1" - resolved "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz" - integrity sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w== - dependencies: - picocolors "^1.0.0" - shell-quote "^1.8.1" - -leven@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz" - integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== - -lilconfig@^3.0.0, lilconfig@^3.1.1, lilconfig@^3.1.3: - version "3.1.3" - resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz" - integrity sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw== - -lines-and-columns@^1.1.6: - version "1.2.4" - resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" - integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== - -loader-runner@^4.2.0: - version "4.3.0" - resolved "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz" - integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== - -loader-utils@^2.0.0: - version "2.0.4" - resolved "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz" - integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^2.1.2" - -loader-utils@^3.2.0: - version "3.3.1" - resolved "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz" - integrity sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg== - -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== - dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" - -locate-path@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" - integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== - dependencies: - p-locate "^5.0.0" - -locate-path@^7.1.0: - version "7.2.0" - resolved "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz" - integrity sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA== - dependencies: - p-locate "^6.0.0" - -lodash.debounce@^4.0.8: - version "4.0.8" - resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz" - integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== - -lodash.memoize@^4.1.2: - version "4.1.2" - resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz" - integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== - -lodash.uniq@^4.5.0: - version "4.5.0" - resolved "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz" - integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== - -lodash@^4.17.20, lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -longest-streak@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz" - integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g== - -loose-envify@^1.0.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - -lower-case@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz" - integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== - dependencies: - tslib "^2.0.3" - -lowercase-keys@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz" - integrity sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ== - -lru-cache@^10.2.0: - version "10.4.3" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz" - integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== - -lru-cache@^5.1.1: - version "5.1.1" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz" - integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== - dependencies: - yallist "^3.0.2" - -lucide-react@^0.475.0: - version "0.475.0" - resolved "https://registry.npmjs.org/lucide-react/-/lucide-react-0.475.0.tgz" - integrity sha512-NJzvVu1HwFVeZ+Gwq2q00KygM1aBhy/ZrhY9FsAgJtpB+E4R7uxRk9M2iKvHa6/vNxZydIB59htha4c2vvwvVg== - -markdown-extensions@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz" - integrity sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q== - -markdown-table@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz" - integrity sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A== - dependencies: - repeat-string "^1.0.0" - -markdown-table@^3.0.0: - version "3.0.4" - resolved "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz" - integrity sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw== - -math-intrinsics@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz" - integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== - -mdast-util-directive@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz" - integrity sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q== - dependencies: - "@types/mdast" "^4.0.0" - "@types/unist" "^3.0.0" - ccount "^2.0.0" - devlop "^1.0.0" - mdast-util-from-markdown "^2.0.0" - mdast-util-to-markdown "^2.0.0" - parse-entities "^4.0.0" - stringify-entities "^4.0.0" - unist-util-visit-parents "^6.0.0" - -mdast-util-find-and-replace@^3.0.0, mdast-util-find-and-replace@^3.0.1: - version "3.0.2" - resolved "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz" - integrity sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg== - dependencies: - "@types/mdast" "^4.0.0" - escape-string-regexp "^5.0.0" - unist-util-is "^6.0.0" - unist-util-visit-parents "^6.0.0" - -mdast-util-from-markdown@^2.0.0: - version "2.0.2" - resolved "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz" - integrity sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA== - dependencies: - "@types/mdast" "^4.0.0" - "@types/unist" "^3.0.0" - decode-named-character-reference "^1.0.0" - devlop "^1.0.0" - mdast-util-to-string "^4.0.0" - micromark "^4.0.0" - micromark-util-decode-numeric-character-reference "^2.0.0" - micromark-util-decode-string "^2.0.0" - micromark-util-normalize-identifier "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - unist-util-stringify-position "^4.0.0" - -mdast-util-frontmatter@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz" - integrity sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA== - dependencies: - "@types/mdast" "^4.0.0" - devlop "^1.0.0" - escape-string-regexp "^5.0.0" - mdast-util-from-markdown "^2.0.0" - mdast-util-to-markdown "^2.0.0" - micromark-extension-frontmatter "^2.0.0" - -mdast-util-gfm-autolink-literal@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz" - integrity sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ== - dependencies: - "@types/mdast" "^4.0.0" - ccount "^2.0.0" - devlop "^1.0.0" - mdast-util-find-and-replace "^3.0.0" - micromark-util-character "^2.0.0" - -mdast-util-gfm-footnote@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz" - integrity sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ== - dependencies: - "@types/mdast" "^4.0.0" - devlop "^1.1.0" - mdast-util-from-markdown "^2.0.0" - mdast-util-to-markdown "^2.0.0" - micromark-util-normalize-identifier "^2.0.0" - -mdast-util-gfm-strikethrough@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz" - integrity sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg== - dependencies: - "@types/mdast" "^4.0.0" - mdast-util-from-markdown "^2.0.0" - mdast-util-to-markdown "^2.0.0" - -mdast-util-gfm-table@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz" - integrity sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg== - dependencies: - "@types/mdast" "^4.0.0" - devlop "^1.0.0" - markdown-table "^3.0.0" - mdast-util-from-markdown "^2.0.0" - mdast-util-to-markdown "^2.0.0" - -mdast-util-gfm-task-list-item@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz" - integrity sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ== - dependencies: - "@types/mdast" "^4.0.0" - devlop "^1.0.0" - mdast-util-from-markdown "^2.0.0" - mdast-util-to-markdown "^2.0.0" - -mdast-util-gfm@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz" - integrity sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw== - dependencies: - mdast-util-from-markdown "^2.0.0" - mdast-util-gfm-autolink-literal "^2.0.0" - mdast-util-gfm-footnote "^2.0.0" - mdast-util-gfm-strikethrough "^2.0.0" - mdast-util-gfm-table "^2.0.0" - mdast-util-gfm-task-list-item "^2.0.0" - mdast-util-to-markdown "^2.0.0" - -mdast-util-mdx-expression@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz" - integrity sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ== - dependencies: - "@types/estree-jsx" "^1.0.0" - "@types/hast" "^3.0.0" - "@types/mdast" "^4.0.0" - devlop "^1.0.0" - mdast-util-from-markdown "^2.0.0" - mdast-util-to-markdown "^2.0.0" - -mdast-util-mdx-jsx@^3.0.0: - version "3.2.0" - resolved "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz" - integrity sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q== - dependencies: - "@types/estree-jsx" "^1.0.0" - "@types/hast" "^3.0.0" - "@types/mdast" "^4.0.0" - "@types/unist" "^3.0.0" - ccount "^2.0.0" - devlop "^1.1.0" - mdast-util-from-markdown "^2.0.0" - mdast-util-to-markdown "^2.0.0" - parse-entities "^4.0.0" - stringify-entities "^4.0.0" - unist-util-stringify-position "^4.0.0" - vfile-message "^4.0.0" - -mdast-util-mdx@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz" - integrity sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w== - dependencies: - mdast-util-from-markdown "^2.0.0" - mdast-util-mdx-expression "^2.0.0" - mdast-util-mdx-jsx "^3.0.0" - mdast-util-mdxjs-esm "^2.0.0" - mdast-util-to-markdown "^2.0.0" - -mdast-util-mdxjs-esm@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz" - integrity sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg== - dependencies: - "@types/estree-jsx" "^1.0.0" - "@types/hast" "^3.0.0" - "@types/mdast" "^4.0.0" - devlop "^1.0.0" - mdast-util-from-markdown "^2.0.0" - mdast-util-to-markdown "^2.0.0" - -mdast-util-phrasing@^4.0.0: - version "4.1.0" - resolved "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz" - integrity sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w== - dependencies: - "@types/mdast" "^4.0.0" - unist-util-is "^6.0.0" - -mdast-util-to-hast@^13.0.0: - version "13.2.0" - resolved "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz" - integrity sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA== - dependencies: - "@types/hast" "^3.0.0" - "@types/mdast" "^4.0.0" - "@ungap/structured-clone" "^1.0.0" - devlop "^1.0.0" - micromark-util-sanitize-uri "^2.0.0" - trim-lines "^3.0.0" - unist-util-position "^5.0.0" - unist-util-visit "^5.0.0" - vfile "^6.0.0" - -mdast-util-to-markdown@^2.0.0: - version "2.1.2" - resolved "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz" - integrity sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA== - dependencies: - "@types/mdast" "^4.0.0" - "@types/unist" "^3.0.0" - longest-streak "^3.0.0" - mdast-util-phrasing "^4.0.0" - mdast-util-to-string "^4.0.0" - micromark-util-classify-character "^2.0.0" - micromark-util-decode-string "^2.0.0" - unist-util-visit "^5.0.0" - zwitch "^2.0.0" - -mdast-util-to-string@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz" - integrity sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg== - dependencies: - "@types/mdast" "^4.0.0" - -mdn-data@2.0.28: - version "2.0.28" - resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz" - integrity sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g== - -mdn-data@2.0.30: - version "2.0.30" - resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz" - integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA== - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" - integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== - -memfs@^3.1.2, memfs@^3.4.3: - version "3.5.3" - resolved "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz" - integrity sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw== - dependencies: - fs-monkey "^1.0.4" - -merge-descriptors@1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz" - integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== - -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - -merge2@^1.3.0, merge2@^1.4.1: - version "1.4.1" - resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - -methods@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" - integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== - -micromark-core-commonmark@^2.0.0: - version "2.0.2" - resolved "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.2.tgz" - integrity sha512-FKjQKbxd1cibWMM1P9N+H8TwlgGgSkWZMmfuVucLCHaYqeSvJ0hFeHsIa65pA2nYbes0f8LDHPMrd9X7Ujxg9w== - dependencies: - decode-named-character-reference "^1.0.0" - devlop "^1.0.0" - micromark-factory-destination "^2.0.0" - micromark-factory-label "^2.0.0" - micromark-factory-space "^2.0.0" - micromark-factory-title "^2.0.0" - micromark-factory-whitespace "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-chunked "^2.0.0" - micromark-util-classify-character "^2.0.0" - micromark-util-html-tag-name "^2.0.0" - micromark-util-normalize-identifier "^2.0.0" - micromark-util-resolve-all "^2.0.0" - micromark-util-subtokenize "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-extension-directive@^3.0.0: - version "3.0.2" - resolved "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-3.0.2.tgz" - integrity sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA== - dependencies: - devlop "^1.0.0" - micromark-factory-space "^2.0.0" - micromark-factory-whitespace "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - parse-entities "^4.0.0" - -micromark-extension-frontmatter@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz" - integrity sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg== - dependencies: - fault "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-extension-gfm-autolink-literal@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz" - integrity sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw== - dependencies: - micromark-util-character "^2.0.0" - micromark-util-sanitize-uri "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-extension-gfm-footnote@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz" - integrity sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw== - dependencies: - devlop "^1.0.0" - micromark-core-commonmark "^2.0.0" - micromark-factory-space "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-normalize-identifier "^2.0.0" - micromark-util-sanitize-uri "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-extension-gfm-strikethrough@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz" - integrity sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw== - dependencies: - devlop "^1.0.0" - micromark-util-chunked "^2.0.0" - micromark-util-classify-character "^2.0.0" - micromark-util-resolve-all "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-extension-gfm-table@^2.0.0: - version "2.1.1" - resolved "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz" - integrity sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg== - dependencies: - devlop "^1.0.0" - micromark-factory-space "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-extension-gfm-tagfilter@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz" - integrity sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg== - dependencies: - micromark-util-types "^2.0.0" - -micromark-extension-gfm-task-list-item@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz" - integrity sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw== - dependencies: - devlop "^1.0.0" - micromark-factory-space "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-extension-gfm@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz" - integrity sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w== - dependencies: - micromark-extension-gfm-autolink-literal "^2.0.0" - micromark-extension-gfm-footnote "^2.0.0" - micromark-extension-gfm-strikethrough "^2.0.0" - micromark-extension-gfm-table "^2.0.0" - micromark-extension-gfm-tagfilter "^2.0.0" - micromark-extension-gfm-task-list-item "^2.0.0" - micromark-util-combine-extensions "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-extension-mdx-expression@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.0.tgz" - integrity sha512-sI0nwhUDz97xyzqJAbHQhp5TfaxEvZZZ2JDqUo+7NvyIYG6BZ5CPPqj2ogUoPJlmXHBnyZUzISg9+oUmU6tUjQ== - dependencies: - "@types/estree" "^1.0.0" - devlop "^1.0.0" - micromark-factory-mdx-expression "^2.0.0" - micromark-factory-space "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-events-to-acorn "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-extension-mdx-jsx@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.1.tgz" - integrity sha512-vNuFb9czP8QCtAQcEJn0UJQJZA8Dk6DXKBqx+bg/w0WGuSxDxNr7hErW89tHUY31dUW4NqEOWwmEUNhjTFmHkg== - dependencies: - "@types/acorn" "^4.0.0" - "@types/estree" "^1.0.0" - devlop "^1.0.0" - estree-util-is-identifier-name "^3.0.0" - micromark-factory-mdx-expression "^2.0.0" - micromark-factory-space "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-events-to-acorn "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - vfile-message "^4.0.0" - -micromark-extension-mdx-md@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz" - integrity sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ== - dependencies: - micromark-util-types "^2.0.0" - -micromark-extension-mdxjs-esm@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz" - integrity sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A== - dependencies: - "@types/estree" "^1.0.0" - devlop "^1.0.0" - micromark-core-commonmark "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-events-to-acorn "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - unist-util-position-from-estree "^2.0.0" - vfile-message "^4.0.0" - -micromark-extension-mdxjs@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz" - integrity sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ== - dependencies: - acorn "^8.0.0" - acorn-jsx "^5.0.0" - micromark-extension-mdx-expression "^3.0.0" - micromark-extension-mdx-jsx "^3.0.0" - micromark-extension-mdx-md "^2.0.0" - micromark-extension-mdxjs-esm "^3.0.0" - micromark-util-combine-extensions "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-factory-destination@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz" - integrity sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA== - dependencies: - micromark-util-character "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-factory-label@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz" - integrity sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg== - dependencies: - devlop "^1.0.0" - micromark-util-character "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-factory-mdx-expression@^2.0.0: - version "2.0.2" - resolved "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.2.tgz" - integrity sha512-5E5I2pFzJyg2CtemqAbcyCktpHXuJbABnsb32wX2U8IQKhhVFBqkcZR5LRm1WVoFqa4kTueZK4abep7wdo9nrw== - dependencies: - "@types/estree" "^1.0.0" - devlop "^1.0.0" - micromark-factory-space "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-events-to-acorn "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - unist-util-position-from-estree "^2.0.0" - vfile-message "^4.0.0" - -micromark-factory-space@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz" - integrity sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-factory-space@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz" - integrity sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg== - dependencies: - micromark-util-character "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-factory-title@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz" - integrity sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw== - dependencies: - micromark-factory-space "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-factory-whitespace@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz" - integrity sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ== - dependencies: - micromark-factory-space "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-util-character@^1.0.0, micromark-util-character@^1.1.0: - version "1.2.0" - resolved "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz" - integrity sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg== - dependencies: - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-util-character@^2.0.0: - version "2.1.1" - resolved "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz" - integrity sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q== - dependencies: - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-util-chunked@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz" - integrity sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA== - dependencies: - micromark-util-symbol "^2.0.0" - -micromark-util-classify-character@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz" - integrity sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q== - dependencies: - micromark-util-character "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-util-combine-extensions@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz" - integrity sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg== - dependencies: - micromark-util-chunked "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-util-decode-numeric-character-reference@^2.0.0: - version "2.0.2" - resolved "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz" - integrity sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw== - dependencies: - micromark-util-symbol "^2.0.0" - -micromark-util-decode-string@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz" - integrity sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ== - dependencies: - decode-named-character-reference "^1.0.0" - micromark-util-character "^2.0.0" - micromark-util-decode-numeric-character-reference "^2.0.0" - micromark-util-symbol "^2.0.0" - -micromark-util-encode@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz" - integrity sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw== - -micromark-util-events-to-acorn@^2.0.0: - version "2.0.2" - resolved "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.2.tgz" - integrity sha512-Fk+xmBrOv9QZnEDguL9OI9/NQQp6Hz4FuQ4YmCb/5V7+9eAh1s6AYSvL20kHkD67YIg7EpE54TiSlcsf3vyZgA== - dependencies: - "@types/acorn" "^4.0.0" - "@types/estree" "^1.0.0" - "@types/unist" "^3.0.0" - devlop "^1.0.0" - estree-util-visit "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - vfile-message "^4.0.0" - -micromark-util-html-tag-name@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz" - integrity sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA== - -micromark-util-normalize-identifier@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz" - integrity sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q== - dependencies: - micromark-util-symbol "^2.0.0" - -micromark-util-resolve-all@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz" - integrity sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg== - dependencies: - micromark-util-types "^2.0.0" - -micromark-util-sanitize-uri@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz" - integrity sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ== - dependencies: - micromark-util-character "^2.0.0" - micromark-util-encode "^2.0.0" - micromark-util-symbol "^2.0.0" - -micromark-util-subtokenize@^2.0.0: - version "2.0.4" - resolved "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.4.tgz" - integrity sha512-N6hXjrin2GTJDe3MVjf5FuXpm12PGm80BrUAeub9XFXca8JZbP+oIwY4LJSVwFUCL1IPm/WwSVUN7goFHmSGGQ== - dependencies: - devlop "^1.0.0" - micromark-util-chunked "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-util-symbol@^1.0.0, micromark-util-symbol@^1.0.1: - version "1.1.0" - resolved "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz" - integrity sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag== - -micromark-util-symbol@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz" - integrity sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q== - -micromark-util-types@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz" - integrity sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg== - -micromark-util-types@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.1.tgz" - integrity sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ== - -micromark@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/micromark/-/micromark-4.0.1.tgz" - integrity sha512-eBPdkcoCNvYcxQOAKAlceo5SNdzZWfF+FcSupREAzdAh9rRmE239CEQAiTwIgblwnoM8zzj35sZ5ZwvSEOF6Kw== - dependencies: - "@types/debug" "^4.0.0" - debug "^4.0.0" - decode-named-character-reference "^1.0.0" - devlop "^1.0.0" - micromark-core-commonmark "^2.0.0" - micromark-factory-space "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-chunked "^2.0.0" - micromark-util-combine-extensions "^2.0.0" - micromark-util-decode-numeric-character-reference "^2.0.0" - micromark-util-encode "^2.0.0" - micromark-util-normalize-identifier "^2.0.0" - micromark-util-resolve-all "^2.0.0" - micromark-util-sanitize-uri "^2.0.0" - micromark-util-subtokenize "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromatch@^4.0.2, micromatch@^4.0.5, micromatch@^4.0.8: - version "4.0.8" - resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz" - integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== - dependencies: - braces "^3.0.3" - picomatch "^2.3.1" - -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -"mime-db@>= 1.43.0 < 2": - version "1.53.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz" - integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg== - -mime-db@~1.33.0: - version "1.33.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz" - integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ== - -mime-types@2.1.18, mime-types@~2.1.17: - version "2.1.18" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz" - integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ== - dependencies: - mime-db "~1.33.0" - -mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.24, mime-types@~2.1.34: - version "2.1.35" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -mime@1.6.0: - version "1.6.0" - resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - -mimic-response@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz" - integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== - -mimic-response@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz" - integrity sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg== - -mini-css-extract-plugin@^2.9.1: - version "2.9.2" - resolved "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz" - integrity sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w== - dependencies: - schema-utils "^4.0.0" - tapable "^2.2.1" - -minimalistic-assert@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - -minimatch@3.1.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1: - version "3.1.2" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^9.0.4: - version "9.0.5" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz" - integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== - dependencies: - brace-expansion "^2.0.1" - -minimist@^1.2.0: - version "1.2.8" - resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" - integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== - -"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: - version "7.1.2" - resolved "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz" - integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== - -motion-dom@^11.18.1: - version "11.18.1" - resolved "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz" - integrity sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw== - dependencies: - motion-utils "^11.18.1" - -motion-utils@^11.18.1: - version "11.18.1" - resolved "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz" - integrity sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA== - -mrmime@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz" - integrity sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw== - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" - integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== - -ms@2.1.3, ms@^2.1.3: - version "2.1.3" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -multicast-dns@^7.2.5: - version "7.2.5" - resolved "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz" - integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== - dependencies: - dns-packet "^5.2.2" - thunky "^1.0.2" - -mz@^2.7.0: - version "2.7.0" - resolved "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz" - integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== - dependencies: - any-promise "^1.0.0" - object-assign "^4.0.1" - thenify-all "^1.0.0" - -nanoid@^3.3.8: - version "3.3.8" - resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz" - integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== - -negotiator@0.6.3: - version "0.6.3" - resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz" - integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== - -negotiator@~0.6.4: - version "0.6.4" - resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz" - integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w== - -neo-async@^2.6.2: - version "2.6.2" - resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" - integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== - -no-case@^3.0.4: - version "3.0.4" - resolved "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz" - integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== - dependencies: - lower-case "^2.0.2" - tslib "^2.0.3" - -node-emoji@^2.1.0: - version "2.2.0" - resolved "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz" - integrity sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw== - dependencies: - "@sindresorhus/is" "^4.6.0" - char-regex "^1.0.2" - emojilib "^2.4.0" - skin-tone "^2.0.0" - -node-forge@^1: - version "1.3.1" - resolved "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz" - integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== - -node-releases@^2.0.19: - version "2.0.19" - resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz" - integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -normalize-range@^0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz" - integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== - -normalize-url@^8.0.0: - version "8.0.1" - resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz" - integrity sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w== - -npm-run-path@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz" - integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== - dependencies: - path-key "^3.0.0" - -nprogress@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz" - integrity sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA== - -nth-check@^2.0.1: - version "2.1.1" - resolved "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz" - integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== - dependencies: - boolbase "^1.0.0" - -null-loader@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/null-loader/-/null-loader-4.0.1.tgz" - integrity sha512-pxqVbi4U6N26lq+LmgIbB5XATP0VdZKOG25DhHi8btMmJJefGArFyDg1yc4U3hWCJbMqSrw0qyrz1UQX+qYXqg== - dependencies: - loader-utils "^2.0.0" - schema-utils "^3.0.0" - -object-assign@^4.0.1, object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" - integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== - -object-hash@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz" - integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== - -object-inspect@^1.13.3: - version "1.13.3" - resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz" - integrity sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA== - -object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object.assign@^4.1.0: - version "4.1.7" - resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz" - integrity sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.3" - define-properties "^1.2.1" - es-object-atoms "^1.0.0" - has-symbols "^1.1.0" - object-keys "^1.1.1" - -obuf@^1.0.0, obuf@^1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz" - integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== - -on-finished@2.4.1: - version "2.4.1" - resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" - integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== - dependencies: - ee-first "1.1.1" - -on-headers@~1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz" - integrity sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A== - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -onetime@^5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== - dependencies: - mimic-fn "^2.1.0" - -open@^8.0.9, open@^8.4.0: - version "8.4.2" - resolved "https://registry.npmjs.org/open/-/open-8.4.2.tgz" - integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== - dependencies: - define-lazy-prop "^2.0.0" - is-docker "^2.1.1" - is-wsl "^2.2.0" - -opener@^1.5.2: - version "1.5.2" - resolved "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz" - integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== - -p-cancelable@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz" - integrity sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw== - -p-limit@^2.0.0: - version "2.3.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-limit@^3.0.2: - version "3.1.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -p-limit@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz" - integrity sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ== - dependencies: - yocto-queue "^1.0.0" - -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== - dependencies: - p-limit "^2.0.0" - -p-locate@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz" - integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== - dependencies: - p-limit "^3.0.2" - -p-locate@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz" - integrity sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw== - dependencies: - p-limit "^4.0.0" - -p-map@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz" - integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== - dependencies: - aggregate-error "^3.0.0" - -p-retry@^4.5.0: - version "4.6.2" - resolved "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz" - integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== - dependencies: - "@types/retry" "0.12.0" - retry "^0.13.1" - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -package-json-from-dist@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz" - integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== - -package-json@^8.1.0: - version "8.1.1" - resolved "https://registry.npmjs.org/package-json/-/package-json-8.1.1.tgz" - integrity sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA== - dependencies: - got "^12.1.0" - registry-auth-token "^5.0.1" - registry-url "^6.0.0" - semver "^7.3.7" - -param-case@^3.0.4: - version "3.0.4" - resolved "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz" - integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A== - dependencies: - dot-case "^3.0.4" - tslib "^2.0.3" - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -parse-entities@^4.0.0: - version "4.0.2" - resolved "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz" - integrity sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw== - dependencies: - "@types/unist" "^2.0.0" - character-entities-legacy "^3.0.0" - character-reference-invalid "^2.0.0" - decode-named-character-reference "^1.0.0" - is-alphanumerical "^2.0.0" - is-decimal "^2.0.0" - is-hexadecimal "^2.0.0" - -parse-json@^5.0.0, parse-json@^5.2.0: - version "5.2.0" - resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" - integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== - dependencies: - "@babel/code-frame" "^7.0.0" - error-ex "^1.3.1" - json-parse-even-better-errors "^2.3.0" - lines-and-columns "^1.1.6" - -parse-numeric-range@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz" - integrity sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ== - -parse5-htmlparser2-tree-adapter@^7.0.0: - version "7.1.0" - resolved "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz" - integrity sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g== - dependencies: - domhandler "^5.0.3" - parse5 "^7.0.0" - -parse5@^7.0.0: - version "7.2.1" - resolved "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz" - integrity sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ== - dependencies: - entities "^4.5.0" - -parseurl@~1.3.2, parseurl@~1.3.3: - version "1.3.3" - resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== - -pascal-case@^3.1.2: - version "3.1.2" - resolved "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz" - integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== - dependencies: - no-case "^3.0.4" - tslib "^2.0.3" - -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz" - integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-exists@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz" - integrity sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - -path-is-inside@1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz" - integrity sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w== - -path-key@^3.0.0, path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -path-parse@^1.0.7: - version "1.0.7" - resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -path-scurry@^1.11.1: - version "1.11.1" - resolved "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz" - integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== - dependencies: - lru-cache "^10.2.0" - minipass "^5.0.0 || ^6.0.2 || ^7.0.0" - -path-to-regexp@0.1.12: - version "0.1.12" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz" - integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== - -path-to-regexp@3.3.0: - version "3.3.0" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz" - integrity sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw== - -path-to-regexp@^1.7.0: - version "1.9.0" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz" - integrity sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g== - dependencies: - isarray "0.0.1" - -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -picocolors@^1.0.0, picocolors@^1.0.1, picocolors@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" - integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== - -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -pify@^2.3.0: - version "2.3.0" - resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz" - integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== - -pirates@^4.0.1: - version "4.0.6" - resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz" - integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== - -pkg-dir@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz" - integrity sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA== - dependencies: - find-up "^6.3.0" - -pkg-up@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz" - integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA== - dependencies: - find-up "^3.0.0" - -postcss-attribute-case-insensitive@^7.0.1: - version "7.0.1" - resolved "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-7.0.1.tgz" - integrity sha512-Uai+SupNSqzlschRyNx3kbCTWgY/2hcwtHEI/ej2LJWc9JJ77qKgGptd8DHwY1mXtZ7Aoh4z4yxfwMBue9eNgw== - dependencies: - postcss-selector-parser "^7.0.0" - -postcss-calc@^9.0.1: - version "9.0.1" - resolved "https://registry.npmjs.org/postcss-calc/-/postcss-calc-9.0.1.tgz" - integrity sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ== - dependencies: - postcss-selector-parser "^6.0.11" - postcss-value-parser "^4.2.0" - -postcss-clamp@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz" - integrity sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-color-functional-notation@^7.0.7: - version "7.0.7" - resolved "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.7.tgz" - integrity sha512-EZvAHsvyASX63vXnyXOIynkxhaHRSsdb7z6yiXKIovGXAolW4cMZ3qoh7k3VdTsLBS6VGdksGfIo3r6+waLoOw== - dependencies: - "@csstools/css-color-parser" "^3.0.7" - "@csstools/css-parser-algorithms" "^3.0.4" - "@csstools/css-tokenizer" "^3.0.3" - "@csstools/postcss-progressive-custom-properties" "^4.0.0" - "@csstools/utilities" "^2.0.0" - -postcss-color-hex-alpha@^10.0.0: - version "10.0.0" - resolved "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-10.0.0.tgz" - integrity sha512-1kervM2cnlgPs2a8Vt/Qbe5cQ++N7rkYo/2rz2BkqJZIHQwaVuJgQH38REHrAi4uM0b1fqxMkWYmese94iMp3w== - dependencies: - "@csstools/utilities" "^2.0.0" - postcss-value-parser "^4.2.0" - -postcss-color-rebeccapurple@^10.0.0: - version "10.0.0" - resolved "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-10.0.0.tgz" - integrity sha512-JFta737jSP+hdAIEhk1Vs0q0YF5P8fFcj+09pweS8ktuGuZ8pPlykHsk6mPxZ8awDl4TrcxUqJo9l1IhVr/OjQ== - dependencies: - "@csstools/utilities" "^2.0.0" - postcss-value-parser "^4.2.0" - -postcss-colormin@^6.1.0: - version "6.1.0" - resolved "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.1.0.tgz" - integrity sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw== - dependencies: - browserslist "^4.23.0" - caniuse-api "^3.0.0" - colord "^2.9.3" - postcss-value-parser "^4.2.0" - -postcss-convert-values@^6.1.0: - version "6.1.0" - resolved "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.1.0.tgz" - integrity sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w== - dependencies: - browserslist "^4.23.0" - postcss-value-parser "^4.2.0" - -postcss-custom-media@^11.0.5: - version "11.0.5" - resolved "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-11.0.5.tgz" - integrity sha512-SQHhayVNgDvSAdX9NQ/ygcDQGEY+aSF4b/96z7QUX6mqL5yl/JgG/DywcF6fW9XbnCRE+aVYk+9/nqGuzOPWeQ== - dependencies: - "@csstools/cascade-layer-name-parser" "^2.0.4" - "@csstools/css-parser-algorithms" "^3.0.4" - "@csstools/css-tokenizer" "^3.0.3" - "@csstools/media-query-list-parser" "^4.0.2" - -postcss-custom-properties@^14.0.4: - version "14.0.4" - resolved "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-14.0.4.tgz" - integrity sha512-QnW8FCCK6q+4ierwjnmXF9Y9KF8q0JkbgVfvQEMa93x1GT8FvOiUevWCN2YLaOWyByeDX8S6VFbZEeWoAoXs2A== - dependencies: - "@csstools/cascade-layer-name-parser" "^2.0.4" - "@csstools/css-parser-algorithms" "^3.0.4" - "@csstools/css-tokenizer" "^3.0.3" - "@csstools/utilities" "^2.0.0" - postcss-value-parser "^4.2.0" - -postcss-custom-selectors@^8.0.4: - version "8.0.4" - resolved "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-8.0.4.tgz" - integrity sha512-ASOXqNvDCE0dAJ/5qixxPeL1aOVGHGW2JwSy7HyjWNbnWTQCl+fDc968HY1jCmZI0+BaYT5CxsOiUhavpG/7eg== - dependencies: - "@csstools/cascade-layer-name-parser" "^2.0.4" - "@csstools/css-parser-algorithms" "^3.0.4" - "@csstools/css-tokenizer" "^3.0.3" - postcss-selector-parser "^7.0.0" - -postcss-dir-pseudo-class@^9.0.1: - version "9.0.1" - resolved "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-9.0.1.tgz" - integrity sha512-tRBEK0MHYvcMUrAuYMEOa0zg9APqirBcgzi6P21OhxtJyJADo/SWBwY1CAwEohQ/6HDaa9jCjLRG7K3PVQYHEA== - dependencies: - postcss-selector-parser "^7.0.0" - -postcss-discard-comments@^6.0.2: - version "6.0.2" - resolved "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz" - integrity sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw== - -postcss-discard-duplicates@^6.0.3: - version "6.0.3" - resolved "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.3.tgz" - integrity sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw== - -postcss-discard-empty@^6.0.3: - version "6.0.3" - resolved "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.3.tgz" - integrity sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ== - -postcss-discard-overridden@^6.0.2: - version "6.0.2" - resolved "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.2.tgz" - integrity sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ== - -postcss-discard-unused@^6.0.5: - version "6.0.5" - resolved "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-6.0.5.tgz" - integrity sha512-wHalBlRHkaNnNwfC8z+ppX57VhvS+HWgjW508esjdaEYr3Mx7Gnn2xA4R/CKf5+Z9S5qsqC+Uzh4ueENWwCVUA== - dependencies: - postcss-selector-parser "^6.0.16" - -postcss-double-position-gradients@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-6.0.0.tgz" - integrity sha512-JkIGah3RVbdSEIrcobqj4Gzq0h53GG4uqDPsho88SgY84WnpkTpI0k50MFK/sX7XqVisZ6OqUfFnoUO6m1WWdg== - dependencies: - "@csstools/postcss-progressive-custom-properties" "^4.0.0" - "@csstools/utilities" "^2.0.0" - postcss-value-parser "^4.2.0" - -postcss-focus-visible@^10.0.1: - version "10.0.1" - resolved "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-10.0.1.tgz" - integrity sha512-U58wyjS/I1GZgjRok33aE8juW9qQgQUNwTSdxQGuShHzwuYdcklnvK/+qOWX1Q9kr7ysbraQ6ht6r+udansalA== - dependencies: - postcss-selector-parser "^7.0.0" - -postcss-focus-within@^9.0.1: - version "9.0.1" - resolved "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-9.0.1.tgz" - integrity sha512-fzNUyS1yOYa7mOjpci/bR+u+ESvdar6hk8XNK/TRR0fiGTp2QT5N+ducP0n3rfH/m9I7H/EQU6lsa2BrgxkEjw== - dependencies: - postcss-selector-parser "^7.0.0" - -postcss-font-variant@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz" - integrity sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA== - -postcss-gap-properties@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-6.0.0.tgz" - integrity sha512-Om0WPjEwiM9Ru+VhfEDPZJAKWUd0mV1HmNXqp2C29z80aQ2uP9UVhLc7e3aYMIor/S5cVhoPgYQ7RtfeZpYTRw== - -postcss-image-set-function@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-7.0.0.tgz" - integrity sha512-QL7W7QNlZuzOwBTeXEmbVckNt1FSmhQtbMRvGGqqU4Nf4xk6KUEQhAoWuMzwbSv5jxiRiSZ5Tv7eiDB9U87znA== - dependencies: - "@csstools/utilities" "^2.0.0" - postcss-value-parser "^4.2.0" - -postcss-import@^15.1.0: - version "15.1.0" - resolved "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz" - integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew== - dependencies: - postcss-value-parser "^4.0.0" - read-cache "^1.0.0" - resolve "^1.1.7" - -postcss-import@^16.1.0: - version "16.1.0" - resolved "https://registry.npmjs.org/postcss-import/-/postcss-import-16.1.0.tgz" - integrity sha512-7hsAZ4xGXl4MW+OKEWCnF6T5jqBw80/EE9aXg1r2yyn1RsVEU8EtKXbijEODa+rg7iih4bKf7vlvTGYR4CnPNg== - dependencies: - postcss-value-parser "^4.0.0" - read-cache "^1.0.0" - resolve "^1.1.7" - -postcss-js@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz" - integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw== - dependencies: - camelcase-css "^2.0.1" - -postcss-lab-function@^7.0.7: - version "7.0.7" - resolved "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-7.0.7.tgz" - integrity sha512-+ONj2bpOQfsCKZE2T9VGMyVVdGcGUpr7u3SVfvkJlvhTRmDCfY25k4Jc8fubB9DclAPR4+w8uVtDZmdRgdAHig== - dependencies: - "@csstools/css-color-parser" "^3.0.7" - "@csstools/css-parser-algorithms" "^3.0.4" - "@csstools/css-tokenizer" "^3.0.3" - "@csstools/postcss-progressive-custom-properties" "^4.0.0" - "@csstools/utilities" "^2.0.0" - -postcss-load-config@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz" - integrity sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ== - dependencies: - lilconfig "^3.0.0" - yaml "^2.3.4" - -postcss-loader@^7.3.3: - version "7.3.4" - resolved "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.4.tgz" - integrity sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A== - dependencies: - cosmiconfig "^8.3.5" - jiti "^1.20.0" - semver "^7.5.4" - -postcss-logical@^8.0.0: - version "8.0.0" - resolved "https://registry.npmjs.org/postcss-logical/-/postcss-logical-8.0.0.tgz" - integrity sha512-HpIdsdieClTjXLOyYdUPAX/XQASNIwdKt5hoZW08ZOAiI+tbV0ta1oclkpVkW5ANU+xJvk3KkA0FejkjGLXUkg== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-merge-idents@^6.0.3: - version "6.0.3" - resolved "https://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-6.0.3.tgz" - integrity sha512-1oIoAsODUs6IHQZkLQGO15uGEbK3EAl5wi9SS8hs45VgsxQfMnxvt+L+zIr7ifZFIH14cfAeVe2uCTa+SPRa3g== - dependencies: - cssnano-utils "^4.0.2" - postcss-value-parser "^4.2.0" - -postcss-merge-longhand@^6.0.5: - version "6.0.5" - resolved "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.5.tgz" - integrity sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w== - dependencies: - postcss-value-parser "^4.2.0" - stylehacks "^6.1.1" - -postcss-merge-rules@^6.1.1: - version "6.1.1" - resolved "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.1.1.tgz" - integrity sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ== - dependencies: - browserslist "^4.23.0" - caniuse-api "^3.0.0" - cssnano-utils "^4.0.2" - postcss-selector-parser "^6.0.16" - -postcss-minify-font-values@^6.1.0: - version "6.1.0" - resolved "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.1.0.tgz" - integrity sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-minify-gradients@^6.0.3: - version "6.0.3" - resolved "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.3.tgz" - integrity sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q== - dependencies: - colord "^2.9.3" - cssnano-utils "^4.0.2" - postcss-value-parser "^4.2.0" - -postcss-minify-params@^6.1.0: - version "6.1.0" - resolved "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.1.0.tgz" - integrity sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA== - dependencies: - browserslist "^4.23.0" - cssnano-utils "^4.0.2" - postcss-value-parser "^4.2.0" - -postcss-minify-selectors@^6.0.4: - version "6.0.4" - resolved "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.4.tgz" - integrity sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ== - dependencies: - postcss-selector-parser "^6.0.16" - -postcss-modules-extract-imports@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz" - integrity sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q== - -postcss-modules-local-by-default@^4.0.5: - version "4.2.0" - resolved "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz" - integrity sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw== - dependencies: - icss-utils "^5.0.0" - postcss-selector-parser "^7.0.0" - postcss-value-parser "^4.1.0" - -postcss-modules-scope@^3.2.0: - version "3.2.1" - resolved "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz" - integrity sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA== - dependencies: - postcss-selector-parser "^7.0.0" - -postcss-modules-values@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz" - integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== - dependencies: - icss-utils "^5.0.0" - -postcss-nested@^6.2.0: - version "6.2.0" - resolved "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz" - integrity sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ== - dependencies: - postcss-selector-parser "^6.1.1" - -postcss-nesting@^13.0.1: - version "13.0.1" - resolved "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.1.tgz" - integrity sha512-VbqqHkOBOt4Uu3G8Dm8n6lU5+9cJFxiuty9+4rcoyRPO9zZS1JIs6td49VIoix3qYqELHlJIn46Oih9SAKo+yQ== - dependencies: - "@csstools/selector-resolve-nested" "^3.0.0" - "@csstools/selector-specificity" "^5.0.0" - postcss-selector-parser "^7.0.0" - -postcss-normalize-charset@^6.0.2: - version "6.0.2" - resolved "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz" - integrity sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ== - -postcss-normalize-display-values@^6.0.2: - version "6.0.2" - resolved "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.2.tgz" - integrity sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-normalize-positions@^6.0.2: - version "6.0.2" - resolved "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.2.tgz" - integrity sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-normalize-repeat-style@^6.0.2: - version "6.0.2" - resolved "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.2.tgz" - integrity sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-normalize-string@^6.0.2: - version "6.0.2" - resolved "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.2.tgz" - integrity sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-normalize-timing-functions@^6.0.2: - version "6.0.2" - resolved "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.2.tgz" - integrity sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-normalize-unicode@^6.1.0: - version "6.1.0" - resolved "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.1.0.tgz" - integrity sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg== - dependencies: - browserslist "^4.23.0" - postcss-value-parser "^4.2.0" - -postcss-normalize-url@^6.0.2: - version "6.0.2" - resolved "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.2.tgz" - integrity sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-normalize-whitespace@^6.0.2: - version "6.0.2" - resolved "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.2.tgz" - integrity sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-opacity-percentage@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-3.0.0.tgz" - integrity sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ== - -postcss-ordered-values@^6.0.2: - version "6.0.2" - resolved "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz" - integrity sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q== - dependencies: - cssnano-utils "^4.0.2" - postcss-value-parser "^4.2.0" - -postcss-overflow-shorthand@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-6.0.0.tgz" - integrity sha512-BdDl/AbVkDjoTofzDQnwDdm/Ym6oS9KgmO7Gr+LHYjNWJ6ExORe4+3pcLQsLA9gIROMkiGVjjwZNoL/mpXHd5Q== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-page-break@^3.0.4: - version "3.0.4" - resolved "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz" - integrity sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ== - -postcss-place@^10.0.0: - version "10.0.0" - resolved "https://registry.npmjs.org/postcss-place/-/postcss-place-10.0.0.tgz" - integrity sha512-5EBrMzat2pPAxQNWYavwAfoKfYcTADJ8AXGVPcUZ2UkNloUTWzJQExgrzrDkh3EKzmAx1evfTAzF9I8NGcc+qw== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-preset-env@^10.1.0: - version "10.1.3" - resolved "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-10.1.3.tgz" - integrity sha512-9qzVhcMFU/MnwYHyYpJz4JhGku/4+xEiPTmhn0hj3IxnUYlEF9vbh7OC1KoLAnenS6Fgg43TKNp9xcuMeAi4Zw== - dependencies: - "@csstools/postcss-cascade-layers" "^5.0.1" - "@csstools/postcss-color-function" "^4.0.7" - "@csstools/postcss-color-mix-function" "^3.0.7" - "@csstools/postcss-content-alt-text" "^2.0.4" - "@csstools/postcss-exponential-functions" "^2.0.6" - "@csstools/postcss-font-format-keywords" "^4.0.0" - "@csstools/postcss-gamut-mapping" "^2.0.7" - "@csstools/postcss-gradients-interpolation-method" "^5.0.7" - "@csstools/postcss-hwb-function" "^4.0.7" - "@csstools/postcss-ic-unit" "^4.0.0" - "@csstools/postcss-initial" "^2.0.0" - "@csstools/postcss-is-pseudo-class" "^5.0.1" - "@csstools/postcss-light-dark-function" "^2.0.7" - "@csstools/postcss-logical-float-and-clear" "^3.0.0" - "@csstools/postcss-logical-overflow" "^2.0.0" - "@csstools/postcss-logical-overscroll-behavior" "^2.0.0" - "@csstools/postcss-logical-resize" "^3.0.0" - "@csstools/postcss-logical-viewport-units" "^3.0.3" - "@csstools/postcss-media-minmax" "^2.0.6" - "@csstools/postcss-media-queries-aspect-ratio-number-values" "^3.0.4" - "@csstools/postcss-nested-calc" "^4.0.0" - "@csstools/postcss-normalize-display-values" "^4.0.0" - "@csstools/postcss-oklab-function" "^4.0.7" - "@csstools/postcss-progressive-custom-properties" "^4.0.0" - "@csstools/postcss-random-function" "^1.0.2" - "@csstools/postcss-relative-color-syntax" "^3.0.7" - "@csstools/postcss-scope-pseudo-class" "^4.0.1" - "@csstools/postcss-sign-functions" "^1.1.1" - "@csstools/postcss-stepped-value-functions" "^4.0.6" - "@csstools/postcss-text-decoration-shorthand" "^4.0.1" - "@csstools/postcss-trigonometric-functions" "^4.0.6" - "@csstools/postcss-unset-value" "^4.0.0" - autoprefixer "^10.4.19" - browserslist "^4.23.1" - css-blank-pseudo "^7.0.1" - css-has-pseudo "^7.0.2" - css-prefers-color-scheme "^10.0.0" - cssdb "^8.2.3" - postcss-attribute-case-insensitive "^7.0.1" - postcss-clamp "^4.1.0" - postcss-color-functional-notation "^7.0.7" - postcss-color-hex-alpha "^10.0.0" - postcss-color-rebeccapurple "^10.0.0" - postcss-custom-media "^11.0.5" - postcss-custom-properties "^14.0.4" - postcss-custom-selectors "^8.0.4" - postcss-dir-pseudo-class "^9.0.1" - postcss-double-position-gradients "^6.0.0" - postcss-focus-visible "^10.0.1" - postcss-focus-within "^9.0.1" - postcss-font-variant "^5.0.0" - postcss-gap-properties "^6.0.0" - postcss-image-set-function "^7.0.0" - postcss-lab-function "^7.0.7" - postcss-logical "^8.0.0" - postcss-nesting "^13.0.1" - postcss-opacity-percentage "^3.0.0" - postcss-overflow-shorthand "^6.0.0" - postcss-page-break "^3.0.4" - postcss-place "^10.0.0" - postcss-pseudo-class-any-link "^10.0.1" - postcss-replace-overflow-wrap "^4.0.0" - postcss-selector-not "^8.0.1" - -postcss-pseudo-class-any-link@^10.0.1: - version "10.0.1" - resolved "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-10.0.1.tgz" - integrity sha512-3el9rXlBOqTFaMFkWDOkHUTQekFIYnaQY55Rsp8As8QQkpiSgIYEcF/6Ond93oHiDsGb4kad8zjt+NPlOC1H0Q== - dependencies: - postcss-selector-parser "^7.0.0" - -postcss-reduce-idents@^6.0.3: - version "6.0.3" - resolved "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-6.0.3.tgz" - integrity sha512-G3yCqZDpsNPoQgbDUy3T0E6hqOQ5xigUtBQyrmq3tn2GxlyiL0yyl7H+T8ulQR6kOcHJ9t7/9H4/R2tv8tJbMA== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-reduce-initial@^6.1.0: - version "6.1.0" - resolved "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.1.0.tgz" - integrity sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw== - dependencies: - browserslist "^4.23.0" - caniuse-api "^3.0.0" - -postcss-reduce-transforms@^6.0.2: - version "6.0.2" - resolved "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.2.tgz" - integrity sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-replace-overflow-wrap@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz" - integrity sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw== - -postcss-selector-not@^8.0.1: - version "8.0.1" - resolved "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-8.0.1.tgz" - integrity sha512-kmVy/5PYVb2UOhy0+LqUYAhKj7DUGDpSWa5LZqlkWJaaAV+dxxsOG3+St0yNLu6vsKD7Dmqx+nWQt0iil89+WA== - dependencies: - postcss-selector-parser "^7.0.0" - -postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.16, postcss-selector-parser@^6.1.1, postcss-selector-parser@^6.1.2: - version "6.1.2" - resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz" - integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - -postcss-selector-parser@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz" - integrity sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - -postcss-sort-media-queries@^5.2.0: - version "5.2.0" - resolved "https://registry.npmjs.org/postcss-sort-media-queries/-/postcss-sort-media-queries-5.2.0.tgz" - integrity sha512-AZ5fDMLD8SldlAYlvi8NIqo0+Z8xnXU2ia0jxmuhxAU+Lqt9K+AlmLNJ/zWEnE9x+Zx3qL3+1K20ATgNOr3fAA== - dependencies: - sort-css-media-queries "2.2.0" - -postcss-svgo@^6.0.3: - version "6.0.3" - resolved "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.3.tgz" - integrity sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g== - dependencies: - postcss-value-parser "^4.2.0" - svgo "^3.2.0" - -postcss-unique-selectors@^6.0.4: - version "6.0.4" - resolved "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.4.tgz" - integrity sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg== - dependencies: - postcss-selector-parser "^6.0.16" - -postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" - integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== - -postcss-zindex@^6.0.2: - version "6.0.2" - resolved "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-6.0.2.tgz" - integrity sha512-5BxW9l1evPB/4ZIc+2GobEBoKC+h8gPGCMi+jxsYvd2x0mjq7wazk6DrP71pStqxE9Foxh5TVnonbWpFZzXaYg== - -postcss@^8.4.21, postcss@^8.4.24, postcss@^8.4.26, postcss@^8.4.33, postcss@^8.4.35, postcss@^8.4.38, postcss@^8.4.47: - version "8.5.2" - resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz" - integrity sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA== - dependencies: - nanoid "^3.3.8" - picocolors "^1.1.1" - source-map-js "^1.2.1" - -pretty-error@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz" - integrity sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw== - dependencies: - lodash "^4.17.20" - renderkid "^3.0.0" - -pretty-time@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/pretty-time/-/pretty-time-1.1.0.tgz" - integrity sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA== - -prism-react-renderer@^2.3.0: - version "2.4.1" - resolved "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz" - integrity sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig== - dependencies: - "@types/prismjs" "^1.26.0" - clsx "^2.0.0" - -prismjs@^1.29.0: - version "1.30.0" - resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz" - integrity sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw== - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -prompts@^2.4.2: - version "2.4.2" - resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz" - integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== - dependencies: - kleur "^3.0.3" - sisteransi "^1.0.5" - -prop-types@^15.6.2, prop-types@^15.7.2: - version "15.8.1" - resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" - integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== - dependencies: - loose-envify "^1.4.0" - object-assign "^4.1.1" - react-is "^16.13.1" - -property-information@^6.0.0: - version "6.5.0" - resolved "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz" - integrity sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig== - -proto-list@~1.2.1: - version "1.2.4" - resolved "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz" - integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== - -proxy-addr@~2.0.7: - version "2.0.7" - resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz" - integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== - dependencies: - forwarded "0.2.0" - ipaddr.js "1.9.1" - -punycode@^2.1.0: - version "2.3.1" - resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz" - integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== - -pupa@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/pupa/-/pupa-3.1.0.tgz" - integrity sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug== - dependencies: - escape-goat "^4.0.0" - -qs@6.13.0: - version "6.13.0" - resolved "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz" - integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== - dependencies: - side-channel "^1.0.6" - -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - -queue@6.0.2: - version "6.0.2" - resolved "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz" - integrity sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA== - dependencies: - inherits "~2.0.3" - -quick-lru@^5.1.1: - version "5.1.1" - resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz" - integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== - -randombytes@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -range-parser@1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz" - integrity sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A== - -range-parser@^1.2.1, range-parser@~1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - -raw-body@2.5.2: - version "2.5.2" - resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz" - integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== - dependencies: - bytes "3.1.2" - http-errors "2.0.0" - iconv-lite "0.4.24" - unpipe "1.0.0" - -rc@1.2.8: - version "1.2.8" - resolved "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz" - integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== - dependencies: - deep-extend "^0.6.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - -react-dev-utils@^12.0.1: - version "12.0.1" - resolved "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz" - integrity sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ== - dependencies: - "@babel/code-frame" "^7.16.0" - address "^1.1.2" - browserslist "^4.18.1" - chalk "^4.1.2" - cross-spawn "^7.0.3" - detect-port-alt "^1.1.6" - escape-string-regexp "^4.0.0" - filesize "^8.0.6" - find-up "^5.0.0" - fork-ts-checker-webpack-plugin "^6.5.0" - global-modules "^2.0.0" - globby "^11.0.4" - gzip-size "^6.0.0" - immer "^9.0.7" - is-root "^2.1.0" - loader-utils "^3.2.0" - open "^8.4.0" - pkg-up "^3.1.0" - prompts "^2.4.2" - react-error-overlay "^6.0.11" - recursive-readdir "^2.2.2" - shell-quote "^1.7.3" - strip-ansi "^6.0.1" - text-table "^0.2.0" - -react-dom@^19.0.0: - version "19.0.0" - resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz" - integrity sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ== - dependencies: - scheduler "^0.25.0" - -react-error-overlay@^6.0.11: - version "6.0.11" - resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz" - integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== - -react-fast-compare@^3.2.0: - version "3.2.2" - resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz" - integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ== - -"react-helmet-async@npm:@slorber/react-helmet-async@*", "react-helmet-async@npm:@slorber/react-helmet-async@1.3.0": - version "1.3.0" - resolved "https://registry.npmjs.org/@slorber/react-helmet-async/-/react-helmet-async-1.3.0.tgz" - integrity sha512-e9/OK8VhwUSc67diWI8Rb3I0YgI9/SBQtnhe9aEuK6MhZm7ntZZimXgwXnd8W96YTmSOb9M4d8LwhRZyhWr/1A== - dependencies: - "@babel/runtime" "^7.12.5" - invariant "^2.2.4" - prop-types "^15.7.2" - react-fast-compare "^3.2.0" - shallowequal "^1.1.0" - -react-hot-toast@^2.5.2: - version "2.5.2" - resolved "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.2.tgz" - integrity sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw== - dependencies: - csstype "^3.1.3" - goober "^2.1.16" - -react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0: - version "16.13.1" - resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" - integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== - -react-json-view-lite@^1.2.0: - version "1.5.0" - resolved "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-1.5.0.tgz" - integrity sha512-nWqA1E4jKPklL2jvHWs6s+7Na0qNgw9HCP6xehdQJeg6nPBTFZgGwyko9Q0oj+jQWKTTVRS30u0toM5wiuL3iw== - -react-loadable-ssr-addon-v5-slorber@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.1.tgz" - integrity sha512-lq3Lyw1lGku8zUEJPDxsNm1AfYHBrO9Y1+olAYwpUJ2IGFBskM0DMKok97A6LWUpHm+o7IvQBOWu9MLenp9Z+A== - dependencies: - "@babel/runtime" "^7.10.3" - -"react-loadable@npm:@docusaurus/react-loadable@6.0.0": - version "6.0.0" - resolved "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz" - integrity sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ== - dependencies: - "@types/react" "*" - -react-markdown@^10.1.0: - version "10.1.0" - resolved "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz" - integrity sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ== - dependencies: - "@types/hast" "^3.0.0" - "@types/mdast" "^4.0.0" - devlop "^1.0.0" - hast-util-to-jsx-runtime "^2.0.0" - html-url-attributes "^3.0.0" - mdast-util-to-hast "^13.0.0" - remark-parse "^11.0.0" - remark-rehype "^11.0.0" - unified "^11.0.0" - unist-util-visit "^5.0.0" - vfile "^6.0.0" - -react-router-config@^5.1.1: - version "5.1.1" - resolved "https://registry.npmjs.org/react-router-config/-/react-router-config-5.1.1.tgz" - integrity sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg== - dependencies: - "@babel/runtime" "^7.1.2" - -react-router-dom@^5.3.4: - version "5.3.4" - resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz" - integrity sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ== - dependencies: - "@babel/runtime" "^7.12.13" - history "^4.9.0" - loose-envify "^1.3.1" - prop-types "^15.6.2" - react-router "5.3.4" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - -react-router@5.3.4, react-router@^5.3.4: - version "5.3.4" - resolved "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz" - integrity sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA== - dependencies: - "@babel/runtime" "^7.12.13" - history "^4.9.0" - hoist-non-react-statics "^3.1.0" - loose-envify "^1.3.1" - path-to-regexp "^1.7.0" - prop-types "^15.6.2" - react-is "^16.6.0" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - -react@^19.0.0: - version "19.0.0" - resolved "https://registry.npmjs.org/react/-/react-19.0.0.tgz" - integrity sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ== - -read-cache@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz" - integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA== - dependencies: - pify "^2.3.0" - -readable-stream@^2.0.1: - version "2.3.8" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz" - integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.0.6: - version "3.6.2" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" - integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - -reading-time@^1.5.0: - version "1.5.0" - resolved "https://registry.npmjs.org/reading-time/-/reading-time-1.5.0.tgz" - integrity sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg== - -rechoir@^0.6.2: - version "0.6.2" - resolved "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz" - integrity sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw== - dependencies: - resolve "^1.1.6" - -recma-build-jsx@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz" - integrity sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew== - dependencies: - "@types/estree" "^1.0.0" - estree-util-build-jsx "^3.0.0" - vfile "^6.0.0" - -recma-jsx@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.0.tgz" - integrity sha512-5vwkv65qWwYxg+Atz95acp8DMu1JDSqdGkA2Of1j6rCreyFUE/gp15fC8MnGEuG1W68UKjM6x6+YTWIh7hZM/Q== - dependencies: - acorn-jsx "^5.0.0" - estree-util-to-js "^2.0.0" - recma-parse "^1.0.0" - recma-stringify "^1.0.0" - unified "^11.0.0" - -recma-parse@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz" - integrity sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ== - dependencies: - "@types/estree" "^1.0.0" - esast-util-from-js "^2.0.0" - unified "^11.0.0" - vfile "^6.0.0" - -recma-stringify@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz" - integrity sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g== - dependencies: - "@types/estree" "^1.0.0" - estree-util-to-js "^2.0.0" - unified "^11.0.0" - vfile "^6.0.0" - -recursive-readdir@^2.2.2: - version "2.2.3" - resolved "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz" - integrity sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA== - dependencies: - minimatch "^3.0.5" - -regenerate-unicode-properties@^10.2.0: - version "10.2.0" - resolved "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz" - integrity sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA== - dependencies: - regenerate "^1.4.2" - -regenerate@^1.4.2: - version "1.4.2" - resolved "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz" - integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== - -regenerator-runtime@^0.14.0: - version "0.14.1" - resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz" - integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== - -regenerator-transform@^0.15.2: - version "0.15.2" - resolved "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz" - integrity sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg== - dependencies: - "@babel/runtime" "^7.8.4" - -regexpu-core@^6.2.0: - version "6.2.0" - resolved "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz" - integrity sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA== - dependencies: - regenerate "^1.4.2" - regenerate-unicode-properties "^10.2.0" - regjsgen "^0.8.0" - regjsparser "^0.12.0" - unicode-match-property-ecmascript "^2.0.0" - unicode-match-property-value-ecmascript "^2.1.0" - -registry-auth-token@^5.0.1: - version "5.0.3" - resolved "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.0.3.tgz" - integrity sha512-1bpc9IyC+e+CNFRaWyn77tk4xGG4PPUyfakSmA6F6cvUDjrm58dfyJ3II+9yb10EDkHoy1LaPSmHaWLOH3m6HA== - dependencies: - "@pnpm/npm-conf" "^2.1.0" - -registry-url@^6.0.0: - version "6.0.1" - resolved "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz" - integrity sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q== - dependencies: - rc "1.2.8" - -regjsgen@^0.8.0: - version "0.8.0" - resolved "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz" - integrity sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q== - -regjsparser@^0.12.0: - version "0.12.0" - resolved "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz" - integrity sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ== - dependencies: - jsesc "~3.0.2" - -rehype-raw@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz" - integrity sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww== - dependencies: - "@types/hast" "^3.0.0" - hast-util-raw "^9.0.0" - vfile "^6.0.0" - -rehype-recma@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz" - integrity sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw== - dependencies: - "@types/estree" "^1.0.0" - "@types/hast" "^3.0.0" - hast-util-to-estree "^3.0.0" - -relateurl@^0.2.7: - version "0.2.7" - resolved "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz" - integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog== - -remark-directive@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/remark-directive/-/remark-directive-3.0.1.tgz" - integrity sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A== - dependencies: - "@types/mdast" "^4.0.0" - mdast-util-directive "^3.0.0" - micromark-extension-directive "^3.0.0" - unified "^11.0.0" - -remark-emoji@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/remark-emoji/-/remark-emoji-4.0.1.tgz" - integrity sha512-fHdvsTR1dHkWKev9eNyhTo4EFwbUvJ8ka9SgeWkMPYFX4WoI7ViVBms3PjlQYgw5TLvNQso3GUB/b/8t3yo+dg== - dependencies: - "@types/mdast" "^4.0.2" - emoticon "^4.0.1" - mdast-util-find-and-replace "^3.0.1" - node-emoji "^2.1.0" - unified "^11.0.4" - -remark-frontmatter@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz" - integrity sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ== - dependencies: - "@types/mdast" "^4.0.0" - mdast-util-frontmatter "^2.0.0" - micromark-extension-frontmatter "^2.0.0" - unified "^11.0.0" - -remark-gfm@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.0.tgz" - integrity sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA== - dependencies: - "@types/mdast" "^4.0.0" - mdast-util-gfm "^3.0.0" - micromark-extension-gfm "^3.0.0" - remark-parse "^11.0.0" - remark-stringify "^11.0.0" - unified "^11.0.0" - -remark-mdx@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.0.tgz" - integrity sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA== - dependencies: - mdast-util-mdx "^3.0.0" - micromark-extension-mdxjs "^3.0.0" - -remark-parse@^11.0.0: - version "11.0.0" - resolved "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz" - integrity sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA== - dependencies: - "@types/mdast" "^4.0.0" - mdast-util-from-markdown "^2.0.0" - micromark-util-types "^2.0.0" - unified "^11.0.0" - -remark-rehype@^11.0.0: - version "11.1.1" - resolved "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.1.tgz" - integrity sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ== - dependencies: - "@types/hast" "^3.0.0" - "@types/mdast" "^4.0.0" - mdast-util-to-hast "^13.0.0" - unified "^11.0.0" - vfile "^6.0.0" - -remark-stringify@^11.0.0: - version "11.0.0" - resolved "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz" - integrity sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw== - dependencies: - "@types/mdast" "^4.0.0" - mdast-util-to-markdown "^2.0.0" - unified "^11.0.0" - -renderkid@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz" - integrity sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg== - dependencies: - css-select "^4.1.3" - dom-converter "^0.2.0" - htmlparser2 "^6.1.0" - lodash "^4.17.21" - strip-ansi "^6.0.1" - -repeat-string@^1.0.0: - version "1.6.1" - resolved "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz" - integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== - -require-from-string@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz" - integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== - -"require-like@>= 0.1.1": - version "0.1.2" - resolved "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz" - integrity sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A== - -requires-port@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz" - integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== - -resolve-alpn@^1.2.0: - version "1.2.1" - resolved "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz" - integrity sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g== - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -resolve-pathname@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz" - integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== - -resolve@^1.1.6, resolve@^1.1.7, resolve@^1.14.2, resolve@^1.22.8: - version "1.22.10" - resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz" - integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== - dependencies: - is-core-module "^2.16.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -responselike@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz" - integrity sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg== - dependencies: - lowercase-keys "^3.0.0" - -retry@^0.13.1: - version "0.13.1" - resolved "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz" - integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== - -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -rtlcss@^4.1.0: - version "4.3.0" - resolved "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz" - integrity sha512-FI+pHEn7Wc4NqKXMXFM+VAYKEj/mRIcW4h24YVwVtyjI+EqGrLc2Hx/Ny0lrZ21cBWU2goLy36eqMcNj3AQJig== - dependencies: - escalade "^3.1.1" - picocolors "^1.0.0" - postcss "^8.4.21" - strip-json-comments "^3.1.1" - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - -safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -"safer-buffer@>= 2.1.2 < 3": - version "2.1.2" - resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -sax@^1.2.4: - version "1.4.1" - resolved "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz" - integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== - -scheduler@^0.25.0: - version "0.25.0" - resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz" - integrity sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA== - -schema-utils@2.7.0: - version "2.7.0" - resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz" - integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== - dependencies: - "@types/json-schema" "^7.0.4" - ajv "^6.12.2" - ajv-keywords "^3.4.1" - -schema-utils@^3.0.0, schema-utils@^3.2.0: - version "3.3.0" - resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz" - integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== - dependencies: - "@types/json-schema" "^7.0.8" - ajv "^6.12.5" - ajv-keywords "^3.5.2" - -schema-utils@^4.0.0, schema-utils@^4.0.1, schema-utils@^4.3.0: - version "4.3.0" - resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz" - integrity sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g== - dependencies: - "@types/json-schema" "^7.0.9" - ajv "^8.9.0" - ajv-formats "^2.1.1" - ajv-keywords "^5.1.0" - -section-matter@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz" - integrity sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA== - dependencies: - extend-shallow "^2.0.1" - kind-of "^6.0.0" - -select-hose@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz" - integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== - -selfsigned@^2.1.1: - version "2.4.1" - resolved "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz" - integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== - dependencies: - "@types/node-forge" "^1.3.0" - node-forge "^1" - -semver-diff@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/semver-diff/-/semver-diff-4.0.0.tgz" - integrity sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA== - dependencies: - semver "^7.3.5" - -semver@^6.3.1: - version "6.3.1" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" - integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== - -semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.5.4: - version "7.6.3" - resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz" - integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== - -send@0.19.0: - version "0.19.0" - resolved "https://registry.npmjs.org/send/-/send-0.19.0.tgz" - integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== - dependencies: - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "2.0.0" - mime "1.6.0" - ms "2.1.3" - on-finished "2.4.1" - range-parser "~1.2.1" - statuses "2.0.1" - -serialize-javascript@^6.0.0, serialize-javascript@^6.0.1, serialize-javascript@^6.0.2: - version "6.0.2" - resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz" - integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== - dependencies: - randombytes "^2.1.0" - -serve-handler@^6.1.6: - version "6.1.6" - resolved "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz" - integrity sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ== - dependencies: - bytes "3.0.0" - content-disposition "0.5.2" - mime-types "2.1.18" - minimatch "3.1.2" - path-is-inside "1.0.2" - path-to-regexp "3.3.0" - range-parser "1.2.0" - -serve-index@^1.9.1: - version "1.9.1" - resolved "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz" - integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== - dependencies: - accepts "~1.3.4" - batch "0.6.1" - debug "2.6.9" - escape-html "~1.0.3" - http-errors "~1.6.2" - mime-types "~2.1.17" - parseurl "~1.3.2" - -serve-static@1.16.2: - version "1.16.2" - resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz" - integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== - dependencies: - encodeurl "~2.0.0" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.19.0" - -set-function-length@^1.2.2: - version "1.2.2" - resolved "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz" - integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== - dependencies: - define-data-property "^1.1.4" - es-errors "^1.3.0" - function-bind "^1.1.2" - get-intrinsic "^1.2.4" - gopd "^1.0.1" - has-property-descriptors "^1.0.2" - -setprototypeof@1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz" - integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== - -setprototypeof@1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" - integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== - -shallow-clone@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz" - integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== - dependencies: - kind-of "^6.0.2" - -shallowequal@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz" - integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -shell-quote@^1.7.3, shell-quote@^1.8.1: - version "1.8.2" - resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz" - integrity sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA== - -shelljs@^0.8.5: - version "0.8.5" - resolved "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz" - integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== - dependencies: - glob "^7.0.0" - interpret "^1.0.0" - rechoir "^0.6.2" - -side-channel-list@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz" - integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== - dependencies: - es-errors "^1.3.0" - object-inspect "^1.13.3" - -side-channel-map@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz" - integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== - dependencies: - call-bound "^1.0.2" - es-errors "^1.3.0" - get-intrinsic "^1.2.5" - object-inspect "^1.13.3" - -side-channel-weakmap@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz" - integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== - dependencies: - call-bound "^1.0.2" - es-errors "^1.3.0" - get-intrinsic "^1.2.5" - object-inspect "^1.13.3" - side-channel-map "^1.0.1" - -side-channel@^1.0.6: - version "1.1.0" - resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz" - integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== - dependencies: - es-errors "^1.3.0" - object-inspect "^1.13.3" - side-channel-list "^1.0.0" - side-channel-map "^1.0.1" - side-channel-weakmap "^1.0.2" - -signal-exit@^3.0.2, signal-exit@^3.0.3: - version "3.0.7" - resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" - integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== - -signal-exit@^4.0.1: - version "4.1.0" - resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" - integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== - -sirv@^2.0.3: - version "2.0.4" - resolved "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz" - integrity sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ== - dependencies: - "@polka/url" "^1.0.0-next.24" - mrmime "^2.0.0" - totalist "^3.0.0" - -sisteransi@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz" - integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== - -sitemap@^7.1.1: - version "7.1.2" - resolved "https://registry.npmjs.org/sitemap/-/sitemap-7.1.2.tgz" - integrity sha512-ARCqzHJ0p4gWt+j7NlU5eDlIO9+Rkr/JhPFZKKQ1l5GCus7rJH4UdrlVAh0xC/gDS/Qir2UMxqYNHtsKr2rpCw== - dependencies: - "@types/node" "^17.0.5" - "@types/sax" "^1.2.1" - arg "^5.0.0" - sax "^1.2.4" - -skin-tone@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz" - integrity sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA== - dependencies: - unicode-emoji-modifier-base "^1.0.0" - -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - -slash@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz" - integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== - -snake-case@^3.0.4: - version "3.0.4" - resolved "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz" - integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg== - dependencies: - dot-case "^3.0.4" - tslib "^2.0.3" - -sockjs@^0.3.24: - version "0.3.24" - resolved "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz" - integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== - dependencies: - faye-websocket "^0.11.3" - uuid "^8.3.2" - websocket-driver "^0.7.4" - -sort-css-media-queries@2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/sort-css-media-queries/-/sort-css-media-queries-2.2.0.tgz" - integrity sha512-0xtkGhWCC9MGt/EzgnvbbbKhqWjl1+/rncmhTh5qCpbYguXh6S/qwePfv/JQ8jePXXmqingylxoC49pCkSPIbA== - -source-map-js@^1.0.1, source-map-js@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz" - integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== - -source-map-support@~0.5.20: - version "0.5.21" - resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@^0.6.0, source-map@~0.6.0: - version "0.6.1" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -source-map@^0.7.0: - version "0.7.4" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz" - integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== - -space-separated-tokens@^2.0.0: - version "2.0.2" - resolved "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz" - integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== - -spdy-transport@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz" - integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== - dependencies: - debug "^4.1.0" - detect-node "^2.0.4" - hpack.js "^2.1.6" - obuf "^1.1.2" - readable-stream "^3.0.6" - wbuf "^1.7.3" - -spdy@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz" - integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== - dependencies: - debug "^4.1.0" - handle-thing "^2.0.0" - http-deceiver "^1.2.7" - select-hose "^2.0.0" - spdy-transport "^3.0.0" - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" - integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== - -srcset@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/srcset/-/srcset-4.0.0.tgz" - integrity sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw== - -statuses@2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" - integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== - -"statuses@>= 1.4.0 < 2": - version "1.5.0" - resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" - integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== - -std-env@^3.7.0: - version "3.8.0" - resolved "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz" - integrity sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w== - -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0: - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^5.0.1, string-width@^5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz" - integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== - dependencies: - eastasianwidth "^0.2.0" - emoji-regex "^9.2.2" - strip-ansi "^7.0.1" - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -stringify-entities@^4.0.0: - version "4.0.4" - resolved "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz" - integrity sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg== - dependencies: - character-entities-html4 "^2.0.0" - character-entities-legacy "^3.0.0" - -stringify-object@^3.3.0: - version "3.3.0" - resolved "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz" - integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== - dependencies: - get-own-enumerable-property-symbols "^3.0.0" - is-obj "^1.0.1" - is-regexp "^1.0.0" - -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^7.0.1: - version "7.1.0" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" - integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== - dependencies: - ansi-regex "^6.0.1" - -strip-bom-string@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz" - integrity sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g== - -strip-final-newline@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz" - integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== - -strip-json-comments@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" - integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== - -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz" - integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== - -style-to-object@^1.0.0: - version "1.0.8" - resolved "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz" - integrity sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g== - dependencies: - inline-style-parser "0.2.4" - -stylehacks@^6.1.1: - version "6.1.1" - resolved "https://registry.npmjs.org/stylehacks/-/stylehacks-6.1.1.tgz" - integrity sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg== - dependencies: - browserslist "^4.23.0" - postcss-selector-parser "^6.0.16" - -sucrase@^3.35.0: - version "3.35.0" - resolved "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz" - integrity sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA== - dependencies: - "@jridgewell/gen-mapping" "^0.3.2" - commander "^4.0.0" - glob "^10.3.10" - lines-and-columns "^1.1.6" - mz "^2.7.0" - pirates "^4.0.1" - ts-interface-checker "^0.1.9" - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -supports-color@^8.0.0: - version "8.1.1" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - -supports-preserve-symlinks-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" - integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== - -svg-parser@^2.0.4: - version "2.0.4" - resolved "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz" - integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== - -svgo@^3.0.2, svgo@^3.2.0: - version "3.3.2" - resolved "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz" - integrity sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw== - dependencies: - "@trysound/sax" "0.2.0" - commander "^7.2.0" - css-select "^5.1.0" - css-tree "^2.3.1" - css-what "^6.1.0" - csso "^5.0.5" - picocolors "^1.0.0" - -swiper@^11.2.6: - version "11.2.6" - resolved "https://registry.npmjs.org/swiper/-/swiper-11.2.6.tgz" - integrity sha512-8aXpYKtjy3DjcbzZfz+/OX/GhcU5h+looA6PbAzHMZT6ESSycSp9nAjPCenczgJyslV+rUGse64LMGpWE3PX9Q== - -tailwind-merge@^3.0.2: - version "3.2.0" - resolved "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.2.0.tgz" - integrity sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA== - -tailwindcss@^3.4.1: - version "3.4.17" - resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz" - integrity sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og== - dependencies: - "@alloc/quick-lru" "^5.2.0" - arg "^5.0.2" - chokidar "^3.6.0" - didyoumean "^1.2.2" - dlv "^1.1.3" - fast-glob "^3.3.2" - glob-parent "^6.0.2" - is-glob "^4.0.3" - jiti "^1.21.6" - lilconfig "^3.1.3" - micromatch "^4.0.8" - normalize-path "^3.0.0" - object-hash "^3.0.0" - picocolors "^1.1.1" - postcss "^8.4.47" - postcss-import "^15.1.0" - postcss-js "^4.0.1" - postcss-load-config "^4.0.2" - postcss-nested "^6.2.0" - postcss-selector-parser "^6.1.2" - resolve "^1.22.8" - sucrase "^3.35.0" - -tapable@^1.0.0: - version "1.1.3" - resolved "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz" - integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== - -tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: - version "2.2.1" - resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz" - integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== - -terser-webpack-plugin@^5.3.10, terser-webpack-plugin@^5.3.9: - version "5.3.11" - resolved "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz" - integrity sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ== - dependencies: - "@jridgewell/trace-mapping" "^0.3.25" - jest-worker "^27.4.5" - schema-utils "^4.3.0" - serialize-javascript "^6.0.2" - terser "^5.31.1" - -terser@^5.10.0, terser@^5.15.1, terser@^5.31.1: - version "5.37.0" - resolved "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz" - integrity sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA== - dependencies: - "@jridgewell/source-map" "^0.3.3" - acorn "^8.8.2" - commander "^2.20.0" - source-map-support "~0.5.20" - -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" - integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== - -thenify-all@^1.0.0: - version "1.6.0" - resolved "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz" - integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== - dependencies: - thenify ">= 3.1.0 < 4" - -"thenify@>= 3.1.0 < 4": - version "3.3.1" - resolved "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz" - integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== - dependencies: - any-promise "^1.0.0" - -thunky@^1.0.2: - version "1.1.0" - resolved "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz" - integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== - -tiny-invariant@^1.0.2: - version "1.3.3" - resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz" - integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== - -tiny-warning@^1.0.0: - version "1.0.3" - resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz" - integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -toidentifier@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" - integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== - -totalist@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz" - integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ== - -trim-lines@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz" - integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== - -trough@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz" - integrity sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw== - -ts-interface-checker@^0.1.9: - version "0.1.13" - resolved "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz" - integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== - -tslib@^2.0.3, tslib@^2.4.0, tslib@^2.6.0: - version "2.8.1" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" - integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== - -turndown@^7.2.1: - version "7.2.1" - resolved "https://registry.yarnpkg.com/turndown/-/turndown-7.2.1.tgz#633ff4ff88951fe1db58f7dd32a25ba3f6ff2c48" - integrity sha512-7YiPJw6rLClQL3oUKN3KgMaXeJJ2lAyZItclgKDurqnH61so4k4IH/qwmMva0zpuJc/FhRExBBnk7EbeFANlgQ== - dependencies: - "@mixmark-io/domino" "^2.2.0" - -type-fest@^0.21.3: - version "0.21.3" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz" - integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== - -type-fest@^1.0.1: - version "1.4.0" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz" - integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== - -type-fest@^2.13.0, type-fest@^2.5.0: - version "2.19.0" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz" - integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== - -type-is@~1.6.18: - version "1.6.18" - resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" - integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== - dependencies: - media-typer "0.3.0" - mime-types "~2.1.24" - -typedarray-to-buffer@^3.1.5: - version "3.1.5" - resolved "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz" - integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== - dependencies: - is-typedarray "^1.0.0" - -typescript@~5.6.2: - version "5.6.3" - resolved "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz" - integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw== - -undici-types@~6.20.0: - version "6.20.0" - resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz" - integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== - -unicode-canonical-property-names-ecmascript@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz" - integrity sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg== - -unicode-emoji-modifier-base@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz" - integrity sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g== - -unicode-match-property-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz" - integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== - dependencies: - unicode-canonical-property-names-ecmascript "^2.0.0" - unicode-property-aliases-ecmascript "^2.0.0" - -unicode-match-property-value-ecmascript@^2.1.0: - version "2.2.0" - resolved "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz" - integrity sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg== - -unicode-property-aliases-ecmascript@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz" - integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== - -unified@^11.0.0, unified@^11.0.3, unified@^11.0.4: - version "11.0.5" - resolved "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz" - integrity sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA== - dependencies: - "@types/unist" "^3.0.0" - bail "^2.0.0" - devlop "^1.0.0" - extend "^3.0.0" - is-plain-obj "^4.0.0" - trough "^2.0.0" - vfile "^6.0.0" - -unique-string@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz" - integrity sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ== - dependencies: - crypto-random-string "^4.0.0" - -unist-util-is@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz" - integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw== - dependencies: - "@types/unist" "^3.0.0" - -unist-util-position-from-estree@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz" - integrity sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ== - dependencies: - "@types/unist" "^3.0.0" - -unist-util-position@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz" - integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA== - dependencies: - "@types/unist" "^3.0.0" - -unist-util-stringify-position@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz" - integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ== - dependencies: - "@types/unist" "^3.0.0" - -unist-util-visit-parents@^6.0.0: - version "6.0.1" - resolved "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz" - integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw== - dependencies: - "@types/unist" "^3.0.0" - unist-util-is "^6.0.0" - -unist-util-visit@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz" - integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg== - dependencies: - "@types/unist" "^3.0.0" - unist-util-is "^6.0.0" - unist-util-visit-parents "^6.0.0" - -universalify@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz" - integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== - -unpipe@1.0.0, unpipe@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" - integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== - -update-browserslist-db@^1.1.1: - version "1.1.2" - resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz" - integrity sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg== - dependencies: - escalade "^3.2.0" - picocolors "^1.1.1" - -update-notifier@^6.0.2: - version "6.0.2" - resolved "https://registry.npmjs.org/update-notifier/-/update-notifier-6.0.2.tgz" - integrity sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og== - dependencies: - boxen "^7.0.0" - chalk "^5.0.1" - configstore "^6.0.0" - has-yarn "^3.0.0" - import-lazy "^4.0.0" - is-ci "^3.0.1" - is-installed-globally "^0.4.0" - is-npm "^6.0.0" - is-yarn-global "^0.4.0" - latest-version "^7.0.0" - pupa "^3.1.0" - semver "^7.3.7" - semver-diff "^4.0.0" - xdg-basedir "^5.1.0" - -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - -url-loader@^4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz" - integrity sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA== - dependencies: - loader-utils "^2.0.0" - mime-types "^2.1.27" - schema-utils "^3.0.0" - -util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" - integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== - -utila@~0.4: - version "0.4.0" - resolved "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz" - integrity sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA== - -utility-types@^3.10.0: - version "3.11.0" - resolved "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz" - integrity sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw== - -utils-merge@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" - integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== - -uuid@^8.3.2: - version "8.3.2" - resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - -value-equal@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz" - integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== - -vary@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" - integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== - -vfile-location@^5.0.0: - version "5.0.3" - resolved "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz" - integrity sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg== - dependencies: - "@types/unist" "^3.0.0" - vfile "^6.0.0" - -vfile-message@^4.0.0: - version "4.0.2" - resolved "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz" - integrity sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw== - dependencies: - "@types/unist" "^3.0.0" - unist-util-stringify-position "^4.0.0" - -vfile@^6.0.0, vfile@^6.0.1: - version "6.0.3" - resolved "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz" - integrity sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q== - dependencies: - "@types/unist" "^3.0.0" - vfile-message "^4.0.0" - -watchpack@^2.4.1: - version "2.4.2" - resolved "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz" - integrity sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw== - dependencies: - glob-to-regexp "^0.4.1" - graceful-fs "^4.1.2" - -wbuf@^1.1.0, wbuf@^1.7.3: - version "1.7.3" - resolved "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz" - integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== - dependencies: - minimalistic-assert "^1.0.0" - -web-namespaces@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz" - integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== - -webpack-bundle-analyzer@^4.10.2: - version "4.10.2" - resolved "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz" - integrity sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw== - dependencies: - "@discoveryjs/json-ext" "0.5.7" - acorn "^8.0.4" - acorn-walk "^8.0.0" - commander "^7.2.0" - debounce "^1.2.1" - escape-string-regexp "^4.0.0" - gzip-size "^6.0.0" - html-escaper "^2.0.2" - opener "^1.5.2" - picocolors "^1.0.0" - sirv "^2.0.3" - ws "^7.3.1" - -webpack-dev-middleware@^5.3.4: - version "5.3.4" - resolved "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz" - integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q== - dependencies: - colorette "^2.0.10" - memfs "^3.4.3" - mime-types "^2.1.31" - range-parser "^1.2.1" - schema-utils "^4.0.0" - -webpack-dev-server@^4.15.2: - version "4.15.2" - resolved "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz" - integrity sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g== - dependencies: - "@types/bonjour" "^3.5.9" - "@types/connect-history-api-fallback" "^1.3.5" - "@types/express" "^4.17.13" - "@types/serve-index" "^1.9.1" - "@types/serve-static" "^1.13.10" - "@types/sockjs" "^0.3.33" - "@types/ws" "^8.5.5" - ansi-html-community "^0.0.8" - bonjour-service "^1.0.11" - chokidar "^3.5.3" - colorette "^2.0.10" - compression "^1.7.4" - connect-history-api-fallback "^2.0.0" - default-gateway "^6.0.3" - express "^4.17.3" - graceful-fs "^4.2.6" - html-entities "^2.3.2" - http-proxy-middleware "^2.0.3" - ipaddr.js "^2.0.1" - launch-editor "^2.6.0" - open "^8.0.9" - p-retry "^4.5.0" - rimraf "^3.0.2" - schema-utils "^4.0.0" - selfsigned "^2.1.1" - serve-index "^1.9.1" - sockjs "^0.3.24" - spdy "^4.0.2" - webpack-dev-middleware "^5.3.4" - ws "^8.13.0" - -webpack-merge@^5.9.0: - version "5.10.0" - resolved "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz" - integrity sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA== - dependencies: - clone-deep "^4.0.1" - flat "^5.0.2" - wildcard "^2.0.0" - -webpack-merge@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz" - integrity sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg== - dependencies: - clone-deep "^4.0.1" - flat "^5.0.2" - wildcard "^2.0.1" - -webpack-sources@^3.2.3: - version "3.2.3" - resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz" - integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== - -webpack@^5.88.1, webpack@^5.95.0: - version "5.97.1" - resolved "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz" - integrity sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg== - dependencies: - "@types/eslint-scope" "^3.7.7" - "@types/estree" "^1.0.6" - "@webassemblyjs/ast" "^1.14.1" - "@webassemblyjs/wasm-edit" "^1.14.1" - "@webassemblyjs/wasm-parser" "^1.14.1" - acorn "^8.14.0" - browserslist "^4.24.0" - chrome-trace-event "^1.0.2" - enhanced-resolve "^5.17.1" - es-module-lexer "^1.2.1" - eslint-scope "5.1.1" - events "^3.2.0" - glob-to-regexp "^0.4.1" - graceful-fs "^4.2.11" - json-parse-even-better-errors "^2.3.1" - loader-runner "^4.2.0" - mime-types "^2.1.27" - neo-async "^2.6.2" - schema-utils "^3.2.0" - tapable "^2.1.1" - terser-webpack-plugin "^5.3.10" - watchpack "^2.4.1" - webpack-sources "^3.2.3" - -webpackbar@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/webpackbar/-/webpackbar-6.0.1.tgz" - integrity sha512-TnErZpmuKdwWBdMoexjio3KKX6ZtoKHRVvLIU0A47R0VVBDtx3ZyOJDktgYixhoJokZTYTt1Z37OkO9pnGJa9Q== - dependencies: - ansi-escapes "^4.3.2" - chalk "^4.1.2" - consola "^3.2.3" - figures "^3.2.0" - markdown-table "^2.0.0" - pretty-time "^1.1.0" - std-env "^3.7.0" - wrap-ansi "^7.0.0" - -websocket-driver@>=0.5.1, websocket-driver@^0.7.4: - version "0.7.4" - resolved "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz" - integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== - dependencies: - http-parser-js ">=0.5.1" - safe-buffer ">=5.1.0" - websocket-extensions ">=0.1.1" - -websocket-extensions@>=0.1.1: - version "0.1.4" - resolved "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz" - integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== - -which@^1.3.1: - version "1.3.1" - resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -which@^2.0.1: - version "2.0.2" - resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -widest-line@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz" - integrity sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig== - dependencies: - string-width "^5.0.1" - -wildcard@^2.0.0, wildcard@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz" - integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== - -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: - version "8.1.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" - integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== - dependencies: - ansi-styles "^6.1.0" - string-width "^5.0.1" - strip-ansi "^7.0.1" - -wrappy@1: - version "1.0.2" - resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - -write-file-atomic@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz" - integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== - dependencies: - imurmurhash "^0.1.4" - is-typedarray "^1.0.0" - signal-exit "^3.0.2" - typedarray-to-buffer "^3.1.5" - -ws@^7.3.1: - version "7.5.10" - resolved "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz" - integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== - -ws@^8.13.0: - version "8.18.0" - resolved "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz" - integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== - -xdg-basedir@^5.0.1, xdg-basedir@^5.1.0: - version "5.1.0" - resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz" - integrity sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ== - -xml-js@^1.6.11: - version "1.6.11" - resolved "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz" - integrity sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g== - dependencies: - sax "^1.2.4" - -yallist@^3.0.2: - version "3.1.1" - resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== - -yaml-loader@^0.8.1: - version "0.8.1" - resolved "https://registry.npmjs.org/yaml-loader/-/yaml-loader-0.8.1.tgz" - integrity sha512-BCEndnUoi3BaZmePkwGGe93txRxLgMhBa/gE725v1/GHnura8QvNs7c4+4C1yyhhKoj3Dg63M7IqhA++15j6ww== - dependencies: - javascript-stringify "^2.0.1" - loader-utils "^2.0.0" - yaml "^2.0.0" - -yaml@^1.7.2: - version "1.10.2" - resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" - integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== - -yaml@^2.0.0, yaml@^2.3.4: - version "2.7.0" - resolved "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz" - integrity sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA== - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== - -yocto-queue@^1.0.0: - version "1.1.1" - resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz" - integrity sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g== - -zwitch@^2.0.0: - version "2.0.4" - resolved "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz" - integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A== diff --git a/download_cli.sh b/download_cli.sh index 2693f0409b19..572563ded58a 100755 --- a/download_cli.sh +++ b/download_cli.sh @@ -88,26 +88,52 @@ else fi # --- 3) Detect OS/Architecture --- -# Better OS detection for Windows environments -if [[ "${WINDIR:-}" ]] || [[ "${windir:-}" ]] || [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then - OS="windows" -elif [[ -f "/proc/version" ]] && grep -q "Microsoft\|WSL" /proc/version 2>/dev/null; then - # WSL detection +# Allow explicit override for automation or when auto-detection is wrong: +# INSTALL_OS=linux|windows|darwin +if [ -n "${INSTALL_OS:-}" ]; then + case "${INSTALL_OS}" in + linux|windows|darwin) OS="${INSTALL_OS}" ;; + *) echo "[error]: unsupported INSTALL_OS='${INSTALL_OS}' (expected: linux|windows|darwin)"; exit 1 ;; + esac +else + # Better OS detection for Windows environments, with safer WSL handling. + # If explicit Windows-like shells/variables are present (MSYS/Cygwin), treat as windows. + if [[ "${WINDIR:-}" ]] || [[ "${windir:-}" ]] || [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then OS="windows" -elif [[ "$PWD" =~ ^/mnt/[a-zA-Z]/ ]]; then - # WSL mount point detection (like /mnt/c/) + elif [[ -f "/proc/version" ]] && grep -q "Microsoft\|WSL" /proc/version 2>/dev/null; then + # WSL detected. Prefer Linux unless there are clear signs we should install the Windows build: + # - running on a Windows-mounted path like /mnt/c/... OR + # - Windows executables are available AND we're on a Windows mount + if [[ "$PWD" =~ ^/mnt/[a-zA-Z]/ ]]; then + OS="windows" + else + # If powershell/cmd exist, only treat as Windows when in a Windows mount + if command -v powershell.exe >/dev/null 2>&1 || command -v cmd.exe >/dev/null 2>&1; then + if [[ "$PWD" =~ ^/mnt/[a-zA-Z]/ ]] || [[ -d "/c" || -d "/d" || -d "/e" ]]; then + OS="windows" + else + OS="linux" + fi + else + # No strong Windows interop present — install Linux build inside WSL by default + OS="linux" + fi + fi + elif [[ "$PWD" =~ ^/mnt/[a-zA-Z]/ ]]; then + # WSL mount point detection (like /mnt/c/) outside of /proc/version check OS="windows" -elif [[ "$OSTYPE" == "darwin"* ]]; then + elif [[ "$OSTYPE" == "darwin"* ]]; then OS="darwin" -elif command -v powershell.exe >/dev/null 2>&1 || command -v cmd.exe >/dev/null 2>&1; then - # Check if Windows executables are available (another Windows indicator) + elif command -v powershell.exe >/dev/null 2>&1 || command -v cmd.exe >/dev/null 2>&1; then + # Presence of Windows executables (likely a Windows environment) OS="windows" -elif [[ "$PWD" =~ ^/[a-zA-Z]/ ]] && [[ -d "/c" || -d "/d" || -d "/e" ]]; then + elif [[ "$PWD" =~ ^/[a-zA-Z]/ ]] && [[ -d "/c" || -d "/d" || -d "/e" ]]; then # Check for Windows-style mount points (like in Git Bash) OS="windows" -else + else # Fallback to uname for other systems OS=$(uname -s | tr '[:upper:]' '[:lower:]') + fi fi ARCH=$(uname -m) diff --git a/openapi.json b/openapi.json deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/scripts/test-subrecipes-examples/project_analyzer.yaml b/scripts/test-subrecipes-examples/project_analyzer.yaml new file mode 100644 index 000000000000..dae395347b0e --- /dev/null +++ b/scripts/test-subrecipes-examples/project_analyzer.yaml @@ -0,0 +1,36 @@ +version: 1.0.0 +title: Project Analyzer +description: Analyze project codebase using parallel subrecipes for file statistics and code patterns +instructions: You are a code analysis assistant that examines project structure and code patterns. +parameters: + - key: target_directory + input_type: string + requirement: optional + default: "." + description: "Directory to analyze" + - key: include_tests + input_type: string + requirement: optional + default: "true" + description: "Whether to include test files in analysis" +prompt: | + Run two subrecipes sequentially: + - use file_stats subrecipe to gather file statistics for {{ target_directory }} + - use code_patterns subrecipe to analyze code patterns in {{ target_directory }} +extensions: + - type: builtin + name: developer + timeout: 300 + bundled: true +sub_recipes: + - name: file_stats + path: "./subrecipes/file_stats.yaml" + values: + directory: "{{ target_directory }}" + fast_mode: "true" + - name: code_patterns + path: "./subrecipes/code_patterns.yaml" + values: + directory: "{{ target_directory }}" + include_tests: "{{ include_tests }}" + fast_mode: "true" diff --git a/scripts/test-subrecipes-examples/project_analyzer_parallel.yaml b/scripts/test-subrecipes-examples/project_analyzer_parallel.yaml new file mode 100644 index 000000000000..7ea4c3a27ae5 --- /dev/null +++ b/scripts/test-subrecipes-examples/project_analyzer_parallel.yaml @@ -0,0 +1,36 @@ +version: 1.0.0 +title: Project Analyzer (Parallel) +description: Analyze project codebase using parallel subrecipes for file statistics and code patterns +instructions: You are a code analysis assistant that examines project structure and code patterns. +parameters: + - key: target_directory + input_type: string + requirement: optional + default: "." + description: "Directory to analyze" + - key: include_tests + input_type: string + requirement: optional + default: "true" + description: "Whether to include test files in analysis" +prompt: | + Run two subrecipes in parallel: + - use file_stats subrecipe to gather file statistics for {{ target_directory }} + - use code_patterns subrecipe to analyze code patterns in {{ target_directory }} +extensions: + - type: builtin + name: developer + timeout: 300 + bundled: true +sub_recipes: + - name: file_stats + path: "./subrecipes/file_stats.yaml" + values: + directory: "{{ target_directory }}" + fast_mode: "true" + - name: code_patterns + path: "./subrecipes/code_patterns.yaml" + values: + directory: "{{ target_directory }}" + include_tests: "{{ include_tests }}" + fast_mode: "true" diff --git a/scripts/test-subrecipes-examples/subrecipes/activity-suggestions.yaml b/scripts/test-subrecipes-examples/subrecipes/activity-suggestions.yaml deleted file mode 100644 index 9a92b1a1a715..000000000000 --- a/scripts/test-subrecipes-examples/subrecipes/activity-suggestions.yaml +++ /dev/null @@ -1,23 +0,0 @@ -version: "1.0.0" -title: "Activity Recommender" -description: "Suggest activities based on weather conditions" -instructions: | - You are a travel expert. Recommend appropriate activities and attractions - based on current weather conditions. - -parameters: - - key: weather_conditions - input_type: string - requirement: required - description: "Current weather conditions to base recommendations on" - -extensions: - - type: builtin - name: developer - timeout: 300 - bundled: true - -prompt: | - Based on these weather conditions: {{ weather_conditions }}, - suggest appropriate activities, attractions, and travel tips. - Include both indoor and outdoor options as relevant. diff --git a/scripts/test-subrecipes-examples/subrecipes/code_patterns.yaml b/scripts/test-subrecipes-examples/subrecipes/code_patterns.yaml new file mode 100644 index 000000000000..8c961def34f3 --- /dev/null +++ b/scripts/test-subrecipes-examples/subrecipes/code_patterns.yaml @@ -0,0 +1,62 @@ +version: 1.0.0 +title: Code Pattern Analyzer +description: Analyze code patterns and structures in source files +instructions: You are a code pattern analyzer that searches for common programming patterns and constructs. +parameters: + - key: directory + input_type: string + requirement: required + description: "Directory to analyze" + - key: include_tests + input_type: string + requirement: optional + default: "true" + description: "Whether to include test files" + - key: fast_mode + input_type: string + requirement: optional + default: "true" + description: "Use fast single-command mode for CI" +prompt: | + {% if fast_mode == "true" %} + Write code_patterns.json with a single shell command: + + cat > code_patterns.json << 'JSON' + {"todos":[{"file":"README.md","line":3,"text":"Add more tests"}],"function_count":2,"import_count":0,"test_count":2,"files_analyzed":3} + JSON + + Use a single tool call. Do not scan the filesystem. + {% else %} + Analyze source code files in {{ directory }} and identify patterns: + + 1. Find all TODO/FIXME comments + 2. Count function/method definitions (look for 'fn ', 'def ', 'function ') + 3. Find import/use statements + 4. Identify test functions (names containing 'test') + + Write your findings to a file named code_patterns.json with this structure: + ```json + { + "todos": [ + {"file": "", "line": , "text": ""} + ], + "function_count": , + "import_count": , + "test_count": , + "files_analyzed": + } + ``` + + Use grep and other shell tools to find these patterns efficiently. + Only analyze source code files (.rs, .py, .js, .go, etc.), skip binaries and build artifacts. + {% if include_tests == "true" %} + Include test files (those with 'test' in the name) in your analysis. + {% else %} + Exclude test files from your analysis. + {% endif %} + {% endif %} +extensions: + - type: builtin + name: developer + timeout: 300 + bundled: true diff --git a/scripts/test-subrecipes-examples/subrecipes/file_stats.yaml b/scripts/test-subrecipes-examples/subrecipes/file_stats.yaml new file mode 100644 index 000000000000..d5633c71ae16 --- /dev/null +++ b/scripts/test-subrecipes-examples/subrecipes/file_stats.yaml @@ -0,0 +1,60 @@ +version: 1.0.0 +title: File Statistics Analyzer +description: Gather comprehensive file statistics from a directory +instructions: You are a file system analyzer that gathers detailed statistics about files in a directory. +parameters: + - key: directory + input_type: string + requirement: required + description: "Directory to analyze" + - key: fast_mode + input_type: string + requirement: optional + default: "true" + description: "Use fast single-command mode for CI" +prompt: | + {% if fast_mode == "true" %} + Write file_stats.json with a single shell command: + + cat > file_stats.json << 'JSON' + {"total_files":3,"files_by_extension":{".rs":1,".py":1,".md":1},"total_lines":50,"total_size_bytes":1500,"largest_file":{"path":"sample.rs","size_bytes":600},"smallest_file":{"path":"README.md","size_bytes":200}} + JSON + + Use a single tool call. Do not scan the filesystem. + {% else %} + Analyze {{ directory }} and generate file statistics: + + 1. Count total files by extension (.rs, .yaml, .md, .toml, etc.) + 2. Calculate total lines of code across all text files + 3. Determine total file sizes + 4. Find the largest and smallest files + + Write your findings to a file named file_stats.json with this structure: + ```json + { + "total_files": , + "files_by_extension": { + ".rs": , + ".yaml": , + ... + }, + "total_lines": , + "total_size_bytes": , + "largest_file": { + "path": "", + "size_bytes": + }, + "smallest_file": { + "path": "", + "size_bytes": + } + } + ``` + + Use shell commands like find, wc, and du to gather this data efficiently. + {% endif %} +extensions: + - type: builtin + name: developer + timeout: 300 + bundled: true diff --git a/scripts/test-subrecipes-examples/subrecipes/weather-data.yaml b/scripts/test-subrecipes-examples/subrecipes/weather-data.yaml deleted file mode 100644 index 6efb9ce510c7..000000000000 --- a/scripts/test-subrecipes-examples/subrecipes/weather-data.yaml +++ /dev/null @@ -1,23 +0,0 @@ -version: "1.0.0" -title: "Weather Data Collector" -description: "Fetch current weather conditions for a location" -instructions: | - You are a weather data specialist. Gather current weather information - including temperature, conditions, and seasonal context. - -parameters: - - key: location - input_type: string - requirement: required - description: "City or location to get weather data for" - -extensions: - - type: builtin - name: developer - timeout: 300 - bundled: true - -prompt: | - Get the current weather conditions for {{ location }}. - Include temperature, weather conditions (sunny, rainy, etc.), - and any relevant seasonal information. diff --git a/scripts/test-subrecipes-examples/travel_planner.yaml b/scripts/test-subrecipes-examples/travel_planner.yaml deleted file mode 100644 index 1f48b11878f4..000000000000 --- a/scripts/test-subrecipes-examples/travel_planner.yaml +++ /dev/null @@ -1,23 +0,0 @@ -version: "1.0.0" -title: "Travel Activity Planner" -description: "Get weather data and suggest appropriate activities" -instructions: | - Plan activities by first getting weather data, then suggesting activities based on conditions. - -prompt: | - Plan activities for Sydney by first getting weather data, then suggesting activities based on the weather conditions we receive. - -sub_recipes: - - name: weather_data - path: "{{ recipe_dir }}/subrecipes/weather-data.yaml" - # No values - location parameter comes from prompt context - - - name: activity_suggestions - path: "{{ recipe_dir }}/subrecipes/activity-suggestions.yaml" - # weather_conditions parameter comes from conversation context - -extensions: - - type: builtin - name: developer - timeout: 300 - bundled: true diff --git a/scripts/test-subrecipes-examples/travel_planner_parallel.yaml b/scripts/test-subrecipes-examples/travel_planner_parallel.yaml deleted file mode 100644 index bb1ee1a03717..000000000000 --- a/scripts/test-subrecipes-examples/travel_planner_parallel.yaml +++ /dev/null @@ -1,23 +0,0 @@ -version: "1.0.0" -title: "Travel Activity Planner (Parallel)" -description: "Get weather data and suggest activities in parallel" -instructions: | - Plan activities by getting weather data and activity suggestions in parallel to save time. - -prompt: | - Run the following subrecipes in parallel to plan activities for Sydney: - - use weather_data subrecipe to get the weather for Sydney - - use activity_suggestions subrecipe to suggest activities for overcast, cool weather - -sub_recipes: - - name: weather_data - path: "{{ recipe_dir }}/subrecipes/weather-data.yaml" - - - name: activity_suggestions - path: "{{ recipe_dir }}/subrecipes/activity-suggestions.yaml" - -extensions: - - type: builtin - name: developer - timeout: 300 - bundled: true diff --git a/scripts/test_compaction.sh b/scripts/test_compaction.sh new file mode 100755 index 000000000000..f3bf6d430c76 --- /dev/null +++ b/scripts/test_compaction.sh @@ -0,0 +1,242 @@ +#!/bin/bash + +# Compaction smoke test script +# Tests both manual (trigger prompt) and auto compaction (threshold-based) + +if [ -f .env ]; then + export $(grep -v '^#' .env | xargs) +fi + +if [ -z "$SKIP_BUILD" ]; then + echo "Building goose..." + cargo build --release --bin goose + echo "" +else + echo "Skipping build (SKIP_BUILD is set)..." + echo "" +fi + +SCRIPT_DIR=$(pwd) +GOOSE_BIN="$SCRIPT_DIR/target/release/goose" + +# Validation function to check compaction structure in session JSON +validate_compaction() { + local session_id=$1 + local test_name=$2 + + echo "Validating compaction structure for session: $session_id" + + # Export the session to JSON + local session_json=$($GOOSE_BIN session export --format json --session-id "$session_id" 2>&1) + + if [ $? -ne 0 ]; then + echo "✗ FAILED: Could not export session JSON" + echo " Error: $session_json" + return 1 + fi + + if ! command -v jq &> /dev/null; then + echo "⚠ WARNING: jq not available, cannot validate compaction structure" + return 0 + fi + + # Check basic structure + echo "$session_json" | jq -e '.conversation' > /dev/null 2>&1 + if [ $? -ne 0 ]; then + echo "✗ FAILED: Session JSON missing 'conversation' field" + return 1 + fi + + local message_count=$(echo "$session_json" | jq '.conversation | length' 2>/dev/null) + echo " Session has $message_count messages" + + # Look for a summary message (assistant role with userVisible=false, agentVisible=true) + local has_summary=$(echo "$session_json" | jq '[.conversation[] | select(.role == "assistant" and .metadata.userVisible == false and .metadata.agentVisible == true)] | length > 0' 2>/dev/null) + + if [ "$has_summary" != "true" ]; then + echo "✗ FAILED: No summary message found (expected assistant message with userVisible=false, agentVisible=true)" + return 1 + fi + echo "✓ Found summary message with correct visibility flags" + + # Check for original messages with userVisible=true, agentVisible=false + local has_hidden_originals=$(echo "$session_json" | jq '[.conversation[] | select(.metadata.userVisible == true and .metadata.agentVisible == false)] | length > 0' 2>/dev/null) + + if [ "$has_hidden_originals" != "true" ]; then + echo "⚠ WARNING: No original messages found with userVisible=true, agentVisible=false" + echo " This might be OK if all messages were compacted" + else + echo "✓ Found original messages hidden from agent (userVisible=true, agentVisible=false)" + fi + + # For auto-compaction, check for the preserved user message (userVisible=true, agentVisible=true) + local has_preserved_user=$(echo "$session_json" | jq '[.conversation[] | select(.role == "user" and .metadata.userVisible == true and .metadata.agentVisible == true)] | length > 0' 2>/dev/null) + + if [ "$has_preserved_user" == "true" ]; then + echo "✓ Found preserved user message (userVisible=true, agentVisible=true)" + fi + + echo "✓ SUCCESS: Compaction structure is valid for $test_name" + return 0 +} + +echo "==================================================" +echo "COMPACTION SMOKE TESTS" +echo "==================================================" +echo "" + +# Check if jq is available +if ! command -v jq &> /dev/null; then + echo "⚠ WARNING: jq is not installed. Compaction structure validation will be limited." + echo " Install jq to enable full validation: brew install jq (macOS) or apt-get install jq (Linux)" + echo "" +fi + +RESULTS=() + +# ================================================== +# TEST 1: Manual Compaction +# ================================================== +echo "---------------------------------------------------" +echo "TEST 1: Manual Compaction via trigger prompt" +echo "---------------------------------------------------" + +TESTDIR=$(mktemp -d) +echo "hello world" > "$TESTDIR/hello.txt" +echo "Test directory: $TESTDIR" +echo "" + +OUTPUT=$(mktemp) + +echo "Step 1: Creating session with initial messages..." +(cd "$TESTDIR" && "$GOOSE_BIN" run --text "list files and read hello.txt" 2>&1) | tee "$OUTPUT" + +if ! command -v jq &> /dev/null; then + echo "✗ FAILED: jq is required for this test" + RESULTS+=("✗ Manual Compaction (jq required)") + rm -f "$OUTPUT" + rm -rf "$TESTDIR" +else + SESSION_ID=$("$GOOSE_BIN" session list --format json 2>/dev/null | jq -r '.[0].id' 2>/dev/null) + + if [ -z "$SESSION_ID" ] || [ "$SESSION_ID" = "null" ]; then + echo "✗ FAILED: Could not create session" + RESULTS+=("✗ Manual Compaction (no session)") + else + echo "" + echo "Session created: $SESSION_ID" + echo "Step 2: Sending manual compaction trigger..." + + # Send the manual compact trigger prompt + (cd "$TESTDIR" && "$GOOSE_BIN" run --resume --session-id "$SESSION_ID" --text "Please compact this conversation" 2>&1) | tee -a "$OUTPUT" + + echo "" + echo "Checking for compaction evidence..." + + if grep -qi "compacting\|compacted\|compaction" "$OUTPUT"; then + echo "✓ SUCCESS: Manual compaction was triggered" + + if validate_compaction "$SESSION_ID" "manual compaction"; then + RESULTS+=("✓ Manual Compaction") + else + RESULTS+=("✗ Manual Compaction (structure validation failed)") + fi + else + echo "✗ FAILED: Manual compaction was not triggered" + RESULTS+=("✗ Manual Compaction") + fi + fi + + rm -f "$OUTPUT" + rm -rf "$TESTDIR" +fi + +echo "" +echo "" + +# ================================================== +# TEST 2: Auto Compaction +# ================================================== +echo "---------------------------------------------------" +echo "TEST 2: Auto Compaction via threshold (0.01)" +echo "---------------------------------------------------" + +TESTDIR=$(mktemp -d) +echo "test content" > "$TESTDIR/test.txt" +echo "Test directory: $TESTDIR" +echo "" + +# Set auto-compact threshold very low (1%) to trigger it quickly +export GOOSE_AUTO_COMPACT_THRESHOLD=0.01 + +OUTPUT=$(mktemp) + +echo "Step 1: Creating session with first message..." +(cd "$TESTDIR" && "$GOOSE_BIN" run --text "hello" 2>&1) | tee "$OUTPUT" + +if ! command -v jq &> /dev/null; then + echo "✗ FAILED: jq is required for this test" + RESULTS+=("✗ Auto Compaction (jq required)") +else + SESSION_ID=$("$GOOSE_BIN" session list --format json 2>/dev/null | jq -r '.[0].id' 2>/dev/null) + + if [ -z "$SESSION_ID" ] || [ "$SESSION_ID" = "null" ]; then + echo "✗ FAILED: Could not create session" + RESULTS+=("✗ Auto Compaction (no session)") + else + echo "" + echo "Session created: $SESSION_ID" + echo "Step 2: Sending second message (should trigger auto-compact)..." + + # Send second message - auto-compaction should trigger before processing this + (cd "$TESTDIR" && "$GOOSE_BIN" run --resume --session-id "$SESSION_ID" --text "hi again" 2>&1) | tee -a "$OUTPUT" + + echo "" + echo "Checking for auto-compaction evidence..." + + if grep -qi "auto.*compact\|exceeded.*auto.*compact.*threshold" "$OUTPUT"; then + echo "✓ SUCCESS: Auto compaction was triggered" + + if validate_compaction "$SESSION_ID" "auto compaction"; then + RESULTS+=("✓ Auto Compaction") + else + RESULTS+=("✗ Auto Compaction (structure validation failed)") + fi + else + echo "✗ FAILED: Auto compaction was not triggered" + echo " Expected to see auto-compact messages with threshold of 0.01" + RESULTS+=("✗ Auto Compaction") + fi + fi +fi + +# Unset the env variable +unset GOOSE_AUTO_COMPACT_THRESHOLD + +rm -f "$OUTPUT" +rm -rf "$TESTDIR" + +echo "" +echo "" + +# ================================================== +# Summary +# ================================================== +echo "==================================================" +echo "TEST SUMMARY" +echo "==================================================" +for result in "${RESULTS[@]}"; do + echo "$result" +done + +# Count results +FAILURE_COUNT=$(echo "${RESULTS[@]}" | grep -o "✗" | wc -l | tr -d ' ') + +if [ "$FAILURE_COUNT" -gt 0 ]; then + echo "" + echo "❌ $FAILURE_COUNT test(s) failed!" + exit 1 +else + echo "" + echo "✅ All tests passed!" +fi diff --git a/scripts/test_mcp.sh b/scripts/test_mcp.sh new file mode 100755 index 000000000000..d1db833df075 --- /dev/null +++ b/scripts/test_mcp.sh @@ -0,0 +1,118 @@ +#!/bin/bash +if [ -f .env ]; then + export $(grep -v '^#' .env | xargs) +fi + +if [ -z "$SKIP_BUILD" ]; then + echo "Building goose..." + cargo build --release --bin goose + echo "" +else + echo "Skipping build (SKIP_BUILD is set)..." + echo "" +fi + +SCRIPT_DIR=$(pwd) + +JUDGE_PROVIDER=${GOOSE_JUDGE_PROVIDER:-openrouter} +JUDGE_MODEL=${GOOSE_JUDGE_MODEL:-google/gemini-2.5-flash} + +PROVIDERS=( + "anthropic:claude-haiku-4-5-20251001" + "google:gemini-2.5-pro" + "openrouter:google/gemini-2.5-pro" + "openai:gpt-5-mini" +) + +# In CI, only run Databricks tests if DATABRICKS_HOST and DATABRICKS_TOKEN are set +# Locally, always run Databricks tests +if [ -n "$CI" ]; then + if [ -n "$DATABRICKS_HOST" ] && [ -n "$DATABRICKS_TOKEN" ]; then + echo "✓ Including Databricks tests" + PROVIDERS+=("databricks:databricks-claude-sonnet-4:gemini-2-5-flash:gpt-4o") + else + echo "⚠️ Skipping Databricks tests (DATABRICKS_HOST and DATABRICKS_TOKEN required in CI)" + fi +else + echo "✓ Including Databricks tests" + PROVIDERS+=("databricks:databricks-claude-sonnet-4:gemini-2-5-flash:gpt-4o") +fi + +RESULTS=() + +for provider_config in "${PROVIDERS[@]}"; do + IFS=':' read -ra PARTS <<< "$provider_config" + PROVIDER="${PARTS[0]}" + for i in $(seq 1 $((${#PARTS[@]} - 1))); do + MODEL="${PARTS[$i]}" + export GOOSE_PROVIDER="$PROVIDER" + export GOOSE_MODEL="$MODEL" + TESTDIR=$(mktemp -d) + echo "Provider: ${PROVIDER}" + echo "Model: ${MODEL}" + echo "" + TMPFILE=$(mktemp) + (cd "$TESTDIR" && "$SCRIPT_DIR/target/release/goose" run --text "Use the sampleLLM tool to ask for a quote from The Great Gatsby" --with-extension "npx -y @modelcontextprotocol/server-everything" 2>&1) | tee "$TMPFILE" + echo "" + if grep -q "sampleLLM | " "$TMPFILE"; then + + JUDGE_PROMPT=$(cat <&1) + + if echo "$JUDGE_OUT" | tr -d '\r' | grep -Eq '^[[:space:]]*PASS[[:space:]]*$'; then + echo "✓ SUCCESS: MCP sampling test passed - confirmed Gatsby related response" + RESULTS+=("✓ MCP Sampling ${PROVIDER}: ${MODEL}") + else + echo "✗ FAILED: MCP sampling test failed - did not confirm Gatsby related response" + echo " Judge provider/model: ${JUDGE_PROVIDER}:${JUDGE_MODEL}" + echo " Judge output (snippet):" + echo "$JUDGE_OUT" | tail -n 20 + RESULTS+=("✗ MCP Sampling ${PROVIDER}: ${MODEL}") + fi + else + echo "✗ FAILED: MCP sampling test failed - sampleLLM tool not called" + RESULTS+=("✗ MCP Sampling ${PROVIDER}: ${MODEL}") + fi + rm "$TMPFILE" + rm -rf "$TESTDIR" + echo "---" + done +done + +echo "" +echo "=== MCP Sampling Test Summary ===" +for result in "${RESULTS[@]}"; do + echo "$result" +done + +if echo "${RESULTS[@]}" | grep -q "✗"; then + echo "" + echo "Some MCP sampling tests failed!" + exit 1 +else + echo "" + echo "All MCP sampling tests passed!" +fi diff --git a/scripts/test_providers.sh b/scripts/test_providers.sh index 6ca15928ad91..c4261d75c14c 100755 --- a/scripts/test_providers.sh +++ b/scripts/test_providers.sh @@ -15,7 +15,7 @@ fi SCRIPT_DIR=$(pwd) PROVIDERS=( - "openrouter:google/gemini-2.5-pro:google/gemini-2.5-flash:anthropic/claude-sonnet-4.5:qwen/qwen3-coder" + "openrouter:google/gemini-2.5-pro:google/gemini-2.5-flash:anthropic/claude-sonnet-4.5:qwen/qwen3-coder:z-ai/glm-4.6" "openai:gpt-4o:gpt-4o-mini:gpt-3.5-turbo:gpt-5" "anthropic:claude-sonnet-4-5-20250929:claude-opus-4-1-20250805" "google:gemini-2.5-pro:gemini-2.5-flash" @@ -51,7 +51,7 @@ for provider_config in "${PROVIDERS[@]}"; do echo "Model: ${MODEL}" echo "" TMPFILE=$(mktemp) - (cd "$TESTDIR" && "$SCRIPT_DIR/target/release/goose" run --text "please list files in the current directory" --with-builtin developer,autovisualiser,computercontroller,tutorial 2>&1) | tee "$TMPFILE" + (cd "$TESTDIR" && "$SCRIPT_DIR/target/release/goose" run --text "please list files in the current directory" --with-builtin developer,autovisualiser,computercontroller,tutorial,todo,extensionmanager 2>&1) | tee "$TMPFILE" echo "" if grep -q "shell | developer" "$TMPFILE"; then echo "✓ SUCCESS: Test passed - developer tool called" diff --git a/scripts/test_subrecipes.sh b/scripts/test_subrecipes.sh index 29920f2e2c0c..02505565da4f 100755 --- a/scripts/test_subrecipes.sh +++ b/scripts/test_subrecipes.sh @@ -20,8 +20,9 @@ SCRIPT_DIR=$(pwd) export PATH="$SCRIPT_DIR/target/release:$PATH" # Set default provider and model if not already set +# Use fast model for CI to speed up tests export GOOSE_PROVIDER="${GOOSE_PROVIDER:-anthropic}" -export GOOSE_MODEL="${GOOSE_MODEL:-claude-sonnet-4-5-20250929}" +export GOOSE_MODEL="${GOOSE_MODEL:-claude-3-5-haiku-20241022}" echo "Using provider: $GOOSE_PROVIDER" echo "Using model: $GOOSE_MODEL" @@ -35,7 +36,39 @@ echo "Copied test recipes from scripts/test-subrecipes-examples" echo "" echo "=== Testing Subrecipe Workflow ===" -echo "Recipe: $TESTDIR/travel_planner.yaml" +echo "Recipe: $TESTDIR/project_analyzer.yaml" +echo "" + +# Create sample code files for analysis +echo "Creating sample code files for testing..." +cat > "$TESTDIR/sample.rs" << 'EOF' +// TODO: Add error handling +fn calculate(x: i32, y: i32) -> i32 { + x + y +} + +#[test] +fn test_calculate() { + assert_eq!(calculate(2, 2), 4); +} +EOF + +cat > "$TESTDIR/sample.py" << 'EOF' +# FIXME: Optimize this function +def process_data(items): + """Process a list of items""" + return [item * 2 for item in items] + +def test_process_data(): + assert process_data([1, 2, 3]) == [2, 4, 6] +EOF + +cat > "$TESTDIR/README.md" << 'EOF' +# Sample Project +This is a test project for analyzing code patterns. +## TODO +- Add more tests +EOF echo "" RESULTS=() @@ -52,8 +85,8 @@ check_recipe_output() { RESULTS+=("✗ Subrecipe tool invocation ($mode)") fi - if grep -q "weather_data" "$tmpfile" && grep -q "activity_suggestions" "$tmpfile"; then - echo "✓ SUCCESS: Both subrecipes (weather_data, activity_suggestions) found in output" + if grep -q "file_stats" "$tmpfile" && grep -q "code_patterns" "$tmpfile"; then + echo "✓ SUCCESS: Both subrecipes (file_stats, code_patterns) found in output" RESULTS+=("✓ Both subrecipes present ($mode)") else echo "✗ FAILED: Not all subrecipes found in output" @@ -69,37 +102,11 @@ check_recipe_output() { fi } -echo "Test 1: Running recipe with session..." -TMPFILE=$(mktemp) -if (cd "$TESTDIR" && "$SCRIPT_DIR/target/release/goose" run --recipe travel_planner.yaml 2>&1) | tee "$TMPFILE"; then - echo "✓ SUCCESS: Recipe completed successfully" - RESULTS+=("✓ Recipe exit code (with session)") - check_recipe_output "$TMPFILE" "with session" -else - echo "✗ FAILED: Recipe execution failed" - RESULTS+=("✗ Recipe exit code (with session)") -fi -rm "$TMPFILE" -echo "" - -echo "Test 2: Running recipe in --no-session mode..." -TMPFILE=$(mktemp) -if (cd "$TESTDIR" && "$SCRIPT_DIR/target/release/goose" run --recipe travel_planner.yaml --no-session 2>&1) | tee "$TMPFILE"; then - echo "✓ SUCCESS: Recipe completed successfully" - RESULTS+=("✓ Recipe exit code (--no-session)") - check_recipe_output "$TMPFILE" "--no-session" -else - echo "✗ FAILED: Recipe execution failed" - RESULTS+=("✗ Recipe exit code (--no-session)") -fi -rm "$TMPFILE" -echo "" - -echo "Test 3: Running recipe with parallel subrecipes..." +echo "Running recipe with parallel subrecipes..." TMPFILE=$(mktemp) -if (cd "$TESTDIR" && "$SCRIPT_DIR/target/release/goose" run --recipe travel_planner_parallel.yaml 2>&1) | tee "$TMPFILE"; then +if (cd "$TESTDIR" && "$SCRIPT_DIR/target/release/goose" run --recipe project_analyzer_parallel.yaml --no-session 2>&1) | tee "$TMPFILE"; then echo "✓ SUCCESS: Recipe completed successfully" - RESULTS+=("✓ Recipe exit code (parallel)") + RESULTS+=("✓ Recipe exit code") check_recipe_output "$TMPFILE" "parallel" if grep -q "execution_mode: parallel" "$TMPFILE"; then @@ -111,7 +118,7 @@ if (cd "$TESTDIR" && "$SCRIPT_DIR/target/release/goose" run --recipe travel_plan fi else echo "✗ FAILED: Recipe execution failed" - RESULTS+=("✗ Recipe exit code (parallel)") + RESULTS+=("✗ Recipe exit code") fi rm "$TMPFILE" echo "" diff --git a/ui/desktop/eslint.config.js b/ui/desktop/eslint.config.js index 2241702d0e63..391d45a90ffa 100644 --- a/ui/desktop/eslint.config.js +++ b/ui/desktop/eslint.config.js @@ -85,6 +85,7 @@ module.exports = [ HeadersInit: 'readonly', KeyboardEvent: 'readonly', MouseEvent: 'readonly', // Add MouseEvent + Event: 'readonly', // Add Event Node: 'readonly', // Add Node React: 'readonly', handleAction: 'readonly', diff --git a/ui/desktop/forge.config.ts b/ui/desktop/forge.config.ts index 34c91dfb82a1..83f676f28c9b 100644 --- a/ui/desktop/forge.config.ts +++ b/ui/desktop/forge.config.ts @@ -26,12 +26,12 @@ let cfg = { // Document types for drag-and-drop support onto dock icon CFBundleDocumentTypes: [ { - CFBundleTypeName: "Folders", - CFBundleTypeRole: "Viewer", - LSHandlerRank: "Alternate", - LSItemContentTypes: ["public.directory", "public.folder"] - } - ] + CFBundleTypeName: 'Folders', + CFBundleTypeRole: 'Viewer', + LSHandlerRank: 'Alternate', + LSItemContentTypes: ['public.directory', 'public.folder'], + }, + ], }, }; @@ -72,8 +72,9 @@ module.exports = { categories: ['Development'], desktopTemplate: './forge.deb.desktop', options: { - icon: 'src/images/icon.png' - } + icon: 'src/images/icon.png', + prefix: '/opt', + }, }, }, { @@ -86,8 +87,10 @@ module.exports = { categories: ['Development'], desktopTemplate: './forge.rpm.desktop', options: { - icon: 'src/images/icon.png' - } + icon: 'src/images/icon.png', + prefix: '/opt', + fpm: ['--rpm-rpmbuild-define', '_build_id_links none'], + }, }, }, ], diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 83f3a073db9f..ba683314cbb6 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -10,9 +10,87 @@ "license": { "name": "Apache-2.0" }, - "version": "1.11.0" + "version": "1.13.0" }, "paths": { + "/agent/add_extension": { + "post": { + "tags": [ + "super::routes::agent" + ], + "operationId": "agent_add_extension", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddExtensionRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Extension added", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "401": { + "description": "Unauthorized - invalid secret key" + }, + "424": { + "description": "Agent not initialized" + }, + "500": { + "description": "Internal server error" + } + } + } + }, + "/agent/remove_extension": { + "post": { + "tags": [ + "super::routes::agent" + ], + "operationId": "agent_remove_extension", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RemoveExtensionRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Extension removed", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "401": { + "description": "Unauthorized - invalid secret key" + }, + "424": { + "description": "Agent not initialized" + }, + "500": { + "description": "Internal server error" + } + } + } + }, "/agent/resume": { "post": { "tags": [ @@ -1989,6 +2067,21 @@ }, "components": { "schemas": { + "AddExtensionRequest": { + "type": "object", + "required": [ + "session_id", + "config" + ], + "properties": { + "config": { + "$ref": "#/components/schemas/ExtensionConfig" + }, + "session_id": { + "type": "string" + } + } + }, "Annotations": { "type": "object", "properties": { @@ -2827,7 +2920,10 @@ "type": "string" }, "sizes": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } }, "src": { "type": "string" @@ -3185,12 +3281,16 @@ "type": "object", "required": [ "message", + "token_state", "type" ], "properties": { "message": { "$ref": "#/components/schemas/Message" }, + "token_state": { + "$ref": "#/components/schemas/TokenState" + }, "type": { "type": "string", "enum": [ @@ -3221,12 +3321,16 @@ "type": "object", "required": [ "reason", + "token_state", "type" ], "properties": { "reason": { "type": "string" }, + "token_state": { + "$ref": "#/components/schemas/TokenState" + }, "type": { "type": "string", "enum": [ @@ -3658,13 +3762,6 @@ ], "nullable": true }, - "context": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - }, "description": { "type": "string" }, @@ -3812,6 +3909,21 @@ } } }, + "RemoveExtensionRequest": { + "type": "object", + "required": [ + "name", + "session_id" + ], + "properties": { + "name": { + "type": "string" + }, + "session_id": { + "type": "string" + } + } + }, "ResourceContents": { "anyOf": [ { @@ -4012,10 +4124,6 @@ "currently_running": { "type": "boolean" }, - "execution_mode": { - "type": "string", - "nullable": true - }, "id": { "type": "string" }, @@ -4111,6 +4219,9 @@ "type": "string", "nullable": true }, + "session_type": { + "$ref": "#/components/schemas/SessionType" + }, "total_tokens": { "type": "integer", "format": "int32", @@ -4229,6 +4340,15 @@ } } }, + "SessionType": { + "type": "string", + "enum": [ + "user", + "scheduled", + "sub_agent", + "hidden" + ] + }, "SessionsQuery": { "type": "object", "properties": { @@ -4418,6 +4538,43 @@ } } }, + "TokenState": { + "type": "object", + "required": [ + "inputTokens", + "outputTokens", + "totalTokens", + "accumulatedInputTokens", + "accumulatedOutputTokens", + "accumulatedTotalTokens" + ], + "properties": { + "accumulatedInputTokens": { + "type": "integer", + "format": "int32" + }, + "accumulatedOutputTokens": { + "type": "integer", + "format": "int32" + }, + "accumulatedTotalTokens": { + "type": "integer", + "format": "int32" + }, + "inputTokens": { + "type": "integer", + "format": "int32" + }, + "outputTokens": { + "type": "integer", + "format": "int32" + }, + "totalTokens": { + "type": "integer", + "format": "int32" + } + } + }, "Tool": { "type": "object", "required": [ diff --git a/ui/desktop/package-lock.json b/ui/desktop/package-lock.json index 008375acf13a..897a31bdae91 100644 --- a/ui/desktop/package-lock.json +++ b/ui/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "goose-app", - "version": "1.11.0", + "version": "1.13.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "goose-app", - "version": "1.11.0", + "version": "1.13.0", "license": "Apache-2.0", "dependencies": { "@ai-sdk/openai": "^2.0.52", @@ -46,7 +46,7 @@ "react-markdown": "^10.1.0", "react-router-dom": "^7.9.4", "react-select": "^5.10.2", - "react-syntax-highlighter": "^15.6.6", + "react-syntax-highlighter": "^16.1.0", "react-toastify": "^11.0.5", "remark-breaks": "^4.0.0", "remark-gfm": "^4.0.1", @@ -6265,6 +6265,12 @@ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "license": "MIT" }, + "node_modules/@types/prismjs": { + "version": "1.26.5", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", + "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==", + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -11664,10 +11670,13 @@ } }, "node_modules/hast-util-parse-selector": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", - "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -11714,70 +11723,22 @@ } }, "node_modules/hastscript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", - "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", "license": "MIT", "dependencies": { - "@types/hast": "^2.0.0", - "comma-separated-tokens": "^1.0.0", - "hast-util-parse-selector": "^2.0.0", - "property-information": "^5.0.0", - "space-separated-tokens": "^1.0.0" + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/hastscript/node_modules/@types/hast": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", - "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/hastscript/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/hastscript/node_modules/comma-separated-tokens": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", - "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/hastscript/node_modules/property-information": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", - "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/hastscript/node_modules/space-separated-tokens": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", - "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", @@ -17120,17 +17081,20 @@ } }, "node_modules/react-syntax-highlighter": { - "version": "15.6.6", - "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.6.tgz", - "integrity": "sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw==", + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.0.tgz", + "integrity": "sha512-E40/hBiP5rCNwkeBN1vRP+xow1X0pndinO+z3h7HLsHyjztbyjfzNWNKuAsJj+7DLam9iT4AaaOZnueCU+Nplg==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.3.1", + "@babel/runtime": "^7.28.4", "highlight.js": "^10.4.1", "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", "prismjs": "^1.30.0", - "refractor": "^3.6.0" + "refractor": "^5.0.0" + }, + "engines": { + "node": ">= 16.20.2" }, "peerDependencies": { "react": ">= 0.14.0" @@ -17363,121 +17327,21 @@ } }, "node_modules/refractor": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", - "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", - "license": "MIT", - "dependencies": { - "hastscript": "^6.0.0", - "parse-entities": "^2.0.0", - "prismjs": "~1.27.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/character-entities": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", - "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/character-entities-legacy": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", - "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/character-reference-invalid": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", - "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/is-alphabetical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", - "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/is-alphanumerical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", - "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/is-decimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", - "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/is-hexadecimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", - "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/parse-entities": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", - "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-5.0.0.tgz", + "integrity": "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==", "license": "MIT", "dependencies": { - "character-entities": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "character-reference-invalid": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-hexadecimal": "^1.0.0" + "@types/hast": "^3.0.0", + "@types/prismjs": "^1.0.0", + "hastscript": "^9.0.0", + "parse-entities": "^4.0.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/refractor/node_modules/prismjs": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", - "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -20669,15 +20533,6 @@ "dev": true, "license": "MIT" }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/ui/desktop/package.json b/ui/desktop/package.json index ee19c0290888..8952fce455a1 100644 --- a/ui/desktop/package.json +++ b/ui/desktop/package.json @@ -1,7 +1,7 @@ { "name": "goose-app", "productName": "Goose", - "version": "1.11.0", + "version": "1.13.0", "description": "Goose App", "engines": { "node": "^22.17.1" @@ -35,7 +35,7 @@ "test:run": "vitest run", "test:ui": "vitest --ui", "test:coverage": "vitest run --coverage", - "prepare": "cd ../.. && husky install", + "prepare": "husky", "start-alpha-gui": "ALPHA=true npm run start-gui" }, "dependencies": { @@ -76,7 +76,7 @@ "react-markdown": "^10.1.0", "react-router-dom": "^7.9.4", "react-select": "^5.10.2", - "react-syntax-highlighter": "^15.6.6", + "react-syntax-highlighter": "^16.1.0", "react-toastify": "^11.0.5", "remark-breaks": "^4.0.0", "remark-gfm": "^4.0.1", diff --git a/ui/desktop/src/App.test.tsx b/ui/desktop/src/App.test.tsx index 1955821280d4..92313dfcf8cc 100644 --- a/ui/desktop/src/App.test.tsx +++ b/ui/desktop/src/App.test.tsx @@ -131,18 +131,11 @@ vi.mock('./contexts/ChatContext', () => ({ hasActiveSession: false, setRecipe: vi.fn(), clearRecipe: vi.fn(), - draft: '', - setDraft: vi.fn(), - clearDraft: vi.fn(), contextKey: 'hub', }), DEFAULT_CHAT_TITLE: 'New Chat', // Keep this from HEAD })); -vi.mock('./contexts/DraftContext', () => ({ - DraftProvider: ({ children }: { children: React.ReactNode }) => <>{children}, -})); - vi.mock('./components/ui/ConfirmationModal', () => ({ ConfirmationModal: () => null, })); diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 5fa3fde7a047..a32c590f4438 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -27,7 +27,7 @@ import SchedulesView from './components/schedule/SchedulesView'; import ProviderSettings from './components/settings/providers/ProviderSettingsPage'; import { AppLayout } from './components/Layout/AppLayout'; import { ChatProvider } from './contexts/ChatContext'; -import { DraftProvider } from './contexts/DraftContext'; +import LauncherView from './components/LauncherView'; import 'react-toastify/dist/ReactToastify.css'; import { useConfig } from './components/ConfigContext'; @@ -76,6 +76,8 @@ const PairRouteWrapper = ({ setFatalError, agentState, loadCurrentChat, + activeSessionId, + setActiveSessionId, }: { chat: ChatType; setChat: (chat: ChatType) => void; @@ -84,23 +86,26 @@ const PairRouteWrapper = ({ setFatalError: (value: ((prevState: string | null) => string | null) | string | null) => void; agentState: AgentState; loadCurrentChat: (context: InitializationContext) => Promise; + activeSessionId: string | null; + setActiveSessionId: (id: string | null) => void; }) => { const location = useLocation(); const setView = useNavigation(); const routeState = (location.state as PairRouteState) || (window.history.state as PairRouteState) || {}; const [searchParams, setSearchParams] = useSearchParams(); - const [initialMessage] = useState(routeState.initialMessage); - + const initialMessage = routeState.initialMessage; const resumeSessionId = searchParams.get('resumeSessionId') ?? undefined; // Determine which session ID to use: // 1. From route state (when navigating from Hub with a new session) - // 2. From URL params (when resuming a session) - // 3. From the existing chat state (when navigating to Pair directly) - const sessionId = routeState.resumeSessionId || resumeSessionId || chat.sessionId; + // 2. From URL params (when resuming a session or after refresh) + // 3. From active session state (when navigating back from other routes) + // 4. From the existing chat state + const sessionId = + routeState.resumeSessionId || resumeSessionId || activeSessionId || chat.sessionId; - // Update URL with session ID if it's not already there (new chat from pair) + // Update URL with session ID when on /pair route (for refresh support) useEffect(() => { if (process.env.ALPHA && sessionId && sessionId !== resumeSessionId) { setSearchParams((prev) => { @@ -110,6 +115,13 @@ const PairRouteWrapper = ({ } }, [sessionId, resumeSessionId, setSearchParams]); + // Update active session state when session ID changes + useEffect(() => { + if (process.env.ALPHA && sessionId && sessionId !== activeSessionId) { + setActiveSessionId(sessionId); + } + }, [sessionId, activeSessionId, setActiveSessionId]); + return process.env.ALPHA ? ( (null); + const { addExtension } = useConfig(); const { agentState, loadCurrentChat, resetChat } = useAgent(); const resetChatIfNecessary = useCallback(() => { @@ -508,6 +523,62 @@ export function AppInner() { }; }, []); + useEffect(() => { + if (!window.electron) return; + + const handleThemeChanged = (_event: unknown, ...args: unknown[]) => { + const themeData = args[0] as { mode: string; useSystemTheme: boolean; theme: string }; + + if (themeData.useSystemTheme) { + localStorage.setItem('use_system_theme', 'true'); + } else { + localStorage.setItem('use_system_theme', 'false'); + localStorage.setItem('theme', themeData.theme); + } + + const isDark = themeData.useSystemTheme + ? window.matchMedia('(prefers-color-scheme: dark)').matches + : themeData.mode === 'dark'; + + if (isDark) { + document.documentElement.classList.add('dark'); + document.documentElement.classList.remove('light'); + } else { + document.documentElement.classList.remove('dark'); + document.documentElement.classList.add('light'); + } + + const storageEvent = new Event('storage') as Event & { + key: string | null; + newValue: string | null; + }; + storageEvent.key = themeData.useSystemTheme ? 'use_system_theme' : 'theme'; + storageEvent.newValue = themeData.useSystemTheme ? 'true' : themeData.theme; + window.dispatchEvent(storageEvent); + }; + + window.electron.on('theme-changed', handleThemeChanged); + + return () => { + window.electron.off('theme-changed', handleThemeChanged); + }; + }, []); + + // Handle initial message from launcher + useEffect(() => { + const handleSetInitialMessage = (_event: IpcRendererEvent, ...args: unknown[]) => { + const initialMessage = args[0] as string; + if (initialMessage) { + console.log('Received initial message from launcher:', initialMessage); + navigate('/pair', { state: { initialMessage } }); + } + }; + window.electron.on('set-initial-message', handleSetInitialMessage); + return () => { + window.electron.off('set-initial-message', handleSetInitialMessage); + }; + }, [navigate]); + if (fatalError) { return ; } @@ -533,6 +604,7 @@ export function AppInner() {
+ } /> setDidSelectProvider(true)} />} @@ -574,6 +646,8 @@ export function AppInner() { setFatalError={setFatalError} setAgentWaitingMessage={setAgentWaitingMessage} setIsGoosehintsModalOpen={setIsGoosehintsModalOpen} + activeSessionId={activeSessionId} + setActiveSessionId={setActiveSessionId} /> } /> @@ -620,13 +694,11 @@ export function AppInner() { export default function App() { return ( - - - - - - - - + + + + + + ); } diff --git a/ui/desktop/src/api/sdk.gen.ts b/ui/desktop/src/api/sdk.gen.ts index ebcf9ab38b77..7f65f47dc5a7 100644 --- a/ui/desktop/src/api/sdk.gen.ts +++ b/ui/desktop/src/api/sdk.gen.ts @@ -2,7 +2,7 @@ import type { Client, Options as Options2, TDataShape } from './client'; import { client } from './client.gen'; -import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, ConfirmPermissionData, ConfirmPermissionErrors, ConfirmPermissionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetToolsData, GetToolsErrors, GetToolsResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StatusData, StatusResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateRouterToolSelectorData, UpdateRouterToolSelectorErrors, UpdateRouterToolSelectorResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; +import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, ConfirmPermissionData, ConfirmPermissionErrors, ConfirmPermissionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetToolsData, GetToolsErrors, GetToolsResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StatusData, StatusResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateRouterToolSelectorData, UpdateRouterToolSelectorErrors, UpdateRouterToolSelectorResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; export type Options = Options2 & { /** @@ -18,6 +18,28 @@ export type Options; }; +export const agentAddExtension = (options: Options) => { + return (options.client ?? client).post({ + url: '/agent/add_extension', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +export const agentRemoveExtension = (options: Options) => { + return (options.client ?? client).post({ + url: '/agent/remove_extension', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + export const resumeAgent = (options: Options) => { return (options.client ?? client).post({ url: '/agent/resume', diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 6c4eb54cee6c..50a061d25f82 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -4,6 +4,11 @@ export type ClientOptions = { baseUrl: `${string}://${string}` | (string & {}); }; +export type AddExtensionRequest = { + config: ExtensionConfig; + session_id: string; +}; + export type Annotations = { audience?: Array; lastModified?: string; @@ -280,7 +285,7 @@ export type GetToolsQuery = { export type Icon = { mimeType?: string; - sizes?: string; + sizes?: Array; src: string; }; @@ -362,12 +367,14 @@ export type MessageContent = (TextContent & { export type MessageEvent = { message: Message; + token_state: TokenState; type: 'Message'; } | { error: string; type: 'Error'; } | { reason: string; + token_state: TokenState; type: 'Finish'; } | { mode: string; @@ -541,7 +548,6 @@ export type RawTextContent = { export type Recipe = { activities?: Array | null; author?: Author | null; - context?: Array | null; description: string; extensions?: Array | null; instructions?: string | null; @@ -578,6 +584,11 @@ export type RedactedThinkingContent = { data: string; }; +export type RemoveExtensionRequest = { + name: string; + session_id: string; +}; + export type ResourceContents = { _meta?: { [key: string]: unknown; @@ -656,7 +667,6 @@ export type ScheduledJob = { cron: string; current_session_id?: string | null; currently_running?: boolean; - execution_mode?: string | null; id: string; last_run?: string | null; paused?: boolean; @@ -678,6 +688,7 @@ export type Session = { output_tokens?: number | null; recipe?: Recipe | null; schedule_id?: string | null; + session_type?: SessionType; total_tokens?: number | null; updated_at: string; user_recipe_values?: { @@ -714,6 +725,8 @@ export type SessionListResponse = { sessions: Array; }; +export type SessionType = 'user' | 'scheduled' | 'sub_agent' | 'hidden'; + export type SessionsQuery = { limit?: number; }; @@ -779,6 +792,15 @@ export type ThinkingContent = { thinking: string; }; +export type TokenState = { + accumulatedInputTokens: number; + accumulatedOutputTokens: number; + accumulatedTotalTokens: number; + inputTokens: number; + outputTokens: number; + totalTokens: number; +}; + export type Tool = { annotations?: ToolAnnotations | { [key: string]: unknown; @@ -896,6 +918,68 @@ export type UpsertPermissionsQuery = { tool_permissions: Array; }; +export type AgentAddExtensionData = { + body: AddExtensionRequest; + path?: never; + query?: never; + url: '/agent/add_extension'; +}; + +export type AgentAddExtensionErrors = { + /** + * Unauthorized - invalid secret key + */ + 401: unknown; + /** + * Agent not initialized + */ + 424: unknown; + /** + * Internal server error + */ + 500: unknown; +}; + +export type AgentAddExtensionResponses = { + /** + * Extension added + */ + 200: string; +}; + +export type AgentAddExtensionResponse = AgentAddExtensionResponses[keyof AgentAddExtensionResponses]; + +export type AgentRemoveExtensionData = { + body: RemoveExtensionRequest; + path?: never; + query?: never; + url: '/agent/remove_extension'; +}; + +export type AgentRemoveExtensionErrors = { + /** + * Unauthorized - invalid secret key + */ + 401: unknown; + /** + * Agent not initialized + */ + 424: unknown; + /** + * Internal server error + */ + 500: unknown; +}; + +export type AgentRemoveExtensionResponses = { + /** + * Extension removed + */ + 200: string; +}; + +export type AgentRemoveExtensionResponse = AgentRemoveExtensionResponses[keyof AgentRemoveExtensionResponses]; + export type ResumeAgentData = { body: ResumeAgentRequest; path?: never; diff --git a/ui/desktop/src/bin/node-setup-common.sh b/ui/desktop/src/bin/node-setup-common.sh index 97c3551d5ac0..16eee6854714 100755 --- a/ui/desktop/src/bin/node-setup-common.sh +++ b/ui/desktop/src/bin/node-setup-common.sh @@ -57,9 +57,39 @@ log "Updated PATH to include ~/.config/goose/mcp-hermit/bin." log "Checking for hermit in PATH." which hermit >> "$LOG_FILE" -# Initialize hermit -log "Initializing hermit." -hermit init >> "$LOG_FILE" +# Check if hermit environment is already initialized (only run init on first setup) +if [ ! -f bin/activate-hermit ]; then + log "Hermit environment not yet initialized. Setting up hermit." + + # Fix hermit self-update lock issues on Linux by using temp binary for init only + if [[ "$(uname -s)" == "Linux" ]]; then + log "Creating temp dir with bin subdirectory for hermit copy to avoid self-update locks." + HERMIT_TMP_DIR="/tmp/hermit_tmp_$$/bin" + mkdir -p "$HERMIT_TMP_DIR" + cp ~/.config/goose/mcp-hermit/bin/hermit "$HERMIT_TMP_DIR/hermit" + chmod +x "$HERMIT_TMP_DIR/hermit" + export PATH="$HERMIT_TMP_DIR:$PATH" + HERMIT_CLEANUP_DIR="/tmp/hermit_tmp_$$" + fi + + # Initialize hermit + log "Initializing hermit." + hermit init >> "$LOG_FILE" + + # Clean up temp dir if it was created + if [[ -n "${HERMIT_CLEANUP_DIR:-}" ]]; then + log "Cleaning up temporary hermit binary directory." + rm -rf "$HERMIT_CLEANUP_DIR" + fi +else + log "Hermit environment already initialized. Skipping init." +fi + +# Activate the environment with output redirected to log +if [[ "$(uname -s)" == "Linux" ]]; then + log "Activating hermit environment." + { . bin/activate-hermit; } >> "$LOG_FILE" 2>&1 +fi # Install Node.js using hermit log "Installing Node.js with hermit." diff --git a/ui/desktop/src/components/BaseChat.tsx b/ui/desktop/src/components/BaseChat.tsx index c5a96bcdcb3b..fa96740f064c 100644 --- a/ui/desktop/src/components/BaseChat.tsx +++ b/ui/desktop/src/components/BaseChat.tsx @@ -132,6 +132,7 @@ function BaseChatContent({ sessionOutputTokens, localInputTokens, localOutputTokens, + tokenState, commandHistory, toolCallNotifications, sessionMetadata, @@ -442,9 +443,13 @@ function BaseChatContent({ commandHistory={commandHistory} initialValue={input || ''} setView={setView} - numTokens={sessionTokenCount} - inputTokens={sessionInputTokens || localInputTokens} - outputTokens={sessionOutputTokens || localOutputTokens} + totalTokens={tokenState?.totalTokens || sessionTokenCount} + accumulatedInputTokens={ + tokenState?.accumulatedInputTokens || sessionInputTokens || localInputTokens + } + accumulatedOutputTokens={ + tokenState?.accumulatedOutputTokens || sessionOutputTokens || localOutputTokens + } droppedFiles={droppedFiles} onFilesProcessed={() => setDroppedFiles([])} // Clear dropped files after processing messages={messages} diff --git a/ui/desktop/src/components/BaseChat2.tsx b/ui/desktop/src/components/BaseChat2.tsx index bef13f7860a7..4653eea6c0e8 100644 --- a/ui/desktop/src/components/BaseChat2.tsx +++ b/ui/desktop/src/components/BaseChat2.tsx @@ -32,7 +32,11 @@ interface BaseChatProps { renderHeader?: () => React.ReactNode; customChatInputProps?: Record; customMainLayoutProps?: Record; + contentClassName?: string; + disableSearch?: boolean; + showPopularTopics?: boolean; suppressEmptyState: boolean; + autoSubmit?: boolean; sessionId: string; initialMessage?: string; } @@ -44,6 +48,7 @@ function BaseChatContent({ customMainLayoutProps = {}, sessionId, initialMessage, + autoSubmit = false, }: BaseChatProps) { const location = useLocation(); const scrollRef = useRef(null); @@ -72,6 +77,7 @@ function BaseChatContent({ stopStreaming, sessionLoadError, setRecipeUserParams, + tokenState, } = useChatStream({ sessionId, onStreamFinish, @@ -185,7 +191,9 @@ function BaseChatContent({ name: session?.name || 'No Session', }; - const initialPrompt = messages.length == 0 && recipe?.prompt ? recipe.prompt : ''; + const initialPrompt = + initialMessage || (messages.length == 0 && recipe?.prompt ? recipe.prompt : ''); + const shouldAutoSubmit = autoSubmit || !!initialMessage; return (
@@ -281,9 +289,13 @@ function BaseChatContent({ //commandHistory={commandHistory} initialValue={initialPrompt} setView={setView} - numTokens={session?.total_tokens || undefined} - inputTokens={session?.input_tokens || undefined} - outputTokens={session?.output_tokens || undefined} + totalTokens={tokenState?.totalTokens ?? session?.total_tokens ?? undefined} + accumulatedInputTokens={ + tokenState?.accumulatedInputTokens ?? session?.accumulated_input_tokens ?? undefined + } + accumulatedOutputTokens={ + tokenState?.accumulatedOutputTokens ?? session?.accumulated_output_tokens ?? undefined + } droppedFiles={droppedFiles} onFilesProcessed={() => setDroppedFiles([])} // Clear dropped files after processing messages={messages} @@ -294,7 +306,7 @@ function BaseChatContent({ recipeAccepted={!hasNotAcceptedRecipe} initialPrompt={initialPrompt} toolCount={toolCount || 0} - autoSubmit={false} + autoSubmit={shouldAutoSubmit} {...customChatInputProps} />
diff --git a/ui/desktop/src/components/ChatInput.tsx b/ui/desktop/src/components/ChatInput.tsx index e907b00ecdbd..63da447e0ce2 100644 --- a/ui/desktop/src/components/ChatInput.tsx +++ b/ui/desktop/src/components/ChatInput.tsx @@ -20,7 +20,6 @@ import { WaveformVisualizer } from './WaveformVisualizer'; import { toastError } from '../toasts'; import MentionPopover, { FileItemWithMatch } from './MentionPopover'; import { useDictationSettings } from '../hooks/useDictationSettings'; -import { useChatContext } from '../contexts/ChatContext'; import { COST_TRACKING_ENABLED, VOICE_DICTATION_ELEVENLABS_ENABLED } from '../updates'; import { CostTracker } from './bottom_menu/CostTracker'; import { DroppedFile, useFileDrop } from '../hooks/useFileDrop'; @@ -70,9 +69,9 @@ interface ChatInputProps { droppedFiles?: DroppedFile[]; onFilesProcessed?: () => void; // Callback to clear dropped files after processing setView: (view: View) => void; - numTokens?: number; - inputTokens?: number; - outputTokens?: number; + totalTokens?: number; + accumulatedInputTokens?: number; + accumulatedOutputTokens?: number; messages?: Message[]; sessionCosts?: { [key: string]: { @@ -103,9 +102,9 @@ export default function ChatInput({ droppedFiles = [], onFilesProcessed, setView, - numTokens, - inputTokens, - outputTokens, + totalTokens, + accumulatedInputTokens, + accumulatedOutputTokens, messages = [], disableAnimation = false, sessionCosts, @@ -142,20 +141,8 @@ export default function ChatInput({ const { getCurrentModelAndProvider, currentModel, currentProvider } = useModelAndProvider(); const [tokenLimit, setTokenLimit] = useState(TOKEN_LIMIT_DEFAULT); const [isTokenLimitLoaded, setIsTokenLimitLoaded] = useState(false); - - // Draft functionality - get chat context and global draft context - // We need to handle the case where ChatInput is used without ChatProvider (e.g., in Hub) - const chatContext = useChatContext(); // This should always be available now - const agentIsReady = chatContext === null || chatContext.agentWaitingMessage === null; - const draftLoadedRef = useRef(false); - const [diagnosticsOpen, setDiagnosticsOpen] = useState(false); - // Debug logging for draft context - useEffect(() => { - // Debug logging removed - draft functionality is working correctly - }, [chatContext?.contextKey, chatContext?.draft, chatContext]); - // Save queue state (paused/interrupted) to storage useEffect(() => { try { @@ -281,9 +268,6 @@ export default function ChatInput({ setValue(initialValue); setDisplayValue(initialValue); - // Reset draft loaded flag when initialValue changes - draftLoadedRef.current = false; - // Use a functional update to get the current pastedImages // and perform cleanup. This avoids needing pastedImages in the deps. setPastedImages((currentPastedImages) => { @@ -313,38 +297,6 @@ export default function ChatInput({ } }, [recipeAccepted, initialPrompt, messages.length]); - // Draft functionality - load draft if no initial value or recipe - useEffect(() => { - // Reset draft loaded flag when context changes - draftLoadedRef.current = false; - }, [chatContext?.contextKey]); - - useEffect(() => { - // Only load draft once and if conditions are met - if (!initialValue && !recipe && !draftLoadedRef.current && chatContext) { - const draftText = chatContext.draft || ''; - - if (draftText) { - setDisplayValue(draftText); - setValue(draftText); - } - - // Always mark as loaded after checking, regardless of whether we found a draft - draftLoadedRef.current = true; - } - }, [chatContext, initialValue, recipe]); - - // Save draft when user types (debounced) - const debouncedSaveDraft = useMemo( - () => - debounce((value: string) => { - if (chatContext && chatContext.setDraft) { - chatContext.setDraft(value); - } - }, 500), // Save draft after 500ms of no typing - [chatContext] - ); - // State to track if the IME is composing (i.e., in the middle of Japanese IME input) const [isComposing, setIsComposing] = useState(false); const [historyIndex, setHistoryIndex] = useState(-1); @@ -505,16 +457,16 @@ export default function ChatInput({ clearAlerts(); // Show alert when either there is registered token usage, or we know the limit - if ((numTokens && numTokens > 0) || (isTokenLimitLoaded && tokenLimit)) { + if ((totalTokens && totalTokens > 0) || (isTokenLimitLoaded && tokenLimit)) { addAlert({ type: AlertType.Info, message: 'Context window', progress: { - current: numTokens || 0, + current: totalTokens || 0, total: tokenLimit, }, showCompactButton: true, - compactButtonDisabled: !numTokens, + compactButtonDisabled: !totalTokens, onCompact: () => { window.dispatchEvent(new CustomEvent('hide-alert-popover')); @@ -542,7 +494,7 @@ export default function ChatInput({ } // We intentionally omit setView as it shouldn't trigger a re-render of alerts // eslint-disable-next-line react-hooks/exhaustive-deps - }, [numTokens, toolCount, tokenLimit, isTokenLimitLoaded, addAlert, clearAlerts]); + }, [totalTokens, toolCount, tokenLimit, isTokenLimitLoaded, addAlert, clearAlerts]); // Cleanup effect for component unmount - prevent memory leaks useEffect(() => { @@ -608,13 +560,9 @@ export default function ChatInput({ const val = evt.target.value; const cursorPosition = evt.target.selectionStart; - setDisplayValue(val); // Update display immediately - updateValue(val); // Update actual value immediately for better responsiveness - debouncedSaveDraft(val); // Save draft with debounce - // Mark that the user has typed something + setDisplayValue(val); + updateValue(val); setHasUserTyped(true); - - // Check for @ mention checkForMention(val, cursorPosition, evt.target); }; @@ -769,9 +717,8 @@ export default function ChatInput({ useEffect(() => { return () => { debouncedAutosize.cancel?.(); - debouncedSaveDraft.cancel?.(); }; - }, [debouncedAutosize, debouncedSaveDraft]); + }, [debouncedAutosize]); // Handlers for composition events, which are crucial for proper IME behavior const handleCompositionStart = () => { @@ -910,7 +857,6 @@ export default function ChatInput({ const canSubmit = !isLoading && - agentIsReady && (displayValue.trim() || pastedImages.some((img) => img.filePath && !img.error && !img.isLoading) || allDroppedFiles.some((file) => !file.error && !file.isLoading)); @@ -964,11 +910,6 @@ export default function ChatInput({ setIsInGlobalHistory(false); setHasUserTyped(false); - // Clear draft when message is sent - if (chatContext && chatContext.clearDraft) { - chatContext.clearDraft(); - } - // Clear both parent and local dropped files after processing if (onFilesProcessed && droppedFiles.length > 0) { onFilesProcessed(); @@ -980,7 +921,6 @@ export default function ChatInput({ }, [ allDroppedFiles, - chatContext, displayValue, droppedFiles.length, handleSubmit, @@ -1066,7 +1006,6 @@ export default function ChatInput({ e.preventDefault(); const canSubmit = !isLoading && - agentIsReady && (displayValue.trim() || pastedImages.some((img) => img.filePath && !img.error && !img.isLoading) || allDroppedFiles.some((file) => !file.error && !file.isLoading)); @@ -1120,7 +1059,6 @@ export default function ChatInput({ isAnyDroppedFileLoading || isRecording || isTranscribing || - !agentIsReady || isExtensionsLoading; // Queue management functions - no storage persistence, only in-memory @@ -1372,7 +1310,7 @@ export default function ChatInput({ ? 'Recording...' : isTranscribing ? 'Transcribing...' - : (chatContext?.agentWaitingMessage ?? 'Send')} + : 'Send'}

@@ -1540,8 +1478,8 @@ export default function ChatInput({ <>
diff --git a/ui/desktop/src/components/ConfigContext.tsx b/ui/desktop/src/components/ConfigContext.tsx index 8bda007082de..ab0618a58e6f 100644 --- a/ui/desktop/src/components/ConfigContext.tsx +++ b/ui/desktop/src/components/ConfigContext.tsx @@ -19,7 +19,6 @@ import type { ExtensionQuery, ExtensionConfig, } from '../api'; -import { removeShims } from './settings/extensions/utils'; export type { ExtensionConfig } from '../api/types.gen'; @@ -122,10 +121,6 @@ export const ConfigProvider: React.FC = ({ children }) => { const addExtension = useCallback( async (name: string, config: ExtensionConfig, enabled: boolean) => { - // remove shims if present - if (config.type === 'stdio') { - config.cmd = removeShims(config.cmd); - } const query: ExtensionQuery = { name, config, enabled }; await apiAddExtension({ body: query, diff --git a/ui/desktop/src/components/GooseSidebar/ThemeSelector.tsx b/ui/desktop/src/components/GooseSidebar/ThemeSelector.tsx index f9499fb59916..7fd1d438f9f3 100644 --- a/ui/desktop/src/components/GooseSidebar/ThemeSelector.tsx +++ b/ui/desktop/src/components/GooseSidebar/ThemeSelector.tsx @@ -8,37 +8,67 @@ interface ThemeSelectorProps { horizontal?: boolean; } +const getIsDarkMode = (mode: 'light' | 'dark' | 'system'): boolean => { + if (mode === 'system') { + return window.matchMedia('(prefers-color-scheme: dark)').matches; + } + return mode === 'dark'; +}; + +const getThemeMode = (): 'light' | 'dark' | 'system' => { + const savedUseSystemTheme = localStorage.getItem('use_system_theme'); + if (savedUseSystemTheme === 'true') { + return 'system'; + } + + const savedTheme = localStorage.getItem('theme'); + if (savedTheme) { + return savedTheme === 'dark' ? 'dark' : 'light'; + } + + return getIsDarkMode('system') ? 'dark' : 'light'; +}; + +const setThemeModeStorage = (mode: 'light' | 'dark' | 'system') => { + if (mode === 'system') { + localStorage.setItem('use_system_theme', 'true'); + } else { + localStorage.setItem('use_system_theme', 'false'); + localStorage.setItem('theme', mode); + } + + const themeData = { + mode, + useSystemTheme: mode === 'system', + theme: mode === 'system' ? '' : mode, + }; + + window.electron?.broadcastThemeChange(themeData); +}; + const ThemeSelector: React.FC = ({ className = '', hideTitle = false, horizontal = false, }) => { - const [themeMode, setThemeMode] = useState<'light' | 'dark' | 'system'>(() => { - const savedUseSystemTheme = localStorage.getItem('use_system_theme') === 'true'; - if (savedUseSystemTheme) { - return 'system'; - } - const savedTheme = localStorage.getItem('theme'); - return savedTheme === 'dark' ? 'dark' : 'light'; - }); - - const [isDarkMode, setDarkMode] = useState(() => { - // First check localStorage to determine the intended theme - const savedUseSystemTheme = localStorage.getItem('use_system_theme') === 'true'; - const savedTheme = localStorage.getItem('theme'); - - if (savedUseSystemTheme) { - // Use system preference - const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; - return systemPrefersDark; - } else if (savedTheme) { - // Use saved theme preference - return savedTheme === 'dark'; - } else { - // Fallback: check current DOM state to maintain consistency - return document.documentElement.classList.contains('dark'); - } - }); + const [themeMode, setThemeMode] = useState<'light' | 'dark' | 'system'>(getThemeMode); + const [isDarkMode, setDarkMode] = useState(() => getIsDarkMode(getThemeMode())); + + useEffect(() => { + const handleStorageChange = (e: { key: string | null; newValue: string | null }) => { + if (e.key === 'use_system_theme' || e.key === 'theme') { + const newThemeMode = getThemeMode(); + setThemeMode(newThemeMode); + setDarkMode(getIsDarkMode(newThemeMode)); + } + }; + + window.addEventListener('storage', handleStorageChange); + + return () => { + window.removeEventListener('storage', handleStorageChange); + }; + }, []); useEffect(() => { const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); @@ -51,14 +81,8 @@ const ThemeSelector: React.FC = ({ mediaQuery.addEventListener('change', handleThemeChange); - if (themeMode === 'system') { - setDarkMode(mediaQuery.matches); - localStorage.setItem('use_system_theme', 'true'); - } else { - setDarkMode(themeMode === 'dark'); - localStorage.setItem('use_system_theme', 'false'); - localStorage.setItem('theme', themeMode); - } + setThemeModeStorage(themeMode); + setDarkMode(getIsDarkMode(themeMode)); return () => mediaQuery.removeEventListener('change', handleThemeChange); }, [themeMode]); diff --git a/ui/desktop/src/components/GroupedExtensionLoadingToast.tsx b/ui/desktop/src/components/GroupedExtensionLoadingToast.tsx new file mode 100644 index 000000000000..01a2887518a0 --- /dev/null +++ b/ui/desktop/src/components/GroupedExtensionLoadingToast.tsx @@ -0,0 +1,163 @@ +import { useState } from 'react'; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible'; +import { ChevronDown, ChevronUp, Loader2 } from 'lucide-react'; +import { Button } from './ui/button'; +import { startNewSession } from '../sessions'; +import { useNavigation } from '../hooks/useNavigation'; +import { formatExtensionErrorMessage } from '../utils/extensionErrorUtils'; + +export interface ExtensionLoadingStatus { + name: string; + status: 'loading' | 'success' | 'error'; + error?: string; + recoverHints?: string; +} + +interface ExtensionLoadingToastProps { + extensions: ExtensionLoadingStatus[]; + totalCount: number; + isComplete: boolean; +} + +export function GroupedExtensionLoadingToast({ + extensions, + totalCount, + isComplete, +}: ExtensionLoadingToastProps) { + const [isOpen, setIsOpen] = useState(false); + const [copiedExtension, setCopiedExtension] = useState(null); + const setView = useNavigation(); + + const successCount = extensions.filter((ext) => ext.status === 'success').length; + const errorCount = extensions.filter((ext) => ext.status === 'error').length; + + const getStatusIcon = (status: 'loading' | 'success' | 'error') => { + switch (status) { + case 'loading': + return ; + case 'success': + return
; + case 'error': + return
; + } + }; + + const getSummaryText = () => { + if (!isComplete) { + return `Loading ${totalCount} extension${totalCount !== 1 ? 's' : ''}...`; + } + + if (errorCount === 0) { + return `Successfully loaded ${successCount} extension${successCount !== 1 ? 's' : ''}`; + } + + return `Loaded ${successCount}/${totalCount} extension${totalCount !== 1 ? 's' : ''}`; + }; + + const getSummaryIcon = () => { + if (!isComplete) { + return ; + } + + if (errorCount === 0) { + return
; + } + + return
; + }; + + return ( +
+ +
+ {/* Main summary section - clickable */} + +
+
+ {getSummaryIcon()} +
+
{getSummaryText()}
+ {errorCount > 0 && ( +
+ {errorCount} extension{errorCount !== 1 ? 's' : ''} failed to load +
+ )} +
+
+
+
+ + {/* Expanded details section */} + +
+
+ {extensions.map((ext) => ( +
+
+ {getStatusIcon(ext.status)} +
{ext.name}
+
+ {ext.status === 'error' && ext.error && ( +
+
+ {formatExtensionErrorMessage(ext.error, 'Failed to add extension')} +
+ {ext.recoverHints && setView ? ( + + ) : ( + + )} +
+ )} +
+ ))} +
+
+
+ + {/* Toggle button */} + {totalCount > 0 && ( + + + + )} +
+
+
+ ); +} diff --git a/ui/desktop/src/components/LauncherView.tsx b/ui/desktop/src/components/LauncherView.tsx new file mode 100644 index 000000000000..60b0ed3f7ebd --- /dev/null +++ b/ui/desktop/src/components/LauncherView.tsx @@ -0,0 +1,44 @@ +import { useRef, useState } from 'react'; + +export default function LauncherView() { + const [query, setQuery] = useState(''); + const inputRef = useRef(null); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (query.trim()) { + // Create a new chat window with the query + const workingDir = window.appConfig?.get('GOOSE_WORKING_DIR') as string; + window.electron.createChatWindow(query, workingDir); + setQuery(''); + // Don't manually close - the blur handler will close the launcher when the new window takes focus + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + // Close on Escape + if (e.key === 'Escape') { + window.electron.closeWindow(); + } + }; + + return ( +
+
+ setQuery(e.target.value)} + onKeyDown={handleKeyDown} + className="w-full h-full bg-transparent text-text-default text-xl px-6 outline-none placeholder-text-muted" + placeholder="Ask goose anything..." + autoFocus + /> +
+
+ ); +} diff --git a/ui/desktop/src/components/__tests__/GroupedExtensionLoadingToast.test.tsx b/ui/desktop/src/components/__tests__/GroupedExtensionLoadingToast.test.tsx new file mode 100644 index 000000000000..701e9e041391 --- /dev/null +++ b/ui/desktop/src/components/__tests__/GroupedExtensionLoadingToast.test.tsx @@ -0,0 +1,80 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { GroupedExtensionLoadingToast } from '../GroupedExtensionLoadingToast'; + +const renderWithRouter = (component: React.ReactElement) => { + return render({component}); +}; + +describe('GroupedExtensionLoadingToast', () => { + it('renders loading state correctly', () => { + const extensions = [ + { name: 'developer', status: 'loading' as const }, + { name: 'memory', status: 'loading' as const }, + ]; + + renderWithRouter( + + ); + + expect(screen.getByText('Loading 2 extensions...')).toBeInTheDocument(); + expect(screen.getByText('Show details')).toBeInTheDocument(); + }); + + it('renders success state correctly', () => { + const extensions = [ + { name: 'developer', status: 'success' as const }, + { name: 'memory', status: 'success' as const }, + ]; + + renderWithRouter( + + ); + + expect(screen.getByText('Successfully loaded 2 extensions')).toBeInTheDocument(); + expect(screen.getByText('Show details')).toBeInTheDocument(); + }); + + it('renders partial failure state correctly', () => { + const extensions = [ + { name: 'developer', status: 'success' as const }, + { name: 'memory', status: 'error' as const, error: 'Failed to connect' }, + ]; + + renderWithRouter( + + ); + + expect(screen.getByText('Loaded 1/2 extensions')).toBeInTheDocument(); + expect(screen.getByText('1 extension failed to load')).toBeInTheDocument(); + expect(screen.getByText('Show details')).toBeInTheDocument(); + }); + + it('renders single extension correctly', () => { + const extensions = [{ name: 'developer', status: 'success' as const }]; + + renderWithRouter( + + ); + + expect(screen.getByText('Successfully loaded 1 extension')).toBeInTheDocument(); + }); + + it('renders mixed status states correctly', () => { + const extensions = [ + { name: 'developer', status: 'success' as const }, + { name: 'memory', status: 'loading' as const }, + { name: 'Square MCP Server', status: 'error' as const, error: 'Connection failed' }, + ]; + + renderWithRouter( + + ); + + // Summary should show loading state with error count + expect(screen.getByText('Loading 3 extensions...')).toBeInTheDocument(); + expect(screen.getByText('1 extension failed to load')).toBeInTheDocument(); + expect(screen.getByText('Show details')).toBeInTheDocument(); + }); +}); diff --git a/ui/desktop/src/components/hub.tsx b/ui/desktop/src/components/hub.tsx index 4ae2e64978d9..db4266656b3f 100644 --- a/ui/desktop/src/components/hub.tsx +++ b/ui/desktop/src/components/hub.tsx @@ -19,7 +19,7 @@ import ChatInput from './ChatInput'; import { ChatState } from '../types/chatState'; import 'react-toastify/dist/ReactToastify.css'; import { View, ViewOptions } from '../utils/navigationUtils'; -import { startAgent } from '../api'; +import { startNewSession } from '../sessions'; export default function Hub({ setView, @@ -37,28 +37,7 @@ export default function Hub({ const combinedTextFromInput = customEvent.detail?.value || ''; if (combinedTextFromInput.trim()) { - if (process.env.ALPHA) { - const newAgent = await startAgent({ - body: { - working_dir: window.appConfig.get('GOOSE_WORKING_DIR') as string, - }, - throwOnError: true, - }); - const session = newAgent.data; - setView('pair', { - disableAnimation: true, - initialMessage: combinedTextFromInput, - resumeSessionId: session.id, - }); - } else { - // Navigate to pair page with the message to be submitted - // Pair will handle creating the new chat session - resetChat(); - setView('pair', { - disableAnimation: true, - initialMessage: combinedTextFromInput, - }); - } + await startNewSession(combinedTextFromInput, resetChat, setView); e.preventDefault(); } }; @@ -78,9 +57,9 @@ export default function Hub({ commandHistory={[]} initialValue="" setView={setView} - numTokens={0} - inputTokens={0} - outputTokens={0} + totalTokens={0} + accumulatedInputTokens={0} + accumulatedOutputTokens={0} droppedFiles={[]} onFilesProcessed={() => {}} messages={[]} diff --git a/ui/desktop/src/components/pair.tsx b/ui/desktop/src/components/pair.tsx index 44ee659141fe..0aa7353ab55a 100644 --- a/ui/desktop/src/components/pair.tsx +++ b/ui/desktop/src/components/pair.tsx @@ -1,5 +1,4 @@ import { useEffect, useState } from 'react'; -import { View, ViewOptions } from '../utils/navigationUtils'; import BaseChat from './BaseChat'; import { useRecipeManager } from '../hooks/useRecipeManager'; import { useIsMobile } from '../hooks/use-mobile'; @@ -10,6 +9,7 @@ import { cn } from '../utils'; import { ChatType } from '../types/chat'; import { useSearchParams } from 'react-router-dom'; +import type { setViewType } from '../hooks/useNavigation'; export interface PairRouteState { resumeSessionId?: string; @@ -19,7 +19,7 @@ export interface PairRouteState { interface PairProps { chat: ChatType; setChat: (chat: ChatType) => void; - setView: (view: View, viewOptions?: ViewOptions) => void; + setView: setViewType; setIsGoosehintsModalOpen: (isOpen: boolean) => void; setFatalError: (value: ((prevState: string | null) => string | null) | string | null) => void; setAgentWaitingMessage: (msg: string | null) => void; diff --git a/ui/desktop/src/components/schedule/CreateScheduleModal.tsx b/ui/desktop/src/components/schedule/CreateScheduleModal.tsx index 11663e59dd49..a9d9f1c3fd80 100644 --- a/ui/desktop/src/components/schedule/CreateScheduleModal.tsx +++ b/ui/desktop/src/components/schedule/CreateScheduleModal.tsx @@ -60,9 +60,6 @@ interface CleanRecipe { prompt?: string; activities?: string[]; extensions?: CleanExtension[]; - goosehints?: string; - context?: string[]; - profile?: string; author?: { contact?: string; metadata?: string; @@ -238,10 +235,6 @@ function recipeToYaml(recipe: Recipe, executionMode: ExecutionMode): string { }); } - if (recipe.context && recipe.context.length > 0) { - cleanRecipe.context = recipe.context; - } - if (recipe.author) { cleanRecipe.author = { contact: recipe.author.contact || undefined, diff --git a/ui/desktop/src/components/sessions/SessionHistoryView.tsx b/ui/desktop/src/components/sessions/SessionHistoryView.tsx index f68eca0410c4..48624ae33022 100644 --- a/ui/desktop/src/components/sessions/SessionHistoryView.tsx +++ b/ui/desktop/src/components/sessions/SessionHistoryView.tsx @@ -31,6 +31,7 @@ import { SearchView } from '../conversation/SearchView'; import BackButton from '../ui/BackButton'; import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/Tooltip'; import { Message, Session } from '../../api'; +import { useNavigation } from '../../hooks/useNavigation'; // Helper function to determine if a message is a user message (same as useChatEngine) const isUserMessage = (message: Message): boolean => { @@ -150,6 +151,8 @@ const SessionHistoryView: React.FC = ({ const messages = session.conversation || []; + const setView = useNavigation(); + useEffect(() => { const savedSessionConfig = localStorage.getItem('session_sharing_config'); if (savedSessionConfig) { @@ -212,15 +215,14 @@ const SessionHistoryView: React.FC = ({ }); }; - const handleLaunchInNewWindow = () => { + const handleResumeSession = () => { try { - resumeSession(session); + resumeSession(session, setView); } catch (error) { toast.error(`Could not launch session: ${error instanceof Error ? error.message : error}`); } }; - // Define action buttons const actionButtons = showActionButtons ? ( <> @@ -254,7 +256,7 @@ const SessionHistoryView: React.FC = ({ ) : null} - diff --git a/ui/desktop/src/components/sessions/SessionsInsights.tsx b/ui/desktop/src/components/sessions/SessionsInsights.tsx index 713af579558f..15c2ad53578a 100644 --- a/ui/desktop/src/components/sessions/SessionsInsights.tsx +++ b/ui/desktop/src/components/sessions/SessionsInsights.tsx @@ -13,6 +13,7 @@ import { SessionInsights as ApiSessionInsights, } from '../../api'; import { resumeSession } from '../../sessions'; +import { useNavigation } from '../../hooks/useNavigation'; export function SessionInsights() { const [insights, setInsights] = useState(null); @@ -21,6 +22,7 @@ export function SessionInsights() { const [isLoading, setIsLoading] = useState(true); const [isLoadingSessions, setIsLoadingSessions] = useState(true); const navigate = useNavigate(); + const setView = useNavigation(); useEffect(() => { let loadingTimeout: ReturnType; @@ -86,9 +88,7 @@ export function SessionInsights() { const handleSessionClick = async (session: Session) => { try { - resumeSession(session, (sessionId: string) => { - navigate(`/pair?resumeSessionId=${sessionId}`); - }); + resumeSession(session, setView); } catch (error) { console.error('Failed to start session:', error); navigate('/sessions', { diff --git a/ui/desktop/src/components/settings/extensions/ExtensionsSection.tsx b/ui/desktop/src/components/settings/extensions/ExtensionsSection.tsx index d1a72be95873..172f0794928f 100644 --- a/ui/desktop/src/components/settings/extensions/ExtensionsSection.tsx +++ b/ui/desktop/src/components/settings/extensions/ExtensionsSection.tsx @@ -48,11 +48,22 @@ export default function ExtensionsSection({ const [showEnvVarsStateVar, setShowEnvVarsStateVar] = useState( showEnvVars ); + const [pendingActivationExtensions, setPendingActivationExtensions] = useState>( + new Set() + ); // Update deep link state when props change useEffect(() => { setDeepLinkConfigStateVar(deepLinkConfig); setShowEnvVarsStateVar(showEnvVars); + + if (deepLinkConfig && !showEnvVars) { + setPendingActivationExtensions((prev) => { + const updated = new Set(prev); + updated.add(deepLinkConfig.name); + return updated; + }); + } }, [deepLinkConfig, showEnvVars]); // Process extensions from context - this automatically updates when extensionsList changes @@ -102,6 +113,12 @@ export default function ExtensionsSection({ sessionId: sessionId, }); + setPendingActivationExtensions((prev) => { + const updated = new Set(prev); + updated.delete(extensionConfig.name); + return updated; + }); + await fetchExtensions(); return true; }; @@ -122,8 +139,21 @@ export default function ExtensionsSection({ extensionConfig: extensionConfig, sessionId: sessionId, }); + setPendingActivationExtensions((prev) => { + const updated = new Set(prev); + updated.delete(extensionConfig.name); + return updated; + }); } catch (error) { console.error('Failed to activate extension:', error); + // If activation fails, mark as pending if it's enabled in config + if (formData.enabled) { + setPendingActivationExtensions((prev) => { + const updated = new Set(prev); + updated.add(extensionConfig.name); + return updated; + }); + } } finally { await fetchExtensions(); if (onModalClose) { @@ -202,6 +232,7 @@ export default function ExtensionsSection({ onConfigure={handleConfigureClick} disableConfiguration={disableConfiguration} searchTerm={searchTerm} + pendingActivationExtensions={pendingActivationExtensions} /> {!hideButtons && ( diff --git a/ui/desktop/src/components/settings/extensions/agent-api.test.ts b/ui/desktop/src/components/settings/extensions/agent-api.test.ts deleted file mode 100644 index b5e943b0cc82..000000000000 --- a/ui/desktop/src/components/settings/extensions/agent-api.test.ts +++ /dev/null @@ -1,376 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { extensionApiCall, addToAgent, removeFromAgent, sanitizeName } from './agent-api'; -import * as config from '../../../config'; -import * as toasts from '../../../toasts'; -import { ExtensionConfig } from '../../../api/types.gen'; - -// Mock dependencies -vi.mock('../../../config'); -vi.mock('../../../toasts'); -vi.mock('./utils'); - -const mockGetApiUrl = vi.mocked(config.getApiUrl); -const mockToastService = vi.mocked(toasts.toastService); - -// Mock window.electron -const mockElectron = { - getSecretKey: vi.fn(), -}; - -Object.defineProperty(window, 'electron', { - value: mockElectron, - writable: true, -}); - -// Mock fetch -const mockFetch = vi.fn(); -(globalThis as typeof globalThis & { fetch: typeof mockFetch }).fetch = mockFetch; - -describe('Agent API', () => { - beforeEach(() => { - vi.clearAllMocks(); - mockGetApiUrl.mockImplementation((path: string) => `http://localhost:8080${path}`); - mockElectron.getSecretKey.mockResolvedValue('secret-key'); - mockToastService.configure = vi.fn(); - mockToastService.loading = vi.fn().mockReturnValue('toast-id'); - mockToastService.success = vi.fn(); - mockToastService.error = vi.fn(); - mockToastService.dismiss = vi.fn(); - }); - - describe('sanitizeName', () => { - it('should sanitize extension names correctly', () => { - expect(sanitizeName('Test Extension')).toBe('testextension'); - expect(sanitizeName('My-Extension_Name')).toBe('myextensionname'); - expect(sanitizeName('UPPERCASE')).toBe('uppercase'); - }); - }); - - describe('extensionApiCall', () => { - const mockExtensionConfig: ExtensionConfig = { - type: 'stdio', - description: 'description', - name: 'test-extension', - cmd: 'python', - args: ['script.py'], - }; - - it('should make successful API call for adding extension', async () => { - const mockResponse = { - ok: true, - text: vi.fn().mockResolvedValue('{"error": false}'), - }; - mockFetch.mockResolvedValue(mockResponse); - - const response = await extensionApiCall( - '/extensions/add', - mockExtensionConfig, - {}, - 'test-session' - ); - - expect(mockFetch).toHaveBeenCalledWith('http://localhost:8080/extensions/add', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': 'secret-key', - }, - body: JSON.stringify({ ...mockExtensionConfig, session_id: 'test-session' }), - }); - - expect(mockToastService.loading).toHaveBeenCalledWith({ - title: 'test-extension', - msg: 'Activating test-extension extension...', - }); - - expect(mockToastService.success).toHaveBeenCalledWith({ - title: 'test-extension', - msg: 'Successfully activated extension', - }); - - expect(response).toBe(mockResponse); - }); - - it('should make successful API call for removing extension', async () => { - const mockResponse = { - ok: true, - text: vi.fn().mockResolvedValue('{"error": false}'), - }; - mockFetch.mockResolvedValue(mockResponse); - - const response = await extensionApiCall( - '/extensions/remove', - 'test-extension', - {}, - 'test-session' - ); - - expect(mockFetch).toHaveBeenCalledWith('http://localhost:8080/extensions/remove', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': 'secret-key', - }, - body: JSON.stringify({ name: 'test-extension', session_id: 'test-session' }), - }); - - expect(mockToastService.loading).not.toHaveBeenCalled(); // No loading toast for removal - expect(mockToastService.success).toHaveBeenCalledWith({ - title: 'test-extension', - msg: 'Successfully deactivated extension', - }); - - expect(response).toBe(mockResponse); - }); - - it('should handle HTTP error responses', async () => { - const mockResponse = { - ok: false, - status: 500, - statusText: 'Internal Server Error', - }; - mockFetch.mockResolvedValue(mockResponse); - - await expect( - extensionApiCall('/extensions/add', mockExtensionConfig, {}, 'test-session') - ).rejects.toThrow('Server returned 500: Internal Server Error'); - - expect(mockToastService.error).toHaveBeenCalledWith({ - title: 'test-extension', - msg: 'Failed to add test-extension extension: Server returned 500: Internal Server Error', - traceback: 'Server returned 500: Internal Server Error', - }); - }); - - it('should handle 428 error specially', async () => { - const mockResponse = { - ok: false, - status: 428, - statusText: 'Precondition Required', - }; - mockFetch.mockResolvedValue(mockResponse); - - await expect( - extensionApiCall('/extensions/add', mockExtensionConfig, {}, 'test-session') - ).rejects.toThrow('Agent is not initialized. Please initialize the agent first.'); - - expect(mockToastService.error).toHaveBeenCalledWith({ - title: 'test-extension', - msg: 'Failed to add extension. Goose Agent was still starting up. Please try again.', - traceback: 'Server returned 428: Precondition Required', - }); - }); - - it('should handle API error responses', async () => { - const mockResponse = { - ok: true, - text: vi.fn().mockResolvedValue('{"error": true, "message": "Extension not found"}'), - }; - mockFetch.mockResolvedValue(mockResponse); - - await expect( - extensionApiCall('/extensions/remove', 'test-extension', {}, 'test-session') - ).rejects.toThrow('Error deactivating extension: Extension not found'); - - expect(mockToastService.error).toHaveBeenCalledWith({ - title: 'test-extension', - msg: 'Error deactivating extension: Extension not found', - traceback: 'Error deactivating extension: Extension not found', - }); - }); - - it('should handle JSON parse errors', async () => { - const mockResponse = { - ok: true, - text: vi.fn().mockResolvedValue('invalid json'), - }; - mockFetch.mockResolvedValue(mockResponse); - - const response = await extensionApiCall( - '/extensions/add', - mockExtensionConfig, - {}, - 'test-session' - ); - - expect(mockToastService.success).toHaveBeenCalledWith({ - title: 'test-extension', - msg: 'Successfully activated extension', - }); - - expect(response).toBe(mockResponse); - }); - - it('should handle network errors', async () => { - const networkError = new Error('Network error'); - mockFetch.mockRejectedValue(networkError); - - await expect( - extensionApiCall('/extensions/add', mockExtensionConfig, {}, 'test-session') - ).rejects.toThrow('Network error'); - - expect(mockToastService.error).toHaveBeenCalledWith({ - title: 'test-extension', - msg: 'Network error', - traceback: 'Network error', - }); - }); - - it('should configure toast service with options', async () => { - const mockResponse = { - ok: true, - text: vi.fn().mockResolvedValue('{"error": false}'), - }; - mockFetch.mockResolvedValue(mockResponse); - - await extensionApiCall( - '/extensions/add', - mockExtensionConfig, - { silent: true }, - 'test-session' - ); - - expect(mockToastService.configure).toHaveBeenCalledWith({ silent: true }); - }); - }); - - describe('addToAgent', () => { - const mockExtensionConfig: ExtensionConfig = { - type: 'stdio', - name: 'Test Extension', - description: 'Test description', - cmd: 'python', - args: ['script.py'], - }; - - it('should add stdio extension to agent with shim replacement', async () => { - const mockResponse = { - ok: true, - text: vi.fn().mockResolvedValue('{"error": false}'), - }; - mockFetch.mockResolvedValue(mockResponse); - - // Mock replaceWithShims - const { replaceWithShims } = await import('./utils'); - vi.mocked(replaceWithShims).mockResolvedValue('/path/to/python'); - - await addToAgent(mockExtensionConfig, {}, 'test-session'); - - expect(mockFetch).toHaveBeenCalledWith('http://localhost:8080/extensions/add', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': 'secret-key', - }, - body: JSON.stringify({ - ...mockExtensionConfig, - name: 'testextension', - cmd: '/path/to/python', - session_id: 'test-session', - }), - }); - }); - - it('should handle 428 error with enhanced message', async () => { - const mockResponse = { - ok: false, - status: 428, - statusText: 'Precondition Required', - }; - mockFetch.mockResolvedValue(mockResponse); - - await expect(addToAgent(mockExtensionConfig, {}, 'test-session')).rejects.toThrow( - 'Agent is not initialized. Please initialize the agent first.' - ); - }); - - it('should add non-stdio extension without shim replacement', async () => { - const sseConfig: ExtensionConfig = { - type: 'sse', - name: 'SSE Extension', - description: 'Test description', - uri: 'http://localhost:8080/events', - }; - - const mockResponse = { - ok: true, - text: vi.fn().mockResolvedValue('{"error": false}'), - }; - mockFetch.mockResolvedValue(mockResponse); - - await addToAgent(sseConfig, {}, 'test-session'); - - expect(mockFetch).toHaveBeenCalledWith('http://localhost:8080/extensions/add', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': 'secret-key', - }, - body: JSON.stringify({ - ...sseConfig, - name: 'sseextension', - session_id: 'test-session', - }), - }); - }); - - it('should not mutate the original extension config', async () => { - const originalConfig: ExtensionConfig = { - type: 'stdio', - name: 'Extension Manager', - description: 'Test description', - cmd: 'python', - args: ['script.py'], - }; - - const mockResponse = { - ok: true, - text: vi.fn().mockResolvedValue('{"error": false}'), - }; - mockFetch.mockResolvedValue(mockResponse); - - const { replaceWithShims } = await import('./utils'); - vi.mocked(replaceWithShims).mockResolvedValue('/path/to/shim'); - - await addToAgent(originalConfig, {}, 'test-session'); - - // Verify the original config was not mutated - expect(originalConfig.name).toBe('Extension Manager'); - expect(originalConfig.cmd).toBe('python'); - }); - }); - - describe('removeFromAgent', () => { - it('should remove extension from agent', async () => { - const mockResponse = { - ok: true, - text: vi.fn().mockResolvedValue('{"error": false}'), - }; - mockFetch.mockResolvedValue(mockResponse); - - await removeFromAgent('Test Extension', {}, 'test-session'); - - expect(mockFetch).toHaveBeenCalledWith('http://localhost:8080/extensions/remove', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': 'secret-key', - }, - body: JSON.stringify({ name: 'testextension', session_id: 'test-session' }), - }); - }); - - it('should handle removal errors', async () => { - const mockResponse = { - ok: false, - status: 404, - statusText: 'Not Found', - }; - mockFetch.mockResolvedValue(mockResponse); - - await expect(removeFromAgent('Test Extension', {}, 'test-session')).rejects.toThrow(); - - expect(mockToastService.error).toHaveBeenCalled(); - }); - }); -}); diff --git a/ui/desktop/src/components/settings/extensions/agent-api.ts b/ui/desktop/src/components/settings/extensions/agent-api.ts index 7143779b78d0..bd38284873c4 100644 --- a/ui/desktop/src/components/settings/extensions/agent-api.ts +++ b/ui/desktop/src/components/settings/extensions/agent-api.ts @@ -1,204 +1,92 @@ -import { ExtensionConfig } from '../../../api/types.gen'; -import { getApiUrl } from '../../../config'; -import { toastService, ToastServiceOptions } from '../../../toasts'; -import { replaceWithShims } from './utils'; +import { toastService } from '../../../toasts'; +import { agentAddExtension, ExtensionConfig, agentRemoveExtension } from '../../../api'; +import { errorMessage } from '../../../utils/conversionUtils'; +import { + createExtensionRecoverHints, + formatExtensionErrorMessage, +} from '../../../utils/extensionErrorUtils'; -interface ApiResponse { - error?: boolean; - message?: string; -} - -/** - * Makes an API call to the extension endpoints - */ -export async function extensionApiCall( - endpoint: string, - payload: ExtensionConfig | string, - options: ToastServiceOptions & { isDelete?: boolean } = {}, - sessionId: string -): Promise { - // Configure toast notifications - toastService.configure(options); - - // Determine if we're activating, deactivating, or removing an extension - const isActivating = endpoint == '/extensions/add'; - const isRemoving = options.isDelete === true; - - const action = { - type: isActivating ? 'activating' : isRemoving ? 'removing' : 'deactivating', - verb: isActivating ? 'Activating' : isRemoving ? 'Removing' : 'Deactivating', - pastTense: isActivating ? 'activated' : isRemoving ? 'removed' : 'deactivated', - presentTense: isActivating ? 'activate' : isRemoving ? 'remove' : 'deactivate', - }; - - // for adding the payload is an extensionConfig, for removing payload is just the name - const extensionName = isActivating ? (payload as ExtensionConfig).name : (payload as string); - let toastId; - - // Step 1: Show loading toast (only for activation of stdio) - if (isActivating && typeof payload === 'object' && payload.type === 'stdio') { - toastId = toastService.loading({ - title: extensionName, - msg: `${action.verb} ${extensionName} extension...`, - }); - } +export async function addToAgent( + extensionConfig: ExtensionConfig, + sessionId: string, + showToast: boolean +) { + const extensionName = extensionConfig.name; + let toastId = showToast + ? toastService.loading({ + title: extensionName, + msg: `adding ${extensionName} extension...`, + }) + : 0; try { - // Build the request body - let requestBody: ExtensionConfig | { name: string; session_id: string }; - if (typeof payload === 'object') { - // For adding extensions (ExtensionConfig) - requestBody = { ...payload, session_id: sessionId }; - } else { - // For removing extensions (just the name string) - requestBody = { name: payload, session_id: sessionId }; - } - - // Step 2: Make the API call - const response = await fetch(getApiUrl(endpoint), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': await window.electron.getSecretKey(), - }, - body: JSON.stringify(requestBody), + await agentAddExtension({ + body: { session_id: sessionId, config: extensionConfig }, + throwOnError: true, }); - - // Step 3: Handle non-successful responses - if (!response.ok) { - return handleErrorResponse(response, extensionName, action, toastId); - } - - // Step 4: Parse response data - const data = await parseResponseData(response); - - // Step 5: Check for errors in the response data - if (data.error) { - const errorMessage = `Error ${action.type} extension: ${data.message || 'Unknown error'}`; + if (showToast) { toastService.dismiss(toastId); - // Rely on the global error catch to show the copyable error toast here - throw new Error(errorMessage); + toastService.success({ + title: extensionName, + msg: `Successfully added extension`, + }); } - - // Step 6: Success - dismiss loading toast and return - toastService.dismiss(toastId); - toastService.success({ - title: extensionName, - msg: `Successfully ${action.pastTense} extension`, - }); - return response; } catch (error) { - // Final catch-all error handler - toastService.dismiss(toastId); - const errorMessage = error instanceof Error ? error.message : String(error); - const msg = - errorMessage.length < 70 ? errorMessage : `Failed to ${action.presentTense} extension`; - toastService.error({ - title: extensionName, - msg: msg, - traceback: errorMessage, - }); - console.error(`Error in extensionApiCall for ${extensionName}:`, error); + if (showToast) { + toastService.dismiss(toastId); + const errMsg = errorMessage(error); + const recoverHints = createExtensionRecoverHints(errMsg); + const msg = formatExtensionErrorMessage(errMsg, 'Failed to add extension'); + toastService.error({ + title: extensionName, + msg: msg, + traceback: errMsg, + recoverHints, + }); + } throw error; } } -// Helper functions to separate concerns - -// Handles HTTP error responses -function handleErrorResponse( - response: Response, +export async function removeFromAgent( extensionName: string, - action: { type: string; verb: string }, - toastId: string | number | undefined -): never { - const errorMsg = `Server returned ${response.status}: ${response.statusText}`; - console.error(errorMsg); + sessionId: string, + showToast: boolean +) { + let toastId = showToast + ? toastService.loading({ + title: extensionName, + msg: `Removing ${extensionName} extension...`, + }) + : 0; - // Special case: Agent not initialized (status 428) - if (response.status === 428 && action.type === 'activating') { - toastService.dismiss(toastId); - toastService.error({ - title: extensionName, - msg: 'Failed to add extension. Goose Agent was still starting up. Please try again.', - traceback: errorMsg, - }); - throw new Error('Agent is not initialized. Please initialize the agent first.'); - } - - // General error case - const msg = `Failed to ${action.type === 'activating' ? 'add' : action.type === 'removing' ? 'remove' : 'deactivate'} ${extensionName} extension: ${errorMsg}`; - toastService.dismiss(toastId); - toastService.error({ - title: extensionName, - msg: msg, - traceback: errorMsg, - }); - throw new Error(msg); -} - -// Safely parses JSON response -async function parseResponseData(response: Response): Promise { try { - const text = await response.text(); - return text ? JSON.parse(text) : { error: false }; - } catch (parseError) { - console.warn('Could not parse response as JSON, assuming success', parseError); - return { error: false }; - } -} - -/** - * Add an extension to the agent - */ -export async function addToAgent( - extension: ExtensionConfig, - options: ToastServiceOptions = {}, - sessionId: string -): Promise { - // Create a copy to avoid mutating the original extension object - const extensionCopy: ExtensionConfig = { ...extension }; - - try { - if (extensionCopy.type === 'stdio') { - extensionCopy.cmd = await replaceWithShims(extensionCopy.cmd); + await agentRemoveExtension({ + body: { session_id: sessionId, name: extensionName }, + throwOnError: true, + }); + if (showToast) { + toastService.dismiss(toastId); + toastService.success({ + title: extensionName, + msg: `Successfully removed extension`, + }); } - - extensionCopy.name = sanitizeName(extensionCopy.name); - - return await extensionApiCall('/extensions/add', extensionCopy, options, sessionId); } catch (error) { - // Check if this is a 428 error and make the message more descriptive - if (error instanceof Error && error.message && error.message.includes('428')) { - const enhancedError = new Error( - 'Failed to add extension. Goose Agent was still starting up. Please try again.' - ); - console.error( - `Failed to add extension ${extensionCopy.name} to agent: ${enhancedError.message}` - ); - throw enhancedError; + if (showToast) { + toastService.dismiss(toastId); + const errMsg = errorMessage(error); + const msg = formatExtensionErrorMessage(errMsg, 'Failed to remove extension'); + toastService.error({ + title: extensionName, + msg: msg, + traceback: errMsg, + }); } throw error; } } -/** - * Remove an extension from the agent - */ -export async function removeFromAgent( - name: string, - options: ToastServiceOptions & { isDelete?: boolean } = {}, - sessionId: string -): Promise { - try { - return await extensionApiCall('/extensions/remove', sanitizeName(name), options, sessionId); - } catch (error) { - const action = options.isDelete ? 'remove' : 'deactivate'; - console.error(`Failed to ${action} extension ${name} from agent:`, error); - throw error; - } -} - export function sanitizeName(name: string) { return name.toLowerCase().replace(/-/g, '').replace(/_/g, '').replace(/\s/g, ''); } diff --git a/ui/desktop/src/components/settings/extensions/extension-manager.test.ts b/ui/desktop/src/components/settings/extensions/extension-manager.test.ts index 7406d41485d2..0151ebca3658 100644 --- a/ui/desktop/src/components/settings/extensions/extension-manager.test.ts +++ b/ui/desktop/src/components/settings/extensions/extension-manager.test.ts @@ -1,11 +1,5 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { - activateExtension, - addToAgentOnStartup, - updateExtension, - toggleExtension, - deleteExtension, -} from './extension-manager'; +import { addToAgentOnStartup, updateExtension, toggleExtension } from './extension-manager'; import * as agentApi from './agent-api'; import * as toasts from '../../../toasts'; @@ -38,101 +32,27 @@ describe('Extension Manager', () => { mockRemoveFromConfig.mockResolvedValue(undefined); }); - describe('activateExtension', () => { - it('should successfully activate extension', async () => { - mockAddToAgent.mockResolvedValue({} as Response); - - await activateExtension({ - addToConfig: mockAddToConfig, - sessionId: 'test-session', - extensionConfig: mockExtensionConfig, - }); - - expect(mockAddToAgent).toHaveBeenCalledWith( - mockExtensionConfig, - { silent: false }, - 'test-session' - ); - expect(mockAddToConfig).toHaveBeenCalledWith('test-extension', mockExtensionConfig, true); - }); - - it('should add to config as disabled if agent fails', async () => { - const agentError = new Error('Agent failed'); - mockAddToAgent.mockRejectedValue(agentError); - - await expect( - activateExtension({ - addToConfig: mockAddToConfig, - sessionId: 'test-session', - extensionConfig: mockExtensionConfig, - }) - ).rejects.toThrow('Agent failed'); - - expect(mockAddToAgent).toHaveBeenCalledWith( - mockExtensionConfig, - { silent: false }, - 'test-session' - ); - expect(mockAddToConfig).toHaveBeenCalledWith('test-extension', mockExtensionConfig, false); - }); - - it('should remove from agent if config fails', async () => { - const configError = new Error('Config failed'); - mockAddToAgent.mockResolvedValue({} as Response); - mockAddToConfig.mockRejectedValue(configError); - - await expect( - activateExtension({ - addToConfig: mockAddToConfig, - sessionId: 'test-session', - extensionConfig: mockExtensionConfig, - }) - ).rejects.toThrow('Config failed'); - - expect(mockAddToAgent).toHaveBeenCalledWith( - mockExtensionConfig, - { silent: false }, - 'test-session' - ); - expect(mockAddToConfig).toHaveBeenCalledWith('test-extension', mockExtensionConfig, true); - expect(mockRemoveFromAgent).toHaveBeenCalledWith('test-extension', {}, 'test-session'); - }); - }); - describe('addToAgentOnStartup', () => { it('should successfully add extension on startup', async () => { - mockAddToAgent.mockResolvedValue({} as Response); + mockAddToAgent.mockResolvedValue(undefined); await addToAgentOnStartup({ - addToConfig: mockAddToConfig, sessionId: 'test-session', extensionConfig: mockExtensionConfig, }); - expect(mockAddToAgent).toHaveBeenCalledWith( - mockExtensionConfig, - { silent: true }, - 'test-session' - ); - expect(mockAddToConfig).not.toHaveBeenCalled(); + expect(mockAddToAgent).toHaveBeenCalledWith(mockExtensionConfig, 'test-session', true); }); it('should successfully add extension on startup with custom toast options', async () => { - mockAddToAgent.mockResolvedValue({} as Response); + mockAddToAgent.mockResolvedValue(undefined); await addToAgentOnStartup({ - addToConfig: mockAddToConfig, sessionId: 'test-session', extensionConfig: mockExtensionConfig, - toastOptions: { silent: false }, }); - expect(mockAddToAgent).toHaveBeenCalledWith( - mockExtensionConfig, - { silent: false }, - 'test-session' - ); - expect(mockAddToConfig).not.toHaveBeenCalled(); + expect(mockAddToAgent).toHaveBeenCalledWith(mockExtensionConfig, 'test-session', true); }); it('should retry on 428 errors', async () => { @@ -140,10 +60,9 @@ describe('Extension Manager', () => { mockAddToAgent .mockRejectedValueOnce(error428) .mockRejectedValueOnce(error428) - .mockResolvedValue({} as Response); + .mockResolvedValue(undefined); await addToAgentOnStartup({ - addToConfig: mockAddToConfig, sessionId: 'test-session', extensionConfig: mockExtensionConfig, }); @@ -151,30 +70,24 @@ describe('Extension Manager', () => { expect(mockAddToAgent).toHaveBeenCalledTimes(3); }); - it('should disable extension after max retries', async () => { + it('should throw error after max retries', async () => { const error428 = new Error('428 Precondition Required'); mockAddToAgent.mockRejectedValue(error428); - mockToastService.configure = vi.fn(); - mockToastService.error = vi.fn(); - await addToAgentOnStartup({ - addToConfig: mockAddToConfig, - sessionId: 'test-session', - extensionConfig: mockExtensionConfig, - }); + await expect( + addToAgentOnStartup({ + sessionId: 'test-session', + extensionConfig: mockExtensionConfig, + }) + ).rejects.toThrow('428 Precondition Required'); expect(mockAddToAgent).toHaveBeenCalledTimes(4); // Initial + 3 retries - expect(mockToastService.error).toHaveBeenCalledWith({ - title: 'test-extension', - msg: 'Extension failed to start and will be disabled.', - traceback: '428 Precondition Required', - }); }); }); describe('updateExtension', () => { it('should update extension without name change', async () => { - mockAddToAgent.mockResolvedValue({} as Response); + mockAddToAgent.mockResolvedValue(undefined); mockAddToConfig.mockResolvedValue(undefined); mockToastService.success = vi.fn(); @@ -187,11 +100,6 @@ describe('Extension Manager', () => { originalName: 'test-extension', }); - expect(mockAddToAgent).toHaveBeenCalledWith( - { ...mockExtensionConfig, name: 'test-extension' }, - { silent: true }, - 'test-session' - ); expect(mockAddToConfig).toHaveBeenCalledWith( 'test-extension', { ...mockExtensionConfig, name: 'test-extension' }, @@ -204,8 +112,8 @@ describe('Extension Manager', () => { }); it('should handle name change by removing old and adding new', async () => { - mockAddToAgent.mockResolvedValue({} as Response); - mockRemoveFromAgent.mockResolvedValue({} as Response); + mockAddToAgent.mockResolvedValue(undefined); + mockRemoveFromAgent.mockResolvedValue(undefined); mockRemoveFromConfig.mockResolvedValue(undefined); mockAddToConfig.mockResolvedValue(undefined); mockToastService.success = vi.fn(); @@ -219,16 +127,11 @@ describe('Extension Manager', () => { originalName: 'old-extension', }); - expect(mockRemoveFromAgent).toHaveBeenCalledWith( - 'old-extension', - { silent: true }, - 'test-session' - ); expect(mockRemoveFromConfig).toHaveBeenCalledWith('old-extension'); expect(mockAddToAgent).toHaveBeenCalledWith( { ...mockExtensionConfig, name: 'new-extension' }, - { silent: true }, - 'test-session' + 'test-session', + false ); expect(mockAddToConfig).toHaveBeenCalledWith( 'new-extension', @@ -265,7 +168,7 @@ describe('Extension Manager', () => { describe('toggleExtension', () => { it('should toggle extension on successfully', async () => { - mockAddToAgent.mockResolvedValue({} as Response); + mockAddToAgent.mockResolvedValue(undefined); mockAddToConfig.mockResolvedValue(undefined); await toggleExtension({ @@ -275,12 +178,12 @@ describe('Extension Manager', () => { sessionId: 'test-session', }); - expect(mockAddToAgent).toHaveBeenCalledWith(mockExtensionConfig, {}, 'test-session'); + expect(mockAddToAgent).toHaveBeenCalledWith(mockExtensionConfig, 'test-session', true); expect(mockAddToConfig).toHaveBeenCalledWith('test-extension', mockExtensionConfig, true); }); it('should toggle extension off successfully', async () => { - mockRemoveFromAgent.mockResolvedValue({} as Response); + mockRemoveFromAgent.mockResolvedValue(undefined); mockAddToConfig.mockResolvedValue(undefined); await toggleExtension({ @@ -290,7 +193,7 @@ describe('Extension Manager', () => { sessionId: 'test-session', }); - expect(mockRemoveFromAgent).toHaveBeenCalledWith('test-extension', {}, 'test-session'); + expect(mockRemoveFromAgent).toHaveBeenCalledWith('test-extension', 'test-session', true); expect(mockAddToConfig).toHaveBeenCalledWith('test-extension', mockExtensionConfig, false); }); @@ -308,14 +211,14 @@ describe('Extension Manager', () => { }) ).rejects.toThrow('Agent failed'); - expect(mockAddToAgent).toHaveBeenCalledWith(mockExtensionConfig, {}, 'test-session'); + expect(mockAddToAgent).toHaveBeenCalledWith(mockExtensionConfig, 'test-session', true); // addToConfig is called during the rollback (toggleOff) expect(mockAddToConfig).toHaveBeenCalledWith('test-extension', mockExtensionConfig, false); }); it('should remove from agent if config update fails when toggling on', async () => { const configError = new Error('Config failed'); - mockAddToAgent.mockResolvedValue({} as Response); + mockAddToAgent.mockResolvedValue(undefined); mockAddToConfig.mockRejectedValue(configError); await expect( @@ -327,9 +230,9 @@ describe('Extension Manager', () => { }) ).rejects.toThrow('Config failed'); - expect(mockAddToAgent).toHaveBeenCalledWith(mockExtensionConfig, {}, 'test-session'); + expect(mockAddToAgent).toHaveBeenCalledWith(mockExtensionConfig, 'test-session', true); expect(mockAddToConfig).toHaveBeenCalledWith('test-extension', mockExtensionConfig, true); - expect(mockRemoveFromAgent).toHaveBeenCalledWith('test-extension', {}, 'test-session'); + expect(mockRemoveFromAgent).toHaveBeenCalledWith('test-extension', 'test-session', true); }); it('should update config even if agent removal fails when toggling off', async () => { @@ -346,71 +249,7 @@ describe('Extension Manager', () => { }) ).rejects.toThrow('Agent removal failed'); - expect(mockRemoveFromAgent).toHaveBeenCalledWith('test-extension', {}, 'test-session'); expect(mockAddToConfig).toHaveBeenCalledWith('test-extension', mockExtensionConfig, false); }); }); - - describe('deleteExtension', () => { - it('should delete extension successfully', async () => { - mockRemoveFromAgent.mockResolvedValue({} as Response); - mockRemoveFromConfig.mockResolvedValue(undefined); - - await deleteExtension({ - name: 'test-extension', - removeFromConfig: mockRemoveFromConfig, - sessionId: 'test-session', - }); - - expect(mockRemoveFromAgent).toHaveBeenCalledWith( - 'test-extension', - { isDelete: true }, - 'test-session' - ); - expect(mockRemoveFromConfig).toHaveBeenCalledWith('test-extension'); - }); - - it('should remove from config even if agent removal fails', async () => { - const agentError = new Error('Agent removal failed'); - mockRemoveFromAgent.mockRejectedValue(agentError); - mockRemoveFromConfig.mockResolvedValue(undefined); - - await expect( - deleteExtension({ - name: 'test-extension', - removeFromConfig: mockRemoveFromConfig, - sessionId: 'test-session', - }) - ).rejects.toThrow('Agent removal failed'); - - expect(mockRemoveFromAgent).toHaveBeenCalledWith( - 'test-extension', - { isDelete: true }, - 'test-session' - ); - expect(mockRemoveFromConfig).toHaveBeenCalledWith('test-extension'); - }); - - it('should throw config error if both agent and config fail', async () => { - const agentError = new Error('Agent removal failed'); - const configError = new Error('Config removal failed'); - mockRemoveFromAgent.mockRejectedValue(agentError); - mockRemoveFromConfig.mockRejectedValue(configError); - - await expect( - deleteExtension({ - name: 'test-extension', - removeFromConfig: mockRemoveFromConfig, - sessionId: 'test-session', - }) - ).rejects.toThrow('Config removal failed'); - - expect(mockRemoveFromAgent).toHaveBeenCalledWith( - 'test-extension', - { isDelete: true }, - 'test-session' - ); - expect(mockRemoveFromConfig).toHaveBeenCalledWith('test-extension'); - }); - }); }); diff --git a/ui/desktop/src/components/settings/extensions/extension-manager.ts b/ui/desktop/src/components/settings/extensions/extension-manager.ts index a27693eadf5e..3da183fdca58 100644 --- a/ui/desktop/src/components/settings/extensions/extension-manager.ts +++ b/ui/desktop/src/components/settings/extensions/extension-manager.ts @@ -60,23 +60,20 @@ export async function activateExtension({ }: ActivateExtensionProps): Promise { try { // AddToAgent - await addToAgent(extensionConfig, { silent: false }, sessionId); + await addToAgent(extensionConfig, sessionId, true); } catch (error) { console.error('Failed to add extension to agent:', error); - // add to config with enabled = false await addToConfig(extensionConfig.name, extensionConfig, false); - // Rethrow the error to inform the caller throw error; } - // Then add to config try { await addToConfig(extensionConfig.name, extensionConfig, true); } catch (error) { console.error('Failed to add extension to config:', error); // remove from Agent try { - await removeFromAgent(extensionConfig.name, {}, sessionId); + await removeFromAgent(extensionConfig.name, sessionId, true); } catch (removeError) { console.error('Failed to remove extension from agent after config failure:', removeError); } @@ -86,7 +83,6 @@ export async function activateExtension({ } interface AddToAgentOnStartupProps { - addToConfig: (name: string, extensionConfig: ExtensionConfig, enabled: boolean) => Promise; extensionConfig: ExtensionConfig; toastOptions?: ToastServiceOptions; sessionId: string; @@ -94,43 +90,27 @@ interface AddToAgentOnStartupProps { /** * Adds an extension to the agent during application startup with retry logic + * + * TODO(Douwe): Delete this after basecamp lands */ export async function addToAgentOnStartup({ - addToConfig, extensionConfig, - toastOptions = { silent: true }, sessionId, + toastOptions, }: AddToAgentOnStartupProps): Promise { - try { - await retryWithBackoff(() => addToAgent(extensionConfig, toastOptions, sessionId), { - retries: 3, - delayMs: 1000, - shouldRetry: (error: ExtensionError) => - !!error.message && - (error.message.includes('428') || - error.message.includes('Precondition Required') || - error.message.includes('Agent is not initialized')), - }); - } catch (finalError) { - toastService.configure({ silent: false }); - toastService.error({ - title: extensionConfig.name, - msg: 'Extension failed to start and will be disabled.', - traceback: finalError instanceof Error ? finalError.message : String(finalError), - }); - - try { - await toggleExtension({ - toggle: 'toggleOff', - extensionConfig, - addToConfig, - toastOptions: { silent: true }, - sessionId, - }); - } catch (toggleErr) { - console.error('Failed to toggle off after error:', toggleErr); - } - } + const showToast = !toastOptions?.silent; + + // Errors are caught by the grouped notification in providerUtils.ts + // Individual error toasts are suppressed during startup (showToast=false) + await retryWithBackoff(() => addToAgent(extensionConfig, sessionId, showToast), { + retries: 3, + delayMs: 1000, + shouldRetry: (error: ExtensionError) => + !!error.message && + (error.message.includes('428') || + error.message.includes('Precondition Required') || + error.message.includes('Agent is not initialized')), + }); } interface UpdateExtensionProps { @@ -165,7 +145,7 @@ export async function updateExtension({ // First remove the old extension from agent (using original name) try { - await removeFromAgent(originalName!, { silent: true }, sessionId); // Suppress removal toast since we'll show update toast + await removeFromAgent(originalName!, sessionId, false); } catch (error) { console.error('Failed to remove old extension from agent during rename:', error); // Continue with the process even if agent removal fails @@ -188,8 +168,7 @@ export async function updateExtension({ // Add new extension with sanitized name if (enabled) { try { - // AddToAgent with silent option to avoid duplicate toasts - await addToAgent(sanitizedExtensionConfig, { silent: true }, sessionId); + await addToAgent(sanitizedExtensionConfig, sessionId, false); } catch (error) { console.error('[updateExtension]: Failed to add renamed extension to agent:', error); throw error; @@ -218,8 +197,7 @@ export async function updateExtension({ if (enabled) { try { - // AddToAgent with silent option to avoid duplicate toasts - await addToAgent(sanitizedExtensionConfig, { silent: true }, sessionId); + await addToAgent(sanitizedExtensionConfig, sessionId, false); } catch (error) { console.error('[updateExtension]: Failed to add extension to agent during update:', error); // Failed to add to agent -- show that error to user and do not update the config file @@ -278,13 +256,7 @@ export async function toggleExtension({ if (toggle == 'toggleOn') { try { // add to agent with toast options - await addToAgent( - extensionConfig, - { - ...toastOptions, - }, - sessionId - ); + await addToAgent(extensionConfig, sessionId, !toastOptions?.silent); } catch (error) { console.error('Error adding extension to agent. Will try to toggle back off.'); try { @@ -308,7 +280,7 @@ export async function toggleExtension({ console.error('Failed to update config after enabling extension:', error); // remove from agent try { - await removeFromAgent(extensionConfig.name, toastOptions, sessionId); + await removeFromAgent(extensionConfig.name, sessionId, !toastOptions?.silent); } catch (removeError) { console.error('Failed to remove extension from agent after config failure:', removeError); } @@ -318,7 +290,7 @@ export async function toggleExtension({ // enabled to disabled let agentRemoveError = null; try { - await removeFromAgent(extensionConfig.name, toastOptions, sessionId); + await removeFromAgent(extensionConfig.name, sessionId, !toastOptions?.silent); } catch (error) { // note there was an error, but attempt to remove from config anyway console.error('Error removing extension from agent', extensionConfig.name, error); @@ -353,7 +325,7 @@ export async function deleteExtension({ name, removeFromConfig, sessionId }: Del // remove from agent let agentRemoveError = null; try { - await removeFromAgent(name, { isDelete: true }, sessionId); + await removeFromAgent(name, sessionId, true); } catch (error) { console.error('Failed to remove extension from agent during deletion:', error); agentRemoveError = error; diff --git a/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx b/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx index 44b26345f843..947a6d02f288 100644 --- a/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx +++ b/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx @@ -353,7 +353,6 @@ export default function ExtensionModal({ submitAttempted={submitAttempted} /> - {/* Divider */}
{/* Command */} @@ -374,42 +373,43 @@ export default function ExtensionModal({ />
- {/* Divider */} -
+ {formData.type === 'stdio' && ( + <> +
+ +
+ +
+ + )} - {/* Environment Variables */} -
- -
+ {formData.type === 'streamable_http' && ( + <> + {/* Divider */} +
+ +
+ +
+ + )}
)} - {/* Request Headers - Only for streamable_http */} - {formData.type === 'streamable_http' && ( - <> - {/* Divider */} -
- -
- -
- - )} - {showDeleteConfirmation ? ( <> diff --git a/ui/desktop/src/components/settings/extensions/subcomponents/ExtensionItem.tsx b/ui/desktop/src/components/settings/extensions/subcomponents/ExtensionItem.tsx index 872142dfbce9..f677260332eb 100644 --- a/ui/desktop/src/components/settings/extensions/subcomponents/ExtensionItem.tsx +++ b/ui/desktop/src/components/settings/extensions/subcomponents/ExtensionItem.tsx @@ -11,6 +11,7 @@ interface ExtensionItemProps { onToggle: (extension: FixedExtensionEntry) => Promise | void; onConfigure?: (extension: FixedExtensionEntry) => void; isStatic?: boolean; // to not allow users to edit configuration + isPendingActivation?: boolean; } export default function ExtensionItem({ @@ -18,6 +19,7 @@ export default function ExtensionItem({ onToggle, onConfigure, isStatic, + isPendingActivation = false, }: ExtensionItemProps) { // Add local state to track the visual toggle state const [visuallyEnabled, setVisuallyEnabled] = useState(extension.enabled); @@ -79,7 +81,17 @@ export default function ExtensionItem({ onClick={() => handleToggle(extension)} > - {getFriendlyTitle(extension)} + + {getFriendlyTitle(extension)} + {isPendingActivation && ( + + Pending + + )} + e.stopPropagation()}>
diff --git a/ui/desktop/src/components/settings/extensions/subcomponents/ExtensionList.tsx b/ui/desktop/src/components/settings/extensions/subcomponents/ExtensionList.tsx index 1c1b37526702..a2ab0b121fbf 100644 --- a/ui/desktop/src/components/settings/extensions/subcomponents/ExtensionList.tsx +++ b/ui/desktop/src/components/settings/extensions/subcomponents/ExtensionList.tsx @@ -1,8 +1,8 @@ -import { FixedExtensionEntry } from '../../../ConfigContext'; -import { ExtensionConfig } from '../../../../api/types.gen'; import ExtensionItem from './ExtensionItem'; import builtInExtensionsData from '../../../../built-in-extensions.json'; -import { combineCmdAndArgs, removeShims } from '../utils'; +import { combineCmdAndArgs } from '../utils'; +import { ExtensionConfig } from '../../../../api'; +import { FixedExtensionEntry } from '../../../ConfigContext'; interface ExtensionListProps { extensions: FixedExtensionEntry[]; @@ -11,6 +11,7 @@ interface ExtensionListProps { isStatic?: boolean; disableConfiguration?: boolean; searchTerm?: string; + pendingActivationExtensions?: Set; } export default function ExtensionList({ @@ -20,6 +21,7 @@ export default function ExtensionList({ isStatic, disableConfiguration: _disableConfiguration, searchTerm = '', + pendingActivationExtensions = new Set(), }: ExtensionListProps) { const matchesSearch = (extension: FixedExtensionEntry): boolean => { if (!searchTerm) return true; @@ -63,6 +65,7 @@ export default function ExtensionList({ onToggle={onToggle} onConfigure={onConfigure} isStatic={isStatic} + isPendingActivation={pendingActivationExtensions.has(extension.name)} /> ))}
@@ -132,7 +135,7 @@ export function getSubtitle(config: ExtensionConfig) { default: return { description: config.description || null, - command: 'cmd' in config ? combineCmdAndArgs(removeShims(config.cmd), config.args) : null, + command: 'cmd' in config ? combineCmdAndArgs(config.cmd, config.args) : null, }; } } diff --git a/ui/desktop/src/components/settings/extensions/utils.test.ts b/ui/desktop/src/components/settings/extensions/utils.test.ts index dce8ea94ce5d..98c90963d427 100644 --- a/ui/desktop/src/components/settings/extensions/utils.test.ts +++ b/ui/desktop/src/components/settings/extensions/utils.test.ts @@ -6,8 +6,6 @@ import { createExtensionConfig, splitCmdAndArgs, combineCmdAndArgs, - replaceWithShims, - removeShims, extractCommand, extractExtensionName, DEFAULT_EXTENSION_TIMEOUT, @@ -342,54 +340,6 @@ describe('Extension Utils', () => { }); }); - describe('replaceWithShims', () => { - beforeEach(() => { - mockElectron.getBinaryPath.mockImplementation((binary: string) => { - const paths: Record = { - goosed: '/path/to/goosed', - jbang: '/path/to/jbang', - npx: '/path/to/npx', - uvx: '/path/to/uvx', - }; - return Promise.resolve(paths[binary] || binary); - }); - }); - - it('should replace known commands with shim paths', async () => { - expect(await replaceWithShims('goosed')).toBe('/path/to/goosed'); - expect(await replaceWithShims('jbang')).toBe('/path/to/jbang'); - expect(await replaceWithShims('npx')).toBe('/path/to/npx'); - expect(await replaceWithShims('uvx')).toBe('/path/to/uvx'); - }); - - it('should leave unknown commands unchanged', async () => { - expect(await replaceWithShims('python')).toBe('python'); - expect(await replaceWithShims('node')).toBe('node'); - }); - }); - - describe('removeShims', () => { - it('should remove shim paths and return command name', () => { - expect(removeShims('/path/to/goosed')).toBe('goosed'); - expect(removeShims('/usr/local/bin/jbang')).toBe('jbang'); - expect(removeShims('/Applications/Docker.app/Contents/Resources/bin/docker')).toBe('docker'); - expect(removeShims('/path/to/npx.cmd')).toBe('npx.cmd'); - }); - - it('should handle paths with trailing slashes', () => { - // The removeShims function only works if the path ends with the shim pattern - // Trailing slashes prevent the pattern from matching - expect(removeShims('/path/to/goosed/')).toBe('/path/to/goosed/'); - expect(removeShims('/path/to/uvx//')).toBe('/path/to/uvx//'); - }); - - it('should leave non-shim commands unchanged', () => { - expect(removeShims('python')).toBe('python'); - expect(removeShims('node')).toBe('node'); - expect(removeShims('/usr/bin/python3')).toBe('/usr/bin/python3'); - }); - }); - describe('extractCommand', () => { it('should extract command from extension link', () => { const link = 'goose://extension/add?name=Test&cmd=python&arg=script.py&arg=--flag'; diff --git a/ui/desktop/src/components/settings/extensions/utils.ts b/ui/desktop/src/components/settings/extensions/utils.ts index 137853034774..b561ea1437ec 100644 --- a/ui/desktop/src/components/settings/extensions/utils.ts +++ b/ui/desktop/src/components/settings/extensions/utils.ts @@ -190,41 +190,6 @@ export function combineCmdAndArgs(cmd: string, args: string[]): string { return [cmd, ...args].join(' '); } -export async function replaceWithShims(cmd: string) { - const binaryPathMap: Record = { - goosed: await window.electron.getBinaryPath('goosed'), - jbang: await window.electron.getBinaryPath('jbang'), - npx: await window.electron.getBinaryPath('npx'), - uvx: await window.electron.getBinaryPath('uvx'), - }; - - if (binaryPathMap[cmd]) { - console.log('--------> Replacing command with shim ------>', cmd, binaryPathMap[cmd]); - cmd = binaryPathMap[cmd]; - } - - return cmd; -} - -export function removeShims(cmd: string) { - // Only remove shims if the path matches our known shim patterns - const shimPatterns = [/cu$/, /goosed$/, /docker$/, /jbang$/, /npx$/, /uvx$/, /npx.cmd$/]; - - // Check if the command matches any shim pattern - const isShim = shimPatterns.some((pattern) => pattern.test(cmd)); - - if (isShim) { - const segments = cmd.split('/'); - // Filter out any empty segments (which can happen with trailing slashes) - const nonEmptySegments = segments.filter((segment) => segment.length > 0); - // Return the last segment or empty string if there are no segments - return nonEmptySegments.length > 0 ? nonEmptySegments[nonEmptySegments.length - 1] : ''; - } - - // If it's not a shim, return the original command - return cmd; -} - export function extractCommand(link: string): string { const url = new URL(link); const cmd = url.searchParams.get('cmd') || 'Unknown Command'; diff --git a/ui/desktop/src/components/settings/providers/ProviderGrid.tsx b/ui/desktop/src/components/settings/providers/ProviderGrid.tsx index b4bb70fcf23e..f970e9335678 100644 --- a/ui/desktop/src/components/settings/providers/ProviderGrid.tsx +++ b/ui/desktop/src/components/settings/providers/ProviderGrid.tsx @@ -1,7 +1,6 @@ import React, { memo, useMemo, useCallback, useState } from 'react'; import { ProviderCard } from './subcomponents/ProviderCard'; import CardContainer from './subcomponents/CardContainer'; -import { ProviderModalProvider, useProviderModal } from './modal/ProviderModalProvider'; import ProviderConfigurationModal from './modal/ProviderConfiguationModal'; import { DeclarativeProviderConfig, @@ -47,7 +46,7 @@ const CustomProviderCard = memo(function CustomProviderCard({ onClick }: { onCli ); }); -const ProviderCards = memo(function ProviderCards({ +function ProviderCards({ providers, isOnboarding, refreshProviders, @@ -58,7 +57,7 @@ const ProviderCards = memo(function ProviderCards({ refreshProviders?: () => void; onProviderLaunch: (provider: ProviderDetails) => void; }) { - const { openModal } = useProviderModal(); + const [configuringProvider, setConfiguringProvider] = useState(null); const [showCustomProviderModal, setShowCustomProviderModal] = useState(false); const [editingProvider, setEditingProvider] = useState<{ id: string; @@ -66,6 +65,11 @@ const ProviderCards = memo(function ProviderCards({ isEditable: boolean; } | null>(null); + const openModal = useCallback( + (provider: ProviderDetails) => setConfiguringProvider(provider), + [] + ); + const configureProviderViaModal = useCallback( async (provider: ProviderDetails) => { if (provider.provider_type === 'Custom' || provider.provider_type === 'Declarative') { @@ -81,22 +85,10 @@ const ProviderCards = memo(function ProviderCards({ setShowCustomProviderModal(true); } } else { - openModal(provider, { - onSubmit: () => { - if (refreshProviders) { - refreshProviders(); - } - }, - onDelete: (_values: unknown) => { - if (refreshProviders) { - refreshProviders(); - } - }, - formProps: {}, - }); + openModal(provider); } }, - [openModal, refreshProviders] + [openModal] ); const handleUpdateCustomProvider = useCallback( @@ -123,20 +115,12 @@ const ProviderCards = memo(function ProviderCards({ setEditingProvider(null); }, []); - const deleteProviderConfigViaModal = useCallback( - (provider: ProviderDetails) => { - openModal(provider, { - onDelete: (_values: unknown) => { - // Only refresh if the function is provided - if (refreshProviders) { - refreshProviders(); - } - }, - formProps: {}, - }); - }, - [openModal, refreshProviders] - ); + const onCloseProviderConfig = useCallback(() => { + setConfiguringProvider(null); + if (refreshProviders) { + refreshProviders(); + } + }, [refreshProviders]); const handleCreateCustomProvider = useCallback( async (data: UpdateCustomProviderRequest) => { @@ -160,7 +144,6 @@ const ProviderCards = memo(function ProviderCards({ key={provider.name} provider={provider} onConfigure={() => configureProviderViaModal(provider)} - onDelete={() => deleteProviderConfigViaModal(provider)} onLaunch={() => onProviderLaunch(provider)} isOnboarding={isOnboarding} /> @@ -171,13 +154,7 @@ const ProviderCards = memo(function ProviderCards({ ); return cards; - }, [ - providers, - isOnboarding, - configureProviderViaModal, - deleteProviderConfigViaModal, - onProviderLaunch, - ]); + }, [providers, isOnboarding, configureProviderViaModal, onProviderLaunch]); const initialData = editingProvider && { engine: editingProvider.config.engine.toLowerCase() + '_compatible', @@ -206,11 +183,17 @@ const ProviderCards = memo(function ProviderCards({ /> {' '} + {configuringProvider && ( + + )} ); -}); +} -export default memo(function ProviderGrid({ +export default function ProviderGrid({ providers, isOnboarding, refreshProviders, @@ -221,20 +204,14 @@ export default memo(function ProviderGrid({ refreshProviders?: () => void; onProviderLaunch?: (provider: ProviderDetails) => void; }) { - // Memoize the modal provider and its children to avoid recreating on every render - const modalProviderContent = useMemo( - () => ( - - {})} - /> - - - ), - [providers, isOnboarding, refreshProviders, onProviderLaunch] + return ( + + {})} + /> + ); - return {modalProviderContent}; -}); +} diff --git a/ui/desktop/src/components/settings/providers/modal/ProviderConfiguationModal.tsx b/ui/desktop/src/components/settings/providers/modal/ProviderConfiguationModal.tsx index b8971a69b6de..e04b5ec49a13 100644 --- a/ui/desktop/src/components/settings/providers/modal/ProviderConfiguationModal.tsx +++ b/ui/desktop/src/components/settings/providers/modal/ProviderConfiguationModal.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { Dialog, DialogContent, @@ -7,198 +7,123 @@ import { DialogHeader, DialogTitle, } from '../../../ui/dialog'; -import DefaultProviderSetupForm from './subcomponents/forms/DefaultProviderSetupForm'; +import DefaultProviderSetupForm, { + ConfigInput, +} from './subcomponents/forms/DefaultProviderSetupForm'; import ProviderSetupActions from './subcomponents/ProviderSetupActions'; import ProviderLogo from './subcomponents/ProviderLogo'; -import { useProviderModal } from './ProviderModalProvider'; import { SecureStorageNotice } from './subcomponents/SecureStorageNotice'; -import { DefaultSubmitHandler } from './subcomponents/handlers/DefaultSubmitHandler'; -import OllamaSubmitHandler from './subcomponents/handlers/OllamaSubmitHandler'; -import OllamaForm from './subcomponents/forms/OllamaForm'; +import { providerConfigSubmitHandler } from './subcomponents/handlers/DefaultSubmitHandler'; import { useConfig } from '../../../ConfigContext'; import { useModelAndProvider } from '../../../ModelAndProviderContext'; import { AlertTriangle } from 'lucide-react'; -import { ConfigKey, removeCustomProvider } from '../../../../api'; +import { ProviderDetails, removeCustomProvider } from '../../../../api'; -interface FormValues { - [key: string]: string | number | boolean | null; +interface ProviderConfigurationModalProps { + provider: ProviderDetails; + onClose: () => void; } -const customSubmitHandlerMap: Record = { - provider_name: OllamaSubmitHandler, // example -}; - -const customFormsMap: Record = { - provider_name: OllamaForm, // example -}; - -export default function ProviderConfigurationModal() { +export default function ProviderConfigurationModal({ + provider, + onClose, +}: ProviderConfigurationModalProps) { const [validationErrors, setValidationErrors] = useState>({}); const { upsert, remove } = useConfig(); const { getCurrentModelAndProvider } = useModelAndProvider(); - const { isOpen, currentProvider, modalProps, closeModal } = useProviderModal(); - const [configValues, setConfigValues] = useState>({}); + const [configValues, setConfigValues] = useState>({}); const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false); - const [isActiveProvider, setIsActiveProvider] = useState(false); // New state for tracking active provider - const [requiredParameters, setRequiredParameters] = useState([]); // New state for tracking active provider + const [isActiveProvider, setIsActiveProvider] = useState(false); - useEffect(() => { - if (isOpen && currentProvider) { - // Reset form state when the modal opens with a new provider - const requiredParameters = currentProvider.metadata.config_keys.filter( - (param) => param.required === true - ); - setRequiredParameters(requiredParameters); - setConfigValues({}); - setValidationErrors({}); - setShowDeleteConfirmation(false); - setIsActiveProvider(false); // Reset active provider state - } - }, [isOpen, currentProvider]); - - if (!isOpen || !currentProvider) return null; + const requiredParameters = provider.metadata.config_keys.filter( + (param) => param.required === true + ); - const isConfigured = currentProvider.is_configured; + const isConfigured = provider.is_configured; const headerText = showDeleteConfirmation - ? `Delete configuration for ${currentProvider.metadata.display_name}` - : `Configure ${currentProvider.metadata.display_name}`; + ? `Delete configuration for ${provider.metadata.display_name}` + : `Configure ${provider.metadata.display_name}`; - // Modify description text to show warning if it's the active provider const descriptionText = showDeleteConfirmation ? isActiveProvider ? `You cannot delete this provider while it's currently in use. Please switch to a different model first.` : 'This will permanently delete the current provider configuration.' : `Add your API key(s) for this provider to integrate into Goose`; - const SubmitHandler = - (customSubmitHandlerMap[currentProvider.name] as typeof DefaultSubmitHandler) || - DefaultSubmitHandler; - const FormComponent = - (customFormsMap[currentProvider.name] as typeof DefaultProviderSetupForm) || - DefaultProviderSetupForm; - const handleSubmitForm = async (e: React.FormEvent) => { e.preventDefault(); - console.log('Form submitted for:', currentProvider.name); - // Reset previous validation errors setValidationErrors({}); - // Validation logic - const parameters = currentProvider.metadata.config_keys || []; + const parameters = provider.metadata.config_keys || []; const errors: Record = {}; - // Check required fields parameters.forEach((parameter) => { if ( parameter.required && - (configValues[parameter.name] === undefined || - configValues[parameter.name] === null || - configValues[parameter.name] === '') + !configValues[parameter.name]?.value && + !configValues[parameter.name]?.serverValue ) { errors[parameter.name] = `${parameter.name} is required`; } }); - // If there are validation errors, stop the submission if (Object.keys(errors).length > 0) { setValidationErrors(errors); - return; // Stop the submission process + return; } - try { - // Wait for the submission to complete - await SubmitHandler(upsert, currentProvider, configValues); - - // Close the modal before triggering refreshes to avoid UI issues - closeModal(); + const toSubmit = Object.fromEntries( + Object.entries(configValues) + .filter(([_k, entry]) => !!entry.value) + .map(([k, entry]) => [k, entry.value || '']) + ); - // Call onSubmit callback if provided (from modal props) - if (modalProps.onSubmit) { - modalProps.onSubmit(configValues as FormValues); - } - } catch (error) { - console.error('Failed to save configuration:', error); - // Keep modal open if there's an error - } + await providerConfigSubmitHandler(upsert, provider, toSubmit); + onClose(); }; const handleCancel = () => { - // Reset delete confirmation state - setShowDeleteConfirmation(false); - setIsActiveProvider(false); - - // Use custom cancel handler if provided - if (modalProps.onCancel) { - modalProps.onCancel(); - } - - closeModal(); + onClose(); }; const handleDelete = async () => { - // Check if this is the currently active provider try { const providerModel = await getCurrentModelAndProvider(); - if (currentProvider.name === providerModel.provider) { - // It's the active provider - set state and show warning + if (provider.name === providerModel.provider) { setIsActiveProvider(true); setShowDeleteConfirmation(true); - return; // Exit early - don't allow actual deletion + return; } } catch (error) { console.error('Failed to check current provider:', error); } - // If we get here, it's not the active provider setIsActiveProvider(false); setShowDeleteConfirmation(true); }; const handleConfirmDelete = async () => { - // Don't proceed if this is the active provider if (isActiveProvider) { return; } - try { - const isCustomProvider = currentProvider.provider_type === 'Custom'; - - if (isCustomProvider) { - await removeCustomProvider({ - path: { id: currentProvider.name }, - }); - } else { - // Remove the provider configuration - // get the keys - const params = currentProvider.metadata.config_keys; - - // go through the keys are remove them - for (const param of params) { - await remove(param.name, param.secret); - } - } + const isCustomProvider = provider.provider_type === 'Custom'; - // Call onDelete callback if provided - // This should trigger the refreshProviders function - if (modalProps.onDelete) { - modalProps.onDelete(currentProvider.name as unknown as FormValues); + if (isCustomProvider) { + await removeCustomProvider({ + path: { id: provider.name }, + }); + } else { + const params = provider.metadata.config_keys; + for (const param of params) { + await remove(param.name, param.secret); } - - // Reset the delete confirmation state before closing - setShowDeleteConfirmation(false); - setIsActiveProvider(false); - - // Close the modal - // Close the modal after deletion and callback - closeModal(); - } catch (error) { - console.error('Failed to delete provider:', error); - // Keep modal open if there's an error } + + onClose(); }; - // Function to determine which icon to display const getModalIcon = () => { if (showDeleteConfirmation) { return ( @@ -208,11 +133,11 @@ export default function ProviderConfigurationModal() { /> ); } - return ; + return ; }; return ( - !open && closeModal()}> + !open && onClose()}> @@ -228,17 +153,16 @@ export default function ProviderConfigurationModal() { {!showDeleteConfirmation ? ( <> {/* Contains information used to set up each provider */} - {requiredParameters.length > 0 && - currentProvider.metadata.config_keys && - currentProvider.metadata.config_keys.length > 0 && } + provider.metadata.config_keys && + provider.metadata.config_keys.length > 0 && } ) : null}
@@ -252,11 +176,11 @@ export default function ProviderConfigurationModal() { showDeleteConfirmation={showDeleteConfirmation} onConfirmDelete={handleConfirmDelete} onCancelDelete={() => { - setShowDeleteConfirmation(false); setIsActiveProvider(false); + setShowDeleteConfirmation(false); }} canDelete={isConfigured && !isActiveProvider} - providerName={currentProvider.metadata.display_name} + providerName={provider.metadata.display_name} isActiveProvider={isActiveProvider} /> diff --git a/ui/desktop/src/components/settings/providers/modal/ProviderModalProvider.tsx b/ui/desktop/src/components/settings/providers/modal/ProviderModalProvider.tsx deleted file mode 100644 index 5c1a48a9b29b..000000000000 --- a/ui/desktop/src/components/settings/providers/modal/ProviderModalProvider.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React, { createContext, useContext, useState } from 'react'; -import { ProviderDetails } from '../../../../api/types.gen'; - -interface FormValues { - [key: string]: string | number | boolean | null; -} - -interface ModalProps { - onSubmit?: (values: FormValues) => void; - onCancel?: () => void; - onDelete?: (values: FormValues) => void; - formProps?: { - initialValues?: FormValues; - validationSchema?: object; - [key: string]: unknown; - }; -} - -interface ProviderModalContextType { - isOpen: boolean; - currentProvider: ProviderDetails | null; - modalProps: ModalProps; - openModal: (provider: ProviderDetails, additionalProps?: ModalProps) => void; - closeModal: () => void; -} - -const ProviderModalContext = createContext(undefined); - -export const ProviderModalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const [isOpen, setIsOpen] = useState(false); - const [currentProvider, setCurrentProvider] = useState(null); - const [modalProps, setModalProps] = useState({}); - - const openModal = (provider: ProviderDetails, additionalProps: ModalProps = {}) => { - setCurrentProvider(provider); - setModalProps(additionalProps); - setIsOpen(true); - }; - - const closeModal = () => { - setIsOpen(false); - }; - - return ( - - {children} - - ); -}; - -export const useProviderModal = () => { - const context = useContext(ProviderModalContext); - if (context === undefined) { - throw new Error('useProviderModal must be used within a ProviderModalProvider'); - } - return context; -}; diff --git a/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/DefaultProviderSetupForm.tsx b/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/DefaultProviderSetupForm.tsx index 6f8e2dd69476..3d15a979838c 100644 --- a/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/DefaultProviderSetupForm.tsx +++ b/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/DefaultProviderSetupForm.tsx @@ -1,17 +1,40 @@ import React, { useEffect, useMemo, useState, useCallback } from 'react'; import { Input } from '../../../../../ui/input'; -import { useConfig } from '../../../../../ConfigContext'; // Adjust this import path as needed +import { useConfig } from '../../../../../ConfigContext'; import { ProviderDetails, ConfigKey } from '../../../../../../api'; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../../../../../ui/collapsible'; type ValidationErrors = Record; +type ConfigValue = string | { maskedValue: string }; +export interface ConfigInput { + serverValue?: ConfigValue; + value?: string; +} + interface DefaultProviderSetupFormProps { - configValues: Record; - setConfigValues: React.Dispatch>>; + configValues: Record; + setConfigValues: React.Dispatch>>; provider: ProviderDetails; validationErrors: ValidationErrors; } +const envToPrettyName = (envVar: string) => { + const wordReplacements: { [w: string]: string } = { + Api: 'API', + Aws: 'AWS', + Gcp: 'GCP', + }; + + return envVar + .toLowerCase() + .split('_') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .map((word) => wordReplacements[word] || word) + .join(' ') + .trim(); +}; + export default function DefaultProviderSetupForm({ configValues, setConfigValues, @@ -23,73 +46,49 @@ export default function DefaultProviderSetupForm({ [provider.metadata.config_keys] ); const [isLoading, setIsLoading] = useState(true); + const [optionalExpanded, setOptionalExpanded] = useState(false); const { read } = useConfig(); - console.log('configValues default form', configValues); - - // Initialize values when the component mounts or provider changes const loadConfigValues = useCallback(async () => { setIsLoading(true); - const newValues = { ...configValues }; + try { + const values: { [k: string]: ConfigInput } = {}; - // Try to load actual values from config for each parameter that is not secret - for (const parameter of parameters) { - try { - // Check if there's a stored value in the config system + for (const parameter of parameters) { const configKey = `${parameter.name}`; - const configResponse = await read(configKey, parameter.secret || false); - - if (configResponse) { - newValues[parameter.name] = parameter.secret ? 'true' : String(configResponse); - } else if ( - parameter.default !== undefined && - parameter.default !== null && - !configValues[parameter.name] - ) { - // Fall back to default value if no config value exists - newValues[parameter.name] = String(parameter.default); - } - } catch (error) { - console.error(`Failed to load config for ${parameter.name}:`, error); - // Fall back to default if read operation fails - if ( - parameter.default !== undefined && - parameter.default !== null && - !configValues[parameter.name] - ) { - newValues[parameter.name] = String(parameter.default); + const configValue = (await read(configKey, parameter.secret || false)) as ConfigValue; + + if (configValue) { + values[parameter.name] = { serverValue: configValue }; + } else if (parameter.default !== undefined && parameter.default !== null) { + values[parameter.name] = { value: parameter.default }; } } - } - // Update state with loaded values - setConfigValues((prev) => ({ - ...prev, - ...newValues, - })); - setIsLoading(false); - }, [configValues, parameters, read, setConfigValues]); + setConfigValues((prev) => ({ + ...prev, + ...values, + })); + } finally { + setIsLoading(false); + } + }, [parameters, read, setConfigValues]); useEffect(() => { loadConfigValues(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // Filter parameters to only show required ones - const requiredParameters = useMemo(() => { - return parameters.filter((param) => param.required === true); - }, [parameters]); - - // TODO: show all params, not just required ones - // const allParameters = useMemo(() => { - // return parameters; - // }, [parameters]); - - // Helper function to generate appropriate placeholder text const getPlaceholder = (parameter: ConfigKey): string => { - // If default is defined and not null, show it + if (parameter.secret) { + const serverValue = configValues[parameter.name]?.serverValue; + if (typeof serverValue === 'object' && 'maskedValue' in serverValue) { + return serverValue.maskedValue; + } + } + if (parameter.default !== undefined && parameter.default !== null) { - return `Default: ${parameter.default}`; + return parameter.default; } const name = parameter.name.toLowerCase(); @@ -99,66 +98,112 @@ export default function DefaultProviderSetupForm({ return parameter.name .replace(/_/g, ' ') - .replace(/([A-Z])/g, ' $1') .replace(/^./, (str) => str.toUpperCase()) .trim(); }; - // helper for custom labels - const getFieldLabel = (parameter: ConfigKey): string => { + const getFieldLabel = (parameter: ConfigKey) => { const name = parameter.name.toLowerCase(); if (name.includes('api_key')) return 'API Key'; if (name.includes('api_url') || name.includes('host')) return 'API Host'; if (name.includes('models')) return 'Models'; - return parameter.name - .replace(/_/g, ' ') - .replace(/([A-Z])/g, ' $1') - .replace(/^./, (str) => str.toUpperCase()) - .trim(); + let parameter_name = parameter.name.toUpperCase(); + if (parameter_name.startsWith(provider.name.toUpperCase().replace('-', '_'))) { + parameter_name = parameter_name.slice(provider.name.length + 1); + } + let pretty = envToPrettyName(parameter_name); + return ( + + {pretty} + ({parameter.name}) + + ); }; if (isLoading) { return
Loading configuration values...
; } - console.log('required params', requiredParameters); + function getRenderValue(parameter: ConfigKey): string | undefined { + if (parameter.secret) { + return undefined; + } + + const entry = configValues[parameter.name]; + return entry?.value || (entry?.serverValue as string) || ''; + } + + const renderParametersList = (parameters: ConfigKey[]) => { + return parameters.map((parameter) => ( +
+ + ) => { + setConfigValues((prev) => { + const newValue = { ...(prev[parameter.name] || {}), value: e.target.value }; + return { + ...prev, + [parameter.name]: newValue, + }; + }); + }} + placeholder={getPlaceholder(parameter)} + className={`w-full h-14 px-4 font-regular rounded-lg shadow-none ${ + validationErrors[parameter.name] + ? 'border-2 border-red-500' + : 'border border-borderSubtle hover:border-borderStandard' + } bg-background-default text-lg placeholder:text-textSubtle font-regular text-textStandard`} + required={parameter.required} + /> + {validationErrors[parameter.name] && ( +

{validationErrors[parameter.name]}

+ )} +
+ )); + }; + + let aboveFoldParameters = parameters.filter((p) => p.required); + let belowFoldParameters = parameters.filter((p) => !p.required); + if (aboveFoldParameters.length === 0) { + aboveFoldParameters = belowFoldParameters; + belowFoldParameters = []; + } + + const expandCtaText = `${optionalExpanded ? 'Hide' : 'Show'} ${belowFoldParameters.length} options `; + return (
- {requiredParameters.length === 0 ? ( + {aboveFoldParameters.length === 0 && belowFoldParameters.length === 0 ? (
- No required configuration for this provider. + No configuration parameters for this provider.
) : ( - requiredParameters.map((parameter) => ( -
- - ) => { - console.log(`Setting ${parameter.name} to:`, e.target.value); - setConfigValues((prev) => ({ - ...prev, - [parameter.name]: e.target.value, - })); - }} - placeholder={getPlaceholder(parameter)} - className={`w-full h-14 px-4 font-regular rounded-lg shadow-none ${ - validationErrors[parameter.name] - ? 'border-2 border-red-500' - : 'border border-borderSubtle hover:border-borderStandard' - } bg-background-default text-lg placeholder:text-textSubtle font-regular text-textStandard`} - required={parameter.required} - /> - {validationErrors[parameter.name] && ( -

{validationErrors[parameter.name]}

- )} -
- )) +
+
{renderParametersList(aboveFoldParameters)}
+ {belowFoldParameters.length > 0 && ( + + +
+ {expandCtaText} + {optionalExpanded ? '↑' : '↓'} +
+
+ + {renderParametersList(belowFoldParameters)} + +
+ )} +
)}
); diff --git a/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/OllamaForm.tsx b/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/OllamaForm.tsx deleted file mode 100644 index fba9a68fa6f3..000000000000 --- a/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/OllamaForm.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { PROVIDER_REGISTRY } from '../../../ProviderRegistry'; -import { Input } from '../../../../../ui/input'; - -import React, { useState, useEffect, useCallback } from 'react'; -import { RefreshCw } from 'lucide-react'; -import CustomRadio from '../../../../../ui/CustomRadio'; - -export default function OllamaForm({ - configValues, - setConfigValues, - provider, -}: { - configValues: Record; - setConfigValues: React.Dispatch>>; - provider: { name: string; [key: string]: unknown }; -}) { - const providerEntry = PROVIDER_REGISTRY.find((p) => p.name === provider.name); - const parameters = providerEntry?.details?.parameters || []; - const [isCheckingLocal, setIsCheckingLocal] = useState(false); - const [isLocalAvailable, setIsLocalAvailable] = useState(false); - - const handleConnectionTypeChange = useCallback( - (value: string) => { - setConfigValues((prev) => ({ - ...prev, - connection_type: value, - })); - }, - [setConfigValues] - ); - - // Function to handle input changes and auto-select/deselect the host radio - const handleInputChange = (paramName: string, value: string) => { - // Update the parameter value - setConfigValues((prev) => ({ - ...prev, - [paramName]: value, - })); - - // If the user is typing, auto-select the host radio button - if (value && configValues.connection_type !== 'host') { - handleConnectionTypeChange('host'); - } - // If the input becomes empty and the host radio is selected, switch to local if available - else if (!value && configValues.connection_type === 'host') { - if (isLocalAvailable) { - handleConnectionTypeChange('local'); - } - // If local is not available, we keep the host selected but leave the input empty - } - }; - - const checkLocalAvailability = useCallback(async () => { - setIsCheckingLocal(true); - - // Dummy implementation - simulates checking local availability - try { - console.log('Checking for local Ollama instance...'); - // Simulate a network request with a delay - await new Promise((resolve) => setTimeout(resolve, 800)); - - // Randomly determine if Ollama is available (for demo purposes) - const isAvailable = Math.random() > 0.3; - setIsLocalAvailable(isAvailable); - - if (isAvailable) { - console.log('Local Ollama instance found'); - // Enable local radio button - } else { - console.log('No local Ollama instance found'); - // If current selection is local, switch to host - if (configValues.connection_type === 'local') { - handleConnectionTypeChange('host'); - } - } - } catch (error) { - console.error('Error checking for local Ollama:', error); - setIsLocalAvailable(false); - } finally { - setIsCheckingLocal(false); - } - }, [configValues.connection_type, handleConnectionTypeChange]); - - // Check local availability on initial load - useEffect(() => { - checkLocalAvailability(); - }, [checkLocalAvailability]); - - return ( -
-
Connection
- - {/* Local Option */} -
-
- Background App - -
- - handleConnectionTypeChange('local')} - disabled={!isLocalAvailable} - /> -
- - {/* Other Parameters */} - {parameters - .filter((param) => param.name !== 'host_url') // Skip host_url as we handle it above - .map((parameter) => ( -
-
- handleInputChange(parameter.name, e.target.value)} - placeholder={ - parameter.default ? parameter.default : parameter.name.replace(/_/g, ' ') - } - className="w-full h-14 px-4 font-regular rounded-lg border shadow-none border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-lg placeholder:text-gray-400 dark:placeholder:text-gray-500 font-regular text-gray-900 dark:text-gray-100" - required={parameter.default == null} - /> -
-
- handleConnectionTypeChange('host')} - /> -
-
- ))} -
- ); -} diff --git a/ui/desktop/src/components/settings/providers/modal/subcomponents/handlers/DefaultSubmitHandler.tsx b/ui/desktop/src/components/settings/providers/modal/subcomponents/handlers/DefaultSubmitHandler.tsx index 3356f4a8af88..2c9bb82c1500 100644 --- a/ui/desktop/src/components/settings/providers/modal/subcomponents/handlers/DefaultSubmitHandler.tsx +++ b/ui/desktop/src/components/settings/providers/modal/subcomponents/handlers/DefaultSubmitHandler.tsx @@ -2,7 +2,7 @@ * Standalone function to submit provider configuration * Useful for components that don't want to use the hook */ -export const DefaultSubmitHandler = async ( +export const providerConfigSubmitHandler = async ( upsertFn: (key: string, value: unknown, isSecret: boolean) => Promise, provider: { name: string; @@ -15,7 +15,7 @@ export const DefaultSubmitHandler = async ( }>; }; }, - configValues: Record + configValues: Record ) => { const parameters = provider.metadata.config_keys || []; diff --git a/ui/desktop/src/components/settings/providers/modal/subcomponents/handlers/OllamaSubmitHandler.tsx b/ui/desktop/src/components/settings/providers/modal/subcomponents/handlers/OllamaSubmitHandler.tsx deleted file mode 100644 index 329babcc8656..000000000000 --- a/ui/desktop/src/components/settings/providers/modal/subcomponents/handlers/OllamaSubmitHandler.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function OllamaSubmitHandler(configValues: Record) { - // Log each field value individually for clarity - console.log('Ollama field values:'); - Object.entries(configValues).forEach(([key, value]) => { - console.log(`${key}: ${value}`); - }); -} diff --git a/ui/desktop/src/components/settings/providers/subcomponents/ProviderCard.tsx b/ui/desktop/src/components/settings/providers/subcomponents/ProviderCard.tsx index 25d4d9c9f295..4be31bdf41bc 100644 --- a/ui/desktop/src/components/settings/providers/subcomponents/ProviderCard.tsx +++ b/ui/desktop/src/components/settings/providers/subcomponents/ProviderCard.tsx @@ -1,4 +1,4 @@ -import { memo, useMemo } from 'react'; +import { useMemo } from 'react'; import CardContainer from './CardContainer'; import CardHeader from './CardHeader'; import CardBody from './CardBody'; @@ -9,11 +9,10 @@ type ProviderCardProps = { provider: ProviderDetails; onConfigure: () => void; onLaunch: () => void; - onDelete: () => void; isOnboarding: boolean; }; -export const ProviderCard = memo(function ProviderCard({ +export const ProviderCard = function ProviderCard({ provider, onConfigure, onLaunch, @@ -59,4 +58,4 @@ export const ProviderCard = memo(function ProviderCard({ } /> ); -}); +}; diff --git a/ui/desktop/src/components/ui/input.tsx b/ui/desktop/src/components/ui/input.tsx index 73fdb5155de8..39bab270b9d9 100644 --- a/ui/desktop/src/components/ui/input.tsx +++ b/ui/desktop/src/components/ui/input.tsx @@ -8,7 +8,7 @@ const Input = React.forwardRef>( void; clearRecipe: () => void; - // Draft functionality - draft: string; - setDraft: (draft: string) => void; - clearDraft: () => void; // Context identification contextKey: string; // 'hub' or 'pair-{sessionId}' agentWaitingMessage: string | null; @@ -39,19 +34,6 @@ export const ChatProvider: React.FC = ({ agentWaitingMessage, contextKey = 'hub', }) => { - const draftContext = useDraftContext(); - - // Draft functionality using the app-level DraftContext - const draft = draftContext.getDraft(contextKey); - - const setDraft = (newDraft: string) => { - draftContext.setDraft(contextKey, newDraft); - }; - - const clearDraft = () => { - draftContext.clearDraft(contextKey); - }; - const resetChat = () => { setChat({ sessionId: '', @@ -61,7 +43,6 @@ export const ChatProvider: React.FC = ({ recipe: null, recipeParameterValues: null, }); - clearDraft(); }; const setRecipe = (recipe: Recipe | null) => { @@ -88,9 +69,6 @@ export const ChatProvider: React.FC = ({ hasActiveSession, setRecipe, clearRecipe, - draft, - setDraft, - clearDraft, contextKey, agentWaitingMessage, }; diff --git a/ui/desktop/src/contexts/DraftContext.tsx b/ui/desktop/src/contexts/DraftContext.tsx deleted file mode 100644 index 9238f54bfd9f..000000000000 --- a/ui/desktop/src/contexts/DraftContext.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React, { createContext, useContext, useState, ReactNode } from 'react'; - -interface DraftContextType { - getDraft: (contextKey: string) => string; - setDraft: (contextKey: string, draft: string) => void; - clearDraft: (contextKey: string) => void; -} - -const DraftContext = createContext(undefined); - -export const DraftProvider: React.FC<{ children: ReactNode }> = ({ children }) => { - // Store all drafts by contextKey - const [drafts, setDrafts] = useState>({}); - - const getDraft = (contextKey: string): string => { - return drafts[contextKey] || ''; - }; - - const setDraft = (contextKey: string, draft: string) => { - setDrafts((prev) => ({ ...prev, [contextKey]: draft })); - }; - - const clearDraft = (contextKey: string) => { - setDrafts((prev) => { - const newDrafts = { ...prev }; - delete newDrafts[contextKey]; - return newDrafts; - }); - }; - - return ( - - {children} - - ); -}; - -export const useDraftContext = (): DraftContextType => { - const context = useContext(DraftContext); - if (context === undefined) { - throw new Error('useDraftContext must be used within a DraftProvider'); - } - return context; -}; diff --git a/ui/desktop/src/hooks/useChatEngine.ts b/ui/desktop/src/hooks/useChatEngine.ts index 1c0f6b7e47a4..19a333aefda9 100644 --- a/ui/desktop/src/hooks/useChatEngine.ts +++ b/ui/desktop/src/hooks/useChatEngine.ts @@ -77,6 +77,7 @@ export const useChatEngine = ({ notifications, session, setError, + tokenState, } = useMessageStream({ api: getApiUrl('/reply'), id: chat.sessionId, @@ -212,7 +213,6 @@ export const useChatEngine = ({ // Update token counts when session changes from the message stream useEffect(() => { - console.log('Session received:', session); if (session) { setSessionTokenCount(session.total_tokens || 0); setSessionInputTokens(session.accumulated_input_tokens || 0); @@ -451,6 +451,7 @@ export const useChatEngine = ({ sessionOutputTokens, localInputTokens, localOutputTokens, + tokenState, // UI helpers commandHistory, diff --git a/ui/desktop/src/hooks/useChatStream.ts b/ui/desktop/src/hooks/useChatStream.ts index 2b4c9f2b4e02..e084b04d6e2a 100644 --- a/ui/desktop/src/hooks/useChatStream.ts +++ b/ui/desktop/src/hooks/useChatStream.ts @@ -7,6 +7,7 @@ import { reply, resumeAgent, Session, + TokenState, updateFromSession, updateSessionUserRecipeValues, } from '../api'; @@ -60,6 +61,7 @@ interface UseChatStreamReturn { setRecipeUserParams: (values: Record) => Promise; stopStreaming: () => void; sessionLoadError?: string; + tokenState: TokenState; } function pushMessage(currentMessages: Message[], incomingMsg: Message): Message[] { @@ -88,6 +90,7 @@ async function streamFromResponse( stream: AsyncIterable, initialMessages: Message[], updateMessages: (messages: Message[]) => void, + updateTokenState: (tokenState: TokenState) => void, updateChatState: (state: ChatState) => void, onFinish: (error?: string) => void ): Promise { @@ -119,6 +122,8 @@ async function streamFromResponse( }); } + updateTokenState(event.token_state); + updateMessages(currentMessages); break; } @@ -141,6 +146,8 @@ async function streamFromResponse( } case 'UpdateConversation': { log.messages('conversation-update', event.conversation.length); + // WARNING: Since Message handler uses this local variable, we need to update it here to avoid the client clobbering it. + // Longterm fix is to only send the agent the new messages, not the entire conversation. currentMessages = event.conversation; updateMessages(event.conversation); break; @@ -171,6 +178,14 @@ export function useChatStream({ const [session, setSession] = useState(); const [sessionLoadError, setSessionLoadError] = useState(); const [chatState, setChatState] = useState(ChatState.Idle); + const [tokenState, setTokenState] = useState({ + inputTokens: 0, + outputTokens: 0, + totalTokens: 0, + accumulatedInputTokens: 0, + accumulatedOutputTokens: 0, + accumulatedTotalTokens: 0, + }); const abortControllerRef = useRef(null); useEffect(() => { @@ -288,6 +303,7 @@ export function useChatStream({ stream, currentMessages, (messages: Message[]) => setMessagesAndLog(messages, 'streaming'), + setTokenState, setChatState, onFinish ); @@ -373,5 +389,6 @@ export function useChatStream({ handleSubmit, stopStreaming, setRecipeUserParams, + tokenState, }; } diff --git a/ui/desktop/src/hooks/useMessageStream.ts b/ui/desktop/src/hooks/useMessageStream.ts index d05aaadbe067..4e36c7d9ec76 100644 --- a/ui/desktop/src/hooks/useMessageStream.ts +++ b/ui/desktop/src/hooks/useMessageStream.ts @@ -6,7 +6,7 @@ import { getCompactingMessage, hasCompletedToolCalls, } from '../types/message'; -import { Conversation, Message, Role } from '../api'; +import { Conversation, Message, Role, TokenState } from '../api'; import { getSession, Session } from '../api'; import { ChatState } from '../types/chatState'; @@ -35,9 +35,9 @@ export interface NotificationEvent { // Event types for SSE stream type MessageEvent = - | { type: 'Message'; message: Message } + | { type: 'Message'; message: Message; token_state: TokenState } | { type: 'Error'; error: string } - | { type: 'Finish'; reason: string } + | { type: 'Finish'; reason: string; token_state: TokenState } | { type: 'ModelChange'; model: string; mode: string } | { type: 'UpdateConversation'; conversation: Conversation } | NotificationEvent; @@ -165,6 +165,9 @@ export interface UseMessageStreamHelpers { /** Clear error state */ setError: (error: Error | undefined) => void; + + /** Real-time token state from server */ + tokenState: TokenState; } /** @@ -197,6 +200,14 @@ export function useMessageStream({ null ); const [session, setSession] = useState(null); + const [tokenState, setTokenState] = useState({ + inputTokens: 0, + outputTokens: 0, + totalTokens: 0, + accumulatedInputTokens: 0, + accumulatedOutputTokens: 0, + accumulatedTotalTokens: 0, + }); // expose a way to update the body so we can update the session id when CLE occurs const updateMessageStreamBody = useCallback((newBody: object) => { @@ -280,6 +291,8 @@ export function useMessageStream({ // Transition from waiting to streaming on first message mutateChatState(ChatState.Streaming); + setTokenState(parsedEvent.token_state); + // Create a new message object with the properties preserved or defaulted const newMessage: Message = { ...parsedEvent.message, @@ -341,6 +354,8 @@ export function useMessageStream({ } case 'UpdateConversation': { + // WARNING: Since Message handler uses this local variable, we need to update it here to avoid the client clobbering it. + // Longterm fix is to only send the agent the new messages, not the entire conversation. currentMessages = parsedEvent.conversation; setMessages(parsedEvent.conversation); break; @@ -353,6 +368,8 @@ export function useMessageStream({ } case 'Finish': { + setTokenState(parsedEvent.token_state); + if (onFinish && currentMessages.length > 0) { const lastMessage = currentMessages[currentMessages.length - 1]; onFinish(lastMessage, parsedEvent.reason); @@ -650,5 +667,6 @@ export function useMessageStream({ currentModelInfo, session, setError, + tokenState, }; } diff --git a/ui/desktop/src/hooks/useNavigation.ts b/ui/desktop/src/hooks/useNavigation.ts index 2dfefa3a418b..3a16171b3ea6 100644 --- a/ui/desktop/src/hooks/useNavigation.ts +++ b/ui/desktop/src/hooks/useNavigation.ts @@ -11,3 +11,5 @@ export const useNavigation = () => { const navigate = useNavigate(); return createNavigationHandler(navigate); }; + +export type setViewType = ReturnType; diff --git a/ui/desktop/src/hooks/useWhisper.ts b/ui/desktop/src/hooks/useWhisper.ts index fe0ddb62d8c2..385f674e9423 100644 --- a/ui/desktop/src/hooks/useWhisper.ts +++ b/ui/desktop/src/hooks/useWhisper.ts @@ -2,7 +2,7 @@ import { useState, useRef, useCallback, useEffect } from 'react'; import { useConfig } from '../components/ConfigContext'; import { getApiUrl } from '../config'; import { useDictationSettings } from './useDictationSettings'; -import { safeJsonParse } from '../utils/jsonUtils'; +import { safeJsonParse } from '../utils/conversionUtils'; interface UseWhisperOptions { onTranscription?: (text: string) => void; diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index c13d472d4b4e..bfeb1686de20 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -10,6 +10,7 @@ import { MenuItem, Notification, powerSaveBlocker, + screen, session, shell, Tray, @@ -486,10 +487,12 @@ const goosedClients = new Map(); // Track power save blockers per window const windowPowerSaveBlockers = new Map(); // windowId -> blockerId +// Track pending initial messages per window +const pendingInitialMessages = new Map(); // windowId -> initialMessage const createChat = async ( app: App, - _query?: string, + initialMessage?: string, dir?: string, _version?: string, resumeSessionId?: string, @@ -611,29 +614,63 @@ const createChat = async ( mainWindow.webContents.session.setSpellCheckerLanguages(['en-US', 'en-GB']); mainWindow.webContents.on('context-menu', (_event, params) => { const menu = new Menu(); + const hasSpellingSuggestions = params.dictionarySuggestions.length > 0 || params.misspelledWord; - // Add each spelling suggestion - for (const suggestion of params.dictionarySuggestions) { + if (hasSpellingSuggestions) { + for (const suggestion of params.dictionarySuggestions) { + menu.append( + new MenuItem({ + label: suggestion, + click: () => mainWindow.webContents.replaceMisspelling(suggestion), + }) + ); + } + + if (params.misspelledWord) { + menu.append( + new MenuItem({ + label: 'Add to dictionary', + click: () => + mainWindow.webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord), + }) + ); + } + + if (params.selectionText) { + menu.append(new MenuItem({ type: 'separator' })); + } + } + if (params.selectionText) { menu.append( new MenuItem({ - label: suggestion, - click: () => mainWindow.webContents.replaceMisspelling(suggestion), + label: 'Cut', + accelerator: 'CmdOrCtrl+X', + role: 'cut', + }) + ); + menu.append( + new MenuItem({ + label: 'Copy', + accelerator: 'CmdOrCtrl+C', + role: 'copy', }) ); } - // Allow users to add the misspelled word to the dictionary - if (params.misspelledWord) { + // Only show paste in editable fields (text inputs) + if (params.isEditable) { menu.append( new MenuItem({ - label: 'Add to dictionary', - click: () => - mainWindow.webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord), + label: 'Paste', + accelerator: 'CmdOrCtrl+V', + role: 'paste', }) ); } - menu.popup(); + if (menu.items.length > 0) { + menu.popup(); + } }); // Handle new window creation for links @@ -678,7 +715,10 @@ const createChat = async ( } if ( appPath === '/' && - (recipe !== undefined || recipeDeeplink !== undefined || recipeId !== undefined) + (recipe !== undefined || + recipeDeeplink !== undefined || + recipeId !== undefined || + initialMessage) ) { appPath = '/pair'; } @@ -697,6 +737,11 @@ const createChat = async ( log.info('Opening URL: ', formattedUrl); mainWindow.loadURL(formattedUrl); + // If we have an initial message, store it to send after React is ready + if (initialMessage) { + pendingInitialMessages.set(mainWindow.id, initialMessage); + } + // Set up local keyboard shortcuts that only work when the window is focused mainWindow.webContents.on('before-input-event', (event, input) => { if (input.key === 'r' && input.meta) { @@ -733,6 +778,9 @@ const createChat = async ( mainWindow.on('closed', () => { windowMap.delete(windowId); + // Clean up pending initial message + pendingInitialMessages.delete(windowId); + if (windowPowerSaveBlockers.has(windowId)) { const blockerId = windowPowerSaveBlockers.get(windowId)!; try { @@ -756,6 +804,65 @@ const createChat = async ( return mainWindow; }; +const createLauncher = () => { + const launcherWindow = new BrowserWindow({ + width: 600, + height: 80, + frame: false, + transparent: process.platform === 'darwin', + backgroundColor: process.platform === 'darwin' ? '#00000000' : '#ffffff', + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + nodeIntegration: false, + contextIsolation: true, + additionalArguments: [JSON.stringify(appConfig)], + partition: 'persist:goose', + }, + skipTaskbar: true, + alwaysOnTop: true, + resizable: false, + movable: true, + minimizable: false, + maximizable: false, + fullscreenable: false, + hasShadow: true, + vibrancy: process.platform === 'darwin' ? 'window' : undefined, + }); + + // Center on screen + const primaryDisplay = screen.getPrimaryDisplay(); + const { width, height } = primaryDisplay.workAreaSize; + const windowBounds = launcherWindow.getBounds(); + + launcherWindow.setPosition( + Math.round(width / 2 - windowBounds.width / 2), + Math.round(height / 3 - windowBounds.height / 2) + ); + + // Load launcher window content + const url = MAIN_WINDOW_VITE_DEV_SERVER_URL + ? new URL(MAIN_WINDOW_VITE_DEV_SERVER_URL) + : pathToFileURL(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`)); + + url.hash = '/launcher'; + launcherWindow.loadURL(formatUrl(url)); + + // Destroy window when it loses focus + launcherWindow.on('blur', () => { + launcherWindow.destroy(); + }); + + // Also destroy on escape key + launcherWindow.webContents.on('before-input-event', (event, input) => { + if (input.key === 'Escape') { + launcherWindow.destroy(); + event.preventDefault(); + } + }); + + return launcherWindow; +}; + // Track tray instance let tray: Tray | null = null; @@ -766,30 +873,43 @@ const destroyTray = () => { } }; +const disableTray = () => { + const settings = loadSettings(); + settings.showMenuBarIcon = false; + saveSettings(settings); +}; + const createTray = () => { - // If tray already exists, destroy it first destroyTray(); - const isDev = process.env.NODE_ENV === 'development'; - let iconPath: string; + const possiblePaths = [ + path.join(process.resourcesPath, 'images', 'iconTemplate.png'), + path.join(process.cwd(), 'src', 'images', 'iconTemplate.png'), + path.join(__dirname, '..', 'images', 'iconTemplate.png'), + path.join(__dirname, 'images', 'iconTemplate.png'), + path.join(process.cwd(), 'images', 'iconTemplate.png'), + ]; - if (isDev) { - iconPath = path.join(process.cwd(), 'src', 'images', 'iconTemplate.png'); - } else { - iconPath = path.join(process.resourcesPath, 'images', 'iconTemplate.png'); - } - - tray = new Tray(iconPath); + const iconPath = possiblePaths.find((p) => fsSync.existsSync(p)); - // Set tray reference for auto-updater - setTrayRef(tray); + if (!iconPath) { + console.warn('[Main] Tray icon not found. App will continue without system tray.'); + disableTray(); + return; + } - // Initially build menu based on update status - updateTrayMenu(getUpdateAvailable()); + try { + tray = new Tray(iconPath); + setTrayRef(tray); + updateTrayMenu(getUpdateAvailable()); - // On Windows, clicking the tray icon should show the window - if (process.platform === 'win32') { - tray.on('click', showWindow); + if (process.platform === 'win32') { + tray.on('click', showWindow); + } + } catch (error) { + console.error('[Main] Tray creation failed. App will continue without system tray.', error); + disableTray(); + tray = null; } }; @@ -804,7 +924,6 @@ const showWindow = async () => { return; } - // Define the initial offset values const initialOffsetX = 30; const initialOffsetY = 30; @@ -981,9 +1100,21 @@ process.on('unhandledRejection', (error) => { handleFatalError(error instanceof Error ? error : new Error(String(error))); }); -ipcMain.on('react-ready', () => { +ipcMain.on('react-ready', (event) => { log.info('React ready event received'); + // Get the window that sent the react-ready event + const window = BrowserWindow.fromWebContents(event.sender); + const windowId = window?.id; + + // Send any pending initial message for this window + if (windowId && pendingInitialMessages.has(windowId)) { + const initialMessage = pendingInitialMessages.get(windowId)!; + log.info('Sending pending initial message to window:', initialMessage); + window.webContents.send('set-initial-message', initialMessage); + pendingInitialMessages.delete(windowId); + } + if (pendingDeepLink) { log.info('Processing pending deep link:', pendingDeepLink); handleProtocolUrl(pendingDeepLink); @@ -1620,28 +1751,6 @@ const focusWindow = () => { } }; -const registerGlobalHotkey = (accelerator: string) => { - // Unregister any existing shortcuts first - globalShortcut.unregisterAll(); - - try { - globalShortcut.register(accelerator, () => { - focusWindow(); - }); - - // Check if the shortcut was registered successfully - if (globalShortcut.isRegistered(accelerator)) { - return true; - } else { - console.error('Failed to register global hotkey'); - return false; - } - } catch (e) { - console.error('Error registering global hotkey:', e); - return false; - } -}; - async function appMain() { // Ensure Windows shims are available before any MCP processes are spawned await ensureWinShims(); @@ -1697,8 +1806,13 @@ async function appMain() { }); }); - // Register the default global hotkey - registerGlobalHotkey('CommandOrControl+Alt+Shift+G'); + try { + globalShortcut.register('CommandOrControl+Alt+Shift+G', () => { + createLauncher(); + }); + } catch (e) { + console.error('Error registering launcher hotkey:', e); + } session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => { details.requestHeaders['Origin'] = 'http://localhost:5173'; @@ -1872,6 +1986,50 @@ async function appMain() { ); } + if (menu) { + let windowMenu = menu.items.find((item) => item.label === 'Window'); + + if (!windowMenu) { + windowMenu = new MenuItem({ + label: 'Window', + submenu: Menu.buildFromTemplate([]), + }); + + const helpMenuIndex = menu.items.findIndex((item) => item.label === 'Help'); + if (helpMenuIndex >= 0) { + menu.items.splice(helpMenuIndex, 0, windowMenu); + } else { + menu.items.push(windowMenu); + } + } + + if (windowMenu.submenu) { + windowMenu.submenu.append( + new MenuItem({ + label: 'Always on Top', + type: 'checkbox', + accelerator: process.platform === 'darwin' ? 'Cmd+Shift+T' : 'Ctrl+Shift+T', + click(menuItem) { + const focusedWindow = BrowserWindow.getFocusedWindow(); + if (focusedWindow) { + const isAlwaysOnTop = menuItem.checked; + + if (process.platform === 'darwin') { + focusedWindow.setAlwaysOnTop(isAlwaysOnTop, 'floating'); + } else { + focusedWindow.setAlwaysOnTop(isAlwaysOnTop); + } + + console.log( + `[Main] Set always-on-top to ${isAlwaysOnTop} for window ${focusedWindow.id}` + ); + } + }, + }) + ); + } + } + // on macOS, the topbar is hidden if (menu && process.platform !== 'darwin') { let helpMenu = menu.items.find((item) => item.label === 'Help'); @@ -1950,6 +2108,13 @@ async function appMain() { } ); + ipcMain.on('close-window', (event) => { + const window = BrowserWindow.fromWebContents(event.sender); + if (window && !window.isDestroyed()) { + window.close(); + } + }); + ipcMain.on('notify', (_event, data) => { try { // Validate notification data @@ -2009,6 +2174,17 @@ async function appMain() { } }); + ipcMain.on('broadcast-theme-change', (event, themeData) => { + const senderWindow = BrowserWindow.fromWebContents(event.sender); + const allWindows = BrowserWindow.getAllWindows(); + + allWindows.forEach((window) => { + if (window.id !== senderWindow?.id) { + window.webContents.send('theme-changed', themeData); + } + }); + }); + ipcMain.on('reload-app', (event) => { // Get the window that sent the event const window = BrowserWindow.fromWebContents(event.sender); diff --git a/ui/desktop/src/preload.ts b/ui/desktop/src/preload.ts index 58f1b6c5b794..654c558412fa 100644 --- a/ui/desktop/src/preload.ts +++ b/ui/desktop/src/preload.ts @@ -95,6 +95,11 @@ type ElectronAPI = { callback: (event: Electron.IpcRendererEvent, ...args: unknown[]) => void ) => void; emit: (channel: string, ...args: unknown[]) => void; + broadcastThemeChange: (themeData: { + mode: string; + useSystemTheme: boolean; + theme: string; + }) => void; // Functions for image pasting saveDataUrlToTemp: (dataUrl: string, uniqueId: string) => Promise; deleteTempFile: (filePath: string) => void; @@ -209,6 +214,9 @@ const electronAPI: ElectronAPI = { emit: (channel: string, ...args: unknown[]) => { ipcRenderer.emit(channel, ...args); }, + broadcastThemeChange: (themeData: { mode: string; useSystemTheme: boolean; theme: string }) => { + ipcRenderer.send('broadcast-theme-change', themeData); + }, saveDataUrlToTemp: (dataUrl: string, uniqueId: string): Promise => { return ipcRenderer.invoke('save-data-url-to-temp', dataUrl, uniqueId); }, diff --git a/ui/desktop/src/renderer.tsx b/ui/desktop/src/renderer.tsx index 067e14959938..25928db1e502 100644 --- a/ui/desktop/src/renderer.tsx +++ b/ui/desktop/src/renderer.tsx @@ -8,20 +8,25 @@ import { client } from './api/client.gen'; const App = lazy(() => import('./App')); (async () => { - console.log('window created, getting goosed connection info'); - const baseUrl = await window.electron.getGoosedHostPort(); - if (baseUrl === null) { - window.alert('failed to start goose backend process'); - return; + // Check if we're in the launcher view (doesn't need goosed connection) + const isLauncher = window.location.hash === '#/launcher'; + + if (!isLauncher) { + console.log('window created, getting goosed connection info'); + const baseUrl = await window.electron.getGoosedHostPort(); + if (baseUrl === null) { + window.alert('failed to start goose backend process'); + return; + } + console.log('connecting at', baseUrl); + client.setConfig({ + baseUrl, + headers: { + 'Content-Type': 'application/json', + 'X-Secret-Key': await window.electron.getSecretKey(), + }, + }); } - console.log('connecting at', baseUrl); - client.setConfig({ - baseUrl, - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': await window.electron.getSecretKey(), - }, - }); ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/ui/desktop/src/sessions.ts b/ui/desktop/src/sessions.ts index deca3b26da78..ecbcb691b87b 100644 --- a/ui/desktop/src/sessions.ts +++ b/ui/desktop/src/sessions.ts @@ -1,19 +1,17 @@ -import { Session } from './api'; +import { Session, startAgent } from './api'; +import type { setViewType } from './hooks/useNavigation'; -export function resumeSession( - session: Session, - navigateInSameWindow?: (sessionId: string) => void -) { - const workingDir = session.working_dir; - if (!workingDir) { - throw new Error('Cannot resume session: working directory is missing in session'); - } - - // When ALPHA is true and we have a navigation callback, resume in the same window - // Otherwise, open in a new window (old behavior) - if (process.env.ALPHA && navigateInSameWindow) { - navigateInSameWindow(session.id); +export function resumeSession(session: Session, setView: setViewType) { + if (process.env.ALPHA) { + setView('pair', { + disableAnimation: true, + resumeSessionId: session.id, + }); } else { + const workingDir = session.working_dir; + if (!workingDir) { + throw new Error('Cannot resume session: working directory is missing in session'); + } window.electron.createChatWindow( undefined, // query workingDir, @@ -22,3 +20,30 @@ export function resumeSession( ); } } + +export async function startNewSession( + initialText: string | undefined, + resetChat: (() => void) | null, + setView: setViewType +) { + if (!resetChat || process.env.ALPHA) { + const newAgent = await startAgent({ + body: { + working_dir: window.appConfig.get('GOOSE_WORKING_DIR') as string, + }, + throwOnError: true, + }); + const session = newAgent.data; + setView('pair', { + disableAnimation: true, + initialMessage: initialText, + resumeSessionId: session.id, + }); + } else { + resetChat(); + setView('pair', { + disableAnimation: true, + initialMessage: initialText, + }); + } +} diff --git a/ui/desktop/src/sharedSessions.ts b/ui/desktop/src/sharedSessions.ts index 4e6b7184ca1e..849f5549f86e 100644 --- a/ui/desktop/src/sharedSessions.ts +++ b/ui/desktop/src/sharedSessions.ts @@ -1,4 +1,4 @@ -import { safeJsonParse } from './utils/jsonUtils'; +import { safeJsonParse } from './utils/conversionUtils'; import { Message } from './api'; export interface SharedSessionDetails { diff --git a/ui/desktop/src/toasts.tsx b/ui/desktop/src/toasts.tsx index ba34a652997c..4f8849c828ae 100644 --- a/ui/desktop/src/toasts.tsx +++ b/ui/desktop/src/toasts.tsx @@ -1,12 +1,18 @@ import { toast, ToastOptions } from 'react-toastify'; import { Button } from './components/ui/button'; +import { startNewSession } from './sessions'; +import { useNavigation } from './hooks/useNavigation'; +import { + GroupedExtensionLoadingToast, + ExtensionLoadingStatus, +} from './components/GroupedExtensionLoadingToast'; export interface ToastServiceOptions { silent?: boolean; shouldThrow?: boolean; } -export default class ToastService { +class ToastService { private silent: boolean = false; private shouldThrow: boolean = false; @@ -30,13 +36,13 @@ export default class ToastService { } } - error({ title, msg, traceback }: { title: string; msg: string; traceback: string }): void { + error(props: ToastErrorProps): void { if (!this.silent) { - toastError({ title, msg, traceback }); + toastError(props); } if (this.shouldThrow) { - throw new Error(msg); + throw new Error(props.msg); } } @@ -61,6 +67,56 @@ export default class ToastService { if (toastId) toast.dismiss(toastId); } + /** + * Create a grouped extension loading toast that can be updated as extensions load + */ + extensionLoading( + extensions: ExtensionLoadingStatus[], + totalCount: number, + isComplete: boolean = false + ): string | number { + if (this.silent) { + return 'silent'; + } + + const toastId = 'extension-loading'; + + // Check if toast already exists + if (toast.isActive(toastId)) { + // Update existing toast + toast.update(toastId, { + render: ( + + ), + autoClose: isComplete ? 5000 : false, + closeButton: true, + closeOnClick: false, + }); + } else { + // Create new toast + toast( + , + { + ...commonToastOptions, + toastId, + autoClose: false, + closeButton: true, + closeOnClick: false, // Prevent closing when clicking to expand/collapse + } + ); + } + + return toastId; + } + /** * Handle errors with consistent logging and toast notifications * Consolidates the functionality of the original handleError function @@ -68,7 +124,7 @@ export default class ToastService { handleError(title: string, message: string, options: ToastServiceOptions = {}): void { this.configure(options); this.error({ - title: title || 'Error', + title: title, msg: message, traceback: message, }); @@ -78,6 +134,9 @@ export default class ToastService { // Export a singleton instance for use throughout the app export const toastService = ToastService.getInstance(); +// Re-export ExtensionLoadingStatus for convenience +export type { ExtensionLoadingStatus }; + const commonToastOptions: ToastOptions = { position: 'top-right', closeButton: true, @@ -88,6 +147,7 @@ const commonToastOptions: ToastOptions = { }; type ToastSuccessProps = { title?: string; msg?: string; toastOptions?: ToastOptions }; + export function toastSuccess({ title, msg, toastOptions = {} }: ToastSuccessProps) { return toast.success(
@@ -99,26 +159,42 @@ export function toastSuccess({ title, msg, toastOptions = {} }: ToastSuccessProp } type ToastErrorProps = { - title?: string; - msg?: string; + title: string; + msg: string; traceback?: string; - toastOptions?: ToastOptions; + recoverHints?: string; }; -export function toastError({ title, msg, traceback, toastOptions }: ToastErrorProps) { - return toast.error( +function ToastErrorContent({ + title, + msg, + traceback, + recoverHints, +}: Omit) { + const setView = useNavigation(); + const showRecovery = recoverHints && setView; + + return (
- {title ? {title} : null} - {msg ?
{msg}
: null} + {title && {title}} + {msg &&
{msg}
}
-
- {traceback ? ( +
+ {showRecovery ? ( + + ) : traceback ? ( ) : null}
-
, - { ...commonToastOptions, autoClose: traceback ? false : 5000, ...toastOptions } +
+ ); +} + +export function toastError({ title, msg, traceback, recoverHints }: ToastErrorProps) { + return toast.error( + , + { ...commonToastOptions, autoClose: traceback ? false : 5000 } ); } @@ -137,19 +213,3 @@ export function toastLoading({ title, msg, toastOptions }: ToastLoadingProps) { { ...commonToastOptions, autoClose: false, ...toastOptions } ); } - -type ToastInfoProps = { - title?: string; - msg?: string; - toastOptions?: ToastOptions; -}; - -export function toastInfo({ title, msg, toastOptions }: ToastInfoProps) { - return toast.info( -
- {title ? {title} : null} - {msg ?
{msg}
: null} -
, - { ...commonToastOptions, ...toastOptions } - ); -} diff --git a/ui/desktop/src/utils/autoUpdater.ts b/ui/desktop/src/utils/autoUpdater.ts index 6753cc0e8f05..9495c9d19092 100644 --- a/ui/desktop/src/utils/autoUpdater.ts +++ b/ui/desktop/src/utils/autoUpdater.ts @@ -248,7 +248,6 @@ export function setupAutoUpdater(tray?: Tray) { log.info('Setting up auto-updater...'); log.info(`Current app version: ${app.getVersion()}`); log.info(`Platform: ${process.platform}, Arch: ${process.arch}`); - log.info(`NODE_ENV: ${process.env.NODE_ENV}`); log.info(`ENABLE_DEV_UPDATES: ${process.env.ENABLE_DEV_UPDATES}`); log.info(`App is packaged: ${app.isPackaged}`); log.info(`App path: ${app.getAppPath()}`); @@ -470,7 +469,7 @@ function sendStatusToWindow(event: string, data?: unknown) { function updateTrayIcon(hasUpdate: boolean) { if (!trayRef) return; - const isDev = process.env.NODE_ENV === 'development'; + const isDev = !app.isPackaged; let iconPath: string; if (hasUpdate) { diff --git a/ui/desktop/src/utils/jsonUtils.ts b/ui/desktop/src/utils/conversionUtils.ts similarity index 51% rename from ui/desktop/src/utils/jsonUtils.ts rename to ui/desktop/src/utils/conversionUtils.ts index 1f8f3ec01ee0..51b9cb30accf 100644 --- a/ui/desktop/src/utils/jsonUtils.ts +++ b/ui/desktop/src/utils/conversionUtils.ts @@ -11,3 +11,13 @@ export async function safeJsonParse( throw error; } } + +export function errorMessage(err: Error | unknown, default_value?: string) { + if (err instanceof Error) { + return err.message; + } else if (typeof err === 'object' && err !== null && 'message' in err) { + return String(err.message); + } else { + return default_value || String(err); + } +} diff --git a/ui/desktop/src/utils/costDatabase.ts b/ui/desktop/src/utils/costDatabase.ts index 04dc868700ab..5b3bde1de0fe 100644 --- a/ui/desktop/src/utils/costDatabase.ts +++ b/ui/desktop/src/utils/costDatabase.ts @@ -1,6 +1,5 @@ -// Import the proper type from ConfigContext import { getApiUrl } from '../config'; -import { safeJsonParse } from './jsonUtils'; +import { safeJsonParse } from './conversionUtils'; export interface ModelCostInfo { input_token_cost: number; // Cost per token for input (in USD) @@ -206,49 +205,3 @@ export async function fetchAndCachePricing( return null; } } - -/** - * Refresh pricing data from backend - */ -export async function refreshPricing(): Promise { - try { - // Clear session cache to force re-fetch - sessionPricingCache.clear(); - - // The actual refresh happens on the backend when we call with configured_only: false - const apiUrl = getApiUrl('/config/pricing'); - const secretKey = await window.electron.getSecretKey(); - - const headers: HeadersInit = { 'Content-Type': 'application/json' }; - if (secretKey) { - headers['X-Secret-Key'] = secretKey; - } - - const response = await fetch(apiUrl, { - method: 'POST', - headers, - body: JSON.stringify({ configured_only: false }), - }); - - return response.ok; - } catch { - return false; - } -} - -// Expose functions for testing in development mode -declare global { - interface Window { - getCostForModel?: typeof getCostForModel; - fetchAndCachePricing?: typeof fetchAndCachePricing; - refreshPricing?: typeof refreshPricing; - sessionPricingCache?: typeof sessionPricingCache; - } -} - -if (process.env.NODE_ENV === 'development' || typeof window !== 'undefined') { - window.getCostForModel = getCostForModel; - window.fetchAndCachePricing = fetchAndCachePricing; - window.refreshPricing = refreshPricing; - window.sessionPricingCache = sessionPricingCache; -} diff --git a/ui/desktop/src/utils/extensionErrorUtils.ts b/ui/desktop/src/utils/extensionErrorUtils.ts new file mode 100644 index 000000000000..e707c6c0e757 --- /dev/null +++ b/ui/desktop/src/utils/extensionErrorUtils.ts @@ -0,0 +1,30 @@ +/** + * Shared constants and utilities for extension error handling + */ + +export const MAX_ERROR_MESSAGE_LENGTH = 70; + +/** + * Creates recovery hints for the "Ask goose" feature when extension loading fails + */ +export function createExtensionRecoverHints(errorMsg: string): string { + return ( + `Explain the following error: ${errorMsg}. ` + + 'This happened while trying to install an extension. Look out for issues where the ' + + "extension attempted to execute something incorrectly, didn't exist, or there was trouble with " + + 'the network configuration - VPNs like WARP often cause issues.' + ); +} + +/** + * Formats an error message for display, truncating long messages with a fallback + * @param errorMsg - The full error message + * @param fallback - The fallback message to show if the error is too long + * @returns The formatted error message + */ +export function formatExtensionErrorMessage( + errorMsg: string, + fallback: string = 'Failed to add extension' +): string { + return errorMsg.length < MAX_ERROR_MESSAGE_LENGTH ? errorMsg : fallback; +} diff --git a/ui/desktop/src/utils/githubUpdater.ts b/ui/desktop/src/utils/githubUpdater.ts index 4516a2aee442..1b0140b9475a 100644 --- a/ui/desktop/src/utils/githubUpdater.ts +++ b/ui/desktop/src/utils/githubUpdater.ts @@ -4,7 +4,7 @@ import * as fs from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; import log from './logger'; -import { safeJsonParse } from './jsonUtils'; +import { safeJsonParse } from './conversionUtils'; interface GitHubRelease { tag_name: string; diff --git a/ui/desktop/src/utils/logger.ts b/ui/desktop/src/utils/logger.ts index f11e8b2543e4..4a088ad32cd9 100644 --- a/ui/desktop/src/utils/logger.ts +++ b/ui/desktop/src/utils/logger.ts @@ -2,21 +2,11 @@ import log from 'electron-log'; import path from 'node:path'; import { app } from 'electron'; -// Configure electron-log -// In development: ~/Library/Logs/goose/main.log -// In production: ~/Library/Application Support/goose/logs/main.log log.transports.file.resolvePathFn = () => { - const isDev = process.env.NODE_ENV === 'development'; - if (isDev) { - return path.join(app.getPath('home'), 'Library/Logs/goose/main.log'); - } - return path.join(app.getPath('userData'), 'logs/main.log'); + return path.join(app.getPath('userData'), 'logs', 'main.log'); }; -// Configure log level based on environment -log.transports.file.level = process.env.NODE_ENV === 'development' ? 'debug' : 'info'; - -// Also log to console in development -log.transports.console.level = process.env.NODE_ENV === 'development' ? 'debug' : false; +log.transports.file.level = app.isPackaged ? 'info' : 'debug'; +log.transports.console.level = app.isPackaged ? false : 'debug'; export default log; diff --git a/ui/desktop/src/utils/ollamaDetection.test.ts b/ui/desktop/src/utils/ollamaDetection.test.ts index 8048b20187c0..07da6071b074 100644 --- a/ui/desktop/src/utils/ollamaDetection.test.ts +++ b/ui/desktop/src/utils/ollamaDetection.test.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -/* global AbortSignal, TextEncoder, Event, EventListener */ +/* global AbortSignal, TextEncoder, EventListener */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { diff --git a/ui/desktop/src/utils/pathUtils.ts b/ui/desktop/src/utils/pathUtils.ts index cfa2677d09f4..f2865227a80f 100644 --- a/ui/desktop/src/utils/pathUtils.ts +++ b/ui/desktop/src/utils/pathUtils.ts @@ -71,9 +71,7 @@ const addPaths = ( executableName: string, app: Electron.App ): void => { - const isDev = process.env.NODE_ENV === 'development'; - const isPackaged = app.isPackaged; - if (isDev && !isPackaged) { + if (!app.isPackaged) { possiblePaths.push( path.join(process.cwd(), 'src', 'bin', executableName), path.join(process.cwd(), 'bin', executableName), diff --git a/ui/desktop/src/utils/providerUtils.ts b/ui/desktop/src/utils/providerUtils.ts index 3dbd13434423..8862b5c12d69 100644 --- a/ui/desktop/src/utils/providerUtils.ts +++ b/ui/desktop/src/utils/providerUtils.ts @@ -5,6 +5,9 @@ import { } from '../components/settings/extensions'; import type { ExtensionConfig, FixedExtensionEntry } from '../components/ConfigContext'; import { Recipe, updateAgentProvider, updateFromSession } from '../api'; +import { toastService, ExtensionLoadingStatus } from '../toasts'; +import { errorMessage } from './conversionUtils'; +import { createExtensionRecoverHints } from './extensionErrorUtils'; // Helper function to substitute parameters in text export const substituteParameters = (text: string, params: Record): string => { @@ -77,24 +80,70 @@ export const initializeSystem = async ( // Add enabled extensions to agent in parallel const enabledExtensions = refreshedExtensions.filter((ext) => ext.enabled); + if (enabledExtensions.length === 0) { + return; + } + options?.setIsExtensionsLoading?.(true); + // Initialize extension status tracking + const extensionStatuses: Map = new Map( + enabledExtensions.map((ext) => [ext.name, { name: ext.name, status: 'loading' as const }]) + ); + + // Show initial loading toast + const updateToast = (isComplete: boolean = false) => { + toastService.extensionLoading( + Array.from(extensionStatuses.values()), + enabledExtensions.length, + isComplete + ); + }; + + updateToast(); + + // Load extensions in parallel and update status const extensionLoadingPromises = enabledExtensions.map(async (extensionConfig) => { const extensionName = extensionConfig.name; try { await addToAgentOnStartup({ - addToConfig: options.addExtension!, extensionConfig, - toastOptions: { silent: false }, + toastOptions: { silent: true }, // Silent since we're using grouped notification sessionId, }); + + // Update status to success + extensionStatuses.set(extensionName, { + name: extensionName, + status: 'success', + }); + updateToast(); } catch (error) { console.error(`Failed to load extension ${extensionName}:`, error); + + // Extract error message using shared utility + const errMsg = errorMessage(error); + + // Create recovery hints for "Ask goose" button + const recoverHints = createExtensionRecoverHints(errMsg); + + // Update status to error + extensionStatuses.set(extensionName, { + name: extensionName, + status: 'error', + error: errMsg, + recoverHints, + }); + updateToast(); } }); await Promise.allSettled(extensionLoadingPromises); + + // Show final completion toast + updateToast(true); + options?.setIsExtensionsLoading?.(false); } catch (error) { console.error('Failed to initialize agent:', error);