chrore: major overhaul of docs, commands, and workflows (#23)

* ci: add status badges to README

* chore: major overhaul - docs, commands & workflows

* fix: general fixes and improvements

* chore: various updates and fixes

* chore: minor fixes
This commit is contained in:
Sean P. Kane
2026-01-07 09:44:45 -08:00
committed by GitHub
parent d7d6c2c8e7
commit a775852ce9
53 changed files with 4684 additions and 1486 deletions
-3
View File
@@ -3,6 +3,3 @@ contact_links:
- name: Documentation
url: https://github.com/spkane/freecad-robust-mcp-and-more#readme
about: Check the README for usage instructions and troubleshooting
- name: Discussions
url: https://github.com/spkane/freecad-robust-mcp-and-more/discussions
about: Ask questions and discuss ideas with the community
+2
View File
@@ -8,6 +8,8 @@ on:
schedule:
# Run weekly on Sundays at midnight UTC
- cron: "0 0 * * 0"
workflow_dispatch:
workflow_call:
# Cancel in-progress runs for the same branch
concurrency:
-279
View File
@@ -1,279 +0,0 @@
name: Docker Release
# Only trigger on GitHub release publication, not on tag push.
# When a release is created, GitHub fires both 'release' and 'push' events.
# Using only 'release' prevents duplicate workflow runs.
on:
release:
types: [published]
# Cancel in-progress runs for the same tag
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
DOCKERHUB_REPO: spkane/freecad-robust-mcp
jobs:
release:
name: Build and Push to Docker Hub
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Validate semantic version tag
id: version
run: |
TAG="${GITHUB_REF#refs/tags/}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
# Validate semantic versioning format (v1.2.3 or v1.2.3-prerelease)
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
echo "ERROR: Tag '$TAG' does not follow semantic versioning (vX.Y.Z or vX.Y.Z-prerelease)"
exit 1
fi
# Extract version without 'v' prefix
VERSION="${TAG#v}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
# Extract major.minor for additional tag
MAJOR_MINOR=$(echo "$VERSION" | cut -d. -f1,2)
echo "major_minor=$MAJOR_MINOR" >> "$GITHUB_OUTPUT"
# Extract major for additional tag
MAJOR=$(echo "$VERSION" | cut -d. -f1)
echo "major=$MAJOR" >> "$GITHUB_OUTPUT"
# Check if this is a prerelease
if [[ "$TAG" =~ -[a-zA-Z0-9.]+ ]]; then
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
IS_PRERELEASE="true"
else
echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
IS_PRERELEASE="false"
fi
echo "Parsed version: $VERSION (major: $MAJOR, major.minor: $MAJOR_MINOR, prerelease: $IS_PRERELEASE)"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.DOCKERHUB_REPO }}
tags: |
# Full semantic version (v1.2.3 -> 1.2.3)
type=semver,pattern={{version}}
# Major.minor version (v1.2.3 -> 1.2)
type=semver,pattern={{major}}.{{minor}}
# Major version only (v1.2.3 -> 1) - only for stable releases
type=semver,pattern={{major}},enable=${{ steps.version.outputs.is_prerelease == 'false' }}
# Latest tag - only for stable releases
type=raw,value=latest,enable=${{ steps.version.outputs.is_prerelease == 'false' }}
labels: |
org.opencontainers.image.title=FreeCAD Robust MCP Server
org.opencontainers.image.description=Model Context Protocol server for FreeCAD integration
org.opencontainers.image.vendor=spkane
org.opencontainers.image.version=${{ steps.version.outputs.version }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: true
sbom: true
- name: Test pushed image
run: |
# Pull and test the image
docker pull ${{ env.DOCKERHUB_REPO }}:${{ steps.version.outputs.version }}
# Test that the container starts and responds to MCP initialize
echo '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | \
timeout 30 docker run --rm -i \
-e FREECAD_MODE=xmlrpc \
${{ env.DOCKERHUB_REPO }}:${{ steps.version.outputs.version }} 2>&1 | \
grep -q '"result"' && echo "Container test passed" || echo "Container test completed"
- name: Create Docker Hub README
run: |
# Create a concise README for Docker Hub (limit is 25000 bytes)
cat > ./DOCKERHUB_README.md << 'EOF'
# FreeCAD Robust MCP Server
An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that enables integration between AI assistants (Claude, GPT, and other MCP-compatible tools) and [FreeCAD](https://www.freecadweb.org/), allowing AI-assisted development and debugging of 3D models, macros, and workbenches.
## Features
- **82+ MCP Tools**: Comprehensive CAD operations including primitives, PartDesign, booleans, export
- **Multiple Connection Modes**: XML-RPC (recommended), JSON-RPC socket, or embedded
- **GUI & Headless Support**: Full modeling in headless mode, plus screenshots/colors in GUI mode
- **Macro Development**: Create, edit, run, and template FreeCAD macros via MCP
## Quick Start
```bash
docker pull spkane/freecad-robust-mcp:latest
```
### Running with Claude Desktop (macOS/Windows)
Add to your Claude Desktop configuration:
```json
{
"mcpServers": {
"freecad": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "FREECAD_MODE=xmlrpc",
"-e", "FREECAD_SOCKET_HOST=host.docker.internal",
"spkane/freecad-robust-mcp:latest"
]
}
}
}
```
### Linux Users
`host.docker.internal` is only available on Docker Desktop (macOS/Windows).
On Linux Docker Engine, use one of these alternatives:
**Option 1: Host networking (recommended)**
```bash
docker run -i --rm --network=host \
-e FREECAD_MODE=xmlrpc \
-e FREECAD_SOCKET_HOST=localhost \
spkane/freecad-robust-mcp:latest
```
**Option 2: Use host IP address**
```bash
docker run -i --rm \
-e FREECAD_MODE=xmlrpc \
-e FREECAD_SOCKET_HOST=192.168.1.100 \
spkane/freecad-robust-mcp:latest
```
**Option 3: Add host gateway (Docker 20.10+)**
```bash
docker run -i --rm --add-host=host.docker.internal:host-gateway \
-e FREECAD_MODE=xmlrpc \
-e FREECAD_SOCKET_HOST=host.docker.internal \
spkane/freecad-robust-mcp:latest
```
## Connection Modes
| Mode | Description | Use Case |
|------|-------------|----------|
| `xmlrpc` | XML-RPC over HTTP (port 9875) | **Recommended** - Works with FreeCAD GUI or headless |
| `socket` | JSON-RPC over TCP (port 9876) | Alternative to XML-RPC |
| `embedded` | Direct Python import | Linux only, requires FreeCAD in Python path |
## Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `FREECAD_MODE` | `xmlrpc` | Connection mode |
| `FREECAD_SOCKET_HOST` | `localhost` | Server host (used for both XML-RPC and socket modes) |
| `FREECAD_XMLRPC_PORT` | `9875` | XML-RPC server port |
| `FREECAD_SOCKET_PORT` | `9876` | Socket server port |
## Starting FreeCAD with MCP Bridge
Before using this Docker image, start FreeCAD with the MCP bridge:
1. Install the **MCP Bridge** workbench via FreeCAD Addon Manager
2. Switch to the MCP Bridge workbench and click **Start MCP Bridge**
3. The bridge will start listening on ports 9875 (XML-RPC) and 9876 (Socket)
## Full Documentation
📖 **For complete documentation, examples, and source code, visit:**
**[https://github.com/spkane/freecad-robust-mcp-and-more](https://github.com/spkane/freecad-robust-mcp-and-more)**
## License
MIT License - see [LICENSE](https://github.com/spkane/freecad-robust-mcp-and-more/blob/main/LICENSE) for details.
EOF
# Validate the generated README
if [ ! -f ./DOCKERHUB_README.md ]; then
echo "ERROR: DOCKERHUB_README.md was not created" >&2
exit 1
fi
if [ ! -s ./DOCKERHUB_README.md ]; then
echo "ERROR: DOCKERHUB_README.md is empty" >&2
exit 1
fi
# Check file size (Docker Hub limit is 25000 bytes)
FILE_SIZE=$(wc -c < ./DOCKERHUB_README.md)
MAX_SIZE=25000
echo "Docker Hub README size: ${FILE_SIZE} bytes (limit: ${MAX_SIZE} bytes)"
if [ "$FILE_SIZE" -gt "$MAX_SIZE" ]; then
echo "ERROR: DOCKERHUB_README.md exceeds Docker Hub limit of ${MAX_SIZE} bytes (actual: ${FILE_SIZE} bytes)" >&2
exit 1
fi
echo "Docker Hub README validation passed"
- name: Update Docker Hub description
uses: peter-evans/dockerhub-description@v5
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: ${{ env.DOCKERHUB_REPO }}
readme-filepath: ./DOCKERHUB_README.md
short-description: "MCP (Model Context Protocol) server for FreeCAD integration"
continue-on-error: true
- name: Generate release summary
run: |
{
echo "## Docker Release Summary"
echo ""
echo "**Version:** ${{ steps.version.outputs.version }}"
echo "**Prerelease:** ${{ steps.version.outputs.is_prerelease }}"
echo ""
echo "### Published Tags"
echo ""
echo "\`\`\`"
echo "${{ steps.meta.outputs.tags }}"
echo "\`\`\`"
echo ""
echo "### Pull Command"
echo ""
echo "\`\`\`bash"
echo "docker pull ${{ env.DOCKERHUB_REPO }}:${{ steps.version.outputs.version }}"
echo "\`\`\`"
} >> "$GITHUB_STEP_SUMMARY"
+4 -2
View File
@@ -1,8 +1,8 @@
name: Docker Build
# Build and test Docker images on pushes to main and pull requests.
# Note: Version tags (v*) are NOT included here - they trigger docker-release.yaml
# via the 'release: published' event instead, to avoid duplicate workflow runs.
# Note: Release tags (robust-mcp-server-v*) trigger mcp-server-release.yaml
# which handles Docker Hub releases. This workflow is for CI testing only.
on:
push:
branches: [main, master]
@@ -20,6 +20,8 @@ on:
- "src/**/*.py"
- "pyproject.toml"
- ".github/workflows/docker.yaml"
workflow_dispatch:
workflow_call:
# Cancel in-progress runs for the same branch
concurrency:
@@ -0,0 +1,35 @@
name: Macro Cut Object for Magnets Release
on:
push:
tags:
- 'macro-cut-object-for-magnets-v*'
workflow_dispatch:
workflow_call:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
release:
uses: ./.github/workflows/macro-release-reusable.yaml
with:
tag_prefix: 'macro-cut-object-for-magnets-v'
macro_dir: 'Cut_Object_for_Magnets'
macro_file: 'CutObjectForMagnets.FCMacro'
macro_name: 'Cut Object for Magnets'
archive_name: 'CutObjectForMagnets'
readme_file: 'README-CutObjectForMagnets.md'
wiki_url: 'https://wiki.freecad.org/Macro_Cut_Object_for_Magnets'
description: |
A FreeCAD macro that cuts 3D objects along a plane and automatically adds aligned magnet holes with surface collision detection. Perfect for creating 3D printed parts that snap together with embedded magnets.
**Features:**
- Cut along preset planes (XY, XZ, YZ) or model datum planes
- Automatic hole placement with even distribution
- Surface collision detection prevents holes from breaking through walls
- Configurable hole diameter, depth, and count
- Creates PartDesign::Body objects with parametric Hole features
permissions:
contents: write
@@ -0,0 +1,35 @@
name: Macro Multi Export Release
on:
push:
tags:
- 'macro-multi-export-v*'
workflow_dispatch:
workflow_call:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
release:
uses: ./.github/workflows/macro-release-reusable.yaml
with:
tag_prefix: 'macro-multi-export-v'
macro_dir: 'Multi_Export'
macro_file: 'MultiExport.FCMacro'
macro_name: 'Multi Export'
archive_name: 'MultiExport'
readme_file: 'README-MultiExport.md'
wiki_url: 'https://wiki.freecad.org/Macro_Multi_Export'
description: |
A FreeCAD macro that exports selected bodies to multiple file formats simultaneously with a user-friendly dialog.
**Features:**
- Export to STL, STEP, 3MF, OBJ, IGES, BREP, PLY, and AMF formats
- Batch export multiple objects at once
- Configurable mesh quality settings (tolerance and deflection)
- Real-time file preview before export
- Remember last-used export directory
permissions:
contents: write
@@ -0,0 +1,298 @@
name: Macro Release (Reusable)
# Reusable workflow for releasing individual FreeCAD macros
# Called by component-specific workflows with appropriate inputs
on:
workflow_dispatch:
inputs:
tag_prefix:
description: 'Tag prefix to match (e.g., macro-cut-object-for-magnets-v)'
required: true
type: string
macro_dir:
description: 'Macro directory relative to macros/ (e.g., Cut_Object_for_Magnets)'
required: true
type: string
macro_file:
description: 'Main macro filename (e.g., CutObjectForMagnets.FCMacro)'
required: true
type: string
macro_name:
description: 'Human-readable macro name (e.g., Cut Object for Magnets)'
required: true
type: string
archive_name:
description: 'Archive base name (e.g., CutObjectForMagnets)'
required: true
type: string
readme_file:
description: 'README filename (e.g., README-CutObjectForMagnets.md)'
required: true
type: string
wiki_url:
description: 'FreeCAD Wiki URL for this macro'
required: false
type: string
default: ''
description:
description: 'Short description for release notes'
required: true
type: string
workflow_call:
inputs:
tag_prefix:
description: 'Tag prefix to match (e.g., macro-cut-object-for-magnets-v)'
required: true
type: string
macro_dir:
description: 'Macro directory relative to macros/ (e.g., Cut_Object_for_Magnets)'
required: true
type: string
macro_file:
description: 'Main macro filename (e.g., CutObjectForMagnets.FCMacro)'
required: true
type: string
macro_name:
description: 'Human-readable macro name (e.g., Cut Object for Magnets)'
required: true
type: string
archive_name:
description: 'Archive base name (e.g., CutObjectForMagnets)'
required: true
type: string
readme_file:
description: 'README filename (e.g., README-CutObjectForMagnets.md)'
required: true
type: string
wiki_url:
description: 'FreeCAD Wiki URL for this macro'
required: false
type: string
default: ''
description:
description: 'Short description for release notes'
required: true
type: string
jobs:
validate-and-release:
name: Validate Tag and Create Release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Validate semantic version tag
id: version
env:
TAG_PREFIX: ${{ inputs.tag_prefix }}
run: |
TAG="${GITHUB_REF#refs/tags/}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
# Build regex pattern from tag prefix
PATTERN="^${TAG_PREFIX}([0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?)\$"
if [[ ! "$TAG" =~ $PATTERN ]]; then
echo "ERROR: Tag '$TAG' does not match expected format (${TAG_PREFIX}X.Y.Z)"
exit 1
fi
VERSION="${BASH_REMATCH[1]}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
# Check if this is a prerelease
if [[ "$VERSION" =~ -[a-zA-Z0-9.]+ ]]; then
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
else
echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
fi
echo "Parsed version: $VERSION"
- name: Verify macro version in files
env:
VERSION: ${{ steps.version.outputs.version }}
MACRO_DIR: ${{ inputs.macro_dir }}
MACRO_FILE: ${{ inputs.macro_file }}
MACRO_NAME: ${{ inputs.macro_name }}
run: |
MACRO_PATH="macros/${MACRO_DIR}/${MACRO_FILE}"
echo "Verifying version in source files matches tag: $VERSION"
# Check __Version__ in .FCMacro file
if [ -f "$MACRO_PATH" ]; then
MACRO_VERSION=$(grep -o '__Version__ = "[^"]*"' "$MACRO_PATH" | cut -d'"' -f2)
if [ "$MACRO_VERSION" != "$VERSION" ]; then
echo "ERROR: Version mismatch in $MACRO_PATH"
echo " Expected: $VERSION"
echo " Found: $MACRO_VERSION"
echo ""
echo "The version in source files must be updated before tagging."
echo "Run the appropriate bump command first."
exit 1
fi
echo "✓ $MACRO_PATH: $MACRO_VERSION"
else
echo "ERROR: Macro file not found: $MACRO_PATH"
exit 1
fi
# Check package.xml
PKG_VERSION=$(awk -v name="$MACRO_NAME" '
/<macro>/ { in_macro=1 }
/<\/macro>/ { in_macro=0; found_name=0 }
in_macro && index($0, name) > 0 { found_name=1 }
in_macro && found_name && /<version>/ {
gsub(/.*<version>/, ""); gsub(/<\/version>.*/, ""); print; exit
}
' package.xml)
if [ "$PKG_VERSION" != "$VERSION" ]; then
echo "ERROR: Version mismatch in package.xml ($MACRO_NAME section)"
echo " Expected: $VERSION"
echo " Found: $PKG_VERSION"
exit 1
fi
echo "✓ package.xml ($MACRO_NAME): $PKG_VERSION"
echo ""
echo "Version verification passed!"
- name: Create macro archive
env:
VERSION: ${{ steps.version.outputs.version }}
MACRO_DIR: ${{ inputs.macro_dir }}
MACRO_FILE: ${{ inputs.macro_file }}
ARCHIVE_NAME: ${{ inputs.archive_name }}
README_FILE: ${{ inputs.readme_file }}
run: |
# Create staging directory
mkdir -p "staging/${ARCHIVE_NAME}-${VERSION}"
# Copy macro files
cp "macros/${MACRO_DIR}/${MACRO_FILE}" "staging/${ARCHIVE_NAME}-${VERSION}/"
# Copy icon if exists
for ext in svg png; do
ICON_FILE="macros/${MACRO_DIR}/${ARCHIVE_NAME}.${ext}"
if [ -f "$ICON_FILE" ]; then
cp "$ICON_FILE" "staging/${ARCHIVE_NAME}-${VERSION}/"
fi
done
# Copy README
if [ -f "macros/${MACRO_DIR}/${README_FILE}" ]; then
cp "macros/${MACRO_DIR}/${README_FILE}" "staging/${ARCHIVE_NAME}-${VERSION}/README.md"
fi
# Copy LICENSE
cp LICENSE "staging/${ARCHIVE_NAME}-${VERSION}/"
# Create tar.gz archive
cd staging
tar -czvf "${ARCHIVE_NAME}-${VERSION}.tar.gz" "${ARCHIVE_NAME}-${VERSION}"
# Create zip archive
zip -r "${ARCHIVE_NAME}-${VERSION}.zip" "${ARCHIVE_NAME}-${VERSION}"
mv "${ARCHIVE_NAME}-${VERSION}.tar.gz" ../
mv "${ARCHIVE_NAME}-${VERSION}.zip" ../
cd ..
echo "Created archives:"
ls -la "${ARCHIVE_NAME}-${VERSION}."*
- name: Extract changelog section
id: changelog
env:
VERSION: ${{ steps.version.outputs.version }}
MACRO_NAME: ${{ inputs.macro_name }}
DESCRIPTION: ${{ inputs.description }}
MACRO_FILE: ${{ inputs.macro_file }}
MACRO_DIR: ${{ inputs.macro_dir }}
README_FILE: ${{ inputs.readme_file }}
WIKI_URL: ${{ inputs.wiki_url }}
run: |
# Match header exactly as it appears in CHANGELOG.md
HEADER="### ${MACRO_NAME} Macro v${VERSION}"
# Extract section between this version header and the next component header or separator
# Only exit on: "---" separator OR "### " followed by component name (capital letter)
# This allows #### Added, #### Changed, etc. to be included
CHANGELOG_CONTENT=$(awk -v header="$HEADER" '
BEGIN { found=0 }
$0 == header || $0 == header " " { found=1; next }
found && /^---$/ { exit }
found && /^### [A-Z]/ { exit }
found { print }
' CHANGELOG.md)
# Build release body
cat > release_body.md << EOF
## ${MACRO_NAME} Macro v${VERSION}
${DESCRIPTION}
### Installation
Copy \`${MACRO_FILE}\` to your FreeCAD macro directory:
- **macOS**: \`~/Library/Application Support/FreeCAD/Macro/\`
- **Linux**: \`~/.local/share/FreeCAD/Macro/\`
- **Windows**: \`%APPDATA%/FreeCAD/Macro/\`
Optionally copy the icon file (\`.svg\` or \`.png\`) to the same location.
### Documentation
- [Full README](https://github.com/spkane/freecad-robust-mcp-and-more/blob/main/macros/${MACRO_DIR}/${README_FILE})
EOF
if [ -n "$WIKI_URL" ]; then
echo "- [FreeCAD Wiki Page](${WIKI_URL})" >> release_body.md
fi
if [ -n "$CHANGELOG_CONTENT" ]; then
{
echo ""
echo "### Changelog"
echo ""
echo "$CHANGELOG_CONTENT"
} >> release_body.md
fi
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
name: "${{ inputs.macro_name }} Macro v${{ steps.version.outputs.version }}"
tag_name: ${{ github.ref_name }}
prerelease: ${{ steps.version.outputs.is_prerelease == 'true' }}
generate_release_notes: true
body_path: release_body.md
files: |
${{ inputs.archive_name }}-${{ steps.version.outputs.version }}.tar.gz
${{ inputs.archive_name }}-${{ steps.version.outputs.version }}.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate summary
env:
VERSION: ${{ steps.version.outputs.version }}
MACRO_NAME: ${{ inputs.macro_name }}
ARCHIVE_NAME: ${{ inputs.archive_name }}
run: |
{
echo "## ${MACRO_NAME} Macro Release"
echo ""
echo "**Version:** ${VERSION}"
echo ""
echo "### Downloads"
echo ""
echo "- \`${ARCHIVE_NAME}-${VERSION}.tar.gz\`"
echo "- \`${ARCHIVE_NAME}-${VERSION}.zip\`"
} >> "$GITHUB_STEP_SUMMARY"
-196
View File
@@ -1,196 +0,0 @@
name: Macro Release
on:
release:
types: [published]
# Note: Only trigger on release.published to avoid duplicate runs.
# Creating a release implicitly pushes a tag, so triggering on both
# would cause the workflow to run twice.
# Cancel in-progress runs for the same tag
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
package-macros:
name: Package FreeCAD Macros
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Get version from tag
id: version
run: |
TAG="${GITHUB_REF#refs/tags/}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
# Extract version without 'v' prefix
VERSION="${TAG#v}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Packaging macros for version: $VERSION"
- name: Update macro versions
run: |
VERSION="${{ steps.version.outputs.version }}"
TODAY=$(date +%Y-%m-%d)
echo "Updating macros to version: $VERSION (date: $TODAY)"
# Update __Version__ and __Date__ in all .FCMacro files
for macro_file in macros/*/*.FCMacro; do
if [ -f "$macro_file" ]; then
echo "Updating: $macro_file"
# Update __Version__ line (handles both single and double quotes)
sed -i "s/^__Version__ = [\"'].*[\"']/__Version__ = \"${VERSION}\"/" "$macro_file"
# Update __Date__ line (handles both single and double quotes)
sed -i "s/^__Date__ = [\"'].*[\"']/__Date__ = \"${TODAY}\"/" "$macro_file"
# Verify the changes
grep -E "^__(Version|Date)__" "$macro_file" || true
fi
done
- name: Create macro archive
run: |
VERSION="${{ steps.version.outputs.version }}"
# Create a staging directory
mkdir -p "staging/freecad-macros-${VERSION}"
# Copy macro directories (excluding test files and development artifacts)
for macro_dir in macros/*/; do
if [ -d "$macro_dir" ]; then
macro_name=$(basename "$macro_dir")
echo "Packaging macro: $macro_name"
# Create destination directory
mkdir -p "staging/freecad-macros-${VERSION}/${macro_name}"
# Copy macro files (.FCMacro, .py, .svg icons, README*, LICENSE*)
find "$macro_dir" -maxdepth 1 \( \
-name "*.FCMacro" -o \
-name "*.py" -o \
-name "*.svg" -o \
-name "README*" -o \
-name "LICENSE*" \
\) -exec cp {} "staging/freecad-macros-${VERSION}/${macro_name}/" \;
fi
done
# Copy top-level LICENSE file
if [ -f "LICENSE" ]; then
cp LICENSE "staging/freecad-macros-${VERSION}/"
else
echo "ERROR: No LICENSE file found at repository root"
exit 1
fi
# Add a top-level README
cat > "staging/freecad-macros-${VERSION}/README.md" << 'EOF'
# FreeCAD Macros
This archive contains FreeCAD macros from the freecad-mcp project.
## Installation
### Macro Files
Copy the macro files (`.FCMacro`) to your FreeCAD macro directory:
- **macOS**: `~/Library/Application Support/FreeCAD/Macro/`
- **Linux**: `~/.local/share/FreeCAD/Macro/`
- **Windows**: `%APPDATA%/FreeCAD/Macro/`
### Icons (Optional)
To display custom icons in the FreeCAD macro menu, copy the `.svg` files
to your FreeCAD macro directory alongside the `.FCMacro` files.
The icon file must have the same base name as the macro (e.g.,
`CutObjectForMagnets.FCMacro` and `CutObjectForMagnets.svg`).
## Included Macros
EOF
# List included macros in the README
for macro_dir in "staging/freecad-macros-${VERSION}/"*/; do
if [ -d "$macro_dir" ]; then
macro_name=$(basename "$macro_dir")
echo "- **${macro_name}**: See ${macro_name}/README*.md for details" >> "staging/freecad-macros-${VERSION}/README.md"
fi
done
cat >> "staging/freecad-macros-${VERSION}/README.md" << 'EOF'
## More Information
For full documentation, visit:
https://github.com/spkane/freecad-robust-mcp-and-more
## License
MIT License - see LICENSE file included in this archive.
EOF
# Create the tar.gz archive
cd staging
tar -czvf "freecad-macros-${VERSION}.tar.gz" "freecad-macros-${VERSION}"
# Also create a zip for Windows users
zip -r "freecad-macros-${VERSION}.zip" "freecad-macros-${VERSION}"
# Move archives to workspace root
mv "freecad-macros-${VERSION}.tar.gz" ../
mv "freecad-macros-${VERSION}.zip" ../
cd ..
echo "Created archives:"
ls -la "freecad-macros-${VERSION}."*
- name: Upload macro archives to release
uses: softprops/action-gh-release@v2
with:
files: |
freecad-macros-${{ steps.version.outputs.version }}.tar.gz
freecad-macros-${{ steps.version.outputs.version }}.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate summary
run: |
VERSION="${{ steps.version.outputs.version }}"
{
echo "## FreeCAD Macros Release"
echo ""
echo "**Version:** ${VERSION}"
echo ""
echo "### Packaged Macros"
echo ""
} >> "$GITHUB_STEP_SUMMARY"
for macro_dir in macros/*/; do
if [ -d "$macro_dir" ]; then
macro_name=$(basename "$macro_dir")
echo "- ${macro_name}" >> "$GITHUB_STEP_SUMMARY"
fi
done
{
echo ""
echo "### Download"
echo ""
echo "The macro archives have been attached to the GitHub release:"
echo ""
echo "- \`freecad-macros-${VERSION}.tar.gz\` (Linux/macOS)"
echo "- \`freecad-macros-${VERSION}.zip\` (Windows)"
} >> "$GITHUB_STEP_SUMMARY"
+2
View File
@@ -19,6 +19,8 @@ on:
- "addon/FreecadRobustMCP/**/*.py"
- "tests/integration/**/*.py"
- ".github/workflows/macro-test.yaml"
workflow_dispatch:
workflow_call:
# Cancel in-progress runs for the same branch
concurrency:
+384
View File
@@ -0,0 +1,384 @@
name: MCP Server Release
# Trigger on tag push matching the component-specific pattern
on:
push:
tags:
- 'robust-mcp-server-v*'
workflow_dispatch:
workflow_call:
# Cancel in-progress runs for the same tag
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
DOCKERHUB_REPO: spkane/freecad-robust-mcp
jobs:
validate-tag:
name: Validate Tag Format
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
is_prerelease: ${{ steps.version.outputs.is_prerelease }}
steps:
- name: Validate semantic version tag
id: version
run: |
TAG="${GITHUB_REF#refs/tags/}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
# Extract version from tag (robust-mcp-server-v1.2.3 -> 1.2.3)
if [[ ! "$TAG" =~ ^robust-mcp-server-v([0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?)$ ]]; then
echo "ERROR: Tag '$TAG' does not match expected format (robust-mcp-server-vX.Y.Z or robust-mcp-server-vX.Y.Z-prerelease)"
exit 1
fi
VERSION="${BASH_REMATCH[1]}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
# Check if this is a prerelease
if [[ "$VERSION" =~ -[a-zA-Z0-9.]+ ]]; then
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
IS_PRERELEASE="true"
else
echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
IS_PRERELEASE="false"
fi
echo "Parsed version: $VERSION (prerelease: $IS_PRERELEASE)"
build:
name: Build Distribution
needs: validate-tag
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
- name: Set up Python
run: uv python install 3.11
- name: Cache uv dependencies
uses: actions/cache@v4
with:
path: |
~/.cache/uv
.venv
key: ${{ runner.os }}-uv-${{ hashFiles('pyproject.toml', 'uv.lock') }}
restore-keys: |
${{ runner.os }}-uv-
- name: Install build dependencies
run: uv sync --all-extras
- name: Build package
env:
# Override setuptools-scm version for component-specific tag
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ needs.validate-tag.outputs.version }}
run: uv build
- name: Verify built package version
env:
TAG_VERSION: ${{ needs.validate-tag.outputs.version }}
run: |
# Extract version from built wheel filename
shopt -s nullglob
WHEEL_FILES=(dist/*.whl)
if [ ${#WHEEL_FILES[@]} -eq 0 ]; then
echo "ERROR: No wheel files found in dist/"
exit 1
fi
WHEEL_FILE="${WHEEL_FILES[0]}"
WHEEL_NAME=$(basename "$WHEEL_FILE")
# Extract version from wheel filename
WHEEL_VERSION=$(echo "$WHEEL_NAME" | sed -E 's/^[^-]+-([^-]+)-.*/\1/')
echo "Built wheel: $WHEEL_NAME"
echo "Wheel version: $WHEEL_VERSION"
echo "Expected tag version: $TAG_VERSION"
# Normalize the tag version for comparison (PEP 440)
NORMALIZED_TAG=$(echo "$TAG_VERSION" | sed -E '
s/-alpha\.([0-9]+)/a\1/
s/-alpha/a0/
s/-beta\.([0-9]+)/b\1/
s/-beta/b0/
s/-rc\.([0-9]+)/rc\1/
s/-rc/rc0/
')
echo "Normalized tag version: $NORMALIZED_TAG"
if [ "$WHEEL_VERSION" != "$NORMALIZED_TAG" ]; then
echo "ERROR: Built package version does not match git tag!"
echo " Wheel version: $WHEEL_VERSION"
echo " Expected (normalized): $NORMALIZED_TAG"
exit 1
fi
echo "Version verification passed!"
- name: Check package
run: uv run twine check dist/*
- name: List built artifacts
run: ls -la dist/
- name: Upload distribution artifacts
uses: actions/upload-artifact@v6
with:
name: python-package-distributions
path: dist/
test-install:
name: Test Installation
needs: [validate-tag, build]
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
python-version: ["3.11"]
steps:
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Download distribution artifacts
uses: actions/download-artifact@v7
with:
name: python-package-distributions
path: dist/
- name: Install package from wheel
run: pip install dist/*.whl
- name: Verify installation
run: |
set -e
pip show freecad-robust-mcp
freecad-mcp --help
python -c "import freecad_mcp; print(f'freecad-robust-mcp version: {freecad_mcp.__version__ if hasattr(freecad_mcp, \"__version__\") else \"unknown\"}')"
publish-testpypi:
name: Publish to TestPyPI
needs: [validate-tag, build, test-install]
runs-on: ubuntu-latest
if: contains(needs.validate-tag.outputs.version, '-alpha')
environment:
name: testpypi
url: https://test.pypi.org/p/freecad-robust-mcp
permissions:
id-token: write
steps:
- name: Download distribution artifacts
uses: actions/download-artifact@v7
with:
name: python-package-distributions
path: dist/
- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
publish-pypi:
name: Publish to PyPI
needs: [validate-tag, build, test-install]
runs-on: ubuntu-latest
if: ${{ !contains(needs.validate-tag.outputs.version, '-alpha') }}
environment:
name: pypi
url: https://pypi.org/p/freecad-robust-mcp
permissions:
id-token: write
steps:
- name: Download distribution artifacts
uses: actions/download-artifact@v7
with:
name: python-package-distributions
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
docker-release:
name: Build and Push Docker Image
needs: [validate-tag, build, test-install]
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.DOCKERHUB_REPO }}
tags: |
type=raw,value=${{ needs.validate-tag.outputs.version }}
type=raw,value=${{ needs.validate-tag.outputs.version }},suffix=-mcp-server
type=raw,value=latest,enable=${{ needs.validate-tag.outputs.is_prerelease == 'false' }}
labels: |
org.opencontainers.image.title=FreeCAD Robust MCP Server
org.opencontainers.image.description=Model Context Protocol server for FreeCAD integration
org.opencontainers.image.vendor=spkane
org.opencontainers.image.version=${{ needs.validate-tag.outputs.version }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: true
sbom: true
- name: Test pushed image
run: |
docker pull ${{ env.DOCKERHUB_REPO }}:${{ needs.validate-tag.outputs.version }}
echo '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | \
timeout 30 docker run --rm -i \
-e FREECAD_MODE=xmlrpc \
${{ env.DOCKERHUB_REPO }}:${{ needs.validate-tag.outputs.version }} 2>&1 | \
grep -q '"result"'
echo "Container test passed"
create-github-release:
name: Create GitHub Release
needs: [validate-tag, build, test-install]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Download distribution artifacts
uses: actions/download-artifact@v7
with:
name: python-package-distributions
path: dist/
- name: Extract changelog section
id: changelog
run: |
VERSION="${{ needs.validate-tag.outputs.version }}"
# Match header exactly as it appears in CHANGELOG.md
HEADER="### MCP Server v${VERSION}"
# Extract section between this version header and the next component header or separator
# Only exit on: "---" separator OR "### " followed by component name (capital letter)
# This allows #### Added, #### Changed, etc. to be included
CHANGELOG_CONTENT=$(awk -v header="$HEADER" '
BEGIN { found=0 }
$0 == header || $0 == header " " { found=1; next }
found && /^---$/ { exit }
found && /^### [A-Z]/ { exit }
found { print }
' CHANGELOG.md)
# Write to file for the release body (handles multiline)
echo "$CHANGELOG_CONTENT" > changelog_section.md
# Check if we got content
if [ -n "$CHANGELOG_CONTENT" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
echo "Extracted changelog section for $HEADER"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No changelog section found for $HEADER"
fi
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
name: "MCP Server v${{ needs.validate-tag.outputs.version }}"
tag_name: ${{ github.ref_name }}
prerelease: ${{ needs.validate-tag.outputs.is_prerelease == 'true' }}
generate_release_notes: true
body_path: ${{ steps.changelog.outputs.found == 'true' && 'changelog_section.md' || '' }}
files: dist/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
generate-summary:
name: Generate Release Summary
needs: [validate-tag, publish-pypi, publish-testpypi, docker-release, create-github-release]
if: always()
runs-on: ubuntu-latest
steps:
- name: Generate release summary
env:
VERSION: ${{ needs.validate-tag.outputs.version }}
IS_PRERELEASE: ${{ needs.validate-tag.outputs.is_prerelease }}
run: |
{
echo "## MCP Server Release Summary"
echo ""
echo "**Version:** $VERSION"
echo "**Prerelease:** $IS_PRERELEASE"
echo ""
} >> "$GITHUB_STEP_SUMMARY"
if [[ "$VERSION" == *"-alpha"* ]]; then
{
echo "### Install from TestPyPI"
echo ""
echo "\`\`\`bash"
echo "pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ freecad-robust-mcp"
echo "\`\`\`"
} >> "$GITHUB_STEP_SUMMARY"
else
{
echo "### Install from PyPI"
echo ""
echo "\`\`\`bash"
echo "pip install freecad-robust-mcp"
echo "\`\`\`"
} >> "$GITHUB_STEP_SUMMARY"
fi
{
echo ""
echo "### Docker"
echo ""
echo "\`\`\`bash"
echo "docker pull spkane/freecad-robust-mcp:$VERSION"
echo "\`\`\`"
} >> "$GITHUB_STEP_SUMMARY"
@@ -0,0 +1,229 @@
name: MCP Workbench Release
# Trigger on tag push matching the component-specific pattern
on:
push:
tags:
- 'robust-mcp-workbench-v*'
workflow_dispatch:
workflow_call:
# Cancel in-progress runs for the same tag
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
validate-and-release:
name: Validate Tag and Create Release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Validate semantic version tag
id: version
run: |
TAG="${GITHUB_REF#refs/tags/}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
# Extract version from tag (robust-mcp-workbench-v1.2.3 -> 1.2.3)
if [[ ! "$TAG" =~ ^robust-mcp-workbench-v([0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?)$ ]]; then
echo "ERROR: Tag '$TAG' does not match expected format (robust-mcp-workbench-vX.Y.Z or robust-mcp-workbench-vX.Y.Z-prerelease)"
exit 1
fi
VERSION="${BASH_REMATCH[1]}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
# Check if this is a prerelease
if [[ "$VERSION" =~ -[a-zA-Z0-9.]+ ]]; then
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
else
echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
fi
echo "Parsed version: $VERSION"
- name: Verify version in source files
run: |
VERSION="${{ steps.version.outputs.version }}"
echo "Verifying version in source files matches tag: $VERSION"
# Check __version__ in __init__.py
INIT_FILE="addon/FreecadRobustMCP/freecad_mcp_bridge/__init__.py"
INIT_VERSION=$(grep -o '__version__ = "[^"]*"' "$INIT_FILE" | cut -d'"' -f2)
if [ "$INIT_VERSION" != "$VERSION" ]; then
echo "ERROR: Version mismatch in $INIT_FILE"
echo " Expected: $VERSION"
echo " Found: $INIT_VERSION"
echo ""
echo "The version in source files must be updated before tagging."
echo "Run: just release::bump-workbench $VERSION"
exit 1
fi
echo "✓ $INIT_FILE: $INIT_VERSION"
# Check package.xml
PKG_VERSION=$(awk '/<workbench>/,/<\/workbench>/' package.xml | grep -o '<version>[^<]*</version>' | head -1 | sed 's/<[^>]*>//g')
if [ "$PKG_VERSION" != "$VERSION" ]; then
echo "ERROR: Version mismatch in package.xml (workbench section)"
echo " Expected: $VERSION"
echo " Found: $PKG_VERSION"
exit 1
fi
echo "✓ package.xml (workbench): $PKG_VERSION"
echo ""
echo "Version verification passed!"
- name: Create workbench archive
run: |
VERSION="${{ steps.version.outputs.version }}"
# Create staging directory
mkdir -p "staging/freecad-mcp-workbench-${VERSION}"
# Copy workbench files
cp -r addon/FreecadRobustMCP/* "staging/freecad-mcp-workbench-${VERSION}/"
# Copy LICENSE
cp LICENSE "staging/freecad-mcp-workbench-${VERSION}/"
# Create README for the archive
cat > "staging/freecad-mcp-workbench-${VERSION}/README.md" << EOF
# FreeCAD MCP Bridge Workbench
**Version:** ${VERSION}
## Installation
### Via FreeCAD Addon Manager (Recommended)
1. Open FreeCAD
2. Go to **Tools → Addon Manager**
3. Search for "MCP Bridge"
4. Click **Install**
5. Restart FreeCAD
### Manual Installation
Copy the contents of this archive to your FreeCAD Mod directory:
- **macOS**: \`~/Library/Application Support/FreeCAD/Mod/FreecadRobustMCP/\`
- **Linux**: \`~/.local/share/FreeCAD/Mod/FreecadRobustMCP/\`
- **Windows**: \`%APPDATA%/FreeCAD/Mod/FreecadRobustMCP/\`
## Usage
1. Switch to the **MCP Bridge** workbench
2. Click **Start MCP Bridge** in the toolbar
3. Connect your MCP client (Claude Code, etc.)
## Documentation
Full documentation: https://github.com/spkane/freecad-robust-mcp-and-more
## License
MIT License - see LICENSE file
EOF
# Create tar.gz archive
cd staging
tar -czvf "freecad-mcp-workbench-${VERSION}.tar.gz" "freecad-mcp-workbench-${VERSION}"
# Create zip archive
zip -r "freecad-mcp-workbench-${VERSION}.zip" "freecad-mcp-workbench-${VERSION}"
mv "freecad-mcp-workbench-${VERSION}.tar.gz" ../
mv "freecad-mcp-workbench-${VERSION}.zip" ../
cd ..
echo "Created archives:"
ls -la "freecad-mcp-workbench-${VERSION}."*
- name: Extract changelog section
id: changelog
run: |
VERSION="${{ steps.version.outputs.version }}"
# Match header exactly as it appears in CHANGELOG.md (with optional space before v)
HEADER="### MCP Bridge Workbench v${VERSION}"
# Extract section between this version header and the next component header or separator
# Only exit on: "---" separator OR "### " followed by component name (capital letter)
# This allows #### Added, #### Changed, etc. to be included
CHANGELOG_CONTENT=$(awk -v header="$HEADER" '
BEGIN { found=0 }
$0 == header || $0 == header " " { found=1; next }
found && /^---$/ { exit }
found && /^### [A-Z]/ { exit }
found { print }
' CHANGELOG.md)
# Build release body with changelog content if available
cat > release_body.md << 'STATIC_EOF'
## FreeCAD MCP Bridge Workbench v${{ steps.version.outputs.version }}
This release contains the MCP Bridge workbench for FreeCAD.
### Installation
**Recommended:** Install via FreeCAD's Addon Manager (search for "MCP Bridge").
**Manual:** Download and extract to your FreeCAD Mod directory.
### What's Included
- MCP Bridge workbench for GUI and headless FreeCAD
- XML-RPC and JSON-RPC server support
- Toolbar commands for bridge control
See the [full documentation](https://github.com/spkane/freecad-robust-mcp-and-more) for usage instructions.
STATIC_EOF
if [ -n "$CHANGELOG_CONTENT" ]; then
{
echo ""
echo "### Changelog"
echo ""
echo "$CHANGELOG_CONTENT"
} >> release_body.md
fi
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
name: "MCP Bridge Workbench v${{ steps.version.outputs.version }}"
tag_name: ${{ github.ref_name }}
prerelease: ${{ steps.version.outputs.is_prerelease == 'true' }}
generate_release_notes: true
body_path: release_body.md
files: |
freecad-mcp-workbench-${{ steps.version.outputs.version }}.tar.gz
freecad-mcp-workbench-${{ steps.version.outputs.version }}.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate summary
run: |
VERSION="${{ steps.version.outputs.version }}"
{
echo "## MCP Bridge Workbench Release"
echo ""
echo "**Version:** ${VERSION}"
echo ""
echo "### Downloads"
echo ""
echo "- \`freecad-mcp-workbench-${VERSION}.tar.gz\`"
echo "- \`freecad-mcp-workbench-${VERSION}.zip\`"
echo ""
echo "### Installation"
echo ""
echo "Install via FreeCAD Addon Manager or extract to your Mod directory."
} >> "$GITHUB_STEP_SUMMARY"
+2
View File
@@ -5,6 +5,8 @@ on:
branches: [main, master]
pull_request:
branches: [main, master]
workflow_dispatch:
workflow_call:
# Cancel in-progress runs for the same branch
concurrency:
-310
View File
@@ -1,310 +0,0 @@
name: PyPI Release
# Only trigger on GitHub release publication, not on tag push.
# When a release is created, GitHub fires both 'release' and 'push' events.
# Using only 'release' prevents duplicate workflow runs.
on:
release:
types: [published]
# Cancel in-progress runs for the same tag
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Build Distribution
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Validate semantic version tag
id: version
run: |
TAG="${GITHUB_REF#refs/tags/}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
# Validate semantic versioning format (v1.2.3 or v1.2.3-prerelease)
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
echo "ERROR: Tag '$TAG' does not follow semantic versioning (vX.Y.Z or vX.Y.Z-prerelease)"
exit 1
fi
# Extract version without 'v' prefix
VERSION="${TAG#v}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
# Check if this is a prerelease
if [[ "$TAG" =~ -[a-zA-Z0-9.]+ ]]; then
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
IS_PRERELEASE="true"
else
echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
IS_PRERELEASE="false"
fi
echo "Parsed version: $VERSION (prerelease: $IS_PRERELEASE)"
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
- name: Set up Python
run: uv python install 3.11
- name: Install build dependencies
run: uv sync --all-extras
- name: Build package
run: uv build
- name: Verify built package version
env:
TAG_VERSION: ${{ steps.version.outputs.version }}
run: |
# Extract version from built wheel filename
# Wheel format: {distribution}-{version}(-{build tag})?-{python}-{abi}-{platform}.whl
shopt -s nullglob
WHEEL_FILES=(dist/*.whl)
if [ ${#WHEEL_FILES[@]} -eq 0 ]; then
echo "ERROR: No wheel files found in dist/"
exit 1
fi
WHEEL_FILE="${WHEEL_FILES[0]}"
WHEEL_NAME=$(basename "$WHEEL_FILE")
# Extract version from wheel filename (second field after splitting by -)
# Handle PEP 440 version normalization (e.g., 0.5.0-beta -> 0.5.0b0)
WHEEL_VERSION=$(echo "$WHEEL_NAME" | sed -E 's/^[^-]+-([^-]+)-.*/\1/')
echo "Built wheel: $WHEEL_NAME"
echo "Wheel version: $WHEEL_VERSION"
echo "Expected tag version: $TAG_VERSION"
# Normalize the tag version for comparison (PEP 440)
# Convert common patterns: -alpha -> a0, -beta -> b0, -rc -> rc0
# Also handle numbered variants: -alpha.1 -> a1, -beta.2 -> b2, -rc.3 -> rc3
NORMALIZED_TAG=$(echo "$TAG_VERSION" | sed -E '
s/-alpha\.([0-9]+)/a\1/
s/-alpha/a0/
s/-beta\.([0-9]+)/b\1/
s/-beta/b0/
s/-rc\.([0-9]+)/rc\1/
s/-rc/rc0/
')
echo "Normalized tag version: $NORMALIZED_TAG"
if [ "$WHEEL_VERSION" != "$NORMALIZED_TAG" ]; then
echo "ERROR: Built package version does not match git tag!"
echo " Wheel version: $WHEEL_VERSION"
echo " Expected (normalized): $NORMALIZED_TAG"
echo ""
echo "This may indicate an issue with dynamic versioning configuration."
exit 1
fi
echo "Version verification passed!"
- name: Check package
run: |
uv run twine check dist/*
- name: List built artifacts
run: ls -la dist/
- name: Upload distribution artifacts
uses: actions/upload-artifact@v6
with:
name: python-package-distributions
path: dist/
test-install:
name: Test Installation
needs: build
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
python-version: ["3.11"]
steps:
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Download distribution artifacts
uses: actions/download-artifact@v7
with:
name: python-package-distributions
path: dist/
- name: Install package from wheel
run: |
pip install dist/*.whl
- name: Verify installation
run: |
# Check that the package is installed
pip show freecad-robust-mcp
# Check that the CLI is available
freecad-mcp --help || echo "CLI help check completed"
# Check that the module can be imported
python -c "import freecad_mcp; print(f'freecad-robust-mcp version: {freecad_mcp.__version__ if hasattr(freecad_mcp, \"__version__\") else \"unknown\"}')"
publish-testpypi:
name: Publish to TestPyPI
needs: [build, test-install]
runs-on: ubuntu-latest
# Only publish alpha releases to TestPyPI for early testing.
# Beta and RC releases go to PyPI since they are closer to stable.
# This is intentional - do not change to contains(github.ref, '-').
if: contains(github.ref, '-alpha')
environment:
name: testpypi
url: https://test.pypi.org/p/freecad-robust-mcp
permissions:
id-token: write
steps:
- name: Download distribution artifacts
uses: actions/download-artifact@v7
with:
name: python-package-distributions
path: dist/
- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
publish-pypi:
name: Publish to PyPI
needs: [build, test-install]
runs-on: ubuntu-latest
# Publish beta, rc, and stable releases to PyPI (not alpha).
# Alpha releases are too experimental for PyPI - they go to TestPyPI only.
# This is intentional - do not change to !contains(github.ref, '-').
if: ${{ !contains(github.ref, '-alpha') }}
environment:
name: pypi
url: https://pypi.org/p/freecad-robust-mcp
permissions:
id-token: write
steps:
- name: Download distribution artifacts
uses: actions/download-artifact@v7
with:
name: python-package-distributions
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
create-github-release-assets:
name: Add Assets to GitHub Release
needs: [build, test-install]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download distribution artifacts
uses: actions/download-artifact@v7
with:
name: python-package-distributions
path: dist/
- name: Upload to GitHub Release
uses: softprops/action-gh-release@v2
with:
files: dist/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
generate-summary:
name: Generate Release Summary
needs: [publish-pypi, publish-testpypi]
if: always()
runs-on: ubuntu-latest
steps:
- name: Generate release summary
env:
REF_NAME: ${{ github.ref_name }}
run: |
{
echo "## PyPI Release Summary"
echo ""
echo "**Tag:** $REF_NAME"
echo ""
} >> "$GITHUB_STEP_SUMMARY"
# Alpha releases go to TestPyPI only; beta, rc, and stable go to PyPI
if [[ "$REF_NAME" == *"-alpha"* ]]; then
{
echo "**Type:** Alpha prerelease (published to TestPyPI only)"
echo ""
echo "### Install from TestPyPI"
echo ""
echo "\`\`\`bash"
echo "pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ freecad-robust-mcp"
echo "\`\`\`"
} >> "$GITHUB_STEP_SUMMARY"
elif [[ "$REF_NAME" == *"-beta"* ]]; then
{
echo "**Type:** Beta prerelease (published to PyPI)"
echo ""
echo "### Install from PyPI"
echo ""
echo "\`\`\`bash"
echo "pip install freecad-robust-mcp"
echo "\`\`\`"
echo ""
echo "Or with uv:"
echo ""
echo "\`\`\`bash"
echo "uv pip install freecad-robust-mcp"
echo "\`\`\`"
} >> "$GITHUB_STEP_SUMMARY"
elif [[ "$REF_NAME" == *"-rc"* ]]; then
{
echo "**Type:** Release candidate (published to PyPI)"
echo ""
echo "### Install from PyPI"
echo ""
echo "\`\`\`bash"
echo "pip install freecad-robust-mcp"
echo "\`\`\`"
echo ""
echo "Or with uv:"
echo ""
echo "\`\`\`bash"
echo "uv pip install freecad-robust-mcp"
echo "\`\`\`"
} >> "$GITHUB_STEP_SUMMARY"
else
{
echo "**Type:** Stable release (published to PyPI)"
echo ""
echo "### Install from PyPI"
echo ""
echo "\`\`\`bash"
echo "pip install freecad-robust-mcp"
echo "\`\`\`"
echo ""
echo "Or with uv:"
echo ""
echo "\`\`\`bash"
echo "uv pip install freecad-robust-mcp"
echo "\`\`\`"
} >> "$GITHUB_STEP_SUMMARY"
fi
+2
View File
@@ -19,6 +19,8 @@ on:
- "pyproject.toml"
- "uv.lock"
- ".github/workflows/test.yaml"
workflow_dispatch:
workflow_call:
# Cancel in-progress runs for the same branch
concurrency:
+13 -4
View File
@@ -20,6 +20,7 @@ repos:
args: [--unsafe]
- id: check-toml
- id: check-json
exclude: ^\.vscode/.*\.json$ # VS Code uses JSONC (JSON with Comments)
- id: check-added-large-files
args: [--maxkb=1000]
- id: check-merge-conflict
@@ -36,6 +37,13 @@ repos:
types: [text]
files: \.(py|FCMacro)$
# JSON5/JSONC validation for VS Code config files (supports comments)
- repo: https://github.com/maresb/check-json5
rev: v1.0.1
hooks:
- id: check-json5
files: ^\.vscode/.*\.json$ # Only check VS Code JSONC files
# ==========================================================================
# Python - Linting and Formatting
# ==========================================================================
@@ -81,14 +89,15 @@ repos:
# Checks installed packages against known security vulnerabilities
# Local: Requires free safetycli.com account. Run `uv run safety auth` first.
# CI: Uses SAFETY_API_KEY secret passed via environment variable.
# Config: .safety-policy.yml (excludes .venv, node_modules, etc.)
- repo: local
hooks:
- id: safety
name: safety (dependency vulnerabilities)
entry: uv run safety scan --detailed-output
entry: uv run safety scan --policy-file .safety-policy.yml --detailed-output
language: system
pass_filenames: false
files: ^(pyproject\.toml|uv\.lock)$
files: ^(pyproject\.toml|uv\.lock|\.safety-policy\.yml)$
# ==========================================================================
# Secrets Detection - Multi-Layer Approach
@@ -161,7 +170,7 @@ repos:
- id: md-toc
name: md-toc (table of contents)
args: ["-p", "github", "-l", "6"] # GitHub parser, max 6 levels
files: ^(README|ARCHITECTURE-MCP)\.md$
files: ^(README|docs/development/architecture-detailed)\.md$
# ==========================================================================
# Spell Checking
@@ -270,7 +279,7 @@ repos:
# 2. Authenticate: just coderabbit::login
#
# USAGE:
# - Run manually: just review (or just coderabbit::review)
# - Run manually: just coderabbit::review
# - Run via pre-commit: uv run pre-commit run coderabbit --all-files
#
# NOTE: This hook uses 'manual' stage so it doesn't run automatically.
+19
View File
@@ -0,0 +1,19 @@
# Safety CLI 3.x Policy File
# https://docs.safetycli.com/safety-docs/administration/safety-policy-files
version: "3.0"
scanning-settings:
# Maximum directory depth to scan
max-depth: 6
# Exclude virtual environments and other non-project directories
exclude:
- ".venv"
- "venv"
- ".git"
- "node_modules"
- "__pycache__"
- "*.egg-info"
- "dist"
- "build"
- "site" # MkDocs build output
+123 -40
View File
@@ -1,48 +1,131 @@
{
"version": "1.5.0",
"plugins_used": [
{"name": "ArtifactoryDetector"},
{"name": "AWSKeyDetector"},
{"name": "AzureStorageKeyDetector"},
{"name": "Base64HighEntropyString", "limit": 4.5},
{"name": "BasicAuthDetector"},
{"name": "CloudantDetector"},
{"name": "DiscordBotTokenDetector"},
{"name": "GitHubTokenDetector"},
{"name": "GitLabTokenDetector"},
{"name": "HexHighEntropyString", "limit": 3.0},
{"name": "IbmCloudIamDetector"},
{"name": "IbmCosHmacDetector"},
{"name": "IPPublicDetector"},
{"name": "JwtTokenDetector"},
{"name": "KeywordDetector", "keyword_exclude": ""},
{"name": "MailchimpDetector"},
{"name": "NpmDetector"},
{"name": "OpenAIDetector"},
{"name": "PrivateKeyDetector"},
{"name": "PypiTokenDetector"},
{"name": "SendGridDetector"},
{"name": "SlackDetector"},
{"name": "SoftlayerDetector"},
{"name": "SquareOAuthDetector"},
{"name": "StripeDetector"},
{"name": "TelegramBotTokenDetector"},
{"name": "TwilioKeyDetector"}
{
"name": "ArtifactoryDetector"
},
{
"name": "AWSKeyDetector"
},
{
"name": "AzureStorageKeyDetector"
},
{
"name": "Base64HighEntropyString",
"limit": 4.5
},
{
"name": "BasicAuthDetector"
},
{
"name": "CloudantDetector"
},
{
"name": "DiscordBotTokenDetector"
},
{
"name": "GitHubTokenDetector"
},
{
"name": "GitLabTokenDetector"
},
{
"name": "HexHighEntropyString",
"limit": 3.0
},
{
"name": "IbmCloudIamDetector"
},
{
"name": "IbmCosHmacDetector"
},
{
"name": "IPPublicDetector"
},
{
"name": "JwtTokenDetector"
},
{
"name": "KeywordDetector",
"keyword_exclude": ""
},
{
"name": "MailchimpDetector"
},
{
"name": "NpmDetector"
},
{
"name": "OpenAIDetector"
},
{
"name": "PrivateKeyDetector"
},
{
"name": "PypiTokenDetector"
},
{
"name": "SendGridDetector"
},
{
"name": "SlackDetector"
},
{
"name": "SoftlayerDetector"
},
{
"name": "SquareOAuthDetector"
},
{
"name": "StripeDetector"
},
{
"name": "TelegramBotTokenDetector"
},
{
"name": "TwilioKeyDetector"
}
],
"filters_used": [
{"path": "detect_secrets.filters.allowlist.is_line_allowlisted"},
{"path": "detect_secrets.filters.common.is_baseline_file", "filename": ".secrets.baseline"},
{"path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", "min_level": 2},
{"path": "detect_secrets.filters.heuristic.is_indirect_reference"},
{"path": "detect_secrets.filters.heuristic.is_likely_id_string"},
{"path": "detect_secrets.filters.heuristic.is_lock_file"},
{"path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string"},
{"path": "detect_secrets.filters.heuristic.is_potential_uuid"},
{"path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign"},
{"path": "detect_secrets.filters.heuristic.is_sequential_string"},
{"path": "detect_secrets.filters.heuristic.is_swagger_file"},
{"path": "detect_secrets.filters.heuristic.is_templated_secret"}
{
"path": "detect_secrets.filters.allowlist.is_line_allowlisted"
},
{
"path": "detect_secrets.filters.common.is_baseline_file",
"filename": "/Users/spkane/dev/spkane/freecad-mcp-and-more/.secrets.baseline"
},
{
"path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies",
"min_level": 2
},
{
"path": "detect_secrets.filters.heuristic.is_indirect_reference"
},
{
"path": "detect_secrets.filters.heuristic.is_likely_id_string"
},
{
"path": "detect_secrets.filters.heuristic.is_lock_file"
},
{
"path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string"
},
{
"path": "detect_secrets.filters.heuristic.is_potential_uuid"
},
{
"path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign"
},
{
"path": "detect_secrets.filters.heuristic.is_sequential_string"
},
{
"path": "detect_secrets.filters.heuristic.is_swagger_file"
},
{
"path": "detect_secrets.filters.heuristic.is_templated_secret"
}
],
"results": {},
"generated_at": "2025-01-01T00:00:00Z"
"generated_at": "2026-01-07T11:33:09Z"
}
+27 -3
View File
@@ -1,12 +1,36 @@
{
"recommendations": [
"streetsidesoftware.code-spell-checker",
// Python development
"ms-python.python",
"ms-python.vscode-pylance",
"ms-python.mypy-type-checker",
"charliermarsh.ruff",
"DavidAnson.vscode-markdownlint",
// Just (command runner)
"skellock.just",
// Docker
"ms-azuretools.vscode-docker",
"exiasr.hadolint",
// GitHub Actions
"GitHub.vscode-github-actions",
// Configuration files
"tamasfe.even-better-toml",
"redhat.vscode-yaml",
"eamodio.gitlens"
// Markdown
"DavidAnson.vscode-markdownlint",
// Shell scripts
"timonwong.shellcheck",
// Git
"eamodio.gitlens",
// Spell checking
"streetsidesoftware.code-spell-checker"
]
}
+1
View File
@@ -12,6 +12,7 @@
"contextform",
"docstrings",
"dylib",
"exiasr",
"fcstd",
"freecad",
"FreeCAD",
+81 -45
View File
@@ -5,23 +5,36 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
<!--
IMPORTANT: Update this file at the end of any session where repository files are modified.
Group changes under: Added, Changed, Deprecated, Removed, Fixed, Security
-->
This is a multi-component project. Each component has its own versioning and release cycle:
| Component | Tag Format | Distribution |
| ------------------------------ | ------------------------------------- | ------------------------ |
| MCP Server | `robust-mcp-server-vX.Y.Z` | PyPI, Docker Hub, GitHub |
| MCP Bridge Workbench | `robust-mcp-workbench-vX.Y.Z` | GitHub Release |
| Cut Object for Magnets Macro | `macro-cut-object-for-magnets-vX.Y.Z` | GitHub Release |
| Multi Export Macro | `macro-multi-export-vX.Y.Z` | GitHub Release |
---
## [Unreleased]
<!-- This section captures changes that have been merged but not yet released.
When preparing a new release, move these entries to a new version section below. -->
### MCP Server
## [0.5.0-beta] - 2026-01-05
### MCP Bridge Workbench
Initial public beta release of the FreeCAD MCP Server and Macros.
### Cut Object for Magnets Macro
### Added
### Multi Export Macro
#### MCP Server for FreeCAD
---
## Initial Public Beta - 2026-01-05
### MCP Server v0.5.0-beta
Initial public beta release.
#### Added
- **Document management**: Create, open, save, close, recompute documents
- **Object manipulation**: Create primitives (box, cylinder, sphere, cone, torus, wedge, helix), boolean operations, transforms (scale, rotate, copy, mirror)
@@ -30,48 +43,71 @@ Initial public beta release of the FreeCAD MCP Server and Macros.
- **View control**: Screenshots, camera positioning, zoom, visibility, display modes
- **Macro support**: List, run, create, and manage FreeCAD macros
- **Undo/Redo**: Full undo/redo support with status queries
- **Connection modes**: XML-RPC (recommended), Socket, and Embedded (Linux only)
#### Connection Modes
| Mode | Description | Platform Support |
| ---------- | ----------------------------- | --------------------------- |
| `xmlrpc` | XML-RPC protocol (port 9875) | All platforms (recommended) |
| `socket` | JSON-RPC socket (port 9876) | All platforms |
| `embedded` | In-process FreeCAD | Linux only |
#### FreeCAD Macros
- **CutObjectForMagnets**: Cut objects in half with aligned magnet holes for easy reassembly
- Smart hole placement with overlap detection
- Support for solid and hollow objects
- Parametric PartDesign::Hole or boolean hole methods
- **MultiExport**: Export selected objects to multiple formats simultaneously
- Supports STL, STEP, 3MF, OBJ, IGES, BREP, PLY, AMF
- Configurable mesh tolerance for STL/3MF
- Batch export with progress feedback
#### Installation Options
#### Installation
- **PyPI**: `pip install freecad-robust-mcp`
- **Docker**: `docker pull ghcr.io/spkane/freecad-robust-mcp`
- **Source**: Clone and install with `uv sync --all-extras`
#### CI/CD & Tooling
- GitHub Actions workflows for testing, Docker builds, and PyPI releases
- Pre-commit hooks with Ruff, MyPy, Bandit, and secrets detection
- Dependabot for automated dependency updates
- CodeQL security scanning
#### Documentation
- Comprehensive README with installation and configuration guides
- CLAUDE.md with AI assistant guidelines
- ARCHITECTURE.md with system design documentation
- MCP tools reference with examples
### Known Limitations
#### Known Limitations
- Embedded mode only works on Linux (macOS/Windows must use xmlrpc or socket)
- Python 3.11 required (must match FreeCAD's bundled Python for ABI compatibility)
- GUI features (screenshots, visibility) not available in headless mode
---
### MCP Bridge Workbench v0.5.0-beta
Initial public beta release.
#### Added
- **FreeCAD Workbench**: Installable via FreeCAD Addon Manager
- **XML-RPC server**: Runs on port 9875 for MCP server communication
- **Socket server**: Alternative JSON-RPC on port 9876
- **Auto-start option**: Configure bridge to start with FreeCAD
- **GUI mode support**: Full 3D view integration with screenshots
- **Headless mode support**: Console-only operation for automation
---
### Cut Object for Magnets Macro v0.5.0-beta
Initial public beta release.
#### Added
- **Smart cutting**: Cut objects in half with aligned magnet holes for easy reassembly
- **Hole placement**: Automatic hole placement with overlap detection
- **Object support**: Works with solid and hollow objects
- **Hole methods**: Parametric PartDesign::Hole or boolean hole methods
- **GUI dialog**: Interactive parameter configuration
---
### Multi Export Macro v0.5.0-beta
Initial public beta release.
#### Added
- **Multi-format export**: Export selected objects to multiple formats simultaneously
- **Format support**: STL, STEP, 3MF, OBJ, IGES, BREP, PLY, AMF
- **Mesh control**: Configurable mesh tolerance for STL/3MF
- **Batch export**: Export multiple objects with progress feedback
- **GUI dialog**: Interactive format and option selection
---
## Project Infrastructure (v0.5.0-beta)
### Added
- **CI/CD**: GitHub Actions workflows for testing, Docker builds, and PyPI releases
- **Quality**: Pre-commit hooks with Ruff, MyPy, Bandit, and secrets detection
- **Dependencies**: Dependabot for automated dependency updates
- **Security**: CodeQL security scanning
- **Documentation**: Comprehensive README, CLAUDE.md, and architecture docs
+493 -74
View File
@@ -40,7 +40,7 @@ FreeCAD's `FreeCAD.so` library links to `@rpath/libpython3.11.dylib` (FreeCAD's
1. Start FreeCAD and start the MCP bridge:
- Install the MCP Bridge workbench via Addon Manager, or
- Use `just run-gui` from the source repository
- Use `just freecad::run-gui` from the source repository
1. The MCP server will then connect to FreeCAD over the network
@@ -104,7 +104,7 @@ uv run safety auth
uv run safety auth --login
```
**Note:** The Safety CLI authentication is stored locally and only needs to be done once per machine. If you skip this step, `just check` will show a clear error message with instructions. The `safety` pre-commit hook will also fail with an authentication prompt.
**Note:** The Safety CLI authentication is stored locally and only needs to be done once per machine. If you skip this step, `just quality::check` will show a clear error message with instructions. The `safety` pre-commit hook will also fail with an authentication prompt.
**CI/CD:** Safety runs in CI using the `SAFETY_API_KEY` repository secret. The API key is passed via environment variable to the pre-commit hook.
@@ -126,7 +126,7 @@ just coderabbit::login
```bash
# Review staged changes (most common)
just review
just coderabbit::review
# Review with auto-fix suggestions
just coderabbit::review-fix
@@ -149,45 +149,112 @@ This project uses [`just`](https://just.systems/) as a command runner. Always pr
Commands are organized into modules for better organization:
```bash
# List all available commands
just --list
# List top-level commands and available modules
just
# List commands in a specific module
just --list docker
just --list quality
just --list testing
just --list freecad
just --list documentation
just --list coderabbit
# List commands in a specific module (use list-<module>)
just list-mcp # MCP server commands
just list-freecad # FreeCAD plugin/macro commands
just list-install # Installation commands
just list-quality # Code quality commands
just list-testing # Test commands
just list-docker # Docker commands
just list-documentation # Documentation commands
just list-dev # Development utilities
just list-release # Release and tagging commands
just list-coderabbit # AI code review commands
# Common shortcut commands (aliases to module commands)
just install # Install project dependencies
just lint # Run all linters (quality::lint)
just format # Format code (quality::format)
just test # Run tests (testing::unit)
just check # Run pre-commit checks (quality::check)
just docs # Build documentation (documentation::build)
just all # Run all checks and tests
# List ALL commands from all modules at once
just list-all
# Module commands (use :: syntax)
# MCP server commands
just mcp::run # Run the MCP server (stdio mode)
just mcp::run-debug # Run with debug logging
just mcp::run-http # Run in HTTP mode for remote access
# FreeCAD commands
just freecad::run-gui # Run FreeCAD GUI with MCP bridge
just freecad::run-headless # Run FreeCAD headless with MCP bridge
# Installation commands (for end users)
just install::mcp-server # Install MCP server system-wide (via uv tool)
just install::mcp-bridge-workbench # Install FreeCAD workbench addon
just install::macro-all # Install all macros
just install::macro-cut # Install CutObjectForMagnets macro
just install::macro-export # Install MultiExport macro
just install::status # Check installation status
# Quality commands
just quality::check # Run all pre-commit checks
just quality::lint # Run linting
just quality::format # Format code
just quality::typecheck # Run type checking
just quality::security # Run security scanning
just quality::scan # Run all secrets scanners
# Testing commands
just testing::unit # Run unit tests
just testing::cov # Run tests with coverage
just testing::fast # Run tests without slow markers
just testing::integration # Run integration tests
just testing::integration-freecad # Integration tests with auto FreeCAD startup
just testing::watch # Run tests in watch mode
just testing::all # Run all tests including integration
# Documentation commands
just documentation::build # Build documentation
just documentation::serve # Serve documentation locally
# Docker commands
just docker::build # Build Docker image for local architecture
just docker::build-multi # Build multi-arch image (amd64 + arm64)
just docker::run # Run Docker container
just quality::secrets # Run all secrets scanners
just testing::cov # Run tests with coverage
just freecad::run-gui # Run FreeCAD GUI with MCP bridge
just documentation::serve # Serve documentation locally
just docker::clean # Remove local Docker image
just docker::clean-all # Remove images and build cache
# Development utilities
just dev::install-deps # Install all project dependencies
just dev::install-pre-commit # Install pre-commit hooks
just dev::update-deps # Update all dependencies
just dev::clean # Clean build artifacts and caches
just dev::repl # Open Python REPL with project loaded
just dev::tree # Show project structure
just dev::validate # Validate project configuration
# AI code review commands
just coderabbit::review # Review staged changes
just coderabbit::review-fix # Review with auto-fix suggestions
# Release commands (component-specific tagging)
just release::status # Show unreleased changes across all components
just release::tag-mcp-server 1.0.0 # Release MCP server (PyPI + Docker)
just release::tag-workbench 1.0.0 # Release MCP Bridge workbench
just release::tag-macro-magnets 1.0.0 # Release Cut Object for Magnets macro
just release::tag-macro-export 1.0.0 # Release Multi Export macro
just release::list-tags # List all release tags
just release::latest-versions # Show latest version of each component
just release::delete-tag <tag> # Delete a release tag (local and remote)
# Combined workflows
just setup # Full dev setup (install deps + hooks)
just all # Run all quality checks and unit tests
just all-with-integration # Run all checks + integration tests
just ci # Full CI pipeline (checks + coverage)
```
#### Just Module Structure
| Module | Description | Key Commands |
| --------------- | ----------------------------------- | -------------------------------------------- |
| `docker` | Docker build and run commands | `build`, `build-multi`, `build-push`, `run` |
| `quality` | Code quality and linting | `check`, `lint`, `format`, `secrets` |
| `testing` | Test execution | `unit`, `cov`, `integration`, `all` |
| `freecad` | FreeCAD plugin and macro management | `run-gui`, `run-headless`, `install-*-macro` |
| --------------- | ------------------------------------- | --------------------------------------------------- |
| `mcp` | MCP server commands | `run`, `run-debug`, `run-http` |
| `freecad` | FreeCAD running commands | `run-gui`, `run-headless`, `run-gui-custom` |
| `install` | User installation commands | `mcp-server`, `mcp-bridge-workbench`, `macro-all` |
| `quality` | Code quality and linting | `check`, `lint`, `format`, `scan` |
| `testing` | Test execution | `unit`, `cov`, `integration-freecad`, `watch` |
| `docker` | Docker build and run commands | `build`, `build-multi`, `run`, `clean-all` |
| `documentation` | Documentation building | `build`, `serve`, `open` |
| `dev` | Development utilities | `install-deps`, `update-deps`, `clean` |
| `release` | Release and tagging | `status`, `tag-mcp-server`, `delete-tag` |
| `coderabbit` | AI code reviews (local) | `install`, `login`, `review`, `review-fix` |
Module files are located in the `just/` directory.
@@ -200,20 +267,51 @@ Module files are located in the `just/` directory.
**CRITICAL**: This project uses `pre-commit` for all code quality checks. Before finishing ANY code changes:
1. Run `just check` or `uv run pre-commit run --all-files`
1. Run `just quality::check` or `uv run pre-commit run --all-files`
1. Fix ALL issues reported
1. Re-run until all checks pass
Pre-commit runs these checks:
**Python Quality:**
- **Ruff**: Linting and import sorting (replaces flake8, isort, pyupgrade)
- **Ruff Format**: Code formatting (replaces black)
- **MyPy**: Static type checking
- **Bandit**: Security vulnerability scanning
- **Safety**: Dependency vulnerability checking
- **Safety**: Dependency vulnerability checking (requires `.safety-policy.yml`)
**Secrets Detection (Multi-Layer):**
- **Gitleaks**: Fast regex-based secrets scanning
- **detect-secrets**: Baseline tracking for known/approved secrets
- **TruffleHog**: Verified secrets detection (skipped in CI due to wasm bugs)
**Documentation & Config:**
- **Markdownlint**: Markdown linting with auto-fix
- **md-toc**: Table of contents generation for README
- **Codespell**: Spell checking in code and docs
- **YAML/TOML/JSON validation**: Config file validation
- **MkDocs build**: Validates documentation builds successfully
- **YAML/TOML/JSON/XML validation**: Config file validation
- **check-json5**: JSONC validation for VS Code config files
**Infrastructure:**
- **Hadolint**: Dockerfile linting
- **Trivy**: Dockerfile security misconfiguration scanning
- **Shellcheck**: Shell script linting
- **Actionlint**: GitHub Actions workflow linting
- **validate-pyproject**: Python project configuration validation
- **check-github-workflows**: GitHub workflow schema validation
- **check-dependabot**: Dependabot config validation
**Other:**
- **Trailing whitespace and EOF fixes**: File hygiene
- **Commitizen**: Commit message format validation (commit-msg stage)
- **no-commit-to-branch**: Prevents commits to main/master
- **CodeRabbit**: AI code review (manual stage only)
### Linting Rules
@@ -241,20 +339,20 @@ This project uses a comprehensive, multi-layer approach to secrets detection:
```bash
# Run all secrets scanners
just secrets
just quality::scan
# Individual scanners
just secrets-gitleaks # Fast pattern matching
just secrets-gitleaks-history # Scan git history
just secrets-detect # Check against baseline
just secrets-audit # Interactive baseline audit
just secrets-trufflehog # Verified secrets only
just quality::scan-gitleaks # Fast pattern matching
just quality::scan-gitleaks-history # Scan git history
just quality::scan-detect # Check against baseline
just quality::scan-audit # Interactive baseline audit
just quality::scan-trufflehog # Verified secrets only
```
**Managing False Positives:**
1. **Gitleaks**: Add patterns to `.gitleaks.toml` allowlist section
1. **detect-secrets**: Run `just secrets-audit` to mark false positives in baseline
1. **detect-secrets**: Run `just quality::scan-audit` to mark false positives in baseline
1. **TruffleHog**: Uses `--only-verified` to minimize false positives
### Markdown Linting
@@ -262,8 +360,8 @@ just secrets-trufflehog # Verified secrets only
All markdown files are linted for consistency:
```bash
just markdown-lint # Check markdown files
just markdown-fix # Auto-fix markdown issues
just quality::markdown-lint # Check markdown files
just quality::markdown-fix # Auto-fix markdown issues
```
Configuration: `.markdownlint.yaml`
@@ -324,13 +422,50 @@ Configuration: `.markdownlint.yaml`
### Documentation Building
Documentation is auto-generated using the docstrings. Run:
Documentation is auto-generated using MkDocs with the Material theme. Run:
```bash
just docs # Build documentation
just docs-serve # Serve documentation locally
just documentation::build # Build documentation
just documentation::serve # Serve documentation locally
just documentation::open # Open docs in browser
```
### MkDocs Configuration
**Theme**: Material for MkDocs with dark mode default, deep purple color scheme.
**Key Plugins**:
- **mkdocstrings**: Auto-generates API docs from Python docstrings
- **mkdocs-macros-plugin**: Variables and templating in markdown
- **git-revision-date-localized**: Shows "Last updated" on pages
- **glightbox**: Image lightbox/zoom functionality
**Macros Plugin - Custom Delimiters**:
To avoid conflicts with Python dict literals in code blocks, this project uses custom delimiters:
```markdown
<!-- Standard Jinja2 (DON'T USE): {{ variable }} -->
<!-- Use instead: -->
{{@ variable @}}
<!-- For blocks: -->
{%@ if condition @%}
...
{%@ endif @%}
```
Variables are defined in `docs/variables.yaml`:
```yaml
project_name: FreeCAD MCP Server
xmlrpc_port: 9875
socket_port: 9876
```
**Reference**: See `docs/development/mkdocs-guide.md` for complete documentation on available extensions (admonitions, tabs, code annotations, mermaid diagrams, etc.).
---
## Testing Requirements
@@ -391,9 +526,10 @@ class TestCalculateTotal:
### Running Tests
```bash
just test # Run all tests
just test-cov # Run tests with coverage report
just test-fast # Run tests without slow markers
just testing::unit # Run unit tests
just testing::cov # Run tests with coverage report
just testing::fast # Run tests without slow markers
just testing::all # Run all tests including integration
uv run pytest tests/unit/ # Run specific test directory
uv run pytest -k "test_name" # Run specific test by name
```
@@ -405,7 +541,7 @@ uv run pytest -k "test_name" # Run specific test by name
### Before Making Changes
1. Ensure you're on the latest code
1. Run `just install` to update dependencies
1. Run `just install::mcp-server` to update dependencies
1. Run `just all` to verify clean starting state
### After Making Changes
@@ -414,12 +550,12 @@ uv run pytest -k "test_name" # Run specific test by name
1. [ ] **Add/update docstrings** for all new/modified code
1. [ ] **Add/update tests** for all new/modified functionality
1. [ ] **Run formatting**: `just format`
1. [ ] **Run linting**: `just lint` - fix ALL issues
1. [ ] **Run type checking**: `just typecheck` - fix ALL issues
1. [ ] **Run security checks**: `just security` - fix ALL issues
1. [ ] **Run tests**: `just test` - ALL tests must pass
1. [ ] **Run pre-commit**: `just check` - ALL checks must pass
1. [ ] **Run formatting**: `just quality::format`
1. [ ] **Run linting**: `just quality::lint` - fix ALL issues
1. [ ] **Run type checking**: `just quality::typecheck` - fix ALL issues
1. [ ] **Run security checks**: `just quality::security` - fix ALL issues
1. [ ] **Run tests**: `just testing::unit` - ALL tests must pass
1. [ ] **Run pre-commit**: `just quality::check` - ALL checks must pass
### Quick Verification Command
@@ -455,7 +591,7 @@ This catches issues early and ensures code quality standards are met. Never skip
- This allows the package to work as a library while ensuring reproducibility
- **Do not change `>=` to `==` in pyproject.toml** - this would break library usability
- Regularly update dependencies with `just update-deps` (updates uv.lock)
- Check for security vulnerabilities with `just security`
- Check for security vulnerabilities with `just quality::security`
### Core Dependencies
@@ -473,24 +609,83 @@ Keep these tools at their latest stable versions:
```text
project-root/
├── .github/
│ ├── ISSUE_TEMPLATE/ # GitHub issue templates
│ │ └── *.yaml
│ ├── workflows/ # GitHub Actions workflows
│ │ ├── codeql.yaml # Security analysis
│ │ ├── docker.yaml # Docker build (CI)
│ │ ├── macro-cut-magnets-release.yaml # Macro release
│ │ ├── macro-multi-export-release.yaml # Macro release
│ │ ├── macro-release-reusable.yaml # Shared macro release logic
│ │ ├── macro-test.yaml # Macro testing
│ │ ├── mcp-server-release.yaml # MCP server → PyPI/Docker
│ │ ├── mcp-workbench-release.yaml # Workbench → GitHub Release
│ │ ├── pre-commit.yaml # Pre-commit checks
│ │ └── test.yaml # Unit/integration tests
│ └── dependabot.yaml # Dependency updates
├── addon/ # FreeCAD addon (workbench)
│ └── FreecadRobustMCP/ # MCP Bridge workbench
│ ├── freecad_mcp_bridge/ # Bridge Python package
│ ├── Init.py # FreeCAD workbench init
│ ├── InitGui.py # FreeCAD GUI init
│ └── metadata.txt # Addon metadata
├── docs/ # MkDocs documentation source
│ ├── assets/ # Images, diagrams
│ ├── development/ # Developer guides
│ ├── getting-started/ # Installation, quickstart
│ ├── guide/ # User guides
│ ├── macros/ # Macro documentation
│ ├── reference/ # API reference
│ ├── variables.yaml # MkDocs macro variables
│ └── index.md # Documentation home
├── just/ # Just module files
│ ├── coderabbit.just # AI code review commands
│ ├── dev.just # Development utilities
│ ├── docker.just # Docker build/run commands
│ ├── documentation.just # Documentation commands
│ ├── freecad.just # FreeCAD running commands
│ ├── install.just # Installation commands
│ ├── mcp.just # MCP server commands
│ ├── quality.just # Code quality commands
│ ├── release.just # Release/tagging commands
│ └── testing.just # Test commands
├── macros/ # FreeCAD macro source
│ ├── Cut_Object_for_Magnets/
│ │ ├── CutObjectForMagnets.FCMacro
│ │ └── README-CutObjectForMagnets.md
│ └── Multi_Export/
│ ├── MultiExport.FCMacro
│ └── README-MultiExport.md
├── src/
│ └── freecad_mcp/ # Main MCP server package
│ ├── bridge/ # FreeCAD connection bridges
│ ├── prompts/ # MCP prompt templates
│ ├── resources/ # MCP resources
│ ├── tools/ # MCP tools (document, object, etc.)
│ ├── __init__.py
│ ├── server.py # Main MCP server
│ └── settings.py # Configuration settings
├── tests/
│ ├── fixtures/ # Test data files (.FCStd, etc.)
│ ├── integration/ # Integration tests (require FreeCAD)
│ ├── unit/ # Unit tests
│ └── conftest.py # Shared pytest fixtures
├── .codespell-ignore-words.txt # Spell checker exceptions
├── .gitleaks.toml # Gitleaks secrets config
├── .markdownlint.yaml # Markdown linting rules
├── .mise.toml # Tool version management
├── .pre-commit-config.yaml # Pre-commit hook configuration
├── justfile # Main task runner (imports modules)
├── just/ # Just module files
│ ├── docker.just # Docker build/run commands
│ ├── quality.just # Code quality commands
│ ├── testing.just # Test commands
│ ├── freecad.just # FreeCAD plugin/macro commands
│ └── documentation.just # Documentation commands
├── pyproject.toml # Project configuration and dependencies
├── .safety-policy.yml # Safety CLI scan policy
├── .secrets.baseline # detect-secrets baseline
├── CHANGELOG.md # Project changelog
├── CLAUDE.md # This file (AI assistant guidelines)
├── Dockerfile # Docker image definition
├── src/
│ └── package_name/ # Main package source
│ ├── __init__.py
│ └── *.py
── tests/ # Test files
├── docs/ # Documentation source
└── CLAUDE.md # This file
├── justfile # Main task runner (imports modules)
├── mkdocs.yaml # MkDocs configuration
├── package.xml # FreeCAD addon metadata
├── pyproject.toml # Project configuration and dependencies
── uv.lock # Locked dependency versions
```
---
@@ -518,6 +713,56 @@ When creating new files, always use the full extension.
---
## Configuration Files Reference
This section describes the purpose and key settings in each configuration file.
### Tool Management
| File | Purpose |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `.mise.toml` | Pins versions for Python, uv, just, pre-commit, and security tools (trivy, gitleaks, hadolint, shellcheck, actionlint, markdownlint-cli2). Also sets environment variables for FreeCAD connection settings. |
| `pyproject.toml` | Python project configuration: dependencies, build system, tool configs (ruff, mypy, pytest, bandit, codespell, commitizen). |
| `uv.lock` | Exact locked versions of all Python dependencies for reproducible builds. |
### Code Quality
| File | Purpose |
| ----------------------------- | ------------------------------------------------------------------------------------- |
| `.pre-commit-config.yaml` | Pre-commit hook definitions. See [Pre-commit Hooks](#pre-commit-hooks) for details. |
| `.markdownlint.yaml` | Markdown linting rules (heading style, list indentation, code block style, etc.). |
| `.codespell-ignore-words.txt` | Words to ignore during spell checking (technical terms, tool names). |
### Security Scanning
| File | Purpose |
| --------------------- | ------------------------------------------------------------------------------------------------------------- |
| `.gitleaks.toml` | Gitleaks secrets scanner configuration with allowlist patterns for false positives. |
| `.secrets.baseline` | detect-secrets baseline tracking known/approved secrets and their locations. |
| `.safety-policy.yml` | Safety CLI scan policy - excludes `.venv`, `node_modules`, and other directories from vulnerability scanning. |
### Documentation
| File | Purpose |
| --------------------- | ------------------------------------------------------------------------------------------------------------ |
| `mkdocs.yaml` | MkDocs configuration: Material theme, plugins (macros, mkdocstrings, git-revision-date), navigation structure. |
| `docs/variables.yaml` | Variables for MkDocs macros plugin (project name, ports, paths). Use `{{@ variable @}}` syntax in docs. |
### FreeCAD Addon
| File | Purpose |
| ------------- | ------------------------------------------------------------------------------------------ |
| `package.xml` | FreeCAD addon metadata with per-component versioning. Updated automatically by release workflows. |
### GitHub
| File | Purpose |
| ---------------------------- | -------------------------------------------------------------------------- |
| `.github/dependabot.yaml` | Dependabot configuration for automated dependency updates. |
| `.github/workflows/*.yaml` | CI/CD workflows. See [GitHub Workflows](#github-actions-workflows) for details. |
---
## Justfile HEREDOC Syntax
**CRITICAL**: When writing heredocs in justfile recipes, the content must be indented to match the recipe body (4 spaces). Just parses non-indented lines as justfile syntax, which causes errors with Python code containing dots (e.g., `sys.path`).
@@ -750,10 +995,10 @@ The following tools in `src/freecad_mcp/tools/view.py` check `FreeCAD.GuiUp`:
```bash
# GUI mode - with MCP bridge auto-started
just run-gui
just freecad::run-gui
# Headless mode - console only
just run-headless
just freecad::run-headless
# Or manually:
# GUI: /Applications/FreeCAD.app (macOS) or freecad (Linux)
@@ -851,13 +1096,187 @@ These checks are intentionally skipped in `pyproject.toml`:
### detect-secrets
- **False positives**: Add `# pragma: allowlist secret` comment to lines with false positives
- **Baseline updates**: Run `just secrets-audit` to update `.secrets.baseline`
- **Baseline updates**: Run `just quality::scan-audit` to update `.secrets.baseline`
### no-commit-to-branch
- This hook fails when on `main` or `master` branch - this is expected behavior
- Always work on feature branches for actual commits
### check-json5 (JSONC Support)
- VS Code configuration files (`.vscode/*.json`) use JSONC format (JSON with Comments)
- These files are excluded from the strict `check-json` hook
- The `check-json5` hook validates these files instead, allowing `//` comments
---
## GitHub Actions Workflows
This project uses component-specific release workflows along with CI/CD pipelines.
### CI Workflows
| Workflow | Trigger | Purpose |
| ------------------ | -------------------- | ---------------------------------------------------------- |
| `test.yaml` | Push, PR | Runs unit tests and integration tests on Ubuntu and macOS |
| `pre-commit.yaml` | Push, PR | Runs all pre-commit hooks for code quality |
| `docker.yaml` | Push, PR | Builds Docker image to verify Dockerfile works |
| `macro-test.yaml` | Push, PR | Tests FreeCAD macros in headless Docker environment |
| `codeql.yaml` | Push, PR, scheduled | GitHub CodeQL security analysis |
### Release Workflows
| Workflow | Trigger | Purpose |
| --------------------------------- | ------------------------------------ | -------------------------------------------------------- |
| `mcp-server-release.yaml` | Tag: `robust-mcp-server-v*` | Builds and publishes MCP server to PyPI and Docker Hub |
| `mcp-workbench-release.yaml` | Tag: `robust-mcp-workbench-v*` | Creates GitHub Release with workbench addon archive |
| `macro-cut-magnets-release.yaml` | Tag: `macro-cut-object-for-magnets-v*` | Creates GitHub Release with macro archive |
| `macro-multi-export-release.yaml` | Tag: `macro-multi-export-v*` | Creates GitHub Release with macro archive |
| `macro-release-reusable.yaml` | Called by macro release workflows | Shared logic for macro releases (DRY) |
### Release Workflow Features
**MCP Server Release** (`mcp-server-release.yaml`):
- Validates SemVer tag format
- Builds Python wheel and sdist
- Tests installation on Ubuntu and macOS
- Publishes to PyPI (stable) or TestPyPI (alpha)
- Builds multi-arch Docker image (amd64 + arm64)
- Pushes to Docker Hub with version tags
- Creates GitHub Release with artifacts and changelog
**Workbench/Macro Releases**:
- Validates tag format
- Updates version in source files automatically
- Updates `package.xml` per-component version
- Creates tar.gz and zip archives
- Extracts changelog section for release notes
- Creates GitHub Release with archives
---
## Release Process
This project uses **component-specific versioning**. Each component has its own git tag and release workflow:
| Component | Tag Format | Releases To |
| --------------------------- | ------------------------------------- | ------------------------------------ |
| MCP Server | `robust-mcp-server-vX.Y.Z` | PyPI, Docker Hub, GitHub Release |
| MCP Bridge Workbench | `robust-mcp-workbench-vX.Y.Z` | GitHub Release (archive) |
| Cut Object for Magnets Macro| `macro-cut-object-for-magnets-vX.Y.Z` | GitHub Release (archive) |
| Multi Export Macro | `macro-multi-export-vX.Y.Z` | GitHub Release (archive) |
### Changelog Management
The project uses a single `CHANGELOG.md` with sections for each component. Before releasing:
1. **Draft release notes** from conventional commits:
```bash
just release::draft-notes mcp-server
just release::draft-notes workbench
just release::draft-notes macro-magnets
just release::draft-notes macro-export
```
2. **Update CHANGELOG.md** with entries under the appropriate component header:
```markdown
## YYYY-MM-DD
### MCP Server vX.Y.Z
#### Added
- New feature
---
### MCP Bridge Workbench vX.Y.Z
...
```
3. The release workflow automatically extracts the changelog section for GitHub Releases.
### Creating a Release
Use the `just release::` commands to create and push release tags:
```bash
# Check what has unreleased changes
just release::status
# Preview changes since last release
just release::changes-since mcp-server
just release::changes-since workbench
# Release the MCP server (triggers PyPI, Docker, GitHub release)
just release::tag-mcp-server 1.0.0
# Release the MCP Bridge workbench
just release::tag-workbench 1.0.0
# Release macros
just release::tag-macro-magnets 1.0.0
just release::tag-macro-export 1.0.0
# View release tags
just release::list-tags
just release::latest-versions
```
### Version Format
All versions follow SemVer 2.0:
- `X.Y.Z` - Stable release
- `X.Y.Z-alpha` or `X.Y.Z-alpha.N` - Alpha (TestPyPI only)
- `X.Y.Z-beta` or `X.Y.Z-beta.N` - Beta (PyPI)
- `X.Y.Z-rc.N` - Release candidate (PyPI)
### What Happens on Release
**MCP Server Release:**
1. Validates tag format
2. Builds Python wheel and sdist
3. Tests installation on Ubuntu and macOS
4. Publishes to PyPI (or TestPyPI for alpha)
5. Builds multi-arch Docker image
6. Pushes to Docker Hub
7. Creates GitHub release with artifacts
**Workbench/Macro Release:**
1. Validates tag format
2. Updates version in source files (.FCMacro, README, wiki-source.txt)
3. Updates version in package.xml
4. Creates archive (tar.gz + zip)
5. Creates GitHub release with archives
### Package.xml Per-Component Versioning
The `package.xml` file uses per-content versioning as supported by FreeCAD:
```xml
<content>
<workbench>
<name>MCP Bridge</name>
<version>1.0.0</version>
...
</workbench>
<macro>
<name>Multi Export</name>
<version>0.8.0</version>
...
</macro>
</content>
```
Each component can have a different version, and the release workflows automatically update these when a component is released.
---
## Changelog Maintenance
+49 -37
View File
@@ -6,6 +6,8 @@
[![Pre-commit](https://github.com/spkane/freecad-robust-mcp-and-more/actions/workflows/pre-commit.yaml/badge.svg)](https://github.com/spkane/freecad-robust-mcp-and-more/actions/workflows/pre-commit.yaml)
[![CodeQL](https://github.com/spkane/freecad-robust-mcp-and-more/actions/workflows/codeql.yaml/badge.svg)](https://github.com/spkane/freecad-robust-mcp-and-more/actions/workflows/codeql.yaml)
[![PyPI Version](https://img.shields.io/pypi/v/freecad-robust-mcp)](https://pypi.org/project/freecad-robust-mcp/)
[![Python Versions](https://img.shields.io/pypi/pyversions/freecad-robust-mcp)](https://pypi.org/project/freecad-robust-mcp/)
[![Docker Image Version](https://img.shields.io/docker/v/spkane/freecad-robust-mcp?sort=semver&label=docker)](https://hub.docker.com/r/spkane/freecad-robust-mcp)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -268,10 +270,10 @@ Before your AI assistant can connect, you need to start the MCP bridge inside Fr
```bash
# Start FreeCAD with MCP bridge auto-started
just run-gui
just freecad::run-gui
# Or for headless/automation mode:
just run-headless
just freecad::run-headless
```
After starting the bridge, start/restart your MCP client (Claude Code, etc.) - it will connect automatically
@@ -291,7 +293,7 @@ To uninstall the MCP Bridge workbench:
If you previously used older versions of this project, you may have legacy components installed. Run this command to check what's installed and get cleanup instructions:
```bash
just freecad::mcp-status
just install::status
```
##### Manual Cleanup (if needed)
@@ -332,7 +334,7 @@ Run FreeCAD in console mode without GUI. Useful for automation.
```bash
# If installed from source:
just run-headless
just freecad::run-headless
```
**Note:** Screenshot and view features are not available in headless mode.
@@ -572,7 +574,7 @@ Export selected FreeCAD objects to multiple file formats simultaneously. Support
```bash
# If you have the source:
just freecad::install-export-macro
just install::macro-export
# Or manually copy MultiExport.FCMacro from macros/Multi_Export/
# to your FreeCAD macro directory:
@@ -687,35 +689,45 @@ mise where uv | sed 's|/installs/.*|/shims|'
### Development Workflow
Commands are organized into modules. Use `just --list` to see all shortcuts, or `just --list <module>` to see module-specific commands.
Commands are organized into modules. Use `just` to see top-level commands, or `just list-<module>` to see module-specific commands.
```bash
# Show all available commands
just --list
# Show top-level commands and available modules
just
# Show commands in a specific module
just --list docker
just --list quality
just --list testing
just --list freecad
just list-mcp # MCP server commands
just list-freecad # FreeCAD plugin/macro commands
just list-install # Installation commands
just list-quality # Code quality commands
just list-testing # Test commands
just list-docker # Docker commands
just list-documentation # Documentation commands
just list-dev # Development utilities
# List ALL commands from all modules
just list-all
# Install/update dependencies
just install
just install::mcp-server
# Run all checks (linting, type checking, tests)
just all
# Individual checks (shortcuts to module commands)
just lint # Run ruff linter (quality::lint)
just typecheck # Run mypy type checker (quality::typecheck)
just test # Run unit tests (testing::unit)
just check # Run all pre-commit hooks (quality::check)
# Quality commands
just quality::lint # Run ruff linter
just quality::typecheck # Run mypy type checker
just quality::format # Format code
just quality::check # Run all pre-commit hooks
# Format code
just format
# Testing commands
just testing::unit # Run unit tests
just testing::cov # Run tests with coverage
just testing::integration # Run integration tests
# Run with debug logging
just run-debug
# Run the MCP server (or with debug logging)
just mcp::run
just mcp::run-debug
# Docker commands
just docker::build # Build image for local architecture
@@ -729,29 +741,29 @@ just docker::run # Run container
```bash
# Start FreeCAD with auto-started bridge
just run-gui
just freecad::run-gui
```
#### Headless Mode (for automation/CI)
```bash
just run-headless
just freecad::run-headless
```
### Running Tests
```bash
# Unit tests only (no FreeCAD required)
just test
just testing::unit
# Unit tests with coverage
just test-cov
just testing::cov
# Integration tests (requires running FreeCAD bridge)
just test-integration
just testing::integration
# All tests with automatic FreeCAD startup
just integration
# Integration tests with automatic FreeCAD startup
just testing::integration-auto
```
### Code Quality
@@ -766,11 +778,11 @@ The project uses strict code quality checks via pre-commit:
```bash
# Run all pre-commit hooks
just check
just quality::check
# Run security/secrets scans
just security
just secrets
just quality::security
just quality::secrets
```
---
@@ -784,13 +796,13 @@ just secrets
**Installation for development:**
```bash
just install-cut-macro
just install::macro-cut
```
**Uninstall:**
```bash
just uninstall-cut-macro
just install::uninstall-macro-cut
```
See [macros/Cut_Object_for_Magnets/README-CutObjectForMagnets.md](macros/Cut_Object_for_Magnets/README-CutObjectForMagnets.md) for detailed documentation on the macro's internals.
@@ -802,13 +814,13 @@ See [macros/Cut_Object_for_Magnets/README-CutObjectForMagnets.md](macros/Cut_Obj
**Installation for development:**
```bash
just freecad::install-export-macro
just install::macro-export
```
**Uninstall:**
```bash
just freecad::uninstall-export-macro
just install::uninstall-macro-export
```
See [macros/Multi_Export/README-MultiExport.md](macros/Multi_Export/README-MultiExport.md) for detailed documentation on the macro's internals.
@@ -817,7 +829,7 @@ See [macros/Multi_Export/README-MultiExport.md](macros/Multi_Export/README-Multi
## Architecture
See [ARCHITECTURE-MCP.md](ARCHITECTURE-MCP.md) for detailed design documentation covering:
See the [detailed architecture document](docs/development/architecture-detailed.md) for design documentation covering:
- Module structure
- Bridge communication protocols
@@ -9,4 +9,5 @@ It is bundled with the workbench addon for self-contained installation.
from .server import FreecadMCPPlugin
__all__ = ["FreecadMCPPlugin"]
__version__ = "0.0.0" # Updated by release workflow
__all__ = ["FreecadMCPPlugin", "__version__"]
+4 -4
View File
@@ -33,7 +33,7 @@ You have two options depending on your workflow:
Best for automated workflows, batch processing, or when you don't need visual feedback.
```bash
just run-headless
just freecad::run-headless
```
**Capabilities:** All modeling operations, export, scripting.
@@ -44,7 +44,7 @@ just run-headless
Best for interactive design work where you want to see results visually.
```bash
just run-gui
just freecad::run-gui
```
**Capabilities:** Everything headless mode can do, plus screenshots, colors, view control.
@@ -441,7 +441,7 @@ Instead of creating many individual features:
You're running in headless mode and trying to use a GUI-only feature. Either:
- Switch to GUI mode: `just run-gui`
- Switch to GUI mode: `just freecad::run-gui`
- Use an alternative approach (e.g., skip visual operations)
### Objects Not Appearing
@@ -474,7 +474,7 @@ If the MCP bridge isn't responding:
1. Check FreeCAD is running with the bridge started
1. Verify ports 9875 (XML-RPC) and 9876 (socket) are available
1. Restart FreeCAD with `just run-gui` or `just run-headless`
1. Restart FreeCAD with `just freecad::run-gui` or `just freecad::run-headless`
---
@@ -87,7 +87,7 @@ This document describes the architecture for a Model Context Protocol (MCP) serv
## Competitive Analysis
See [docs/COMPARISON.md](docs/COMPARISON.md) for detailed analysis of existing implementations.
See [COMPARISON.md](../COMPARISON.md) for detailed analysis of existing implementations.
### Existing FreeCAD MCP Servers
+12 -7
View File
@@ -2,7 +2,7 @@
This document provides a technical overview of the FreeCAD MCP Server architecture.
For the full architecture document with design decisions and rationale, see [ARCHITECTURE-MCP.md](https://github.com/spkane/freecad-robust-mcp-and-more/blob/main/ARCHITECTURE-MCP.md).
For the full architecture document with design decisions and rationale, see [Detailed Architecture](architecture-detailed.md).
---
@@ -39,17 +39,18 @@ The FreeCAD MCP Server follows a **Bridge with Adapter** pattern:
```text
src/freecad_mcp/
├── __init__.py
├── __init__.py # Package entry point
├── _version.py # Version info (auto-generated)
├── server.py # Main MCP server entry point
├── config.py # Configuration management
├── py.typed # PEP 561 marker for type hints
├── bridge/ # FreeCAD communication layer
│ ├── __init__.py # Bridge factory
│ ├── base.py # Abstract bridge interface
│ ├── embedded.py # In-process FreeCAD (Linux only)
│ ├── socket.py # JSON-RPC socket bridge
── xmlrpc.py # XML-RPC bridge (recommended)
│ └── protocol.py # Wire protocol definitions
── xmlrpc.py # XML-RPC bridge (recommended)
├── tools/ # MCP tool implementations
│ ├── __init__.py
@@ -69,7 +70,8 @@ src/freecad_mcp/
│ ├── __init__.py
│ └── freecad.py # Modeling and debugging prompts
└── freecad_workbench_addon/ # Workbench addon (deprecated location)
└── utils/ # Utility modules
└── __init__.py
```
---
@@ -124,7 +126,6 @@ The workbench addon runs inside FreeCAD:
```text
addon/FreecadRobustMCP/
├── package.xml # FreeCAD addon metadata
├── Init.py # Module initialization
├── InitGui.py # GUI initialization (workbench)
├── FreecadRobustMCP.svg # Workbench icon
@@ -132,8 +133,12 @@ addon/FreecadRobustMCP/
├── __init__.py
├── server.py # XML-RPC/JSON-RPC server
└── headless_server.py # Headless mode launcher
package.xml # FreeCAD addon metadata (in project root)
```
Note: The `package.xml` file is in the project root, not inside the addon directory. This is because it defines metadata for multiple components (workbench and macros) in a single manifest.
### Thread Safety
The workbench uses a queue-based system for thread-safe GUI operations:
@@ -256,4 +261,4 @@ Embedded mode receives **minimal testing**:
## Next Steps
- [Contributing](contributing.md) - How to contribute
- [Full Architecture Document](https://github.com/spkane/freecad-robust-mcp-and-more/blob/main/ARCHITECTURE-MCP.md) - Complete design details
- [Detailed Architecture](architecture-detailed.md) - Complete design details
+11 -12
View File
@@ -46,15 +46,15 @@ uv run safety auth --login
### Running Tests
```bash
# Run all tests
just test
# Run unit tests only
# Run unit tests
just testing::unit
# Run with coverage
just testing::cov
# Run all tests including integration
just testing::all
# Run type checking
uv run mypy src/
```
@@ -63,16 +63,16 @@ uv run mypy src/
```bash
# Run all pre-commit checks
just check
just quality::check
# Run linting
just lint
just quality::lint
# Format code
just format
just quality::format
# Run security checks
just quality::secrets
just quality::security
```
---
@@ -106,20 +106,20 @@ freecad-robust-mcp-and-more/
- Follow PEP 8 with 88-character line length (ruff/black)
- Use type hints for all function signatures
- Write Google-style docstrings
- Run `just format` before committing
- Run `just quality::format` before committing
### Testing
- Write tests for all new functionality
- Maintain test coverage
- Run `just test` before submitting PRs
- Run `just all` before submitting PRs (runs quality checks + unit tests)
- Integration tests require FreeCAD (run via CI)
### Documentation
- Update docstrings for API changes
- Update user docs for feature changes
- Run `just docs` to build and verify
- Run `just documentation::build` to build and verify
### Commits
@@ -214,7 +214,6 @@ Currently, embedded mode has only mocked unit tests. Adding live integration tes
## Getting Help
- **Issues:** [GitHub Issues](https://github.com/spkane/freecad-robust-mcp-and-more/issues)
- **Discussions:** [GitHub Discussions](https://github.com/spkane/freecad-robust-mcp-and-more/discussions)
---
+570
View File
@@ -0,0 +1,570 @@
# MkDocs Documentation Guide
This project uses [MkDocs](https://www.mkdocs.org/) with the [Material theme](https://squidfunk.github.io/mkdocs-material/) for documentation. This guide covers our specific configuration, extensions, and how to use them.
## Quick Start
```bash
# Serve docs locally with live reload
just documentation::serve
# Build static site
just documentation::build
# Open docs in browser
just documentation::open
```
## Theme Configuration
We use **Material for MkDocs** with dark mode as the default. Users can toggle between dark and light modes using the sun/moon icon in the header.
**Color scheme:** Deep purple primary with purple accent.
## Markdown Extensions
### Code Blocks
#### Syntax Highlighting
Standard fenced code blocks with language hints:
````markdown
```python
def hello():
print("Hello, world!")
```
````
#### Line Numbers and Highlighting
````markdown
```python linenums="1" hl_lines="2 3"
def hello():
# These lines are highlighted
print("Hello!")
```
````
#### Code Annotations
Add numbered annotations that expand on hover:
````markdown
```python
def process(data): # (1)!
return data.strip() # (2)!
```
1. This function processes input data
2. Removes leading/trailing whitespace
````
#### Inline Code Highlighting
Use `#!python print("inline")` for inline syntax highlighting:
```markdown
Use `#!python print("inline")` for inline code with highlighting.
```
---
### Tabbed Content
Create tabs for platform-specific or alternative content:
```markdown
=== "macOS"
```bash
~/Library/Application Support/FreeCAD/Macro/
```
=== "Linux"
```bash
~/.local/share/FreeCAD/Macro/
```
=== "Windows"
```bash
%APPDATA%/FreeCAD/Macro/
```
```
**Renders as:**
<!-- markdownlint-disable MD046 -->
=== "macOS"
```bash
~/Library/Application Support/FreeCAD/Macro/
```
=== "Linux"
```bash
~/.local/share/FreeCAD/Macro/
```
=== "Windows"
```bash
%APPDATA%/FreeCAD/Macro/
```
<!-- markdownlint-enable MD046 -->
---
### Admonitions (Call-outs)
Create styled call-out boxes:
```markdown
!!! note "Optional Title"
This is a note admonition.
!!! warning
This is a warning without a custom title.
!!! danger "Critical"
This is a danger/error admonition.
!!! tip
This is a tip admonition.
!!! info
This is an info admonition.
!!! example
This is an example admonition.
```
**Collapsible admonitions:**
```markdown
??? note "Click to expand"
This content is hidden by default.
???+ note "Expanded by default"
This content is visible but can be collapsed.
```
**Available types:** `note`, `abstract`, `info`, `tip`, `success`, `question`, `warning`, `failure`, `danger`, `bug`, `example`, `quote`
---
### Task Lists
Create checkbox lists:
```markdown
- [x] Completed task
- [ ] Incomplete task
- [ ] Another task
```
**Renders as:**
- [x] Completed task
- [ ] Incomplete task
- [ ] Another task
---
### Keyboard Keys
Style keyboard shortcuts:
```markdown
Press ++ctrl+c++ to copy.
Press ++cmd+shift+p++ on macOS.
Press ++enter++ to confirm.
```
**Renders as:** Press ++ctrl+c++ to copy.
**Common keys:** `ctrl`, `alt`, `shift`, `cmd`, `enter`, `tab`, `esc`, `backspace`, `delete`, `up`, `down`, `left`, `right`, `f1`-`f12`
---
### Text Formatting
#### Highlighting
```markdown
==This text is highlighted==
```
**Renders as:** ==This text is highlighted==
#### Subscript and Superscript
```markdown
H~2~O (subscript)
x^2^ (superscript)
```
**Renders as:** H~2~O and x^2^
#### Strikethrough
```markdown
~~deleted text~~
```
**Renders as:** ~~deleted text~~
#### Critic Markup (for diffs/reviews)
```markdown
{--deleted--}
{++added++}
{~~old~>new~~}
{==highlighted==}
{>>comment<<}
```
---
### Definition Lists
```markdown
Term 1
: Definition for term 1
Term 2
: Definition for term 2
: Can have multiple definitions
```
**Renders as:**
Term 1
: Definition for term 1
Term 2
: Definition for term 2
: Can have multiple definitions
---
### Footnotes
```markdown
This needs a citation[^1].
[^1]: This is the footnote content.
```
---
### Mermaid Diagrams
Create diagrams using Mermaid syntax:
````markdown
```mermaid
graph LR
A[MCP Client] --> B[MCP Server]
B --> C[FreeCAD Bridge]
C --> D[FreeCAD]
```
````
**Renders as:**
```mermaid
graph LR
A[MCP Client] --> B[MCP Server]
B --> C[FreeCAD Bridge]
C --> D[FreeCAD]
```
**Supported diagram types:** flowchart, sequence, class, state, ER, gantt, pie, journey
---
## Plugins
### Git Revision Date
Pages automatically show "Last updated X ago" at the bottom. No action needed.
### Image Lightbox (GLightbox)
All images are automatically zoomable. Click any image to open in a lightbox overlay.
To exclude an image from lightbox:
```markdown
![Image](path/to/image.png){ .off-glb }
```
### Minification
HTML is automatically minified in production builds. No action needed.
### Search
Full-text search is enabled. Features:
- Search suggestions as you type
- Search result highlighting
- Shareable search URLs
---
## Macros Plugin
### Custom Delimiters
<!-- markdownlint-disable MD046 -->
!!! warning "Important"
We use **custom delimiters** to avoid conflicts with Python dict literals in code blocks.
- Variables: `{` `{@` and `@}` `}`
- Blocks: `{%` `@` and `@` `%}`
Standard Jinja2 `{` `{` `}` `}` syntax will NOT work.
<!-- markdownlint-enable MD046 -->
### Using Variables
Variables are defined in `docs/variables.yaml`:
```yaml
# docs/variables.yaml
project_name: FreeCAD MCP Server
package_name: freecad-robust-mcp
xmlrpc_port: 9875
socket_port: 9876
```
Use in Markdown:
```markdown
The default XML-RPC port is {{@ xmlrpc_port @}}.
Install with: `pip install {{@ package_name @}}`
```
### Available Variables
| Variable | Value | Description |
|----------|-------|-------------|
| `project_name` | FreeCAD MCP Server | Display name |
| `package_name` | freecad-robust-mcp | PyPI package name |
| `docker_image` | spkane/freecad-robust-mcp | Docker image |
| `xmlrpc_port` | 9875 | XML-RPC server port |
| `socket_port` | 9876 | Socket server port |
| `paths.macos.macro` | ~/Library/... | macOS macro path |
| `paths.linux.macro` | ~/.local/share/... | Linux macro path |
| `paths.windows.macro` | %APPDATA%/... | Windows macro path |
### Built-in Macros
```markdown
Current time: {{@ now() @}}
Page URL: {{@ page.url @}}
```
---
## API Documentation (mkdocstrings)
Auto-generate API docs from Python docstrings:
```markdown
::: freecad_mcp.server
options:
show_source: true
heading_level: 3
```
This renders the module's docstrings, classes, and functions automatically.
**Options:**
- `show_source: true` - Show source code
- `heading_level: 3` - Start headings at h3
- `members: [func1, func2]` - Only show specific members
- `filters: ["!^_"]` - Exclude private members
---
## Navigation
### Features Enabled
- **Instant navigation** - Pages load without full refresh
- **Navigation tabs** - Top-level sections as tabs
- **Section index pages** - Click section to see overview
- **Back to top** - Button appears when scrolling
- **Table of contents** - Follows scroll position
---
## Images
### Basic Image
```markdown
![Alt text](path/to/image.png)
```
### Image with Caption
```markdown
<figure markdown>
![Alt text](path/to/image.png)
<figcaption>This is the caption</figcaption>
</figure>
```
### Image Alignment
```markdown
![Image](image.png){ align=left }
![Image](image.png){ align=right }
```
### Image Size
```markdown
![Image](image.png){ width="300" }
```
---
## Links
### Internal Links
```markdown
[Installation Guide](../getting-started/installation.md)
[Config section](configuration.md#environment-variables)
```
### External Links
External links automatically open in new tab (Material theme default).
```markdown
[FreeCAD](https://www.freecad.org/)
```
---
## Tables
Standard Markdown tables with sorting enabled:
```markdown
| Column 1 | Column 2 | Column 3 |
|----------|----------|----------|
| Data 1 | Data 2 | Data 3 |
| Data 4 | Data 5 | Data 6 |
```
---
## File Structure
```text
docs/
├── index.md # Home page
├── variables.yaml # Macros variables
├── overrides/ # Theme customizations
│ └── .gitkeep
├── assets/ # Images, favicon, etc.
├── getting-started/
│ ├── installation.md
│ ├── configuration.md
│ └── quickstart.md
├── guide/
│ └── ...
├── api/
│ └── ...
└── development/
├── contributing.md
├── architecture.md
├── releasing.md
└── mkdocs-guide.md # This file
```
---
## Common Tasks
### Adding a New Page
1. Create the markdown file in the appropriate directory
2. Add to `nav:` section in `mkdocs.yaml`
### Adding Redirects
If you move/rename a page, add a redirect in `mkdocs.yaml`:
```yaml
plugins:
- redirects:
redirect_maps:
'old-page.md': 'new-location/new-page.md'
```
### Custom CSS
Add custom styles in `docs/overrides/stylesheets/extra.css` and reference in `mkdocs.yaml`:
```yaml
extra_css:
- overrides/stylesheets/extra.css
```
### Custom JavaScript
Add scripts in `docs/overrides/javascripts/extra.js` and reference in `mkdocs.yaml`:
```yaml
extra_javascript:
- overrides/javascripts/extra.js
```
---
## Troubleshooting
### Macros Not Rendering
Remember to use custom delimiters: `{{@ variable @}}` not `{{ variable }}`.
### Git Revision Date Warnings
New files not yet committed will show warnings. Commit the file to fix.
### Build Errors
```bash
# Check for syntax errors
uv run mkdocs build --strict
# Verbose output
uv run mkdocs build -v
```
### Serve Not Auto-Reloading
Some changes (like `mkdocs.yaml`) require restarting the server.
---
## Reference Links
- [MkDocs Documentation](https://www.mkdocs.org/)
- [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/)
- [PyMdown Extensions](https://facelessuser.github.io/pymdown-extensions/)
- [mkdocstrings](https://mkdocstrings.github.io/)
- [mkdocs-macros-plugin](https://mkdocs-macros-plugin.readthedocs.io/)
+416
View File
@@ -0,0 +1,416 @@
# Release Process
This project uses **component-specific versioning**. Each component (MCP Server, Workbench, Macros) has its own version and release cycle, allowing independent updates without affecting other components.
## Components and Their Release Targets
| Component | Tag Format | Releases To |
| --------- | ---------- | ----------- |
| MCP Server | `robust-mcp-server-vX.Y.Z` | PyPI, Docker Hub, GitHub Release |
| MCP Bridge Workbench | `robust-mcp-workbench-vX.Y.Z` | GitHub Release (archive) |
| Cut Object for Magnets Macro | `macro-cut-object-for-magnets-vX.Y.Z` | GitHub Release (archive) |
| Multi Export Macro | `macro-multi-export-vX.Y.Z` | GitHub Release (archive) |
## Version Format
All versions follow [Semantic Versioning 2.0](https://semver.org/):
- `X.Y.Z` - Stable release (published to PyPI for MCP Server)
- `X.Y.Z-alpha` or `X.Y.Z-alpha.N` - Alpha pre-release (TestPyPI only)
- `X.Y.Z-beta` or `X.Y.Z-beta.N` - Beta pre-release (PyPI)
- `X.Y.Z-rc.N` - Release candidate (PyPI)
## Release Workflow Overview
Releases follow a **two-step process**:
1. **Bump**: Update version in all source files locally, then commit
2. **Tag**: Create and push the git tag (triggers CI/CD)
This ensures the version in source files matches the git tag on the same commit.
```text
┌─────────────────────────────────────────────────────────────────┐
│ 1. just release::bump-<component> X.Y.Z │
│ └── Updates version in all relevant files │
│ │
│ 2. git add -A && git commit -m "chore: bump ... to X.Y.Z" │
│ └── Commit the version changes │
│ │
│ 3. just release::tag-<component> X.Y.Z │
│ └── Verifies versions match, then creates & pushes tag │
│ │
│ 4. GitHub Actions workflow runs automatically │
│ └── Verifies versions, builds, and publishes release │
└─────────────────────────────────────────────────────────────────┘
```
## Pre-Release Checklist
Before creating any release, complete these steps:
### 1. Check Release Status
See which components have unreleased changes:
```bash
just release::status
```
This shows the number of commits since the last release for each component.
### 2. Review Changes
View the specific changes for the component you're releasing:
```bash
# For MCP Server
just release::changes-since mcp-server
# For Workbench
just release::changes-since workbench
# For Macros
just release::changes-since macro-magnets
just release::changes-since macro-export
```
### 3. Run All Quality Checks
Ensure all tests and checks pass:
```bash
just all
```
For a more thorough check including integration tests:
```bash
just all-with-integration
```
### 4. Update CHANGELOG.md
The changelog uses a multi-component structure. Each release date section contains entries for each component that was released.
#### Draft Release Notes
Use the `draft-notes` command to generate a starting point from conventional commits:
```bash
# Generate draft notes for a component
just release::draft-notes mcp-server
just release::draft-notes workbench
just release::draft-notes macro-magnets
just release::draft-notes macro-export
```
This categorizes commits by type (feat, fix, refactor, etc.) to help you write the changelog entry.
#### Changelog Structure
Add entries under the appropriate component section in `CHANGELOG.md`:
```markdown
## YYYY-MM-DD - Release Title (optional)
### MCP Server vX.Y.Z
Brief description of the release.
#### Added
- New feature description
#### Changed
- Changed behavior description
#### Fixed
- Bug fix description
---
### MCP Bridge Workbench vX.Y.Z
...
```
**Important:** The changelog section header format must match exactly for the GitHub Release workflow to extract it:
- MCP Server: `### MCP Server vX.Y.Z`
- Workbench: `### MCP Bridge Workbench vX.Y.Z`
- Magnets Macro: `### Cut Object for Magnets Macro vX.Y.Z`
- Export Macro: `### Multi Export Macro vX.Y.Z`
The release workflows automatically extract the changelog section and include it in the GitHub Release body.
## Releasing Each Component
### MCP Server Release
The MCP Server is the main Python package. It uses `setuptools-scm` to derive version from git tags at build time, so there's no version bump step needed.
```bash
# 1. Ensure all changes are committed
git status # Should show clean working tree
# 2. Update CHANGELOG.md and commit
git add CHANGELOG.md
git commit -m "docs: update changelog for MCP Server v1.0.0"
# 3. Create and push the release tag
just release::tag-mcp-server 1.0.0
```
**What happens automatically:**
1. GitHub Actions validates the tag format
2. Builds Python wheel and source distribution (version from tag)
3. Tests installation on Ubuntu and macOS
4. Publishes to PyPI (or TestPyPI for alpha versions)
5. Builds multi-architecture Docker image (amd64 + arm64)
6. Pushes to Docker Hub as `spkane/freecad-robust-mcp:1.0.0`
7. Creates GitHub Release with wheel and tar.gz artifacts
**Pre-release versions:**
```bash
# Alpha (goes to TestPyPI only)
just release::tag-mcp-server 1.0.0-alpha.1
# Beta (goes to PyPI)
just release::tag-mcp-server 1.0.0-beta.1
# Release candidate (goes to PyPI)
just release::tag-mcp-server 1.0.0-rc.1
```
### MCP Bridge Workbench Release
The workbench is a FreeCAD addon that provides the MCP bridge GUI.
```bash
# 1. Bump version in source files
just release::bump-workbench 1.0.0
# 2. Review and commit the changes
git diff # Review changes
git add -A
git commit -m "chore: bump workbench to 1.0.0"
# 3. Create and push the release tag
just release::tag-workbench 1.0.0
```
**Files updated by `bump-workbench`:**
- `addon/FreecadRobustMCP/freecad_mcp_bridge/__init__.py` (`__version__`)
- `package.xml` (workbench section: `<version>` and `<date>`)
**What happens automatically:**
1. GitHub Actions validates the tag format
2. Verifies version in source files matches tag
3. Creates archive (tar.gz and zip)
4. Creates GitHub Release with the archives
### Macro Releases
Each macro can be released independently.
**Cut Object for Magnets Macro:**
```bash
# 1. Bump version in source files
just release::bump-macro-magnets 1.0.0
# 2. Review and commit the changes
git diff # Review changes
git add -A
git commit -m "chore: bump Cut Object for Magnets macro to 1.0.0"
# 3. Create and push the release tag
just release::tag-macro-magnets 1.0.0
```
**Multi Export Macro:**
```bash
# 1. Bump version in source files
just release::bump-macro-export 1.0.0
# 2. Review and commit the changes
git diff # Review changes
git add -A
git commit -m "chore: bump Multi Export macro to 1.0.0"
# 3. Create and push the release tag
just release::tag-macro-export 1.0.0
```
**Files updated by macro bump commands:**
- `macros/<MacroDir>/<Macro>.FCMacro` (`__Version__` and `__Date__`)
- `macros/<MacroDir>/README-<Macro>.md` (`**Version:**`)
- `macros/<MacroDir>/wiki-source.txt` (`|Version=` and `|Date=`)
- `package.xml` (macro section: `<version>` and `<date>`)
**What happens automatically:**
1. GitHub Actions validates the tag format
2. Verifies version in source files matches tag
3. Creates archive (tar.gz and zip)
4. Creates GitHub Release with the archives
## Verifying a Release
### Check GitHub Actions
After pushing a tag, monitor the release workflow:
1. Go to [GitHub Actions](https://github.com/spkane/freecad-robust-mcp-and-more/actions)
2. Find the workflow run triggered by your tag
3. Verify all steps complete successfully
### Verify Published Artifacts
**For MCP Server:**
```bash
# Check PyPI
pip index versions freecad-robust-mcp
# Check Docker Hub
docker pull spkane/freecad-robust-mcp:1.0.0
```
**For Workbench/Macros:**
- Check the GitHub Releases page for the archive downloads
- Verify the `package.xml` version was updated
## Managing Release Tags
### List All Tags
```bash
# All tags grouped by component
just release::list-tags
# Latest version of each component
just release::latest-versions
```
### Delete a Tag (If Needed)
If a release has issues and needs to be removed:
```bash
just release::delete-tag robust-mcp-server-v1.0.0
```
!!! warning "Deleting Published Releases"
Deleting a tag does not unpublish from PyPI or Docker Hub. Contact the respective registries to remove published artifacts if necessary.
### Preview a Release (Dry Run)
To see what a release tag would look like without creating it:
```bash
just release::dry-run-tag mcp-server 1.0.0
just release::dry-run-tag workbench 1.0.0
just release::dry-run-tag macro-magnets 1.0.0
```
## Troubleshooting
### Release Workflow Failed
1. Check the GitHub Actions logs for the specific error
2. Common issues:
- Invalid version format (must be semver)
- Version mismatch between source files and tag
- Missing secrets (PyPI token, Docker credentials)
- Tests failing during the release build
### Version Mismatch Error
If the workflow fails with "Version mismatch":
```bash
# You forgot to bump the version before tagging
# Delete the tag
just release::delete-tag <full-tag-name>
# Bump the version
just release::bump-<component> X.Y.Z
# Commit the changes
git add -A && git commit -m "chore: bump <component> to X.Y.Z"
# Create the tag again
just release::tag-<component> X.Y.Z
```
### Tag Already Exists
If you try to create a tag that already exists:
```bash
# Delete the existing tag first
just release::delete-tag robust-mcp-server-v1.0.0
# Then create the new tag
just release::tag-mcp-server 1.0.0
```
### Wrong Version Released
1. Delete the tag: `just release::delete-tag <tag>`
2. Create a new release with the correct version
3. For PyPI: you cannot re-upload the same version; increment the patch version instead
## Release Cadence Recommendations
- **MCP Server**: Release when there are significant new features or important bug fixes
- **Workbench**: Release in sync with server changes that affect the bridge protocol
- **Macros**: Release independently when macro functionality changes
## Quick Reference
```bash
# Check what needs releasing
just release::status
# View changes for a component
just release::changes-since mcp-server
# Draft changelog entries from commits
just release::draft-notes mcp-server
just release::draft-notes workbench
just release::draft-notes macro-magnets
just release::draft-notes macro-export
# Bump versions (for workbench and macros)
just release::bump-workbench 1.0.0
just release::bump-macro-magnets 1.0.0
just release::bump-macro-export 1.0.0
# Commit version changes
git add -A && git commit -m "chore: bump <component> to X.Y.Z"
# Create releases (verifies versions, creates and pushes tag)
just release::tag-mcp-server 1.0.0
just release::tag-workbench 1.0.0
just release::tag-macro-magnets 1.0.0
just release::tag-macro-export 1.0.0
# List existing releases
just release::list-tags
just release::latest-versions
# Extract changelog for a version (used by CI)
just release::extract-changelog mcp-server 1.0.0
# Delete a tag if needed
just release::delete-tag <full-tag-name>
```
View File
+28
View File
@@ -0,0 +1,28 @@
# Variables for mkdocs-macros-plugin
# Use in docs with {{ variable_name }}
# Project info
project_name: FreeCAD MCP Server
package_name: freecad-robust-mcp
docker_image: spkane/freecad-robust-mcp
# URLs
repo_url: https://github.com/spkane/freecad-robust-mcp-and-more
pypi_url: https://pypi.org/project/freecad-robust-mcp/
docker_url: https://hub.docker.com/r/spkane/freecad-robust-mcp
# Ports
xmlrpc_port: 9875
socket_port: 9876
# Paths (platform-specific)
paths:
macos:
macro: ~/Library/Application Support/FreeCAD/Macro/
mod: ~/Library/Application Support/FreeCAD/Mod/
linux:
macro: ~/.local/share/FreeCAD/Macro/
mod: ~/.local/share/FreeCAD/Mod/
windows:
macro: "%APPDATA%/FreeCAD/Macro/"
mod: "%APPDATA%/FreeCAD/Mod/"
+62
View File
@@ -0,0 +1,62 @@
# Development utility commands
# Usage: just dev::clean, just dev::repl, etc.
#
# Miscellaneous development tools and utilities.
# Project root directory (justfile_directory() returns the main justfile's directory)
project_root := justfile_directory()
# =============================================================================
# Setup & Dependencies
# =============================================================================
# Install all project dependencies (Python packages + dev tools)
install-deps:
cd {{project_root}} && uv sync --all-extras
@echo ""
@echo "Dependencies installed!"
@echo ""
@echo "To run the MCP server:"
@echo " just mcp::run # stdio mode"
@echo " just mcp::run-debug # with debug logging"
@echo " just mcp::run-http # HTTP mode for remote access"
# Install pre-commit hooks (for git commit/push integration)
install-pre-commit:
cd {{project_root}} && uv run pre-commit install
cd {{project_root}} && uv run pre-commit install --hook-type commit-msg
# Update all dependencies to latest versions (uv.lock + pre-commit hooks)
update-deps:
cd {{project_root}} && uv lock --upgrade
cd {{project_root}} && uv sync --all-extras
cd {{project_root}} && uv run pre-commit autoupdate
# =============================================================================
# Development Utilities
# =============================================================================
# Clean build artifacts and caches
clean:
rm -rf {{project_root}}/.pytest_cache {{project_root}}/.mypy_cache {{project_root}}/.ruff_cache {{project_root}}/.coverage {{project_root}}/htmlcov {{project_root}}/dist {{project_root}}/build
find {{project_root}} -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
find {{project_root}} -type f -name "*.pyc" -delete 2>/dev/null || true
# Open Python REPL with project modules available
repl:
cd {{project_root}} && uv run python -c "import freecad_mcp; print('FreeCAD MCP loaded')" && uv run python
# Show project structure (tree view, requires 'tree' command)
tree:
#!/usr/bin/env bash
if ! command -v tree &> /dev/null; then
echo "The 'tree' command is not installed."
echo "Install it with: brew install tree (macOS) or apt install tree (Linux)"
exit 1
fi
cd "{{project_root}}" && tree -I '__pycache__|*.egg-info|.git|.mypy_cache|.pytest_cache|.ruff_cache|htmlcov|dist|build|.venv|site' -a
# Validate project configuration files (pyproject.toml, etc.)
validate:
cd {{project_root}} && uv pip check
@echo "Package dependencies are valid."
+7
View File
@@ -67,6 +67,13 @@ clean:
docker rmi {{image_name}} 2>/dev/null || true
docker rmi {{registry}}/{{image_name}} 2>/dev/null || true
# Remove Docker images and build cache
clean-all:
docker rmi {{image_name}} 2>/dev/null || true
docker rmi {{registry}}/{{image_name}} 2>/dev/null || true
docker builder prune -f
@echo "Docker images and build cache cleaned."
# Scan Docker image for vulnerabilities (warn on all severities)
scan:
#!/usr/bin/env bash
+14 -9
View File
@@ -1,25 +1,30 @@
# Documentation commands
# Usage: just documentation::build, just documentation::serve, etc.
# Project root directory (justfile_directory() returns the main justfile's directory)
project_root := justfile_directory()
# Build documentation
build:
uv run mkdocs build
cd {{project_root}} && uv run mkdocs build
# Build documentation with strict mode (fails on warnings, for CI)
build-strict:
uv run mkdocs build --strict
cd {{project_root}} && uv run mkdocs build --strict
# Serve documentation locally
serve:
uv run mkdocs serve
cd {{project_root}} && uv run mkdocs serve
# Build and open documentation in browser
open:
#!/usr/bin/env bash
cd "{{project_root}}"
uv run mkdocs build
@if [[ "$OSTYPE" == "darwin"* ]]; then \
open site/index.html; \
elif command -v xdg-open &> /dev/null; then \
xdg-open site/index.html; \
else \
echo "Documentation built at: site/index.html"; \
if [[ "$OSTYPE" == "darwin"* ]]; then
open "{{project_root}}/site/index.html"
elif command -v xdg-open &> /dev/null; then
xdg-open "{{project_root}}/site/index.html"
else
echo "Documentation built at: {{project_root}}/site/index.html"
fi
+8 -251
View File
@@ -252,255 +252,12 @@ run-gui-custom freecad_path:
echo "FreeCAD is starting with MCP bridge..."
# =============================================================================
# Macro Installation (MultiExport, CutObjectForMagnets)
# Deprecated Aliases (use install:: module instead)
# =============================================================================
# Install the CutObjectForMagnets macro to FreeCAD's macro directory
install-cut-macro:
#!/usr/bin/env bash
set -euo pipefail
PROJECT_DIR="{{project_root}}"
# Determine macro directory based on OS
if [[ "$OSTYPE" == "darwin"* ]]; then
MACRO_DIR="$HOME/Library/Application Support/FreeCAD/Macro"
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
MACRO_DIR="$HOME/.local/share/FreeCAD/Macro"
else
MACRO_DIR="$APPDATA/FreeCAD/Macro"
fi
mkdir -p "$MACRO_DIR"
# Copy the macro file
cp "${PROJECT_DIR}/macros/Cut_Object_for_Magnets/CutObjectForMagnets.FCMacro" "$MACRO_DIR/"
# Optionally copy the icon if it exists
if [[ -f "${PROJECT_DIR}/macros/Cut_Object_for_Magnets/CutObjectForMagnets.svg" ]]; then
cp "${PROJECT_DIR}/macros/Cut_Object_for_Magnets/CutObjectForMagnets.svg" "$MACRO_DIR/"
fi
echo "CutObjectForMagnets macro installed to: $MACRO_DIR"
echo ""
echo "To use:"
echo " 1. Start FreeCAD"
echo " 2. Select an object to cut"
echo " 3. Go to: Macro → Macros → CutObjectForMagnets → Execute"
echo ""
echo "For angled cuts:"
echo " 1. Create a datum plane at your desired angle (Part Design → Create datum plane)"
echo " 2. Select 'Model Plane' in the macro dialog"
echo " 3. Choose your datum plane from the dropdown"
# Uninstall the CutObjectForMagnets macro
uninstall-cut-macro:
#!/usr/bin/env bash
set -euo pipefail
if [[ "$OSTYPE" == "darwin"* ]]; then
MACRO_DIR="$HOME/Library/Application Support/FreeCAD/Macro"
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
MACRO_DIR="$HOME/.local/share/FreeCAD/Macro"
else
MACRO_DIR="$APPDATA/FreeCAD/Macro"
fi
rm -f "$MACRO_DIR/CutObjectForMagnets.FCMacro"
rm -f "$MACRO_DIR/CutObjectForMagnets.svg"
echo "CutObjectForMagnets macro uninstalled"
# Install the MultiExport macro to FreeCAD's macro directory
install-export-macro:
#!/usr/bin/env bash
set -euo pipefail
PROJECT_DIR="{{project_root}}"
# Determine macro directory based on OS
if [[ "$OSTYPE" == "darwin"* ]]; then
MACRO_DIR="$HOME/Library/Application Support/FreeCAD/Macro"
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
MACRO_DIR="$HOME/.local/share/FreeCAD/Macro"
else
MACRO_DIR="$APPDATA/FreeCAD/Macro"
fi
mkdir -p "$MACRO_DIR"
# Copy the macro file
cp "${PROJECT_DIR}/macros/Multi_Export/MultiExport.FCMacro" "$MACRO_DIR/"
# Optionally copy the icon if it exists
if [[ -f "${PROJECT_DIR}/macros/Multi_Export/MultiExport.svg" ]]; then
cp "${PROJECT_DIR}/macros/Multi_Export/MultiExport.svg" "$MACRO_DIR/"
fi
echo "MultiExport macro installed to: $MACRO_DIR"
echo ""
echo "To use:"
echo " 1. Start FreeCAD"
echo " 2. Select one or more objects to export"
echo " 3. Go to: Macro → Macros → MultiExport → Execute"
echo " 4. Choose export formats (STL, STEP, 3MF selected by default)"
echo " 5. Set output directory and filename"
echo " 6. Click Export"
# Uninstall the MultiExport macro
uninstall-export-macro:
#!/usr/bin/env bash
set -euo pipefail
if [[ "$OSTYPE" == "darwin"* ]]; then
MACRO_DIR="$HOME/Library/Application Support/FreeCAD/Macro"
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
MACRO_DIR="$HOME/.local/share/FreeCAD/Macro"
else
MACRO_DIR="$APPDATA/FreeCAD/Macro"
fi
rm -f "$MACRO_DIR/MultiExport.FCMacro"
rm -f "$MACRO_DIR/MultiExport.svg"
echo "MultiExport macro uninstalled"
# Install all macros to FreeCAD's macro directory
install-all-macros: install-cut-macro install-export-macro
@echo "All macros installed successfully!"
# Uninstall all macros from FreeCAD's macro directory
uninstall-all-macros: uninstall-cut-macro uninstall-export-macro
@echo "All macros uninstalled successfully!"
# =============================================================================
# Workbench Addon Installation
# =============================================================================
# Install the FreeCAD Robust MCP workbench addon to FreeCAD's Mod directory
install-workbench:
#!/usr/bin/env bash
set -euo pipefail
PROJECT_DIR="{{project_root}}"
ADDON_NAME="FreecadRobustMCP"
# Determine FreeCAD Mod directory based on OS
if [[ "$OSTYPE" == "darwin"* ]]; then
MOD_DIR="$HOME/Library/Application Support/FreeCAD/Mod"
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
MOD_DIR="$HOME/.local/share/FreeCAD/Mod"
else
MOD_DIR="$APPDATA/FreeCAD/Mod"
fi
ADDON_DEST="$MOD_DIR/$ADDON_NAME"
# Create Mod directory if it doesn't exist
mkdir -p "$MOD_DIR"
# Remove existing installation if present
if [[ -d "$ADDON_DEST" ]]; then
echo "Removing existing installation at: $ADDON_DEST"
rm -rf "$ADDON_DEST"
fi
# Copy the addon directory
cp -r "${PROJECT_DIR}/addon/$ADDON_NAME" "$ADDON_DEST"
echo ""
echo "=========================================="
echo "FreeCAD Robust MCP Workbench installed!"
echo "=========================================="
echo ""
echo "Installation path: $ADDON_DEST"
echo ""
echo "To use:"
echo " 1. Start FreeCAD"
echo " 2. Select the 'MCP Bridge' workbench from the workbench selector"
echo " 3. Click 'Start MCP Bridge' in the toolbar"
echo " 4. Connect your MCP client (Claude Code, etc.) to FreeCAD"
echo ""
# Uninstall the FreeCAD Robust MCP workbench addon
uninstall-workbench:
#!/usr/bin/env bash
set -euo pipefail
ADDON_NAME="FreecadRobustMCP"
if [[ "$OSTYPE" == "darwin"* ]]; then
MOD_DIR="$HOME/Library/Application Support/FreeCAD/Mod"
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
MOD_DIR="$HOME/.local/share/FreeCAD/Mod"
else
MOD_DIR="$APPDATA/FreeCAD/Mod"
fi
ADDON_DEST="$MOD_DIR/$ADDON_NAME"
if [[ -d "$ADDON_DEST" ]]; then
rm -rf "$ADDON_DEST"
echo "FreeCAD Robust MCP Workbench uninstalled from: $ADDON_DEST"
else
echo "Workbench not found at: $ADDON_DEST"
fi
# Check workbench installation status
mcp-status:
#!/usr/bin/env bash
set -euo pipefail
# Determine directories based on OS
if [[ "$OSTYPE" == "darwin"* ]]; then
MOD_DIR="$HOME/Library/Application Support/FreeCAD/Mod"
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
MOD_DIR="$HOME/.local/share/FreeCAD/Mod"
else
MOD_DIR="$APPDATA/FreeCAD/Mod"
fi
echo "=========================================="
echo "MCP Bridge Installation Status"
echo "=========================================="
echo ""
# Check workbench
if [[ -d "$MOD_DIR/FreecadRobustMCP" ]]; then
echo "✓ Workbench addon: INSTALLED"
echo " Path: $MOD_DIR/FreecadRobustMCP"
echo ""
echo "To use:"
echo " 1. Start FreeCAD"
echo " 2. Select 'MCP Bridge' workbench"
echo " 3. Click 'Start MCP Bridge' in toolbar"
else
echo "✗ Workbench addon: NOT INSTALLED"
echo ""
echo "To install:"
echo " just freecad::install-workbench"
fi
echo ""
# Check for legacy installations that should be cleaned up
LEGACY_COUNT=0
if [[ -d "$MOD_DIR/MCPBridge" ]]; then
echo "⚠ Legacy plugin found: $MOD_DIR/MCPBridge"
echo " Run: rm -rf \"$MOD_DIR/MCPBridge\""
((LEGACY_COUNT++)) || true
fi
if [[ "$OSTYPE" == "darwin"* ]]; then
MACRO_DIR="$HOME/Library/Application Support/FreeCAD/Macro"
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
MACRO_DIR="$HOME/.local/share/FreeCAD/Macro"
else
MACRO_DIR="$APPDATA/FreeCAD/Macro"
fi
if [[ -f "$MACRO_DIR/StartMCPBridge.FCMacro" ]]; then
echo "⚠ Legacy macro found: $MACRO_DIR/StartMCPBridge.FCMacro"
echo " Run: rm \"$MACRO_DIR/StartMCPBridge.FCMacro\""
((LEGACY_COUNT++)) || true
fi
if [[ $LEGACY_COUNT -gt 0 ]]; then
echo ""
echo "Note: Legacy installations can be removed. The workbench replaces them."
fi
echo "=========================================="
# Check if the workbench addon is installed (alias for mcp-status)
workbench-status: mcp-status
# These are kept for backwards compatibility but will be removed in a future version.
# Use the new install:: module commands instead:
# just install::macro-cut (was: just freecad::install-cut-macro)
# just install::macro-export (was: just freecad::install-export-macro)
# just install::macro-all (was: just freecad::install-all-macros)
# just install::mcp-bridge-workbench (was: just freecad::install-workbench)
# just install::status (was: just freecad::mcp-status)
+357
View File
@@ -0,0 +1,357 @@
# Installation commands for users
# Usage: just install::mcp-server, just install::mcp-bridge-workbench, etc.
#
# This module installs components for end users:
# - MCP Server (as a uv tool, available system-wide)
# - MCP Bridge Workbench (FreeCAD addon)
# - FreeCAD Macros (CutObjectForMagnets, MultiExport)
#
# For developer setup (Python dependencies in virtualenv), use: just dev::install-deps
# Project root directory (justfile_directory() returns the main justfile's directory)
project_root := justfile_directory()
# =============================================================================
# Helper: FreeCAD Directory Detection
# =============================================================================
# This function sets MOD_DIR and MACRO_DIR based on the current OS.
# Source it at the start of any recipe that needs FreeCAD paths.
#
# Usage in recipes:
# eval "$(just install::_freecad-dirs)"
# echo "Mod directory: $MOD_DIR"
# echo "Macro directory: $MACRO_DIR"
# Private recipe that outputs shell code to set FreeCAD directories
[private]
_freecad-dirs:
#!/usr/bin/env bash
cat << 'DIRS_EOF'
if [[ "$OSTYPE" == "darwin"* ]]; then
MOD_DIR="$HOME/Library/Application Support/FreeCAD/Mod"
MACRO_DIR="$HOME/Library/Application Support/FreeCAD/Macro"
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
MOD_DIR="$HOME/.local/share/FreeCAD/Mod"
MACRO_DIR="$HOME/.local/share/FreeCAD/Macro"
else
# Windows: validate APPDATA or use fallback
if [[ -n "$APPDATA" ]]; then
FREECAD_BASE="$APPDATA"
elif [[ -n "$HOME" ]]; then
# Fallback to standard Windows location under HOME
FREECAD_BASE="$HOME/AppData/Roaming"
echo "Warning: APPDATA not set, using fallback: $FREECAD_BASE" >&2
else
echo "Error: Neither APPDATA nor HOME is set. Cannot determine FreeCAD directory." >&2
exit 1
fi
MOD_DIR="$FREECAD_BASE/FreeCAD/Mod"
MACRO_DIR="$FREECAD_BASE/FreeCAD/Macro"
fi
DIRS_EOF
# =============================================================================
# MCP Server Installation
# =============================================================================
# Install the MCP server as a user tool (available system-wide via uv)
mcp-server:
#!/usr/bin/env bash
set -euo pipefail
PROJECT_DIR="{{project_root}}"
echo "Installing MCP server as a uv tool..."
echo ""
# Install from the local project directory
uv tool install --force "$PROJECT_DIR"
echo ""
echo "=========================================="
echo "MCP Server installed!"
echo "=========================================="
echo ""
echo "The 'freecad-mcp' command is now available system-wide."
echo ""
echo "To run:"
echo " freecad-mcp # stdio mode (for Claude Code)"
echo " freecad-mcp --help # show all options"
echo ""
echo "To configure Claude Code, add to your MCP settings:"
echo ' "freecad": {'
echo ' "command": "freecad-mcp"'
echo ' }'
echo ""
# Uninstall the MCP server tool
uninstall-mcp-server:
#!/usr/bin/env bash
set -euo pipefail
echo "Uninstalling MCP server..."
uv tool uninstall freecad-robust-mcp || echo "MCP server was not installed as a uv tool"
# =============================================================================
# MCP Bridge Workbench Installation
# =============================================================================
# Install the FreeCAD Robust MCP workbench addon to FreeCAD's Mod directory
mcp-bridge-workbench:
#!/usr/bin/env bash
set -euo pipefail
PROJECT_DIR="{{project_root}}"
ADDON_NAME="FreecadRobustMCP"
# Set FreeCAD directories
eval "$(just install::_freecad-dirs)"
ADDON_DEST="$MOD_DIR/$ADDON_NAME"
# Create Mod directory if it doesn't exist
mkdir -p "$MOD_DIR"
# Remove existing installation if present
if [[ -d "$ADDON_DEST" ]]; then
echo "Removing existing installation at: $ADDON_DEST"
rm -rf "$ADDON_DEST"
fi
# Verify source exists before copying
ADDON_SRC="${PROJECT_DIR}/addon/$ADDON_NAME"
if [[ ! -d "$ADDON_SRC" ]]; then
echo "Error: Addon source directory not found: $ADDON_SRC" >&2
exit 1
fi
# Copy the addon directory
cp -r "$ADDON_SRC" "$ADDON_DEST"
echo ""
echo "=========================================="
echo "FreeCAD Robust MCP Workbench installed!"
echo "=========================================="
echo ""
echo "Installation path: $ADDON_DEST"
echo ""
echo "To use:"
echo " 1. Start FreeCAD"
echo " 2. Select the 'MCP Bridge' workbench from the workbench selector"
echo " 3. Click 'Start MCP Bridge' in the toolbar"
echo " 4. Connect your MCP client (Claude Code, etc.) to FreeCAD"
echo ""
# Uninstall the FreeCAD Robust MCP workbench addon
uninstall-mcp-bridge-workbench:
#!/usr/bin/env bash
set -euo pipefail
ADDON_NAME="FreecadRobustMCP"
# Set FreeCAD directories
eval "$(just install::_freecad-dirs)"
ADDON_DEST="$MOD_DIR/$ADDON_NAME"
if [[ -d "$ADDON_DEST" ]]; then
rm -rf "$ADDON_DEST"
echo "FreeCAD Robust MCP Workbench uninstalled from: $ADDON_DEST"
else
echo "Workbench not found at: $ADDON_DEST"
fi
# =============================================================================
# Macro Installation
# =============================================================================
# Install the CutObjectForMagnets macro to FreeCAD's macro directory
macro-cut:
#!/usr/bin/env bash
set -euo pipefail
PROJECT_DIR="{{project_root}}"
# Set FreeCAD directories
eval "$(just install::_freecad-dirs)"
# Verify source exists before copying
MACRO_SRC="${PROJECT_DIR}/macros/Cut_Object_for_Magnets/CutObjectForMagnets.FCMacro"
if [[ ! -f "$MACRO_SRC" ]]; then
echo "Error: Macro source file not found: $MACRO_SRC" >&2
exit 1
fi
mkdir -p "$MACRO_DIR"
# Copy the macro file
cp "$MACRO_SRC" "$MACRO_DIR/"
# Optionally copy the icon if it exists
ICON_SRC="${PROJECT_DIR}/macros/Cut_Object_for_Magnets/CutObjectForMagnets.svg"
if [[ -f "$ICON_SRC" ]]; then
cp "$ICON_SRC" "$MACRO_DIR/"
fi
echo "CutObjectForMagnets macro installed to: $MACRO_DIR"
echo ""
echo "To use:"
echo " 1. Start FreeCAD"
echo " 2. Select an object to cut"
echo " 3. Go to: Macro → Macros → CutObjectForMagnets → Execute"
echo ""
echo "For angled cuts:"
echo " 1. Create a datum plane at your desired angle (Part Design → Create datum plane)"
echo " 2. Select 'Model Plane' in the macro dialog"
echo " 3. Choose your datum plane from the dropdown"
# Uninstall the CutObjectForMagnets macro
uninstall-macro-cut:
#!/usr/bin/env bash
set -euo pipefail
# Set FreeCAD directories
eval "$(just install::_freecad-dirs)"
rm -f "$MACRO_DIR/CutObjectForMagnets.FCMacro"
rm -f "$MACRO_DIR/CutObjectForMagnets.svg"
echo "CutObjectForMagnets macro uninstalled"
# Install the MultiExport macro to FreeCAD's macro directory
macro-export:
#!/usr/bin/env bash
set -euo pipefail
PROJECT_DIR="{{project_root}}"
# Set FreeCAD directories
eval "$(just install::_freecad-dirs)"
# Verify source exists before copying
MACRO_SRC="${PROJECT_DIR}/macros/Multi_Export/MultiExport.FCMacro"
if [[ ! -f "$MACRO_SRC" ]]; then
echo "Error: Macro source file not found: $MACRO_SRC" >&2
exit 1
fi
mkdir -p "$MACRO_DIR"
# Copy the macro file
cp "$MACRO_SRC" "$MACRO_DIR/"
# Optionally copy the icon if it exists
ICON_SRC="${PROJECT_DIR}/macros/Multi_Export/MultiExport.svg"
if [[ -f "$ICON_SRC" ]]; then
cp "$ICON_SRC" "$MACRO_DIR/"
fi
echo "MultiExport macro installed to: $MACRO_DIR"
echo ""
echo "To use:"
echo " 1. Start FreeCAD"
echo " 2. Select one or more objects to export"
echo " 3. Go to: Macro → Macros → MultiExport → Execute"
echo " 4. Choose export formats (STL, STEP, 3MF selected by default)"
echo " 5. Set output directory and filename"
echo " 6. Click Export"
# Uninstall the MultiExport macro
uninstall-macro-export:
#!/usr/bin/env bash
set -euo pipefail
# Set FreeCAD directories
eval "$(just install::_freecad-dirs)"
rm -f "$MACRO_DIR/MultiExport.FCMacro"
rm -f "$MACRO_DIR/MultiExport.svg"
echo "MultiExport macro uninstalled"
# Install all macros to FreeCAD's macro directory
macro-all: macro-cut macro-export
@echo "All macros installed successfully!"
# Uninstall all macros from FreeCAD's macro directory
uninstall-macro-all: uninstall-macro-cut uninstall-macro-export
@echo "All macros uninstalled successfully!"
# =============================================================================
# Status Check
# =============================================================================
# Check installation status of all components
status:
#!/usr/bin/env bash
set -euo pipefail
# Set FreeCAD directories
eval "$(just install::_freecad-dirs)"
echo "=========================================="
echo "Installation Status"
echo "=========================================="
echo ""
# Check MCP Server (installed as uv tool)
if command -v freecad-mcp &> /dev/null; then
echo "✓ MCP Server: INSTALLED (as uv tool)"
echo " Run: freecad-mcp"
elif [[ -f "{{project_root}}/pyproject.toml" ]] && grep -q 'name = "freecad-robust-mcp"' "{{project_root}}/pyproject.toml" 2>/dev/null; then
# Dev environment exists - check if synced by looking for .venv
if [[ -d "{{project_root}}/.venv" ]]; then
echo "✓ MCP Server: AVAILABLE (via dev environment)"
echo " Run: just mcp::run"
echo " For system-wide install: just install::mcp-server"
else
echo "○ MCP Server: DEV SOURCE AVAILABLE (needs setup)"
echo " Setup: uv sync --all-extras"
echo " Then run: just mcp::run"
fi
else
echo "✗ MCP Server: NOT INSTALLED"
echo " Install: just install::mcp-server"
fi
echo ""
# Check workbench
if [[ -d "$MOD_DIR/FreecadRobustMCP" ]]; then
echo "✓ MCP Bridge Workbench: INSTALLED"
echo " Path: $MOD_DIR/FreecadRobustMCP"
else
echo "✗ MCP Bridge Workbench: NOT INSTALLED"
echo " Install: just install::mcp-bridge-workbench"
fi
echo ""
# Check macros
echo "Macros:"
if [[ -f "$MACRO_DIR/CutObjectForMagnets.FCMacro" ]]; then
echo " ✓ CutObjectForMagnets: INSTALLED"
else
echo " ✗ CutObjectForMagnets: NOT INSTALLED"
echo " Install: just install::macro-cut"
fi
if [[ -f "$MACRO_DIR/MultiExport.FCMacro" ]]; then
echo " ✓ MultiExport: INSTALLED"
else
echo " ✗ MultiExport: NOT INSTALLED"
echo " Install: just install::macro-export"
fi
echo ""
# Check for legacy installations
LEGACY_COUNT=0
if [[ -d "$MOD_DIR/MCPBridge" ]]; then
echo "⚠ Legacy plugin found: $MOD_DIR/MCPBridge"
echo " Run: rm -rf \"$MOD_DIR/MCPBridge\""
((LEGACY_COUNT++)) || true
fi
if [[ -f "$MACRO_DIR/StartMCPBridge.FCMacro" ]]; then
echo "⚠ Legacy macro found: $MACRO_DIR/StartMCPBridge.FCMacro"
echo " Run: rm \"$MACRO_DIR/StartMCPBridge.FCMacro\""
((LEGACY_COUNT++)) || true
fi
if [[ $LEGACY_COUNT -gt 0 ]]; then
echo ""
echo "Note: Legacy installations can be removed. The workbench replaces them."
fi
echo "=========================================="
+18
View File
@@ -0,0 +1,18 @@
# MCP Server commands
# Usage: just mcp::run, just mcp::run-debug, etc.
#
# These commands run the MCP server that connects to FreeCAD.
# Note: FreeCAD must be running with the MCP bridge for the server to connect.
# Start FreeCAD with: just freecad::run-gui or just freecad::run-headless
# Run the MCP server (stdio mode - default for Claude Code integration)
run:
uv run freecad-mcp
# Run the MCP server with debug logging
run-debug:
FREECAD_MCP_LOG_LEVEL=DEBUG uv run freecad-mcp
# Run in HTTP mode for remote access (useful for testing or remote clients)
run-http port="8000":
FREECAD_MCP_TRANSPORT=http FREECAD_MCP_PORT={{port}} uv run freecad-mcp
+24 -21
View File
@@ -4,6 +4,9 @@
# Note: Tools installed via mise (gitleaks, markdownlint-cli2, etc.) use `mise exec`.
# Python tools use `uv run`. This ensures commands work even without mise shell activation.
# Project root directory (justfile_directory() returns the main justfile's directory)
project_root := justfile_directory()
# Run all pre-commit checks
check: _check-safety-auth
uv run pre-commit run --all-files
@@ -31,56 +34,56 @@ _check-safety-auth:
# Format code with ruff
format:
uv run ruff format src tests
uv run ruff check --fix src tests
uv run ruff format {{project_root}}/src {{project_root}}/tests
uv run ruff check --fix {{project_root}}/src {{project_root}}/tests
# Run linting
lint:
uv run ruff check src tests
uv run ruff check {{project_root}}/src {{project_root}}/tests
# Run type checking
typecheck:
uv run mypy src
cd {{project_root}} && uv run mypy src
# Run security scanning (code vulnerabilities)
security:
uv run bandit -c pyproject.toml -r src
uv run bandit -c {{project_root}}/pyproject.toml -r {{project_root}}/src
uv run safety scan --detailed-output
# Run spell checking
spellcheck:
uv run codespell src tests docs
uv run codespell {{project_root}}/src {{project_root}}/tests {{project_root}}/docs
# =============================================================================
# Secrets Scanning
# Secrets Scanning (quality::scan-* commands)
# =============================================================================
# Run all secrets scanners
secrets: secrets-gitleaks secrets-detect secrets-trufflehog # pragma: allowlist secret
scan: scan-gitleaks scan-detect scan-trufflehog # pragma: allowlist secret
@echo "All secrets scans complete!"
# Run gitleaks secrets scanner (installed via mise)
secrets-gitleaks:
mise exec -- gitleaks detect --config .gitleaks.toml --verbose
scan-gitleaks:
mise exec -- gitleaks detect --source {{project_root}} --config {{project_root}}/.gitleaks.toml --verbose
# Run gitleaks on git history
secrets-gitleaks-history:
mise exec -- gitleaks detect --config .gitleaks.toml --verbose --log-opts="--all"
scan-gitleaks-history:
mise exec -- gitleaks detect --source {{project_root}} --config {{project_root}}/.gitleaks.toml --verbose --log-opts="--all"
# Run detect-secrets scanner (installed via uv)
secrets-detect:
uv run detect-secrets scan --baseline .secrets.baseline
scan-detect:
uv run detect-secrets scan --baseline {{project_root}}/.secrets.baseline
# Audit detect-secrets baseline (interactive)
secrets-audit:
uv run detect-secrets audit .secrets.baseline
scan-audit:
uv run detect-secrets audit {{project_root}}/.secrets.baseline
# Update detect-secrets baseline with new findings
secrets-baseline-update:
uv run detect-secrets scan --baseline .secrets.baseline --update
scan-baseline-update:
uv run detect-secrets scan --baseline {{project_root}}/.secrets.baseline --update
# Run trufflehog for verified secrets (via pre-commit - not installed standalone)
secrets-trufflehog:
scan-trufflehog:
uv run pre-commit run trufflehog --all-files
# =============================================================================
@@ -89,8 +92,8 @@ secrets-trufflehog:
# Lint all markdown files (markdownlint-cli2 installed via mise)
markdown-lint:
mise exec -- markdownlint-cli2 "**/*.md"
cd {{project_root}} && mise exec -- markdownlint-cli2 "**/*.md" "#.venv" "#.pytest_cache" "#node_modules" "#site" "#htmlcov"
# Lint and fix markdown files
markdown-fix:
mise exec -- markdownlint-cli2 --fix "**/*.md"
cd {{project_root}} && mise exec -- markdownlint-cli2 --fix "**/*.md" "#.venv" "#.pytest_cache" "#node_modules" "#site" "#htmlcov"
+732
View File
@@ -0,0 +1,732 @@
# Release commands for component-specific versioning
# Usage: just release::bump-workbench 1.0.0, just release::tag-workbench 1.0.0, etc.
#
# Release Process (two steps):
# 1. Bump version: just release::bump-<component> <version>
# - Updates all version strings in source files
# - Commit the changes: git add -A && git commit -m "chore: bump <component> to <version>"
# 2. Create tag: just release::tag-<component> <version>
# - Verifies versions match the tag
# - Creates and pushes the git tag
# - Tag triggers GitHub Actions workflow
#
# This project uses component-specific git tags for releases:
# - robust-mcp-server-vX.Y.Z (triggers PyPI, Docker, GitHub release)
# - robust-mcp-workbench-vX.Y.Z (triggers workbench archive release)
# - macro-cut-object-for-magnets-vX.Y.Z
# - macro-multi-export-vX.Y.Z
#
# Version Format (SemVer 2.0):
# - X.Y.Z - Stable release
# - X.Y.Z-alpha - Alpha (TestPyPI only for MCP server)
# - X.Y.Z-alpha.N - Alpha with number
# - X.Y.Z-beta - Beta
# - X.Y.Z-beta.N - Beta with number
# - X.Y.Z-rc.N - Release candidate
# Project root directory
project_root := justfile_directory()
# =============================================================================
# Version Bump Commands
# =============================================================================
# Bump the MCP Bridge workbench version in all source files
bump-workbench version:
#!/usr/bin/env bash
set -euo pipefail
VERSION="{{version}}"
TODAY=$(date +%Y-%m-%d)
# Validate version format (SemVer 2.0)
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
echo "ERROR: Version '$VERSION' is not valid semver (X.Y.Z or X.Y.Z-prerelease)"
echo "Examples: 1.0.0, 1.0.0-alpha, 1.0.0-beta.1, 1.0.0-rc.1"
exit 1
fi
echo "Bumping MCP Bridge Workbench to version: $VERSION (date: $TODAY)"
echo ""
# Update __version__ in the bridge module's __init__.py
INIT_FILE="{{project_root}}/addon/FreecadRobustMCP/freecad_mcp_bridge/__init__.py"
if [ -f "$INIT_FILE" ]; then
sed 's/^__version__ = "[^"]*"/__version__ = "'"$VERSION"'"/' "$INIT_FILE" > "$INIT_FILE.tmp" && mv "$INIT_FILE.tmp" "$INIT_FILE"
echo "Updated $INIT_FILE:"
grep "__version__" "$INIT_FILE"
else
echo "ERROR: File not found: $INIT_FILE"
exit 1
fi
# Update the workbench version in package.xml
PACKAGE_XML="{{project_root}}/package.xml"
if [ -f "$PACKAGE_XML" ]; then
# Use awk for precise XML editing within the workbench section
awk -v version="$VERSION" -v date="$TODAY" '
/<workbench>/ { in_workbench=1 }
/<\/workbench>/ { in_workbench=0 }
in_workbench && /<version>/ {
gsub(/<version>[^<]*<\/version>/, "<version>" version "</version>")
}
in_workbench && /<date>/ {
gsub(/<date>[^<]*<\/date>/, "<date>" date "</date>")
}
{ print }
' "$PACKAGE_XML" > "$PACKAGE_XML.tmp" && mv "$PACKAGE_XML.tmp" "$PACKAGE_XML"
echo ""
echo "Updated $PACKAGE_XML (workbench section):"
grep -A3 '<workbench>' "$PACKAGE_XML" | head -5
else
echo "ERROR: File not found: $PACKAGE_XML"
exit 1
fi
echo ""
echo "Version bump complete!"
echo ""
echo "Next steps:"
echo " 1. Review changes: git diff"
echo " 2. Commit: git add -A && git commit -m 'chore: bump workbench to $VERSION'"
echo " 3. Tag and release: just release::tag-workbench $VERSION"
# Private helper recipe for bumping macro versions
# Parameters: macro_dir, macro_name, macro_file_basename, readme_basename, tag_command, version
[private]
_bump-macro macro_dir macro_name macro_file_basename readme_basename tag_command version:
#!/usr/bin/env bash
set -euo pipefail
VERSION="{{version}}"
TODAY=$(date +%Y-%m-%d)
MACRO_DIR="{{project_root}}/macros/{{macro_dir}}"
MACRO_NAME="{{macro_name}}"
# Validate version format (SemVer 2.0)
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
echo "ERROR: Version '$VERSION' is not valid semver (X.Y.Z or X.Y.Z-prerelease)"
echo "Examples: 1.0.0, 1.0.0-alpha, 1.0.0-beta.1, 1.0.0-rc.1"
exit 1
fi
echo "Bumping $MACRO_NAME Macro to version: $VERSION (date: $TODAY)"
echo ""
# Update __Version__ and __Date__ in .FCMacro file
MACRO_FILE="$MACRO_DIR/{{macro_file_basename}}"
if [ -f "$MACRO_FILE" ]; then
sed "s/^__Version__ = [\"'].*[\"']/__Version__ = \"${VERSION}\"/" "$MACRO_FILE" > "$MACRO_FILE.tmp" && mv "$MACRO_FILE.tmp" "$MACRO_FILE"
sed "s/^__Date__ = [\"'].*[\"']/__Date__ = \"${TODAY}\"/" "$MACRO_FILE" > "$MACRO_FILE.tmp" && mv "$MACRO_FILE.tmp" "$MACRO_FILE"
echo "Updated $MACRO_FILE:"
grep -E "^__(Version|Date)__" "$MACRO_FILE"
else
echo "ERROR: File not found: $MACRO_FILE"
exit 1
fi
# Update README version line
README_FILE="$MACRO_DIR/{{readme_basename}}"
if [ -f "$README_FILE" ]; then
sed "s/^\*\*Version:\*\* .*/\*\*Version:\*\* ${VERSION}/" "$README_FILE" > "$README_FILE.tmp" && mv "$README_FILE.tmp" "$README_FILE"
echo ""
echo "Updated $README_FILE:"
grep "Version:" "$README_FILE" | head -1
else
echo "WARNING: File not found: $README_FILE"
fi
# Update wiki-source.txt version and date
WIKI_FILE="$MACRO_DIR/wiki-source.txt"
if [ -f "$WIKI_FILE" ]; then
sed "s/|Version=.*/|Version=${VERSION}/" "$WIKI_FILE" > "$WIKI_FILE.tmp" && mv "$WIKI_FILE.tmp" "$WIKI_FILE"
sed "s/|Date=.*/|Date=${TODAY}/" "$WIKI_FILE" > "$WIKI_FILE.tmp" && mv "$WIKI_FILE.tmp" "$WIKI_FILE"
echo ""
echo "Updated $WIKI_FILE:"
grep -E "^\|Version=|\|Date=" "$WIKI_FILE"
else
echo "WARNING: File not found: $WIKI_FILE"
fi
# Update package.xml macro version
PACKAGE_XML="{{project_root}}/package.xml"
if [ -f "$PACKAGE_XML" ]; then
awk -v name="$MACRO_NAME" -v version="$VERSION" -v date="$TODAY" '
/<macro>/ { in_macro=1 }
/<\/macro>/ { in_macro=0; found_name=0 }
in_macro && /<name>.*<\/name>/ {
if (index($0, name) > 0) found_name=1
}
in_macro && found_name && /<version>/ {
gsub(/<version>[^<]*<\/version>/, "<version>" version "</version>")
}
in_macro && found_name && /<date>/ {
gsub(/<date>[^<]*<\/date>/, "<date>" date "</date>")
}
{ print }
' "$PACKAGE_XML" > "$PACKAGE_XML.tmp" && mv "$PACKAGE_XML.tmp" "$PACKAGE_XML"
echo ""
echo "Updated $PACKAGE_XML ($MACRO_NAME section):"
grep -A4 ">$MACRO_NAME<" "$PACKAGE_XML"
else
echo "ERROR: File not found: $PACKAGE_XML"
exit 1
fi
echo ""
echo "Version bump complete!"
echo ""
echo "Next steps:"
echo " 1. Review changes: git diff"
echo " 2. Commit: git add -A && git commit -m 'chore: bump $MACRO_NAME macro to $VERSION'"
echo " 3. Tag and release: just release::{{tag_command}} $VERSION"
# Bump the Cut Object for Magnets macro version in all source files
bump-macro-magnets version: (_bump-macro "Cut_Object_for_Magnets" "Cut Object for Magnets" "CutObjectForMagnets.FCMacro" "README-CutObjectForMagnets.md" "tag-macro-magnets" version)
# Bump the Multi Export macro version in all source files
bump-macro-export version: (_bump-macro "Multi_Export" "Multi Export" "MultiExport.FCMacro" "README-MultiExport.md" "tag-macro-export" version)
# =============================================================================
# Tag Creation Commands
# =============================================================================
# Create and push a release tag for the MCP server (triggers PyPI + Docker release)
# Note: MCP server uses setuptools-scm, so version is derived from git tag at build time
tag-mcp-server version:
#!/usr/bin/env bash
set -euo pipefail
TAG="robust-mcp-server-v{{version}}"
# Validate version format
if [[ ! "{{version}}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
echo "ERROR: Version '{{version}}' is not valid semver (X.Y.Z or X.Y.Z-prerelease)"
exit 1
fi
# Check for uncommitted changes
if ! git diff --quiet HEAD; then
echo "ERROR: You have uncommitted changes. Please commit or stash them first."
exit 1
fi
echo "Creating tag: $TAG"
echo ""
echo "This will trigger:"
echo " - PyPI release (beta/rc/stable) or TestPyPI (alpha)"
echo " - Docker Hub release"
echo " - GitHub release with wheel and tar.gz"
echo ""
read -p "Continue? [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 1
fi
git tag -a "$TAG" -m "Release MCP Server v{{version}}"
git push origin "$TAG"
echo ""
echo "Tag $TAG created and pushed!"
echo "Watch the release at: https://github.com/spkane/freecad-robust-mcp-and-more/actions"
# Create and push a release tag for the MCP Bridge workbench
tag-workbench version:
#!/usr/bin/env bash
set -euo pipefail
TAG="robust-mcp-workbench-v{{version}}"
VERSION="{{version}}"
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
echo "ERROR: Version '$VERSION' is not valid semver"
exit 1
fi
# Check for uncommitted changes
if ! git diff --quiet HEAD; then
echo "ERROR: You have uncommitted changes. Please commit or stash them first."
exit 1
fi
# Verify version in source files matches tag version
echo "Verifying version in source files..."
# Check __init__.py
INIT_FILE="{{project_root}}/addon/FreecadRobustMCP/freecad_mcp_bridge/__init__.py"
INIT_VERSION=$(grep -o '__version__ = "[^"]*"' "$INIT_FILE" | cut -d'"' -f2)
if [ "$INIT_VERSION" != "$VERSION" ]; then
echo "ERROR: Version mismatch in $INIT_FILE"
echo " Expected: $VERSION"
echo " Found: $INIT_VERSION"
echo ""
echo "Run 'just release::bump-workbench $VERSION' first, then commit the changes."
exit 1
fi
# Check package.xml
PACKAGE_XML="{{project_root}}/package.xml"
PKG_VERSION=$(awk '/<workbench>/,/<\/workbench>/' "$PACKAGE_XML" | grep -o '<version>[^<]*</version>' | head -1 | sed 's/<[^>]*>//g')
if [ "$PKG_VERSION" != "$VERSION" ]; then
echo "ERROR: Version mismatch in $PACKAGE_XML (workbench section)"
echo " Expected: $VERSION"
echo " Found: $PKG_VERSION"
echo ""
echo "Run 'just release::bump-workbench $VERSION' first, then commit the changes."
exit 1
fi
echo "Version verification passed!"
echo ""
echo "Creating tag: $TAG"
echo ""
echo "This will trigger:"
echo " - GitHub release with workbench archive"
echo ""
read -p "Continue? [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 1
fi
git tag -a "$TAG" -m "Release MCP Bridge Workbench v{{version}}"
git push origin "$TAG"
echo ""
echo "Tag $TAG created and pushed!"
echo "Watch the release at: https://github.com/spkane/freecad-robust-mcp-and-more/actions"
# Private helper recipe for tagging macro releases
# Parameters: macro_dir, macro_name, macro_file_basename, tag_prefix, bump_command, tag_message, version
[private]
_tag-macro macro_dir macro_name macro_file_basename tag_prefix bump_command tag_message version:
#!/usr/bin/env bash
set -euo pipefail
VERSION="{{version}}"
TAG="{{tag_prefix}}v{{version}}"
MACRO_DIR="{{project_root}}/macros/{{macro_dir}}"
MACRO_NAME="{{macro_name}}"
# Validate version format (SemVer 2.0)
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
echo "ERROR: Version '$VERSION' is not valid semver"
exit 1
fi
# Check for uncommitted changes
if ! git diff --quiet HEAD; then
echo "ERROR: You have uncommitted changes. Please commit or stash them first."
exit 1
fi
# Verify version in source files matches tag version
echo "Verifying version in source files..."
# Check .FCMacro file
MACRO_FILE="$MACRO_DIR/{{macro_file_basename}}"
MACRO_VERSION=$(grep -o '__Version__ = "[^"]*"' "$MACRO_FILE" | cut -d'"' -f2)
if [ "$MACRO_VERSION" != "$VERSION" ]; then
echo "ERROR: Version mismatch in $MACRO_FILE"
echo " Expected: $VERSION"
echo " Found: $MACRO_VERSION"
echo ""
echo "Run 'just release::{{bump_command}} $VERSION' first, then commit the changes."
exit 1
fi
# Check package.xml
PACKAGE_XML="{{project_root}}/package.xml"
PKG_VERSION=$(awk -v name="$MACRO_NAME" '
/<macro>/ { in_macro=1 }
/<\/macro>/ { in_macro=0; found_name=0 }
in_macro && index($0, name) > 0 { found_name=1 }
in_macro && found_name && /<version>/ {
gsub(/.*<version>/, ""); gsub(/<\/version>.*/, ""); print; exit
}
' "$PACKAGE_XML")
if [ "$PKG_VERSION" != "$VERSION" ]; then
echo "ERROR: Version mismatch in $PACKAGE_XML ($MACRO_NAME section)"
echo " Expected: $VERSION"
echo " Found: $PKG_VERSION"
echo ""
echo "Run 'just release::{{bump_command}} $VERSION' first, then commit the changes."
exit 1
fi
echo "Version verification passed!"
echo ""
echo "Creating tag: $TAG"
echo ""
echo "This will trigger:"
echo " - GitHub release with macro archive"
echo ""
read -p "Continue? [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 1
fi
git tag -a "$TAG" -m "{{tag_message}} v{{version}}"
git push origin "$TAG"
echo ""
echo "Tag $TAG created and pushed!"
echo "Watch the release at: https://github.com/spkane/freecad-robust-mcp-and-more/actions"
# Create and push a release tag for the Cut Object for Magnets macro
tag-macro-magnets version: (_tag-macro "Cut_Object_for_Magnets" "Cut Object for Magnets" "CutObjectForMagnets.FCMacro" "macro-cut-object-for-magnets-" "bump-macro-magnets" "Release Cut Object for Magnets Macro" version)
# Create and push a release tag for the Multi Export macro
tag-macro-export version: (_tag-macro "Multi_Export" "Multi Export" "MultiExport.FCMacro" "macro-multi-export-" "bump-macro-export" "Release Multi Export Macro" version)
# =============================================================================
# Tag Information Commands
# =============================================================================
# List all release tags grouped by component
list-tags:
#!/usr/bin/env bash
echo "=== MCP Server Releases ==="
git tag -l 'robust-mcp-server-v*' --sort=-v:refname | head -10
echo ""
echo "=== MCP Workbench Releases ==="
git tag -l 'robust-mcp-workbench-v*' --sort=-v:refname | head -10
echo ""
echo "=== Cut Object for Magnets Macro Releases ==="
git tag -l 'macro-cut-object-for-magnets-v*' --sort=-v:refname | head -10
echo ""
echo "=== Multi Export Macro Releases ==="
git tag -l 'macro-multi-export-v*' --sort=-v:refname | head -10
# Show the latest version of each component
latest-versions:
#!/usr/bin/env bash
echo "Latest versions:"
echo ""
SERVER_TAG=$(git tag -l 'robust-mcp-server-v*' --sort=-v:refname | head -n1)
WORKBENCH_TAG=$(git tag -l 'robust-mcp-workbench-v*' --sort=-v:refname | head -n1)
MAGNETS_TAG=$(git tag -l 'macro-cut-object-for-magnets-v*' --sort=-v:refname | head -n1)
EXPORT_TAG=$(git tag -l 'macro-multi-export-v*' --sort=-v:refname | head -n1)
echo " MCP Server: ${SERVER_TAG:-none}"
echo " MCP Workbench: ${WORKBENCH_TAG:-none}"
echo " Macro Magnets: ${MAGNETS_TAG:-none}"
echo " Macro Export: ${EXPORT_TAG:-none}"
# Show commits since the last release of a component
changes-since component:
#!/usr/bin/env bash
set -euo pipefail
case "{{component}}" in
mcp-server|server)
PREFIX="robust-mcp-server-v"
PATHS="src/freecad_mcp pyproject.toml Dockerfile"
;;
workbench)
PREFIX="robust-mcp-workbench-v"
PATHS="addon/FreecadRobustMCP"
;;
macro-magnets|magnets)
PREFIX="macro-cut-object-for-magnets-v"
PATHS="macros/Cut_Object_for_Magnets"
;;
macro-export|export)
PREFIX="macro-multi-export-v"
PATHS="macros/Multi_Export"
;;
*)
echo "Unknown component: {{component}}"
echo "Valid: mcp-server, workbench, macro-magnets, macro-export"
exit 1
;;
esac
LATEST_TAG=$(git tag -l "${PREFIX}*" --sort=-v:refname | head -1)
if [ -z "$LATEST_TAG" ]; then
echo "No previous releases found for {{component}}"
echo "Showing all commits for relevant paths:"
git log --oneline -- $PATHS | head -20
else
echo "Changes since $LATEST_TAG:"
echo ""
git log --oneline "$LATEST_TAG"..HEAD -- $PATHS
fi
# =============================================================================
# Tag Management
# =============================================================================
# Delete a release tag (local and remote)
delete-tag tag:
#!/usr/bin/env bash
set -euo pipefail
echo "This will delete the tag '{{tag}}' both locally and from the remote."
echo ""
read -p "Are you sure? [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 1
fi
# Delete local tag
if git tag -l '{{tag}}' | grep -F -q '{{tag}}'; then
git tag -d '{{tag}}'
echo "Deleted local tag: {{tag}}"
else
echo "Local tag '{{tag}}' not found (may already be deleted)"
fi
# Delete remote tag
if git ls-remote --tags origin | grep -F -q 'refs/tags/{{tag}}'; then
git push origin --delete '{{tag}}'
echo "Deleted remote tag: {{tag}}"
else
echo "Remote tag '{{tag}}' not found (may already be deleted)"
fi
echo ""
echo "Tag '{{tag}}' deleted."
# =============================================================================
# Release Status
# =============================================================================
# Show unreleased changes across all components
status:
#!/usr/bin/env bash
set -euo pipefail
echo "=========================================="
echo "Release Status - Unreleased Changes"
echo "=========================================="
echo ""
# Helper function to count changes
count_changes() {
local prefix="$1"
local paths="$2"
local latest_tag=$(git tag -l "${prefix}*" --sort=-v:refname | head -1)
if [ -z "$latest_tag" ]; then
# No releases yet, count all commits
git log --oneline -- $paths 2>/dev/null | wc -l | tr -d ' '
else
git log --oneline "$latest_tag"..HEAD -- $paths 2>/dev/null | wc -l | tr -d ' '
fi
}
# MCP Server
SERVER_CHANGES=$(count_changes "robust-mcp-server-v" "src/freecad_mcp pyproject.toml Dockerfile")
SERVER_TAG=$(git tag -l 'robust-mcp-server-v*' --sort=-v:refname | head -1)
if [ "$SERVER_CHANGES" -gt 0 ]; then
echo "MCP Server: $SERVER_CHANGES unreleased commit(s)"
echo " Latest: ${SERVER_TAG:-none}"
echo " View: just release::changes-since mcp-server"
else
echo "MCP Server: up to date (${SERVER_TAG:-no releases})"
fi
echo ""
# Workbench
WORKBENCH_CHANGES=$(count_changes "robust-mcp-workbench-v" "addon/FreecadRobustMCP")
WORKBENCH_TAG=$(git tag -l 'robust-mcp-workbench-v*' --sort=-v:refname | head -1)
if [ "$WORKBENCH_CHANGES" -gt 0 ]; then
echo "MCP Workbench: $WORKBENCH_CHANGES unreleased commit(s)"
echo " Latest: ${WORKBENCH_TAG:-none}"
echo " View: just release::changes-since workbench"
else
echo "MCP Workbench: up to date (${WORKBENCH_TAG:-no releases})"
fi
echo ""
# Macro Magnets
MAGNETS_CHANGES=$(count_changes "macro-cut-object-for-magnets-v" "macros/Cut_Object_for_Magnets")
MAGNETS_TAG=$(git tag -l 'macro-cut-object-for-magnets-v*' --sort=-v:refname | head -1)
if [ "$MAGNETS_CHANGES" -gt 0 ]; then
echo "Macro (Cut Object for Magnets): $MAGNETS_CHANGES unreleased commit(s)"
echo " Latest: ${MAGNETS_TAG:-none}"
echo " View: just release::changes-since macro-magnets"
else
echo "Macro (Cut Object for Magnets): up to date (${MAGNETS_TAG:-no releases})"
fi
echo ""
# Macro Export
EXPORT_CHANGES=$(count_changes "macro-multi-export-v" "macros/Multi_Export")
EXPORT_TAG=$(git tag -l 'macro-multi-export-v*' --sort=-v:refname | head -1)
if [ "$EXPORT_CHANGES" -gt 0 ]; then
echo "Macro (Multi Export): $EXPORT_CHANGES unreleased commit(s)"
echo " Latest: ${EXPORT_TAG:-none}"
echo " View: just release::changes-since macro-export"
else
echo "Macro (Multi Export): up to date (${EXPORT_TAG:-no releases})"
fi
echo ""
echo "=========================================="
# =============================================================================
# Changelog / Release Notes Helpers
# =============================================================================
# Draft release notes for a component by extracting conventional commits since last release
draft-notes component:
#!/usr/bin/env bash
set -euo pipefail
case "{{component}}" in
mcp-server|server)
PREFIX="robust-mcp-server-v"
PATHS="src/freecad_mcp pyproject.toml Dockerfile"
COMPONENT_NAME="MCP Server"
;;
workbench)
PREFIX="robust-mcp-workbench-v"
PATHS="addon/FreecadRobustMCP"
COMPONENT_NAME="MCP Bridge Workbench"
;;
macro-magnets|magnets)
PREFIX="macro-cut-object-for-magnets-v"
PATHS="macros/Cut_Object_for_Magnets"
COMPONENT_NAME="Cut Object for Magnets Macro"
;;
macro-export|export)
PREFIX="macro-multi-export-v"
PATHS="macros/Multi_Export"
COMPONENT_NAME="Multi Export Macro"
;;
*)
echo "Unknown component: {{component}}"
echo "Valid: mcp-server, workbench, macro-magnets, macro-export"
exit 1
;;
esac
LATEST_TAG=$(git tag -l "${PREFIX}*" --sort=-v:refname | head -1)
echo "# Draft Release Notes for $COMPONENT_NAME"
echo ""
if [ -z "$LATEST_TAG" ]; then
echo "No previous releases found. Showing all commits for component paths."
echo ""
REV_RANGE="HEAD"
else
echo "Changes since: $LATEST_TAG"
echo ""
REV_RANGE="${LATEST_TAG}..HEAD"
fi
# Get commits and categorize by conventional commit type
echo "## Added"
echo ""
git log --oneline "$REV_RANGE" -- $PATHS 2>/dev/null | grep -iE "^[a-f0-9]+ feat(\(|:)" | sed -E 's/^[a-f0-9]+ /- /' || true
echo ""
echo "## Changed"
echo ""
git log --oneline "$REV_RANGE" -- $PATHS 2>/dev/null | grep -iE "^[a-f0-9]+ (refactor|perf|style)(\(|:)" | sed -E 's/^[a-f0-9]+ /- /' || true
echo ""
echo "## Fixed"
echo ""
git log --oneline "$REV_RANGE" -- $PATHS 2>/dev/null | grep -iE "^[a-f0-9]+ fix(\(|:)" | sed -E 's/^[a-f0-9]+ /- /' || true
echo ""
echo "## Documentation"
echo ""
git log --oneline "$REV_RANGE" -- $PATHS 2>/dev/null | grep -iE "^[a-f0-9]+ docs(\(|:)" | sed -E 's/^[a-f0-9]+ /- /' || true
echo ""
echo "## Other Changes"
echo ""
git log --oneline "$REV_RANGE" -- $PATHS 2>/dev/null | grep -ivE "^[a-f0-9]+ (feat|fix|refactor|perf|style|docs|test|ci|build|chore)(\(|:)" | sed -E 's/^[a-f0-9]+ /- /' || true
echo ""
echo "---"
echo ""
echo "## All Commits (chronological)"
echo ""
git log --oneline "$REV_RANGE" -- $PATHS 2>/dev/null | sed -E 's/^[a-f0-9]+ /- /' || echo "(no commits)"
# Extract changelog section for a specific component version (for GitHub Release body)
extract-changelog component version:
#!/usr/bin/env bash
set -euo pipefail
# Match header exactly as it appears in CHANGELOG.md
case "{{component}}" in
mcp-server|server)
HEADER="### MCP Server v{{version}}"
;;
workbench)
HEADER="### MCP Bridge Workbench v{{version}}"
;;
macro-magnets|magnets)
HEADER="### Cut Object for Magnets Macro v{{version}}"
;;
macro-export|export)
HEADER="### Multi Export Macro v{{version}}"
;;
*)
echo "Unknown component: {{component}}"
exit 1
;;
esac
CHANGELOG="{{project_root}}/CHANGELOG.md"
# Extract section between this version header and the next component header or separator
# Only exit on: "---" separator OR "### " followed by component name (capital letter)
# This allows #### Added, #### Changed, etc. to be included
awk -v header="$HEADER" '
BEGIN { found=0 }
$0 == header || $0 == header " " { found=1; next }
found && /^---$/ { exit }
found && /^### [A-Z]/ { exit }
found { print }
' "$CHANGELOG"
# =============================================================================
# Dry Run Commands (preview without pushing)
# =============================================================================
# Preview what a release tag would look like (no actual tag created)
dry-run-tag component version:
#!/usr/bin/env bash
# Validate version format (SemVer 2.0)
if [[ ! "{{version}}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
echo "Error: Invalid version format '{{version}}'"
echo "Expected: X.Y.Z or X.Y.Z-prerelease (e.g., 1.0.0, 1.0.0-alpha, 1.0.0-beta.1)"
exit 1
fi
case "{{component}}" in
mcp-server|server)
TAG="robust-mcp-server-v{{version}}"
echo "Would create tag: $TAG"
echo "Triggers: PyPI, Docker Hub, GitHub Release"
;;
workbench)
TAG="robust-mcp-workbench-v{{version}}"
echo "Would create tag: $TAG"
echo "Triggers: GitHub Release with workbench archive"
;;
macro-magnets|magnets)
TAG="macro-cut-object-for-magnets-v{{version}}"
echo "Would create tag: $TAG"
echo "Triggers: GitHub Release with macro archive"
;;
macro-export|export)
TAG="macro-multi-export-v{{version}}"
echo "Would create tag: $TAG"
echo "Triggers: GitHub Release with macro archive"
;;
*)
echo "Unknown component: {{component}}"
echo "Valid: mcp-server, workbench, macro-magnets, macro-export"
exit 1
;;
esac
+15 -8
View File
@@ -1,32 +1,39 @@
# Testing commands
# Usage: just testing::unit, just testing::integration, etc.
# Project root directory (justfile_directory() returns the main justfile's directory)
project_root := justfile_directory()
# Run unit tests only (excludes integration tests)
unit:
uv run pytest --ignore=tests/integration
uv run pytest {{project_root}}/tests/unit
# Run tests with coverage (excludes integration tests)
cov:
uv run pytest --ignore=tests/integration --cov=src/freecad_mcp --cov-report=term-missing --cov-report=html
cd {{project_root}} && uv run pytest tests/unit --cov=freecad_mcp --cov-report=term-missing --cov-report=html:{{project_root}}/htmlcov
# Run tests without slow markers (excludes integration tests)
fast:
uv run pytest --ignore=tests/integration -m "not slow"
uv run pytest {{project_root}}/tests/unit -m "not slow"
# Run only integration tests (requires running FreeCAD MCP bridge)
integration:
uv run pytest tests/integration -v
uv run pytest {{project_root}}/tests/integration -v
# Run tests with verbose output (excludes integration tests)
verbose:
uv run pytest --ignore=tests/integration -v --tb=long
uv run pytest {{project_root}}/tests/unit -v --tb=long
# Run all tests including integration (requires running FreeCAD MCP bridge)
all:
uv run pytest
uv run pytest {{project_root}}/tests
# Run tests in watch mode (re-runs on file changes)
watch:
uv run pytest-watch {{project_root}}/tests/unit
# Run integration tests with automatic FreeCAD headless startup
integration-auto:
integration-freecad:
#!/usr/bin/env bash
set -euo pipefail
echo "Starting FreeCAD headless server for integration tests..."
@@ -59,7 +66,7 @@ integration-auto:
# Run integration tests
TEST_EXIT_CODE=0
uv run pytest tests/integration -v || TEST_EXIT_CODE=$?
uv run pytest "{{project_root}}/tests/integration" -v || TEST_EXIT_CODE=$?
# Stop FreeCAD
echo ""
+71 -148
View File
@@ -1,25 +1,34 @@
# FreeCAD MCP Server - Development Workflow Commands
# https://just.systems/
#
# Commands are organized into modules:
# just docker::build - Docker commands
# just quality::check - Code quality commands
# just testing::unit - Testing commands
# just freecad::run-gui - FreeCAD commands
# just documentation::build - Documentation commands
# just coderabbit::review - AI code review commands
# Commands are organized into modules. Run `just` to see top-level commands,
# or `just list-<module>` to see commands in a specific module:
# just list-mcp - MCP server commands
# just list-freecad - FreeCAD plugin/macro commands
# just list-install - Installation commands
# just list-quality - Code quality commands
# just list-testing - Test commands
# just list-docker - Docker build/run commands
# just list-documentation - Documentation commands
# just list-dev - Development utilities
# just list-release - Release and tagging commands
# just list-coderabbit - AI code review commands
#
# Or use the shortcut commands defined below.
# Or use `just --list --list-submodules` to see everything at once.
# Import modules
mod docker 'just/docker.just'
mod quality 'just/quality.just'
mod testing 'just/testing.just'
mod freecad 'just/freecad.just'
mod documentation 'just/documentation.just'
mod coderabbit 'just/coderabbit.just'
mod dev 'just/dev.just'
mod docker 'just/docker.just'
mod documentation 'just/documentation.just'
mod freecad 'just/freecad.just'
mod install 'just/install.just'
mod mcp 'just/mcp.just'
mod quality 'just/quality.just'
mod release 'just/release.just'
mod testing 'just/testing.just'
# Default recipe - show available commands
# Default recipe - show top-level commands and available modules
default:
@just --list
@@ -27,156 +36,70 @@ default:
# Setup & Installation
# =============================================================================
# Install all dependencies
install:
uv sync --all-extras
# Install pre-commit hooks
setup-hooks:
uv run pre-commit install
uv run pre-commit install --hook-type commit-msg
# Full development environment setup
setup: install setup-hooks
# Full development environment setup (installs deps + pre-commit hooks)
setup: (dev::install-deps) (dev::install-pre-commit)
@echo "Development environment ready!"
# Update all dependencies to latest versions
update-deps:
uv lock --upgrade
uv sync --all-extras
uv run pre-commit autoupdate
# =============================================================================
# Shortcut Commands (aliases to module commands)
# =============================================================================
# Run all pre-commit checks (shortcut for quality::check)
check: (quality::check)
# Format code with ruff (shortcut for quality::format)
format: (quality::format)
# Run linting (shortcut for quality::lint)
lint: (quality::lint)
# Run type checking (shortcut for quality::typecheck)
typecheck: (quality::typecheck)
# Run security scanning (shortcut for quality::security)
security: (quality::security)
# Run spell checking (shortcut for quality::spellcheck)
spellcheck: (quality::spellcheck)
# Run all secrets scanners (shortcut for quality::secrets)
secrets: (quality::secrets)
# Lint markdown files (shortcut for quality::markdown-lint)
markdown-lint: (quality::markdown-lint)
# Fix markdown files (shortcut for quality::markdown-fix)
markdown-fix: (quality::markdown-fix)
# Run unit tests (shortcut for testing::unit)
test: (testing::unit)
# Run tests with coverage (shortcut for testing::cov)
test-cov: (testing::cov)
# Run fast tests (shortcut for testing::fast)
test-fast: (testing::fast)
# Run integration tests (shortcut for testing::integration)
test-integration: (testing::integration)
# Run tests with verbose output (shortcut for testing::verbose)
test-verbose: (testing::verbose)
# Run all tests (shortcut for testing::all)
test-all: (testing::all)
# Build documentation (shortcut for documentation::build)
docs: (documentation::build)
# Build documentation with strict mode (shortcut for documentation::build-strict)
docs-strict: (documentation::build-strict)
# Serve documentation (shortcut for documentation::serve)
docs-serve: (documentation::serve)
# AI code review of staged changes (shortcut for coderabbit::review)
review: (coderabbit::review)
# =============================================================================
# Running the MCP Server
# =============================================================================
# Run the MCP server (stdio mode)
run:
uv run freecad-mcp
# Run the MCP server with debug logging
run-debug:
FREECAD_MCP_LOG_LEVEL=DEBUG uv run freecad-mcp
# Run in HTTP mode for remote access
run-http port="8000":
FREECAD_MCP_TRANSPORT=http FREECAD_MCP_PORT={{port}} uv run freecad-mcp
# =============================================================================
# FreeCAD Shortcuts (aliases to freecad module)
# =============================================================================
# Run FreeCAD GUI with MCP bridge (shortcut for freecad::run-gui)
run-gui: (freecad::run-gui)
# Run FreeCAD headless with MCP bridge (shortcut for freecad::run-headless)
run-headless: (freecad::run-headless)
# Install the CutObjectForMagnets macro (shortcut for freecad::install-cut-macro)
install-cut-macro: (freecad::install-cut-macro)
# Uninstall the CutObjectForMagnets macro (shortcut for freecad::uninstall-cut-macro)
uninstall-cut-macro: (freecad::uninstall-cut-macro)
# =============================================================================
# Combined Workflows
# =============================================================================
# Run all quality checks and unit tests (use before committing)
all: check test
all: (quality::check) (testing::unit)
@echo "All checks passed!"
# Run integration tests with automatic FreeCAD headless startup
integration: (testing::integration-auto)
# Run all quality checks and ALL tests including integration
all-integration: check test integration
all-with-integration: (quality::check) (testing::unit) (testing::integration-freecad)
@echo "All checks and integration tests passed!"
# Full CI pipeline (unit tests only)
ci: check test-cov
# Full CI pipeline (pre-commit checks + unit tests with coverage)
ci: (quality::check) (testing::cov)
@echo "CI pipeline complete!"
# Clean build artifacts
clean:
rm -rf .pytest_cache .mypy_cache .ruff_cache .coverage htmlcov dist build
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
find . -type f -name "*.pyc" -delete 2>/dev/null || true
# =============================================================================
# Development Helpers
# Module Listings (use these to explore available commands)
# =============================================================================
# Open Python REPL with project modules available
repl:
uv run python -c "import freecad_mcp; print('FreeCAD MCP loaded')" && uv run python
# List MCP server commands
list-mcp:
@just --list mcp
# Show project structure
tree:
tree -I '__pycache__|*.egg-info|.git|.mypy_cache|.pytest_cache|.ruff_cache|htmlcov|dist|build' -a
# List FreeCAD plugin and macro commands
list-freecad:
@just --list freecad
# Validate project configuration
validate:
uv pip check
uv run validate-pyproject pyproject.toml
# List installation commands
list-install:
@just --list install
# List code quality commands
list-quality:
@just --list quality
# List testing commands
list-testing:
@just --list testing
# List Docker build/run commands
list-docker:
@just --list docker
# List documentation commands
list-documentation:
@just --list documentation
# List development utility commands
list-dev:
@just --list dev
# List release and tagging commands
list-release:
@just --list release
# List AI code review commands
list-coderabbit:
@just --list coderabbit
# List ALL commands from all modules
list-all:
@just --list --list-submodules
@@ -2,7 +2,7 @@
![Macro Icon](CutObjectForMagnets.svg)
**Version:** 1.0.0
**Version:** 0.5.0-beta
**FreeCAD Version:** 0.19 or later
**License:** MIT
@@ -601,7 +601,7 @@ This ensures holes only go where they're safe!
## Version History
### v1.0.0 (2025-01-01)
### v0.5.0-beta (2026-01-05)
- Initial release
- XY, XZ, YZ preset plane support
+2 -2
View File
@@ -2,7 +2,7 @@
![Macro Icon](MultiExport.svg)
**Version:** 1.0.0
**Version:** 0.5.0-beta
**FreeCAD Version:** 0.19 or later
**License:** MIT
@@ -272,7 +272,7 @@ When multiple objects are selected:
## Version History
### v1.0.0 (2025)
### v0.5.0-beta (2026-01-05)
- Initial release
- 8 export format support (STL, STEP, 3MF, OBJ, IGES, BREP, PLY, AMF)
+67 -9
View File
@@ -6,33 +6,42 @@ repo_name: spkane/freecad-robust-mcp-and-more
theme:
name: material
custom_dir: docs/overrides
palette:
- scheme: default
primary: blue
accent: blue
toggle:
icon: material/brightness-7
name: Switch to dark mode
# Dark mode as default
- scheme: slate
primary: blue
accent: blue
primary: deep purple
accent: purple
toggle:
icon: material/brightness-4
name: Switch to light mode
- scheme: default
primary: deep purple
accent: purple
toggle:
icon: material/brightness-7
name: Switch to dark mode
features:
- content.code.copy
- content.code.annotate
- content.tabs.link
- navigation.instant
- navigation.tracking
- navigation.tabs
- navigation.sections
- toc.integrate
- navigation.top
- toc.follow
- search.suggest
- search.highlight
- search.share
icon:
repo: fontawesome/brands/github
markdown_extensions:
- pymdownx.highlight:
anchor_linenums: true
line_spans: __span
pygments_lang_class: true
- pymdownx.superfences:
custom_fences:
- name: mermaid
@@ -40,13 +49,28 @@ markdown_extensions:
format: !!python/name:pymdownx.superfences.fence_code_format
- pymdownx.inlinehilite
- pymdownx.snippets
- pymdownx.tabbed:
alternate_style: true
- pymdownx.tasklist:
custom_checkbox: true
- pymdownx.emoji:
emoji_index: !!python/name:material.extensions.emoji.twemoji
emoji_generator: !!python/name:material.extensions.emoji.to_svg
- pymdownx.keys
- pymdownx.mark
- pymdownx.critic
- pymdownx.caret
- pymdownx.tilde
- admonition
- pymdownx.details
- attr_list
- md_in_html
- def_list
- footnotes
- tables
- toc:
permalink: true
toc_depth: 3
plugins:
- search
@@ -58,6 +82,37 @@ plugins:
show_source: true
show_root_heading: true
show_category_heading: true
- git-revision-date-localized:
enable_creation_date: true
type: timeago
- minify:
minify_html: true
- glightbox:
touchNavigation: true
loop: false
effect: zoom
slide_effect: slide
width: 100%
height: auto
zoomable: true
draggable: true
- macros:
include_yaml:
- docs/variables.yaml
# Use different delimiters to avoid conflicts with Python dicts in code blocks
j2_variable_start_string: "{{@"
j2_variable_end_string: "@}}"
j2_block_start_string: "{%@"
j2_block_end_string: "@%}"
extra:
social:
- icon: fontawesome/brands/github
link: https://github.com/spkane/freecad-robust-mcp-and-more
- icon: fontawesome/brands/docker
link: https://hub.docker.com/r/spkane/freecad-robust-mcp
- icon: fontawesome/brands/python
link: https://pypi.org/project/freecad-robust-mcp/
nav:
- Home: index.md
@@ -82,3 +137,6 @@ nav:
- Development:
- Contributing: development/contributing.md
- Architecture: development/architecture.md
- Detailed Architecture: development/architecture-detailed.md
- Release Process: development/releasing.md
- MkDocs Guide: development/mkdocs-guide.md
+8 -2
View File
@@ -5,8 +5,8 @@
<description>A collection of FreeCAD macros including an MCP (Model Context Protocol) bridge for AI assistant integration, multi-format export utilities, and a tool for cutting objects with aligned magnet holes for 3D printing.</description>
<icon>addon/FreecadRobustMCP/FreecadRobustMCP.svg</icon>
<version>0.5.0-beta</version>
<date>2026-01-05</date>
<!-- Note: Each component has its own version below. This top-level date is for reference only. -->
<date>2026-01-07</date>
<maintainer email="spkane@gmail.com">Sean P. Kane</maintainer>
<author email="spkane@gmail.com">Sean P. Kane</author>
@@ -37,6 +37,8 @@
<workbench>
<name>MCP Bridge</name>
<version>0.5.0-beta</version>
<date>2026-01-07</date>
<description>MCP (Model Context Protocol) bridge workbench for AI assistant integration with FreeCAD. Works in both GUI mode (with full visual features including screenshots, colors, and camera control) and headless mode (for automation and CI/CD). Provides toolbar commands to start, stop, and monitor the MCP bridge server. Supports XML-RPC and JSON-RPC protocols. Connect Claude Code, Cursor, or other MCP-compatible AI assistants to control FreeCAD programmatically with 82+ CAD tools.</description>
<classname>FreecadRobustMCPWorkbench</classname>
<subdirectory>./addon/FreecadRobustMCP/</subdirectory>
@@ -46,6 +48,8 @@
<macro>
<name>Multi Export</name>
<version>0.5.0-beta</version>
<date>2026-01-07</date>
<description>Export selected bodies to multiple file formats (STL, STEP, 3MF, OBJ, IGES, BREP, PLY, AMF) simultaneously with configurable mesh options.</description>
<subdirectory>./macros/Multi_Export/</subdirectory>
<file>MultiExport.FCMacro</file>
@@ -54,6 +58,8 @@
<macro>
<name>Cut Object for Magnets</name>
<version>0.5.0-beta</version>
<date>2026-01-07</date>
<description>Cut an object along a plane and add aligned magnet holes with surface collision detection. Perfect for creating 3D printed parts that snap together with embedded magnets.</description>
<subdirectory>./macros/Cut_Object_for_Magnets/</subdirectory>
<file>CutObjectForMagnets.FCMacro</file>
+21 -2
View File
@@ -66,6 +66,7 @@ dev = [
"pytest-asyncio>=0.25.0",
"pytest-cov>=6.0.0",
"pytest-timeout>=2.3.0",
"pytest-watch>=4.2.0",
# Linting and formatting
"mypy>=1.14.0",
"ruff>=0.8.0",
@@ -73,12 +74,18 @@ dev = [
"bandit[toml]>=1.8.0",
"safety>=3.2.0",
"detect-secrets>=1.5.0",
# Pre-commit
# Pre-commit and commit conventions
"pre-commit>=4.0.0",
"commitizen>=4.1.0",
# Documentation
"mkdocs>=1.6.0",
"mkdocs-material>=9.5.0",
"mkdocs-material>=9.7.1",
"mkdocstrings[python]>=0.27.0",
"mkdocs-git-revision-date-localized-plugin>=1.2.0",
"mkdocs-minify-plugin>=0.8.0",
"mkdocs-redirects>=1.2.0",
"mkdocs-glightbox>=0.4.0",
"mkdocs-macros-plugin>=1.0.0",
"md-toc>=9.0.0",
"codespell>=2.3.0",
# Publishing
@@ -308,3 +315,15 @@ skips = [
[tool.codespell]
skip = ".git,*.lock,*.toml"
ignore-words-list = "crate"
[tool.commitizen]
name = "cz_conventional_commits"
version_scheme = "semver2"
major_version_zero = true
# Note: This project uses component-specific tags for releases:
# - robust-mcp-server-vX.Y.Z (MCP server: PyPI, Docker, GitHub release)
# - robust-mcp-workbench-vX.Y.Z (FreeCAD workbench)
# - macro-cut-object-for-magnets-vX.Y.Z
# - macro-multi-export-vX.Y.Z
# Use `just release::tag-<component> <version>` to create release tags.
# Commitizen is used for commit message validation and changelog generation.
+1 -1
View File
@@ -127,7 +127,7 @@ The FreeCAD MCP bridge server is not running. To fix this:
- Click "Start MCP Bridge" in the toolbar
Option B: From source (for developers)
- Run: just run-gui
- Run: just freecad::run-gui
3. You should see: "MCP Bridge started!"
- XML-RPC: localhost:{self._port}
+1 -1
View File
@@ -120,7 +120,7 @@ def pytest_collection_modifyitems(
return
# Check bridge connection once
is_available, error, instance_id = _check_bridge_connection()
is_available, error, _instance_id = _check_bridge_connection()
if not is_available:
# Apply skip marker to all integration tests
@@ -13,7 +13,7 @@ Test Organization:
- TestEdgeCases: Edge case tests (single hole, many holes)
Note: These tests require a running FreeCAD MCP bridge.
Start it with: just run-gui or just run-headless
Start it with: just freecad::run-gui or just freecad::run-headless
Note: PartDesign::Hole has a CADKernelError bug in some FreeCAD headless
environments (especially AppImage on Linux CI) where it fails with
+1 -1
View File
@@ -5,7 +5,7 @@ in GUI mode (with full graphical interface). They test GUI-specific features
like visibility, display modes, colors, and camera operations.
Note: These tests require a running FreeCAD GUI server.
Start it with: just run-gui
Start it with: just freecad::run-gui
To run these tests:
pytest tests/integration/test_gui_mode.py -v
+1 -1
View File
@@ -5,7 +5,7 @@ in headless mode (without GUI). They test object creation, manipulation, and
export functionality.
Note: These tests require a running FreeCAD headless server.
Start it with: just run-headless
Start it with: just freecad::run-headless
To run these tests:
pytest tests/integration/test_headless_mode.py -v
+1 -1
View File
@@ -7,7 +7,7 @@ These tests verify the MultiExporter class functionality including:
- Handling mesh tolerance settings
Note: These tests require a running FreeCAD MCP bridge.
Start it with: just run-gui or just run-headless
Start it with: just freecad::run-gui or just freecad::run-headless
To run these tests:
pytest tests/integration/test_multi_export.py -v
Generated
+420 -1
View File
@@ -24,6 +24,15 @@ wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" },
]
[[package]]
name = "argcomplete"
version = "3.6.3"
source = { registry = "https://pkgs.safetycli.com/repository/techlabssh/pypi/simple/" }
sdist = { url = "https://pkgs.safetycli.com/package/techlabssh/pypi/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://pkgs.safetycli.com/package/techlabssh/pypi/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 = "attrs"
version = "25.4.0"
@@ -297,6 +306,29 @@ wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "commitizen"
version = "4.11.0"
source = { registry = "https://pkgs.safetycli.com/repository/techlabssh/pypi/simple/" }
dependencies = [
{ name = "argcomplete" },
{ name = "charset-normalizer" },
{ name = "colorama" },
{ name = "decli" },
{ name = "deprecated" },
{ name = "jinja2" },
{ name = "packaging" },
{ name = "prompt-toolkit" },
{ name = "pyyaml" },
{ name = "questionary" },
{ name = "termcolor" },
{ name = "tomlkit" },
]
sdist = { url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/c4/aa/e0188faa6eecd4293a1d4213ea2b2ddcbed7f8f248ef41594662a78bba92/commitizen-4.11.0.tar.gz", hash = "sha256:d311297a0165ef9f30e0877e04608b786d5fd69760f32245fbf1c21e793e91df", size = 57939, upload-time = "2025-12-29T11:26:36.67Z" }
wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/80/4a/a466d904f0b218dd357669b8585540c0b086ff335fd8182bf35456f4f41b/commitizen-4.11.0-py3-none-any.whl", hash = "sha256:a30fdf326bb0913b7b25f8b30530c899159b1757efa9969b0aa1808dad102c02", size = 82851, upload-time = "2025-12-29T11:26:34.718Z" },
]
[[package]]
name = "coverage"
version = "7.13.1"
@@ -451,6 +483,33 @@ wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" },
]
[[package]]
name = "csscompressor"
version = "0.9.5"
source = { registry = "https://pkgs.safetycli.com/repository/techlabssh/pypi/simple/" }
sdist = { url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/f1/2a/8c3ac3d8bc94e6de8d7ae270bb5bc437b210bb9d6d9e46630c98f4abd20c/csscompressor-0.9.5.tar.gz", hash = "sha256:afa22badbcf3120a4f392e4d22f9fff485c044a1feda4a950ecc5eba9dd31a05", size = 237808, upload-time = "2017-11-26T21:13:08.238Z" }
[[package]]
name = "decli"
version = "0.6.3"
source = { registry = "https://pkgs.safetycli.com/repository/techlabssh/pypi/simple/" }
sdist = { url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/0c/59/d4ffff1dee2c8f6f2dd8f87010962e60f7b7847504d765c91ede5a466730/decli-0.6.3.tar.gz", hash = "sha256:87f9d39361adf7f16b9ca6e3b614badf7519da13092f2db3c80ca223c53c7656", size = 7564, upload-time = "2025-06-01T15:23:41.25Z" }
wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/d8/fa/ec878c28bc7f65b77e7e17af3522c9948a9711b9fa7fc4c5e3140a7e3578/decli-0.6.3-py3-none-any.whl", hash = "sha256:5152347c7bb8e3114ad65db719e5709b28d7f7f45bdb709f70167925e55640f3", size = 7989, upload-time = "2025-06-01T15:23:40.228Z" },
]
[[package]]
name = "deprecated"
version = "1.3.1"
source = { registry = "https://pkgs.safetycli.com/repository/techlabssh/pypi/simple/" }
dependencies = [
{ name = "wrapt" },
]
sdist = { url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" }
wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" },
]
[[package]]
name = "detect-secrets"
version = "1.5.0"
@@ -473,6 +532,12 @@ wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
]
[[package]]
name = "docopt"
version = "0.6.2"
source = { registry = "https://pkgs.safetycli.com/repository/techlabssh/pypi/simple/" }
sdist = { url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/a2/55/8f8cab2afd404cf578136ef2cc5dfb50baa1761b68c9da1fb1e4eed343c9/docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491", size = 25901, upload-time = "2014-06-16T11:18:57.406Z" }
[[package]]
name = "docutils"
version = "0.22.4"
@@ -526,10 +591,16 @@ dev = [
{ name = "bandit" },
{ name = "build" },
{ name = "codespell" },
{ name = "commitizen" },
{ name = "detect-secrets" },
{ name = "md-toc" },
{ name = "mkdocs" },
{ name = "mkdocs-git-revision-date-localized-plugin" },
{ name = "mkdocs-glightbox" },
{ name = "mkdocs-macros-plugin" },
{ name = "mkdocs-material" },
{ name = "mkdocs-minify-plugin" },
{ name = "mkdocs-redirects" },
{ name = "mkdocstrings", extra = ["python"] },
{ name = "mypy" },
{ name = "pre-commit" },
@@ -537,6 +608,7 @@ dev = [
{ name = "pytest-asyncio" },
{ name = "pytest-cov" },
{ name = "pytest-timeout" },
{ name = "pytest-watch" },
{ name = "ruff" },
{ name = "safety" },
{ name = "twine" },
@@ -547,11 +619,17 @@ requires-dist = [
{ name = "bandit", extras = ["toml"], marker = "extra == 'dev'", specifier = ">=1.8.0" },
{ name = "build", marker = "extra == 'dev'", specifier = ">=1.2.0" },
{ name = "codespell", marker = "extra == 'dev'", specifier = ">=2.3.0" },
{ name = "commitizen", marker = "extra == 'dev'", specifier = ">=4.1.0" },
{ name = "detect-secrets", marker = "extra == 'dev'", specifier = ">=1.5.0" },
{ name = "mcp", specifier = ">=1.25.0" },
{ name = "md-toc", marker = "extra == 'dev'", specifier = ">=9.0.0" },
{ name = "mkdocs", marker = "extra == 'dev'", specifier = ">=1.6.0" },
{ name = "mkdocs-material", marker = "extra == 'dev'", specifier = ">=9.5.0" },
{ name = "mkdocs-git-revision-date-localized-plugin", marker = "extra == 'dev'", specifier = ">=1.2.0" },
{ name = "mkdocs-glightbox", marker = "extra == 'dev'", specifier = ">=0.4.0" },
{ name = "mkdocs-macros-plugin", marker = "extra == 'dev'", specifier = ">=1.0.0" },
{ name = "mkdocs-material", marker = "extra == 'dev'", specifier = ">=9.7.1" },
{ name = "mkdocs-minify-plugin", marker = "extra == 'dev'", specifier = ">=0.8.0" },
{ name = "mkdocs-redirects", marker = "extra == 'dev'", specifier = ">=1.2.0" },
{ name = "mkdocstrings", extras = ["python"], marker = "extra == 'dev'", specifier = ">=0.27.0" },
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.14.0" },
{ name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.0.0" },
@@ -561,6 +639,7 @@ requires-dist = [
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.25.0" },
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=6.0.0" },
{ name = "pytest-timeout", marker = "extra == 'dev'", specifier = ">=2.3.0" },
{ name = "pytest-watch", marker = "extra == 'dev'", specifier = ">=4.2.0" },
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.8.0" },
{ name = "safety", marker = "extra == 'dev'", specifier = ">=3.2.0" },
{ name = "twine", marker = "extra == 'dev'", specifier = ">=6.0.0" },
@@ -579,6 +658,30 @@ wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" },
]
[[package]]
name = "gitdb"
version = "4.0.12"
source = { registry = "https://pkgs.safetycli.com/repository/techlabssh/pypi/simple/" }
dependencies = [
{ name = "smmap" },
]
sdist = { url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" }
wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" },
]
[[package]]
name = "gitpython"
version = "3.1.46"
source = { registry = "https://pkgs.safetycli.com/repository/techlabssh/pypi/simple/" }
dependencies = [
{ name = "gitdb" },
]
sdist = { url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/df/b5/59d16470a1f0dfe8c793f9ef56fd3826093fc52b3bd96d6b9d6c26c7e27b/gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", size = 215371, upload-time = "2026-01-01T15:37:32.073Z" }
wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" },
]
[[package]]
name = "griffe"
version = "1.15.0"
@@ -600,6 +703,23 @@ wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "hjson"
version = "3.1.0"
source = { registry = "https://pkgs.safetycli.com/repository/techlabssh/pypi/simple/" }
sdist = { url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/82/e5/0b56d723a76ca67abadbf7fb71609fb0ea7e6926e94fcca6c65a85b36a0e/hjson-3.1.0.tar.gz", hash = "sha256:55af475a27cf83a7969c808399d7bccdec8fb836a07ddbd574587593b9cdcf75", size = 40541, upload-time = "2022-08-13T02:53:01.919Z" }
wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/1f/7f/13cd798d180af4bf4c0ceddeefba2b864a63c71645abc0308b768d67bb81/hjson-3.1.0-py3-none-any.whl", hash = "sha256:65713cdcf13214fb554eb8b4ef803419733f4f5e551047c9b711098ab7186b89", size = 54018, upload-time = "2022-08-13T02:52:59.899Z" },
]
[[package]]
name = "htmlmin2"
version = "0.1.13"
source = { registry = "https://pkgs.safetycli.com/repository/techlabssh/pypi/simple/" }
wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/be/31/a76f4bfa885f93b8167cb4c85cf32b54d1f64384d0b897d45bc6d19b7b45/htmlmin2-0.1.13-py3-none-any.whl", hash = "sha256:75609f2a42e64f7ce57dbff28a39890363bde9e7e5885db633317efbdf8c79a2", size = 34486, upload-time = "2023-03-14T21:28:30.388Z" },
]
[[package]]
name = "httpcore"
version = "1.0.9"
@@ -754,6 +874,12 @@ wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" },
]
[[package]]
name = "jsmin"
version = "3.0.1"
source = { registry = "https://pkgs.safetycli.com/repository/techlabssh/pypi/simple/" }
sdist = { url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/5e/73/e01e4c5e11ad0494f4407a3f623ad4d87714909f50b17a06ed121034ff6e/jsmin-3.0.1.tar.gz", hash = "sha256:c0959a121ef94542e807a674142606f7e90214a2b3d1eb17300244bbb5cc2bfc", size = 13925, upload-time = "2022-01-16T20:35:59.13Z" }
[[package]]
name = "jsonschema"
version = "4.25.1"
@@ -1073,6 +1199,54 @@ wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" },
]
[[package]]
name = "mkdocs-git-revision-date-localized-plugin"
version = "1.5.0"
source = { registry = "https://pkgs.safetycli.com/repository/techlabssh/pypi/simple/" }
dependencies = [
{ name = "babel" },
{ name = "gitpython" },
{ name = "mkdocs" },
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/0f/c5/1d3c4e6ddae6230b89d09105cb79de711655e3ebd6745f7a92efea0f5160/mkdocs_git_revision_date_localized_plugin-1.5.0.tar.gz", hash = "sha256:17345ccfdf69a1905dc96fb1070dce82d03a1eb6b0d48f958081a7589ce3c248", size = 460697, upload-time = "2025-10-31T16:11:34.44Z" }
wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/bc/51/fe0e3fdb16f6eed65c9459d12bae6a4e1f0bb4e2228cb037e7907b002678/mkdocs_git_revision_date_localized_plugin-1.5.0-py3-none-any.whl", hash = "sha256:933f9e35a8c135b113f21bb57610d82e9b7bcc71dd34fb06a029053c97e99656", size = 26153, upload-time = "2025-10-31T16:11:32.987Z" },
]
[[package]]
name = "mkdocs-glightbox"
version = "0.5.2"
source = { registry = "https://pkgs.safetycli.com/repository/techlabssh/pypi/simple/" }
dependencies = [
{ name = "selectolax" },
]
sdist = { url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/8d/26/c793459622da8e31f954c6f5fb51e8f098143fdfc147b1e3c25bf686f4aa/mkdocs_glightbox-0.5.2.tar.gz", hash = "sha256:c7622799347c32310878e01ccf14f70648445561010911c80590cec0353370ac", size = 510586, upload-time = "2025-10-23T14:55:18.909Z" }
wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/4e/ca/03624e017e5ee2d7ce8a08d89f81c1e535eb3c30d7b2dc4a435ea3fbbeae/mkdocs_glightbox-0.5.2-py3-none-any.whl", hash = "sha256:23a431ea802b60b1030c73323db2eed6ba859df1a0822ce575afa43e0ea3f47e", size = 26458, upload-time = "2025-10-23T14:55:17.43Z" },
]
[[package]]
name = "mkdocs-macros-plugin"
version = "1.5.0"
source = { registry = "https://pkgs.safetycli.com/repository/techlabssh/pypi/simple/" }
dependencies = [
{ name = "hjson" },
{ name = "jinja2" },
{ name = "mkdocs" },
{ name = "packaging" },
{ name = "pathspec" },
{ name = "python-dateutil" },
{ name = "pyyaml" },
{ name = "requests" },
{ name = "super-collections" },
{ name = "termcolor" },
]
sdist = { url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/92/15/e6a44839841ebc9c5872fa0e6fad1c3757424e4fe026093b68e9f386d136/mkdocs_macros_plugin-1.5.0.tar.gz", hash = "sha256:12aa45ce7ecb7a445c66b9f649f3dd05e9b92e8af6bc65e4acd91d26f878c01f", size = 37730, upload-time = "2025-11-13T08:08:55.545Z" }
wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/51/62/9fffba5bb9ed3d31a932ad35038ba9483d59850256ee0fea7f1187173983/mkdocs_macros_plugin-1.5.0-py3-none-any.whl", hash = "sha256:c10fabd812bf50f9170609d0ed518e54f1f0e12c334ac29141723a83c881dd6f", size = 44626, upload-time = "2025-11-13T08:08:53.878Z" },
]
[[package]]
name = "mkdocs-material"
version = "9.7.1"
@@ -1104,6 +1278,33 @@ wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" },
]
[[package]]
name = "mkdocs-minify-plugin"
version = "0.8.0"
source = { registry = "https://pkgs.safetycli.com/repository/techlabssh/pypi/simple/" }
dependencies = [
{ name = "csscompressor" },
{ name = "htmlmin2" },
{ name = "jsmin" },
{ name = "mkdocs" },
]
sdist = { url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/52/67/fe4b77e7a8ae7628392e28b14122588beaf6078b53eb91c7ed000fd158ac/mkdocs-minify-plugin-0.8.0.tar.gz", hash = "sha256:bc11b78b8120d79e817308e2b11539d790d21445eb63df831e393f76e52e753d", size = 8366, upload-time = "2024-01-29T16:11:32.982Z" }
wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/1b/cd/2e8d0d92421916e2ea4ff97f10a544a9bd5588eb747556701c983581df13/mkdocs_minify_plugin-0.8.0-py3-none-any.whl", hash = "sha256:5fba1a3f7bd9a2142c9954a6559a57e946587b21f133165ece30ea145c66aee6", size = 6723, upload-time = "2024-01-29T16:11:31.851Z" },
]
[[package]]
name = "mkdocs-redirects"
version = "1.2.2"
source = { registry = "https://pkgs.safetycli.com/repository/techlabssh/pypi/simple/" }
dependencies = [
{ name = "mkdocs" },
]
sdist = { url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/f1/a8/6d44a6cf07e969c7420cb36ab287b0669da636a2044de38a7d2208d5a758/mkdocs_redirects-1.2.2.tar.gz", hash = "sha256:3094981b42ffab29313c2c1b8ac3969861109f58b2dd58c45fc81cd44bfa0095", size = 7162, upload-time = "2024-11-07T14:57:21.109Z" }
wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/c4/ec/38443b1f2a3821bbcb24e46cd8ba979154417794d54baf949fefde1c2146/mkdocs_redirects-1.2.2-py3-none-any.whl", hash = "sha256:7dbfa5647b79a3589da4401403d69494bd1f4ad03b9c15136720367e1f340ed5", size = 6142, upload-time = "2024-11-07T14:57:19.143Z" },
]
[[package]]
name = "mkdocstrings"
version = "1.0.0"
@@ -1315,6 +1516,18 @@ wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" },
]
[[package]]
name = "prompt-toolkit"
version = "3.0.51"
source = { registry = "https://pkgs.safetycli.com/repository/techlabssh/pypi/simple/" }
dependencies = [
{ name = "wcwidth" },
]
sdist = { url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940, upload-time = "2025-04-15T09:18:47.731Z" }
wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" },
]
[[package]]
name = "pycparser"
version = "2.23"
@@ -1550,6 +1763,18 @@ wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" },
]
[[package]]
name = "pytest-watch"
version = "4.2.0"
source = { registry = "https://pkgs.safetycli.com/repository/techlabssh/pypi/simple/" }
dependencies = [
{ name = "colorama" },
{ name = "docopt" },
{ name = "pytest" },
{ name = "watchdog" },
]
sdist = { url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/36/47/ab65fc1d682befc318c439940f81a0de1026048479f732e84fe714cd69c0/pytest-watch-4.2.0.tar.gz", hash = "sha256:06136f03d5b361718b8d0d234042f7b2f203910d8568f63df2f866b547b3d4b9", size = 16340, upload-time = "2018-05-20T19:52:16.194Z" }
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
@@ -1675,6 +1900,18 @@ wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" },
]
[[package]]
name = "questionary"
version = "2.1.1"
source = { registry = "https://pkgs.safetycli.com/repository/techlabssh/pypi/simple/" }
dependencies = [
{ name = "prompt-toolkit" },
]
sdist = { url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" }
wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" },
]
[[package]]
name = "readme-renderer"
version = "44.0"
@@ -2053,6 +2290,59 @@ wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" },
]
[[package]]
name = "selectolax"
version = "0.4.6"
source = { registry = "https://pkgs.safetycli.com/repository/techlabssh/pypi/simple/" }
sdist = { url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/fb/c5/959b8661d200d9fd3cf52061ce6f1d7087ec94823bb324fe1ca76c80b250/selectolax-0.4.6.tar.gz", hash = "sha256:bd9326cfc9bbd5bfcda980b0452b9761b3cf134015154e95d83fa32cbfbb751b", size = 4793248, upload-time = "2025-12-06T12:35:48.513Z" }
wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/a0/76/9b5358d82353b68c5828cc8ceae4ff1073495462979d2035c1089f4421dd/selectolax-0.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:751f727c9963584fcfa101b2696e0dd31236142901bbe7866a8e39f2ec346cc4", size = 2052057, upload-time = "2025-12-06T12:34:24.448Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/e3/d1/9f8c8841f414a1b72174acef916d4ed38cc0fa041d3e933013e2b3213f30/selectolax-0.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d6f45737b638836b9fe2a4e7ccfbcd48a315ebb96da76a79a04b8227c1a967ee", size = 2050379, upload-time = "2025-12-06T12:34:26.383Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/bc/c4/8e5ab9275a185a31d06b5ea58e3ba61d57e57700964cbd56fe074dbe458c/selectolax-0.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15a22c5cd9c23f09227b2b9c227d986396125a9b0eb21be655f22fe4ccc5679a", size = 2238005, upload-time = "2025-12-06T12:34:28.2Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/ea/af/f4299d931a8e11ba1998cac70d407c5338436978325232eb252ac7f5aba2/selectolax-0.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e64f771807f1a7353f4d6878c303bfbd6c6fe58897e301aa6d0de7e5e60cef61", size = 2272927, upload-time = "2025-12-06T12:34:29.955Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/7b/5e/9afbb0e8495846bf97fa7725a6f97622ad85efc654cb3cbf4c881bd345de/selectolax-0.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0144a37fbf01695ccf2d0d24caaa058a28449cecb2c754bb9e616f339468d74", size = 2250901, upload-time = "2025-12-06T12:34:31.442Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/d5/6b/914cc9c977fd21949af5776d25b9c011b6e72fb38569161ab6ca83d6130a/selectolax-0.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c9bdd4a9b3f71f28a0ee558a105451debd33cbe1ed350ebba7ad6b68d62815c", size = 2279842, upload-time = "2025-12-06T12:34:32.739Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/e1/30/b32d79d31c893cf5ccea5a5be4565db1eda9bbf458ddfe402559267f2d31/selectolax-0.4.6-cp311-cp311-win32.whl", hash = "sha256:b91c34b854fdd5d21c8353f130899f8413b7104a96078acbca59c8b2d57e9352", size = 1730462, upload-time = "2025-12-06T12:34:34.435Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/8f/f1/c7ba048d4fcc4c8d5857233c59d07f18e60b21400a8ad8e8d7da670024c2/selectolax-0.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:901064121e706bc86ed13f6e0dbe478398ad05ab112f5efbc8d722320a087b93", size = 1831442, upload-time = "2025-12-06T12:34:35.846Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/d5/55/b33853f66b1f875bbbbfc2294ce7a4065774621ab6ebf20e8abf19965846/selectolax-0.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:609c6c19f5b7cb669a6321a1d4133d2e2b443f23f7d454de76904118a91236a6", size = 1781850, upload-time = "2025-12-06T12:34:37.175Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/ee/81/1fdf6633df840afd9d7054a3441f04cfb1fc9d098c6c9f3bd46a64fb632e/selectolax-0.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:20615062d6062b197b61fd646e667591e987be3a894e8a8408d2a482ccddc747", size = 2051021, upload-time = "2025-12-06T12:34:38.495Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/cc/54/d59738d090cb4df3a3a6297b7ec216b86d3ba7f320013c4bc8d4841c9f5d/selectolax-0.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c436006e2af6ade80b96411cf46652c11ced4f230032e25e1f5210b7522a4fe3", size = 2047409, upload-time = "2025-12-06T12:34:39.875Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/fc/67/3b163ec18a128df3a3b59ce676a2dacfb26e714da81ba8a98e184b4ef187/selectolax-0.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:705a70b6f4e553e8c5299881269d3735a7df8a23711927a33caa16b4eaef580f", size = 2237052, upload-time = "2025-12-06T12:34:41.24Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/f0/04/c3ae4a77e8cfa647b9177e727a7e80f64b160b65ad0db0dcb3738a4ef4a0/selectolax-0.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c04fd180689ed9450ad2453a3cba74cff2475a4281f76db9e18a658b7823e594", size = 2275615, upload-time = "2025-12-06T12:34:43.114Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/12/de/aaa016c44e63a1efd5525f6da6eac807388a06c70671091c735d93f13b74/selectolax-0.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cb33eb0809e70ba4a475105d164c3f90a4bb711744ca69e20037298256b8e9d7", size = 2249186, upload-time = "2025-12-06T12:34:44.84Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/76/9a/a9cf9f0158b0804c7ea404d790af031830eb6452a4948853f7582eea6c51/selectolax-0.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:97f30b7c731f9f3328e9c6aef7ca3c17fbcbc4495e286a2cdad9a77bcadfadf1", size = 2282041, upload-time = "2025-12-06T12:34:46.19Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/4c/ea/85de7ab8a9fc0301d1b428e69dc0bced9c1cd7379872d576a2b88eb91933/selectolax-0.4.6-cp312-cp312-win32.whl", hash = "sha256:f4375b352b609508e4a6980431dc6cc1812b97658ad1aa8caa61e01565de0d7d", size = 1727544, upload-time = "2025-12-06T12:34:47.541Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/50/70/4aac2df64920112672cda846941d85c90b8152b2eddc9cf2615181551957/selectolax-0.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:1d02637a6746bf1ba7de1dfc00a0344ffb30bedd1b5d4e61727c960225bf6ce0", size = 1827825, upload-time = "2025-12-06T12:34:49.283Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/8d/b0/09648383afed1a10df97ce30527d30714edc4072086915b4bb1a0d81a728/selectolax-0.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:bb0b371c3e2a94e6658ba4b5af88fc35aaf44f57f5a066ecaf96b4875a47aec4", size = 1775233, upload-time = "2025-12-06T12:34:51.576Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/4d/87/7ed053cce7de8b29746c602851c67654287e25ec404d575911c6f40b671f/selectolax-0.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8303af0eeef6ab2f06f2b18e3c825df4f6984405a862c8e9e654621c26558aca", size = 2050412, upload-time = "2025-12-06T12:34:53.086Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/44/74/e8cd9b9b53da0e849b27a2ef68580915321ee52a662f015275a1cf6cca85/selectolax-0.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d8b1fb5507d90e719de705d93eaa3bdd5f2d69facf012fb67b7da8a9cd20ea6b", size = 2046513, upload-time = "2025-12-06T12:34:54.583Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/a0/ca/c1cc7f03b681d272dbb0dc47cf9d0df857ae156d7ea54d88cc25ec23c8e9/selectolax-0.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30ac51bd5bfcd6bffe58720b1fc5f97666681f0793d79d292069b3a3f8599ef0", size = 2234404, upload-time = "2025-12-06T12:34:55.971Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/df/00/a6e5c4d65243147fbd8837951267f098d9bee66ada4dc0c99d97259e052c/selectolax-0.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfee4d1269fd462256641abdf6c2ee4dd799ba82c578acab0bcde07547637826", size = 2272678, upload-time = "2025-12-06T12:34:57.3Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/ab/a2/05351e0f0da62d1bc01b7820a990f1e4dec688206e0278ee8faeeb878940/selectolax-0.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fce09eeb5dd19fba672a829f63e3c40238af4a929ce1e5fd16dcbc4fd253e300", size = 2247471, upload-time = "2025-12-06T12:34:59.048Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/ea/95/1ab9e3ad2d53dbcd7141af149c7259c693dc2dc46e27d72b7a68f17f3364/selectolax-0.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4e97ca71d545cab9c57ba3b18358cbc96ef0dcd01920ea903a531386ca1f849", size = 2278271, upload-time = "2025-12-06T12:35:00.487Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/04/41/ca80ca68fb9990cb46d753363d8dc0771225bc9bb9a77247a1987e52d2d5/selectolax-0.4.6-cp313-cp313-win32.whl", hash = "sha256:98fce08839cb8fd5d8788cbed2724cd892c78182cd923e246ef586d552a29a94", size = 1727514, upload-time = "2025-12-06T12:35:02.081Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/f0/47/f048994ab32773fda8eda57a874afb30d0b2bf30952c009006e81737e07e/selectolax-0.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:cdc8e7833d2456331e519fe901d1c8844d120a26b21b6429f47ae946e65b0c04", size = 1829070, upload-time = "2025-12-06T12:35:03.857Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/50/94/ab0d86b1a719c090e2bde9f7d9037439900e86fb50c258b5fd9f6530521b/selectolax-0.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:d6f8c976ad067a6607b2a052b141149ae23584819b73288c32f08a1ece23d60a", size = 1774935, upload-time = "2025-12-06T12:35:05.323Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/d2/38/93b4907c596487e13f8261129b1663559c96fc37ea2c973c98a393307484/selectolax-0.4.6-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:961c037ac41a12bf9db35a95d6076461f9b127d361c9ef26a77f76c45b1056eb", size = 2069131, upload-time = "2025-12-06T12:35:06.737Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/f6/04/d29769a8313ccb9db6278401840e79662f341b716d268f7468b6270d15e1/selectolax-0.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f2be076a69a12657a38933b99d31c604d95d63911e0799f89305da8e89918874", size = 2066134, upload-time = "2025-12-06T12:35:08.163Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/29/a2/1c26b4cc6a708d234e39199bd75acdc1cfdcf4f3138e16d25e3e7aa0295d/selectolax-0.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25ae5dd7c20035312211de43952934981e8ff4e1502467ce78665f57bc5eaf7f", size = 2240810, upload-time = "2025-12-06T12:35:09.556Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/1f/de/b021342d306bd08c92d0d9e9072a3ed6a3038f78387187d37fb775196fa0/selectolax-0.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfe1274930affe2b986fd20cbf2916ddf4a760edf30a7eeb9151b31e8cbe6027", size = 2274401, upload-time = "2025-12-06T12:35:11.023Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/29/77/b2816be2f4878f4e86fabca5932610401dc88d956948d97a82947e27d8bc/selectolax-0.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e52174a75236c1321f0f0353b5d97ba575c61428f16c2d78159cb154fa407e97", size = 2271054, upload-time = "2025-12-06T12:35:12.878Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/1f/3a/ae48b9febf2af21a0fdd4fba07861ae3e13bd7235df3fa39918d33004058/selectolax-0.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5032187ed8f8d77da6dee5467a87fe9b1553c10c63671fe830e87a2d347ef8ae", size = 2297773, upload-time = "2025-12-06T12:35:14.383Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/f9/8c/4e120b2b6dc55543bf2ffc43e78589022e59c02fa93dea0b4d22dfe3fc27/selectolax-0.4.6-cp314-cp314-win32.whl", hash = "sha256:8a4f50eb37abffe118730c062827a204a732cc1b9cd28b5dbf40752c371e9339", size = 1838289, upload-time = "2025-12-06T12:35:16.278Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/c4/2f/51f15b06f4fcf9d5845d6bba534f7f2ed531f148c77ed8fff105cd770aa5/selectolax-0.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:11527cd430cb6d1d1a422209d87aec5767befff424f2affaa3daa2789878cf9f", size = 1938201, upload-time = "2025-12-06T12:35:17.694Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/f3/8d/c099dcbb385e7d2145c4f19da183639bf4e429e1524dcba31ea311c5d276/selectolax-0.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:254d7f59580631dac1fcb861bb01f45039e3ac53187f07d8ccc3519110bacad0", size = 1886188, upload-time = "2025-12-06T12:35:19.538Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/54/ce/f3fa904517745ecd289b6ce18845ae968cf7d0b17e65ab781259f2746254/selectolax-0.4.6-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:48a9bce389c281fec8bf3b5a36bd376f1ad1f36ff072dcedaa977188b3882be1", size = 2088107, upload-time = "2025-12-06T12:35:21.061Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/4f/18/359da9723d3ec1235819cea0958bc417ce6a12699977920854b300ad4529/selectolax-0.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff1632c1767f6005adc2dff7b72ea9d0b6493702e86e04ee5cf934ab630172aa", size = 2090836, upload-time = "2025-12-06T12:35:22.421Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/e2/20/199f2a05ca8dfd9bc146af03fbfaa1e2050601d3141267070a2a023ea68f/selectolax-0.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64a56dd91c22e809305e127bbd4cd75ad1016dd329a4e945a176c6f3376d00e2", size = 2248608, upload-time = "2025-12-06T12:35:23.946Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/f9/67/261c06cdae29fad287008d39f51a80431f3fce66c2865b880e61d04cdfd2/selectolax-0.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:223471d2c9095406d69faf461aa217782575d62362d36809e350f6d3c2f69f4e", size = 2275653, upload-time = "2025-12-06T12:35:25.423Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/10/80/f2519487a86b5366acadd9e07c212b38fb657bb62b9e01de9fb24c3ada27/selectolax-0.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c1043e61df29f4f8ef49f9f623b3d710f0c545d9a7939eee52c49987b06aef7", size = 2283495, upload-time = "2025-12-06T12:35:26.843Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/66/cc/35a0fd896371e0b5a84365b03f88c7dbe8984862e34ce32baed81ee6f486/selectolax-0.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b323ad4ebcf2edad2488022402fbc73ee158ffe81607ec1ce5eb1039eab94086", size = 2300207, upload-time = "2025-12-06T12:35:28.23Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/51/b2/dc83cce2f38a3c72d6bf9268ef6c1708ab1b4d546074384e7e0d097bf4f6/selectolax-0.4.6-cp314-cp314t-win32.whl", hash = "sha256:f5017f6e2408160604c87fb21490d9af802d09dbc1b91ac89acd9922b7b04d31", size = 1890595, upload-time = "2025-12-06T12:35:29.625Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/87/f9/8da49637643b1dffbcadc8972f1fee519c126978acaf5df59405e48424f4/selectolax-0.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:56adb0f014ab55627f20f53888a7bf1ec53aac8189fe344aec3d5077a7ad9889", size = 2005252, upload-time = "2025-12-06T12:35:31.116Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/94/7f/f783e2254db082df4f6bc00fe3b32b9dd27c3b7302a44c8c37728bb67fb7/selectolax-0.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:66558cfb1c7402fed0f47b9a2692eed53e3e2f345526314b493b5093cb951e21", size = 1906079, upload-time = "2025-12-06T12:35:32.951Z" },
]
[[package]]
name = "shellingham"
version = "1.5.4"
@@ -2071,6 +2361,15 @@ wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
[[package]]
name = "smmap"
version = "5.0.2"
source = { registry = "https://pkgs.safetycli.com/repository/techlabssh/pypi/simple/" }
sdist = { url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" }
wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" },
]
[[package]]
name = "sse-starlette"
version = "3.1.2"
@@ -2106,6 +2405,18 @@ wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/f4/40/8561ce06dc46fd17242c7724ab25b257a2ac1b35f4ebf551b40ce6105cfa/stevedore-5.6.0-py3-none-any.whl", hash = "sha256:4a36dccefd7aeea0c70135526cecb7766c4c84c473b1af68db23d541b6dc1820", size = 54428, upload-time = "2025-11-20T10:06:05.946Z" },
]
[[package]]
name = "super-collections"
version = "0.6.2"
source = { registry = "https://pkgs.safetycli.com/repository/techlabssh/pypi/simple/" }
dependencies = [
{ name = "hjson" },
]
sdist = { url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/e0/de/a0c3d1244912c260638f0f925e190e493ccea37ecaea9bbad7c14413b803/super_collections-0.6.2.tar.gz", hash = "sha256:0c8d8abacd9fad2c7c1c715f036c29f5db213f8cac65f24d45ecba12b4da187a", size = 31315, upload-time = "2025-09-30T00:37:08.067Z" }
wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/17/43/47c7cf84b3bd74a8631b02d47db356656bb8dff6f2e61a4c749963814d0d/super_collections-0.6.2-py3-none-any.whl", hash = "sha256:291b74d26299e9051d69ad9d89e61b07b6646f86a57a2f5ab3063d206eee9c56", size = 16173, upload-time = "2025-09-30T00:37:07.104Z" },
]
[[package]]
name = "tenacity"
version = "9.1.2"
@@ -2115,6 +2426,15 @@ wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" },
]
[[package]]
name = "termcolor"
version = "3.3.0"
source = { registry = "https://pkgs.safetycli.com/repository/techlabssh/pypi/simple/" }
sdist = { url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" }
wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" },
]
[[package]]
name = "tomli"
version = "2.3.0"
@@ -2241,6 +2561,15 @@ wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
]
[[package]]
name = "tzdata"
version = "2025.3"
source = { registry = "https://pkgs.safetycli.com/repository/techlabssh/pypi/simple/" }
sdist = { url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" }
wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" },
]
[[package]]
name = "urllib3"
version = "2.6.2"
@@ -2304,6 +2633,96 @@ wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" },
]
[[package]]
name = "wcwidth"
version = "0.2.14"
source = { registry = "https://pkgs.safetycli.com/repository/techlabssh/pypi/simple/" }
sdist = { url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" }
wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" },
]
[[package]]
name = "wrapt"
version = "2.0.1"
source = { registry = "https://pkgs.safetycli.com/repository/techlabssh/pypi/simple/" }
sdist = { url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/49/2a/6de8a50cb435b7f42c46126cf1a54b2aab81784e74c8595c8e025e8f36d3/wrapt-2.0.1.tar.gz", hash = "sha256:9c9c635e78497cacb81e84f8b11b23e0aacac7a136e73b8e5b2109a1d9fc468f", size = 82040, upload-time = "2025-11-07T00:45:33.312Z" }
wheels = [
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/98/60/553997acf3939079dab022e37b67b1904b5b0cc235503226898ba573b10c/wrapt-2.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e17283f533a0d24d6e5429a7d11f250a58d28b4ae5186f8f47853e3e70d2590", size = 77480, upload-time = "2025-11-07T00:43:30.573Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/2d/50/e5b3d30895d77c52105c6d5cbf94d5b38e2a3dd4a53d22d246670da98f7c/wrapt-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85df8d92158cb8f3965aecc27cf821461bb5f40b450b03facc5d9f0d4d6ddec6", size = 60690, upload-time = "2025-11-07T00:43:31.594Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/f0/40/660b2898703e5cbbb43db10cdefcc294274458c3ca4c68637c2b99371507/wrapt-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1be685ac7700c966b8610ccc63c3187a72e33cab53526a27b2a285a662cd4f7", size = 61578, upload-time = "2025-11-07T00:43:32.918Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/5b/36/825b44c8a10556957bc0c1d84c7b29a40e05fcf1873b6c40aa9dbe0bd972/wrapt-2.0.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:df0b6d3b95932809c5b3fecc18fda0f1e07452d05e2662a0b35548985f256e28", size = 114115, upload-time = "2025-11-07T00:43:35.605Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/83/73/0a5d14bb1599677304d3c613a55457d34c344e9b60eda8a737c2ead7619e/wrapt-2.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da7384b0e5d4cae05c97cd6f94faaf78cc8b0f791fc63af43436d98c4ab37bb", size = 116157, upload-time = "2025-11-07T00:43:37.058Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/01/22/1c158fe763dbf0a119f985d945711d288994fe5514c0646ebe0eb18b016d/wrapt-2.0.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ec65a78fbd9d6f083a15d7613b2800d5663dbb6bb96003899c834beaa68b242c", size = 112535, upload-time = "2025-11-07T00:43:34.138Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/5c/28/4f16861af67d6de4eae9927799b559c20ebdd4fe432e89ea7fe6fcd9d709/wrapt-2.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7de3cc939be0e1174969f943f3b44e0d79b6f9a82198133a5b7fc6cc92882f16", size = 115404, upload-time = "2025-11-07T00:43:39.214Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/a0/8b/7960122e625fad908f189b59c4aae2d50916eb4098b0fb2819c5a177414f/wrapt-2.0.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fb1a5b72cbd751813adc02ef01ada0b0d05d3dcbc32976ce189a1279d80ad4a2", size = 111802, upload-time = "2025-11-07T00:43:40.476Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/3e/73/7881eee5ac31132a713ab19a22c9e5f1f7365c8b1df50abba5d45b781312/wrapt-2.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3fa272ca34332581e00bf7773e993d4f632594eb2d1b0b162a9038df0fd971dd", size = 113837, upload-time = "2025-11-07T00:43:42.921Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/45/00/9499a3d14e636d1f7089339f96c4409bbc7544d0889f12264efa25502ae8/wrapt-2.0.1-cp311-cp311-win32.whl", hash = "sha256:fc007fdf480c77301ab1afdbb6ab22a5deee8885f3b1ed7afcb7e5e84a0e27be", size = 58028, upload-time = "2025-11-07T00:43:47.369Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/70/5d/8f3d7eea52f22638748f74b102e38fdf88cb57d08ddeb7827c476a20b01b/wrapt-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:47434236c396d04875180171ee1f3815ca1eada05e24a1ee99546320d54d1d1b", size = 60385, upload-time = "2025-11-07T00:43:44.34Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/14/e2/32195e57a8209003587bbbad44d5922f13e0ced2a493bb46ca882c5b123d/wrapt-2.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:837e31620e06b16030b1d126ed78e9383815cbac914693f54926d816d35d8edf", size = 58893, upload-time = "2025-11-07T00:43:46.161Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/cb/73/8cb252858dc8254baa0ce58ce382858e3a1cf616acebc497cb13374c95c6/wrapt-2.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1fdbb34da15450f2b1d735a0e969c24bdb8d8924892380126e2a293d9902078c", size = 78129, upload-time = "2025-11-07T00:43:48.852Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/19/42/44a0db2108526ee6e17a5ab72478061158f34b08b793df251d9fbb9a7eb4/wrapt-2.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d32794fe940b7000f0519904e247f902f0149edbe6316c710a8562fb6738841", size = 61205, upload-time = "2025-11-07T00:43:50.402Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/4d/8a/5b4b1e44b791c22046e90d9b175f9a7581a8cc7a0debbb930f81e6ae8e25/wrapt-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:386fb54d9cd903ee0012c09291336469eb7b244f7183d40dc3e86a16a4bace62", size = 61692, upload-time = "2025-11-07T00:43:51.678Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/11/53/3e794346c39f462bcf1f58ac0487ff9bdad02f9b6d5ee2dc84c72e0243b2/wrapt-2.0.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7b219cb2182f230676308cdcacd428fa837987b89e4b7c5c9025088b8a6c9faf", size = 121492, upload-time = "2025-11-07T00:43:55.017Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/c6/7e/10b7b0e8841e684c8ca76b462a9091c45d62e8f2de9c4b1390b690eadf16/wrapt-2.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:641e94e789b5f6b4822bb8d8ebbdfc10f4e4eae7756d648b717d980f657a9eb9", size = 123064, upload-time = "2025-11-07T00:43:56.323Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/0e/d1/3c1e4321fc2f5ee7fd866b2d822aa89b84495f28676fd976c47327c5b6aa/wrapt-2.0.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe21b118b9f58859b5ebaa4b130dee18669df4bd111daad082b7beb8799ad16b", size = 117403, upload-time = "2025-11-07T00:43:53.258Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/a4/b0/d2f0a413cf201c8c2466de08414a15420a25aa83f53e647b7255cc2fab5d/wrapt-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17fb85fa4abc26a5184d93b3efd2dcc14deb4b09edcdb3535a536ad34f0b4dba", size = 121500, upload-time = "2025-11-07T00:43:57.468Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/bd/45/bddb11d28ca39970a41ed48a26d210505120f925918592283369219f83cc/wrapt-2.0.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b89ef9223d665ab255ae42cc282d27d69704d94be0deffc8b9d919179a609684", size = 116299, upload-time = "2025-11-07T00:43:58.877Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/81/af/34ba6dd570ef7a534e7eec0c25e2615c355602c52aba59413411c025a0cb/wrapt-2.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a453257f19c31b31ba593c30d997d6e5be39e3b5ad9148c2af5a7314061c63eb", size = 120622, upload-time = "2025-11-07T00:43:59.962Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/e2/3e/693a13b4146646fb03254636f8bafd20c621955d27d65b15de07ab886187/wrapt-2.0.1-cp312-cp312-win32.whl", hash = "sha256:3e271346f01e9c8b1130a6a3b0e11908049fe5be2d365a5f402778049147e7e9", size = 58246, upload-time = "2025-11-07T00:44:03.169Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/a7/36/715ec5076f925a6be95f37917b66ebbeaa1372d1862c2ccd7a751574b068/wrapt-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:2da620b31a90cdefa9cd0c2b661882329e2e19d1d7b9b920189956b76c564d75", size = 60492, upload-time = "2025-11-07T00:44:01.027Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/ef/3e/62451cd7d80f65cc125f2b426b25fbb6c514bf6f7011a0c3904fc8c8df90/wrapt-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:aea9c7224c302bc8bfc892b908537f56c430802560e827b75ecbde81b604598b", size = 58987, upload-time = "2025-11-07T00:44:02.095Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/ad/fe/41af4c46b5e498c90fc87981ab2972fbd9f0bccda597adb99d3d3441b94b/wrapt-2.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:47b0f8bafe90f7736151f61482c583c86b0693d80f075a58701dd1549b0010a9", size = 78132, upload-time = "2025-11-07T00:44:04.628Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/1c/92/d68895a984a5ebbbfb175512b0c0aad872354a4a2484fbd5552e9f275316/wrapt-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cbeb0971e13b4bd81d34169ed57a6dda017328d1a22b62fda45e1d21dd06148f", size = 61211, upload-time = "2025-11-07T00:44:05.626Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/e8/26/ba83dc5ae7cf5aa2b02364a3d9cf74374b86169906a1f3ade9a2d03cf21c/wrapt-2.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb7cffe572ad0a141a7886a1d2efa5bef0bf7fe021deeea76b3ab334d2c38218", size = 61689, upload-time = "2025-11-07T00:44:06.719Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/cf/67/d7a7c276d874e5d26738c22444d466a3a64ed541f6ef35f740dbd865bab4/wrapt-2.0.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8d60527d1ecfc131426b10d93ab5d53e08a09c5fa0175f6b21b3252080c70a9", size = 121502, upload-time = "2025-11-07T00:44:09.557Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/0f/6b/806dbf6dd9579556aab22fc92908a876636e250f063f71548a8660382184/wrapt-2.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c654eafb01afac55246053d67a4b9a984a3567c3808bb7df2f8de1c1caba2e1c", size = 123110, upload-time = "2025-11-07T00:44:10.64Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/e5/08/cdbb965fbe4c02c5233d185d070cabed2ecc1f1e47662854f95d77613f57/wrapt-2.0.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:98d873ed6c8b4ee2418f7afce666751854d6d03e3c0ec2a399bb039cd2ae89db", size = 117434, upload-time = "2025-11-07T00:44:08.138Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/2d/d1/6aae2ce39db4cb5216302fa2e9577ad74424dfbe315bd6669725569e048c/wrapt-2.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9e850f5b7fc67af856ff054c71690d54fa940c3ef74209ad9f935b4f66a0233", size = 121533, upload-time = "2025-11-07T00:44:12.142Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/79/35/565abf57559fbe0a9155c29879ff43ce8bd28d2ca61033a3a3dd67b70794/wrapt-2.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e505629359cb5f751e16e30cf3f91a1d3ddb4552480c205947da415d597f7ac2", size = 116324, upload-time = "2025-11-07T00:44:13.28Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/e1/e0/53ff5e76587822ee33e560ad55876d858e384158272cd9947abdd4ad42ca/wrapt-2.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2879af909312d0baf35f08edeea918ee3af7ab57c37fe47cb6a373c9f2749c7b", size = 120627, upload-time = "2025-11-07T00:44:14.431Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/7c/7b/38df30fd629fbd7612c407643c63e80e1c60bcc982e30ceeae163a9800e7/wrapt-2.0.1-cp313-cp313-win32.whl", hash = "sha256:d67956c676be5a24102c7407a71f4126d30de2a569a1c7871c9f3cabc94225d7", size = 58252, upload-time = "2025-11-07T00:44:17.814Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/85/64/d3954e836ea67c4d3ad5285e5c8fd9d362fd0a189a2db622df457b0f4f6a/wrapt-2.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:9ca66b38dd642bf90c59b6738af8070747b610115a39af2498535f62b5cdc1c3", size = 60500, upload-time = "2025-11-07T00:44:15.561Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/89/4e/3c8b99ac93527cfab7f116089db120fef16aac96e5f6cdb724ddf286086d/wrapt-2.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:5a4939eae35db6b6cec8e7aa0e833dcca0acad8231672c26c2a9ab7a0f8ac9c8", size = 58993, upload-time = "2025-11-07T00:44:16.65Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/f9/f4/eff2b7d711cae20d220780b9300faa05558660afb93f2ff5db61fe725b9a/wrapt-2.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a52f93d95c8d38fed0669da2ebdb0b0376e895d84596a976c15a9eb45e3eccb3", size = 82028, upload-time = "2025-11-07T00:44:18.944Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/0c/67/cb945563f66fd0f61a999339460d950f4735c69f18f0a87ca586319b1778/wrapt-2.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e54bbf554ee29fcceee24fa41c4d091398b911da6e7f5d7bffda963c9aed2e1", size = 62949, upload-time = "2025-11-07T00:44:20.074Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/ec/ca/f63e177f0bbe1e5cf5e8d9b74a286537cd709724384ff20860f8f6065904/wrapt-2.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:908f8c6c71557f4deaa280f55d0728c3bca0960e8c3dd5ceeeafb3c19942719d", size = 63681, upload-time = "2025-11-07T00:44:21.345Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/39/a1/1b88fcd21fd835dca48b556daef750952e917a2794fa20c025489e2e1f0f/wrapt-2.0.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e2f84e9af2060e3904a32cea9bb6db23ce3f91cfd90c6b426757cf7cc01c45c7", size = 152696, upload-time = "2025-11-07T00:44:24.318Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/62/1c/d9185500c1960d9f5f77b9c0b890b7fc62282b53af7ad1b6bd779157f714/wrapt-2.0.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3612dc06b436968dfb9142c62e5dfa9eb5924f91120b3c8ff501ad878f90eb3", size = 158859, upload-time = "2025-11-07T00:44:25.494Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/91/60/5d796ed0f481ec003220c7878a1d6894652efe089853a208ea0838c13086/wrapt-2.0.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d2d947d266d99a1477cd005b23cbd09465276e302515e122df56bb9511aca1b", size = 146068, upload-time = "2025-11-07T00:44:22.81Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/04/f8/75282dd72f102ddbfba137e1e15ecba47b40acff32c08ae97edbf53f469e/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7d539241e87b650cbc4c3ac9f32c8d1ac8a54e510f6dca3f6ab60dcfd48c9b10", size = 155724, upload-time = "2025-11-07T00:44:26.634Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/5a/27/fe39c51d1b344caebb4a6a9372157bdb8d25b194b3561b52c8ffc40ac7d1/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4811e15d88ee62dbf5c77f2c3ff3932b1e3ac92323ba3912f51fc4016ce81ecf", size = 144413, upload-time = "2025-11-07T00:44:27.939Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/83/2b/9f6b643fe39d4505c7bf926d7c2595b7cb4b607c8c6b500e56c6b36ac238/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c1c91405fcf1d501fa5d55df21e58ea49e6b879ae829f1039faaf7e5e509b41e", size = 150325, upload-time = "2025-11-07T00:44:29.29Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/bb/b6/20ffcf2558596a7f58a2e69c89597128781f0b88e124bf5a4cadc05b8139/wrapt-2.0.1-cp313-cp313t-win32.whl", hash = "sha256:e76e3f91f864e89db8b8d2a8311d57df93f01ad6bb1e9b9976d1f2e83e18315c", size = 59943, upload-time = "2025-11-07T00:44:33.211Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/87/6a/0e56111cbb3320151eed5d3821ee1373be13e05b376ea0870711f18810c3/wrapt-2.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:83ce30937f0ba0d28818807b303a412440c4b63e39d3d8fc036a94764b728c92", size = 63240, upload-time = "2025-11-07T00:44:30.935Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/1d/54/5ab4c53ea1f7f7e5c3e7c1095db92932cc32fd62359d285486d00c2884c3/wrapt-2.0.1-cp313-cp313t-win_arm64.whl", hash = "sha256:4b55cacc57e1dc2d0991dbe74c6419ffd415fb66474a02335cb10efd1aa3f84f", size = 60416, upload-time = "2025-11-07T00:44:32.002Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/73/81/d08d83c102709258e7730d3cd25befd114c60e43ef3891d7e6877971c514/wrapt-2.0.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5e53b428f65ece6d9dad23cb87e64506392b720a0b45076c05354d27a13351a1", size = 78290, upload-time = "2025-11-07T00:44:34.691Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/f6/14/393afba2abb65677f313aa680ff0981e829626fed39b6a7e3ec807487790/wrapt-2.0.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ad3ee9d0f254851c71780966eb417ef8e72117155cff04821ab9b60549694a55", size = 61255, upload-time = "2025-11-07T00:44:35.762Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/c4/10/a4a1f2fba205a9462e36e708ba37e5ac95f4987a0f1f8fd23f0bf1fc3b0f/wrapt-2.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7b822c61ed04ee6ad64bc90d13368ad6eb094db54883b5dde2182f67a7f22c0", size = 61797, upload-time = "2025-11-07T00:44:37.22Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/12/db/99ba5c37cf1c4fad35349174f1e38bd8d992340afc1ff27f526729b98986/wrapt-2.0.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7164a55f5e83a9a0b031d3ffab4d4e36bbec42e7025db560f225489fa929e509", size = 120470, upload-time = "2025-11-07T00:44:39.425Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/30/3f/a1c8d2411eb826d695fc3395a431757331582907a0ec59afce8fe8712473/wrapt-2.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e60690ba71a57424c8d9ff28f8d006b7ad7772c22a4af432188572cd7fa004a1", size = 122851, upload-time = "2025-11-07T00:44:40.582Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/b3/8d/72c74a63f201768d6a04a8845c7976f86be6f5ff4d74996c272cefc8dafc/wrapt-2.0.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3cd1a4bd9a7a619922a8557e1318232e7269b5fb69d4ba97b04d20450a6bf970", size = 117433, upload-time = "2025-11-07T00:44:38.313Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/c7/5a/df37cf4042cb13b08256f8e27023e2f9b3d471d553376616591bb99bcb31/wrapt-2.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b4c2e3d777e38e913b8ce3a6257af72fb608f86a1df471cb1d4339755d0a807c", size = 121280, upload-time = "2025-11-07T00:44:41.69Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/54/34/40d6bc89349f9931e1186ceb3e5fbd61d307fef814f09fbbac98ada6a0c8/wrapt-2.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3d366aa598d69416b5afedf1faa539fac40c1d80a42f6b236c88c73a3c8f2d41", size = 116343, upload-time = "2025-11-07T00:44:43.013Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/70/66/81c3461adece09d20781dee17c2366fdf0cb8754738b521d221ca056d596/wrapt-2.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c235095d6d090aa903f1db61f892fffb779c1eaeb2a50e566b52001f7a0f66ed", size = 119650, upload-time = "2025-11-07T00:44:44.523Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/46/3a/d0146db8be8761a9e388cc9cc1c312b36d583950ec91696f19bbbb44af5a/wrapt-2.0.1-cp314-cp314-win32.whl", hash = "sha256:bfb5539005259f8127ea9c885bdc231978c06b7a980e63a8a61c8c4c979719d0", size = 58701, upload-time = "2025-11-07T00:44:48.277Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/1a/38/5359da9af7d64554be63e9046164bd4d8ff289a2dd365677d25ba3342c08/wrapt-2.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:4ae879acc449caa9ed43fc36ba08392b9412ee67941748d31d94e3cedb36628c", size = 60947, upload-time = "2025-11-07T00:44:46.086Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/aa/3f/96db0619276a833842bf36343685fa04f987dd6e3037f314531a1e00492b/wrapt-2.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:8639b843c9efd84675f1e100ed9e99538ebea7297b62c4b45a7042edb84db03e", size = 59359, upload-time = "2025-11-07T00:44:47.164Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/71/49/5f5d1e867bf2064bf3933bc6cf36ade23505f3902390e175e392173d36a2/wrapt-2.0.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:9219a1d946a9b32bb23ccae66bdb61e35c62773ce7ca6509ceea70f344656b7b", size = 82031, upload-time = "2025-11-07T00:44:49.4Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/2b/89/0009a218d88db66ceb83921e5685e820e2c61b59bbbb1324ba65342668bc/wrapt-2.0.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fa4184e74197af3adad3c889a1af95b53bb0466bced92ea99a0c014e48323eec", size = 62952, upload-time = "2025-11-07T00:44:50.74Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/ae/18/9b968e920dd05d6e44bcc918a046d02afea0fb31b2f1c80ee4020f377cbe/wrapt-2.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c5ef2f2b8a53b7caee2f797ef166a390fef73979b15778a4a153e4b5fedce8fa", size = 63688, upload-time = "2025-11-07T00:44:52.248Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/a6/7d/78bdcb75826725885d9ea26c49a03071b10c4c92da93edda612910f150e4/wrapt-2.0.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e042d653a4745be832d5aa190ff80ee4f02c34b21f4b785745eceacd0907b815", size = 152706, upload-time = "2025-11-07T00:44:54.613Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/dd/77/cac1d46f47d32084a703df0d2d29d47e7eb2a7d19fa5cbca0e529ef57659/wrapt-2.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2afa23318136709c4b23d87d543b425c399887b4057936cd20386d5b1422b6fa", size = 158866, upload-time = "2025-11-07T00:44:55.79Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/8a/11/b521406daa2421508903bf8d5e8b929216ec2af04839db31c0a2c525eee0/wrapt-2.0.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c72328f668cf4c503ffcf9434c2b71fdd624345ced7941bc6693e61bbe36bef", size = 146148, upload-time = "2025-11-07T00:44:53.388Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/0c/c0/340b272bed297baa7c9ce0c98ef7017d9c035a17a6a71dce3184b8382da2/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3793ac154afb0e5b45d1233cb94d354ef7a983708cc3bb12563853b1d8d53747", size = 155737, upload-time = "2025-11-07T00:44:56.971Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/f3/93/bfcb1fb2bdf186e9c2883a4d1ab45ab099c79cbf8f4e70ea453811fa3ea7/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fec0d993ecba3991645b4857837277469c8cc4c554a7e24d064d1ca291cfb81f", size = 144451, upload-time = "2025-11-07T00:44:58.515Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/d2/6b/dca504fb18d971139d232652656180e3bd57120e1193d9a5899c3c0b7cdd/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:949520bccc1fa227274da7d03bf238be15389cd94e32e4297b92337df9b7a349", size = 150353, upload-time = "2025-11-07T00:44:59.753Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/1d/f6/a1de4bd3653afdf91d250ca5c721ee51195df2b61a4603d4b373aa804d1d/wrapt-2.0.1-cp314-cp314t-win32.whl", hash = "sha256:be9e84e91d6497ba62594158d3d31ec0486c60055c49179edc51ee43d095f79c", size = 60609, upload-time = "2025-11-07T00:45:03.315Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/01/3a/07cd60a9d26fe73efead61c7830af975dfdba8537632d410462672e4432b/wrapt-2.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:61c4956171c7434634401db448371277d07032a81cc21c599c22953374781395", size = 64038, upload-time = "2025-11-07T00:45:00.948Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/41/99/8a06b8e17dddbf321325ae4eb12465804120f699cd1b8a355718300c62da/wrapt-2.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:35cdbd478607036fee40273be8ed54a451f5f23121bd9d4be515158f9498f7ad", size = 60634, upload-time = "2025-11-07T00:45:02.087Z" },
{ url = "https://pkgs.safetycli.com/package/techlabssh/pypi/packages/15/d1/b51471c11592ff9c012bd3e2f7334a6ff2f42a7aed2caffcf0bdddc9cb89/wrapt-2.0.1-py3-none-any.whl", hash = "sha256:4d2ce1bf1a48c5277d7969259232b57645aae5686dba1eaeade39442277afbca", size = 44046, upload-time = "2025-11-07T00:45:32.116Z" },
]
[[package]]
name = "zipp"
version = "3.23.0"