From 34f814c79e537609cd7f953d176bc9e8f17b00c2 Mon Sep 17 00:00:00 2001 From: Adam Poulemanos Date: Sat, 6 Dec 2025 16:26:35 -0500 Subject: [PATCH 01/37] initial test --- .github/workflows/_reusable-test.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/_reusable-test.yml b/.github/workflows/_reusable-test.yml index b3e78e042..a0ea1572b 100644 --- a/.github/workflows/_reusable-test.yml +++ b/.github/workflows/_reusable-test.yml @@ -75,7 +75,7 @@ jobs: with: fetch-depth: 3 - - name: Setup Python environment + - name: Setup Python environment with Mise if: ${{ matrix.experimental == false }} uses: ./.github/actions/setup-mise-env with: @@ -84,7 +84,7 @@ jobs: profile: "dev" skip-checkout: true id: setup-mise - - name: Run quality checks + - name: Run quality checks before tests if: ${{ inputs.run-quality-checks && matrix.experimental == false }} env: MISE_YES: 1 @@ -136,11 +136,10 @@ jobs: if: ${{ matrix.experimental == true }} shell: bash run: | - uv sync --no-extra --group test --resolution highest --frozen 2>&1 + uv sync --group test --resolution highest --frozen 2>&1 - name: Run test coverage for experimental Python if: ${{ matrix.experimental == true }} run: | export PATH="$HOME/.local/bin:$PATH" uv run -m pytest tests/ --cov=codeweaver --cov-report=xml --cov-report=term-missing --junit-xml=test-results.xml -v -m "${{ inputs.test-markers }}" - From 3b287ea3de1d708269cbea12413c0805ee358e6e Mon Sep 17 00:00:00 2001 From: Adam Poulemanos Date: Sat, 6 Dec 2025 16:45:45 -0500 Subject: [PATCH 02/37] fix: enhance CI configuration with Python version support and Mise environment setup --- .github/workflows/ci.yml | 7 ++++++- .github/workflows/copilot-setup-steps.yml | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04e681975..64253c1e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,6 +51,11 @@ jobs: CODEWEAVER_VECTOR_STORE_URL: ${{ secrets.CODEWEAVER_VECTOR_STORE_URL }} QDRANT__SERVICE__API_KEY: ${{ secrets.QDRANT__SERVICE__API_KEY }} VOYAGE_API_KEY: ${{ secrets.VOYAGE_API_KEY }} + with: + python-versions: '["3.12", "3.13", "3.14"]' + test-markers: "not docker and not qdrant and not dev_only and not skip_ci and not network and not external_api and not flaky" + upload-coverage: true + run-quality-checks: true lint: name: Lint @@ -63,4 +68,4 @@ jobs: - test - lint uses: ./.github/workflows/_reusable-build.yml - # No secrets needed for build job + # Build doesn't need secrets. diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 5651e67fb..ecd36a083 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -60,3 +60,21 @@ jobs: run: | chmod -R +x scripts chmod -R +x mise-tasks + - name: Setup Mise Environment + uses: ./.github/actions/setup-mise-env + with: + python-version: "3.13" + github-token: ${{ secrets.GITHUB_TOKEN }} + profile: "dev" + skip-checkout: "true" + id: setup-mise + - name: Verify Mise Setup + shell: bash + env: + MISE_PYTHON: "3.13" + MISE_YES: 1 + MISE_EXPERIMENTAL: 1 + MISE_PATH: ${{ steps.setup-mise.outputs.MISE_PATH }} + run: | + mise --version || echo "Mise not in path" || "$MISE_PATH" --version + echo "Mise Python:" From 036b74b59ced069b8c16ca1287d64041392671d7 Mon Sep 17 00:00:00 2001 From: Adam Poulemanos Date: Sat, 6 Dec 2025 16:48:34 -0500 Subject: [PATCH 03/37] fix: enhance CI configuration with Python version support and Mise environment setup --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64253c1e1..c2cdf8834 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,6 @@ jobs: QDRANT__SERVICE__API_KEY: ${{ secrets.QDRANT__SERVICE__API_KEY }} VOYAGE_API_KEY: ${{ secrets.VOYAGE_API_KEY }} with: - python-versions: '["3.12", "3.13", "3.14"]' test-markers: "not docker and not qdrant and not dev_only and not skip_ci and not network and not external_api and not flaky" upload-coverage: true run-quality-checks: true From f48671c6d20eabd8f21afca0cffe5125d9ef7c8a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 6 Dec 2025 22:08:18 +0000 Subject: [PATCH 04/37] fix: prevent CI skip by disabling env export in mise-action The mise-action was hanging during the "Exporting mise environment variables" step, causing the runner to kill the job with exit code 137 (SIGKILL). This was happening because: 1. The mise.toml has complex template expressions with command executions 2. The hooks.enter configuration may execute during env export 3. The env export was timing out after ~1 minute Solution: Disable the env parameter (set to false) in all mise-action configurations to prevent the hanging during environment variable export. Fixes #issue --- .github/actions/setup-mise-env/action.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/actions/setup-mise-env/action.yml b/.github/actions/setup-mise-env/action.yml index 386b88f9b..481418353 100644 --- a/.github/actions/setup-mise-env/action.yml +++ b/.github/actions/setup-mise-env/action.yml @@ -57,6 +57,7 @@ runs: install: true cache: true cache_key_prefix: ${{ inputs.profile }} + env: false id: mise-setup-minimal - name: Setup Mise for Reviewer Profile if: inputs.profile == 'reviewer' @@ -72,6 +73,7 @@ runs: install: true cache: true cache_key_prefix: ${{ inputs.profile }} + env: false - name: Activate Reviewer Profile Environment if: inputs.profile == 'reviewer' shell: bash @@ -92,6 +94,7 @@ runs: install: true cache: true cache_key_prefix: ${{ inputs.profile }} + env: false id: mise-setup-dev - name: Set outputs From 596900d26e6a24d90e45c869698a21f8349dcaa2 Mon Sep 17 00:00:00 2001 From: Adam Poulemanos Date: Sat, 6 Dec 2025 17:15:12 -0500 Subject: [PATCH 05/37] fix: remove unnecessary echo statement for Mise Python version in CI setup --- .github/workflows/copilot-setup-steps.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index ecd36a083..e7f009dd2 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -77,4 +77,3 @@ jobs: MISE_PATH: ${{ steps.setup-mise.outputs.MISE_PATH }} run: | mise --version || echo "Mise not in path" || "$MISE_PATH" --version - echo "Mise Python:" From 5cbce61ce985078da8a75a54064d76d7d404ec92 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 6 Dec 2025 22:20:56 +0000 Subject: [PATCH 06/37] fix: add mise bin directory to PATH for proper command execution The previous fix disabled env export to prevent hanging, but this caused MISE_PATH to be empty because mise wasn't in the PATH. Changes: 1. Add mise bin directory to PATH in "Set outputs" step 2. Add mise bin directory to PATH in workflow steps that execute mise commands This ensures: - The "Set outputs" step can find mise using which - Workflow steps can execute mise commands via $MISE_PATH - No hanging during env export (env: false is still set) Fixes the "command not found" error when running mise commands. --- .github/actions/setup-mise-env/action.yml | 2 ++ .github/workflows/_reusable-test.yml | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/actions/setup-mise-env/action.yml b/.github/actions/setup-mise-env/action.yml index 481418353..74ac44427 100644 --- a/.github/actions/setup-mise-env/action.yml +++ b/.github/actions/setup-mise-env/action.yml @@ -101,6 +101,8 @@ runs: shell: bash id: mise-outputs run: | + # Add mise to PATH since env export is disabled + export PATH="$HOME/.local/share/mise/bin:$PATH" echo "MISE_PATH=$(which mise)" >> $GITHUB_OUTPUT echo "MISE_ENV=$MISE_ENV" >> $GITHUB_OUTPUT echo "PROFILE=${{ inputs.profile }}" >> $GITHUB_OUTPUT diff --git a/.github/workflows/_reusable-test.yml b/.github/workflows/_reusable-test.yml index b3e78e042..4a26b2636 100644 --- a/.github/workflows/_reusable-test.yml +++ b/.github/workflows/_reusable-test.yml @@ -93,7 +93,7 @@ jobs: MISE_EXPERIMENTAL: 1 MISE_PATH: ${{ steps.setup-mise.outputs.MISE_PATH }} run: | - export PATH="$HOME/.local/bin:$PATH" + export PATH="$HOME/.local/share/mise/bin:$HOME/.local/bin:$PATH" "$MISE_PATH" //:check 2>&1 continue-on-error: true - name: Run tests with coverage @@ -105,7 +105,7 @@ jobs: MISE_EXPERIMENTAL: 1 MISE_PATH: ${{ steps.setup-mise.outputs.MISE_PATH }} run: | - export PATH="$HOME/.local/bin:$PATH" + export PATH="$HOME/.local/share/mise/bin:$HOME/.local/bin:$PATH" "$MISE_PATH" //:test-cov -m "${{ inputs.test-markers }}" - name: Upload coverage to Codecov if: ${{ inputs.upload-coverage && matrix.python-version == '3.12' }} From 9dfdd3c486c907ecddd2c39eb370e1a08ae0414e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 6 Dec 2025 22:26:10 +0000 Subject: [PATCH 07/37] fix: add outputs section to composite action for proper output exposure The composite action was setting outputs in the mise-outputs step, but those outputs were not being exposed by the action itself because the action.yml was missing the outputs section. Changes: - Add outputs section to action.yml defining all output variables - Map outputs from mise-outputs step to action outputs This ensures that steps.setup-mise.outputs.MISE_PATH and other outputs are properly available to the calling workflow. Related to the CI skip issue fix. --- .github/actions/setup-mise-env/action.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/actions/setup-mise-env/action.yml b/.github/actions/setup-mise-env/action.yml index 74ac44427..a8781bbb8 100644 --- a/.github/actions/setup-mise-env/action.yml +++ b/.github/actions/setup-mise-env/action.yml @@ -28,6 +28,23 @@ inputs: required: true default: 'minimal' +outputs: + MISE_PATH: + description: Path to mise executable + value: ${{ steps.mise-outputs.outputs.MISE_PATH }} + MISE_ENV: + description: Mise environment name + value: ${{ steps.mise-outputs.outputs.MISE_ENV }} + PROFILE: + description: Profile used for setup + value: ${{ steps.mise-outputs.outputs.PROFILE }} + PYTHON_VERSION: + description: Python version installed + value: ${{ steps.mise-outputs.outputs.PYTHON_VERSION }} + PYTHON_PATH: + description: Path to Python executable + value: ${{ steps.mise-outputs.outputs.PYTHON_PATH }} + runs: using: composite steps: From fb5ad88d1d57bbbcc2ff96eaf93a3973c137fc5e Mon Sep 17 00:00:00 2001 From: Adam Poulemanos Date: Sat, 6 Dec 2025 17:35:28 -0500 Subject: [PATCH 08/37] fix: missing tools on path --- .github/actions/setup-mise-env/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/setup-mise-env/action.yml b/.github/actions/setup-mise-env/action.yml index a8781bbb8..3a032eaaa 100644 --- a/.github/actions/setup-mise-env/action.yml +++ b/.github/actions/setup-mise-env/action.yml @@ -119,7 +119,7 @@ runs: id: mise-outputs run: | # Add mise to PATH since env export is disabled - export PATH="$HOME/.local/share/mise/bin:$PATH" + export PATH="$HOME/.local/share/mise/bin:$HOME/.local/share/mise/shims:$PATH" echo "MISE_PATH=$(which mise)" >> $GITHUB_OUTPUT echo "MISE_ENV=$MISE_ENV" >> $GITHUB_OUTPUT echo "PROFILE=${{ inputs.profile }}" >> $GITHUB_OUTPUT From 147548c77c16bb5b5dc37c91376d048208d1e412 Mon Sep 17 00:00:00 2001 From: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> Date: Sat, 6 Dec 2025 17:46:55 -0500 Subject: [PATCH 09/37] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> --- .github/actions/setup-mise-env/action.yml | 2 +- .github/workflows/copilot-setup-steps.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/setup-mise-env/action.yml b/.github/actions/setup-mise-env/action.yml index 3a032eaaa..8f88b9bcf 100644 --- a/.github/actions/setup-mise-env/action.yml +++ b/.github/actions/setup-mise-env/action.yml @@ -121,7 +121,7 @@ runs: # Add mise to PATH since env export is disabled export PATH="$HOME/.local/share/mise/bin:$HOME/.local/share/mise/shims:$PATH" echo "MISE_PATH=$(which mise)" >> $GITHUB_OUTPUT - echo "MISE_ENV=$MISE_ENV" >> $GITHUB_OUTPUT + echo "MISE_ENV=${MISE_ENV:-}" >> $GITHUB_OUTPUT echo "PROFILE=${{ inputs.profile }}" >> $GITHUB_OUTPUT echo "PYTHON_VERSION=$(python --version | awk '{print $2}')" >> $GITHUB_OUTPUT echo "PYTHON_PATH=$(which python)" >> $GITHUB_OUTPUT diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index e7f009dd2..2f0da4588 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -76,4 +76,4 @@ jobs: MISE_EXPERIMENTAL: 1 MISE_PATH: ${{ steps.setup-mise.outputs.MISE_PATH }} run: | - mise --version || echo "Mise not in path" || "$MISE_PATH" --version + mise --version || { echo "Mise not in path"; "$MISE_PATH" --version; } From a6397861bf3ef3417a24fa8d6c490adafb24d5e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 6 Dec 2025 22:50:55 +0000 Subject: [PATCH 10/37] Initial plan From eae8605933d131dcb15b764539c16622cd500181 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 6 Dec 2025 22:55:32 +0000 Subject: [PATCH 11/37] fix: remove duplicate Setup Mise Environment step Remove the first instance of "Setup Mise Environment" step (lines 52-58) as it was a duplicate. The second instance with id: setup-mise is kept since it's needed for the Verify Mise Setup step that references it. Co-authored-by: bashandbone <89049923+bashandbone@users.noreply.github.com> --- .github/workflows/copilot-setup-steps.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 2f0da4588..2e6a7164e 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -49,13 +49,6 @@ jobs: fetch-depth: 2 - name: Free up disk space run: ./scripts/dev-env/ci-free-disk-space.sh - - name: Setup Mise Environment - uses: ./.github/actions/setup-mise-env - with: - python-version: "3.13" - github-token: ${{ secrets.GITHUB_TOKEN }} - profile: "dev" - skip-checkout: "true" - name: Make scripts executable run: | chmod -R +x scripts From db32aad7e95584384ef075fc3d9811923b2a8a13 Mon Sep 17 00:00:00 2001 From: Adam Poulemanos Date: Sat, 6 Dec 2025 21:52:11 -0500 Subject: [PATCH 12/37] fix: update PATH in workflows and improve MCP Registry submission process --- .github/actions/setup-mise-env/action.yml | 2 - .github/workflows/_reusable-lint.yml | 4 +- .github/workflows/_reusable-test.yml | 6 +- .github/workflows/claude.yml | 1 + .github/workflows/copilot-setup-steps.yml | 1 + .github/workflows/mcp-registry-submit.yml | 246 +++++++--------------- .github/workflows/release.yml | 4 +- 7 files changed, 90 insertions(+), 174 deletions(-) diff --git a/.github/actions/setup-mise-env/action.yml b/.github/actions/setup-mise-env/action.yml index 8f88b9bcf..319070f4b 100644 --- a/.github/actions/setup-mise-env/action.yml +++ b/.github/actions/setup-mise-env/action.yml @@ -125,5 +125,3 @@ runs: echo "PROFILE=${{ inputs.profile }}" >> $GITHUB_OUTPUT echo "PYTHON_VERSION=$(python --version | awk '{print $2}')" >> $GITHUB_OUTPUT echo "PYTHON_PATH=$(which python)" >> $GITHUB_OUTPUT - - diff --git a/.github/workflows/_reusable-lint.yml b/.github/workflows/_reusable-lint.yml index 50e5299a3..7e5bb283f 100644 --- a/.github/workflows/_reusable-lint.yml +++ b/.github/workflows/_reusable-lint.yml @@ -53,7 +53,7 @@ jobs: MISE_EXPERIMENTAL: 1 PYTHON_PATH: ${{ steps.mise-outputs.outputs.PYTHON_PATH }} run: | - export PATH="$HOME/.local/bin:$PATH" + export PATH="$HOME/.local/bin:$HOME/.local/share/mise/bin:$HOME/.local/share/mise/shims:$PATH" mise //:lint || echo "::warning::Lint checks failed but allowing workflow to continue" continue-on-error: true @@ -66,6 +66,6 @@ jobs: MISE_EXPERIMENTAL: 1 PYTHON_PATH: ${{ steps.mise-outputs.outputs.PYTHON_PATH }} run: | - export PATH="$HOME/.local/bin:$PATH" + export PATH="$HOME/.local/bin:$HOME/.local/share/mise/bin:$HOME/.local/share/mise/shims:$PATH" mise //:format || echo "::warning::License checks or formatting checks failed but allowing workflow to continue" continue-on-error: true diff --git a/.github/workflows/_reusable-test.yml b/.github/workflows/_reusable-test.yml index 760e60a9c..535f21fcf 100644 --- a/.github/workflows/_reusable-test.yml +++ b/.github/workflows/_reusable-test.yml @@ -93,7 +93,7 @@ jobs: MISE_EXPERIMENTAL: 1 MISE_PATH: ${{ steps.setup-mise.outputs.MISE_PATH }} run: | - export PATH="$HOME/.local/share/mise/bin:$HOME/.local/bin:$PATH" + export PATH="$HOME/.local/bin:$HOME/.local/share/mise/bin:$HOME/.local/share/mise/shims:$PATH" "$MISE_PATH" //:check 2>&1 continue-on-error: true - name: Run tests with coverage @@ -105,7 +105,7 @@ jobs: MISE_EXPERIMENTAL: 1 MISE_PATH: ${{ steps.setup-mise.outputs.MISE_PATH }} run: | - export PATH="$HOME/.local/share/mise/bin:$HOME/.local/bin:$PATH" + export PATH="$HOME/.local/bin:$HOME/.local/share/mise/bin:$HOME/.local/share/mise/shims:$PATH" "$MISE_PATH" //:test-cov -m "${{ inputs.test-markers }}" - name: Upload coverage to Codecov if: ${{ inputs.upload-coverage && matrix.python-version == '3.12' }} @@ -141,5 +141,5 @@ jobs: - name: Run test coverage for experimental Python if: ${{ matrix.experimental == true }} run: | - export PATH="$HOME/.local/bin:$PATH" + export PATH="$HOME/.local/bin:$HOME/.local/share/mise/bin:$HOME/.local/share/mise/shims:$PATH" uv run -m pytest tests/ --cov=codeweaver --cov-report=xml --cov-report=term-missing --junit-xml=test-results.xml -v -m "${{ inputs.test-markers }}" diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index fde6b9af6..036145d57 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -61,6 +61,7 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} profile: "reviewer" skip-checkout: "true" + - name: PR Review if: github.event_name == 'pull_request_review' uses: anthropics/claude-code-action@e8bad572273ce919ba15fec95aef0ce974464753 diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 2e6a7164e..61324ec86 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -69,4 +69,5 @@ jobs: MISE_EXPERIMENTAL: 1 MISE_PATH: ${{ steps.setup-mise.outputs.MISE_PATH }} run: | + export PATH="$HOME/.local/bin:$HOME/.local/share/mise/bin:$HOME/.local/share/mise/shims:$PATH" mise --version || { echo "Mise not in path"; "$MISE_PATH" --version; } diff --git a/.github/workflows/mcp-registry-submit.yml b/.github/workflows/mcp-registry-submit.yml index 9b85ca145..785d16107 100644 --- a/.github/workflows/mcp-registry-submit.yml +++ b/.github/workflows/mcp-registry-submit.yml @@ -29,17 +29,14 @@ jobs: uses: actions/checkout@b3498302c5c423fa896b97a26bb183df735d08f8 with: ref: ${{ github.event.release.tag_name || github.ref }} + fetch-depth: 1 - - name: Set up Python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c + - name: Install uv + uses: ./.github/actions/setup-uv-env with: python-version: "3.12" - - - name: Install uv - run: | - curl -LsSf "https://astral.sh/uv/install.sh" | sh - echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" - + github-token: ${{ secrets.GITHUB_TOKEN }} + id: uv-setup - name: Extract version information id: version run: | @@ -64,90 +61,7 @@ jobs: # Run the generation script (uses PEP 723 dependencies) uv run scripts/build/generate-mcp-server-json.py - - # Update version in generated server.json - python -c " - import json - - with open('server.json', 'r') as f: - data = json.load(f) - - # Update top-level version - data['version'] = '${VERSION}' - - # Update package versions - if 'packages' in data: - for pkg in data['packages']: - pkg['version'] = '${VERSION}' - - with open('server.json', 'w') as f: - json.dump(data, f, indent=2) - - print(f'✓ Regenerated and versioned server.json to ${VERSION}') - " - - - name: Update tools.json with release version - run: | - VERSION="${{ steps.version.outputs.version }}" - # Update the version fields in tools.json - python -c " - import json - import sys - - with open('tools.json', 'r') as f: - tools_data = json.load(f) - - # Update top-level version - tools_data['version'] = '${VERSION}' - # Update server version if present - if 'server' in tools_data: - tools_data['server']['version'] = '${VERSION}' - - with open('tools.json', 'w') as f: - json.dump(tools_data, f, indent=2) - - print(f'Updated tools.json to version ${VERSION}') - " - - - name: Validate server.json format - run: | - python -c " - import json - import sys - - try: - with open('server.json', 'r') as f: - data = json.load(f) - - # Basic validation checks - required_fields = ['name', 'description', 'version'] - missing = [f for f in required_fields if f not in data] - - if missing: - print(f'ERROR: Missing required fields: {missing}') - sys.exit(1) - - # Validate name format (must be namespace/server-name) - if '/' not in data['name']: - print('ERROR: name must be in format namespace/server-name') - sys.exit(1) - - # Validate packages or remotes exist - if 'packages' not in data and 'remotes' not in data: - print('ERROR: Must have either packages or remotes array') - sys.exit(1) - - print('✓ server.json validation passed') - print(f' Name: {data[\"name\"]}') - print(f' Version: {data[\"version\"]}') - print(f' Description: {data[\"description\"][:80]}...') - - except json.JSONDecodeError as e: - print(f'ERROR: Invalid JSON in server.json: {e}') - sys.exit(1) - except Exception as e: - print(f'ERROR: Validation failed: {e}') - sys.exit(1) + echo "✓ server.json regenerated " - name: Wait for PyPI package availability @@ -201,48 +115,38 @@ jobs: - name: Install mcp-publisher CLI env: - MCP_PUBLISHER_VERSION: "v1.0.0" + MCP_PUBLISHER_VERSION: "v1.3.10" run: | ARCH="$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')" OS="$(uname -s | tr '[:upper:]' '[:lower:]')" - VERSION="${MCP_PUBLISHER_VERSION:- v1.0.0}" - NO_V_VERSION="${VERSION#v}" - + VERSION="${MCP_PUBLISHER_VERSION:- v1.3.10}" + # TODO: I don't know why we're doing all this to get and use the CLI when we could use the api directly echo "Downloading mcp-publisher ${VERSION} for ${OS}/${ARCH}..." - curl -L "https://github.com/modelcontextprotocol/registry/releases/download/${VERSION}/mcp-publisher_${NO_V_VERSION}_${OS}_${ARCH}.tar.gz" \ - -o mcp-publisher.tar.gz - curl -L "https://github.com/modelcontextprotocol/registry/releases/download/${VERSION}/mcp-publisher_${NO_V_VERSION}_${OS}_${ARCH}.tar.gz.sha256" \ - -o mcp-publisher.tar.gz.sha256 - - echo "Verifying checksum for mcp-publisher.tar.gz..." - if ! sha256sum -c mcp-publisher.tar.gz.sha256; then - echo "ERROR: Checksum verification failed for mcp-publisher.tar.gz" + assets="$(curl -L \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer " \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/OWNER/REPO/releases/latest)" + our_index="$(echo "$assets" | jq -r '.assets[].name' | grep -n "^mcp-publisher_${OS}_${ARCH}.tar.gz$" | cut -d: -f1)" + asset="$(echo "$assets" | jq -r ".assets[$((our_index - 1))]")" + download_url="$(echo "$asset" | jq -r '.browser_download_url')" + digest="$(echo "$asset" | jq -r '.digest' | cut -d: -f2)" + curl -L -o mcp-publisher.tar.gz "$download_url" + downloaded_digest="$(sha256sum mcp-publisher.tar.gz | awk '{print $1}')" + if [ "$digest" != "$downloaded_digest" ]; then + echo "ERROR: Checksum verification failed for mcp-publisher" + echo "Expected: $digest" + echo "Got: $downloaded_digest" exit 1 fi - tar xz < mcp-publisher.tar.gz mcp-publisher sudo mv mcp-publisher /usr/local/bin/ chmod +x /usr/local/bin/mcp-publisher - rm mcp-publisher.tar.gz mcp-publisher.tar.gz.sha256 + rm mcp-publisher.tar.gz echo "✓ mcp-publisher installed" mcp-publisher --version || echo "Version check not available" - - name: Validate server.json with mcp-publisher - run: | - echo "Validating server.json against MCP schema..." - - # mcp-publisher validate should validate the server.json - # Note: exact command may vary based on mcp-publisher version - if command -v mcp-publisher &> /dev/null; then - mcp-publisher validate server.json || { - echo "ERROR: server.json validation failed" - exit 1 - } - else - echo "WARNING: mcp-publisher validate command not available, skipping" - fi - - name: Submit to MCP Registry env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -250,52 +154,62 @@ jobs: run: | echo "Submitting to MCP Registry..." - # Note: The exact submission process depends on mcp-publisher CLI implementation - # and may require manual intervention if the API/CLI has changed. - # The following attempts multiple submission methods in order of preference. - MESSAGE="" - # login to mcp-publisher - if command -v mcp-publisher &> /dev/null; then - echo "Logging in to mcp-publisher..." - mcp-publisher login dns --domain knitli.com --private-key "${MCP_REGISTRY_KEY}" || { - echo "WARNING: mcp-publisher login failed, proceeding without login" - } - MESSAGE="$(mcp-publisher publish)" - else - echo "WARNING: mcp-publisher CLI not available, skipping login" - fi - if [ -n "$MESSAGE" ] && [[ "$MESSAGE" == *Successfully* ]]; then - echo "✓ Successfully submitted to MCP Registry via mcp-publisher" - echo "$MESSAGE" - exit 0 - else - RESPONSE="$(curl -X POST \ - -H "Authorization: Bearer ${GITHUB_TOKEN}" \ - -H "Content-Type: application/json" \ - -d @server.json \ - -w "\n%{http_code}" \ - https://registry.modelcontextprotocol.io/v0/servers 2>&1)" + # we need to login with our domain key first + # the request has three fields: domain (knitli.com), signed_timestamp (hex encoded signature), timestamp (RFC3339) + # we need to sign the timestamp with the private key, which is in MCP_REGISTRY_KEY + # MCP_REGISTRY_KEY is expected to be in PEM format, and it uses ed25519 algorithm + # unfortunately, pkeyutl can only work with files because of how it has to buffer the input for signing - HTTP_CODE="$(echo "$RESPONSE" | tail -n 1)" - BODY="$(echo "$RESPONSE" | head -n -1)" + domain="knitli.com" + indata="$(mktemp)" + echo -n "$(date -u +"%Y-%m-%dT%+H:%+M:%+SZ")" > "$indata" - if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ]; then - echo "✓ Successfully submitted to MCP Registry (HTTP ${HTTP_CODE})" - exit 0 - else - # Submission failed - provide helpful error message - echo "ERROR: Failed to submit to MCP Registry (HTTP ${HTTP_CODE})" - echo "Response: ${BODY}" - echo "" - echo "This may require manual submission via:" - echo " 1. The registry web interface at https://registry.modelcontextprotocol.io" - echo " 2. The mcp-publisher CLI tool" - echo " 3. Contact registry maintainers for assistance" - exit 1 - fi + key_file="$(mktemp)" + echo "$MCP_REGISTRY_KEY" > "$key_file" + chmod 600 "$key_file" + outfile="$(mktemp)" + + openssl pkeyutl -sign -inkey "$key_file" -out "$outfile" -rawin -in "$indata" + signed_timestamp_hex="$(xxd -p "$outfile" | tr -d '\n')" + request_body="$(mktemp)" + { + echo -n '{"domain":"'"$domain"'",' + echo -n '"signed_timestamp":"'"$signed_timestamp_hex"'",' + echo -n '"timestamp":"'"$(cat "$indata")"'"}' + } > "$request_body" + echo "Logging in to MCP Registry..." + response="$(curl --request POST \ + --url "https://registry.modelcontextprotocol.io/v0.1/auth/dns" \ + --header "Accept: application/json, application/problem+json" \ + --header "Content-Type: application/json" \ + --data @"$request_body")" + token="$(echo "$response" | jq -r '.registry_token')" + rm "$indata" "$key_file" "$outfile" + rm "$request_body" + if [ -z "$token" ] || [ "$token" = "null" ]; then + echo "ERROR: Failed to obtain registry token" + echo "Response: $response" + exit 1 fi + echo "✓ Obtained registry token" + export MCP_LOGIN_TOKEN="$token" + publication_response="$(curl --request POST \ + --url https://registry.modelcontextprotocol.io/v0.1/publish \ + --header 'Accept: application/json, application/problem+json' \ + --header "Authorization: Bearer $MCP_LOGIN_TOKEN" \ + --header 'Content-Type: application/json' \ + --data @./server.json)" + echo "Publication response: $publication_response" + publication_time="$(echo "$publication_response" | jq -r '._meta."io.modelcontextprotocol.registry/official".published_at')" + if [ -z "$publication_time" ] || [ "$publication_time" = "null" ]; then + echo "ERROR: Submission to MCP Registry failed" + exit 1 + fi + echo "✓ Successfully submitted to MCP Registry at $publication_time" + export "MCP_PUBLICATION_TIME"="$publication_time" + exit 0 - - name: Summary + - name: Summary of MCP Registry Submission if: always() run: | { @@ -303,6 +217,9 @@ jobs: echo "" echo "- **Version**: ${{ steps.version.outputs.version }}" echo "- **Status**: ${{ job.status }}" + if [ -n "$MCP_PUBLICATION_TIME" ]; then + echo "- **Published At**: $MCP_PUBLICATION_TIME" + fi echo "" } >> "$GITHUB_STEP_SUMMARY" @@ -310,13 +227,12 @@ jobs: { echo "✅ Successfully submitted to MCP Registry" echo "" - echo "View your server at: https://registry.modelcontextprotocol.io/servers/com.knitli/codeweaver" + echo "View your server at: https://registry.modelcontextprotocol.io/v0.1/servers/com.knitli%2Fcodeweaver/versions/${{ steps.version.outputs.version }}" } >> "$GITHUB_STEP_SUMMARY" else { echo "❌ Submission failed" echo "" echo "Please check the workflow logs for details." - echo "You may need to submit manually at: https://registry.modelcontextprotocol.io" } >> "$GITHUB_STEP_SUMMARY" fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6205d2f94..289a74129 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,8 +32,8 @@ jobs: if: github.event_name == 'workflow_dispatch' || github.event_name == 'release' uses: ./.github/workflows/_reusable-test.yml with: - python-versions: '["3.12", "3.13"]' - test-markers: "not docker and not qdrant and not dev_only and not skip_ci and not network and not external_api" + python-versions: '["3.12", "3.13", "3.14"]' + test-markers: "not docker and not qdrant and not dev_only and not skip_ci and not network and not external_api and not flaky" upload-coverage: false run-quality-checks: true secrets: inherit From 7631fdc1c581271bc6b08e1a2bf33a91cc6db5f6 Mon Sep 17 00:00:00 2001 From: Adam Poulemanos Date: Sat, 6 Dec 2025 22:05:46 -0500 Subject: [PATCH 13/37] fix: removed 'frozen' flag from uv for experimental python installs. This was preventing package resolution --- .github/workflows/_reusable-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_reusable-test.yml b/.github/workflows/_reusable-test.yml index 535f21fcf..cdcb6dfc7 100644 --- a/.github/workflows/_reusable-test.yml +++ b/.github/workflows/_reusable-test.yml @@ -136,7 +136,7 @@ jobs: if: ${{ matrix.experimental == true }} shell: bash run: | - uv sync --group test --resolution highest --frozen 2>&1 + uv sync --group test --resolution highest 2>&1 - name: Run test coverage for experimental Python if: ${{ matrix.experimental == true }} From 38d05088cda5d7acef14a1fa80e2793fffef1053 Mon Sep 17 00:00:00 2001 From: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> Date: Sat, 6 Dec 2025 22:18:11 -0500 Subject: [PATCH 14/37] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> --- .github/workflows/mcp-registry-submit.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/mcp-registry-submit.yml b/.github/workflows/mcp-registry-submit.yml index 785d16107..9e54e108a 100644 --- a/.github/workflows/mcp-registry-submit.yml +++ b/.github/workflows/mcp-registry-submit.yml @@ -61,8 +61,7 @@ jobs: # Run the generation script (uses PEP 723 dependencies) uv run scripts/build/generate-mcp-server-json.py - echo "✓ server.json regenerated - " + echo "✓ server.json regenerated" - name: Wait for PyPI package availability run: | @@ -162,7 +161,7 @@ jobs: domain="knitli.com" indata="$(mktemp)" - echo -n "$(date -u +"%Y-%m-%dT%+H:%+M:%+SZ")" > "$indata" + echo -n "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" > "$indata" key_file="$(mktemp)" echo "$MCP_REGISTRY_KEY" > "$key_file" @@ -206,7 +205,7 @@ jobs: exit 1 fi echo "✓ Successfully submitted to MCP Registry at $publication_time" - export "MCP_PUBLICATION_TIME"="$publication_time" + export MCP_PUBLICATION_TIME="$publication_time" exit 0 - name: Summary of MCP Registry Submission From 6092ce7d458e9149f889632e747c2ffeee012942 Mon Sep 17 00:00:00 2001 From: Adam Poulemanos Date: Sat, 6 Dec 2025 22:21:46 -0500 Subject: [PATCH 15/37] fix: remove mcp-publisher CLI installation step and streamline submission process --- .github/workflows/mcp-registry-submit.yml | 42 +++-------------------- 1 file changed, 4 insertions(+), 38 deletions(-) diff --git a/.github/workflows/mcp-registry-submit.yml b/.github/workflows/mcp-registry-submit.yml index 9e54e108a..9488cc0dd 100644 --- a/.github/workflows/mcp-registry-submit.yml +++ b/.github/workflows/mcp-registry-submit.yml @@ -112,44 +112,11 @@ jobs: echo "Please verify that the package was successfully published" exit 1 - - name: Install mcp-publisher CLI - env: - MCP_PUBLISHER_VERSION: "v1.3.10" - run: | - ARCH="$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')" - OS="$(uname -s | tr '[:upper:]' '[:lower:]')" - VERSION="${MCP_PUBLISHER_VERSION:- v1.3.10}" - # TODO: I don't know why we're doing all this to get and use the CLI when we could use the api directly - echo "Downloading mcp-publisher ${VERSION} for ${OS}/${ARCH}..." - assets="$(curl -L \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer " \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - https://api.github.com/repos/OWNER/REPO/releases/latest)" - our_index="$(echo "$assets" | jq -r '.assets[].name' | grep -n "^mcp-publisher_${OS}_${ARCH}.tar.gz$" | cut -d: -f1)" - asset="$(echo "$assets" | jq -r ".assets[$((our_index - 1))]")" - download_url="$(echo "$asset" | jq -r '.browser_download_url')" - digest="$(echo "$asset" | jq -r '.digest' | cut -d: -f2)" - curl -L -o mcp-publisher.tar.gz "$download_url" - downloaded_digest="$(sha256sum mcp-publisher.tar.gz | awk '{print $1}')" - if [ "$digest" != "$downloaded_digest" ]; then - echo "ERROR: Checksum verification failed for mcp-publisher" - echo "Expected: $digest" - echo "Got: $downloaded_digest" - exit 1 - fi - tar xz < mcp-publisher.tar.gz mcp-publisher - sudo mv mcp-publisher /usr/local/bin/ - chmod +x /usr/local/bin/mcp-publisher - rm mcp-publisher.tar.gz - - echo "✓ mcp-publisher installed" - mcp-publisher --version || echo "Version check not available" - - name: Submit to MCP Registry env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} MCP_REGISTRY_KEY: ${{ secrets.MCP_REGISTRY_KEY }} + id: submit-mcp run: | echo "Submitting to MCP Registry..." @@ -183,8 +150,7 @@ jobs: --header "Content-Type: application/json" \ --data @"$request_body")" token="$(echo "$response" | jq -r '.registry_token')" - rm "$indata" "$key_file" "$outfile" - rm "$request_body" + rm "$indata" "$key_file" "$outfile" "$request_body" if [ -z "$token" ] || [ "$token" = "null" ]; then echo "ERROR: Failed to obtain registry token" echo "Response: $response" @@ -205,7 +171,7 @@ jobs: exit 1 fi echo "✓ Successfully submitted to MCP Registry at $publication_time" - export MCP_PUBLICATION_TIME="$publication_time" + echo "MCP_PUBLICATION_TIME=$publication_time" >> "$GITHUB_OUTPUT" exit 0 - name: Summary of MCP Registry Submission @@ -217,7 +183,7 @@ jobs: echo "- **Version**: ${{ steps.version.outputs.version }}" echo "- **Status**: ${{ job.status }}" if [ -n "$MCP_PUBLICATION_TIME" ]; then - echo "- **Published At**: $MCP_PUBLICATION_TIME" + echo "- **Published At**: ${{ steps.submit-mcp.outputs.MCP_PUBLICATION_TIME }}" fi echo "" } >> "$GITHUB_STEP_SUMMARY" From 9508d15f3ac0c2c6fc9c0933f4412eef928154e9 Mon Sep 17 00:00:00 2001 From: Adam Poulemanos Date: Sat, 6 Dec 2025 23:32:09 -0500 Subject: [PATCH 16/37] fix: enhance CI workflows by adding outputs and environment variables for better integration and testing --- .github/actions/setup-uv-env/action.yml | 8 ++++ .github/workflows/_reusable-build.yml | 5 ++- .github/workflows/_reusable-lint.yml | 35 ++++++++-------- .github/workflows/_reusable-test.yml | 50 +++++++++++------------ .github/workflows/claude.yml | 41 ++++++++++--------- .github/workflows/copilot-setup-steps.yml | 8 ++++ .github/workflows/mcp-registry-submit.yml | 4 +- .github/workflows/release.yml | 4 ++ 8 files changed, 89 insertions(+), 66 deletions(-) diff --git a/.github/actions/setup-uv-env/action.yml b/.github/actions/setup-uv-env/action.yml index 918113526..a5972c170 100644 --- a/.github/actions/setup-uv-env/action.yml +++ b/.github/actions/setup-uv-env/action.yml @@ -28,6 +28,14 @@ inputs: required: false default: 'false' +outputs: + UV_PATH: + description: Path to uv executable + value: ${{ steps.uv-outputs.outputs.UV_PATH }} + UVX_PATH: + description: Path to uvx executable + value: ${{ steps.uv-outputs.outputs.UVX_PATH }} + runs: using: composite steps: diff --git a/.github/workflows/_reusable-build.yml b/.github/workflows/_reusable-build.yml index a3f0346f8..b59641359 100644 --- a/.github/workflows/_reusable-build.yml +++ b/.github/workflows/_reusable-build.yml @@ -33,13 +33,15 @@ jobs: build: name: Build Package runs-on: ubuntu-latest + env: + UV_PYTHON: ${{ inputs.python-version }} permissions: contents: read steps: - name: Checkout code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 with: - fetch-depth: 0 + fetch-depth: 2 - name: Setup UV and build uses: ./.github/actions/setup-uv-env @@ -54,7 +56,6 @@ jobs: - name: Build package with UV env: UV_PATH: ${{ steps.setup-uv.outputs.uv-path }} - UV_PYTHON: ${{ inputs.python-version }} run: | if [ "${{ inputs.clean-build }}" = "true" ]; then "$UV_PATH" build --clear diff --git a/.github/workflows/_reusable-lint.yml b/.github/workflows/_reusable-lint.yml index 7e5bb283f..066b90456 100644 --- a/.github/workflows/_reusable-lint.yml +++ b/.github/workflows/_reusable-lint.yml @@ -21,15 +21,21 @@ on: type: boolean default: true +env: + MISE_EXPERIMENTAL: 1 + MISE_YES: 1 + PROFILE: "reviewer" + jobs: lint: permissions: contents: read name: Lint and Format - runs-on: ubuntu-latest env: - MISE_ENV: ci - MISE_EXPERIMENTAL: 1 + python-version: ${{ inputs.python-version }} + MISE_PYTHON: ${{ inputs.python-version }} + UV_PYTHON: ${{ inputs.python-version }} + runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 @@ -39,33 +45,26 @@ jobs: - name: Setup Python environment uses: ./.github/actions/setup-mise-env with: - python-version: ${{ inputs.python-version }} + python-version: ${{ env.python-version }} github-token: ${{ secrets.GITHUB_TOKEN }} - profile: "reviewer" + profile: "${{ env.PROFILE }}" skip-checkout: true id: mise-outputs - name: Check code style and linting env: - MISE_YES: 1 - MISE_PYTHON: ${{ inputs.python-version }} - UV_PYTHON: ${{ inputs.python-version }} - MISE_EXPERIMENTAL: 1 PYTHON_PATH: ${{ steps.mise-outputs.outputs.PYTHON_PATH }} + shell: bash run: | - export PATH="$HOME/.local/bin:$HOME/.local/share/mise/bin:$HOME/.local/share/mise/shims:$PATH" + export PATH="$HOME/.local/bin:$HOME/.local/share/mise/bin:$PATH" + echo "PYTHON_PATH=$PYTHON_PATH" >> "$GITHUB_ENV" + eval "$(mise activate bash)" + echo "PATH=$PATH" >> "$GITHUB_ENV" mise //:lint || echo "::warning::Lint checks failed but allowing workflow to continue" continue-on-error: true - name: Check formatting and licenses - if: inputs.run-format-check - env: - MISE_YES: 1 - MISE_PYTHON: ${{ inputs.python-version }} - UV_PYTHON: ${{ inputs.python-version }} - MISE_EXPERIMENTAL: 1 - PYTHON_PATH: ${{ steps.mise-outputs.outputs.PYTHON_PATH }} + if: ${{ inputs.run-format-check == true }} run: | - export PATH="$HOME/.local/bin:$HOME/.local/share/mise/bin:$HOME/.local/share/mise/shims:$PATH" mise //:format || echo "::warning::License checks or formatting checks failed but allowing workflow to continue" continue-on-error: true diff --git a/.github/workflows/_reusable-test.yml b/.github/workflows/_reusable-test.yml index cdcb6dfc7..f1422893c 100644 --- a/.github/workflows/_reusable-test.yml +++ b/.github/workflows/_reusable-test.yml @@ -39,6 +39,18 @@ on: required: false VOYAGE_API_KEY: required: false +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MISE_EXPERIMENTAL: 1 + MISE_YES: 1 + PROFILE: "dev" + CODEWEAVER_PROJECT_PATH: ${{ github.workspace }} + CODEWEAVER_TESTING: "true" + CODEWEAVER_VECTOR_STORE_URL: ${{ secrets.CODEWEAVER_VECTOR_STORE_URL }} + QDRANT__SERVICE__API_KEY: ${{ secrets.QDRANT__SERVICE__API_KEY }} + VOYAGE_API_KEY: ${{ secrets.VOYAGE_API_KEY }} + MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} jobs: test: @@ -57,16 +69,6 @@ jobs: - python-version: "3.14t" experimental: true env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - CODEWEAVER_TESTING: "true" - CODEWEAVER_VECTOR_STORE_URL: ${{ secrets.CODEWEAVER_VECTOR_STORE_URL }} - QDRANT__SERVICE__API_KEY: ${{ secrets.QDRANT__SERVICE__API_KEY }} - VOYAGE_API_KEY: ${{ secrets.VOYAGE_API_KEY }} - CODEWEAVER_PROJECT_PATH: ${{ github.workspace }} - MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - MISE_EXPERIMENTAL: 1 - MISE_YES: 1 MISE_PYTHON: ${{ matrix.python-version }} UV_PYTHON: ${{ matrix.python-version }} steps: @@ -87,26 +89,18 @@ jobs: - name: Run quality checks before tests if: ${{ inputs.run-quality-checks && matrix.experimental == false }} env: - MISE_YES: 1 - MISE_PYTHON: ${{ matrix.python-version }} - UV_PYTHON: ${{ matrix.python-version }} - MISE_EXPERIMENTAL: 1 MISE_PATH: ${{ steps.setup-mise.outputs.MISE_PATH }} run: | export PATH="$HOME/.local/bin:$HOME/.local/share/mise/bin:$HOME/.local/share/mise/shims:$PATH" - "$MISE_PATH" //:check 2>&1 + echo "MISE_PATH=$MISE_PATH" >> "$GITHUB_ENV" + eval "$(mise activate bash)" + echo "PATH=$PATH" >> "$GITHUB_ENV" + mise //:check 2>&1 continue-on-error: true - name: Run tests with coverage if: ${{ matrix.experimental == false }} - env: - MISE_YES: 1 - MISE_PYTHON: ${{ matrix.python-version }} - UV_PYTHON: ${{ matrix.python-version }} - MISE_EXPERIMENTAL: 1 - MISE_PATH: ${{ steps.setup-mise.outputs.MISE_PATH }} run: | - export PATH="$HOME/.local/bin:$HOME/.local/share/mise/bin:$HOME/.local/share/mise/shims:$PATH" - "$MISE_PATH" //:test-cov -m "${{ inputs.test-markers }}" + mise //:test-cov -m "${{ inputs.test-markers }}" - name: Upload coverage to Codecov if: ${{ inputs.upload-coverage && matrix.python-version == '3.12' }} uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 @@ -132,14 +126,18 @@ jobs: with: python-version: ${{ matrix.python-version }} skip-checkout: true + id: setup-uv - name: Setup Env for Experimental Python if: ${{ matrix.experimental == true }} shell: bash + env: + UV_PYTHON: ${{ matrix.python-version }} + UV_PATH: ${{ steps.setup-uv.outputs.UV_PATH }} run: | - uv sync --group test --resolution highest 2>&1 + "$UV_PATH" sync --group test --resolution highest 2>&1 + echo "UV_PATH=$UV_PATH" >> "$GITHUB_ENV" - name: Run test coverage for experimental Python if: ${{ matrix.experimental == true }} run: | - export PATH="$HOME/.local/bin:$HOME/.local/share/mise/bin:$HOME/.local/share/mise/shims:$PATH" - uv run -m pytest tests/ --cov=codeweaver --cov-report=xml --cov-report=term-missing --junit-xml=test-results.xml -v -m "${{ inputs.test-markers }}" + "$UV_PATH" run -m pytest tests/ --cov=codeweaver --cov-report=xml --cov-report=term-missing --junit-xml=test-results.xml -v -m "${{ inputs.test-markers }}" diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 036145d57..fb3035e5d 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -19,6 +19,7 @@ on: pull_request_review: types: - submitted + permissions: actions: read checks: read @@ -26,27 +27,26 @@ permissions: contents: write discussions: write pull-requests: write + +env: + CODEWEAVER_VECTOR_STORE_URL: ${{ secrets.CODEWEAVER_VECTOR_STORE_URL }} + QDRANT__SERVICE__API_KEY: ${{ secrets.QDRANT__SERVICE__API_KEY }} + VOYAGE_API_KEY: ${{ secrets.VOYAGE_API_KEY }} + CODEWEAVER_PROJECT_PATH: ${{ github.workspace }} + MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MISE_ENV: "dev" + MISE_YES: 1 + TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} + # Limit CodeQL memory usage to prevent OOM kills on standard runners (7GB total RAM) + # Note: Standard runners have ~7GB, so limit to 3GB to leave headroom for system + CODEQL_RAM: 3072 + CODEQL_THREADS: 2 + # Additional limits for CodeQL extractor and evaluator + CODEQL_EXTRACTOR_PYTHON_RAM: 2048 + CODEQL_EVALUATOR_RAM: 2048 jobs: claude-response: - # Alternative: Use ubuntu-latest-4-cores (16GB RAM) if OOM issues persist - # Requires GitHub Team/Enterprise plan runs-on: ubuntu-latest - env: - CODEWEAVER_VECTOR_STORE_URL: ${{ secrets.CODEWEAVER_VECTOR_STORE_URL }} - QDRANT__SERVICE__API_KEY: ${{ secrets.QDRANT__SERVICE__API_KEY }} - VOYAGE_API_KEY: ${{ secrets.VOYAGE_API_KEY }} - CODEWEAVER_PROJECT_PATH: ${{ github.workspace }} - MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - MISE_ENV: "dev" - MISE_YES: 1 - TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} - # Limit CodeQL memory usage to prevent OOM kills on standard runners (7GB total RAM) - # Note: Standard runners have ~7GB, so limit to 3GB to leave headroom for system - CODEQL_RAM: 3072 - CODEQL_THREADS: 2 - # Additional limits for CodeQL extractor and evaluator - CODEQL_EXTRACTOR_PYTHON_RAM: 2048 - CODEQL_EVALUATOR_RAM: 2048 steps: - name: Checkout Repository uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 @@ -156,6 +156,7 @@ jobs: - Provide feedback on the code quality, functionality, and adherence to best practices. - Consider the library's existing code style and whether the code aligns with it. - Consider possible security or performance effects. + - If the code does not follow APIs as you would expect, remember that you have access to the context7 tool to look up library documentation. APIs may have changed since your training data. - Suggest improvements or alternatives where applicable. - If the changes are satisfactory and the code passes checks, approve the PR with a comment. - name: Setup Environment for Assigned or Labeled Issues/PRs @@ -186,11 +187,13 @@ jobs: --mcp-config .mcp.json prompt: | REPO: ${{ github.repository }} - ISSUE NUMBER: ${{ github.event.issue.number }} + ISSUE/ PR NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }} When you are assigned an issue or it's labeled 'claude': - Your job is to resolve it. - Gather all necessary information about the issue from discussions and comments and the codebase. - If the issue involves external libraries, use the context7 tool to get the latest information on the API. + - Pay attention to external API versions--they may have changed since your training data, or even since the sources you research. When in doubt, use the tavily tool or web search to find the current documentation. + - Research similar issues in this repository and others to inform your approach. - Communicate with the issue reporter for clarification if needed. - Create an issue branch. - Develop a detailed plan to fix the problem. diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 61324ec86..8fe587f3b 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -14,14 +14,20 @@ on: pull_request: paths: - .github/workflows/copilot-setup-steps.yml + jobs: # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. copilot-setup-steps: + outputs: + MISE_PATH: ${{ steps.setup-mise.outputs.MISE_PATH }} + env: CODEWEAVER_VECTOR_STORE_URL: ${{ secrets.CODEWEAVER_VECTOR_STORE_URL }} QDRANT__SERVICE__API_KEY: ${{ secrets.QDRANT__SERVICE__API_KEY }} VOYAGE_API_KEY: ${{ secrets.VOYAGE_API_KEY }} CODEWEAVER_PROJECT_PATH: ${{ github.workspace }} + MISE_ENV: dev + MISE_EXPERIMENTAL: 1 MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} MISE_YES: 1 # Limit CodeQL memory usage to prevent OOM kills on standard runners (7GB total RAM) @@ -71,3 +77,5 @@ jobs: run: | export PATH="$HOME/.local/bin:$HOME/.local/share/mise/bin:$HOME/.local/share/mise/shims:$PATH" mise --version || { echo "Mise not in path"; "$MISE_PATH" --version; } + echo "MISE_PATH=$MISE_PATH" >> "$GITHUB_OUTPUT" + echo "MISE_ENV=$MISE_ENV" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/mcp-registry-submit.yml b/.github/workflows/mcp-registry-submit.yml index 9488cc0dd..4d78447c5 100644 --- a/.github/workflows/mcp-registry-submit.yml +++ b/.github/workflows/mcp-registry-submit.yml @@ -8,7 +8,9 @@ name: Submit to MCP Registry on: release: - types: [published] + types: + - published + - prereleased workflow_dispatch: inputs: version: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 289a74129..c5bc8bedc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,6 +6,9 @@ name: Release on: push: + branches: + - main + - staging tags: - v* workflow_dispatch: @@ -29,6 +32,7 @@ jobs: tests: name: Run Tests # EFFICIENCY: Skip tests on tag push (rely on PR tests), but allow manual trigger + # TODO: Add smoke tests for releases if: github.event_name == 'workflow_dispatch' || github.event_name == 'release' uses: ./.github/workflows/_reusable-test.yml with: From 0fa134c6d2b461811b3dfaf9cf566ce04ffebd6f Mon Sep 17 00:00:00 2001 From: Adam Poulemanos Date: Sat, 6 Dec 2025 23:41:18 -0500 Subject: [PATCH 17/37] fix: update Python setup action to include outputs for PYTHON_PATH and UV_PATH --- .github/actions/setup-python-env/action.yml | 12 ++++++++++++ .github/workflows/_reusable-test.yml | 5 ++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/actions/setup-python-env/action.yml b/.github/actions/setup-python-env/action.yml index c90a79aad..61235a3b6 100644 --- a/.github/actions/setup-python-env/action.yml +++ b/.github/actions/setup-python-env/action.yml @@ -24,6 +24,14 @@ inputs: required: false default: '' +outputs: + PYTHON_PATH: + description: Path to the Python executable + value: ${{ steps.python-setup.outputs.python-path }} + UV_PATH: + description: Path to uv executable + value: ${{ steps.python-setup.outputs.UV_PATH }} + runs: using: composite steps: @@ -48,4 +56,8 @@ runs: run: | python -m pip install --upgrade pip setuptools wheel python -m pip install uv + echo "PYTHON_PATH=$(which python)" >> "$GITHUB_ENV" + echo "UV_PATH=$(which uv)" >> "$GITHUB_ENV" + id: python-setup + diff --git a/.github/workflows/_reusable-test.yml b/.github/workflows/_reusable-test.yml index f1422893c..a343bcb0b 100644 --- a/.github/workflows/_reusable-test.yml +++ b/.github/workflows/_reusable-test.yml @@ -126,13 +126,12 @@ jobs: with: python-version: ${{ matrix.python-version }} skip-checkout: true - id: setup-uv + id: setup-python - name: Setup Env for Experimental Python if: ${{ matrix.experimental == true }} shell: bash env: - UV_PYTHON: ${{ matrix.python-version }} - UV_PATH: ${{ steps.setup-uv.outputs.UV_PATH }} + UV_PATH: ${{ steps.setup-python.outputs.UV_PATH }} run: | "$UV_PATH" sync --group test --resolution highest 2>&1 echo "UV_PATH=$UV_PATH" >> "$GITHUB_ENV" From df2099878f4466b252ecf10bf389eb7d02a3391c Mon Sep 17 00:00:00 2001 From: Adam Poulemanos Date: Sat, 6 Dec 2025 23:53:48 -0500 Subject: [PATCH 18/37] fix: update CI workflows to ensure uv is available when called --- .github/workflows/_reusable-lint.yml | 1 + .github/workflows/_reusable-test.yml | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/_reusable-lint.yml b/.github/workflows/_reusable-lint.yml index 066b90456..f141757ce 100644 --- a/.github/workflows/_reusable-lint.yml +++ b/.github/workflows/_reusable-lint.yml @@ -60,6 +60,7 @@ jobs: echo "PYTHON_PATH=$PYTHON_PATH" >> "$GITHUB_ENV" eval "$(mise activate bash)" echo "PATH=$PATH" >> "$GITHUB_ENV" + mise use -g uv@latest mise //:lint || echo "::warning::Lint checks failed but allowing workflow to continue" continue-on-error: true diff --git a/.github/workflows/_reusable-test.yml b/.github/workflows/_reusable-test.yml index a343bcb0b..72e0ae4b8 100644 --- a/.github/workflows/_reusable-test.yml +++ b/.github/workflows/_reusable-test.yml @@ -94,6 +94,7 @@ jobs: export PATH="$HOME/.local/bin:$HOME/.local/share/mise/bin:$HOME/.local/share/mise/shims:$PATH" echo "MISE_PATH=$MISE_PATH" >> "$GITHUB_ENV" eval "$(mise activate bash)" + mise use -g uv@latest echo "PATH=$PATH" >> "$GITHUB_ENV" mise //:check 2>&1 continue-on-error: true @@ -133,6 +134,14 @@ jobs: env: UV_PATH: ${{ steps.setup-python.outputs.UV_PATH }} run: | + if [ -z "$UV_PATH" ]; then + echo "UV_PATH is not set." + if ! command -v uv &> /dev/null; then + echo "uv could not be found" + exit 1 + fi + UV_PATH="$(which uv)" + fi "$UV_PATH" sync --group test --resolution highest 2>&1 echo "UV_PATH=$UV_PATH" >> "$GITHUB_ENV" From 855c5ba2f6fee2f317fe6bd66000d6e2f7b69fa8 Mon Sep 17 00:00:00 2001 From: Adam Poulemanos Date: Sun, 7 Dec 2025 00:01:20 -0500 Subject: [PATCH 19/37] fix: add tools to mise check task for CI availability --- mise.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mise.toml b/mise.toml index 988a6f217..f0b00cdc7 100644 --- a/mise.toml +++ b/mise.toml @@ -155,6 +155,9 @@ sources = ["{src,scripts,tests,mise-tasks}/*.py"] [tasks.check] tools.hk = "latest" tools.pkl = "latest" +tools."pipx:ty" = "latest" +tools."pipx:reuse" = "latest" +tools.ruff = "latest" run = ''' echo "${CW_PREFIX} Running quality checks..." hk check From b7dbc413fc073aa035437b318ae4b99422f80427 Mon Sep 17 00:00:00 2001 From: Adam Poulemanos Date: Sun, 7 Dec 2025 00:10:13 -0500 Subject: [PATCH 20/37] fix: add setup step for mise dev installs in CI --- .github/actions/setup-mise-env/action.yml | 14 ++++++++++++++ .github/workflows/claude.yml | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/actions/setup-mise-env/action.yml b/.github/actions/setup-mise-env/action.yml index 319070f4b..fbc211ac5 100644 --- a/.github/actions/setup-mise-env/action.yml +++ b/.github/actions/setup-mise-env/action.yml @@ -45,6 +45,7 @@ outputs: description: Path to Python executable value: ${{ steps.mise-outputs.outputs.PYTHON_PATH }} + runs: using: composite steps: @@ -94,6 +95,8 @@ runs: - name: Activate Reviewer Profile Environment if: inputs.profile == 'reviewer' shell: bash + env: + MISE_GITHUB_TOKEN: ${{ inputs.github-token }} id: mise-setup-reviewer run: | uv sync @@ -113,6 +116,17 @@ runs: cache_key_prefix: ${{ inputs.profile }} env: false id: mise-setup-dev + - name: Activate Dev Profile Environment + if: inputs.profile == 'dev' + shell: bash + id: mise-setup-dev-activate + env: + MISE_ENV: dev + MISE_EXPERIMENTAL: 1 + MISE_YES: 1 + MISE_GITHUB_TOKEN: ${{ inputs.github-token }} + run: | + mise ://setup - name: Set outputs shell: bash diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index fb3035e5d..05db17a6d 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -187,7 +187,7 @@ jobs: --mcp-config .mcp.json prompt: | REPO: ${{ github.repository }} - ISSUE/ PR NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }} + ISSUE/PR NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }} When you are assigned an issue or it's labeled 'claude': - Your job is to resolve it. - Gather all necessary information about the issue from discussions and comments and the codebase. From 0bf40811357a8946ea84b5f96278a623ebbea75a Mon Sep 17 00:00:00 2001 From: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> Date: Sun, 7 Dec 2025 00:22:46 -0500 Subject: [PATCH 21/37] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> --- .github/actions/setup-python-env/action.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/setup-python-env/action.yml b/.github/actions/setup-python-env/action.yml index 61235a3b6..ce12ad1ca 100644 --- a/.github/actions/setup-python-env/action.yml +++ b/.github/actions/setup-python-env/action.yml @@ -27,7 +27,7 @@ inputs: outputs: PYTHON_PATH: description: Path to the Python executable - value: ${{ steps.python-setup.outputs.python-path }} + value: ${{ steps.python-setup.outputs.PYTHON_PATH }} UV_PATH: description: Path to uv executable value: ${{ steps.python-setup.outputs.UV_PATH }} @@ -56,8 +56,8 @@ runs: run: | python -m pip install --upgrade pip setuptools wheel python -m pip install uv - echo "PYTHON_PATH=$(which python)" >> "$GITHUB_ENV" - echo "UV_PATH=$(which uv)" >> "$GITHUB_ENV" + echo "PYTHON_PATH=$(which python)" >> "$GITHUB_OUTPUT" + echo "UV_PATH=$(which uv)" >> "$GITHUB_OUTPUT" id: python-setup From 47a86142565a4b1851a87b6e064235030666c4a0 Mon Sep 17 00:00:00 2001 From: Adam Poulemanos Date: Sun, 7 Dec 2025 00:41:24 -0500 Subject: [PATCH 22/37] fix: improve CI workflows by setting PATH for mise and cleaning up temporary files --- .github/workflows/_reusable-lint.yml | 9 +++++---- .github/workflows/_reusable-test.yml | 21 +++++++++++++-------- .github/workflows/copilot-setup-steps.yml | 2 -- .github/workflows/mcp-registry-submit.yml | 6 +++++- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/.github/workflows/_reusable-lint.yml b/.github/workflows/_reusable-lint.yml index f141757ce..434c3c382 100644 --- a/.github/workflows/_reusable-lint.yml +++ b/.github/workflows/_reusable-lint.yml @@ -50,16 +50,17 @@ jobs: profile: "${{ env.PROFILE }}" skip-checkout: true id: mise-outputs + - name: Set Path + run: | + export PATH="$HOME/.local/bin:$HOME/.local/share/mise/bin:$HOME/.local/share/mise/shims:$PATH" + eval "$(mise activate bash)" + echo "PATH=$PATH" >> "$GITHUB_ENV" - name: Check code style and linting env: PYTHON_PATH: ${{ steps.mise-outputs.outputs.PYTHON_PATH }} shell: bash run: | - export PATH="$HOME/.local/bin:$HOME/.local/share/mise/bin:$PATH" - echo "PYTHON_PATH=$PYTHON_PATH" >> "$GITHUB_ENV" - eval "$(mise activate bash)" - echo "PATH=$PATH" >> "$GITHUB_ENV" mise use -g uv@latest mise //:lint || echo "::warning::Lint checks failed but allowing workflow to continue" continue-on-error: true diff --git a/.github/workflows/_reusable-test.yml b/.github/workflows/_reusable-test.yml index 72e0ae4b8..c02c72d0e 100644 --- a/.github/workflows/_reusable-test.yml +++ b/.github/workflows/_reusable-test.yml @@ -86,16 +86,18 @@ jobs: profile: "dev" skip-checkout: true id: setup-mise + - name: Set Path + if: ${{ matrix.experimental == false }} + run: | + export PATH="$HOME/.local/bin:$HOME/.local/share/mise/bin:$HOME/.local/share/mise/shims:$PATH" + eval "$(mise activate bash)" + echo "PATH=$PATH" >> "$GITHUB_ENV" - name: Run quality checks before tests if: ${{ inputs.run-quality-checks && matrix.experimental == false }} env: MISE_PATH: ${{ steps.setup-mise.outputs.MISE_PATH }} run: | - export PATH="$HOME/.local/bin:$HOME/.local/share/mise/bin:$HOME/.local/share/mise/shims:$PATH" - echo "MISE_PATH=$MISE_PATH" >> "$GITHUB_ENV" - eval "$(mise activate bash)" mise use -g uv@latest - echo "PATH=$PATH" >> "$GITHUB_ENV" mise //:check 2>&1 continue-on-error: true - name: Run tests with coverage @@ -128,12 +130,11 @@ jobs: python-version: ${{ matrix.python-version }} skip-checkout: true id: setup-python - - name: Setup Env for Experimental Python + - name: Set UV Path if: ${{ matrix.experimental == true }} shell: bash - env: - UV_PATH: ${{ steps.setup-python.outputs.UV_PATH }} run: | + UV_PATH="${{ steps.setup-python.outputs.UV_PATH }}" if [ -z "$UV_PATH" ]; then echo "UV_PATH is not set." if ! command -v uv &> /dev/null; then @@ -142,8 +143,12 @@ jobs: fi UV_PATH="$(which uv)" fi - "$UV_PATH" sync --group test --resolution highest 2>&1 echo "UV_PATH=$UV_PATH" >> "$GITHUB_ENV" + - name: Setup Env for Experimental Python + if: ${{ matrix.experimental == true }} + shell: bash + run: | + "$UV_PATH" sync --group test --resolution highest 2>&1 - name: Run test coverage for experimental Python if: ${{ matrix.experimental == true }} diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 8fe587f3b..a59ccd116 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -77,5 +77,3 @@ jobs: run: | export PATH="$HOME/.local/bin:$HOME/.local/share/mise/bin:$HOME/.local/share/mise/shims:$PATH" mise --version || { echo "Mise not in path"; "$MISE_PATH" --version; } - echo "MISE_PATH=$MISE_PATH" >> "$GITHUB_OUTPUT" - echo "MISE_ENV=$MISE_ENV" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/mcp-registry-submit.yml b/.github/workflows/mcp-registry-submit.yml index 4d78447c5..2db0788b9 100644 --- a/.github/workflows/mcp-registry-submit.yml +++ b/.github/workflows/mcp-registry-submit.yml @@ -127,6 +127,11 @@ jobs: # we need to sign the timestamp with the private key, which is in MCP_REGISTRY_KEY # MCP_REGISTRY_KEY is expected to be in PEM format, and it uses ed25519 algorithm # unfortunately, pkeyutl can only work with files because of how it has to buffer the input for signing + # shellcheck disable=SC2329 + cleanup() { + shred -u "$key_file" "$outfile" || rm -f "$key_file" "$outfile" + } + trap cleanup EXIT domain="knitli.com" indata="$(mktemp)" @@ -152,7 +157,6 @@ jobs: --header "Content-Type: application/json" \ --data @"$request_body")" token="$(echo "$response" | jq -r '.registry_token')" - rm "$indata" "$key_file" "$outfile" "$request_body" if [ -z "$token" ] || [ "$token" = "null" ]; then echo "ERROR: Failed to obtain registry token" echo "Response: $response" From 766575ce703213e6911396beeff0617376c36723 Mon Sep 17 00:00:00 2001 From: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> Date: Sun, 7 Dec 2025 00:45:14 -0500 Subject: [PATCH 23/37] Update .github/workflows/mcp-registry-submit.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> --- .github/workflows/mcp-registry-submit.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/mcp-registry-submit.yml b/.github/workflows/mcp-registry-submit.yml index 2db0788b9..9c6b95fa6 100644 --- a/.github/workflows/mcp-registry-submit.yml +++ b/.github/workflows/mcp-registry-submit.yml @@ -198,7 +198,10 @@ jobs: { echo "✅ Successfully submitted to MCP Registry" echo "" - echo "View your server at: https://registry.modelcontextprotocol.io/v0.1/servers/com.knitli%2Fcodeweaver/versions/${{ steps.version.outputs.version }}" + # Extract and encode server name from server.json + SERVER_NAME=$(jq -r '.name' server.json) + ENCODED_SERVER_NAME=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "$SERVER_NAME") + echo "View your server at: https://registry.modelcontextprotocol.io/v0.1/servers/${ENCODED_SERVER_NAME}/versions/${{ steps.version.outputs.version }}" } >> "$GITHUB_STEP_SUMMARY" else { From 407161d889a8e5b6176f8db46a3db8dc37539dda Mon Sep 17 00:00:00 2001 From: Adam Poulemanos Date: Sun, 7 Dec 2025 09:46:10 -0500 Subject: [PATCH 24/37] fix: enhance CI workflows by adding NEWPATH output and updating PATH setup --- .github/actions/setup-mise-env/action.yml | 27 ++++++++++++---- .github/workflows/_reusable-test.yml | 22 ++++++++----- .github/workflows/copilot-setup-steps.yml | 3 +- mise.toml | 39 +++++++++++++++++++++++ 4 files changed, 75 insertions(+), 16 deletions(-) diff --git a/.github/actions/setup-mise-env/action.yml b/.github/actions/setup-mise-env/action.yml index fbc211ac5..10e49d693 100644 --- a/.github/actions/setup-mise-env/action.yml +++ b/.github/actions/setup-mise-env/action.yml @@ -44,6 +44,9 @@ outputs: PYTHON_PATH: description: Path to Python executable value: ${{ steps.mise-outputs.outputs.PYTHON_PATH }} + NEWPATH: + description: Updated PATH including mise and python paths + value: ${{ steps.mise-outputs.outputs.NEWPATH }} runs: @@ -57,7 +60,7 @@ runs: - name: Install system dependencies shell: bash run: | - sudo apt-get update && sudo apt-get install -y zsh curl git + sudo apt-get update && sudo apt-get install -y curl git - name: Free up disk space shell: bash run: ./scripts/dev-env/ci-free-disk-space.sh @@ -133,9 +136,19 @@ runs: id: mise-outputs run: | # Add mise to PATH since env export is disabled - export PATH="$HOME/.local/share/mise/bin:$HOME/.local/share/mise/shims:$PATH" - echo "MISE_PATH=$(which mise)" >> $GITHUB_OUTPUT - echo "MISE_ENV=${MISE_ENV:-}" >> $GITHUB_OUTPUT - echo "PROFILE=${{ inputs.profile }}" >> $GITHUB_OUTPUT - echo "PYTHON_VERSION=$(python --version | awk '{print $2}')" >> $GITHUB_OUTPUT - echo "PYTHON_PATH=$(which python)" >> $GITHUB_OUTPUT + if [ "${{ inputs.profile }}" == "dev" ]; then + echo "MISE_ENV=dev" >> "$GITHUB_OUTPUT" + mise //:cloud-setup 2>&1 || true + else + echo "MISE_ENV=" >> "$GITHUB_OUTPUT" + fi + # setup PATH + echo "MISE_PATH=$(which mise)" >> "$GITHUB_OUTPUT" + mise_bin_dir="$(dirname "$MISE_PATH")" + python_path="$(which python)" + python_bin_dir="$(dirname "$python_path")" + NEWPATH="$mise_bin_dir:$python_bin_dir:$PATH" + echo "NEWPATH=${NEWPATH}" >> "$GITHUB_OUTPUT" + echo "PROFILE=${{ inputs.profile }}" >> "$GITHUB_OUTPUT" + echo "PYTHON_VERSION=$(python --version | awk '{print $2}')" >> "$GITHUB_OUTPUT" + echo "PYTHON_PATH=$python_path" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/_reusable-test.yml b/.github/workflows/_reusable-test.yml index c02c72d0e..4b48a2805 100644 --- a/.github/workflows/_reusable-test.yml +++ b/.github/workflows/_reusable-test.yml @@ -88,14 +88,13 @@ jobs: id: setup-mise - name: Set Path if: ${{ matrix.experimental == false }} + env: + NEWPATH: ${{ steps.setup-mise.outputs.NEWPATH }} + shell: bash run: | - export PATH="$HOME/.local/bin:$HOME/.local/share/mise/bin:$HOME/.local/share/mise/shims:$PATH" - eval "$(mise activate bash)" - echo "PATH=$PATH" >> "$GITHUB_ENV" + echo "PATH=$NEWPATH" >> "$GITHUB_ENV" - name: Run quality checks before tests if: ${{ inputs.run-quality-checks && matrix.experimental == false }} - env: - MISE_PATH: ${{ steps.setup-mise.outputs.MISE_PATH }} run: | mise use -g uv@latest mise //:check 2>&1 @@ -103,6 +102,7 @@ jobs: - name: Run tests with coverage if: ${{ matrix.experimental == false }} run: | + mise use -g uv@latest mise //:test-cov -m "${{ inputs.test-markers }}" - name: Upload coverage to Codecov if: ${{ inputs.upload-coverage && matrix.python-version == '3.12' }} @@ -137,20 +137,26 @@ jobs: UV_PATH="${{ steps.setup-python.outputs.UV_PATH }}" if [ -z "$UV_PATH" ]; then echo "UV_PATH is not set." + if [ -d ".venv" ]; then + export PATH=".venv/bin:$PATH" + else + export PATH="$HOME/.local/bin:$HOME/.local/share/uv/bin:$HOME/.uv/bin:$PATH" + fi if ! command -v uv &> /dev/null; then - echo "uv could not be found" + echo "uv could not be found even after adjusting PATH." exit 1 fi UV_PATH="$(which uv)" fi + echo "PATH=$PATH" >> "$GITHUB_ENV" echo "UV_PATH=$UV_PATH" >> "$GITHUB_ENV" - name: Setup Env for Experimental Python if: ${{ matrix.experimental == true }} shell: bash run: | - "$UV_PATH" sync --group test --resolution highest 2>&1 + uv sync --group test --resolution highest 2>&1 - name: Run test coverage for experimental Python if: ${{ matrix.experimental == true }} run: | - "$UV_PATH" run -m pytest tests/ --cov=codeweaver --cov-report=xml --cov-report=term-missing --junit-xml=test-results.xml -v -m "${{ inputs.test-markers }}" + uv run -m pytest tests/ --cov=codeweaver --cov-report=xml --cov-report=term-missing --junit-xml=test-results.xml -v -m "${{ inputs.test-markers }}" diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index a59ccd116..7848f25dc 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -74,6 +74,7 @@ jobs: MISE_YES: 1 MISE_EXPERIMENTAL: 1 MISE_PATH: ${{ steps.setup-mise.outputs.MISE_PATH }} + NEWPATH: ${{ steps.setup-mise.outputs.NEWPATH }} run: | - export PATH="$HOME/.local/bin:$HOME/.local/share/mise/bin:$HOME/.local/share/mise/shims:$PATH" + export PATH="$NEWPATH" mise --version || { echo "Mise not in path"; "$MISE_PATH" --version; } diff --git a/mise.toml b/mise.toml index f0b00cdc7..dc0a0a0f7 100644 --- a/mise.toml +++ b/mise.toml @@ -135,6 +135,45 @@ ACTIVE_SHELL = '''{{ exec(command='basename "$SHELL" 2>/dev/null || echo zsh') } [tasks.setup.tools] uv = "latest" +[tasks.cloud-setup] +run = [ + "echo \"[codeweaver] Setting up cloud development environment...\"", + "ln -sf AGENTS.md CLAUDE.md", + '''if ! command -v mise &>/dev/null; then + if [ -d "$HOME/.local/share/mise" || -d "$HOME/.mise" || -f "$HOME/.local/bin/mise" ]; then + export PATH="$HOME/.local/bin:$HOME/.mise/bin:$HOME/.local/share/mise/bin:$PATH" + if ! grep -qx "mise activate" "$HOME/.bashrc"; then + echo "eval \"$(mise activate \"${ACTIVE_SHELL:-bash}\")\" 2>&1 || true" >> "$HOME/.bashrc" + fi + else + chmod +x scripts/dev-env/install-mise.sh 2>&1 || true + ./scripts/dev-env/install-mise.sh 2>&1 || true + fi + fi''', + "mise -y trust --all 2>&1 || true", + "mise install 2>&1 || true", + "hk install --mise 2>&1 || mise x hk@latest -- hk install --mise 2>&1 || true", + "mise x uv@latest -- uv venv --allow-existing --seed --keyring-provider subprocess --python {{ get_env(name='MISE_PYTHON', default='3.13') }} .venv 2>&1", + "source .venv/bin/activate 2>&1 || true", + "mise x uv@latest -- uv sync --group dev --group test 2>&1", + "git config include.path .gitconfig 2>&1 || true", + ''' + ignore_files=('src/codeweaver/_version.py' 'coverage.xml' 'test-results.xml') + for file in "${ignore_files[@]}"; do + if ! grep -qx "$file" .git/info/exclude; then + echo "$file" >> .git/info/exclude 2>&1 + fi + done + ''', + "echo \"[codeweaver] Cloud development environment setup complete!\"", +] +[tasks.cloud-setup.env] +ACTIVE_SHELL = '''{{ exec(command='basename "$SHELL" 2>/dev/null || echo bash') }}''' +MISE_EXPERIMENTAL = "1" +MISE_ENV = "dev" +MISE_YES = "1" + + [tasks.fix-python] tools.uv = "latest" tools.ast-grep = "latest" From cb8618ce64004178c6d91595badd2f90b9fe872f Mon Sep 17 00:00:00 2001 From: Adam Poulemanos Date: Sun, 7 Dec 2025 09:46:55 -0500 Subject: [PATCH 25/37] fix: update cloud setup task environment variables for consistency --- mise.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mise.toml b/mise.toml index dc0a0a0f7..545ef24a5 100644 --- a/mise.toml +++ b/mise.toml @@ -167,13 +167,13 @@ run = [ ''', "echo \"[codeweaver] Cloud development environment setup complete!\"", ] + [tasks.cloud-setup.env] ACTIVE_SHELL = '''{{ exec(command='basename "$SHELL" 2>/dev/null || echo bash') }}''' MISE_EXPERIMENTAL = "1" MISE_ENV = "dev" MISE_YES = "1" - [tasks.fix-python] tools.uv = "latest" tools.ast-grep = "latest" From 0d768714acd0c02edf7d40cfe2095af0676bb363 Mon Sep 17 00:00:00 2001 From: Adam Poulemanos Date: Sun, 7 Dec 2025 10:00:34 -0500 Subject: [PATCH 26/37] fix: enhance setup and testing workflows by adding missing dependencies and adjusting environment variables --- .github/actions/setup-mise-env/action.yml | 2 +- .github/workflows/_reusable-test.yml | 1 + mise.dev.toml | 5 ++++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/actions/setup-mise-env/action.yml b/.github/actions/setup-mise-env/action.yml index 10e49d693..5e45bb673 100644 --- a/.github/actions/setup-mise-env/action.yml +++ b/.github/actions/setup-mise-env/action.yml @@ -60,7 +60,7 @@ runs: - name: Install system dependencies shell: bash run: | - sudo apt-get update && sudo apt-get install -y curl git + sudo apt-get update && sudo apt-get install -y curl git readline-common lzma bzip2 - name: Free up disk space shell: bash run: ./scripts/dev-env/ci-free-disk-space.sh diff --git a/.github/workflows/_reusable-test.yml b/.github/workflows/_reusable-test.yml index 4b48a2805..5aa39523e 100644 --- a/.github/workflows/_reusable-test.yml +++ b/.github/workflows/_reusable-test.yml @@ -92,6 +92,7 @@ jobs: NEWPATH: ${{ steps.setup-mise.outputs.NEWPATH }} shell: bash run: | + echo "PROFILE=dev" >> "$GITHUB_ENV" echo "PATH=$NEWPATH" >> "$GITHUB_ENV" - name: Run quality checks before tests if: ${{ inputs.run-quality-checks && matrix.experimental == false }} diff --git a/mise.dev.toml b/mise.dev.toml index c7a2b942d..858bdd9e3 100644 --- a/mise.dev.toml +++ b/mise.dev.toml @@ -22,7 +22,10 @@ ruff = "latest" # Python linting/formatting [hooks.enter] env.MISE_EXPERIMENTAL = "1" script = ''' -mise //:enter +# PROFILE is set to 'dev' in a cloud CI environment for cloud agents. 'enter' tends to break there. +if [ ! "$PROFILE" == "dev" ]; then + mise //:enter +fi ''' [tasks] From c40e7c18d9dbeb382ea7d9fad53e960414520a66 Mon Sep 17 00:00:00 2001 From: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> Date: Sun, 7 Dec 2025 11:20:40 -0500 Subject: [PATCH 27/37] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> --- .github/actions/setup-mise-env/action.yml | 5 +++-- mise.dev.toml | 2 +- mise.toml | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/actions/setup-mise-env/action.yml b/.github/actions/setup-mise-env/action.yml index 5e45bb673..611faafb9 100644 --- a/.github/actions/setup-mise-env/action.yml +++ b/.github/actions/setup-mise-env/action.yml @@ -136,14 +136,15 @@ runs: id: mise-outputs run: | # Add mise to PATH since env export is disabled - if [ "${{ inputs.profile }}" == "dev" ]; then + if [ "${{ inputs.profile }}" = "dev" ]; then echo "MISE_ENV=dev" >> "$GITHUB_OUTPUT" mise //:cloud-setup 2>&1 || true else echo "MISE_ENV=" >> "$GITHUB_OUTPUT" fi # setup PATH - echo "MISE_PATH=$(which mise)" >> "$GITHUB_OUTPUT" + MISE_PATH="$(which mise)" + echo "MISE_PATH=$MISE_PATH" >> "$GITHUB_OUTPUT" mise_bin_dir="$(dirname "$MISE_PATH")" python_path="$(which python)" python_bin_dir="$(dirname "$python_path")" diff --git a/mise.dev.toml b/mise.dev.toml index 858bdd9e3..af14d9de6 100644 --- a/mise.dev.toml +++ b/mise.dev.toml @@ -23,7 +23,7 @@ ruff = "latest" # Python linting/formatting env.MISE_EXPERIMENTAL = "1" script = ''' # PROFILE is set to 'dev' in a cloud CI environment for cloud agents. 'enter' tends to break there. -if [ ! "$PROFILE" == "dev" ]; then +if [ ! "$PROFILE" = "dev" ]; then mise //:enter fi ''' diff --git a/mise.toml b/mise.toml index 545ef24a5..993b21704 100644 --- a/mise.toml +++ b/mise.toml @@ -140,9 +140,9 @@ run = [ "echo \"[codeweaver] Setting up cloud development environment...\"", "ln -sf AGENTS.md CLAUDE.md", '''if ! command -v mise &>/dev/null; then - if [ -d "$HOME/.local/share/mise" || -d "$HOME/.mise" || -f "$HOME/.local/bin/mise" ]; then + if [ -d "$HOME/.local/share/mise" ] || [ -d "$HOME/.mise" ] || [ -f "$HOME/.local/bin/mise" ]; then export PATH="$HOME/.local/bin:$HOME/.mise/bin:$HOME/.local/share/mise/bin:$PATH" - if ! grep -qx "mise activate" "$HOME/.bashrc"; then + if ! grep -q "mise activate" "$HOME/.bashrc"; then echo "eval \"$(mise activate \"${ACTIVE_SHELL:-bash}\")\" 2>&1 || true" >> "$HOME/.bashrc" fi else From a5ce307c5b03acc19dc00ca3cbb9578d5ccbb83c Mon Sep 17 00:00:00 2001 From: Adam Poulemanos Date: Sun, 7 Dec 2025 11:25:42 -0500 Subject: [PATCH 28/37] fix: improve cleanup process in MCP Registry submission workflow --- .github/workflows/mcp-registry-submit.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/mcp-registry-submit.yml b/.github/workflows/mcp-registry-submit.yml index 9c6b95fa6..29db9d5b4 100644 --- a/.github/workflows/mcp-registry-submit.yml +++ b/.github/workflows/mcp-registry-submit.yml @@ -128,8 +128,15 @@ jobs: # MCP_REGISTRY_KEY is expected to be in PEM format, and it uses ed25519 algorithm # unfortunately, pkeyutl can only work with files because of how it has to buffer the input for signing # shellcheck disable=SC2329 + indata="" + key_file="" + outfile="" + request_body="" + # shellcheck disable=SC2329 cleanup() { shred -u "$key_file" "$outfile" || rm -f "$key_file" "$outfile" + # these aren't sensitive, but let's keep things tidy + rm -f "$indata" "$request_body" } trap cleanup EXIT From 9032d4308e848d61174721378532da25233c3e3d Mon Sep 17 00:00:00 2001 From: Adam Poulemanos Date: Sun, 7 Dec 2025 14:38:10 -0500 Subject: [PATCH 29/37] fix: hotfix for incorrect property access. DicoveredFile.file_hash was accessed in multiple places across the codebase as a method, when its 'computed_field' decorator made it a property. Fixed the incorrect calls and added property decorator for clarity. Also standardized optional dependency naming and switched to pydantic-ai-slim to correct a CI/CD problem for an unused dependency. --- .github/workflows/mcp-registry-submit.yml | 1 - mise.dev.toml | 2 +- pyproject.toml | 67 ++-- src/codeweaver/config/profiles.py | 4 +- src/codeweaver/config/settings.py | 4 +- src/codeweaver/config/telemetry.py | 2 +- src/codeweaver/core/discovery.py | 3 +- src/codeweaver/core/types/aliases.py | 3 +- src/codeweaver/engine/failover_tracker.py | 2 +- src/codeweaver/engine/indexer/indexer.py | 8 +- tests/unit/test_indexer_remove_path.py | 2 +- uv.lock | 450 ++++------------------ 12 files changed, 125 insertions(+), 423 deletions(-) diff --git a/.github/workflows/mcp-registry-submit.yml b/.github/workflows/mcp-registry-submit.yml index 29db9d5b4..4c703ad66 100644 --- a/.github/workflows/mcp-registry-submit.yml +++ b/.github/workflows/mcp-registry-submit.yml @@ -127,7 +127,6 @@ jobs: # we need to sign the timestamp with the private key, which is in MCP_REGISTRY_KEY # MCP_REGISTRY_KEY is expected to be in PEM format, and it uses ed25519 algorithm # unfortunately, pkeyutl can only work with files because of how it has to buffer the input for signing - # shellcheck disable=SC2329 indata="" key_file="" outfile="" diff --git a/mise.dev.toml b/mise.dev.toml index af14d9de6..283e89dda 100644 --- a/mise.dev.toml +++ b/mise.dev.toml @@ -39,7 +39,7 @@ case "$python_path" in ;; *) # Not in venv or python not found, source it - source .venv/bin/activate 2>/dev/null || true + . .venv/bin/activate 2>/dev/null || true ;; esac ''' diff --git a/pyproject.toml b/pyproject.toml index eb002feff..8b1a7934c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,8 +107,8 @@ dependencies = [ "ast-grep-py>=0.40.0", "blake3>=1.0.8", # CLI framework and output - "cyclopts>=4.2.4", - "fastembed>=0.7.3", + "cyclopts>=4.3.0", + "fastembed>=0.7.4", # MCP server framework "fastmcp>=2.13.3", "httpx[http2]>=0.28.1", @@ -120,8 +120,8 @@ dependencies = [ "psutil>=7.1.3", # Core data models and validation "py-cpuinfo>=9.0.0", - "pydantic>=2.12.4", - "pydantic-ai>=1.27.0", + "pydantic>=2.12.5", + "pydantic-ai-slim>=1.27.0", "pydantic-graph>=1.27.0", "pydantic-settings[toml,yaml]>=2.12.0", # Pulls: tomli>=2.0.1, pyyaml>=6.0.1 "qdrant-client>=1.16.1", @@ -153,6 +153,8 @@ codeweaver = "codeweaver.cli.__main__:main" cw = "codeweaver.cli.__main__:main" [project.optional-dependencies] +# ==== Providers ==== +anthropic = ["pydantic-ai-slim[anthropic]"] # eunomia middleware - see https://gofastmcp.com/integrations/eunomia-authorization auth-eunomia = ["eunomia-mcp"] # ==== Authentication and Authorization Middleware ==== @@ -166,6 +168,16 @@ auth-permitio = ["permit-fastmcp"] # Settings/Config Secrets aws-secrets-manager = ["pydantic-settings[yaml,toml,aws-secrets-manager]"] azure-key-vault = ["pydantic-settings[yaml,toml,azure-key-vault]"] +bedrock = [ + "pydantic-ai-slim[bedrock]", + "types-boto3-custom", +] +cohere = ["pydantic-ai-slim[cohere]"] +# ==== Data Sources ==== +duckduckgo = ["pydantic-ai-slim[duckduckgo]"] +fastembed = ["fastembed", "py-cpuinfo"] +# Requires additional setup, see https://qdrant.github.io/fastembed/examples/FastEmbed_GPU/ +fastembed-gpu = ["fastembed-gpu", "py-cpuinfo"] # workos AuthKit middleware - see https://gofastmcp.com/integrations/authkit # No additional dependencies required, it's purely REST-based. # Enable with environment variables: @@ -178,6 +190,7 @@ azure-key-vault = ["pydantic-settings[yaml,toml,azure-key-vault]"] full = [ "permit-fastmcp", "eunomia-mcp", + "pydantic-ai-slim[openai,cohere,google,anthropic,groq,mistral,bedrock,huggingface,duckduckgo,tavily,retries]", "pydantic-settings[yaml,toml,aws-secrets-manager,azure-key-vault,gcp-secret-manager]", "permit-fastmcp", "sentence-transformers>=5.1.2", @@ -194,38 +207,34 @@ full-gpu = [ # "sentence-transformers[onnx-gpu]; python_version>='3.12' and python_version<'3.14'", ] gcp-secret-manager = ["pydantic-settings[yaml,toml,gcp-secret-manager]"] +google = ["pydantic-ai-slim[google]"] gpu-support = [ "fastembed-gpu", # see note below about why this is commented out # "sentence-transformers[onnx-gpu]; python_version>='3.12' and python_version<'3.14'", ] -# ==== Providers ==== -provider-anthropic = ["pydantic-ai-slim[anthropic]"] -provider-bedrock = [ - "pydantic-ai-slim[bedrock]", - "types-boto3-custom", -] -provider-cohere = ["pydantic-ai-slim[cohere]"] -provider-fastembed = ["fastembed", "py-cpuinfo"] -# Requires additional setup, see https://qdrant.github.io/fastembed/examples/FastEmbed_GPU/ -provider-fastembed-gpu = ["fastembed-gpu", "py-cpuinfo"] -provider-google = ["pydantic-ai-slim[google]"] -provider-groq = ["pydantic-ai-slim[groq]"] +groq = ["pydantic-ai-slim[groq]"] # --- Embedding and Agent Providers --- (no rerank) -provider-huggingface = ["pydantic-ai-slim[huggingface]"] +huggingface = ["pydantic-ai-slim[huggingface]"] # ==== Vector Stores ==== -provider-in-memory = ["qdrant-client", "tokenizers"] -provider-mistral = ["pydantic-ai-slim[mistral]"] +in-memory = ["qdrant-client", "tokenizers"] +mistral = ["pydantic-ai-slim[mistral]"] # OpenAI includes support for: # Cerebras, DeekSeek, Ollama, OpenRouter, Vercel, Perplexity, Moonshot, FireworksAI, # TogetherAI, Azure AI Foundry, Heroku, and Github Models, X-AI # which all use the OpenAI API -provider-openai = ["pydantic-ai-slim[openai]"] -provider-qdrant = ["qdrant-client"] -provider-sentence-transformers = [ +openai = ["pydantic-ai-slim[openai]"] +qdrant = ["qdrant-client"] +recommended-local-only = [ + "fastembed", + "sentence-transformers", + "tokenizers", +] +sentence-transformers = [ "sentence-transformers", "py-cpuinfo", ] +tavily = ["pydantic-ai-slim[tavily]"] # NOTE: Why not GPU or OpenVino support for sentence-transformers? # The underlying runtimes for these packages, when resolved against our other dependencies, # essentially require us to choose between support for Python 3.14 and these packages. @@ -234,23 +243,15 @@ provider-sentence-transformers = [ # We think the sparse-encoding use case is much broader than GPU or OpenVino acceleration for most users, # so we are leaving these out of the official optional dependencies for now. # If you need GPU or OpenVino support, you can install these packages manually in your environment. -# provider-sentence-transformers-gpu = [ +# sentence-transformers-gpu = [ # GPU requires additional setup # "sentence-transformers[onnx-gpu]; python_version>='3.12' and python_version<'3.14'", # ] -# provider-sentence-transformers-openvino = [ +# sentence-transformers-openvino = [ # "sentence-transformers[openvino]", # "py-cpuinfo", # ] -provider-voyageai = ["voyageai"] -recommended-local-only = [ - "fastembed", - "sentence-transformers", - "tokenizers", -] -# ==== Data Sources ==== -source-duckduckgo = ["pydantic-ai-slim[duckduckgo]"] -source-tavily = ["pydantic-ai-slim[tavily]"] +voyageai = ["voyageai"] [dependency-groups] dev = [ diff --git a/src/codeweaver/config/profiles.py b/src/codeweaver/config/profiles.py index 9df495833..e76096c5f 100644 --- a/src/codeweaver/config/profiles.py +++ b/src/codeweaver/config/profiles.py @@ -11,8 +11,8 @@ The recommended flag gives you access to: - All current vector, agent and data providers - All embedding and reranking providers except for Sentence Transformers (because these install paths are aligned with pydantic-ai's default dependencies, and Sentence Transformers is not a default dependency of pydantic-ai). -- A-la-Carte installations: You can also use the `required-core` install flag (`pip install code-weaver[required-core]`) to install only the core dependencies of CodeWeaver, and then add individual providers using their own install flags (all prefixed with `provider-`), like: - `pip install code-weaver[required-core,provider-openai,provider-qdrant]` +- A-la-Carte installations: You can also use the `required-core` install flag (`pip install code-weaver[required-core]`) to install only the core dependencies of CodeWeaver, and then add individual providers using their own install flags, like: + `pip install code-weaver[required-core,openai,qdrant]` """ diff --git a/src/codeweaver/config/settings.py b/src/codeweaver/config/settings.py index a106098b9..e078912db 100644 --- a/src/codeweaver/config/settings.py +++ b/src/codeweaver/config/settings.py @@ -521,12 +521,12 @@ class CodeWeaverSettings(BaseSettings): pattern=r"\d{1,2}\.\d{1,3}\.\d{1,3}", alias="schema_version", ), - ] = "1.1.0" + ] = "1.1.1" schema_: HttpUrl = Field( description="URL to the CodeWeaver settings schema", default_factory=lambda data: HttpUrl( - f"https://raw.githubusercontent.com/knitli/codeweaver/main/schema/v{data.get('__version__', data.get('schema_version')) or '1.1.0'}/codeweaver.schema.json" + f"https://raw.githubusercontent.com/knitli/codeweaver/main/schema/v{data.get('__version__', data.get('schema_version')) or '1.1.1'}/codeweaver.schema.json" ), ) diff --git a/src/codeweaver/config/telemetry.py b/src/codeweaver/config/telemetry.py index 78c840605..51665f7eb 100644 --- a/src/codeweaver/config/telemetry.py +++ b/src/codeweaver/config/telemetry.py @@ -73,7 +73,7 @@ class TelemetrySettings(BasedModel): If you want to disable telemetry, you have several options: 1. set this setting to False in your codeweaver.toml/yaml/json file, 2. set the environment variable `CODEWEAVER_ENABLE_TELEMETRY` to `false` - 3. install CodeWeaver with the `codeweaver[recommended-no-telemetry]` extra, or use the a-la-carte install with `codeweaver[required-core]` and your choice of providers (like, `codeweaver[required-core,cli,provider-anthropic,provider-fastembed,provider-azure]`) to install without telemetry + 3. install CodeWeaver with the `codeweaver[recommended-no-telemetry]` extra, or use the a-la-carte install with `codeweaver[required-core]` and your choice of providers (like, `codeweaver[anthropic,fastembed,azure]`) to install without telemetry 4. Point the `CODEWEAVER__TELEMETRY__POSTHOG_PROJECT_KEY` environment variable to your own Posthog project (if you're a data nerd, or want to collect internal telemetry for your organization). If you disable telemetry, we won't collect any data at all.""" ), ] = _set_bool_env_var("CODEWEAVER__TELEMETRY__DISABLE_TELEMETRY") diff --git a/src/codeweaver/core/discovery.py b/src/codeweaver/core/discovery.py index 6a366e2a5..bb4349dff 100644 --- a/src/codeweaver/core/discovery.py +++ b/src/codeweaver/core/discovery.py @@ -159,6 +159,7 @@ def size(self) -> NonNegativeInt: return 0 # Return 0 for non-existent files (e.g., test fixtures) @computed_field + @property def file_hash(self) -> BlakeHashKey: """Return the blake3 hash of the file contents, if available.""" if self._file_hash is not None: @@ -181,7 +182,7 @@ def is_same(self, other_path: Path) -> bool: """ if other_path.is_file() and other_path.exists(): file = type(self).from_path(other_path) - return bool(file and file.file_hash() == self.file_hash()) + return bool(file and file.file_hash == self.file_hash) return False @computed_field diff --git a/src/codeweaver/core/types/aliases.py b/src/codeweaver/core/types/aliases.py index 7f6c84538..9023ce550 100644 --- a/src/codeweaver/core/types/aliases.py +++ b/src/codeweaver/core/types/aliases.py @@ -127,7 +127,8 @@ description="""A file extension string as the `FileExt` NewType, including the leading dot. E.g. '.txt'. May also be an exact filename like 'Makefile' that has no extension.""", pattern=r"""^(\.[^<>:;,?*|\\]+|[^<>:;,?*|\\]+)$""", min_length=2, - max_length=20, + # this may seem long but sometimes extensions are actually filenames, so we allow longer ones within reason + max_length=50, ), ] diff --git a/src/codeweaver/engine/failover_tracker.py b/src/codeweaver/engine/failover_tracker.py index a7bd17e6f..5a2278b5f 100644 --- a/src/codeweaver/engine/failover_tracker.py +++ b/src/codeweaver/engine/failover_tracker.py @@ -137,7 +137,7 @@ def record_file_indexed(self, discovered_file: DiscoveredFile) -> None: serialized_rel_path = str(rel_path) old_hash = self.file_hashes.get(serialized_rel_path) - new_hash = discovered_file.file_hash() + new_hash = discovered_file.file_hash if old_hash != new_hash: self.pending_changes.add(serialized_rel_path) diff --git a/src/codeweaver/engine/indexer/indexer.py b/src/codeweaver/engine/indexer/indexer.py index ed8e56620..7e487aa1f 100644 --- a/src/codeweaver/engine/indexer/indexer.py +++ b/src/codeweaver/engine/indexer/indexer.py @@ -496,7 +496,7 @@ async def _index_file(self, path: Path, context: Any = None) -> None: logger.debug("Skipping non-text file: %s", path) return - self._store.set(discovered_file.file_hash(), discovered_file) + self._store.set(discovered_file.file_hash, discovered_file) self._stats.files_discovered += 1 # Track file discovery in session statistics @@ -667,7 +667,7 @@ async def _index_file(self, path: Path, context: Any = None) -> None: async with self._manifest_lock: self._file_manifest.add_file( path=relative_path, - content_hash=discovered_file.file_hash(), + content_hash=discovered_file.file_hash, chunk_ids=chunk_ids, dense_embedding_provider=model_info["dense_provider"], dense_embedding_model=model_info["dense_model"], @@ -975,7 +975,7 @@ async def _update_manifest_for_batch( async with self._manifest_lock: self._file_manifest.add_file( path=relative_path, - content_hash=discovered_file.file_hash(), + content_hash=discovered_file.file_hash, chunk_ids=chunk_ids, dense_embedding_provider=model_info["dense_provider"], dense_embedding_model=model_info["dense_model"], @@ -1518,7 +1518,7 @@ def _discover_files_for_batch(self, files: list[Path]) -> list[DiscoveredFile]: discovered_file = DiscoveredFile.from_path(path) if discovered_file and discovered_file.is_text: discovered_files.append(discovered_file) - self._store.set(discovered_file.file_hash(), discovered_file) + self._store.set(discovered_file.file_hash, discovered_file) except Exception: logger.warning("Failed to discover file %s", path, exc_info=True) self._stats.files_with_errors.append(path) diff --git a/tests/unit/test_indexer_remove_path.py b/tests/unit/test_indexer_remove_path.py index 30f44bb20..bcf33c35d 100644 --- a/tests/unit/test_indexer_remove_path.py +++ b/tests/unit/test_indexer_remove_path.py @@ -107,7 +107,7 @@ def test_remove_with_multiple_files_in_store( discovered = DiscoveredFile( path=discovered.path, ext_kind=discovered.ext_kind, - file_hash=discovered.file_hash(), + file_hash=discovered.file_hash, git_branch=resolved_branch, source_id=unique_id, ) diff --git a/uv.lock b/uv.lock index c27da6f7a..b24f50fd1 100644 --- a/uv.lock +++ b/uv.lock @@ -15,18 +15,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/52/6ad8f63ec8da1bf40f96996d25d5b650fdd38f5975f8c813732c47388f18/aenum-3.1.16-py3-none-any.whl", hash = "sha256:9035092855a98e41b66e3d0998bd7b96280e85ceb3a04cc035636138a1943eaf", size = 165627, upload-time = "2025-04-25T03:17:58.89Z" }, ] -[[package]] -name = "ag-ui-protocol" -version = "0.1.10" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/67/bb/5a5ec893eea5805fb9a3db76a9888c3429710dfb6f24bbb37568f2cf7320/ag_ui_protocol-0.1.10.tar.gz", hash = "sha256:3213991c6b2eb24bb1a8c362ee270c16705a07a4c5962267a083d0959ed894f4", size = 6945, upload-time = "2025-11-06T15:17:17.068Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/78/eb55fabaab41abc53f52c0918a9a8c0f747807e5306273f51120fd695957/ag_ui_protocol-0.1.10-py3-none-any.whl", hash = "sha256:c81e6981f30aabdf97a7ee312bfd4df0cd38e718d9fc10019c7d438128b93ab5", size = 7889, upload-time = "2025-11-06T15:17:15.325Z" }, -] - [[package]] name = "aiohappyeyeballs" version = "2.6.1" @@ -185,15 +173,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, ] -[[package]] -name = "argcomplete" -version = "3.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/38/61/0b9ae6399dd4a58d8c1b1dc5a27d6f2808023d0b5dd3104bb99f45a33ff6/argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c", size = 73754, upload-time = "2025-10-20T03:33:34.741Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" }, -] - [[package]] name = "ast-grep-py" version = "0.40.0" @@ -682,7 +661,7 @@ dependencies = [ { name = "psutil" }, { name = "py-cpuinfo" }, { name = "pydantic" }, - { name = "pydantic-ai" }, + { name = "pydantic-ai-slim" }, { name = "pydantic-graph" }, { name = "pydantic-settings", extra = ["toml", "yaml"] }, { name = "qdrant-client" }, @@ -702,6 +681,9 @@ dependencies = [ ] [package.optional-dependencies] +anthropic = [ + { name = "pydantic-ai-slim", extra = ["anthropic"] }, +] auth-eunomia = [ { name = "eunomia-mcp" }, ] @@ -714,9 +696,28 @@ aws-secrets-manager = [ azure-key-vault = [ { name = "pydantic-settings", extra = ["azure-key-vault", "toml", "yaml"] }, ] +bedrock = [ + { name = "pydantic-ai-slim", extra = ["bedrock"] }, + { name = "types-boto3-custom" }, +] +cohere = [ + { name = "pydantic-ai-slim", extra = ["cohere"] }, +] +duckduckgo = [ + { name = "pydantic-ai-slim", extra = ["duckduckgo"] }, +] +fastembed = [ + { name = "fastembed" }, + { name = "py-cpuinfo" }, +] +fastembed-gpu = [ + { name = "fastembed-gpu" }, + { name = "py-cpuinfo" }, +] full = [ { name = "eunomia-mcp" }, { name = "permit-fastmcp" }, + { name = "pydantic-ai-slim", extra = ["anthropic", "bedrock", "cohere", "duckduckgo", "google", "groq", "huggingface", "mistral", "openai", "retries", "tavily"] }, { name = "pydantic-settings", extra = ["aws-secrets-manager", "azure-key-vault", "gcp-secret-manager", "toml", "yaml"] }, { name = "sentence-transformers" }, { name = "types-boto3-custom" }, @@ -731,67 +732,46 @@ full-gpu = [ gcp-secret-manager = [ { name = "pydantic-settings", extra = ["gcp-secret-manager", "toml", "yaml"] }, ] -gpu-support = [ - { name = "fastembed-gpu" }, -] -provider-anthropic = [ - { name = "pydantic-ai-slim", extra = ["anthropic"] }, -] -provider-bedrock = [ - { name = "pydantic-ai-slim", extra = ["bedrock"] }, - { name = "types-boto3-custom" }, -] -provider-cohere = [ - { name = "pydantic-ai-slim", extra = ["cohere"] }, -] -provider-fastembed = [ - { name = "fastembed" }, - { name = "py-cpuinfo" }, +google = [ + { name = "pydantic-ai-slim", extra = ["google"] }, ] -provider-fastembed-gpu = [ +gpu-support = [ { name = "fastembed-gpu" }, - { name = "py-cpuinfo" }, ] -provider-google = [ - { name = "pydantic-ai-slim", extra = ["google"] }, -] -provider-groq = [ +groq = [ { name = "pydantic-ai-slim", extra = ["groq"] }, ] -provider-huggingface = [ +huggingface = [ { name = "pydantic-ai-slim", extra = ["huggingface"] }, ] -provider-in-memory = [ +in-memory = [ { name = "qdrant-client" }, { name = "tokenizers" }, ] -provider-mistral = [ +mistral = [ { name = "pydantic-ai-slim", extra = ["mistral"] }, ] -provider-openai = [ +openai = [ { name = "pydantic-ai-slim", extra = ["openai"] }, ] -provider-qdrant = [ +qdrant = [ { name = "qdrant-client" }, ] -provider-sentence-transformers = [ - { name = "py-cpuinfo" }, - { name = "sentence-transformers" }, -] -provider-voyageai = [ - { name = "voyageai" }, -] recommended-local-only = [ { name = "fastembed" }, { name = "sentence-transformers" }, { name = "tokenizers" }, ] -source-duckduckgo = [ - { name = "pydantic-ai-slim", extra = ["duckduckgo"] }, +sentence-transformers = [ + { name = "py-cpuinfo" }, + { name = "sentence-transformers" }, ] -source-tavily = [ +tavily = [ { name = "pydantic-ai-slim", extra = ["tavily"] }, ] +voyageai = [ + { name = "voyageai" }, +] [package.dev-dependencies] build = [ @@ -835,16 +815,16 @@ requires-dist = [ { name = "aenum", specifier = ">=3.1.16" }, { name = "ast-grep-py", specifier = ">=0.40.0" }, { name = "blake3", specifier = ">=1.0.8" }, - { name = "cyclopts", specifier = ">=4.2.4" }, + { name = "cyclopts", specifier = ">=4.3.0" }, { name = "eunomia-mcp", marker = "extra == 'auth-eunomia'" }, { name = "eunomia-mcp", marker = "extra == 'full'" }, { name = "eunomia-mcp", marker = "extra == 'full-gpu'" }, - { name = "fastembed", specifier = ">=0.7.3" }, - { name = "fastembed", marker = "extra == 'provider-fastembed'" }, + { name = "fastembed", specifier = ">=0.7.4" }, + { name = "fastembed", marker = "extra == 'fastembed'" }, { name = "fastembed", marker = "extra == 'recommended-local-only'" }, + { name = "fastembed-gpu", marker = "extra == 'fastembed-gpu'" }, { name = "fastembed-gpu", marker = "extra == 'full-gpu'" }, { name = "fastembed-gpu", marker = "extra == 'gpu-support'" }, - { name = "fastembed-gpu", marker = "extra == 'provider-fastembed-gpu'" }, { name = "fastmcp", specifier = ">=2.13.3" }, { name = "httpx", extras = ["http2"], specifier = ">=0.28.1" }, { name = "mcp", specifier = ">=1.19.0" }, @@ -855,21 +835,22 @@ requires-dist = [ { name = "posthog", specifier = ">=7.0.1" }, { name = "psutil", specifier = ">=7.1.3" }, { name = "py-cpuinfo", specifier = ">=9.0.0" }, - { name = "py-cpuinfo", marker = "extra == 'provider-fastembed'" }, - { name = "py-cpuinfo", marker = "extra == 'provider-fastembed-gpu'" }, - { name = "py-cpuinfo", marker = "extra == 'provider-sentence-transformers'" }, - { name = "pydantic", specifier = ">=2.12.4" }, - { name = "pydantic-ai", specifier = ">=1.27.0" }, - { name = "pydantic-ai-slim", extras = ["anthropic"], marker = "extra == 'provider-anthropic'" }, - { name = "pydantic-ai-slim", extras = ["bedrock"], marker = "extra == 'provider-bedrock'" }, - { name = "pydantic-ai-slim", extras = ["cohere"], marker = "extra == 'provider-cohere'" }, - { name = "pydantic-ai-slim", extras = ["duckduckgo"], marker = "extra == 'source-duckduckgo'" }, - { name = "pydantic-ai-slim", extras = ["google"], marker = "extra == 'provider-google'" }, - { name = "pydantic-ai-slim", extras = ["groq"], marker = "extra == 'provider-groq'" }, - { name = "pydantic-ai-slim", extras = ["huggingface"], marker = "extra == 'provider-huggingface'" }, - { name = "pydantic-ai-slim", extras = ["mistral"], marker = "extra == 'provider-mistral'" }, - { name = "pydantic-ai-slim", extras = ["openai"], marker = "extra == 'provider-openai'" }, - { name = "pydantic-ai-slim", extras = ["tavily"], marker = "extra == 'source-tavily'" }, + { name = "py-cpuinfo", marker = "extra == 'fastembed'" }, + { name = "py-cpuinfo", marker = "extra == 'fastembed-gpu'" }, + { name = "py-cpuinfo", marker = "extra == 'sentence-transformers'" }, + { name = "pydantic", specifier = ">=2.12.5" }, + { name = "pydantic-ai-slim", specifier = ">=1.27.0" }, + { name = "pydantic-ai-slim", extras = ["anthropic"], marker = "extra == 'anthropic'" }, + { name = "pydantic-ai-slim", extras = ["anthropic", "bedrock", "cohere", "duckduckgo", "google", "groq", "huggingface", "mistral", "openai", "retries", "tavily"], marker = "extra == 'full'" }, + { name = "pydantic-ai-slim", extras = ["bedrock"], marker = "extra == 'bedrock'" }, + { name = "pydantic-ai-slim", extras = ["cohere"], marker = "extra == 'cohere'" }, + { name = "pydantic-ai-slim", extras = ["duckduckgo"], marker = "extra == 'duckduckgo'" }, + { name = "pydantic-ai-slim", extras = ["google"], marker = "extra == 'google'" }, + { name = "pydantic-ai-slim", extras = ["groq"], marker = "extra == 'groq'" }, + { name = "pydantic-ai-slim", extras = ["huggingface"], marker = "extra == 'huggingface'" }, + { name = "pydantic-ai-slim", extras = ["mistral"], marker = "extra == 'mistral'" }, + { name = "pydantic-ai-slim", extras = ["openai"], marker = "extra == 'openai'" }, + { name = "pydantic-ai-slim", extras = ["tavily"], marker = "extra == 'tavily'" }, { name = "pydantic-graph", specifier = ">=1.27.0" }, { name = "pydantic-settings", extras = ["aws-secrets-manager", "azure-key-vault", "gcp-secret-manager", "toml", "yaml"], marker = "extra == 'full'" }, { name = "pydantic-settings", extras = ["aws-secrets-manager", "azure-key-vault", "gcp-secret-manager", "toml", "yaml"], marker = "extra == 'full-gpu'" }, @@ -878,32 +859,32 @@ requires-dist = [ { name = "pydantic-settings", extras = ["gcp-secret-manager", "toml", "yaml"], marker = "extra == 'gcp-secret-manager'" }, { name = "pydantic-settings", extras = ["toml", "yaml"], specifier = ">=2.12.0" }, { name = "qdrant-client", specifier = ">=1.16.1" }, - { name = "qdrant-client", marker = "extra == 'provider-in-memory'" }, - { name = "qdrant-client", marker = "extra == 'provider-qdrant'" }, + { name = "qdrant-client", marker = "extra == 'in-memory'" }, + { name = "qdrant-client", marker = "extra == 'qdrant'" }, { name = "rich", specifier = ">=14.2.0" }, { name = "rignore", specifier = ">=0.7.6" }, { name = "sentence-transformers", marker = "extra == 'full'", specifier = ">=5.1.2" }, { name = "sentence-transformers", marker = "extra == 'full-gpu'" }, - { name = "sentence-transformers", marker = "extra == 'provider-sentence-transformers'" }, { name = "sentence-transformers", marker = "extra == 'recommended-local-only'" }, + { name = "sentence-transformers", marker = "extra == 'sentence-transformers'" }, { name = "tenacity", specifier = ">=9.1.2" }, { name = "textcase", specifier = ">=0.4.5" }, { name = "tiktoken", specifier = ">=0.12.0" }, { name = "tokenizers", specifier = ">=0.22.1" }, - { name = "tokenizers", marker = "extra == 'provider-in-memory'" }, + { name = "tokenizers", marker = "extra == 'in-memory'" }, { name = "tokenizers", marker = "extra == 'recommended-local-only'" }, { name = "tomli-w", specifier = ">=1.2.0" }, + { name = "types-boto3-custom", marker = "extra == 'bedrock'", path = "vendored/types_boto3_custom-1.41.2-py3-none-any.whl" }, { name = "types-boto3-custom", marker = "extra == 'full'", path = "vendored/types_boto3_custom-1.41.2-py3-none-any.whl" }, - { name = "types-boto3-custom", marker = "extra == 'provider-bedrock'", path = "vendored/types_boto3_custom-1.41.2-py3-none-any.whl" }, { name = "typing-extensions", specifier = ">=4.15.0" }, { name = "uuid7", marker = "python_full_version < '3.14'", specifier = ">=0.1.0" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.38.0" }, { name = "uvloop", marker = "platform_python_implementation == 'CPython' and sys_platform != 'win32'", specifier = ">=0.22.1" }, { name = "voyageai", specifier = ">=0.3.5" }, - { name = "voyageai", marker = "extra == 'provider-voyageai'" }, + { name = "voyageai", marker = "extra == 'voyageai'" }, { name = "watchfiles", specifier = ">=1.1.1" }, ] -provides-extras = ["auth-eunomia", "auth-permitio", "aws-secrets-manager", "azure-key-vault", "full", "full-gpu", "gcp-secret-manager", "gpu-support", "provider-anthropic", "provider-bedrock", "provider-cohere", "provider-fastembed", "provider-fastembed-gpu", "provider-google", "provider-groq", "provider-huggingface", "provider-in-memory", "provider-mistral", "provider-openai", "provider-qdrant", "provider-sentence-transformers", "provider-voyageai", "recommended-local-only", "source-duckduckgo", "source-tavily"] +provides-extras = ["anthropic", "auth-eunomia", "auth-permitio", "aws-secrets-manager", "azure-key-vault", "bedrock", "cohere", "duckduckgo", "fastembed", "fastembed-gpu", "full", "full-gpu", "gcp-secret-manager", "google", "gpu-support", "groq", "huggingface", "in-memory", "mistral", "openai", "qdrant", "recommended-local-only", "sentence-transformers", "tavily", "voyageai"] [package.metadata.requires-dev] build = [ @@ -1113,7 +1094,7 @@ wheels = [ [[package]] name = "cyclopts" -version = "4.2.4" +version = "4.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -1121,9 +1102,9 @@ dependencies = [ { name = "rich" }, { name = "rich-rst" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/08/f20fe40a5db9cd1c42316de5a26209dd008f30d38eea297f94e744dea463/cyclopts-4.2.4.tar.gz", hash = "sha256:27e1d175df2889aba72cd960c4e34c8b0a501c5b0161e849c72d9fee5903ecbb", size = 149362, upload-time = "2025-11-14T21:38:46.722Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/0f/fe026df2ab8301e30a2b0bd425ff1462ad858fd4f991c1ac0389c2059c24/cyclopts-4.3.0.tar.gz", hash = "sha256:e95179cd0a959ce250ecfb2f0262a5996a92c1f9467bccad2f3d829e6833cef5", size = 151411, upload-time = "2025-11-25T02:59:33.572Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/b2/fabcd6020b63b9d9f7a79cfc61b9c03c4e08ccb54f9cf9db9791be5669ef/cyclopts-4.2.4-py3-none-any.whl", hash = "sha256:41054f5e921a4f2b8ab9c839f12a274dec06a19560dc4898ce37cb775ca68ca4", size = 185023, upload-time = "2025-11-14T21:38:45.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e8/77a231ae531cf38765b75ddf27dae28bb5f70b41d8bb4f15ce1650e93f57/cyclopts-4.3.0-py3-none-any.whl", hash = "sha256:91a30b69faf128ada7cfeaefd7d9649dc222e8b2a8697f1fc99e4ee7b7ca44f3", size = 187184, upload-time = "2025-11-25T02:59:32.21Z" }, ] [[package]] @@ -1359,7 +1340,7 @@ wheels = [ [[package]] name = "fastembed" -version = "0.7.3" +version = "0.7.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, @@ -1373,9 +1354,9 @@ dependencies = [ { name = "tokenizers" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/65/f6/e8d3d9d487f95b698c9ff0d04d4e050d8fca9fa4cba58cff60fd519d1976/fastembed-0.7.3.tar.gz", hash = "sha256:04e95eb5ccc706513166c23bf8e5429ed160c5783b7b11514431a77624d480a5", size = 66561, upload-time = "2025-08-29T11:19:46.521Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/c2/9c708680de1b54480161e0505f9d6d3d8eb47a1dc1a1f7f3c5106ba355d2/fastembed-0.7.4.tar.gz", hash = "sha256:8b8a4ea860ca295002f4754e8f5820a636e1065a9444959e18d5988d7f27093b", size = 68807, upload-time = "2025-12-05T12:08:10.447Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/38/447aabefddda026c3b65b3b9f1fec48ab78b648441e3e530bf8d78b26bdf/fastembed-0.7.3-py3-none-any.whl", hash = "sha256:a377b57843abd773318042960be39f1aef29827530acb98b035a554742a85cdf", size = 105322, upload-time = "2025-08-29T11:19:45.4Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/8da01492bc8b69184257d0c951bf0e77aec8ce110f06d8ce16c6ed9084f7/fastembed-0.7.4-py3-none-any.whl", hash = "sha256:79250a775f70bd6addb0e054204df042b5029ecae501e40e5bbd08e75844ad83", size = 108491, upload-time = "2025-12-05T12:08:09.059Z" }, ] [[package]] @@ -2324,29 +2305,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/5c/521a3d8295e2e7caea67032e65554866293b6dc8e934bd86be8cc1f7b955/langsmith-0.4.43-py3-none-any.whl", hash = "sha256:c97846a0b15061bc15844aac32fd1ce4a8e50983905f80a0d6079bb41b112ae3", size = 410232, upload-time = "2025-11-15T00:32:10.557Z" }, ] -[[package]] -name = "logfire" -version = "4.14.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "executing" }, - { name = "opentelemetry-exporter-otlp-proto-http" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-sdk" }, - { name = "protobuf" }, - { name = "rich" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5c/89/d26951b6b21790641720c12cfd6dca0cf7ead0f5ddd7de4299837b90b8b1/logfire-4.14.2.tar.gz", hash = "sha256:8dcedbd59c3d06a8794a93bbf09add788de3b74c45afa821750992f0c822c628", size = 548291, upload-time = "2025-10-24T20:14:39.115Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/92/4fba7b8f4f56f721ad279cb0c08164bffa14e93cfd184d1a4cc7151c52a2/logfire-4.14.2-py3-none-any.whl", hash = "sha256:caa8111b20f263f4ebb0ae380a62f2a214aeb07d5e2f03c9300fa096d0a8e692", size = 228364, upload-time = "2025-10-24T20:14:34.495Z" }, -] - -[package.optional-dependencies] -httpx = [ - { name = "opentelemetry-instrumentation-httpx" }, -] - [[package]] name = "logfire-api" version = "4.14.2" @@ -2848,18 +2806,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406, upload-time = "2025-05-29T11:35:04.961Z" }, ] -[[package]] -name = "nexus-rpc" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ef/66/540687556bd28cf1ec370cc6881456203dfddb9dab047b8979c6865b5984/nexus_rpc-1.1.0.tar.gz", hash = "sha256:d65ad6a2f54f14e53ebe39ee30555eaeb894102437125733fb13034a04a44553", size = 77383, upload-time = "2025-07-07T19:03:58.368Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/2f/9e9d0dcaa4c6ffa22b7aa31069a8a264c753ff8027b36af602cce038c92f/nexus_rpc-1.1.0-py3-none-any.whl", hash = "sha256:d1b007af2aba186a27e736f8eaae39c03aed05b488084ff6c3d1785c9ba2ad38", size = 27743, upload-time = "2025-07-07T19:03:57.556Z" }, -] - [[package]] name = "nh3" version = "0.3.2" @@ -3181,115 +3127,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/a2/d86e01c28300bd41bab8f18afd613676e2bd63515417b77636fc1add426f/opentelemetry_api-1.38.0-py3-none-any.whl", hash = "sha256:2891b0197f47124454ab9f0cf58f3be33faca394457ac3e09daba13ff50aa582", size = 65947, upload-time = "2025-10-16T08:35:30.23Z" }, ] -[[package]] -name = "opentelemetry-exporter-otlp-proto-common" -version = "1.38.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-proto" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/83/dd4660f2956ff88ed071e9e0e36e830df14b8c5dc06722dbde1841accbe8/opentelemetry_exporter_otlp_proto_common-1.38.0.tar.gz", hash = "sha256:e333278afab4695aa8114eeb7bf4e44e65c6607d54968271a249c180b2cb605c", size = 20431, upload-time = "2025-10-16T08:35:53.285Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/9e/55a41c9601191e8cd8eb626b54ee6827b9c9d4a46d736f32abc80d8039fc/opentelemetry_exporter_otlp_proto_common-1.38.0-py3-none-any.whl", hash = "sha256:03cb76ab213300fe4f4c62b7d8f17d97fcfd21b89f0b5ce38ea156327ddda74a", size = 18359, upload-time = "2025-10-16T08:35:34.099Z" }, -] - -[[package]] -name = "opentelemetry-exporter-otlp-proto-http" -version = "1.38.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "googleapis-common-protos" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-common" }, - { name = "opentelemetry-proto" }, - { name = "opentelemetry-sdk" }, - { name = "requests" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/81/0a/debcdfb029fbd1ccd1563f7c287b89a6f7bef3b2902ade56797bfd020854/opentelemetry_exporter_otlp_proto_http-1.38.0.tar.gz", hash = "sha256:f16bd44baf15cbe07633c5112ffc68229d0edbeac7b37610be0b2def4e21e90b", size = 17282, upload-time = "2025-10-16T08:35:54.422Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/77/154004c99fb9f291f74aa0822a2f5bbf565a72d8126b3a1b63ed8e5f83c7/opentelemetry_exporter_otlp_proto_http-1.38.0-py3-none-any.whl", hash = "sha256:84b937305edfc563f08ec69b9cb2298be8188371217e867c1854d77198d0825b", size = 19579, upload-time = "2025-10-16T08:35:36.269Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation" -version = "0.59b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "packaging" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/04/ed/9c65cd209407fd807fa05be03ee30f159bdac8d59e7ea16a8fe5a1601222/opentelemetry_instrumentation-0.59b0.tar.gz", hash = "sha256:6010f0faaacdaf7c4dff8aac84e226d23437b331dcda7e70367f6d73a7db1adc", size = 31544, upload-time = "2025-10-16T08:39:31.959Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/f5/7a40ff3f62bfe715dad2f633d7f1174ba1a7dd74254c15b2558b3401262a/opentelemetry_instrumentation-0.59b0-py3-none-any.whl", hash = "sha256:44082cc8fe56b0186e87ee8f7c17c327c4c2ce93bdbe86496e600985d74368ee", size = 33020, upload-time = "2025-10-16T08:38:31.463Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation-httpx" -version = "0.59b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/18/6b/1bdf36b68cace9b4eae3cbbade4150c71c90aa392b127dda5bb5c2a49307/opentelemetry_instrumentation_httpx-0.59b0.tar.gz", hash = "sha256:a1cb9b89d9f05a82701cc9ab9cfa3db54fd76932489449778b350bc1b9f0e872", size = 19886, upload-time = "2025-10-16T08:39:48.428Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/16/c1e0745d20af392ec9060693531d7f01239deb2d81e460d0c379719691b8/opentelemetry_instrumentation_httpx-0.59b0-py3-none-any.whl", hash = "sha256:7dc9f66aef4ca3904d877f459a70c78eafd06131dc64d713b9b1b5a7d0a48f05", size = 15197, upload-time = "2025-10-16T08:38:55.507Z" }, -] - -[[package]] -name = "opentelemetry-proto" -version = "1.38.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/51/14/f0c4f0f6371b9cb7f9fa9ee8918bfd59ac7040c7791f1e6da32a1839780d/opentelemetry_proto-1.38.0.tar.gz", hash = "sha256:88b161e89d9d372ce723da289b7da74c3a8354a8e5359992be813942969ed468", size = 46152, upload-time = "2025-10-16T08:36:01.612Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/6a/82b68b14efca5150b2632f3692d627afa76b77378c4999f2648979409528/opentelemetry_proto-1.38.0-py3-none-any.whl", hash = "sha256:b6ebe54d3217c42e45462e2a1ae28c3e2bf2ec5a5645236a490f55f45f1a0a18", size = 72535, upload-time = "2025-10-16T08:35:45.749Z" }, -] - -[[package]] -name = "opentelemetry-sdk" -version = "1.38.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/85/cb/f0eee1445161faf4c9af3ba7b848cc22a50a3d3e2515051ad8628c35ff80/opentelemetry_sdk-1.38.0.tar.gz", hash = "sha256:93df5d4d871ed09cb4272305be4d996236eedb232253e3ab864c8620f051cebe", size = 171942, upload-time = "2025-10-16T08:36:02.257Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/2e/e93777a95d7d9c40d270a371392b6d6f1ff170c2a3cb32d6176741b5b723/opentelemetry_sdk-1.38.0-py3-none-any.whl", hash = "sha256:1c66af6564ecc1553d72d811a01df063ff097cdc82ce188da9951f93b8d10f6b", size = 132349, upload-time = "2025-10-16T08:35:46.995Z" }, -] - -[[package]] -name = "opentelemetry-semantic-conventions" -version = "0.59b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/40/bc/8b9ad3802cd8ac6583a4eb7de7e5d7db004e89cb7efe7008f9c8a537ee75/opentelemetry_semantic_conventions-0.59b0.tar.gz", hash = "sha256:7a6db3f30d70202d5bf9fa4b69bc866ca6a30437287de6c510fb594878aed6b0", size = 129861, upload-time = "2025-10-16T08:36:03.346Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/24/7d/c88d7b15ba8fe5c6b8f93be50fc11795e9fc05386c44afaf6b76fe191f9b/opentelemetry_semantic_conventions-0.59b0-py3-none-any.whl", hash = "sha256:35d3b8833ef97d614136e253c1da9342b4c3c083bbaf29ce31d572a1c3825eed", size = 207954, upload-time = "2025-10-16T08:35:48.054Z" }, -] - -[[package]] -name = "opentelemetry-util-http" -version = "0.59b0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/34/f7/13cd081e7851c42520ab0e96efb17ffbd901111a50b8252ec1e240664020/opentelemetry_util_http-0.59b0.tar.gz", hash = "sha256:ae66ee91be31938d832f3b4bc4eb8a911f6eddd38969c4a871b1230db2a0a560", size = 9412, upload-time = "2025-10-16T08:40:11.335Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/56/62282d1d4482061360449dacc990c89cad0fc810a2ed937b636300f55023/opentelemetry_util_http-0.59b0-py3-none-any.whl", hash = "sha256:6d036a07563bce87bf521839c0671b507a02a0d39d7ea61b88efa14c6e25355d", size = 7648, upload-time = "2025-10-16T08:39:25.706Z" }, -] - [[package]] name = "orjson" version = "3.11.4" @@ -3847,7 +3684,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.4" +version = "2.12.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -3855,9 +3692,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" }, + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] [package.optional-dependencies] @@ -3865,18 +3702,6 @@ email = [ { name = "email-validator" }, ] -[[package]] -name = "pydantic-ai" -version = "1.27.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic-ai-slim", extra = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "fastmcp", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "retries", "temporal", "ui", "vertexai"] }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e3/7f/c56413e9dd3a1f4330981bc4f261f2edb22d3c19480102fdf50e590a5fd7/pydantic_ai-1.27.0.tar.gz", hash = "sha256:6741111d0be19c0fb5f9612beab5af12b945ce715780b19e861fe104e656d61d", size = 11862, upload-time = "2025-12-05T03:23:03.818Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/6f/dfa4a1895b5011b0daff684198226d6a16be1f1104a4e7e9f8b1377c4ab0/pydantic_ai-1.27.0-py3-none-any.whl", hash = "sha256:cdb77c1a1f6569c42483ba05b1da99aa3b5f7f9042ad3492b5dac741f45d5450", size = 7167, upload-time = "2025-12-05T03:22:54.825Z" }, -] - [[package]] name = "pydantic-ai-slim" version = "1.27.0" @@ -3896,34 +3721,18 @@ wheels = [ ] [package.optional-dependencies] -ag-ui = [ - { name = "ag-ui-protocol" }, - { name = "starlette" }, -] anthropic = [ { name = "anthropic" }, ] bedrock = [ { name = "boto3" }, ] -cli = [ - { name = "argcomplete" }, - { name = "prompt-toolkit" }, - { name = "pyperclip" }, - { name = "rich" }, -] cohere = [ { name = "cohere", marker = "sys_platform != 'emscripten'" }, ] duckduckgo = [ { name = "ddgs" }, ] -evals = [ - { name = "pydantic-evals" }, -] -fastmcp = [ - { name = "fastmcp" }, -] google = [ { name = "google-genai" }, ] @@ -3933,12 +3742,6 @@ groq = [ huggingface = [ { name = "huggingface-hub", extra = ["inference"] }, ] -logfire = [ - { name = "logfire", extra = ["httpx"] }, -] -mcp = [ - { name = "mcp" }, -] mistral = [ { name = "mistralai" }, ] @@ -3951,16 +3754,6 @@ retries = [ tavily = [ { name = "tavily-python" }, ] -temporal = [ - { name = "temporalio" }, -] -ui = [ - { name = "starlette" }, -] -vertexai = [ - { name = "google-auth" }, - { name = "requests" }, -] [[package]] name = "pydantic-core" @@ -4033,23 +3826,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, ] -[[package]] -name = "pydantic-evals" -version = "1.27.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "logfire-api" }, - { name = "pydantic" }, - { name = "pydantic-ai-slim" }, - { name = "pyyaml" }, - { name = "rich" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b6/75/f668b4d83e3c6d6806b37f97b84397ca1667a559147d8d4133a3665db55d/pydantic_evals-1.27.0.tar.gz", hash = "sha256:d8f08c4a61c7adea8cabd84368d3acde88f196162ab37ca1f99ffbbac6872f8f", size = 47078, upload-time = "2025-12-05T03:23:08.792Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/b0/1fcde1b8263ec681a46e1af1a6a01850ed25ae856db52c477b127d12947e/pydantic_evals-1.27.0-py3-none-any.whl", hash = "sha256:086b3264c6602dceddaeb66ee18df7f9a3d5def2acd27d5251018bc95430c47e", size = 56225, upload-time = "2025-12-05T03:22:59.876Z" }, -] - [[package]] name = "pydantic-graph" version = "1.27.0" @@ -5030,24 +4806,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/4d/e5e4c65cd66144ac3d0d5a6a2bbfba22eb6a63e6e450beba10ee8413b86d/tavily_python-0.7.13-py3-none-any.whl", hash = "sha256:911825467f2bb19b8162b4766d3e81081160a7c0fb8a15c7c716b2bef73e6296", size = 15484, upload-time = "2025-11-13T18:53:02.821Z" }, ] -[[package]] -name = "temporalio" -version = "1.19.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nexus-rpc" }, - { name = "protobuf" }, - { name = "types-protobuf" }, - { name = "typing-extensions" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/92/0775d831fa245d61b74db2059d5a24a04cef0532ed2c48310a5ab007de9c/temporalio-1.19.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c2d6d5cad8aec56e048705aa4f0bab83fec15343757ea7acf8504f2e0c289b60", size = 13175255, upload-time = "2025-11-13T22:35:54.22Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e1/2a818fefc0023eb132bfff1a03440bcaff154d4d97445ef88a40c23c20c8/temporalio-1.19.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:d85c89018cba9471ce529d90c9cee5bcc31790fd64176b9ada32cc76440f8d73", size = 12854549, upload-time = "2025-11-13T22:35:57.217Z" }, - { url = "https://files.pythonhosted.org/packages/ff/78/fe5c8c9b112b38e01aba845335df17a8bbfd60a434ffe3c1c4737ced40a0/temporalio-1.19.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f772f0698d60f808bc3c4a055fb53e40d757fa646411845b911863eebbf0549d", size = 13237772, upload-time = "2025-11-13T22:36:00.511Z" }, - { url = "https://files.pythonhosted.org/packages/d9/82/be0fd31119651f518f8db8685fd61976d9d5bbecf3b562d51f13a6442a17/temporalio-1.19.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f706c8f49771daf342ac8daa8ed07f4124fae943177f9feef458a1255aee717c", size = 13374621, upload-time = "2025-11-13T22:36:03.431Z" }, - { url = "https://files.pythonhosted.org/packages/d8/94/18f6ae06ffd91507ded9111af1041146a5ba4b56e9256520c5ce82629fc4/temporalio-1.19.0-cp310-abi3-win_amd64.whl", hash = "sha256:162459c293553be39994f20c635a132f7332ae71bd7ba4042f8473701fcf1c7c", size = 14256891, upload-time = "2025-11-13T22:36:06.778Z" }, -] - [[package]] name = "tenacity" version = "9.1.2" @@ -5409,15 +5167,6 @@ requires-dist = [ { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] -[[package]] -name = "types-protobuf" -version = "6.32.1.20251105" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/ab/0dce6a9841b5ebf3e37401879bb8cc20724ad9c770a7649bee997696cc75/types_protobuf-6.32.1.20251105.tar.gz", hash = "sha256:641002611ff87dd9fedc38a39a29cacb9907ae5ce61489b53e99ca2074bef764", size = 63846, upload-time = "2025-11-05T03:04:43.456Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/57/3a0d89b33b7485b7ffd99ec7cf53b0c5c89194c481f0bd673fd67e5f273f/types_protobuf-6.32.1.20251105-py3-none-any.whl", hash = "sha256:a15109d38f7cfefd2539ef86d3f93a6a41c7cad53924f8aa1a51eaddbb72a660", size = 77890, upload-time = "2025-11-05T03:04:42.067Z" }, -] - [[package]] name = "types-requests" version = "2.32.4.20250913" @@ -5714,55 +5463,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, ] -[[package]] -name = "wrapt" -version = "1.17.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, - { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, - { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, - { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, - { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, - { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, - { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, - { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, - { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, - { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, - { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, - { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, - { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, - { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, - { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, - { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, - { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, - { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, - { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, - { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, - { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, - { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, - { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, - { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, - { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, - { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, - { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, - { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, - { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, - { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, - { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, - { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, - { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, - { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, - { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, - { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, -] - [[package]] name = "yarl" version = "1.22.0" From 4a5b3701c8733b678b5901d3fcebb753fb3c3d81 Mon Sep 17 00:00:00 2001 From: Adam Poulemanos Date: Sun, 7 Dec 2025 15:06:03 -0500 Subject: [PATCH 30/37] chore: clarify descriptions in registry generators. Yml/json formatting --- .github/actionlint.yml | 2 +- .github/actions/setup-mise-env/action.yml | 16 ++-- .github/actions/setup-python-env/action.yml | 15 +--- .github/actions/setup-uv-env/action.yml | 19 ++--- .github/workflows/_reusable-build.yml | 23 ++---- .github/workflows/_reusable-lint.yml | 14 +--- .github/workflows/_reusable-test.yml | 27 +++---- .github/workflows/changelog.yml | 12 +-- .github/workflows/ci.yml | 43 +++++------ .github/workflows/cla.yml | 6 +- .github/workflows/claude.yml | 23 +++--- .github/workflows/copilot-setup-steps.yml | 4 +- .github/workflows/docker.yml | 7 +- .github/workflows/mcp-registry-submit.yml | 11 +-- .github/workflows/publish-test.yml | 4 +- .github/workflows/release.yml | 80 +------------------- .mcp.json | 2 +- .vscode/mcp.json | 2 +- context7.json | 2 +- data/node_types/hcl-node-types.json | 2 +- docker-compose.yml | 21 +++-- docs-site/.vscode/extensions.json | 4 +- docs-site/package.json | 2 +- docs-site/tsconfig.json | 2 +- schema/v1.0.0/codeweaver.schema.json | 2 +- scripts/build/generate-docker-server-yaml.py | 6 +- scripts/build/generate-mcp-server-json.py | 2 +- server.yaml | 48 ++++++------ 28 files changed, 129 insertions(+), 272 deletions(-) diff --git a/.github/actionlint.yml b/.github/actionlint.yml index 138f61b35..feb35f292 100644 --- a/.github/actionlint.yml +++ b/.github/actionlint.yml @@ -6,4 +6,4 @@ paths: .github/actions/setup-uv-env/action.yml: ignore: - - '.*is not defined in object type.*' \ No newline at end of file + - .*is not defined in object type.* diff --git a/.github/actions/setup-mise-env/action.yml b/.github/actions/setup-mise-env/action.yml index 611faafb9..7eb178366 100644 --- a/.github/actions/setup-mise-env/action.yml +++ b/.github/actions/setup-mise-env/action.yml @@ -5,29 +5,27 @@ name: Setup Mise Environment description: Complete Python development environment setup with mise and UV caching - inputs: python-version: description: Python version to install required: false - default: '3.12' + default: "3.12" fetch-depth: description: Git fetch depth for checkout required: false - default: '1' + default: "1" skip-checkout: description: Skip checkout step (if already checked out by calling workflow) required: false - default: 'false' + default: "false" github-token: description: GitHub token for mise installation (to avoid rate limits) required: false - default: '' + default: "" profile: description: Task profile to use for mise setup one of 'minimal', 'reviewer', 'dev' required: true - default: 'minimal' - + default: minimal outputs: MISE_PATH: description: Path to mise executable @@ -47,8 +45,6 @@ outputs: NEWPATH: description: Updated PATH including mise and python paths value: ${{ steps.mise-outputs.outputs.NEWPATH }} - - runs: using: composite steps: @@ -64,7 +60,6 @@ runs: - name: Free up disk space shell: bash run: ./scripts/dev-env/ci-free-disk-space.sh - - name: Setup Mise for Minimal Profile if: inputs.profile == 'minimal' uses: jdx/mise-action@eb508e65f0eb73d4fc9fc9505569833136217cc9 @@ -130,7 +125,6 @@ runs: MISE_GITHUB_TOKEN: ${{ inputs.github-token }} run: | mise ://setup - - name: Set outputs shell: bash id: mise-outputs diff --git a/.github/actions/setup-python-env/action.yml b/.github/actions/setup-python-env/action.yml index ce12ad1ca..95753c4da 100644 --- a/.github/actions/setup-python-env/action.yml +++ b/.github/actions/setup-python-env/action.yml @@ -5,25 +5,23 @@ name: Setup Python Environment description: Minimal Python environment setup - inputs: python-version: description: Python version to install required: false - default: '3.12' + default: "3.12" fetch-depth: description: Git fetch depth for checkout required: false - default: '0' + default: "0" skip-checkout: description: Skip checkout step (if already checked out by calling workflow) required: false - default: 'false' + default: "false" github-token: description: GitHub token for mise installation (to avoid rate limits) required: false - default: '' - + default: "" outputs: PYTHON_PATH: description: Path to the Python executable @@ -31,7 +29,6 @@ outputs: UV_PATH: description: Path to uv executable value: ${{ steps.python-setup.outputs.UV_PATH }} - runs: using: composite steps: @@ -40,11 +37,9 @@ runs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 with: fetch-depth: ${{ inputs.fetch-depth }} - - name: Free up disk space shell: bash run: ./scripts/dev-env/ci-free-disk-space.sh - - name: Set up Python ${{ inputs.python-version }} uses: actions/setup-python@9b03c627221f32cb88b299427119da30671cf9cc # v5 with: @@ -59,5 +54,3 @@ runs: echo "PYTHON_PATH=$(which python)" >> "$GITHUB_OUTPUT" echo "UV_PATH=$(which uv)" >> "$GITHUB_OUTPUT" id: python-setup - - diff --git a/.github/actions/setup-uv-env/action.yml b/.github/actions/setup-uv-env/action.yml index a5972c170..ba960e7d4 100644 --- a/.github/actions/setup-uv-env/action.yml +++ b/.github/actions/setup-uv-env/action.yml @@ -5,29 +5,27 @@ name: Setup UV Environment description: Minimal UV environment setup with caching - inputs: fetch-depth: description: Git fetch depth for checkout required: false - default: '1' + default: "1" skip-checkout: description: Skip checkout step (if already checked out by calling workflow) required: false - default: 'false' + default: "false" github-token: description: GitHub token for mise installation (to avoid rate limits) required: false - default: '' + default: "" python-version: description: Python version to install required: false - default: '3.12' + default: "3.12" activate-venv: description: Whether to activate the Python virtual environment required: false - default: 'false' - + default: "false" outputs: UV_PATH: description: Path to uv executable @@ -35,7 +33,6 @@ outputs: UVX_PATH: description: Path to uvx executable value: ${{ steps.uv-outputs.outputs.UVX_PATH }} - runs: using: composite steps: @@ -44,11 +41,10 @@ runs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 with: fetch-depth: ${{ inputs.fetch-depth }} - - name: Setup UV and build uses: astral-sh/setup-uv@9c12baee9699f1b4f4318d7175fab8635c2b8e45 with: - resolution-strategy: "highest" + resolution-strategy: highest python-version: ${{ inputs.python-version }} activate-environment: ${{ inputs.activate-venv }} github-token: ${{ inputs.github-token }} @@ -56,7 +52,7 @@ runs: cache-dependency-glob: | **/pyproject.toml **/uv.lock - cache-suffix: "highest" + cache-suffix: highest restore-cache: "true" save-cache: "true" prune-cache: "true" @@ -68,4 +64,3 @@ runs: run: | echo "UV_PATH=${{ steps.setup-uv.outputs.uv-path }}" >> $GITHUB_OUTPUT echo "UVX_PATH=${{ steps.setup-uv.outputs.uvx-path }}" >> $GITHUB_OUTPUT - diff --git a/.github/workflows/_reusable-build.yml b/.github/workflows/_reusable-build.yml index b59641359..0797366c4 100644 --- a/.github/workflows/_reusable-build.yml +++ b/.github/workflows/_reusable-build.yml @@ -4,31 +4,29 @@ # SPDX-License-Identifier: MIT OR Apache-2.0 name: Reusable Build Workflow - on: workflow_call: inputs: artifact-name: - description: "Name for the build artifacts" + description: Name for the build artifacts required: false type: string - default: "python-package-distributions" + default: python-package-distributions python-version: - description: "Python version for building" + description: Python version for building required: false type: string default: "3.12" upload-artifacts: - description: "Upload build artifacts" + description: Upload build artifacts required: false type: boolean default: true clean-build: - description: "Run clean build (uv build --clear)" + description: Run clean build (uv build --clear) required: false type: boolean default: false - jobs: build: name: Build Package @@ -42,7 +40,6 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 with: fetch-depth: 2 - - name: Setup UV and build uses: ./.github/actions/setup-uv-env with: @@ -62,21 +59,13 @@ jobs: else "$UV_PATH" build fi - - name: Check build artifacts - run: | - ls -la dist/ - echo "Built packages:" - for file in dist/*; do - echo "📦 $(basename "$file")" - done - + run: "ls -la dist/\necho \"Built packages:\"\nfor file in dist/*; do\n echo \"\U0001F4E6 $(basename \"$file\")\"\ndone\n" - name: Verify package integrity with twine env: UVX_PATH: ${{ steps.setup-uv.outputs.uvx-path }} run: | "$UVX_PATH" twine check dist/* - - name: Upload build artifacts if: inputs.upload-artifacts uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 diff --git a/.github/workflows/_reusable-lint.yml b/.github/workflows/_reusable-lint.yml index 434c3c382..38d83a0a6 100644 --- a/.github/workflows/_reusable-lint.yml +++ b/.github/workflows/_reusable-lint.yml @@ -6,26 +6,23 @@ name: Reusable Lint Workflow permissions: contents: read - on: workflow_call: inputs: python-version: - description: "Python version for linting" + description: Python version for linting required: false type: string default: "3.12" run-format-check: - description: "Run format/license checks" + description: Run format/license checks required: false type: boolean default: true - env: MISE_EXPERIMENTAL: 1 MISE_YES: 1 - PROFILE: "reviewer" - + PROFILE: reviewer jobs: lint: permissions: @@ -41,13 +38,12 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 with: fetch-depth: 1 - - name: Setup Python environment uses: ./.github/actions/setup-mise-env with: python-version: ${{ env.python-version }} github-token: ${{ secrets.GITHUB_TOKEN }} - profile: "${{ env.PROFILE }}" + profile: ${{ env.PROFILE }} skip-checkout: true id: mise-outputs - name: Set Path @@ -55,7 +51,6 @@ jobs: export PATH="$HOME/.local/bin:$HOME/.local/share/mise/bin:$HOME/.local/share/mise/shims:$PATH" eval "$(mise activate bash)" echo "PATH=$PATH" >> "$GITHUB_ENV" - - name: Check code style and linting env: PYTHON_PATH: ${{ steps.mise-outputs.outputs.PYTHON_PATH }} @@ -64,7 +59,6 @@ jobs: mise use -g uv@latest mise //:lint || echo "::warning::Lint checks failed but allowing workflow to continue" continue-on-error: true - - name: Check formatting and licenses if: ${{ inputs.run-format-check == true }} run: | diff --git a/.github/workflows/_reusable-test.yml b/.github/workflows/_reusable-test.yml index 5aa39523e..6af645923 100644 --- a/.github/workflows/_reusable-test.yml +++ b/.github/workflows/_reusable-test.yml @@ -6,27 +6,26 @@ name: Reusable Test Workflow permissions: contents: read - on: workflow_call: inputs: python-versions: - description: 'JSON array of Python versions to test (e.g., ["3.12", "3.13"]). Experimental versions (3.13t, 3.14t) are always included via matrix.include.' + description: JSON array of Python versions to test (e.g., ["3.12", "3.13"]). Experimental versions (3.13t, 3.14t) are always included via matrix.include. required: false type: string default: '["3.12", "3.13", "3.14"]' test-markers: - description: "Pytest markers for test filtering" + description: Pytest markers for test filtering required: false type: string - default: "not docker and not qdrant and not dev_only and not skip_ci and not network and not external_api" + default: not docker and not qdrant and not dev_only and not skip_ci and not network and not external_api upload-coverage: - description: "Upload coverage to Codecov" + description: Upload coverage to Codecov required: false type: boolean default: true run-quality-checks: - description: "Run quality checks before tests" + description: Run quality checks before tests required: false type: boolean default: true @@ -43,7 +42,7 @@ env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} MISE_EXPERIMENTAL: 1 MISE_YES: 1 - PROFILE: "dev" + PROFILE: dev CODEWEAVER_PROJECT_PATH: ${{ github.workspace }} CODEWEAVER_TESTING: "true" CODEWEAVER_VECTOR_STORE_URL: ${{ secrets.CODEWEAVER_VECTOR_STORE_URL }} @@ -51,7 +50,6 @@ env: VOYAGE_API_KEY: ${{ secrets.VOYAGE_API_KEY }} MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - jobs: test: name: Test Python ${{ matrix.python-version }}${{ matrix.experimental && ' (experimental)' || '' }} @@ -61,12 +59,13 @@ jobs: fail-fast: false matrix: python-version: ${{ fromJSON(inputs.python-versions) }} - experimental: [false] + experimental: + - false include: # Experimental free-threaded Python versions (no-GIL) - - python-version: "3.13t" + - python-version: 3.13t experimental: true - - python-version: "3.14t" + - python-version: 3.14t experimental: true env: MISE_PYTHON: ${{ matrix.python-version }} @@ -76,14 +75,13 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 with: fetch-depth: 3 - - name: Setup Python environment with Mise if: ${{ matrix.experimental == false }} uses: ./.github/actions/setup-mise-env with: python-version: ${{ matrix.python-version }} github-token: ${{ secrets.GITHUB_TOKEN }} - profile: "dev" + profile: dev skip-checkout: true id: setup-mise - name: Set Path @@ -112,7 +110,6 @@ jobs: files: ./coverage.xml fail_ci_if_error: false token: ${{ secrets.CODECOV_TOKEN }} - - name: Upload test results uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 if: always() @@ -122,7 +119,6 @@ jobs: test-results.xml coverage.xml if-no-files-found: warn - # Run experimental last - name: Setup Experimental Python environment if: ${{ matrix.experimental == true }} @@ -156,7 +152,6 @@ jobs: shell: bash run: | uv sync --group test --resolution highest 2>&1 - - name: Run test coverage for experimental Python if: ${{ matrix.experimental == true }} run: | diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 055f48130..ad67e3bad 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -4,23 +4,20 @@ # SPDX-License-Identifier: MIT OR Apache-2.0 name: Generate Changelog - on: workflow_dispatch: inputs: tag: - description: "Generate changelog for specific tag (e.g., v0.1.0). Leave empty for unreleased changes." + description: Generate changelog for specific tag (e.g., v0.1.0). Leave empty for unreleased changes. required: false type: string commit: - description: "Commit and push the generated CHANGELOG.md" + description: Commit and push the generated CHANGELOG.md required: false type: boolean default: false - permissions: contents: write - jobs: generate-changelog: name: Generate Changelog @@ -30,12 +27,10 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 with: fetch-depth: 0 # Fetch all history for changelog generation - - name: Install git-cliff uses: taiki-e/install-action@331a600f1b10a3fed8dc56f925012bede91ae51f with: tool: git-cliff@2.10.0 - - name: Generate changelog id: changelog run: | @@ -49,12 +44,10 @@ jobs: echo "Generating changelog for unreleased changes" git-cliff --unreleased --prepend --output CHANGELOG.md fi - - name: Display changelog run: | echo "Generated CHANGELOG.md:" cat CHANGELOG.md - - name: Commit and push if: github.event.inputs.commit == 'true' run: | @@ -71,7 +64,6 @@ jobs: fi git push fi - - name: Upload changelog artifact uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2cdf8834..3c3a50c82 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,42 +6,39 @@ name: CI permissions: contents: read - on: push: branches: - main - staging paths: - - "src/**" - - "tests/**" - - "pyproject.toml" - - "uv.lock" - - "**/mise.*.toml" - - "**/mise.toml" - - ".github/workflows/ci.yml" - - ".github/workflows/_reusable-*.yml" - - ".github/actions/**" + - src/** + - tests/** + - pyproject.toml + - uv.lock + - '**/mise.*.toml' + - '**/mise.toml' + - .github/workflows/ci.yml + - .github/workflows/_reusable-*.yml + - .github/actions/** pull_request: branches: - main - staging paths: - - "src/**" - - "tests/**" - - "pyproject.toml" - - "uv.lock" - - "mise.toml" - - "mise.ci.toml" - - ".github/workflows/ci.yml" - - ".github/workflows/_reusable-*.yml" - - ".github/actions/**" + - src/** + - tests/** + - pyproject.toml + - uv.lock + - mise.toml + - mise.ci.toml + - .github/workflows/ci.yml + - .github/workflows/_reusable-*.yml + - .github/actions/** workflow_dispatch: - concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true - jobs: test: name: Test @@ -52,15 +49,13 @@ jobs: QDRANT__SERVICE__API_KEY: ${{ secrets.QDRANT__SERVICE__API_KEY }} VOYAGE_API_KEY: ${{ secrets.VOYAGE_API_KEY }} with: - test-markers: "not docker and not qdrant and not dev_only and not skip_ci and not network and not external_api and not flaky" + test-markers: not docker and not qdrant and not dev_only and not skip_ci and not network and not external_api and not flaky upload-coverage: true run-quality-checks: true - lint: name: Lint uses: ./.github/workflows/_reusable-lint.yml secrets: inherit - build: name: Build needs: diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index 291e5c30b..97bc38c29 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -5,7 +5,6 @@ # ! GitHub Action to check CLA signatures for Knitli repositories # ! This action is triggered on issue comments and pull request events. name: CLA Assistant - on: issue_comment: types: @@ -20,11 +19,10 @@ permissions: contents: read pull-requests: write statuses: write - jobs: cla-check: uses: knitli/.github/.github/workflows/cla-check.yml@main with: - repo_name: "codeweaver" - cla_document_url: "https://github.com/knitli/codeweaver/blob/main/CONTRIBUTORS_LICENSE_AGREEMENT.md" + repo_name: codeweaver + cla_document_url: https://github.com/knitli/codeweaver/blob/main/CONTRIBUTORS_LICENSE_AGREEMENT.md secrets: inherit diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 05db17a6d..20d28c441 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -19,7 +19,6 @@ on: pull_request_review: types: - submitted - permissions: actions: read checks: read @@ -27,14 +26,13 @@ permissions: contents: write discussions: write pull-requests: write - env: CODEWEAVER_VECTOR_STORE_URL: ${{ secrets.CODEWEAVER_VECTOR_STORE_URL }} QDRANT__SERVICE__API_KEY: ${{ secrets.QDRANT__SERVICE__API_KEY }} VOYAGE_API_KEY: ${{ secrets.VOYAGE_API_KEY }} CODEWEAVER_PROJECT_PATH: ${{ github.workspace }} MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - MISE_ENV: "dev" + MISE_ENV: dev MISE_YES: 1 TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} # Limit CodeQL memory usage to prevent OOM kills on standard runners (7GB total RAM) @@ -59,17 +57,16 @@ jobs: with: python-version: "3.13" github-token: ${{ secrets.GITHUB_TOKEN }} - profile: "reviewer" + profile: reviewer skip-checkout: "true" - - name: PR Review if: github.event_name == 'pull_request_review' uses: anthropics/claude-code-action@e8bad572273ce919ba15fec95aef0ce974464753 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }} - allowed_non_write_users: "Copilot" - trigger_phrase: "@claude" + allowed_non_write_users: Copilot + trigger_phrase: '@claude' assignee_trigger: claude label_trigger: claude base_branch: main @@ -95,7 +92,7 @@ jobs: with: python-version: "3.13" github-token: ${{ secrets.GITHUB_TOKEN }} - profile: "dev" + profile: dev skip-checkout: "true" - name: Issue Opened if: github.event_name == 'issues' && github.event.action == 'opened' @@ -103,7 +100,7 @@ jobs: with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }} - trigger_phrase: "@claude" + trigger_phrase: '@claude' assignee_trigger: claude label_trigger: claude track_progress: true @@ -130,7 +127,7 @@ jobs: with: python-version: "3.13" github-token: ${{ secrets.GITHUB_TOKEN }} - profile: "minimal" + profile: minimal skip-checkout: "true" - name: PR Review Comment if: github.event_name == 'pull_request_review_comment' @@ -138,7 +135,7 @@ jobs: with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }} - trigger_phrase: "@claude" + trigger_phrase: '@claude' assignee_trigger: claude label_trigger: claude use_commit_signing: true @@ -166,7 +163,7 @@ jobs: with: python-version: "3.13" github-token: ${{ secrets.GITHUB_TOKEN }} - profile: "dev" + profile: dev skip-checkout: "true" - name: Issue Assigned or Labeled Claude if: | @@ -175,7 +172,7 @@ jobs: with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }} - trigger_phrase: "@claude" + trigger_phrase: '@claude' assignee_trigger: claude label_trigger: claude track_progress: true diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 7848f25dc..f3b349cc8 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -14,13 +14,11 @@ on: pull_request: paths: - .github/workflows/copilot-setup-steps.yml - jobs: # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. copilot-setup-steps: outputs: MISE_PATH: ${{ steps.setup-mise.outputs.MISE_PATH }} - env: CODEWEAVER_VECTOR_STORE_URL: ${{ secrets.CODEWEAVER_VECTOR_STORE_URL }} QDRANT__SERVICE__API_KEY: ${{ secrets.QDRANT__SERVICE__API_KEY }} @@ -64,7 +62,7 @@ jobs: with: python-version: "3.13" github-token: ${{ secrets.GITHUB_TOKEN }} - profile: "dev" + profile: dev skip-checkout: "true" id: setup-mise - name: Verify Mise Setup diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index b8b26fb91..6300d4d2d 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -34,7 +34,7 @@ on: workflow_dispatch: inputs: update_description_only: - description: "Only update Docker Hub description (no build)" + description: Only update Docker Hub description (no build) required: false type: boolean default: false @@ -61,10 +61,9 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} repository: ${{ env.IMAGE_NAME }} readme-filepath: ./DOCKER.md - short-description: "The missing abstraction layer between AI and your code - deep, structural understanding for better AI results" + short-description: The missing abstraction layer between AI and your code - deep, structural understanding for better AI results - name: Confirmation run: echo "✅ Docker Hub description updated successfully!" - # Build and test Docker image build: name: Build Docker Image @@ -304,7 +303,7 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} repository: ${{ env.IMAGE_NAME }} readme-filepath: ./DOCKER.md - short-description: "The missing abstraction layer between AI and your code - deep, structural understanding for better AI results" + short-description: The missing abstraction layer between AI and your code - deep, structural understanding for better AI results - name: Generate image summary run: | { diff --git a/.github/workflows/mcp-registry-submit.yml b/.github/workflows/mcp-registry-submit.yml index 4c703ad66..54097162d 100644 --- a/.github/workflows/mcp-registry-submit.yml +++ b/.github/workflows/mcp-registry-submit.yml @@ -5,7 +5,6 @@ # Triggers on release publication after PyPI package is available name: Submit to MCP Registry - on: release: types: @@ -14,25 +13,21 @@ on: workflow_dispatch: inputs: version: - description: "Version to submit (leave empty to use latest release)" + description: Version to submit (leave empty to use latest release) required: false type: string - permissions: contents: read - jobs: validate-and-submit: name: Validate and Submit to MCP Registry runs-on: ubuntu-latest - steps: - name: Checkout repository uses: actions/checkout@b3498302c5c423fa896b97a26bb183df735d08f8 with: ref: ${{ github.event.release.tag_name || github.ref }} fetch-depth: 1 - - name: Install uv uses: ./.github/actions/setup-uv-env with: @@ -55,7 +50,6 @@ jobs: echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "Detected version: ${VERSION}" - - name: Regenerate server.json from code run: | VERSION="${{ steps.version.outputs.version }}" @@ -64,7 +58,6 @@ jobs: # Run the generation script (uses PEP 723 dependencies) uv run scripts/build/generate-mcp-server-json.py echo "✓ server.json regenerated" - - name: Wait for PyPI package availability run: | VERSION="${{ steps.version.outputs.version }}" @@ -113,7 +106,6 @@ jobs: echo "ERROR: Package not available on PyPI after ${MAX_ATTEMPTS} attempts" echo "Please verify that the package was successfully published" exit 1 - - name: Submit to MCP Registry env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -185,7 +177,6 @@ jobs: echo "✓ Successfully submitted to MCP Registry at $publication_time" echo "MCP_PUBLICATION_TIME=$publication_time" >> "$GITHUB_OUTPUT" exit 0 - - name: Summary of MCP Registry Submission if: always() run: | diff --git a/.github/workflows/publish-test.yml b/.github/workflows/publish-test.yml index 710910bb4..d83cd2852 100644 --- a/.github/workflows/publish-test.yml +++ b/.github/workflows/publish-test.yml @@ -22,11 +22,10 @@ jobs: uses: ./.github/workflows/_reusable-test.yml with: python-versions: '["3.12", "3.13"]' - test-markers: "not docker and not qdrant and not dev_only and not skip_ci" + test-markers: not docker and not qdrant and not dev_only and not skip_ci upload-coverage: false run-quality-checks: true secrets: inherit - build: name: Build Distribution needs: tests @@ -37,7 +36,6 @@ jobs: python-version: "3.12" upload-artifacts: true clean-build: true - publish-to-testpypi: name: Publish to TestPyPI needs: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c5bc8bedc..5d2f5ffc1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,23 +37,21 @@ jobs: uses: ./.github/workflows/_reusable-test.yml with: python-versions: '["3.12", "3.13", "3.14"]' - test-markers: "not docker and not qdrant and not dev_only and not skip_ci and not network and not external_api and not flaky" + test-markers: not docker and not qdrant and not dev_only and not skip_ci and not network and not external_api and not flaky upload-coverage: false run-quality-checks: true secrets: inherit - build: name: Build Distribution needs: tests if: always() && (needs.tests.result == 'success' || needs.tests.result == 'skipped') uses: ./.github/workflows/_reusable-build.yml with: - artifact-name: "python-package-distributions" + artifact-name: python-package-distributions python-version: "3.12" upload-artifacts: true clean-build: true secrets: inherit - github-release: name: Create GitHub Release needs: @@ -85,84 +83,13 @@ jobs: fi echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "tag=v$VERSION" >> "$GITHUB_OUTPUT" - - name: Install git-cliff uses: taiki-e/install-action@331a600f1b10a3fed8dc56f925012bede91ae51f with: tool: git-cliff@2.10.0 - - name: Generate release notes id: release_notes - run: | - # Get the latest release tag (before current) - PREVIOUS_TAG="$(git tag --sort=-version:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+([ab]|rc|-alpha|-beta)?[0-9]*(\.[0-9]+)?$' | head -2 | tail -1 || echo "")" - CURRENT_TAG="${{ steps.version.outputs.tag }}" - VERSION="${{ steps.version.outputs.version }}" - - echo "Previous tag: $PREVIOUS_TAG" - echo "Current tag: $CURRENT_TAG" - - # Generate changelog using git-cliff. - # Note: We use '--strip header' to intentionally remove the changelog header (as defined in cliff.toml), - # so that only the changes section is extracted for embedding in the release notes. - if [ -n "$PREVIOUS_TAG" ] && [ "$PREVIOUS_TAG" != "$CURRENT_TAG" ]; then - # Generate changelog for the range between previous and current tag - git-cliff --strip header --tag "$CURRENT_TAG" "$PREVIOUS_TAG..$CURRENT_TAG" > changelog_section.md - else - # First release - get all commits - git-cliff --strip header --tag "$CURRENT_TAG" > changelog_section.md - fi - - # Build complete release notes with installation instructions - cat > release_notes.md << 'EOF' - ## What's Changed - - EOF - - cat changelog_section.md >> release_notes.md - - cat >> release_notes.md << 'EOF' - - ## Installation - - EOF - - # Add pre-release specific installation instructions - if [[ "$VERSION" == *"a"* ]] || [[ "$VERSION" == *"b"* ]] || [[ "$VERSION" == *"rc"* ]]; then - cat >> release_notes.md << EOF - **This is a pre-release version.** Install with: - \`\`\`bash - pip install --pre code-weaver - # Or specific version: - pip install code-weaver==$VERSION - \`\`\` - EOF - else - cat >> release_notes.md << 'EOF' - Install from PyPI: - ```bash - pip install code-weaver - ``` - EOF - fi - - cat >> release_notes.md << 'EOF' - - Or download the wheel/source distribution from the assets below. - - ## Verification - - All release artifacts are built from source and include: - - 📦 Wheel distribution (.whl) - - 📦 Source distribution (.tar.gz) - - EOF - - if [ -n "$PREVIOUS_TAG" ]; then - echo "**Full Changelog**: https://github.com/knitli/codeweaver/compare/$PREVIOUS_TAG...$CURRENT_TAG" >> release_notes.md - else - echo "**Full Changelog**: https://github.com/knitli/codeweaver/commits/$CURRENT_TAG" >> release_notes.md - fi + run: "# Get the latest release tag (before current)\nPREVIOUS_TAG=\"$(git tag --sort=-version:refname | grep -E '^v[0-9]+\\.[0-9]+\\.[0-9]+([ab]|rc|-alpha|-beta)?[0-9]*(\\.[0-9]+)?$' | head -2 | tail -1 || echo \"\")\"\nCURRENT_TAG=\"${{ steps.version.outputs.tag }}\"\nVERSION=\"${{ steps.version.outputs.version }}\"\n\necho \"Previous tag: $PREVIOUS_TAG\"\necho \"Current tag: $CURRENT_TAG\"\n\n# Generate changelog using git-cliff.\n# Note: We use '--strip header' to intentionally remove the changelog header (as defined in cliff.toml),\n# so that only the changes section is extracted for embedding in the release notes.\nif [ -n \"$PREVIOUS_TAG\" ] && [ \"$PREVIOUS_TAG\" != \"$CURRENT_TAG\" ]; then\n # Generate changelog for the range between previous and current tag\n git-cliff --strip header --tag \"$CURRENT_TAG\" \"$PREVIOUS_TAG..$CURRENT_TAG\" > changelog_section.md\nelse\n # First release - get all commits\n git-cliff --strip header --tag \"$CURRENT_TAG\" > changelog_section.md\nfi\n\n# Build complete release notes with installation instructions\ncat > release_notes.md << 'EOF'\n## What's Changed\n\nEOF\n\ncat changelog_section.md >> release_notes.md\n\ncat >> release_notes.md << 'EOF'\n\n## Installation\n\nEOF\n\n# Add pre-release specific installation instructions\nif [[ \"$VERSION\" == *\"a\"* ]] || [[ \"$VERSION\" == *\"b\"* ]] || [[ \"$VERSION\" == *\"rc\"* ]]; then\n cat >> release_notes.md << EOF\n**This is a pre-release version.** Install with:\n\\`\\`\\`bash\npip install --pre code-weaver\n# Or specific version:\npip install code-weaver==$VERSION\n\\`\\`\\`\nEOF\nelse\n cat >> release_notes.md << 'EOF'\nInstall from PyPI:\n```bash\npip install code-weaver\n```\nEOF\nfi\n\ncat >> release_notes.md << 'EOF'\n\nOr download the wheel/source distribution from the assets below.\n\n## Verification\n\nAll release artifacts are built from source and include:\n- \U0001F4E6 Wheel distribution (.whl)\n- \U0001F4E6 Source distribution (.tar.gz)\n\nEOF\n\nif [ -n \"$PREVIOUS_TAG\" ]; then\n echo \"**Full Changelog**: https://github.com/knitli/codeweaver/compare/$PREVIOUS_TAG...$CURRENT_TAG\" >> release_notes.md\nelse\n echo \"**Full Changelog**: https://github.com/knitli/codeweaver/commits/$CURRENT_TAG\" >> release_notes.md\nfi\n" - name: Create GitHub Release uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe with: @@ -186,7 +113,6 @@ jobs: environment: name: production url: https://pypi.org/p/codeweaver - permissions: id-token: write contents: read diff --git a/.mcp.json b/.mcp.json index a9c745d5c..f54caf66c 100644 --- a/.mcp.json +++ b/.mcp.json @@ -58,4 +58,4 @@ "type": "stdio" } } -} \ No newline at end of file +} diff --git a/.vscode/mcp.json b/.vscode/mcp.json index 07526fa34..213f0db27 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -45,4 +45,4 @@ } } } -} \ No newline at end of file +} diff --git a/context7.json b/context7.json index bb1dc4546..bf553d9d9 100644 --- a/context7.json +++ b/context7.json @@ -35,4 +35,4 @@ "CodeWeaver supports multiple AI Agents and vector models, allowing you to choose the best fit for your use case.", "Focus on types. CodeWeaver has a strong type system that ensures all components are type-safe and can be easily extended." ] -} \ No newline at end of file +} diff --git a/data/node_types/hcl-node-types.json b/data/node_types/hcl-node-types.json index 1443092c1..908719129 100644 --- a/data/node_types/hcl-node-types.json +++ b/data/node_types/hcl-node-types.json @@ -1286,4 +1286,4 @@ "type": "}", "named": false } -] \ No newline at end of file +] diff --git a/docker-compose.yml b/docker-compose.yml index bc801cacf..ffb345b70 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,7 +25,6 @@ services: restart: unless-stopped networks: - codeweaver-network - # CodeWeaver MCP Server # Semantic code search and context delivery for AI agents codeweaver: @@ -34,21 +33,26 @@ services: dockerfile: Dockerfile container_name: codeweaver-server # Use HTTP transport for persistent service mode (daemon not needed in Docker) - command: ["codeweaver", "server", "--host", "0.0.0.0", "--port", "9328", "--transport", "streamable-http"] + command: + - codeweaver + - server + - --host + - 0.0.0.0 + - --port + - "9328" + - --transport + - streamable-http ports: - ${CODEWEAVER_PORT:-9328}:9328 volumes: # Mount your codebase to be indexed (read-only) # Changes on host are immediately visible in container - ${PROJECT_PATH:-.}:/workspace:ro - # Persistent storage for checkpoints, config, and secrets # CRITICAL: This must persist between restarts for index checkpoints - codeweaver_config:/app/config - # Application data and cache - codeweaver_data:/app/data - # Note: Config auto-discovery # CodeWeaver finds codeweaver.toml in your project root automatically. # Just add it to PROJECT_PATH - no explicit mount needed. @@ -62,7 +66,6 @@ services: # - quickstart: FastEmbed/Sentence Transformers (free, local) # - backup: Lightest local models + in-memory vectors - CODEWEAVER_PROFILE=${CODEWEAVER_PROFILE:-quickstart} - # Vector store deployment type - CODEWEAVER_VECTOR_DEPLOYMENT=${VECTOR_DEPLOYMENT:-local} # For cloud deployment, set CODEWEAVER_VECTOR_URL @@ -74,19 +77,15 @@ services: - CODEWEAVER_PROJECT_PATH=/workspace - CODEWEAVER_TOKEN_LIMIT=${TOKEN_LIMIT:-30000} - CODEWEAVER_PROJECT_NAME=${PROJECT_NAME} - # Redirect user config to mounted volume for checkpoint persistence - XDG_CONFIG_HOME=/app/config - # ================================================================= # API Keys (Pass through to providers) # ================================================================= # Required for 'recommended' profile - VOYAGE_API_KEY=${VOYAGE_API_KEY:-} - # Required for agent functionality (future feature) - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-} - # Optional: Alternative providers # - OPENAI_API_KEY=${OPENAI_API_KEY:-} # - COHERE_API_KEY=${COHERE_API_KEY:-} @@ -119,7 +118,6 @@ services: memory: 4G reservations: memory: 2G - volumes: # Persistent storage for Qdrant vector database qdrant_storage: @@ -130,7 +128,6 @@ volumes: # CodeWeaver application data codeweaver_data: driver: local - networks: codeweaver-network: driver: bridge diff --git a/docs-site/.vscode/extensions.json b/docs-site/.vscode/extensions.json index 22a15055d..2d7e89370 100644 --- a/docs-site/.vscode/extensions.json +++ b/docs-site/.vscode/extensions.json @@ -1,4 +1,6 @@ { - "recommendations": ["astro-build.astro-vscode"], + "recommendations": [ + "astro-build.astro-vscode" + ], "unwantedRecommendations": [] } diff --git a/docs-site/package.json b/docs-site/package.json index c9e56b0e5..a08508672 100644 --- a/docs-site/package.json +++ b/docs-site/package.json @@ -31,4 +31,4 @@ }, "type": "module", "version": "0.0.1" -} \ No newline at end of file +} diff --git a/docs-site/tsconfig.json b/docs-site/tsconfig.json index 9202d199e..f36744a58 100644 --- a/docs-site/tsconfig.json +++ b/docs-site/tsconfig.json @@ -15,4 +15,4 @@ ] } } -} \ No newline at end of file +} diff --git a/schema/v1.0.0/codeweaver.schema.json b/schema/v1.0.0/codeweaver.schema.json index e858f0d4e..b4bc64cb4 100644 --- a/schema/v1.0.0/codeweaver.schema.json +++ b/schema/v1.0.0/codeweaver.schema.json @@ -2518,4 +2518,4 @@ }, "title": "CodeWeaver Settings", "type": "object" -} \ No newline at end of file +} diff --git a/scripts/build/generate-docker-server-yaml.py b/scripts/build/generate-docker-server-yaml.py index 6ae2991fa..2dc94c30d 100755 --- a/scripts/build/generate-docker-server-yaml.py +++ b/scripts/build/generate-docker-server-yaml.py @@ -163,7 +163,11 @@ def generate_server_yaml() -> dict: }, "about": { "title": "CodeWeaver - Code Search for AI Agents", - "description": f"Semantic code search built for AI agents. Hybrid AST-aware context for {len(_languages())} languages with intelligent chunking, intent detection, and multi-provider support.", + "description": f"""Semantic code search for AI agents. Hybrid search across {len(_languages())} languages with {len(embedding_providers())} embedding providers. + + Works offline with local models or cloud providers (Voyage, OpenAI, Cohere, etc.). AST-aware parsing, intelligent chunking, automatic failover. + + One simple MCP tool for any client.""", "icon": "https://github.com/knitli/codeweaver/raw/refs/heads/main/docs/assets/codeweaver-favico.png", }, "source": { diff --git a/scripts/build/generate-mcp-server-json.py b/scripts/build/generate-mcp-server-json.py index 24d48ceca..a7ca95832 100755 --- a/scripts/build/generate-mcp-server-json.py +++ b/scripts/build/generate-mcp-server-json.py @@ -848,7 +848,7 @@ def generate_server_detail() -> ServerDetail: return ServerDetail( field_schema=AnyUrl("https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json"), name="com.knitli/codeweaver", - description=f"Semantic code search built for AI agents. Hybrid, AST-aware, context for {len(_languages())} languages.", + description=f"Semantic code search for AI agents. Hybrid search, {len(_languages())} languages, {len([provider for provider in Provider if provider != Provider.NOT_SET])} providers.", title="CodeWeaver - Code Search for AI Agents", version=__version__, repository=REPOSITORY, diff --git a/server.yaml b/server.yaml index 27cbd612b..a67ab5425 100644 --- a/server.yaml +++ b/server.yaml @@ -7,31 +7,31 @@ name: codeweaver image: mcp/codeweaver type: server meta: - category: devops - tags: - - devops + category: devops + tags: + - devops about: - title: Codeweaver - Code Search for AI Agents - description: Semantic code search built for AI agents. Advanced hybrid, AST-aware context curation for 166 languages with intelligent chunking, intent detection, and multi-provider support. Deploy CodeWeaver locally or as a service, in the cloud or completely local. Use with any IDE or AI client. CodeWeaver gives your AI agents one great tool to get the information they need. - icon: https://github.com/knitli/codeweaver/raw/refs/heads/main/docs/assets/codeweaver-favico.png + title: Codeweaver - Code Search for AI Agents + description: Semantic code search built for AI agents. Advanced hybrid, AST-aware context curation for 166 languages with intelligent chunking, intent detection, and multi-provider support. Deploy CodeWeaver locally or as a service, in the cloud or completely local. Use with any IDE or AI client. CodeWeaver gives your AI agents one great tool to get the information they need. + icon: https://github.com/knitli/codeweaver/raw/refs/heads/main/docs/assets/codeweaver-favico.png source: - project: https://github.com/knitli/codeweaver - commit: b2809e688507c84d20867de1199d00d037ca859a + project: https://github.com/knitli/codeweaver + commit: b2809e688507c84d20867de1199d00d037ca859a run: - volumes: - - /data:{{codeweaver.data}} + volumes: + - /data:{{codeweaver.data}} config: - description: Configure the connection to Codeweaver - Code Search for AI Agents - secrets: - - name: codeweaver.foo - env: bar - example: - env: - - name: foo - example: value - value: '{{value}}' - parameters: - type: object - properties: - foo: - type: string + description: Configure the connection to Codeweaver - Code Search for AI Agents + secrets: + - name: codeweaver.foo + env: bar + example: + env: + - name: foo + example: value + value: '{{value}}' + parameters: + type: object + properties: + foo: + type: string From f2a85706f131944d6753d6432a4bc7d0c9c9ce76 Mon Sep 17 00:00:00 2001 From: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> Date: Mon, 8 Dec 2025 22:09:44 -0500 Subject: [PATCH 31/37] Fix: Mise configuration changes causing blocking CI failures (#196) Short version: avoid debugging GH actions at all costs. * initial test * fix: enhance CI configuration with Python version support and Mise environment setup * fix: enhance CI configuration with Python version support and Mise environment setup * fix: prevent CI skip by disabling env export in mise-action The mise-action was hanging during the "Exporting mise environment variables" step, causing the runner to kill the job with exit code 137 (SIGKILL). This was happening because: 1. The mise.toml has complex template expressions with command executions 2. The hooks.enter configuration may execute during env export 3. The env export was timing out after ~1 minute Solution: Disable the env parameter (set to false) in all mise-action configurations to prevent the hanging during environment variable export. Fixes #issue * fix: remove unnecessary echo statement for Mise Python version in CI setup * fix: add mise bin directory to PATH for proper command execution The previous fix disabled env export to prevent hanging, but this caused MISE_PATH to be empty because mise wasn't in the PATH. Changes: 1. Add mise bin directory to PATH in "Set outputs" step 2. Add mise bin directory to PATH in workflow steps that execute mise commands This ensures: - The "Set outputs" step can find mise using which - Workflow steps can execute mise commands via $MISE_PATH - No hanging during env export (env: false is still set) Fixes the "command not found" error when running mise commands. * fix: add outputs section to composite action for proper output exposure The composite action was setting outputs in the mise-outputs step, but those outputs were not being exposed by the action itself because the action.yml was missing the outputs section. Changes: - Add outputs section to action.yml defining all output variables - Map outputs from mise-outputs step to action outputs This ensures that steps.setup-mise.outputs.MISE_PATH and other outputs are properly available to the calling workflow. Related to the CI skip issue fix. * fix: missing tools on path * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> * Initial plan * fix: remove duplicate Setup Mise Environment step Remove the first instance of "Setup Mise Environment" step (lines 52-58) as it was a duplicate. The second instance with id: setup-mise is kept since it's needed for the Verify Mise Setup step that references it. Co-authored-by: bashandbone <89049923+bashandbone@users.noreply.github.com> * fix: update PATH in workflows and improve MCP Registry submission process * fix: removed 'frozen' flag from uv for experimental python installs. This was preventing package resolution * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> * fix: remove mcp-publisher CLI installation step and streamline submission process * fix: enhance CI workflows by adding outputs and environment variables for better integration and testing * fix: update Python setup action to include outputs for PYTHON_PATH and UV_PATH * fix: update CI workflows to ensure uv is available when called * fix: add tools to mise check task for CI availability * fix: add setup step for mise dev installs in CI * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> * fix: improve CI workflows by setting PATH for mise and cleaning up temporary files * Update .github/workflows/mcp-registry-submit.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> * fix: enhance CI workflows by adding NEWPATH output and updating PATH setup * fix: update cloud setup task environment variables for consistency * fix: enhance setup and testing workflows by adding missing dependencies and adjusting environment variables * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> * fix: improve cleanup process in MCP Registry submission workflow * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> * fix: add missing outputs to actions and simplify CI workflows Root cause: Custom actions were missing `outputs:` sections in action.yml. Steps wrote to $GITHUB_OUTPUT but outputs weren't exposed to callers. Changes: - Add outputs section to setup-mise-env action (MISE_PATH, PYTHON_PATH, etc.) - Add outputs section to setup-python-env action (PYTHON_PATH, UV_PATH) - Add outputs section to setup-uv-env action (uv-path, uvx-path) - Simplify reusable workflows to use `mise run task` directly - Remove complex $MISE_PATH variable usage (mise is already in PATH) - Fix experimental Python tests: remove --frozen flag that fails with prereleases - Clean up redundant env vars (move to job level) - Remove duplicate disk space cleanup in copilot-setup-steps * Fix: Add missing 'runs-on' specification for lint job Signed-off-by: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> * fix: update CI workflows and environment setup for consistency * fix: fixed a malformed block in setup-mise-env, consolidated freethreaded handling into normal workflows using environment variables * fix: update hk installation in CI setup and add HK_MISE environment variable * fix: update CI workflows and environment setup, remove unused variables * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> * fix: update setup-mise-env and reusable-lint workflows for consistency and improved output handling * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> * fix: streamline mise configuration by consolidating tool definitions and removing unused venv creation arguments * fix: incorrect-tool references in mise.toml * fix: add mise-debug input for enhanced output in setup-mise-env action and update reusable test workflow * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> * Change condition for inserting Free-Threaded Python correct use of nonexistent 'matches' Signed-off-by: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> * Add MISE_PYTHON_COMPILE variable to action.yml Signed-off-by: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> * fix: remove minor version check for uv * fix: update dependency references for type checking to use latest version * fix: streamline system dependency installation and update free-threaded Python setup * fix: add command to list available Python versions in mise setup * fix: correct command for creating virtual environment in mise.toml * fix: remove compile settings to experiment if cause of a CI failure. * fix: refine free-threaded Python installation process in CI setup * fix: enhance cloud setup script with informative messages and streamline mise commands * fix: add message to ensure Python version is installed during cloud setup * fix: improve cloud setup script to ensure mise activation and Python version installation * fix: enhance cloud setup with mise doctor output and ensure latest uv installation * fix: enhance cloud setup with improved mise activation and debugging output * fix: remove auto-venv to debug CI issue * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> --------- Signed-off-by: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> Co-authored-by: Claude Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- .github/workflows/mcp-registry-submit.yml | 49 +++++++++------ .github/workflows/stale.yml | 30 ++++++++++ .vscode/terminal.extra.zsh | 14 ++--- .vscode/zsh/.zshrc | 42 +++++++++++-- mise.toml | 73 +++++++++++------------ scripts/dev-env/dev-shell-init.zsh | 19 +----- 6 files changed, 142 insertions(+), 85 deletions(-) create mode 100644 .github/workflows/stale.yml diff --git a/.github/workflows/mcp-registry-submit.yml b/.github/workflows/mcp-registry-submit.yml index 54097162d..90144084e 100644 --- a/.github/workflows/mcp-registry-submit.yml +++ b/.github/workflows/mcp-registry-submit.yml @@ -28,7 +28,8 @@ jobs: with: ref: ${{ github.event.release.tag_name || github.ref }} fetch-depth: 1 - - name: Install uv + + - name: Setup UV and Python environment uses: ./.github/actions/setup-uv-env with: python-version: "3.12" @@ -58,6 +59,7 @@ jobs: # Run the generation script (uses PEP 723 dependencies) uv run scripts/build/generate-mcp-server-json.py echo "✓ server.json regenerated" + - name: Wait for PyPI package availability run: | VERSION="${{ steps.version.outputs.version }}" @@ -106,6 +108,7 @@ jobs: echo "ERROR: Package not available on PyPI after ${MAX_ATTEMPTS} attempts" echo "Please verify that the package was successfully published" exit 1 + - name: Submit to MCP Registry env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -119,34 +122,34 @@ jobs: # we need to sign the timestamp with the private key, which is in MCP_REGISTRY_KEY # MCP_REGISTRY_KEY is expected to be in PEM format, and it uses ed25519 algorithm # unfortunately, pkeyutl can only work with files because of how it has to buffer the input for signing - indata="" - key_file="" - outfile="" - request_body="" + + # setup temporary files and cleanup + umask 077 + indata="$(mktemp)" + key_file="$(mktemp)" + outfile="$(mktemp)" + request_body="$(mktemp)" # shellcheck disable=SC2329 cleanup() { + # ensure sensitive files are securely deleted -- on ubuntu, shred should be available shred -u "$key_file" "$outfile" || rm -f "$key_file" "$outfile" - # these aren't sensitive, but let's keep things tidy + # these aren't sensitive, but let's keep things tidy: rm -f "$indata" "$request_body" } trap cleanup EXIT domain="knitli.com" - indata="$(mktemp)" - echo -n "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" > "$indata" + timestamp="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" + echo -n "$timestamp" > "$indata" - key_file="$(mktemp)" echo "$MCP_REGISTRY_KEY" > "$key_file" - chmod 600 "$key_file" - outfile="$(mktemp)" - - openssl pkeyutl -sign -inkey "$key_file" -out "$outfile" -rawin -in "$indata" + # it wasn't easy to figure out how to do ed25519 signing with openssl, but this is the correct incantation with recent versions: + openssl pkeyutl -sign -inkey "$key_file" -out "$outfile" -rawin -in "$indata" || { echo "ERROR: Failed to sign timestamp"; exit 1; } signed_timestamp_hex="$(xxd -p "$outfile" | tr -d '\n')" - request_body="$(mktemp)" { echo -n '{"domain":"'"$domain"'",' echo -n '"signed_timestamp":"'"$signed_timestamp_hex"'",' - echo -n '"timestamp":"'"$(cat "$indata")"'"}' + echo -n '"timestamp":"'"$timestamp"'"}' } > "$request_body" echo "Logging in to MCP Registry..." response="$(curl --request POST \ @@ -161,22 +164,32 @@ jobs: exit 1 fi echo "✓ Obtained registry token" - export MCP_LOGIN_TOKEN="$token" publication_response="$(curl --request POST \ --url https://registry.modelcontextprotocol.io/v0.1/publish \ --header 'Accept: application/json, application/problem+json' \ - --header "Authorization: Bearer $MCP_LOGIN_TOKEN" \ + --header "Authorization: Bearer $token" \ --header 'Content-Type: application/json' \ --data @./server.json)" echo "Publication response: $publication_response" publication_time="$(echo "$publication_response" | jq -r '._meta."io.modelcontextprotocol.registry/official".published_at')" if [ -z "$publication_time" ] || [ "$publication_time" = "null" ]; then echo "ERROR: Submission to MCP Registry failed" + # Try to extract error details from the publication response + error_message="$(echo "$publication_response" | jq -r '.error // .message // .detail // empty')" + validation_errors="$(echo "$publication_response" | jq -r '.validation_errors // empty')" + if [ -n "$error_message" ]; then + echo "Error message: $error_message" + fi + if [ -n "$validation_errors" ] && [ "$validation_errors" != "null" ]; then + echo "Validation errors: $validation_errors" + fi + echo "Raw response: $publication_response" exit 1 fi echo "✓ Successfully submitted to MCP Registry at $publication_time" echo "MCP_PUBLICATION_TIME=$publication_time" >> "$GITHUB_OUTPUT" exit 0 + - name: Summary of MCP Registry Submission if: always() run: | @@ -185,7 +198,7 @@ jobs: echo "" echo "- **Version**: ${{ steps.version.outputs.version }}" echo "- **Status**: ${{ job.status }}" - if [ -n "$MCP_PUBLICATION_TIME" ]; then + if [ -n "${{ steps.submit-mcp.outputs.MCP_PUBLICATION_TIME }}" ]; then echo "- **Published At**: ${{ steps.submit-mcp.outputs.MCP_PUBLICATION_TIME }}" fi echo "" diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000..b9793da1c --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: 2025 Knitli Inc. +# SPDX-FileContributor: Adam Poulemanos +# +# SPDX-License-Identifier: MIT OR Apache-2.0 + +name: "Stale Issues and PRs" +on: + schedule: + - cron: "0 11 * * *" + +permissions: + issues: write + pull-requests: write + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d + with: + days-before-stale: 7 + days-before-close: 3 + + any-of-issue-labels: "question,more info" + stale-issue-message: "This issue is stale, we will close it in 3 days if you don't reply." + close-issue-message: "This issue hasn't been active for 10 days, closing." + + any-of-pr-labels: "needs revision" + stale-pr-message: "This PR is stale, we will close it in 3 days if you don't reply." + close-pr-message: "This PR hasn't been active for 10 days, closing." diff --git a/.vscode/terminal.extra.zsh b/.vscode/terminal.extra.zsh index 97524ea97..7260c7eec 100755 --- a/.vscode/terminal.extra.zsh +++ b/.vscode/terminal.extra.zsh @@ -14,23 +14,23 @@ full_setup() { print -P "%F{209}[codeweaver]%f %F{red}Failed to trust the Mise environment!%f" } mise run setup - # Skip 'mise run setup' to avoid circular activation } # Make sure Mise is installed and available print -P "%F{209}[codeweaver]%f Welcome to the%f %F{209}CodeWeaver%f development environment!" print -P "%F{209}[codeweaver]%f If you run into any issues, please visit %F{cyan}https://github.com/knitli/codeweaver/issues%f for assistance." -print -P "%F{209}[codeweaver]%f We're going to set up your environment now..." -REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD")" -cd "$REPO_ROOT" || { - print -P "%F{209}[codeweaver]%f %F{red}Failed to change directory to the repository root!%f" -} + +# set a few aliases for convenience +alias mx='mise exec' +alias mr='mise run' + if command -v mise >/dev/null 2>&1; then # We have Mise installed, check if it's initialized for this workspace print -P "%F{209}[codeweaver]%f You're all set!" else + REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo "${PWD}")" # Mise is not installed, run the install script - chmod -R +x "${REPO_ROOT}/scripts" || { + chmod -R +x "${REPO_ROOT}/scripts" || { print -P "%F{209}[codeweaver]%f %F{red}Failed to make install scripts executable!%f" } print -P "%F{209}[codeweaver]%f Installing Mise for the first time..." diff --git a/.vscode/zsh/.zshrc b/.vscode/zsh/.zshrc index 340ba7799..1a2c10edb 100644 --- a/.vscode/zsh/.zshrc +++ b/.vscode/zsh/.zshrc @@ -6,6 +6,27 @@ # VS Code workspace-local zshrc # Loaded because VS Code sets ZDOTDIR to this directory for integrated terminals. +typeset -i _mise_updated + +# replace default mise hook +function _mise_hook { + local diff=${__MISE_DIFF} + source <(command mise hook-env -s zsh) + [[ ${diff} == ${__MISE_DIFF} ]] + _mise_updated=$? +} + +_PROMPT="❱ " # or _PROMPT=${PROMPT} to keep the default + +function _prompt { + if (( ${_mise_updated} )); then + PROMPT='%F{blue}${_PROMPT}%f' + else + PROMPT='%(?.%F{green}${_PROMPT}%f.%F{red}${_PROMPT}%f)' + fi +} + + # Source repo dev shell init (idempotent), then hand off to user's normal zshrc. REPO_ROOT="${PWD}" if [[ -n "${VSCODE_CWD:-}" ]]; then @@ -17,6 +38,17 @@ if [[ -f "${REPO_ROOT}/scripts/dev-env/dev-shell-init.zsh" ]]; then source "${REPO_ROOT}/scripts/dev-env/dev-shell-init.zsh" fi +if [[ -f "${ZDOTDIR:-${HOME}}/.vscode/terminal.extra.zsh" ]]; then + source "${ZDOTDIR:-${HOME}}/.vscode/terminal.extra.zsh" +fi + +# PERSONALIZE: You can add more *local* customizations in .vscode/terminal.local.zsh +# (This file is gitignored so it won't be checked in.) +if [[ -f "${ZDOTDIR:-${HOME}}/.vscode/terminal.local.zsh" ]]; then + source "${ZDOTDIR:-${HOME}}/.vscode/terminal.local.zsh" +fi + + # After workspace init, source the user's real ~/.zshrc if it exists if [[ -f "${HOME}/.zshrc" ]]; then # Avoid infinite loop if ZDOTDIR points here @@ -27,8 +59,8 @@ if [[ -f "${HOME}/.zshrc" ]]; then source "${HOME}/.zshrc" fi -# PERSONALIZE: You can add more *local* customizations in .vscode/terminal.local.zsh -# (This file is gitignored so it won't be checked in.) -if [[ -f "${ZDOTDIR:-${HOME}}/.vscode/terminal.local.zsh" ]]; then - source "${ZDOTDIR:-${HOME}}/.vscode/terminal.local.zsh" -fi \ No newline at end of file +autoload -Uz add-zsh-hook 2>/dev/null +if (( $+functions[add-zsh-hook] )); then + add-zsh-hook precmd _prompt +fi + diff --git a/mise.toml b/mise.toml index 993b21704..b3a6e8408 100644 --- a/mise.toml +++ b/mise.toml @@ -28,24 +28,26 @@ _.path = [ ] MISE_ENV = '''{% if get_env(name="CI", default="") == "true" %}""{% else %}dev{% endif %}''' CW_PREFIX = '''{% if get_env(name="CI", default="") == "true" %}[codeweaver]{% elif get_env(name="TERM", default="xterm-256color") == "*truecolor*" %}\033[38;2;181;108;48;m[codeweaver]\033[0m{% else %}\033[38;5;131m[codeweaver]\033[0m{% endif %}''' +# this is a bit redundant but it makes it easier to override in CI environments +MISE_PYTHON_VERSION = '''{{ get_env(name="MISE_PYTHON_VERSION", default="3.13") }}''' +HK_MISE = "1" [settings] idiomatic_version_file_enable_tools = [] experimental = true [settings.python] -compile = true -uv_venv_create_args = [ - "--seed", - "--keyring-provider", - "subprocess", - "--python", - "{{ get_env(name='MISE_PYTHON', default='3.13') }}", -] [tools] # most tools are defined in mise.dev.toml to keep CI workflows lightweight -python = "3.13" +# Where needed, tools are defined at the task level here, which allows for more granular control +python = '''{{ get_env(name="MISE_PYTHON_VERSION", default="3.13") }}''' +uv = "latest" + +# Quick note for those unfamiliar with mise: +# - despite the namespace, tools prefixed with "pipx:" are installed via "uv" (I assume once upon a time they were installed with pipx before uv came along) +# - some tools have no prefix -- these are defined in mise's internal registry directly +# - The prefix refers to the "backend" used to install the tool -- most of the no-prefix tools are installed through `aqua` or github/gitlab releases, and failing that, `ubi`. Language-specific package managers (cargo/npm/go) are also used where appropriate. [tasks.info] description = "Print project information" @@ -82,7 +84,11 @@ run = ''' # Source the virtual environment echo "${CW_PREFIX} Sourcing virtual environment..." git rev-parse --show-toplevel || true -source .venv/bin/activate +if [ ! -d ".venv" ]; then + echo "${CW_PREFIX} ⚠️ \033[0;31mVirtual environment not found. We'll try to proceed, but things may break!\033[0m" + uv venv --seed --keyring-provider subprocess --python {{ get_env(name='MISE_PYTHON_VERSION', default='3.13') }} .venv +fi +. .venv/bin/activate || echo "${CW_PREFIX} ⚠️ \033[0;31mFailed to source virtual environment.\033[0m" ''' run_windows = ''' echo [codeweaver] Sourcing virtual environment... @@ -95,12 +101,10 @@ git rev-parse --show-toplevel run = [ "echo \"${CW_PREFIX} Setting up development environment for the first time...\"", "mise set &>/dev/null", - "mise trust &>/dev/null", - "mise install &>/dev/null || true", - "hk install --mise &>/dev/null || true", + "mise trust --all &>/dev/null", + "mise install &>/dev/null || echo \"${CW_PREFIX} ⚠️ WARNING: mise install failed during setup.\"", "mise //:venv &>/dev/null", "mise //:sync &>/dev/null", - "mise env &>/dev/null || true", "git config include.path .gitconfig &>/dev/null || true", "echo 'src/codeweaver/_version.py' >> .git/info/exclude", "echo 'coverage.xml' >> .git/info/exclude", @@ -111,12 +115,10 @@ run = [ run_windows = [ "echo [codeweaver] Setting up development environment for the first time...", "mise set > NUL 2>&1", - "mise trust > NUL 2>&1", - "mise install > NUL 2>&1 || echo.", - "hk install --mise > NUL 2>&1 || echo.", + "mise trust --all > NUL 2>&1", + "mise install > NUL 2>&1 || echo \"[codeweaver] WARNING: mise install failed during setup.\"", "mise //:venv > NUL 2>&1", "mise //:sync > NUL 2>&1", - "mise env > NUL 2>&1", "git config include.path .gitconfig > NUL 2>&1", "echo src/codeweaver/_version.py >> .git\\info\\exclude", "echo coverage.xml >> .git\\info\\exclude", @@ -210,7 +212,7 @@ hk check tools.uv = "latest" run = ''' echo "Running linting checks..." -if [[ "$CI" == "true" ]]; then +if [ "$CI" = "true" ]; then uv run ruff check . --output-format=github else uv run ruff check . @@ -236,11 +238,10 @@ hk fix ''' [tasks.format.tools] -"cargo:rust-parallel" = "latest" -"cargo:typos" = "latest" +"aqua:typos" = "latest" "pipx:reuse" = "latest" -"pipx:tombi" = "latest" -"pipx:ty" = "latest" +"aqua:tombi-toml/tombi" = "latest" +"ubi:astral-sh/ty" = "latest" actionlint = "latest" hk = "latest" pkl = "latest" @@ -254,13 +255,10 @@ echo "${CW_PREFIX} Applying formatting...hold onto your butts 🚬...🦖 " # mise -y //:fix-ruff-patterns || { # there are some bugs to work out # echo "${CW_PREFIX} ⚠️ \033[0;32mRuff pattern fixer encountered issues.\033[0m"} uv run ruff format . -files="$(git ls-files "*.yaml" "*.yml" "*.json" 2>/dev/null)" -if [ -n "$files" ]; then - echo "$files" | rust-parallel -d stderr -i - --no-run-if-empty yq -iP --indent=2 "." || true -fi -files="$(git ls-files "*.toml" 2>/dev/null)" -if [ -n "$files" ]; then - tombi format $files 2>/dev/null || true +git ls-files "*.yaml" "*.yml" "*.json" 2>/dev/null | xargs -r yq -iP --indent=2 eval "." 2>/dev/null || true +toml_files="$(git ls-files "*.toml" 2>/dev/null)" +if [ -n "$toml_files" ]; then + printf '%s\n' "$toml_files" | xargs -r tombi format 2>/dev/null || true fi ''' run_windows = ''' @@ -275,9 +273,8 @@ for /f "delims=" %%f in ('git ls-files "*.toml" 2^>NUL') do ( ''' [tasks.format-fix.tools] -"cargo:rust-parallel" = "latest" -"pipx:tombi" = "latest" -"yq" = "latest" +"aqua:tombi-toml/tombi" = "latest" +yq = "latest" uv = "latest" [tasks.graph] @@ -296,15 +293,15 @@ env.MISE_EXPERIMENTAL = "1" tools.uv = "latest" run = ''' echo "${CW_PREFIX} Setting up Python virtual environment..." -uv venv --allow-existing --seed --keyring-provider subprocess --python 3.13 --refresh .venv && +uv venv --allow-existing --seed --keyring-provider subprocess --python {{ get_env(name="MISE_PYTHON_VERSION", default="3.13") }} --refresh .venv && echo "${CW_PREFIX} ✅ Virtual environment is ready!" -source .venv/bin/activate || echo "${CW_PREFIX} ⚠️ \033[0;31mFailed to source virtual environment.\033[0m" +. .venv/bin/activate || echo "${CW_PREFIX} ⚠️ \033[0;31mFailed to source virtual environment.\033[0m" echo "${CW_PREFIX} Running dependency sync..." mise //:sync & ''' run_windows = ''' echo [codeweaver] Setting up Python virtual environment... -uv venv --allow-existing --seed --keyring-provider subprocess --python 3.13 --refresh .venv +uv venv --allow-existing --seed --keyring-provider subprocess --python {{ get_env(name="MISE_PYTHON_VERSION", default="3.13") }} --refresh .venv if %ERRORLEVEL% equ 0 ( echo [codeweaver] Virtual environment is ready! call .venv\Scripts\activate.bat @@ -318,7 +315,7 @@ start /b mise //:sync [tasks.test] # Testing -tools.python = '''{{ get_env(name="MISE_PYTHON", default="3.13") }}''' +tools.python = '''{{ get_env(name="MISE_PYTHON_VERSION", default="3.13") }}''' tools.uv = "latest" usage = ''' args "[dir]" help="directory to test" default="tests/" @@ -335,7 +332,7 @@ uv run pytest "%usage_dir%" %usage_marks:~-3% -v %* [tasks.test-cov] tools.uv = "latest" -tools.python = '''{{ get_env(name="MISE_PYTHON", default="3.13") }}''' +tools.python = '''{{ get_env(name="MISE_PYTHON_VERSION", default="3.13") }}''' run = ''' echo "${CW_PREFIX} Running tests with coverage..." uv sync --group test --inexact diff --git a/scripts/dev-env/dev-shell-init.zsh b/scripts/dev-env/dev-shell-init.zsh index ef978827e..f2174345a 100755 --- a/scripts/dev-env/dev-shell-init.zsh +++ b/scripts/dev-env/dev-shell-init.zsh @@ -18,7 +18,7 @@ __cw_this_file="${(%):-%N}" __cw_dirname() { builtin cd -- "${1%/*}" 2>/dev/null && pwd; } __cw_script_dir="$(__cw_dirname "$__cw_this_file")" -# Compute repo root (script lives in /scripts) +# Compute repo root (script lives in /scripts/dev-env/) if [[ -n "$__cw_script_dir" ]]; then REPO_ROOT="$(builtin cd "${__cw_script_dir}/../.." 2>/dev/null && pwd)" fi @@ -44,19 +44,4 @@ if [[ ! -f "${_cw_venv_activate}" && -z "${CODEWEAVER_SILENT_SHELL:-}" ]]; then print -P "%F{yellow}[codeweaver]%f Create it with: %F{green}mise run venv "$REPO_ROOT/.venv" %f" fi -# Source optional workspace extras if provided -_cw_extras="${REPO_ROOT}/.vscode/terminal.extra.zsh" -if [[ -f "${_cw_extras}" ]]; then - # shellcheck disable=SC1090 - source "${_cw_extras}" -fi - -# source optional (per-user) local workspace extras if provided -# this file is gitignored -_cw_local="${REPO_ROOT}/.vscode/terminal.local.zsh" -if [[ -f "${_cw_local}" ]]; then - # shellcheck disable=SC1090 - source "${_cw_local}" -fi - -unset __cw_this_file __cw_script_dir _cw_repo_venv_path _cw_venv_activate _cw_extras +unset __cw_this_file __cw_script_dir _cw_repo_venv_path _cw_venv_activate From 242972779371c0d23b0056811b45af31d5cc939c Mon Sep 17 00:00:00 2001 From: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> Date: Mon, 8 Dec 2025 22:25:22 -0500 Subject: [PATCH 32/37] feat: implement connection pooling for supported providers (#194) * docs: add connection pooling implementation plan Create detailed implementation plan for HTTP client connection pooling based on the client-pooling-analysis document. The plan outlines: - Phase 1: Infrastructure (HttpClientPool class, config settings, state integration) - Phase 2: Provider integration (Voyage AI, Cohere, Qdrant considerations) - Testing strategy and rollback plan - Provider-specific connection pool settings This addresses connection overhead, potential exhaustion during high-load indexing, and Qdrant timeout issues (httpcore.ReadError). * feat: implement HTTP client connection pooling for providers Add centralized HTTP client pooling infrastructure to improve connection reuse across Voyage AI and Cohere providers during high-load operations. Key changes: - Create HttpClientPool class with singleton pattern for application-wide connection reuse (src/codeweaver/common/http_pool.py) - Integrate http_pool into CodeWeaverState with proper cleanup on shutdown - Update ProviderRegistry to inject pooled httpx clients for Voyage/Cohere - Add provider-specific settings (50 max connections, 90s read timeout) - Add comprehensive unit tests for HttpClientPool This addresses: - Connection overhead during batch embedding operations - Potential connection exhaustion during large indexing jobs - Improved reliability with HTTP/2 support for better multiplexing The implementation includes graceful fallbacks - if pooling fails, providers will create their own clients as before. * refactor: address PR #194 review suggestions for HTTP connection pooling - Add thread safety via asyncio.Lock for coroutine-safe client creation - Make reset_http_pool() async to properly close clients before reset - Add reset_http_pool_sync() for testing fixtures without cleanup - Unify dual singleton pattern (remove redundant module-level instance) - Change INFO logging to DEBUG for client creation (reduce noise) - Narrow exception types (httpx.HTTPError, OSError) instead of Exception - Merge duplicate VOYAGE/COHERE conditionals using _POOLED_HTTP_PROVIDERS - Use get_client_sync() in provider registry for sync context - Add comprehensive tests: - Test override settings are actually applied to clients - Test aclose() error handling (graceful degradation) - Test PoolTimeouts immutability - Test concurrent get_client calls (thread safety) - Test _get_pooled_httpx_client for various providers - Update implementation plan status to Implemented - Export reset_http_pool_sync in common/__init__.py * feat: expand HTTP connection pooling to OpenAI-compatible and Mistral providers Extend connection pooling support to 11 providers: - OpenAI-compatible (8): OpenAI, Azure, Fireworks, Groq, Together, Ollama, Cerebras, Heroku - Voyage and Cohere (existing) - Mistral (new) Changes: - OpenAI factory: Accept http_client parameter for AsyncOpenAI - Mistral provider: Accept httpx_client mapped to async_client - Provider registry: Convert _POOLED_HTTP_PROVIDERS to dict mapping provider -> param name - Registry _instantiate_client: Use provider-specific parameter names - Tests: Add tests for OpenAI, Mistral pooling; update provider mapping tests - Docs: Update implementation plan with full provider table Provider parameter mapping: - httpx_client: Voyage, Cohere, Mistral - http_client: OpenAI, Azure, Fireworks, Groq, Together, Ollama, Cerebras, Heroku Not supported (no httpx injection): - Bedrock (uses boto3) - HuggingFace (global factory pattern) - Google GenAI (args only, not full client) * fix: add thread safety to singleton and sync client creation Address Copilot review suggestions from PR #194: 1. get_instance() race condition: Add double-checked locking with threading.Lock to prevent multiple singleton instances when called concurrently from different threads 2. get_client_sync() race condition: Add thread-safe double-checked locking pattern using _sync_lock to prevent duplicate client creation when called concurrently during provider initialization 3. Remove unused 'patch' import from test_http_pool.py Thread safety guarantees are now documented in the module docstring: - Singleton: threading.Lock with double-checked locking - Async clients: asyncio.Lock for coroutine safety - Sync clients: threading.Lock for thread safety * fix: resolve test failures for HTTP connection pooling - Mark _POOLED_HTTP_PROVIDERS as ClassVar to prevent Pydantic treating it as a private attribute - Fix import path: use codeweaver.providers.provider for ProviderKind instead of non-existent codeweaver.providers.kinds - Remove _limits assertions from tests since httpx.AsyncClient doesn't expose limits directly (timeout assertions are sufficient) * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> * docs: document client caching behavior for HTTP pool Add Note section to get_client() and get_client_sync() docstrings clarifying that clients are cached by name only, not by override parameters. Subsequent calls with different overrides will return the originally cached client. This is intentional behavior for connection pooling - each provider should use consistent settings, which ProviderRegistry ensures. * fix: address Copilot review suggestions for HTTP pool 1. asyncio.Lock event loop binding issue: - Changed _async_lock to be lazily created on first use - asyncio.Lock is bound to the event loop it's created in, so lazy creation ensures it's bound to the correct loop - Renamed _lock to _async_lock for clarity 2. Race condition in fast-path checks: - Changed if-check to try-except pattern in get_client() and get_client_sync() to handle client removal between check and return - Prevents KeyError if close_client/close_all removes client mid-check 3. Sync/async mixing warning: - Added Warning section to get_client_sync() docstring explaining that sync and async methods should not be mixed 4. Walrus operator clarity: - Replaced walrus operator with explicit get() and None check in provider.py for better readability --------- Signed-off-by: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> Co-authored-by: Claude Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../connection-pooling-implementation-plan.md | 285 ++++++++++ src/codeweaver/common/__init__.py | 20 + src/codeweaver/common/http_pool.py | 424 +++++++++++++++ src/codeweaver/common/registry/provider.py | 76 +++ .../providers/embedding/providers/mistral.py | 4 + .../embedding/providers/openai_factory.py | 5 +- src/codeweaver/server/server.py | 27 + tests/unit/common/test_http_pool.py | 505 ++++++++++++++++++ 8 files changed, 1345 insertions(+), 1 deletion(-) create mode 100644 claudedocs/connection-pooling-implementation-plan.md create mode 100644 src/codeweaver/common/http_pool.py create mode 100644 tests/unit/common/test_http_pool.py diff --git a/claudedocs/connection-pooling-implementation-plan.md b/claudedocs/connection-pooling-implementation-plan.md new file mode 100644 index 000000000..6516647fd --- /dev/null +++ b/claudedocs/connection-pooling-implementation-plan.md @@ -0,0 +1,285 @@ + + +# Connection Pooling Implementation Plan + +**Status**: ✅ Implemented +**Priority**: High +**Branch**: `claude/plan-connection-pooling-011nqd3xSBSn6a1FQdjnf8Qg` +**Based on**: `claudedocs/client-pooling-analysis.md` + +--- + +## Overview + +This document outlines the immediate implementation plan for HTTP connection pooling across CodeWeaver's HTTP-based providers. The implementation will be phased, starting with core infrastructure and progressing to provider integration. + +## Phase 1: Infrastructure (Core Implementation) + +### Task 1.1: Create HttpClientPool Class + +**File**: `src/codeweaver/common/http_pool.py` + +Create a centralized HTTP client pool manager with: +- Singleton pattern for application-wide access +- Configurable connection limits and timeouts +- Per-provider client management +- HTTP/2 support for better multiplexing +- Proper async cleanup + +**Key Components**: +```python +@dataclass(frozen=True) +class PoolLimits: + max_connections: int = 100 + max_keepalive_connections: int = 20 + keepalive_expiry: float = 5.0 + +@dataclass(frozen=True) +class PoolTimeouts: + connect: float = 10.0 + read: float = 60.0 + write: float = 10.0 + pool: float = 5.0 + +class HttpClientPool: + # Singleton instance + # Per-provider client dict + # get_client(name, **overrides) method + # close_all() cleanup method +``` + +### Task 1.2: Add Configuration Settings + +**File**: `src/codeweaver/config/settings.py` + +Add `HttpPoolSettings` TypedDict for configuration: +```python +class HttpPoolSettings(TypedDict, total=False): + max_connections: int + max_keepalive_connections: int + keepalive_expiry: float + connect_timeout: float + read_timeout: float + write_timeout: float + pool_timeout: float + enable_http2: bool +``` + +### Task 1.3: Integrate into CodeWeaverState + +**File**: `src/codeweaver/server/server.py` + +Add `http_pool` field to `CodeWeaverState`: +```python +http_pool: Annotated[ + HttpClientPool | None, + Field( + default=None, + description="Shared HTTP client pool for provider connections", + exclude=True, + ), +] = None +``` + +Initialize in `_initialize_cw_state()` function. + +### Task 1.4: Add Cleanup to Lifespan + +**File**: `src/codeweaver/server/server.py` + +Update `_cleanup_state()` to close HTTP pools: +```python +# Close HTTP client pools +if state.http_pool: + try: + await state.http_pool.close_all() + except Exception: + _logger.exception("Error closing HTTP client pools") +``` + +--- + +## Phase 2: Provider Integration + +### Task 2.1: Update Voyage AI Provider + +**File**: `src/codeweaver/providers/embedding/providers/voyage.py` + +The Voyage AI `AsyncClient` accepts an `httpx_client` parameter. We'll modify initialization to use the pooled client. + +**Current Flow**: +```python +client = AsyncClient(api_key=api_key) +``` + +**New Flow**: +```python +def _get_pooled_httpx_client() -> httpx.AsyncClient | None: + """Get pooled HTTP client for Voyage AI.""" + try: + from codeweaver.server.server import get_state + state = get_state() + if state.http_pool: + return state.http_pool.get_client( + 'voyage', + max_connections=50, + read_timeout=90.0, + ) + except Exception: + pass # Fallback to default client + return None + +# In provider initialization +httpx_client = _get_pooled_httpx_client() +client = AsyncClient(api_key=api_key, httpx_client=httpx_client) +``` + +### Task 2.2: Update Cohere Provider + +**File**: `src/codeweaver/providers/embedding/providers/cohere.py` + +The Cohere `AsyncClientV2` accepts an `httpx_client` parameter in `client_options`. + +**Current Flow** (line 123): +```python +known_client_options = { + "api_key", "base_url", "timeout", "max_retries", "httpx_client", +} +``` + +**New Flow**: Add pooled client to options: +```python +def _get_pooled_httpx_client() -> httpx.AsyncClient | None: + """Get pooled HTTP client for Cohere.""" + try: + from codeweaver.server.server import get_state + state = get_state() + if state.http_pool: + return state.http_pool.get_client( + 'cohere', + max_connections=50, + read_timeout=90.0, + ) + except Exception: + pass + return None + +# In __init__ before creating client: +if not client_options.get("httpx_client"): + client_options["httpx_client"] = _get_pooled_httpx_client() +``` + +### Task 2.3: Qdrant Considerations + +**File**: `src/codeweaver/providers/vector_stores/qdrant_base.py` + +The `qdrant_client` library uses httpx internally but doesn't expose an `httpx_client` parameter. We have two options: + +1. **Use timeout configuration** (recommended for now): + - Configure longer timeouts via `qdrant_client` parameters + - This addresses the httpcore.ReadError issues + +2. **Future enhancement**: + - Investigate if newer qdrant_client versions expose httpx configuration + - Consider contributing upstream if needed + +--- + +## Implementation Order + +``` +1. Create http_pool.py module +2. Add HttpPoolSettings to config +3. Add http_pool to CodeWeaverState +4. Add cleanup to _cleanup_state() +5. Update Voyage AI provider +6. Update Cohere provider +7. Add unit tests +8. Integration testing +``` + +--- + +## Provider-Specific Settings + +| Provider | max_connections | read_timeout | keepalive_expiry | HTTP/2 | Parameter Name | +|----------|----------------|--------------|------------------|--------|----------------| +| Voyage | 50 | 90s | 5s | Yes | `httpx_client` | +| Cohere | 50 | 90s | 5s | Yes | `httpx_client` | +| OpenAI | 50 | 90s | 5s | Yes | `http_client` | +| Azure | 50 | 90s | 5s | Yes | `http_client` | +| Fireworks| 50 | 90s | 5s | Yes | `http_client` | +| Groq | 50 | 90s | 5s | Yes | `http_client` | +| Together | 50 | 90s | 5s | Yes | `http_client` | +| Ollama | 50 | 90s | 5s | Yes | `http_client` | +| Cerebras | 50 | 90s | 5s | Yes | `http_client` | +| Heroku | 50 | 90s | 5s | Yes | `http_client` | +| Mistral | 50 | 90s | 5s | Yes | `httpx_client` | +| Qdrant | N/A | N/A | N/A | N/A | Not supported | + +**Note**: Qdrant uses httpx internally but doesn't expose a client injection parameter. + +--- + +## Testing Strategy + +### Unit Tests +- `test_http_pool_singleton()` - Verify singleton behavior +- `test_client_reuse()` - Verify client caching +- `test_cleanup()` - Verify proper cleanup +- `test_provider_overrides()` - Verify per-provider settings + +### Integration Tests +- Test Voyage AI with pooled client +- Test Cohere with pooled client +- Verify connection reuse (check logs) + +### Manual Verification +- Run `cw index` on a large repo +- Monitor for httpcore.ReadError (should be eliminated) +- Check connection reuse in debug logs + +--- + +## Rollback Plan + +The implementation includes fallbacks: +1. If `get_state()` fails, providers fall back to default clients +2. If `http_pool` is None, providers create their own clients +3. No breaking changes to existing interfaces + +--- + +## Success Metrics + +1. **No httpcore.ReadError** during indexing operations +2. **Reduced connection overhead** (visible in debug logs) +3. **All existing tests pass** without modification +4. **Memory usage stable** during long indexing operations + +--- + +## Files to Create/Modify + +| File | Action | Description | +|------|--------|-------------| +| `src/codeweaver/common/http_pool.py` | **Create** | HTTP client pool manager | +| `src/codeweaver/config/settings.py` | Modify | Add HttpPoolSettings | +| `src/codeweaver/server/server.py` | Modify | Add http_pool to state, cleanup | +| `src/codeweaver/providers/embedding/providers/voyage.py` | Modify | Use pooled client | +| `src/codeweaver/providers/embedding/providers/cohere.py` | Modify | Use pooled client | +| `tests/unit/common/test_http_pool.py` | **Create** | Unit tests | + +--- + +## Notes + +- The implementation prioritizes backward compatibility +- Fallbacks ensure the system works without pooling if needed +- HTTP/2 is enabled by default for better multiplexing on modern APIs +- Qdrant integration may require upstream changes to qdrant_client diff --git a/src/codeweaver/common/__init__.py b/src/codeweaver/common/__init__.py index 97b4972a7..1090f7725 100644 --- a/src/codeweaver/common/__init__.py +++ b/src/codeweaver/common/__init__.py @@ -12,6 +12,14 @@ if TYPE_CHECKING: # Import everything for IDE and type checker support # These imports are never executed at runtime, only during type checking + from codeweaver.common.http_pool import ( + HttpClientPool, + PoolLimits, + PoolTimeouts, + get_http_pool, + reset_http_pool, + reset_http_pool_sync, + ) from codeweaver.common.logging import log_to_client_or_fallback, setup_logger from codeweaver.common.statistics import ( FileStatistics, @@ -103,6 +111,7 @@ _dynamic_imports: MappingProxyType[str, tuple[str, str]] = MappingProxyType({ "CallHookTimingDict": (__spec__.parent, "types"), "FileStatistics": (__spec__.parent, "statistics"), + "HttpClientPool": (__spec__.parent, "http_pool"), "HttpRequestsDict": (__spec__.parent, "types"), "Identifier": (__spec__.parent, "statistics"), "LazyImport": (__spec__.parent, "utils"), @@ -110,6 +119,8 @@ "McpComponentTimingDict": (__spec__.parent, "types"), "McpOperationRequests": (__spec__.parent, "types"), "McpTimingDict": (__spec__.parent, "types"), + "PoolLimits": (__spec__.parent, "http_pool"), + "PoolTimeouts": (__spec__.parent, "http_pool"), "ResourceUri": (__spec__.parent, "types"), "SessionStatistics": (__spec__.parent, "statistics"), "TimingStatistics": (__spec__.parent, "statistics"), @@ -140,6 +151,7 @@ "get_function_signature": (__spec__.parent, "utils"), "get_git_branch": (__spec__.parent, "utils"), "get_git_revision": (__spec__.parent, "utils"), + "get_http_pool": (__spec__.parent, "http_pool"), "get_optimal_workers": (__spec__.parent, "utils"), "get_possible_env_vars": (__spec__.parent, "utils"), "get_project_path": (__spec__.parent, "utils"), @@ -169,6 +181,8 @@ "low_priority": (__spec__.parent, "utils"), "normalize_ext": (__spec__.parent, "utils"), "record_timed_http_request": (__spec__.parent, "statistics"), + "reset_http_pool": (__spec__.parent, "http_pool"), + "reset_http_pool_sync": (__spec__.parent, "http_pool"), "rpartial": (__spec__.parent, "utils"), "sanitize_unicode": (__spec__.parent, "utils"), "set_relative_path": (__spec__.parent, "utils"), @@ -203,6 +217,7 @@ def __getattr__(name: str) -> object: "CODEWEAVER_PREFIX", "CallHookTimingDict", "FileStatistics", + "HttpClientPool", "HttpRequestsDict", "Identifier", "LazyImport", @@ -210,6 +225,8 @@ def __getattr__(name: str) -> object: "McpComponentTimingDict", "McpOperationRequests", "McpTimingDict", + "PoolLimits", + "PoolTimeouts", "ResourceUri", "SessionStatistics", "TimingStatistics", @@ -240,6 +257,7 @@ def __getattr__(name: str) -> object: "get_function_signature", "get_git_branch", "get_git_revision", + "get_http_pool", "get_optimal_workers", "get_possible_env_vars", "get_project_path", @@ -269,6 +287,8 @@ def __getattr__(name: str) -> object: "low_priority", "normalize_ext", "record_timed_http_request", + "reset_http_pool", + "reset_http_pool_sync", "rpartial", "sanitize_unicode", "set_relative_path", diff --git a/src/codeweaver/common/http_pool.py b/src/codeweaver/common/http_pool.py new file mode 100644 index 000000000..7acbb4949 --- /dev/null +++ b/src/codeweaver/common/http_pool.py @@ -0,0 +1,424 @@ +# SPDX-FileCopyrightText: 2025 Knitli Inc. +# SPDX-FileContributor: Adam Poulemanos +# +# SPDX-License-Identifier: MIT OR Apache-2.0 +"""HTTP client connection pool manager for CodeWeaver providers. + +This module provides centralized HTTP client pooling for providers that use httpx, +including Voyage AI, Cohere, and other HTTP-based API providers. Connection pooling +reduces overhead from repeated TCP handshakes and TLS negotiations, improving +performance and reliability during high-load operations like indexing. + +Usage: + from codeweaver.common.http_pool import HttpClientPool, get_http_pool + + # Get singleton instance + pool = get_http_pool() + + # Get a pooled client for a specific provider + client = await pool.get_client('voyage', read_timeout=90.0) + + # Cleanup on shutdown + await pool.close_all() + +Thread Safety: + - Singleton initialization: Thread-safe via double-checked locking (threading.Lock) + - Async client creation (get_client): Coroutine-safe via asyncio.Lock + - Sync client creation (get_client_sync): Thread-safe via threading.Lock + - WARNING: Synchronous and asynchronous methods should NOT be mixed. The locking strategy does not protect against concurrent access from both threads and coroutines. Only use sync methods in a purely threaded context, and async methods in a purely async context. + - All methods can be called concurrently within their respective contexts (sync or async) without creating duplicate clients. +""" + +from __future__ import annotations + +import asyncio +import logging +import threading +from dataclasses import dataclass, field +from typing import Any, ClassVar + +import httpx + + +logger = logging.getLogger(__name__) + +# Module-level lock for thread-safe singleton initialization +_instance_lock = threading.Lock() + + +@dataclass(frozen=True) +class PoolLimits: + """HTTP connection pool limits configuration. + + Attributes: + max_connections: Maximum total connections across all hosts. + max_keepalive_connections: Maximum persistent connections to keep alive. + keepalive_expiry: Seconds to keep idle connections alive. + """ + + max_connections: int = 100 + max_keepalive_connections: int = 20 + keepalive_expiry: float = 5.0 + + +@dataclass(frozen=True) +class PoolTimeouts: + """HTTP timeout configuration for pooled clients. + + Attributes: + connect: Connection establishment timeout in seconds. + read: Read timeout in seconds (longer for embedding/vector operations). + write: Write timeout in seconds. + pool: Pool acquire timeout in seconds. + """ + + connect: float = 10.0 + read: float = 60.0 # Longer for embedding/vector operations + write: float = 10.0 + pool: float = 5.0 + + +@dataclass +class HttpClientPool: + """Singleton HTTP client pool manager for provider connections. + + Manages a collection of httpx.AsyncClient instances, one per provider, + with configurable connection limits and timeouts. Clients are created + lazily on first request and reused for subsequent requests. + + The pool supports HTTP/2 by default for better multiplexing on modern APIs. + + Thread Safety: + Client creation is protected by an asyncio.Lock to prevent race conditions + when multiple coroutines request the same client simultaneously. + + Example: + pool = HttpClientPool.get_instance() + client = await pool.get_client('voyage', max_connections=50, read_timeout=90.0) + # Use client for API calls... + await pool.close_all() # Cleanup on shutdown + """ + + _instance: ClassVar[HttpClientPool | None] = None + + limits: PoolLimits = field(default_factory=PoolLimits) + timeouts: PoolTimeouts = field(default_factory=PoolTimeouts) + _clients: dict[str, httpx.AsyncClient] = field(default_factory=dict, repr=False) + # Note: _async_lock is created lazily to avoid event loop binding issues. + # asyncio.Lock is bound to the event loop it's created in, so we create it + # on first use in get_client() to ensure it's bound to the correct loop. + _async_lock: asyncio.Lock | None = field(default=None, repr=False) + _sync_lock: threading.Lock = field(default_factory=threading.Lock, repr=False) + + @classmethod + def get_instance( + cls, + limits: PoolLimits | None = None, + timeouts: PoolTimeouts | None = None, + ) -> HttpClientPool: + """Get or create the singleton HttpClientPool instance. + + This method is thread-safe via double-checked locking pattern. + + Args: + limits: Optional connection pool limits (only used on first call). + timeouts: Optional timeout configuration (only used on first call). + + Returns: + The singleton HttpClientPool instance. + """ + # Fast path: instance already exists + if cls._instance is not None: + return cls._instance + + # Slow path: acquire lock and double-check + with _instance_lock: + if cls._instance is None: + cls._instance = cls( + limits=limits or PoolLimits(), + timeouts=timeouts or PoolTimeouts(), + ) + logger.debug( + "Created HttpClientPool singleton with limits=%s, timeouts=%s", + cls._instance.limits, + cls._instance.timeouts, + ) + return cls._instance + + @classmethod + def reset_instance(cls) -> None: + """Reset the singleton instance (primarily for testing).""" + with _instance_lock: + cls._instance = None + + async def get_client(self, name: str, **overrides: Any) -> httpx.AsyncClient: + """Get or create a pooled HTTP client for a specific provider. + + Clients are cached by name and reused for subsequent requests. + Override parameters allow per-provider customization of limits and timeouts. + + This method is coroutine-safe: concurrent calls for the same provider + will not create duplicate clients. + + Note: + Clients are cached by name only, not by override parameters. If the same + provider name is requested multiple times with different overrides, the + client created on the first call will be returned for all subsequent calls. + This is intentional for connection pooling - each provider should use + consistent settings. The ProviderRegistry ensures consistent overrides + per provider type. + + Args: + name: Provider name (e.g., 'voyage', 'cohere', 'qdrant'). + **overrides: Override default limits/timeouts for this client: + - max_connections: int + - max_keepalive_connections: int + - keepalive_expiry: float + - connect_timeout: float + - read_timeout: float + - write_timeout: float + - pool_timeout: float + - http2: bool (default True) + + Returns: + Configured httpx.AsyncClient with connection pooling. + """ + # Fast path: return existing client without lock + # Use try-except to handle race condition where client could be removed + # between check and return + try: + return self._clients[name] + except KeyError: + pass # Continue to locked creation + + # Lazily create asyncio.Lock on first use to avoid event loop binding issues. + # asyncio.Lock must be created in the same event loop where it will be used. + if self._async_lock is None: + self._async_lock = asyncio.Lock() + + # Slow path: acquire lock for client creation + async with self._async_lock: + # Double-check after acquiring lock + if name in self._clients: + return self._clients[name] + + limits = httpx.Limits( + max_connections=overrides.get("max_connections", self.limits.max_connections), + max_keepalive_connections=overrides.get( + "max_keepalive_connections", + self.limits.max_keepalive_connections, + ), + keepalive_expiry=overrides.get("keepalive_expiry", self.limits.keepalive_expiry), + ) + + timeout = httpx.Timeout( + connect=overrides.get("connect_timeout", self.timeouts.connect), + read=overrides.get("read_timeout", self.timeouts.read), + write=overrides.get("write_timeout", self.timeouts.write), + pool=overrides.get("pool_timeout", self.timeouts.pool), + ) + + self._clients[name] = httpx.AsyncClient( + limits=limits, + timeout=timeout, + http2=overrides.get("http2", True), # Enable HTTP/2 for better multiplexing + ) + + logger.debug( + "Created HTTP client pool for %s: max_conn=%d, keepalive=%d, read_timeout=%.1fs", + name, + limits.max_connections, + limits.max_keepalive_connections, + timeout.read, + ) + + return self._clients[name] + + def get_client_sync(self, name: str, **overrides: Any) -> httpx.AsyncClient: + """Synchronous version of get_client for non-async contexts. + + This method is thread-safe via double-checked locking pattern. + It can be called concurrently from multiple threads during provider + initialization without creating duplicate clients. + + Note: + Clients are cached by name only, not by override parameters. If the same + provider name is requested multiple times with different overrides, the + client created on the first call will be returned for all subsequent calls. + This is intentional for connection pooling - each provider should use + consistent settings. + + Warning: + Synchronous and asynchronous methods should NOT be mixed. The locking + strategy does not protect against concurrent access from both threads + and coroutines. Only use sync methods in a purely threaded context, + and async methods in a purely async context. + + Args: + name: Provider name (e.g., 'voyage', 'cohere', 'qdrant'). + **overrides: Override default limits/timeouts for this client. + + Returns: + Configured httpx.AsyncClient with connection pooling. + """ + # Fast path: client already exists + # Use try-except to handle race condition where client could be removed + # between check and return + try: + return self._clients[name] + except KeyError: + pass # Continue to locked creation + + # Slow path: acquire lock and double-check + with self._sync_lock: + if name not in self._clients: + limits = httpx.Limits( + max_connections=overrides.get("max_connections", self.limits.max_connections), + max_keepalive_connections=overrides.get( + "max_keepalive_connections", + self.limits.max_keepalive_connections, + ), + keepalive_expiry=overrides.get("keepalive_expiry", self.limits.keepalive_expiry), + ) + + timeout = httpx.Timeout( + connect=overrides.get("connect_timeout", self.timeouts.connect), + read=overrides.get("read_timeout", self.timeouts.read), + write=overrides.get("write_timeout", self.timeouts.write), + pool=overrides.get("pool_timeout", self.timeouts.pool), + ) + + self._clients[name] = httpx.AsyncClient( + limits=limits, + timeout=timeout, + http2=overrides.get("http2", True), + ) + + logger.debug( + "Created HTTP client pool for %s: max_conn=%d, keepalive=%d, read_timeout=%.1fs", + name, + limits.max_connections, + limits.max_keepalive_connections, + timeout.read, + ) + + return self._clients[name] + + def has_client(self, name: str) -> bool: + """Check if a client exists for the given provider name. + + Args: + name: Provider name to check. + + Returns: + True if a client exists for this provider. + """ + with self._sync_lock: + return name in self._clients + + @property + def client_names(self) -> list[str]: + """Get list of all provider names with active clients.""" + with self._sync_lock: + return list(self._clients.keys()) + + async def close_client(self, name: str) -> bool: + """Close a specific provider's HTTP client. + + Args: + name: Provider name whose client to close. + + Returns: + True if client was closed, False if no client existed. + """ + # Ensure async lock exists + if self._async_lock is None: + self._async_lock = asyncio.Lock() + + async with self._async_lock: + if name in self._clients: + client = self._clients.pop(name) + try: + await client.aclose() + logger.debug("Closed HTTP client pool for %s", name) + return True + except (httpx.HTTPError, OSError) as e: + logger.warning("Error closing HTTP client pool for %s: %s", name, e) + # Client already removed from dict above + return False + + async def close_all(self) -> None: + """Close all pooled clients (cleanup on shutdown). + + This method should be called during application shutdown to properly + close all HTTP connections and release resources. + """ + # Ensure async lock exists + if self._async_lock is None: + self._async_lock = asyncio.Lock() + + async with self._async_lock: + client_names = list(self._clients.keys()) + for name in client_names: + client = self._clients.pop(name, None) + if client is None: + continue + try: + await client.aclose() + logger.debug("Closed HTTP client pool for %s", name) + except (httpx.HTTPError, OSError) as e: + logger.warning("Error closing HTTP client pool for %s: %s", name, e) + + if client_names: + logger.debug("Closed %d HTTP client pool(s)", len(client_names)) + + async def __aenter__(self) -> HttpClientPool: + """Async context manager entry.""" + return self + + async def __aexit__(self, *args: Any) -> None: + """Async context manager exit - closes all clients.""" + await self.close_all() + + +def get_http_pool() -> HttpClientPool: + """Get the global HTTP client pool instance. + + This is the recommended way to access the HTTP client pool throughout + the application. It returns the singleton instance, creating it if needed. + + Returns: + The singleton HttpClientPool instance. + """ + return HttpClientPool.get_instance() + + +async def reset_http_pool() -> None: + """Reset the global HTTP client pool (primarily for testing). + + This async function properly closes all existing clients before resetting + the singleton instance to prevent resource leaks. + """ + instance = HttpClientPool._instance + if instance is not None: + await instance.close_all() + HttpClientPool.reset_instance() + + +def reset_http_pool_sync() -> None: + """Synchronous reset for testing fixtures (no cleanup). + + Warning: This does NOT close existing clients. Use reset_http_pool() + if you need to clean up resources properly. + """ + HttpClientPool.reset_instance() + + +__all__ = ( + "HttpClientPool", + "PoolLimits", + "PoolTimeouts", + "get_http_pool", + "reset_http_pool", + "reset_http_pool_sync", +) diff --git a/src/codeweaver/common/registry/provider.py b/src/codeweaver/common/registry/provider.py index 5485f37a6..bb3cc0634 100644 --- a/src/codeweaver/common/registry/provider.py +++ b/src/codeweaver/common/registry/provider.py @@ -549,6 +549,65 @@ def _get_base_url_for_provider(self, provider: Provider, **kwargs: Any) -> str | # 🔧 NEW: Client Factory Methods + # Providers that support HTTP client injection for connection pooling + # Maps provider -> parameter name used by that provider's SDK + _POOLED_HTTP_PROVIDERS: ClassVar[dict[Provider, str]] = { + # Voyage/Cohere use 'httpx_client' parameter + Provider.VOYAGE: "httpx_client", + Provider.COHERE: "httpx_client", + # OpenAI-compatible providers use 'http_client' parameter + Provider.OPENAI: "http_client", + Provider.AZURE: "http_client", + Provider.FIREWORKS: "http_client", + Provider.GROQ: "http_client", + Provider.TOGETHER: "http_client", + Provider.OLLAMA: "http_client", + Provider.CEREBRAS: "http_client", + Provider.HEROKU: "http_client", + # Mistral uses 'httpx_client' (mapped to 'async_client' in provider) + Provider.MISTRAL: "httpx_client", + } + + def _get_pooled_httpx_client( + self, provider: Provider, provider_kind: ProviderKind + ) -> Any: + """Get a pooled HTTP client for providers that support HTTP client injection. + + This method provides connection pooling for HTTP-based providers, + reducing connection overhead and improving reliability during high-load operations. + + Args: + provider: Provider enum (VOYAGE, COHERE, OPENAI, etc.) + provider_kind: Provider kind (embedding, reranking, etc.) + + Returns: + Pooled httpx.AsyncClient if available, None otherwise (provider will create own client) + """ + try: + from codeweaver.common.http_pool import get_http_pool + + pool = get_http_pool() + + # Provider-specific pool settings + # Longer read timeouts for embedding operations which can be slow + pool_overrides: dict[str, Any] = { + "max_connections": 50, + "read_timeout": 90.0, # Embeddings can take time + } + + # Create unique client name based on provider and kind + client_name = f"{provider.value.lower()}_{provider_kind.value.lower()}" + return pool.get_client_sync(client_name, **pool_overrides) + except (ImportError, AttributeError, RuntimeError) as e: + # Fallback to provider-created client if pool fails + logger.debug( + "Failed to get pooled HTTP client for %s (%s): %s. Provider will create own client.", + provider, + provider_kind, + e, + ) + return None + def collect_env_vars(self, provider: Provider) -> dict[str, str]: """Collect relevant environment variables for a provider. @@ -818,6 +877,23 @@ def _instantiate_client( provider_settings = provider_settings or {} merged_settings = provider_settings | client_options + + # Inject pooled HTTP client for providers that support it + # This improves connection reuse and reduces overhead during high-load operations + param_name = self._POOLED_HTTP_PROVIDERS.get(provider) + if param_name is not None: + # Check if a client isn't already provided (via httpx_client or http_client) + if param_name not in merged_settings: + pooled_client = self._get_pooled_httpx_client(provider, provider_kind) + if pooled_client is not None: + merged_settings[param_name] = pooled_client + logger.debug( + "Injected pooled HTTP client for %s provider (kind: %s, param: %s)", + provider, + provider_kind, + param_name, + ) + args, kwargs = clean_args(merged_settings, client_class) args = tuple(arg.get_secret_value() if isinstance(arg, SecretStr) else arg for arg in args) kwargs = { diff --git a/src/codeweaver/providers/embedding/providers/mistral.py b/src/codeweaver/providers/embedding/providers/mistral.py index 1b0d45b8b..37f0e5cf2 100644 --- a/src/codeweaver/providers/embedding/providers/mistral.py +++ b/src/codeweaver/providers/embedding/providers/mistral.py @@ -49,6 +49,10 @@ def __init__( api_key = os.environ.get( "MISTRAL_API_KEY", kwargs.get("api_key") ) or client_options.get("api_key") + # Support connection pooling via async_client injection + # Mistral SDK accepts async_client as an AsyncHttpClient protocol (httpx.AsyncClient compatible) + if "httpx_client" in kwargs: + client_options["async_client"] = kwargs.pop("httpx_client") client = Mistral(api_key=api_key, **client_options) # Call super().__init__() FIRST which handles all Pydantic initialization diff --git a/src/codeweaver/providers/embedding/providers/openai_factory.py b/src/codeweaver/providers/embedding/providers/openai_factory.py index 661fda42f..5d71f94a8 100644 --- a/src/codeweaver/providers/embedding/providers/openai_factory.py +++ b/src/codeweaver/providers/embedding/providers/openai_factory.py @@ -144,13 +144,16 @@ def __init__(self: EmbeddingProvider[AsyncOpenAI], *args: Any, **kwargs: Any) -> if client_instance is None: from openai import AsyncOpenAI - client_kwargs = { + client_kwargs: dict[str, Any] = { "api_key": kwargs.get( "api_key", "ollama" if provider == Provider.OLLAMA else None ) } if base_url: client_kwargs["base_url"] = base_url + # Support connection pooling via http_client injection + if "http_client" in kwargs: + client_kwargs["http_client"] = kwargs.pop("http_client") client_instance = AsyncOpenAI(**client_kwargs) # 3. Call parent __init__ FIRST with proper arguments diff --git a/src/codeweaver/server/server.py b/src/codeweaver/server/server.py index 8fd9f79f4..648a68e48 100644 --- a/src/codeweaver/server/server.py +++ b/src/codeweaver/server/server.py @@ -41,6 +41,7 @@ if TYPE_CHECKING: + from codeweaver.common.http_pool import HttpClientPool from codeweaver.common.utils import LazyImport from codeweaver.core.types import AnonymityConversion, FilteredKeyT @@ -153,6 +154,15 @@ class CodeWeaverState(DataclassSerializationMixin): telemetry: Annotated[PostHogClient | None, PrivateAttr(default=None)] + http_pool: Annotated[ + "HttpClientPool | None", + Field( + default=None, + description="Shared HTTP client pool for provider connections (Voyage, Cohere, etc.)", + exclude=True, + ), + ] = None + _mcp_http_server: Annotated[FastMCP[CwMcpHttpState] | None, PrivateAttr()] = None _tasks: Annotated[list[asyncio.Task] | None, PrivateAttr(default_factory=list)] = None @@ -279,6 +289,15 @@ async def _cleanup_state( except Exception: logging.getLogger(__name__).exception("Error shutting down telemetry client") + # Close HTTP client pools to release connections + if state.http_pool: + try: + await state.http_pool.close_all() + if verbose: + _logger.info("Closed HTTP client pools") + except Exception: + logging.getLogger(__name__).exception("Error closing HTTP client pools") + if verbose: _logger.info("Exiting CodeWeaver lifespan context manager...") @@ -396,7 +415,14 @@ def _initialize_cw_state( settings: CodeWeaverSettings | None = None, statistics: SessionStatistics | None = None ) -> CodeWeaverState: """Initialize application state if not already present.""" + from codeweaver.common.http_pool import HttpClientPool + resolved_settings = settings or get_settings._resolve()() + + # Initialize HTTP client pool for connection reuse + http_pool = HttpClientPool.get_instance() + _logger.debug("Initialized HTTP client pool for provider connections") + state = CodeWeaverState( # type: ignore initialized=False, # for lazy imports, we need to call resolve() to get the function/object and then call it @@ -416,6 +442,7 @@ def _initialize_cw_state( health_service=None, # Initialize as None, will be set after CodeWeaverState construction failover_manager=None, # Initialize as None, will be set after CodeWeaverState construction telemetry=PostHogClient.from_settings(), + http_pool=http_pool, indexer=Indexer.from_settings(), ) state.health_service = _get_health_service() diff --git a/tests/unit/common/test_http_pool.py b/tests/unit/common/test_http_pool.py new file mode 100644 index 000000000..c3b52a6a5 --- /dev/null +++ b/tests/unit/common/test_http_pool.py @@ -0,0 +1,505 @@ +# SPDX-FileCopyrightText: 2025 Knitli Inc. +# SPDX-FileContributor: Adam Poulemanos +# +# SPDX-License-Identifier: MIT OR Apache-2.0 +"""Unit tests for HTTP client connection pool manager.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock + +import httpx +import pytest + +from codeweaver.common.http_pool import ( + HttpClientPool, + PoolLimits, + PoolTimeouts, + get_http_pool, + reset_http_pool, + reset_http_pool_sync, +) + + +class TestPoolLimits: + """Tests for PoolLimits configuration dataclass.""" + + def test_default_values(self): + """Test default pool limits.""" + limits = PoolLimits() + assert limits.max_connections == 100 + assert limits.max_keepalive_connections == 20 + assert limits.keepalive_expiry == 5.0 + + def test_custom_values(self): + """Test custom pool limits.""" + limits = PoolLimits( + max_connections=50, + max_keepalive_connections=10, + keepalive_expiry=10.0, + ) + assert limits.max_connections == 50 + assert limits.max_keepalive_connections == 10 + assert limits.keepalive_expiry == 10.0 + + def test_immutable(self): + """Test that PoolLimits is frozen (immutable).""" + limits = PoolLimits() + with pytest.raises(AttributeError): + limits.max_connections = 200 # type: ignore + + +class TestPoolTimeouts: + """Tests for PoolTimeouts configuration dataclass.""" + + def test_default_values(self): + """Test default pool timeouts.""" + timeouts = PoolTimeouts() + assert timeouts.connect == 10.0 + assert timeouts.read == 60.0 + assert timeouts.write == 10.0 + assert timeouts.pool == 5.0 + + def test_custom_values(self): + """Test custom pool timeouts.""" + timeouts = PoolTimeouts( + connect=5.0, + read=120.0, + write=15.0, + pool=10.0, + ) + assert timeouts.connect == 5.0 + assert timeouts.read == 120.0 + assert timeouts.write == 15.0 + assert timeouts.pool == 10.0 + + def test_immutable(self): + """Test that PoolTimeouts is frozen (immutable).""" + timeouts = PoolTimeouts() + with pytest.raises(AttributeError): + timeouts.read = 200.0 # type: ignore + + +class TestHttpClientPool: + """Tests for HttpClientPool manager.""" + + @pytest.fixture(autouse=True) + async def reset_pool(self): + """Reset pool singleton before and after each test with proper cleanup.""" + await reset_http_pool() + yield + await reset_http_pool() + + def test_singleton_pattern(self): + """Test that get_instance returns the same instance.""" + pool1 = HttpClientPool.get_instance() + pool2 = HttpClientPool.get_instance() + assert pool1 is pool2 + + def test_get_http_pool_returns_singleton(self): + """Test that get_http_pool returns the singleton instance.""" + pool1 = get_http_pool() + pool2 = get_http_pool() + assert pool1 is pool2 + + @pytest.mark.asyncio + async def test_reset_instance(self): + """Test that reset_instance clears the singleton.""" + pool1 = HttpClientPool.get_instance() + await reset_http_pool() + pool2 = HttpClientPool.get_instance() + assert pool1 is not pool2 + + @pytest.mark.asyncio + async def test_get_client_creates_client(self): + """Test that get_client creates an httpx.AsyncClient.""" + pool = HttpClientPool.get_instance() + client = await pool.get_client("test_provider") + + assert isinstance(client, httpx.AsyncClient) + assert pool.has_client("test_provider") + assert "test_provider" in pool.client_names + + @pytest.mark.asyncio + async def test_get_client_reuses_client(self): + """Test that get_client returns the same client for same name.""" + pool = HttpClientPool.get_instance() + client1 = await pool.get_client("test_provider") + client2 = await pool.get_client("test_provider") + + assert client1 is client2 + + @pytest.mark.asyncio + async def test_get_client_with_overrides(self): + """Test that get_client applies override settings correctly.""" + pool = HttpClientPool.get_instance() + client = await pool.get_client( + "custom_provider", + max_connections=25, + read_timeout=120.0, + connect_timeout=5.0, + ) + + # Verify client was created + assert pool.has_client("custom_provider") + + # Verify timeout overrides are actually applied + assert client.timeout.read == 120.0 + assert client.timeout.connect == 5.0 + + # Client was created with specified limits (httpx doesn't expose limits directly, + # but we can verify the client exists and timeouts are correct) + + def test_get_client_sync_creates_client(self): + """Test that get_client_sync creates an httpx.AsyncClient.""" + pool = HttpClientPool.get_instance() + client = pool.get_client_sync("test_provider") + + assert isinstance(client, httpx.AsyncClient) + assert pool.has_client("test_provider") + + def test_get_client_sync_with_overrides(self): + """Test that get_client_sync applies override settings correctly.""" + pool = HttpClientPool.get_instance() + client = pool.get_client_sync( + "custom_provider", + max_connections=25, + read_timeout=120.0, + ) + + # Verify timeout overrides are actually applied + assert client.timeout.read == 120.0 + + @pytest.mark.asyncio + async def test_multiple_providers(self): + """Test that different providers get different clients.""" + pool = HttpClientPool.get_instance() + voyage_client = await pool.get_client("voyage") + cohere_client = await pool.get_client("cohere") + + assert voyage_client is not cohere_client + assert pool.has_client("voyage") + assert pool.has_client("cohere") + assert len(pool.client_names) == 2 + + @pytest.mark.asyncio + async def test_close_client(self): + """Test that close_client closes and removes a client.""" + pool = HttpClientPool.get_instance() + await pool.get_client("test_provider") + + assert pool.has_client("test_provider") + + result = await pool.close_client("test_provider") + + assert result is True + assert not pool.has_client("test_provider") + + @pytest.mark.asyncio + async def test_close_nonexistent_client(self): + """Test that close_client returns False for nonexistent client.""" + pool = HttpClientPool.get_instance() + result = await pool.close_client("nonexistent") + assert result is False + + @pytest.mark.asyncio + async def test_close_client_handles_aclose_error(self): + """Test that close_client handles aclose() exceptions gracefully.""" + pool = HttpClientPool.get_instance() + await pool.get_client("test_provider") + + # Mock the client's aclose to raise an error + client = pool._clients["test_provider"] + client.aclose = AsyncMock(side_effect=httpx.HTTPError("connection error")) + + # close_client should not raise and should return False (client is still removed) + result = await pool.close_client("test_provider") + + # Client should be removed despite error + assert not pool.has_client("test_provider") + # Returns False when there's an error during close + assert result is False + + @pytest.mark.asyncio + async def test_close_client_handles_os_error(self): + """Test that close_client handles OSError gracefully.""" + pool = HttpClientPool.get_instance() + await pool.get_client("test_provider") + + # Mock the client's aclose to raise an OSError + client = pool._clients["test_provider"] + client.aclose = AsyncMock(side_effect=OSError("socket error")) + + result = await pool.close_client("test_provider") + + # Client should be removed despite error + assert not pool.has_client("test_provider") + assert result is False + + @pytest.mark.asyncio + async def test_close_all(self): + """Test that close_all closes all clients.""" + pool = HttpClientPool.get_instance() + await pool.get_client("provider1") + await pool.get_client("provider2") + await pool.get_client("provider3") + + assert len(pool.client_names) == 3 + + await pool.close_all() + + assert len(pool.client_names) == 0 + + @pytest.mark.asyncio + async def test_close_all_handles_errors(self): + """Test that close_all handles aclose() exceptions gracefully.""" + pool = HttpClientPool.get_instance() + await pool.get_client("provider1") + await pool.get_client("provider2") + + # Mock one client to raise an error + pool._clients["provider1"].aclose = AsyncMock(side_effect=OSError("error")) + + # close_all should not raise + await pool.close_all() + + # All clients should be removed + assert len(pool.client_names) == 0 + + @pytest.mark.asyncio + async def test_context_manager(self): + """Test async context manager usage.""" + async with HttpClientPool.get_instance() as pool: + await pool.get_client("test_provider") + assert pool.has_client("test_provider") + + # After context exit, clients should be closed + assert len(pool.client_names) == 0 + + def test_custom_limits_and_timeouts(self): + """Test creating pool with custom limits and timeouts.""" + # Reset first to ensure fresh instance + reset_http_pool_sync() + + custom_limits = PoolLimits(max_connections=50) + custom_timeouts = PoolTimeouts(read=120.0) + + pool = HttpClientPool.get_instance( + limits=custom_limits, + timeouts=custom_timeouts, + ) + + assert pool.limits.max_connections == 50 + assert pool.timeouts.read == 120.0 + + @pytest.mark.asyncio + async def test_http2_enabled_by_default(self): + """Test that HTTP/2 is enabled by default.""" + pool = HttpClientPool.get_instance() + client = await pool.get_client("test_provider") + + # Check that http2 is enabled + assert isinstance(client, httpx.AsyncClient) + # HTTP/2 support is configured via transport, we set http2=True + + @pytest.mark.asyncio + async def test_client_names_property(self): + """Test client_names property returns correct list.""" + pool = HttpClientPool.get_instance() + assert pool.client_names == [] + + await pool.get_client("provider_a") + await pool.get_client("provider_b") + + names = pool.client_names + assert "provider_a" in names + assert "provider_b" in names + assert len(names) == 2 + + @pytest.mark.asyncio + async def test_concurrent_get_client_same_provider(self): + """Test that concurrent get_client calls for same provider don't create duplicates.""" + import asyncio + + pool = HttpClientPool.get_instance() + + # Request the same client concurrently + results = await asyncio.gather( + pool.get_client("test_provider"), + pool.get_client("test_provider"), + pool.get_client("test_provider"), + ) + + # All should return the same client instance + assert results[0] is results[1] + assert results[1] is results[2] + assert len(pool.client_names) == 1 + + +class TestModuleLevelFunctions: + """Tests for module-level convenience functions.""" + + @pytest.fixture(autouse=True) + async def reset_pool(self): + """Reset pool singleton before and after each test with proper cleanup.""" + await reset_http_pool() + yield + await reset_http_pool() + + def test_get_http_pool(self): + """Test get_http_pool creates and returns singleton.""" + pool = get_http_pool() + assert isinstance(pool, HttpClientPool) + + @pytest.mark.asyncio + async def test_reset_http_pool(self): + """Test reset_http_pool closes clients and clears the global instance.""" + pool1 = get_http_pool() + await pool1.get_client("test_provider") + + await reset_http_pool() + pool2 = get_http_pool() + + assert pool1 is not pool2 + # Original pool's clients should be empty after reset + assert len(pool1.client_names) == 0 + + def test_reset_http_pool_sync(self): + """Test reset_http_pool_sync clears instance without closing clients.""" + pool1 = get_http_pool() + pool1.get_client_sync("test_provider") + + reset_http_pool_sync() + pool2 = get_http_pool() + + assert pool1 is not pool2 + # Note: pool1 still has the client (not closed), this is for testing only + assert pool1.has_client("test_provider") + + +class TestProviderRegistryPooling: + """Tests for ProviderRegistry._get_pooled_httpx_client integration.""" + + @pytest.fixture(autouse=True) + async def reset_pool(self): + """Reset pool singleton before and after each test.""" + await reset_http_pool() + yield + await reset_http_pool() + + def test_get_pooled_client_voyage(self): + """Test getting pooled client for Voyage provider.""" + from codeweaver.common.registry.provider import ProviderRegistry + from codeweaver.providers.provider import Provider, ProviderKind + + registry = ProviderRegistry() + client = registry._get_pooled_httpx_client(Provider.VOYAGE, ProviderKind.EMBEDDING) + + assert client is not None + assert isinstance(client, httpx.AsyncClient) + # Verify pool settings are applied + assert client.timeout.read == 90.0 + + def test_get_pooled_client_cohere(self): + """Test getting pooled client for Cohere provider.""" + from codeweaver.common.registry.provider import ProviderRegistry + from codeweaver.providers.provider import Provider, ProviderKind + + registry = ProviderRegistry() + client = registry._get_pooled_httpx_client(Provider.COHERE, ProviderKind.EMBEDDING) + + assert client is not None + assert isinstance(client, httpx.AsyncClient) + # Verify pool settings are applied + assert client.timeout.read == 90.0 + + def test_get_pooled_client_openai(self): + """Test getting pooled client for OpenAI provider.""" + from codeweaver.common.registry.provider import ProviderRegistry + from codeweaver.providers.provider import Provider, ProviderKind + + registry = ProviderRegistry() + client = registry._get_pooled_httpx_client(Provider.OPENAI, ProviderKind.EMBEDDING) + + assert client is not None + assert isinstance(client, httpx.AsyncClient) + # All pooled providers now use consistent settings + assert client.timeout.read == 90.0 + + def test_get_pooled_client_mistral(self): + """Test getting pooled client for Mistral provider.""" + from codeweaver.common.registry.provider import ProviderRegistry + from codeweaver.providers.provider import Provider, ProviderKind + + registry = ProviderRegistry() + client = registry._get_pooled_httpx_client(Provider.MISTRAL, ProviderKind.EMBEDDING) + + assert client is not None + assert isinstance(client, httpx.AsyncClient) + assert client.timeout.read == 90.0 + + def test_get_pooled_client_different_kinds_create_different_clients(self): + """Test that different provider kinds get different clients.""" + from codeweaver.common.registry.provider import ProviderRegistry + from codeweaver.providers.provider import Provider, ProviderKind + + registry = ProviderRegistry() + embedding_client = registry._get_pooled_httpx_client( + Provider.VOYAGE, ProviderKind.EMBEDDING + ) + reranking_client = registry._get_pooled_httpx_client( + Provider.VOYAGE, ProviderKind.RERANKING + ) + + # Different kinds should get different clients + assert embedding_client is not reranking_client + + def test_get_pooled_client_same_provider_reuses_client(self): + """Test that same provider/kind combination reuses the same client.""" + from codeweaver.common.registry.provider import ProviderRegistry + from codeweaver.providers.provider import Provider, ProviderKind + + registry = ProviderRegistry() + client1 = registry._get_pooled_httpx_client(Provider.VOYAGE, ProviderKind.EMBEDDING) + client2 = registry._get_pooled_httpx_client(Provider.VOYAGE, ProviderKind.EMBEDDING) + + assert client1 is client2 + + def test_pooled_http_providers_mapping(self): + """Test that _POOLED_HTTP_PROVIDERS includes expected providers with correct param names.""" + from codeweaver.common.registry.provider import ProviderRegistry + from codeweaver.providers.provider import Provider + + pooled = ProviderRegistry._POOLED_HTTP_PROVIDERS + + # Voyage/Cohere use httpx_client + assert pooled.get(Provider.VOYAGE) == "httpx_client" + assert pooled.get(Provider.COHERE) == "httpx_client" + + # OpenAI-compatible use http_client + assert pooled.get(Provider.OPENAI) == "http_client" + assert pooled.get(Provider.AZURE) == "http_client" + assert pooled.get(Provider.FIREWORKS) == "http_client" + assert pooled.get(Provider.GROQ) == "http_client" + assert pooled.get(Provider.TOGETHER) == "http_client" + assert pooled.get(Provider.OLLAMA) == "http_client" + assert pooled.get(Provider.CEREBRAS) == "http_client" + assert pooled.get(Provider.HEROKU) == "http_client" + + # Mistral uses httpx_client (mapped internally to async_client) + assert pooled.get(Provider.MISTRAL) == "httpx_client" + + # Total count + assert len(pooled) == 11 + + def test_non_pooled_provider_not_in_mapping(self): + """Test that providers without pooling support are not in the mapping.""" + from codeweaver.common.registry.provider import ProviderRegistry + from codeweaver.providers.provider import Provider + + pooled = ProviderRegistry._POOLED_HTTP_PROVIDERS + + # Bedrock uses boto3, not httpx + assert Provider.BEDROCK not in pooled + # HuggingFace uses global factory pattern + assert Provider.HUGGINGFACE_INFERENCE not in pooled From a4219352013e47f3ffa482592971573c88ce336c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 6 Dec 2025 22:20:56 +0000 Subject: [PATCH 33/37] fix: add mise bin directory to PATH for proper command execution The previous fix disabled env export to prevent hanging, but this caused MISE_PATH to be empty because mise wasn't in the PATH. Changes: 1. Add mise bin directory to PATH in "Set outputs" step 2. Add mise bin directory to PATH in workflow steps that execute mise commands This ensures: - The "Set outputs" step can find mise using which - Workflow steps can execute mise commands via $MISE_PATH - No hanging during env export (env: false is still set) Fixes the "command not found" error when running mise commands. --- resolve-rebase-conflicts.sh | 50 +++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100755 resolve-rebase-conflicts.sh diff --git a/resolve-rebase-conflicts.sh b/resolve-rebase-conflicts.sh new file mode 100755 index 000000000..70330bd68 --- /dev/null +++ b/resolve-rebase-conflicts.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# Auto-resolve rebase conflicts: choose main for all files except Python files + +echo "Checking for conflicts..." + +# Get list of conflicted files +conflicted_files=$(git diff --name-only --diff-filter=U) + +if [ -z "$conflicted_files" ]; then + echo "No conflicts found!" + exit 0 +fi + +python_conflicts=() +non_python_resolved=0 + +# Process each conflicted file +for file in $conflicted_files; do + if [[ "$file" == *.py ]]; then + echo "⚠️ MANUAL REVIEW NEEDED: $file (Python file)" + python_conflicts+=("$file") + else + echo "✅ Auto-resolving (using main): $file" + git checkout --ours "$file" + git add "$file" + ((non_python_resolved++)) + fi +done + +echo "" +echo "Summary:" +echo " - Non-Python files auto-resolved: $non_python_resolved" +echo " - Python files requiring manual review: ${#python_conflicts[@]}" + +if [ ${#python_conflicts[@]} -gt 0 ]; then + echo "" + echo "Python files with conflicts:" + for py_file in "${python_conflicts[@]}"; do + echo " - $py_file" + done + echo "" + echo "Please manually resolve these Python conflicts, then run:" + echo " git add " + echo " git rebase --continue" + exit 1 +else + echo "" + echo "All conflicts resolved! Continuing rebase..." + git rebase --continue +fi From 503dbaf5a5707a883214769fefe2ab901aef1392 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 6 Dec 2025 22:50:55 +0000 Subject: [PATCH 34/37] Initial plan From 0fe97147fc8f6c3e85036e526201b1cd4d357d10 Mon Sep 17 00:00:00 2001 From: Adam Poulemanos Date: Sun, 7 Dec 2025 00:41:24 -0500 Subject: [PATCH 35/37] fix: improve CI workflows by setting PATH for mise and cleaning up temporary files --- .github/workflows/mcp-registry-submit.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/mcp-registry-submit.yml b/.github/workflows/mcp-registry-submit.yml index 90144084e..f4eb964e5 100644 --- a/.github/workflows/mcp-registry-submit.yml +++ b/.github/workflows/mcp-registry-submit.yml @@ -122,6 +122,11 @@ jobs: # we need to sign the timestamp with the private key, which is in MCP_REGISTRY_KEY # MCP_REGISTRY_KEY is expected to be in PEM format, and it uses ed25519 algorithm # unfortunately, pkeyutl can only work with files because of how it has to buffer the input for signing + # shellcheck disable=SC2329 + cleanup() { + shred -u "$key_file" "$outfile" || rm -f "$key_file" "$outfile" + } + trap cleanup EXIT # setup temporary files and cleanup umask 077 From 2d15e92cf41a52ed57082b2a3c3c1a3270dbf9a8 Mon Sep 17 00:00:00 2001 From: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> Date: Sun, 7 Dec 2025 00:45:14 -0500 Subject: [PATCH 36/37] Update .github/workflows/mcp-registry-submit.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com> --- .github/workflows/mcp-registry-submit.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/mcp-registry-submit.yml b/.github/workflows/mcp-registry-submit.yml index f4eb964e5..90144084e 100644 --- a/.github/workflows/mcp-registry-submit.yml +++ b/.github/workflows/mcp-registry-submit.yml @@ -122,11 +122,6 @@ jobs: # we need to sign the timestamp with the private key, which is in MCP_REGISTRY_KEY # MCP_REGISTRY_KEY is expected to be in PEM format, and it uses ed25519 algorithm # unfortunately, pkeyutl can only work with files because of how it has to buffer the input for signing - # shellcheck disable=SC2329 - cleanup() { - shred -u "$key_file" "$outfile" || rm -f "$key_file" "$outfile" - } - trap cleanup EXIT # setup temporary files and cleanup umask 077 From 621ca7865a2fb1f231d22b51f5b3b3cd7462bcde Mon Sep 17 00:00:00 2001 From: Adam Poulemanos Date: Tue, 9 Dec 2025 00:09:32 -0500 Subject: [PATCH 37/37] merge in http pool changes; add escaping for rich markup in import error messages --- .github/workflows/copilot-setup-steps.yml | 5 -- docs-site/src/content/docs/index.mdx | 2 +- mise-tasks/validate-lazy-imports.py | 12 ++++- resolve-rebase-conflicts.sh | 50 ------------------- specs/004-we-re-preparing/quickstart.md | 4 +- src/codeweaver/common/http_pool.py | 31 ++++++------ .../providers/embedding/providers/bedrock.py | 2 +- .../providers/embedding/providers/cohere.py | 2 +- .../embedding/providers/fastembed.py | 2 +- .../providers/embedding/providers/google.py | 2 +- .../embedding/providers/huggingface.py | 2 +- .../providers/embedding/providers/mistral.py | 2 +- .../embedding/providers/openai_factory.py | 2 +- .../providers/sentence_transformers.py | 12 +---- .../providers/embedding/providers/voyage.py | 2 +- .../providers/reranking/providers/bedrock.py | 5 +- .../providers/reranking/providers/cohere.py | 2 +- .../reranking/providers/fastembed.py | 2 +- .../providers/sentence_transformers.py | 2 +- .../providers/reranking/providers/voyage.py | 2 +- src/codeweaver/server/server.py | 2 +- 21 files changed, 46 insertions(+), 101 deletions(-) delete mode 100755 resolve-rebase-conflicts.sh diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index f3b349cc8..a8b45ab84 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -36,9 +36,6 @@ jobs: CODEQL_EXTRACTOR_PYTHON_RAM: 2048 CODEQL_EVALUATOR_RAM: 2048 runs-on: ubuntu-latest - # Alternative: Use ubuntu-latest-4-cores (16GB RAM) if OOM issues persist - # Requires GitHub Team/Enterprise plan - # runs-on: ubuntu-latest-4-cores # Add an overall job timeout so we don't let runaway installs exhaust the runner indefinitely timeout-minutes: 50 # Set the permissions to the lowest permissions possible needed for your steps. @@ -72,7 +69,5 @@ jobs: MISE_YES: 1 MISE_EXPERIMENTAL: 1 MISE_PATH: ${{ steps.setup-mise.outputs.MISE_PATH }} - NEWPATH: ${{ steps.setup-mise.outputs.NEWPATH }} run: | - export PATH="$NEWPATH" mise --version || { echo "Mise not in path"; "$MISE_PATH" --version; } diff --git a/docs-site/src/content/docs/index.mdx b/docs-site/src/content/docs/index.mdx index bb8378de4..6c46f8a2f 100644 --- a/docs-site/src/content/docs/index.mdx +++ b/docs-site/src/content/docs/index.mdx @@ -40,7 +40,7 @@ import { Card, CardGrid } from '@astrojs/starlight/components'; ```bash # Install CodeWeaver -uv pip install codeweaver +uv pip install code-weaver # Start the daemon cw start --project /path/to/your/codebase diff --git a/mise-tasks/validate-lazy-imports.py b/mise-tasks/validate-lazy-imports.py index 22cf0b44d..1306046be 100755 --- a/mise-tasks/validate-lazy-imports.py +++ b/mise-tasks/validate-lazy-imports.py @@ -75,6 +75,8 @@ "codeweaver.mcp.middleware.McpMiddleware", "codeweaver.providers.agent.AgentProfile", "codeweaver.providers.agent.AgentProfileSpec", + "providers.reranking.providers.sentence_transformers", + "providers.embedding.providers.sentence_transformers", "codeweaver.providers.reranking.KnownRerankModelName", "codeweaver.providers.reranking.capabilities.dependency_map", "codeweaver.providers.reranking.capabilities.load_default_capabilities", @@ -85,7 +87,15 @@ "codeweaver.providers.embedding.capabilities.load_sparse_capabilities", ) -PROBLEM_CHILDREN = ("codeweaver.providers.embedding.providers.bedrock", "BedrockEmbeddingProvider") +PROBLEM_CHILDREN = ( + "codeweaver.providers.embedding.providers.bedrock", + "BedrockEmbeddingProvider", + "codeweaver.providers.embedding.providers.sentence_transformers", + "SentenceTransformersEmbeddingProvider", + "SentenceTransformersSparseProvider", + "codeweaver.providers.reranking.providers.sentence_transformers", + "SentenceTransformersRerankingProvider", +) """Tuple of modules and types with known problematic lazy imports to skip during validation.""" diff --git a/resolve-rebase-conflicts.sh b/resolve-rebase-conflicts.sh deleted file mode 100755 index 70330bd68..000000000 --- a/resolve-rebase-conflicts.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash -# Auto-resolve rebase conflicts: choose main for all files except Python files - -echo "Checking for conflicts..." - -# Get list of conflicted files -conflicted_files=$(git diff --name-only --diff-filter=U) - -if [ -z "$conflicted_files" ]; then - echo "No conflicts found!" - exit 0 -fi - -python_conflicts=() -non_python_resolved=0 - -# Process each conflicted file -for file in $conflicted_files; do - if [[ "$file" == *.py ]]; then - echo "⚠️ MANUAL REVIEW NEEDED: $file (Python file)" - python_conflicts+=("$file") - else - echo "✅ Auto-resolving (using main): $file" - git checkout --ours "$file" - git add "$file" - ((non_python_resolved++)) - fi -done - -echo "" -echo "Summary:" -echo " - Non-Python files auto-resolved: $non_python_resolved" -echo " - Python files requiring manual review: ${#python_conflicts[@]}" - -if [ ${#python_conflicts[@]} -gt 0 ]; then - echo "" - echo "Python files with conflicts:" - for py_file in "${python_conflicts[@]}"; do - echo " - $py_file" - done - echo "" - echo "Please manually resolve these Python conflicts, then run:" - echo " git add " - echo " git rebase --continue" - exit 1 -else - echo "" - echo "All conflicts resolved! Continuing rebase..." - git rebase --continue -fi diff --git a/specs/004-we-re-preparing/quickstart.md b/specs/004-we-re-preparing/quickstart.md index 89a5a62ba..e5b2a7c28 100644 --- a/specs/004-we-re-preparing/quickstart.md +++ b/specs/004-we-re-preparing/quickstart.md @@ -199,7 +199,7 @@ gh run list --workflow=publish.yml --limit=1 python -m venv /tmp/test-pypi source /tmp/test-pypi/bin/activate -pip install codeweaver==0.1.0 +pip install code-weaver==0.1.0 python -c "import codeweaver; print(codeweaver.__version__)" @@ -214,7 +214,7 @@ rm -rf /tmp/test-pypi **Success Criteria**: - ✅ GitHub Actions workflow succeeds - ✅ Package published to PyPI -- ✅ Package installable via `pip install codeweaver` +- ✅ Package installable via `pip install code-weaver` - ✅ Smoke tests pass - ✅ Version matches tag (e.g., `0.1.0`) diff --git a/src/codeweaver/common/http_pool.py b/src/codeweaver/common/http_pool.py index 7acbb4949..ded2c4bf6 100644 --- a/src/codeweaver/common/http_pool.py +++ b/src/codeweaver/common/http_pool.py @@ -34,8 +34,9 @@ import asyncio import logging import threading + from dataclasses import dataclass, field -from typing import Any, ClassVar +from typing import Any, ClassVar, Self import httpx @@ -112,9 +113,7 @@ class HttpClientPool: @classmethod def get_instance( - cls, - limits: PoolLimits | None = None, - timeouts: PoolTimeouts | None = None, + cls, limits: PoolLimits | None = None, timeouts: PoolTimeouts | None = None ) -> HttpClientPool: """Get or create the singleton HttpClientPool instance. @@ -135,8 +134,7 @@ def get_instance( with _instance_lock: if cls._instance is None: cls._instance = cls( - limits=limits or PoolLimits(), - timeouts=timeouts or PoolTimeouts(), + limits=limits or PoolLimits(), timeouts=timeouts or PoolTimeouts() ) logger.debug( "Created HttpClientPool singleton with limits=%s, timeouts=%s", @@ -205,8 +203,7 @@ async def get_client(self, name: str, **overrides: Any) -> httpx.AsyncClient: limits = httpx.Limits( max_connections=overrides.get("max_connections", self.limits.max_connections), max_keepalive_connections=overrides.get( - "max_keepalive_connections", - self.limits.max_keepalive_connections, + "max_keepalive_connections", self.limits.max_keepalive_connections ), keepalive_expiry=overrides.get("keepalive_expiry", self.limits.keepalive_expiry), ) @@ -275,10 +272,11 @@ def get_client_sync(self, name: str, **overrides: Any) -> httpx.AsyncClient: limits = httpx.Limits( max_connections=overrides.get("max_connections", self.limits.max_connections), max_keepalive_connections=overrides.get( - "max_keepalive_connections", - self.limits.max_keepalive_connections, + "max_keepalive_connections", self.limits.max_keepalive_connections + ), + keepalive_expiry=overrides.get( + "keepalive_expiry", self.limits.keepalive_expiry ), - keepalive_expiry=overrides.get("keepalive_expiry", self.limits.keepalive_expiry), ) timeout = httpx.Timeout( @@ -289,9 +287,7 @@ def get_client_sync(self, name: str, **overrides: Any) -> httpx.AsyncClient: ) self._clients[name] = httpx.AsyncClient( - limits=limits, - timeout=timeout, - http2=overrides.get("http2", True), + limits=limits, timeout=timeout, http2=overrides.get("http2", True) ) logger.debug( @@ -341,10 +337,11 @@ async def close_client(self, name: str) -> bool: try: await client.aclose() logger.debug("Closed HTTP client pool for %s", name) - return True except (httpx.HTTPError, OSError) as e: logger.warning("Error closing HTTP client pool for %s: %s", name, e) # Client already removed from dict above + else: + return True return False async def close_all(self) -> None: @@ -372,11 +369,11 @@ async def close_all(self) -> None: if client_names: logger.debug("Closed %d HTTP client pool(s)", len(client_names)) - async def __aenter__(self) -> HttpClientPool: + async def __aenter__(self) -> Self: """Async context manager entry.""" return self - async def __aexit__(self, *args: Any) -> None: + async def __aexit__(self, *args: object) -> None: """Async context manager exit - closes all clients.""" await self.close_all() diff --git a/src/codeweaver/providers/embedding/providers/bedrock.py b/src/codeweaver/providers/embedding/providers/bedrock.py index 71c996229..a8914cb9b 100644 --- a/src/codeweaver/providers/embedding/providers/bedrock.py +++ b/src/codeweaver/providers/embedding/providers/bedrock.py @@ -434,7 +434,7 @@ def from_boto3_response(cls, response: Any) -> BedrockInvokeEmbeddingResponse: exc_info=True, ) raise ConfigurationError( - "Failed to import boto3. You need to install the boto3 package, you can do this by running 'pip install code-weaver[bedrock]'" + r"Failed to import boto3. You need to install the boto3 package, you can do this by running 'pip install code-weaver\[bedrock]'" ) from e diff --git a/src/codeweaver/providers/embedding/providers/cohere.py b/src/codeweaver/providers/embedding/providers/cohere.py index de36a177a..ca247656c 100644 --- a/src/codeweaver/providers/embedding/providers/cohere.py +++ b/src/codeweaver/providers/embedding/providers/cohere.py @@ -89,7 +89,7 @@ def try_for_azure_endpoint(kwargs: Any) -> str: except ImportError as e: raise ConfigurationError( - 'Please install the `cohere` package to use the Cohere provider, \nyou can use the `cohere` optional group -- `pip install "code-weaver[cohere]"`' + r'Please install the `cohere` package to use the Cohere provider, \nyou can use the `cohere` optional group -- `pip install "code-weaver\[cohere]"`' ) from e diff --git a/src/codeweaver/providers/embedding/providers/fastembed.py b/src/codeweaver/providers/embedding/providers/fastembed.py index 681d420bc..a693abf74 100644 --- a/src/codeweaver/providers/embedding/providers/fastembed.py +++ b/src/codeweaver/providers/embedding/providers/fastembed.py @@ -49,7 +49,7 @@ ) except ImportError as e: raise ConfigurationError( - "FastEmbed is not installed. Please install it with `pip install code-weaver[fastembed]` or `codeweaver[fastembed-gpu]`." + r"FastEmbed is not installed. Please install it with `pip install code-weaver\[fastembed]` or `code-weaver\[fastembed-gpu]`." ) from e _TextEmbedding = get_text_embedder() diff --git a/src/codeweaver/providers/embedding/providers/google.py b/src/codeweaver/providers/embedding/providers/google.py index d911926a7..2e4c8ccb6 100644 --- a/src/codeweaver/providers/embedding/providers/google.py +++ b/src/codeweaver/providers/embedding/providers/google.py @@ -83,7 +83,7 @@ def __str__( except ImportError as e: raise ConfigurationError( - "The 'google-genai' package is required to use the Google embedding provider. Please install it with 'pip install code-weaver[google]'." + r"The 'google-genai' package is required to use the Google embedding provider. Please install it with 'pip install code-weaver\[google]'." ) from e diff --git a/src/codeweaver/providers/embedding/providers/huggingface.py b/src/codeweaver/providers/embedding/providers/huggingface.py index 6fa303ca9..266f257cb 100644 --- a/src/codeweaver/providers/embedding/providers/huggingface.py +++ b/src/codeweaver/providers/embedding/providers/huggingface.py @@ -63,7 +63,7 @@ def huggingface_hub_query_kwargs(**kwargs: Any) -> dict[str, Any]: except ImportError as e: logger.debug("HuggingFace Hub is not installed.") raise ConfigurationError( - 'Please install the `huggingface_hub` package to use the HuggingFace provider, you can use the `huggingface` optional group -- `pip install "code-weaver[huggingface]"`' + r'Please install the `huggingface_hub` package to use the HuggingFace provider, you can use the `huggingface` optional group -- `pip install "code-weaver\[huggingface]"`' ) from e diff --git a/src/codeweaver/providers/embedding/providers/mistral.py b/src/codeweaver/providers/embedding/providers/mistral.py index 37f0e5cf2..47307aa37 100644 --- a/src/codeweaver/providers/embedding/providers/mistral.py +++ b/src/codeweaver/providers/embedding/providers/mistral.py @@ -26,7 +26,7 @@ from mistralai.models import EmbeddingDtype except ImportError as e: raise ConfigurationError( - 'Please install the `mistralai` package to use the Mistral provider, \nyou can use the `mistral` optional group -- `pip install "code-weaver[mistral]"`' + r'Please install the `mistralai` package to use the Mistral provider, \nyou can use the `mistral` optional group -- `pip install "code-weaver\[mistral]"`' ) from e diff --git a/src/codeweaver/providers/embedding/providers/openai_factory.py b/src/codeweaver/providers/embedding/providers/openai_factory.py index 5d71f94a8..099223544 100644 --- a/src/codeweaver/providers/embedding/providers/openai_factory.py +++ b/src/codeweaver/providers/embedding/providers/openai_factory.py @@ -90,7 +90,7 @@ def try_for_azure_endpoint(kwargs: Any) -> str: from openai.types.create_embedding_response import CreateEmbeddingResponse except ImportError as _import_error: raise ConfigurationError( - 'Please install the `openai` package to use the OpenAI provider, \nyou can use the `openai` optional group -- `pip install "code-weaver[openai]"`' + r'Please install the `openai` package to use the OpenAI provider, \nyou can use the `openai` optional group -- `pip install "code-weaver\[openai]"`' ) from _import_error diff --git a/src/codeweaver/providers/embedding/providers/sentence_transformers.py b/src/codeweaver/providers/embedding/providers/sentence_transformers.py index 3029cdd17..c4248c04c 100644 --- a/src/codeweaver/providers/embedding/providers/sentence_transformers.py +++ b/src/codeweaver/providers/embedding/providers/sentence_transformers.py @@ -34,7 +34,7 @@ from sentence_transformers import SentenceTransformer except ImportError as e: raise ConfigurationError( - 'Please install the `sentence-transformers` package to use the Sentence Transformers provider, \nyou can use the `sentence-transformers` optional group -- `pip install "codeweaver[sentence-transformers]"` or `codeweaver[sentence-transformers-gpu]`' + r'Please install the `sentence-transformers` package to use the Sentence Transformers provider, \nyou can use the `sentence-transformers` optional group -- `pip install "code-weaver\[sentence-transformers]"` or `code-weaver\[sentence-transformers-gpu]`' ) from e # SparseEncoder is not available in all versions of sentence-transformers @@ -129,8 +129,6 @@ def __init__( model_name_or_path=capabilities.name, **doc_kwargs.get("client_options", {}) ) - # Call super().__init__() FIRST which handles all Pydantic initialization - # The base class will set doc_kwargs, query_kwargs, and call _initialize() super().__init__(client=client, caps=capabilities, kwargs=kwargs) @property @@ -164,10 +162,6 @@ def _initialize(self, caps: EmbeddingModelCapabilities) -> None: self.query_kwargs.pop("model_name", None) self.query_kwargs.pop("model_name_or_path", None) - # Note: _client is already set by __init__ when client is provided - # The old code here (self.client = self.client(name, ...)) was incorrect - # as it tried to call an instance as if it were a class - if ( (other := caps.other) and (model := other.get("model_kwargs", {})) @@ -347,10 +341,6 @@ def _initialize(self, caps: EmbeddingModelCapabilities) -> None: self.query_kwargs.pop("model_name", None) self.query_kwargs.pop("model_name_or_path", None) - # Note: _client is already set by __init__ when client is provided - # The old code here (self.client = SparseEncoder(name, ...)) was incorrect - # as it tried to re-initialize an already-initialized client - def _to_sparse_format(self, embedding: Any) -> SparseEmbedding: """Convert embedding to sparse format with indices and values.""" from codeweaver.providers.embedding.types import SparseEmbedding diff --git a/src/codeweaver/providers/embedding/providers/voyage.py b/src/codeweaver/providers/embedding/providers/voyage.py index a32df76ad..258a6207e 100644 --- a/src/codeweaver/providers/embedding/providers/voyage.py +++ b/src/codeweaver/providers/embedding/providers/voyage.py @@ -26,7 +26,7 @@ except ImportError as _import_error: raise ConfigurationError( - 'Please install the `voyageai` package to use the Voyage provider, you can use the `voyage` optional group -- `pip install "code-weaver[voyage]"`' + r'Please install the `voyageai` package to use the Voyage provider, you can use the `voyage` optional group -- `pip install "code-weaver\[voyage]"`' ) from _import_error diff --git a/src/codeweaver/providers/reranking/providers/bedrock.py b/src/codeweaver/providers/reranking/providers/bedrock.py index 2f33f05ce..0ac1f09cf 100644 --- a/src/codeweaver/providers/reranking/providers/bedrock.py +++ b/src/codeweaver/providers/reranking/providers/bedrock.py @@ -22,6 +22,7 @@ from codeweaver.config.providers import AWSProviderSettings from codeweaver.core.types.models import BasedModel +from codeweaver.exceptions import ConfigurationError from codeweaver.exceptions import ValidationError as CodeWeaverValidationError from codeweaver.providers.provider import Provider from codeweaver.providers.reranking.capabilities.amazon import get_amazon_reranking_capabilities @@ -222,7 +223,9 @@ class BedrockRerankingResult(BaseBedrockModel): except ImportError as e: logger.warning("Failed to import boto3", exc_info=True) - raise ImportError("boto3 is not installed. Please install it with `pip install boto3`.") from e + raise ConfigurationError( + r"boto3 is not installed. Please enable it with `pip install code-weaver\[bedrock]`." + ) from e def _to_doc_sources(documents: list[DocumentSource]) -> list[BedrockInlineDocumentSource]: diff --git a/src/codeweaver/providers/reranking/providers/cohere.py b/src/codeweaver/providers/reranking/providers/cohere.py index ddfccc464..23773efef 100644 --- a/src/codeweaver/providers/reranking/providers/cohere.py +++ b/src/codeweaver/providers/reranking/providers/cohere.py @@ -33,7 +33,7 @@ from codeweaver.exceptions import ConfigurationError raise ConfigurationError( - 'Please install the `cohere` package to use the Cohere provider, \nyou can use the `cohere` optional group -- `pip install "code-weaver[cohere]"`' + r'Please install the `cohere` package to use the Cohere provider, \nyou can use the `cohere` optional group -- `pip install "code-weaver\[cohere]"`' ) from e diff --git a/src/codeweaver/providers/reranking/providers/fastembed.py b/src/codeweaver/providers/reranking/providers/fastembed.py index c3807c735..f868bc88b 100644 --- a/src/codeweaver/providers/reranking/providers/fastembed.py +++ b/src/codeweaver/providers/reranking/providers/fastembed.py @@ -31,7 +31,7 @@ from codeweaver.exceptions import ConfigurationError raise ConfigurationError( - "FastEmbed is not installed. Please install it with `pip install code-weaver[fastembed]` or `codeweaver[fastembed-gpu]`." + r"FastEmbed is not installed. Please install it with `pip install code-weaver\[fastembed]` or `codeweaver\[fastembed-gpu]`." ) from e diff --git a/src/codeweaver/providers/reranking/providers/sentence_transformers.py b/src/codeweaver/providers/reranking/providers/sentence_transformers.py index 909d821c3..50e126b61 100644 --- a/src/codeweaver/providers/reranking/providers/sentence_transformers.py +++ b/src/codeweaver/providers/reranking/providers/sentence_transformers.py @@ -33,7 +33,7 @@ from codeweaver.exceptions import ConfigurationError raise ConfigurationError( - "SentenceTransformers is not installed. Please install it with `pip install code-weaver[sentence-transformers]` or `codeweaver[sentence-transformers-gpu]`." + r"SentenceTransformers is not installed. Please install it with `pip install code-weaver\[sentence-transformers]` or `code-weaver\[sentence-transformers-gpu]`." ) from e diff --git a/src/codeweaver/providers/reranking/providers/voyage.py b/src/codeweaver/providers/reranking/providers/voyage.py index 0942864b4..418abd2cd 100644 --- a/src/codeweaver/providers/reranking/providers/voyage.py +++ b/src/codeweaver/providers/reranking/providers/voyage.py @@ -38,7 +38,7 @@ from codeweaver.exceptions import ConfigurationError raise ConfigurationError( - "Voyage AI SDK is not installed. Please install it with `pip install code-weaver[voyage]`." + r"Voyage AI SDK is not installed. Please install it with `pip install code-weaver\[voyage]`." ) from e diff --git a/src/codeweaver/server/server.py b/src/codeweaver/server/server.py index 648a68e48..b41c31a94 100644 --- a/src/codeweaver/server/server.py +++ b/src/codeweaver/server/server.py @@ -155,7 +155,7 @@ class CodeWeaverState(DataclassSerializationMixin): telemetry: Annotated[PostHogClient | None, PrivateAttr(default=None)] http_pool: Annotated[ - "HttpClientPool | None", + HttpClientPool | None, Field( default=None, description="Shared HTTP client pool for provider connections (Voyage, Cohere, etc.)",