Repository: blobtoolkit/blobtoolkit (language: python)

Issue:
SVG files not displayed properly in Ai, Gimp, Inkscape, and Corel Vector
Hi, 
I need to use the svg files generated by BlobToolKit to be able to adjust the font size, as it is quite small in the plots. 
However, the svg files are not properly displayed in any graphics program I've tried. 

For example, when I try to open the snail plot in Adobe Illustrator 2023 (similar in Inkscape), I get the message that the path text is reduced, and then I get an "Open Sans: unknown error" message followed by this image:
<img width="737" alt="Bildschirmfoto 2022-12-14 um 13 43 15" src="https://user-images.githubusercontent.com/36346531/207599132-42dfa555-4e5c-4318-b72f-8309a309cc30.png">

I can remove the main black overlay, but then there is still an issue with the GC coloring for small scaffolds:
<img width="598" alt="Bildschirmfoto 2022-12-14 um 13 48 52" src="https://user-images.githubusercontent.com/36346531/207599477-ff2d417a-650b-42a8-aa9d-c03573e4b964.png">

Gimp removes all labels on the plot, and Corel Vector, for whatever reason, messes up the label numbers and placement:
<img width="795" alt="Bildschirmfoto 2022-12-14 um 13 52 48" src="https://user-images.githubusercontent.com/36346531/207600339-28e45673-7d59-4afc-90c4-afdd669a9540.png">

Thanks for your help!

Sven 
Feature/split-packaging



Discussion / hints:

Fix patch:
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index f19954cd..b0056a9b 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,9 +1,7 @@
 [bumpversion]
-current_version = 3.4.0
-commit = True
-tag = True
-tag_name = {new_version}
-message = "Bump version: {current_version} → {new_version}"
+current_version = 4.0.2
+commit = False
+tag = False
 
 [bumpversion:file:setup.py]
 search = version="{current_version}"
@@ -17,42 +15,18 @@ replace = blobtoolkit v{new_version}
 search = blobtoolkit v{current_version}
 replace = blobtoolkit v{new_version}
 
-[bumpversion:file:src/pipeline/lib/version.py]
-search = blobtoolkit v{current_version}
-replace = blobtoolkit v{new_version}
-
 [bumpversion:file:README.md]
 search = v{current_version}
 replace = v{new_version}
 
-[bumpversion:file:src/viewer/README.md]
-search = v{current_version}
-replace = v{new_version}
-
 [bumpversion:file:src/docker/env.yaml]
-search = blobtoolkit=={current_version}
-replace = blobtoolkit=={new_version}
+search = blobtoolkit[full]=={current_version}
+replace = blobtoolkit[full]=={new_version}
 
 [bumpversion:file:src/docker/blobtools/env.yaml]
 search = blobtoolkit=={current_version}
 replace = blobtoolkit=={new_version}
 
-[bumpversion:file:src/docker/Dockerfile]
-search = ARG VERSION={current_version}
-replace = ARG VERSION={new_version}
-
-[bumpversion:file:src/docker/api/Dockerfile]
-search = ARG VERSION={current_version}
-replace = ARG VERSION={new_version}
-
-[bumpversion:file:src/docker/blobtools/Dockerfile]
-search = ARG VERSION={current_version}
-replace = ARG VERSION={new_version}
-
-[bumpversion:file:src/docker/viewer/Dockerfile]
-search = ARG VERSION={current_version}
-replace = ARG VERSION={new_version}
-
-[bumpversion:file:.github/workflows/package-blobtoolkit.yml]
+[bumpversion:file:.github/workflows/changes.yml]
 search = VERSION: {current_version}
 replace = VERSION: {new_version}
diff --git a/.github/workflows/build-api.yml b/.github/workflows/build-api.yml
new file mode 100644
index 00000000..c5d22819
--- /dev/null
+++ b/.github/workflows/build-api.yml
@@ -0,0 +1,44 @@
+name: build-api
+
+on:
+  workflow_call:
+    inputs:
+      container-version:
+        required: true
+        type: string
+      docker-tags:
+        required: true
+        type: string
+
+jobs:
+  build-api:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+      - name: Collect artifacts
+        uses: actions/download-artifact@v3
+        with:
+          name: node
+          path: ./dist
+      - name: Move API
+        run: |
+          mv ./dist/blobtoolkit-api-linux src/docker/viewer/
+      - name: Login to Docker Hub
+        uses: docker/login-action@v2
+        with:
+          username: ${{ secrets.DOCKER_HUB_USERNAME }}
+          password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
+      - name: Extract metadata (tags, labels) for Docker
+        id: meta
+        uses: docker/metadata-action@v4
+        with:
+          images: genomehubs/blobtoolkit-api
+      - name: Build and push Docker image
+        uses: docker/build-push-action@v3
+        with:
+          context: src/docker/api
+          push: true
+          build-args: VERSION=${{ inputs.container-version }}
+          tags: ${{ inputs.docker-tags }}
+          labels: ${{ steps.meta.outputs.labels }}
diff --git a/.github/workflows/build-blobtoolkit.yml b/.github/workflows/build-blobtoolkit.yml
new file mode 100644
index 00000000..555c4f5c
--- /dev/null
+++ b/.github/workflows/build-blobtoolkit.yml
@@ -0,0 +1,44 @@
+name: build-blobtoolkit
+
+on:
+  workflow_call:
+    inputs:
+      container-version:
+        required: true
+        type: string
+      docker-tags:
+        required: true
+        type: string
+
+jobs:
+  build-blobtoolkit:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+      - name: Collect artifacts
+        uses: actions/download-artifact@v3
+        with:
+          path: ./dist
+      - name: Move artifacts
+        run: |
+          mv dist/node/*-linux src/docker/
+          mv dist/pip/*-${{ inputs.container-version }}-*linux*.whl src/docker/
+      - name: Login to Docker Hub
+        uses: docker/login-action@v2
+        with:
+          username: ${{ secrets.DOCKER_HUB_USERNAME }}
+          password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
+      - name: Extract metadata (tags, labels) for Docker
+        id: meta
+        uses: docker/metadata-action@v4
+        with:
+          images: genomehubs/blobtoolkit
+      - name: Build and push Docker image
+        uses: docker/build-push-action@v3
+        with:
+          context: src/docker
+          build-args: VERSION=${{ inputs.container-version }}
+          push: true
+          tags: ${{ inputs.docker-tags }}
+          labels: ${{ steps.meta.outputs.labels }}
diff --git a/.github/workflows/build-ui.yml b/.github/workflows/build-ui.yml
new file mode 100644
index 00000000..16b517e8
--- /dev/null
+++ b/.github/workflows/build-ui.yml
@@ -0,0 +1,44 @@
+name: build-ui
+
+on:
+  workflow_call:
+    inputs:
+      container-version:
+        required: true
+        type: string
+      docker-tags:
+        required: true
+        type: string
+
+jobs:
+  build-ui:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+      - name: Collect artifacts
+        uses: actions/download-artifact@v3
+        with:
+          name: node
+          path: ./dist
+      - name: Move UI
+        run: |
+          mv ./dist/blobtoolkit-viewer-linux src/docker/viewer/
+      - name: Login to Docker Hub
+        uses: docker/login-action@v2
+        with:
+          username: ${{ secrets.DOCKER_HUB_USERNAME }}
+          password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
+      - name: Extract metadata (tags, labels) for Docker
+        id: meta
+        uses: docker/metadata-action@v4
+        with:
+          images: genomehubs/blobtoolkit-viewer
+      - name: Build and push Docker image
+        uses: docker/build-push-action@v3
+        with:
+          context: src/docker/viewer
+          push: true
+          build-args: VERSION=${{ inputs.container-version }}
+          tags: ${{ inputs.docker-tags }}
+          labels: ${{ steps.meta.outputs.labels }}
diff --git a/.github/workflows/changes.yml b/.github/workflows/changes.yml
new file mode 100644
index 00000000..33d86f50
--- /dev/null
+++ b/.github/workflows/changes.yml
@@ -0,0 +1,213 @@
+name: detect-changes
+
+env:
+  DEFAULT_PYTHON: '["3.9"]'
+  #MATRIX_PYTHON: '["3.9"]'
+  MATRIX_PYTHON: '["3.8","3.9","3.10","3.11"]'
+  DEFAULT_PLATFORM: '["linux"]'
+  MATRIX_PLATFORM: '["linux", "macos"]'
+  BASE_TAG: "genomehubs/blobtoolkit"
+  VERSION: 4.0.2
+
+on:
+  workflow_call:
+    outputs:
+      api:
+        value: ${{ jobs.detect-changes.outputs.api }}
+      ui:
+        value: ${{ jobs.detect-changes.outputs.ui }}
+      node:
+        value: ${{ jobs.detect-changes.outputs.node }}
+      host:
+        value: ${{ jobs.detect-changes.outputs.host }}
+      pipeline:
+        value: ${{ jobs.detect-changes.outputs.pipeline }}
+      blobtools:
+        value: ${{ jobs.detect-changes.outputs.blobtools }}
+      docker:
+        value: ${{ jobs.detect-changes.outputs.docker }}
+      all:
+        value: ${{ jobs.detect-changes.outputs.all }}
+      is-api-release:
+        value: ${{ jobs.is-release.outputs.host }}
+      is-ui-release:
+        value: ${{ jobs.is-release.outputs.host }}
+      is-node-release:
+        value: ${{ jobs.is-release.outputs.host }}
+      is-host-release:
+        value: ${{ jobs.is-release.outputs.host }}
+      is-pipeline-release:
+        value: ${{ jobs.is-release.outputs.host }}
+      is-blobtools-release:
+        value: ${{ jobs.is-release.outputs.host }}
+      requires-node:
+        value: ${{ jobs.release-dependent.outputs.node }}
+      platform-matrix:
+        value: ${{ jobs.release-dependent.outputs.platform-matrix }}
+      host-matrix:
+        value: ${{ jobs.release-dependent.outputs.host-matrix }}
+      pipeline-matrix:
+        value: ${{ jobs.release-dependent.outputs.pipeline-matrix }}
+      blobtools-matrix:
+        value: ${{ jobs.release-dependent.outputs.blobtools-matrix }}
+      container-version:
+        value: ${{ jobs.release-dependent.outputs.container-version }}
+      blobtoolkit-tag:
+        value: ${{ jobs.release-dependent.outputs.blobtoolkit-tag }}
+      blobtools-tag:
+        value: ${{ jobs.release-dependent.outputs.blobtools-tag }}
+      api-tag:
+        value: ${{ jobs.release-dependent.outputs.api-tag }}
+      ui-tag:
+        value: ${{ jobs.release-dependent.outputs.ui-tag }}
+
+jobs:
+  detect-changes:
+    name: Detect changes
+    runs-on: ubuntu-latest
+    outputs:
+      api: ${{ steps.filter.outputs.api }}
+      ui: ${{ steps.filter.outputs.ui }}
+      node: ${{ steps.filter.outputs.node }}
+      host: ${{ steps.filter.outputs.host }}
+      pipeline: ${{ steps.filter.outputs.pipeline }}
+      blobtools: ${{ steps.filter.outputs.blobtools }}
+      docker: ${{ steps.filter.outputs.docker }}
+      all: ${{ steps.filter.outputs.all }}
+    steps:
+      - uses: actions/checkout@v3
+      - uses: dorny/paths-filter@v2
+        id: filter
+        with:
+          # base: develop
+          filters: |
+            api:
+              - 'src/api/**'
+            ui:
+              - 'src/viewer/**'
+              - 'src/packaged-viewer/**'
+            node:
+              - 'src/api/**'
+              - 'src/viewer/**'
+              - 'src/packaged-viewer/**'
+            host:
+              - 'src/blobtoolkit-host/**'
+            pipeline:
+              - 'src/blobtoolkit-pipeline/**'
+            blobtools:
+              - 'src/blobtoolkit-host/**'
+              - 'src/blobtoolkit-pipeline/**'
+              - 'src/blobtools/**'
+              - 'src/btk/**'
+            docker:
+              - 'src/docker/**'
+            blobtoolkit:
+              - 'src/**'
+
+  is-release:
+    needs: detect-changes
+    runs-on: ubuntu-latest
+    outputs:
+      api: ${{ steps.release.outputs.api || 'false' }}
+      ui: ${{ steps.release.outputs.ui || 'false' }}
+      node: ${{ steps.release.outputs.node || 'false' }}
+      host: ${{ steps.release.outputs.host || 'false' }}
+      pipeline: ${{ steps.release.outputs.pipeline || 'false' }}
+      blobtools: ${{ steps.release.outputs.blobtools || 'false' }}
+    if: startsWith(github.ref, 'refs/tags/')
+    steps:
+      - id: release
+        run: |
+          if ${{ needs.detect-changes.outputs.api == 'true' }}; then
+            echo "api=true" >> $GITHUB_OUTPUT
+          fi
+          if ${{ needs.detect-changes.outputs.ui == 'true' }}; then
+            echo "ui=true" >> $GITHUB_OUTPUT
+          fi
+          if ${{ needs.detect-changes.outputs.node == 'true' }}; then
+            echo "node=true" >> $GITHUB_OUTPUT
+          fi
+          if ${{ needs.detect-changes.outputs.host == 'true' }}; then
+            echo "host=true" >> $GITHUB_OUTPUT
+          fi
+          if ${{ needs.detect-changes.outputs.pipeline == 'true' }}; then
+            echo "pipeline=true" >> $GITHUB_OUTPUT
+          fi
+          if ${{ needs.detect-changes.outputs.blobtools == 'true' }}; then
+            echo "blobtools=true" >> $GITHUB_OUTPUT
+          fi
+
+  release-dependent:
+    needs:
+      - detect-changes
+      - is-release
+    if: |
+      always() &&
+      !contains(needs.*.result, 'failure') &&
+      !contains(needs.*.result, 'cancelled')
+    runs-on: ubuntu-latest
+    outputs:
+      node: ${{ steps.needs-node.outputs.node || 'false' }}
+      platform-matrix: ${{ steps.platform-matrix.outputs.matrix }}
+      host-matrix: ${{ steps.host-matrix.outputs.matrix }}
+      pipeline-matrix: ${{ steps.pipeline-matrix.outputs.matrix }}
+      blobtools-matrix: ${{ steps.blobtools-matrix.outputs.matrix }}
+      blobtoolkit-tag: ${{ steps.docker-tags.outputs.blobtoolkit-tag }}
+      blobtools-tag: ${{ steps.docker-tags.outputs.blobtools-tag }}
+      api-tag: ${{ steps.docker-tags.outputs.api-tag }}
+      ui-tag: ${{ steps.docker-tags.outputs.ui-tag }}
+      container-version: ${{ steps.docker-tags.outputs.container-version }}
+    steps:
+      - id: needs-node
+        run: |
+          if [[ "${{ needs.detect-changes.outputs.node == 'true' }}" == "true" || "${{ needs.is-release.outputs.host == 'true' }}" == "true" ]]; then
+            echo "node=true" >> $GITHUB_OUTPUT
+          fi
+      - id: platform-matrix
+        run: |
+          if ${{ needs.is-release.outputs.blobtools == 'true' }}; then
+            echo "matrix=${MATRIX_PLATFORM}" >> $GITHUB_OUTPUT
+          else
+            echo "matrix=${DEFAULT_PLATFORM}" >> $GITHUB_OUTPUT
+          fi
+      - id: host-matrix
+        run: |
+          if ${{ needs.is-release.outputs.host == 'true' }}; then
+            echo "matrix=${MATRIX_PYTHON}" >> $GITHUB_OUTPUT
+          else
+            echo "matrix=${DEFAULT_PYTHON}" >> $GITHUB_OUTPUT
+          fi
+      - id: pipeline-matrix
+        run: |
+          if ${{ needs.is-release.outputs.pipeline == 'true' }}; then
+            echo "matrix=${MATRIX_PYTHON}" >> $GITHUB_OUTPUT
+          else
+            echo "matrix=${DEFAULT_PYTHON}" >> $GITHUB_OUTPUT
+          fi
+      - id: blobtools-matrix
+        run: |
+          if ${{ needs.is-release.outputs.blobtools == 'true' }}; then
+            echo "matrix=${MATRIX_PYTHON}" >> $GITHUB_OUTPUT
+          else
+            echo "matrix=${DEFAULT_PYTHON}" >> $GITHUB_OUTPUT
+          fi
+      - id: docker-tags
+        run: |
+          echo "container-version=${VERSION}" >> $GITHUB_OUTPUT
+          if ${{ needs.is-release.outputs.blobtools == 'true' }}; then
+            echo "blobtoolkit-tag=${BASE_TAG}:${VERSION},${BASE_TAG}:latest" >> $GITHUB_OUTPUT
+            echo "blobtools-tag=${BASE_TAG}-blobtools:${VERSION},${BASE_TAG}-blobtools:latest" >> $GITHUB_OUTPUT
+          else
+            echo "blobtoolkit-tag=${BASE_TAG}:develop" >> $GITHUB_OUTPUT
+            echo "blobtools-tag=${BASE_TAG}-blobtools:develop" >> $GITHUB_OUTPUT
+          fi
+          if ${{ needs.is-release.outputs.api == 'true' }}; then
+            echo "api-tag=${BASE_TAG}-api:${VERSION},${BASE_TAG}-blobtools:latest" >> $GITHUB_OUTPUT
+          else
+            echo "api-tag=${BASE_TAG}-api:develop" >> $GITHUB_OUTPUT
+          fi
+          if ${{ needs.is-release.outputs.ui == 'true' }}; then
+            echo "ui-tag=${BASE_TAG}-viewer:${VERSION},${BASE_TAG}-viewer:latest" >> $GITHUB_OUTPUT
+          else
+            echo "ui-tag=${BASE_TAG}-viewer:develop" >> $GITHUB_OUTPUT
+          fi
diff --git a/.github/workflows/conditional-workflow.yml b/.github/workflows/conditional-workflow.yml
new file mode 100644
index 00000000..d5d3c761
--- /dev/null
+++ b/.github/workflows/conditional-workflow.yml
@@ -0,0 +1,164 @@
+name: package-blobtoolkit
+
+on:
+  push:
+
+jobs:
+  changes:
+    uses: ./.github/workflows/changes.yml
+
+  package-api:
+    needs: changes
+    if: |
+      needs.changes.outputs.api == 'true' ||
+      needs.changes.outputs.requires-node == 'true'
+    uses: ./.github/workflows/package-api.yml
+
+  build-api:
+    needs:
+      - changes
+      - package-api
+    # if: needs.changes.outputs.is-api-release == 'true'
+    secrets: inherit
+    uses: ./.github/workflows/build-api.yml
+    with:
+      container-version: ${{ needs.changes.outputs.container-version }}
+      docker-tags: ${{ needs.changes.outputs.api-tag }}
+
+  package-ui:
+    needs: changes
+    if: |
+      needs.changes.outputs.ui == 'true' ||
+      needs.changes.outputs.requires-node == 'true'
+    secrets: inherit
+    uses: ./.github/workflows/package-ui.yml
+
+  build-ui:
+    needs:
+      - changes
+      - package-ui
+    # if: needs.changes.outputs.is-ui-release == 'true'
+    secrets: inherit
+    uses: ./.github/workflows/build-ui.yml
+    with:
+      container-version: ${{ needs.changes.outputs.container-version }}
+      docker-tags: ${{ needs.changes.outputs.ui-tag }}
+
+  package-host:
+    needs:
+      - changes
+      - package-ui
+      - package-api
+    strategy:
+      matrix:
+        python-version: ${{ fromJSON(needs.changes.outputs.host-matrix) }}
+        platform: ${{ fromJSON(needs.changes.outputs.platform-matrix) }}
+    if: |
+      always() &&
+      !contains(needs.*.result, 'failure') &&
+      !contains(needs.*.result, 'cancelled') &&
+      needs.changes.outputs.host == 'true'
+    uses: ./.github/workflows/package-host.yml
+    with:
+      python-version: ${{ matrix.python-version }}
+      platform: ${{ matrix.platform }}
+
+  release-host:
+    needs:
+      - changes
+      - package-host
+    if: needs.changes.outputs.is-host-release == 'true'
+    secrets: inherit
+    uses: ./.github/workflows/upload-to-pypi.yml
+    with:
+      component: blobtoolkit_host
+      version: ${{ needs.changes.outputs.container-version }}
+
+  package-pipeline:
+    needs: changes
+    strategy:
+      matrix:
+        python-version: ${{ fromJSON(needs.changes.outputs.pipeline-matrix) }}
+        platform: ${{ fromJSON(needs.changes.outputs.platform-matrix) }}
+    if: needs.changes.outputs.pipeline == 'true'
+    uses: ./.github/workflows/package-pipeline.yml
+    with:
+      python-version: ${{ matrix.python-version }}
+      platform: ${{ matrix.platform }}
+
+  release-pipeline:
+    needs:
+      - changes
+      - package-host
+      - package-pipeline
+    if: |
+      always() &&
+      !contains(needs.*.result, 'failure') &&
+      !contains(needs.*.result, 'cancelled') &&
+      needs.changes.outputs.is-pipeline-release == 'true'
+    secrets: inherit
+    uses: ./.github/workflows/upload-to-pypi.yml
+    with:
+      component: blobtoolkit_pipeline
+      version: ${{ needs.changes.outputs.container-version }}
+
+  package-blobtools:
+    needs: changes
+    strategy:
+      matrix:
+        python-version: ${{ fromJSON(needs.changes.outputs.blobtools-matrix) }}
+        platform: ${{ fromJSON(needs.changes.outputs.platform-matrix) }}
+    if: needs.changes.outputs.blobtools == 'true'
+    uses: ./.github/workflows/package-blobtools.yml
+    with:
+      python-version: ${{ matrix.python-version }}
+      platform: ${{ matrix.platform }}
+
+  release-blobtools:
+    needs:
+      - changes
+      - package-blobtools
+      - release-host
+      - release-pipeline
+    if: |
+      always() &&
+      !contains(needs.*.result, 'failure') &&
+      !contains(needs.*.result, 'cancelled') &&
+      needs.changes.outputs.is-host-release == 'true'
+    uses: ./.github/workflows/upload-to-pypi.yml
+    secrets: inherit
+    with:
+      component: blobtoolkit
+      version: ${{ needs.changes.outputs.container-version }}
+
+  build-blobtoolkit:
+    needs:
+      - changes
+      - package-ui
+      - package-api
+      - package-host
+      - package-pipeline
+      - package-blobtools
+    # if: needs.changes.outputs.is-blobtools-release == 'true'
+    secrets: inherit
+    uses: ./.github/workflows/build-blobtoolkit.yml
+    with:
+      container-version: ${{ needs.changes.outputs.container-version }}
+      docker-tags: ${{ needs.changes.outputs.blobtoolkit-tag }}
+
+  create-github-release:
+    needs:
+      - package-api
+      - package-ui
+      - release-blobtools
+    if: |
+      always() &&
+      !contains(needs.*.result, 'failure') &&
+      !contains(needs.*.result, 'cancelled') &&
+      (
+        needs.changes.outputs.is-api-release == 'true' ||
+        needs.changes.outputs.is-ui-release == 'true' ||
+        needs.changes.outputs.is-blobtools-release == 'true'
+      )
+    secrets: inherit
+    uses: ./.github/workflows/github-release.yml
diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml
new file mode 100644
index 00000000..7c32c89f
--- /dev/null
+++ b/.github/workflows/github-release.yml
@@ -0,0 +1,28 @@
+name: github-release
+on:
+  workflow_call:
+
+jobs:
+  github-release:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/download-artifact@v3
+        with:
+          path: ./tmp
+      - run: |
+          ls ./tmp
+          mkdir -p ./dist
+          mv tmp/node/blobtoolkit-api-* ./dist/
+          mv tmp/node/blobtoolkit-viewer-* ./dist/
+          mv tmp/node/blobtoolkit-viewer.tgz* ./dist/
+          ls ./dist
+      - name: create release
+        uses: "marvinpinto/action-automatic-releases@v1.2.1"
+        with:
+          repo_token: "${{ secrets.GITHUB_TOKEN }}"
+          draft: false
+          prerelease: false
+          title: "v${{github.ref_name}}"
+          files: |
+            dist/*
diff --git a/.github/workflows/package-api.yml b/.github/workflows/package-api.yml
new file mode 100644
index 00000000..b707de8a
--- /dev/null
+++ b/.github/workflows/package-api.yml
@@ -0,0 +1,18 @@
+name: package-api
+
+on: workflow_call
+
+jobs:
+  package-api:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/setup-node@v3
+        with:
+          node-version: "16"
+      - run: npm install -g pkg
+      - run: ./package-api.sh
+      - uses: actions/upload-artifact@v3
+        with:
+          name: node
+          path: ./dist/*
diff --git a/.github/workflows/package-blobtoolkit.yml b/.github/workflows/package-blobtoolkit.yml
deleted file mode 100644
index 7dc160fe..00000000
--- a/.github/workflows/package-blobtoolkit.yml
+++ /dev/null
@@ -1,258 +0,0 @@
-name: release-blobtoolkit
-env:
-  VERSION: 3.4.0
-
-on:
-  push:
-    tags:
-      - "*"
-
-jobs:
-  package-ui:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v3
-      - uses: actions/setup-node@v3
-        with:
-          node-version: "16"
-      - run: npm install -g pkg
-      - run: ./package-ui.sh
-      - uses: actions/upload-artifact@v3
-        with:
-          path: ./dist/*
-
-  package-api:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v3
-      - uses: actions/setup-node@v3
-        with:
-          node-version: "16"
-      - run: npm install -g pkg
-      - run: ./package-api.sh
-      - uses: actions/upload-artifact@v3
-        with:
-          path: ./dist/*
-
-  package-blobtoolkit-linux:
-    runs-on: ubuntu-latest
-    needs:
-      - package-ui
-      - package-api
-    steps:
-      - uses: actions/checkout@v3
-      - name: Collect artifacts
-        uses: actions/download-artifact@v3
-        with:
-          path: ./dist
-      - name: Install dependencies
-        run: |
-          python3 -m pip install --upgrade pip
-          pip install setuptools wheel twine
-      - name: Build and publish
-        run: |
-          mkdir -p src/data/bin
-          cp dist/artifact/*-linux src/data/bin/
-          rm -r dist/artifact
-          chmod 755 src/data/bin/*
-          ./pip_install_latest.sh manylinux2014_x86_64
-          blobtools -v
-      - uses: actions/upload-artifact@v3
-        with:
-          path: ./dist/*
-
-  package-blobtoolkit-macos:
-    runs-on: macos-11
-    needs:
-      - package-ui
-      - package-api
-    steps:
-      - uses: actions/checkout@v3
-      - name: Collect artifacts
-        uses: actions/download-artifact@v3
-        with:
-          path: ./dist
-      - name: Install dependencies
-        run: |
-          python3 -m pip install --upgrade pip
-          pip3 install setuptools wheel twine
-          pip3 install --upgrade cython
-      - name: Build and publish
-        run: |
-          mkdir -p src/data/bin
-          cp dist/artifact/*-macos src/data/bin/
-          rm -r dist/artifact
-          chmod 755 src/data/bin/*
-          python3 setup.py sdist bdist_wheel --python-tag py3 --plat-name macosx_11_0_x86_64
-          python3 setup.py sdist bdist_wheel --python-tag py3 --plat-name macosx_11_0_arm64
-          ./pip_install_latest.sh macosx_10_9_x86_64
-          blobtools -v
-      - uses: actions/upload-artifact@v3
-        with:
-          path: ./dist/*.whl
-
-  package-blobtoolkit-windows:
-    runs-on: ubuntu-latest
-    needs:
-      - package-ui
-      - package-api
-    steps:
-      - uses: actions/checkout@v3
-      - name: Collect artifacts
-        uses: actions/download-artifact@v3
-        with:
-          path: ./dist
-      - name: Install dependencies
-        run: |
-          python3 -m pip install --upgrade pip
-          pip install setuptools wheel twine
-      - name: Build and publish
-        run: |
-          mkdir -p src/data/bin
-          cp dist/artifact/*.exe src/data/bin/
-          rm -r dist/artifact
-          chmod 755 src/data/bin/*
-          python3 setup.py sdist bdist_wheel --python-tag py3 --plat-name win_amd64
-          ls dist
-      - uses: actions/upload-artifact@v3
-        with:
-          path: ./dist/*.whl
-
-  create-github-release:
-    runs-on: ubuntu-latest
-    needs:
-      - package-blobtoolkit-linux
-      - package-blobtoolkit-macos
-      - package-blobtoolkit-windows
-    steps:
-      - uses: actions/checkout@v3
-      - uses: actions/download-artifact@v3
-        with:
-          path: ./dist
-      - name: create release
-        uses: "marvinpinto/action-automatic-releases@v1.2.1"
-        with:
-          repo_token: "${{ secrets.GITHUB_TOKEN }}"
-          draft: false
-          prerelease: false
-          title: "v${{github.ref_name}}"
-          files: |
-            dist/artifact/blobtoolkit-api-*
-            dist/artifact/blobtoolkit-viewer-*
-            dist/artifact/blobtoolkit-viewer.tgz*
-
-  upload-to-pypi:
-    runs-on: ubuntu-latest
-    needs: create-github-release
-    steps:
-      - uses: actions/checkout@v3
-      - uses: actions/download-artifact@v3
-        with:
-          path: ./dist
-      - run: |
-          mv dist/artifact/*.whl dist/
-          mv dist/artifact/*.tar.gz dist/
-          rm -r dist/artifact
-          ls -lh dist
-      - name: Publish package to PyPI
-        uses: pypa/gh-action-pypi-publish@v1.5.0
-        with:
-          user: __token__
-          password: ${{ secrets.PYPI_API_TOKEN }}
-          repository_url: https://upload.pypi.org/legacy/
-
-  build-and-push-blobtoolkit:
-    runs-on: ubuntu-latest
-    needs: upload-to-pypi
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v3
-      - name: Login to Docker Hub
-        uses: docker/login-action@v2
-        with:
-          username: ${{ secrets.DOCKER_HUB_USERNAME }}
-          password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
-      - name: Extract metadata (tags, labels) for Docker
-        id: meta
-        uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
-        with:
-          images: genomehubs/blobtoolkit
-      - name: Build and push Docker image
-        uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
-        with:
-          context: src/docker
-          push: true
-          tags: genomehubs/blobtoolkit:${{ env.VERSION }}, genomehubs/blobtoolkit:latest
-          labels: ${{ steps.meta.outputs.labels }}
-
-  build-and-push-blobtools:
-    runs-on: ubuntu-latest
-    needs: upload-to-pypi
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v3
-      - name: Login to Docker Hub
-        uses: docker/login-action@v2
-        with:
-          username: ${{ secrets.DOCKER_HUB_USERNAME }}
-          password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
-      - name: Extract metadata (tags, labels) for Docker
-        id: meta
-        uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
-        with:
-          images: genomehubs/blobtoolkit-blobtools
-      - name: Build and push Docker image
-        uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
-        with:
-          context: src/docker/blobtools
-          push: true
-          tags: genomehubs/blobtoolkit-blobtools:${{ env.VERSION }}, genomehubs/blobtoolkit-blobtools:latest
-          labels: ${{ steps.meta.outputs.labels }}
-
-  build-and-push-api:
-    runs-on: ubuntu-latest
-    needs: upload-to-pypi
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v3
-      - name: Login to Docker Hub
-        uses: docker/login-action@v2
-        with:
-          username: ${{ secrets.DOCKER_HUB_USERNAME }}
-          password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
-      - name: Extract metadata (tags, labels) for Docker
-        id: meta
-        uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
-        with:
-          images: genomehubs/blobtoolkit-api
-      - name: Build and push Docker image
-        uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
-        with:
-          context: src/docker/api
-          push: true
-          tags: genomehubs/blobtoolkit-api:${{ env.VERSION }}, genomehubs/blobtoolkit-api:latest
-          labels: ${{ steps.meta.outputs.labels }}
-
-  build-and-push-viewer:
-    runs-on: ubuntu-latest
-    needs: upload-to-pypi
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v3
-      - name: Login to Docker Hub
-        uses: docker/login-action@v2
-        with:
-          username: ${{ secrets.DOCKER_HUB_USERNAME }}
-          password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
-      - name: Extract metadata (tags, labels) for Docker
-        id: meta
-        uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
-        with:
-          images: genomehubs/blobtoolkit-viewer
-      - name: Build and push Docker image
-        uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
-        with:
-          context: src/docker/viewer
-          push: true
-          tags: genomehubs/blobtoolkit-viewer:${{ env.VERSION }}, genomehubs/blobtoolkit-viewer:latest
-          labels: ${{ steps.meta.outputs.labels }}
diff --git a/.github/workflows/package-blobtools.yml b/.github/workflows/package-blobtools.yml
new file mode 100644
index 00000000..5062ceeb
--- /dev/null
+++ b/.github/workflows/package-blobtools.yml
@@ -0,0 +1,66 @@
+name: package-pipeline
+on:
+  workflow_call:
+    inputs:
+      platform:
+        required: true
+        type: string
+      python-version:
+        required: true
+        type: string
+
+jobs:
+  package-python-blobtoolkit:
+    runs-on: ubuntu-latest
+    if: inputs.platform == 'linux'
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/setup-python@v4
+        with:
+          python-version: ${{ inputs.python-version }}
+      - name: Install dependencies
+        run: |
+          python3 -m pip install --upgrade pip
+          pip install setuptools wheel twine
+      - name: Build pip package
+        run: |
+          mkdir -p artifacts
+          PY_VERSION=${{ inputs.python-version }}
+          PY_VERSION=${PY_VERSION/./}
+          python3 setup.py sdist bdist_wheel --python-tag py${PY_VERSION} --plat-name manylinux2014_x86_64
+          if [ "${{ inputs.python-version }}" == "3.9" ]; then
+            pip3 install dist/blobtoolkit-*-py${PY_VERSION}-none-$PLATFORM.whl &&
+            blobtools -v
+            mv dist/blobtoolkit*.tar.gz artifacts/
+          fi
+          mv dist/*.whl artifacts/
+      - uses: actions/upload-artifact@v3
+        with:
+          name: pip
+          path: ./artifacts/*
+
+  package-python-blobtoolkit-macos:
+    runs-on: macos-11
+    if: inputs.platform == 'macos'
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/setup-python@v4
+        with:
+          python-version: ${{ inputs.python-version }}
+      - name: Install dependencies
+        run: |
+          python3 -m pip install --upgrade pip
+          pip3 install setuptools wheel twine
+          pip3 install --upgrade cython
+      - name: Build pip package
+        run: |
+          mkdir -p artifacts
+          PY_VERSION=${{ inputs.python-version }}
+          PY_VERSION=${PY_VERSION/./}
+          python3 setup.py sdist bdist_wheel --python-tag py${PY_VERSION} --plat-name macosx_11_0_x86_64
+          python3 setup.py sdist bdist_wheel --python-tag py${PY_VERSION} --plat-name macosx_11_0_arm64
+          mv dist/*.whl artifacts/
+      - uses: actions/upload-artifact@v3
+        with:
+          name: pip
+          path: ./artifacts/*
diff --git a/.github/workflows/package-host.yml b/.github/workflows/package-host.yml
new file mode 100644
index 00000000..ce80df84
--- /dev/null
+++ b/.github/workflows/package-host.yml
@@ -0,0 +1,86 @@
+name: package-host
+on:
+  workflow_call:
+    inputs:
+      platform:
+        required: true
+        type: string
+      python-version:
+        required: true
+        type: string
+
+jobs:
+  package-python-host:
+    runs-on: ubuntu-latest
+    if: inputs.platform == 'linux'
+    steps:
+      - uses: actions/checkout@v3
+      - name: Collect artifacts
+        uses: actions/download-artifact@v3
+        with:
+          name: node
+          path: ./dist
+      - uses: actions/setup-python@v4
+        with:
+          python-version: ${{ inputs.python-version }}
+      - name: Install dependencies
+        run: |
+          python3 -m pip install --upgrade pip
+          pip install setuptools wheel twine
+      - name: Move viewer
+        run: |
+          mkdir -p ./src/blobtoolkit-host/src/data/bin
+          mv ./dist/*-linux ./src/blobtoolkit-host/src/data/bin/ && chmod 755 ./src/blobtoolkit-host/src/data/bin/*-linux || exit 0
+      - name: Build pip package
+        working-directory: ./src/blobtoolkit-host
+        run: |
+          mkdir -p artifacts
+          PY_VERSION=${{ inputs.python-version }}
+          PY_VERSION=${PY_VERSION/./}
+          python3 setup.py sdist bdist_wheel --python-tag py${PY_VERSION} --plat-name manylinux2014_x86_64
+          if [ "${{ inputs.python-version }}" == "3.9" ]; then
+            pip3 install dist/blobtoolkit_host-*-py${PY_VERSION}-none-$PLATFORM.whl &&
+            blobtoolkit-host -v
+            mv dist/blobtoolkit*.tar.gz artifacts/
+          fi
+          mv dist/*.whl artifacts/
+      - uses: actions/upload-artifact@v3
+        with:
+          name: pip
+          path: ./src/blobtoolkit-host/artifacts/*
+
+  package-python-host-macos:
+    runs-on: macos-11
+    if: inputs.platform == 'macos'
+    steps:
+      - uses: actions/checkout@v3
+      - name: Collect artifacts
+        uses: actions/download-artifact@v3
+        with:
+          name: node
+          path: ./dist
+      - uses: actions/setup-python@v4
+        with:
+          python-version: ${{ inputs.python-version }}
+      - name: Install dependencies
+        run: |
+          python3 -m pip install --upgrade pip
+          pip3 install setuptools wheel twine
+          pip3 install --upgrade cython
+      - name: Move viewer
+        run: |
+          mkdir -p ./src/blobtoolkit-host/src/data/bin
+          mv ./dist/*-macos ./src/blobtoolkit-host/src/data/bin/ && chmod 755 ./src/blobtoolkit-host/src/data/bin/*-macos || exit 0
+      - name: Build pip package
+        working-directory: ./src/blobtoolkit-host
+        run: |
+          mkdir -p artifacts
+          PY_VERSION=${{ inputs.python-version }}
+          PY_VERSION=${PY_VERSION/./}
+          python3 setup.py sdist bdist_wheel --python-tag py${PY_VERSION} --plat-name macosx_11_0_x86_64
+          python3 setup.py sdist bdist_wheel --python-tag py${PY_VERSION} --plat-name macosx_11_0_arm64
+          mv dist/*.whl artifacts/
+      - uses: actions/upload-artifact@v3
+        with:
+          name: pip
+          path: ./src/blobtoolkit-host/artifacts/*
diff --git a/.github/workflows/package-pipeline.yml b/.github/workflows/package-pipeline.yml
new file mode 100644
index 00000000..7aa2da35
--- /dev/null
+++ b/.github/workflows/package-pipeline.yml
@@ -0,0 +1,68 @@
+name: package-pipeline
+on:
+  workflow_call:
+    inputs:
+      platform:
+        required: true
+        type: string
+      python-version:
+        required: true
+        type: string
+
+jobs:
+  package-python-pipeline:
+    runs-on: ubuntu-latest
+    if: inputs.platform == 'linux'
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/setup-python@v4
+        with:
+          python-version: ${{ inputs.python-version }}
+      - name: Install dependencies
+        run: |
+          python3 -m pip install --upgrade pip
+          pip install setuptools wheel twine
+      - name: Build pip package
+        working-directory: ./src/blobtoolkit-pipeline
+        run: |
+          mkdir -p artifacts
+          PY_VERSION=${{ inputs.python-version }}
+          PY_VERSION=${PY_VERSION/./}
+          python3 setup.py sdist bdist_wheel --python-tag py${PY_VERSION} --plat-name manylinux2014_x86_64
+          if [ "${{ inputs.python-version }}" == "3.9" ]; then
+            pip3 install dist/blobtoolkit_pipeline-*-py${PY_VERSION}-none-$PLATFORM.whl &&
+            blobtoolkit-pipeline -v
+            mv dist/blobtoolkit*.tar.gz artifacts/
+          fi
+          mv dist/*.whl artifacts/
+      - uses: actions/upload-artifact@v3
+        with:
+          name: pip
+          path: ./src/blobtoolkit-pipeline/artifacts/*
+
+  package-python-pipeline-macos:
+    runs-on: macos-11
+    if: inputs.platform == 'macos'
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/setup-python@v4
+        with:
+          python-version: ${{ inputs.python-version }}
+      - name: Install dependencies
+        run: |
+          python3 -m pip install --upgrade pip
+          pip3 install setuptools wheel twine
+          pip3 install --upgrade cython
+      - name: Build pip package
+        working-directory: ./src/blobtoolkit-pipeline
+        run: |
+          mkdir -p artifacts
+          PY_VERSION=${{ inputs.python-version }}
+          PY_VERSION=${PY_VERSION/./}
+          python3 setup.py sdist bdist_wheel --python-tag py${PY_VERSION} --plat-name macosx_11_0_x86_64
+          python3 setup.py sdist bdist_wheel --python-tag py${PY_VERSION} --plat-name macosx_11_0_arm64
+          mv dist/*.whl artifacts/
+      - uses: actions/upload-artifact@v3
+        with:
+          name: pip
+          path: ./src/blobtoolkit-pipeline/artifacts/*
diff --git a/.github/workflows/package-ui.yml b/.github/workflows/package-ui.yml
new file mode 100644
index 00000000..c704e26c
--- /dev/null
+++ b/.github/workflows/package-ui.yml
@@ -0,0 +1,18 @@
+name: package-ui
+
+on: workflow_call
+
+jobs:
+  package-ui:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/setup-node@v3
+        with:
+          node-version: "16"
+      - run: npm install -g pkg
+      - run: ./package-ui.sh
+      - uses: actions/upload-artifact@v3
+        with:
+          name: node
+          path: ./dist/*
diff --git a/.github/workflows/upload-to-pypi.yml b/.github/workflows/upload-to-pypi.yml
new file mode 100644
index 00000000..94c870ef
--- /dev/null
+++ b/.github/workflows/upload-to-pypi.yml
@@ -0,0 +1,31 @@
+name: upload-to-pypi
+on:
+  workflow_call:
+    inputs:
+      component:
+        required: true
+        type: string
+      version:
+        required: true
+        type: string
+
+jobs:
+  upload-to-pypi:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/download-artifact@v3
+        with:
+          name: pip
+          path: ./tmp
+      - run: |
+          mkdir -p ./dist
+          COMPONENT=${{ inputs.component }}
+          mv tmp/${{ inputs.component }}-${{ inputs.version }}-*.whl dist/
+          mv tmp/${COMPONENT/_/-}-${{ inputs.version }}.tar.gz dist/
+      - name: Publish package to PyPI
+        uses: pypa/gh-action-pypi-publish@v1.5.0
+        with:
+          user: __token__
+          password: ${{ secrets.PYPI_API_TOKEN }}
+          repository_url: https://upload.pypi.org/legacy/
diff --git a/.hostbumpversion.cfg b/.hostbumpversion.cfg
new file mode 100644
index 00000000..1d0f7c10
--- /dev/null
+++ b/.hostbumpversion.cfg
@@ -0,0 +1,8 @@
+[bumpversion]
+current_version = 4.0.2
+commit = False
+tag = False
+
+[bumpversion:file:src/blobtoolkit-host/setup.py]
+search = version="{current_version}"
+replace = version="{new_version}"
diff --git a/.pipelinebumpversion.cfg b/.pipelinebumpversion.cfg
new file mode 100644
index 00000000..0fca5938
--- /dev/null
+++ b/.pipelinebumpversion.cfg
@@ -0,0 +1,8 @@
+[bumpversion]
+current_version = 4.0.2
+commit = False
+tag = False
+
+[bumpversion:file:src/blobtoolkit-pipeline/setup.py]
+search = version="{current_version}"
+replace = version="{new_version}"
diff --git a/.prebumpversion.cfg b/.prebumpversion.cfg
new file mode 100644
index 00000000..f684757c
--- /dev/null
+++ b/.prebumpversion.cfg
@@ -0,0 +1,4 @@
+[bumpversion]
+current_version = 4.0.2
+commit = False
+tag = False
diff --git a/MANIFEST.in b/MANIFEST.in
index 6078c4a5..fdc8d79a 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -12,10 +12,5 @@ recursive-exclude tests *
 recursive-exclude conda-recipe *
 recursive-exclude ci *
 recursive-exclude tests *
-recursive-include src/data/example *
+recursive-exclude src *
 recursive-include src/data/schema *
-recursive-include src/data/bin *
-recursive-include src/pipeline *.smk
-recursive-include src/pipeline/rules *
-recursive-include src/pipeline/lib *
-recursive-include src/btk/lib *
diff --git a/README.md b/README.md
index 2edc0d35..6183a256 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# BlobToolKit (v3.4.0)
+# BlobToolKit (v4.0.2)
 
 [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
 [![DOI](https://zenodo.org/badge/150091036.svg)](https://zenodo.org/badge/latestdoi/150091036)
diff --git a/bump_full_version.sh b/bump_full_version.sh
new file mode 100755
index 00000000..898dd00c
--- /dev/null
+++ b/bump_full_version.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+
+LEVEL=$1
+VERSION=$2
+NPM_UPDATE=$3
+
+CURRENT_VERSION=$(grep current_version .bumpversion.cfg | head -n 1 | cut -d' ' -f 3)
+
+if [ $NPM_UPDATE == 1 ]; then
+  cd src/api &&
+  npm version --no-git-tag-version $VERSION &&
+  cd - &&
+
+  cd src/viewer &&
+  npm version --no-git-tag-version $VERSION &&
+  cd - &&
+
+  cd src/packaged-viewer &&
+  npm version --no-git-tag-version $VERSION &&
+  cd -
+fi
+
+bump2version --allow-dirty $LEVEL
+
+NEW_VERSION=$(grep current_version .bumpversion.cfg | head -n 1 | cut -d' ' -f 3)
+
+git commit -a -m "Bump version: ${CURRENT_VERSION} → ${NEW_VERSION}"
+git tag -a $NEW_VERSION -m "Bump version: ${CURRENT_VERSION} → ${NEW_VERSION}"
diff --git a/bump_npm_version.sh b/bump_npm_version.sh
new file mode 100755
index 00000000..b63fc746
--- /dev/null
+++ b/bump_npm_version.sh
@@ -0,0 +1,37 @@
+#!/bin/bash
+
+LEVEL=$1
+
+if [ -z "$LEVEL" ]; then
+  echo "Usage: ./bump_version.sh major|minor|patch"
+  exit 1;
+fi
+
+CURRENT_VERSION=$(grep '"version":' src/viewer/package.json | head -n 1 | cut -d'"' -f 4)
+
+if [[ "$LEVEL" == pre* ]]; then
+  LEVEL="-preid rc $LEVEL"
+fi
+
+cd src/api &&
+
+npm version --no-git-tag-version $LEVEL &&
+
+cd - &&
+
+cd src/viewer &&
+
+npm version --no-git-tag-version $LEVEL &&
+
+cd - &&
+
+cd src/packaged-viewer &&
+
+npm version --no-git-tag-version $LEVEL &&
+
+cd -
+
+NEW_VERSION=$(grep '"version":' src/viewer/package.json | head -n 1 | cut -d'"' -f 4)
+
+git commit -a -m "bump UI/API version: ${CURRENT_VERSION} → ${NEW_VERSION}"
+git tag -a $NEW_VERSION -m "Bump version: ${CURRENT_VERSION} → ${NEW_VERSION}"
diff --git a/bump_version.sh b/bump_version.sh
deleted file mode 100755
index 02bb2aaa..00000000
--- a/bump_version.sh
+++ /dev/null
@@ -1,30 +0,0 @@
-#!/bin/bash
-
-LEVEL=$1
-
-if [ -z "$LEVEL" ]; then
-  echo "Usage: ./bump_version.sh major|minor|patch"
-  exit 1;
-fi
-
-cd src/api &&
-
-npm version --no-git-tag-version $LEVEL &&
-
-cd - &&
-
-cd src/viewer &&
-
-npm version --no-git-tag-version $LEVEL &&
-
-cd - &&
-
-cd src/packaged-viewer &&
-
-npm version --no-git-tag-version $LEVEL &&
-
-cd - &&
-
-git commit -a -m "bump UI/API version"
-
-bump2version $LEVEL
diff --git a/bump_versions.sh b/bump_versions.sh
new file mode 100755
index 00000000..c6d0680a
--- /dev/null
+++ b/bump_versions.sh
@@ -0,0 +1,95 @@
+#!/bin/bash
+
+LEVEL=$1
+
+if [ -z "$LEVEL" ]; then
+  echo "Usage: ./bump_version.sh major|minor|patch|prepatch"
+  exit 1;
+fi
+
+if output=$(git status --porcelain) && [ ! -z "$output" ]; then
+  # Uncommitted changes
+  echo "Unable to bump version. Git working directory is not clean."
+  exit 1
+fi
+
+LATEST_TAG=$(grep current_version .prebumpversion.cfg | head -n 1 | cut -d' ' -f 3)
+NPM_UPDATE=0
+HOST_UPDATE=0
+PIPELINE_UPDATE=0
+BLOBTOOLS_UPDATE=0
+
+if [[ "$LEVEL" == *major ]] || [[ "$LEVEL" == *minor ]]; then
+  NPM_UPDATE=1
+  HOST_UPDATE=1
+  PIPELINE_UPDATE=1
+  BLOBTOOLS_UPDATE=1
+else
+  if [ "$NPM_UPDATE" == 0 ]; then
+    git diff --exit-code -s ${LATEST_TAG}..HEAD src/api
+    NPM_UPDATE=$?
+  fi
+  if [ "$NPM_UPDATE" == 0 ]; then
+    git diff --exit-code -s ${LATEST_TAG}..HEAD src/viewer
+    NPM_UPDATE=$?
+  fi
+  if [ "$NPM_UPDATE" == 0 ]; then
+    git diff --exit-code -s ${LATEST_TAG}..HEAD src/packaged-viewer
+    NPM_UPDATE=$?
+  fi
+
+  if [ "$HOST_UPDATE" == 0 ]; then
+    git diff --exit-code -s ${LATEST_TAG}..HEAD src/blobtoolkit-host
+    HOST_UPDATE=$?
+    if [ "$HOST_UPDATE" == 1 ]; then
+      BLOBTOOLS_UPDATE=1
+    fi
+  fi
+
+  if [ "$PIPELINE_UPDATE" == 0 ]; then
+    git diff --exit-code -s ${LATEST_TAG}..HEAD src/blobtoolkit-pipeline
+    PIPELINE_UPDATE=$?
+    if [ "$PIPELINE_UPDATE" == 1 ]; then
+      BLOBTOOLS_UPDATE=1
+    fi
+  fi
+
+  if [ "$BLOBTOOLS_UPDATE" == 0 ]; then
+    git diff --exit-code -s ${LATEST_TAG}..HEAD src/btk
+    BLOBTOOLS_UPDATE=$?
+  fi
+
+  if [ "$BLOBTOOLS_UPDATE" == 0 ]; then
+    git diff --exit-code -s ${LATEST_TAG}..HEAD src/blobtools
+    BLOBTOOLS_UPDATE=$?
+  fi
+  
+  if [ "$BLOBTOOLS_UPDATE" == 0 ]; then
+    LEVEL="-preid rc pre$LEVEL"
+  fi
+fi
+
+if [ "$BLOBTOOLS_UPDATE" == 1 ]; then
+  bump2version --allow-dirty --config-file .prebumpversion.cfg $LEVEL
+  VERSION=$(grep current_version .prebumpversion.cfg | cut -d' ' -f 3)
+  if [ "$HOST_UPDATE" == 1 ]; then
+    bump2version --allow-dirty --config-file .hostbumpversion.cfg $LEVEL
+    sed -i ".bak" 's/blobtoolkit-host=='$LATEST_TAG'/blobtoolkit-host=='$VERSION'/' setup.py
+    rm setup.py.bak
+    sed -i ".bak" 's/blobtoolkit-host v'$LATEST_TAG'/blobtoolkit-host v'$VERSION'/' src/blobtoolkit-host/src/lib/version.py
+    rm src/blobtoolkit-host/src/lib/version.py.bak
+  fi
+  if [ "$PIPELINE_UPDATE" == 1 ]; then
+    bump2version --allow-dirty --config-file .pipelinebumpversion.cfg $LEVEL
+    sed -i ".bak" 's/blobtoolkit-pipeline=='$LATEST_TAG'/blobtoolkit-pipeline=='$VERSION'/' setup.py
+    rm setup.py.bak
+    sed -i ".bak" 's/blobtoolkit-pipeline v'$LATEST_TAG'/blobtoolkit-pipeline v'$VERSION'/' src/blobtoolkit-pipeline/src/lib/version.py
+    rm src/blobtoolkit-pipeline/src/lib/version.py.bak
+  fi
+  ./bump_full_version.sh $LEVEL $VERSION $NPM_UPDATE
+elif [ "$NPM_UPDATE" == 1 ]; then
+  ./bump_npm_version.sh $LEVEL
+fi
+
+
+
diff --git a/pyproject.toml b/pyproject.toml
index 694671bb..8c0878c7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,7 @@
 [build-system]
 requires = [
-    'setuptools>=42',
-    'wheel',
+    "setuptools>=42",
+    "wheel",
 ]
 
 [tool.pytest.ini_options]
diff --git a/requirements.txt b/requirements.txt
index 1f5bd6f5..d4c3a224 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,18 +1,11 @@
-chromedriver-binary-auto==0.1.1
+blobtoolkit-core==0.1.0
 docopt==0.6.2
-geckodriver-autoinstaller==0.1.0
-psutil==5.9.0
+psutil==5.9.4
 pysam==0.19.1
 pytest==6.2.5
 pytest_mock==3.7.0
-pyvirtualdisplay==3.0
 PyYAML==6.0
-selenium==4.3.0
-setuptools==49.2.0
-tolkein==0.4.0
-tqdm==4.64.0
-ujson==5.4.0
-
-defusedxml==0.7.1
-requests==2.28.1
-snakemake==7.16.1
\ No newline at end of file
+setuptools==65.6.3
+tolkein==0.5.0
+tqdm==4.64.1
+ujson==5.7.0
diff --git a/setup.py b/setup.py
index 7585b849..c11a6b15 100644
--- a/setup.py
+++ b/setup.py
@@ -97,7 +97,7 @@ def read(*names, **kwargs):
 
 setup(
     name="blobtoolkit",  # Required
-    version="3.4.0",
+    version="4.0.2",
     description="blobtoolkit",  # Optional
     long_description="blobtoolkit",  # Optional
     long_description_content_type="text/markdown",
@@ -109,7 +109,7 @@ def read(*names, **kwargs):
     #     re.sub(":[a-z]+:`~?(.*?)`", r"``\1``", read("CHANGELOG.rst")),
     # ),
     # long_description_content_type="text/x-rst",  # Optional (see note above)
-    url="https://github.com/blobtoolkit/blobtools2",  # Optional
+    url="https://github.com/blobtoolkit/blobtoolkit",  # Optional
     # This should be your name or the name of the organization which owns the
     # project.
     author="blobtoolkit",  # Optional
@@ -138,6 +138,8 @@ def read(*names, **kwargs):
         "Programming Language :: Python :: 3.7",
         "Programming Language :: Python :: 3.8",
         "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.10",
+        "Programming Language :: Python :: 3.11",
         "Programming Language :: Python :: 3 :: Only",
     ],
     # This field adds keywords for your project which will appear on the
@@ -172,7 +174,7 @@ def read(*names, **kwargs):
     # 'Programming Language' classifiers above, 'pip install' will check this
     # and refuse to install the project if the version does not match. See
     # https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires
-    python_requires=">=3.7, <3.11",
+    python_requires=">=3.7, <3.12",
     # This field lists other packages that your project depends on to run.
     # Any package you put here will be installed by pip when your project is
     # installed, so they must be valid existing projects.
@@ -180,22 +182,19 @@ def read(*names, **kwargs):
     # For an analysis of "install_requires" vs pip's requirements files see:
     # https://packaging.python.org/en/latest/requirements.html
     install_requires=[
-        "chromedriver-binary-auto==0.1.1",
+        "blobtoolkit-core>=0.1.0",
+        "chromedriver-binary-auto==0.2.3",
         "docopt>=0.6.2",
         "fastjsonschema==2.15.3",
         "geckodriver-autoinstaller==0.1.0",
-        "psutil==5.9.0",
+        "psutil==5.9.4",
         "pysam==0.19.1",
         "pyvirtualdisplay==3.0",
         "pyyaml",
-        "selenium==4.3.0",
-        "tolkein>=0.4.0",
-        "tqdm==4.64.0",
-        "ujson>=3.0.0",
-        # Additional pipeline dependencies
-        "defusedxml==0.7.1",
-        "requests==2.28.1",
-        "snakemake==7.16.1",
+        "selenium==4.7.2",
+        "tolkein>=0.5.0",
+        "tqdm==4.64.1",
+        "ujson>=5.7.0",
     ],  # Optional
     # List additional groups of dependencies here (e.g. development
     # dependencies). Users will be able to install these using the "extras"
@@ -216,6 +215,9 @@ def read(*names, **kwargs):
             "pytest-mock>=3.1.1",
             "pytest>=6.0.0",
         ],
+        "full": ["blobtoolkit-host==4.0.2", "blobtoolkit-pipeline==4.0.2"],
+        "host": ["blobtoolkit-host==4.0.2"],
+        "pipeline": ["blobtoolkit-pipeline==4.0.2"],
     },
     entry_points={
         "console_scripts": [
@@ -233,27 +235,12 @@ def read(*names, **kwargs):
             "view = blobtools.lib.view:cli",
         ],
         "btk.subcmd": [
-            "pipeline = pipeline:main",
-            # "blobtools = blobtools:cli",
-        ],
-        "pipeline.subcmd": [
-            "data = pipeline.lib.data:main",
-            "run = pipeline.lib.run:main",
-            "add-summary-to-metadata = pipeline.lib.add_summary_to_metadata:main",
-            "chunk-fasta = pipeline.lib.chunk_fasta:main",
-            "count-busco-genes = pipeline.lib.count_busco_genes:main",
-            "extract-busco-genes = pipeline.lib.extract_busco_genes:main",
-            "generate-config = pipeline.lib.generate_config:main",
-            "generate-static-images = pipeline.lib.generate_static_images:main",
-            "resume-pipeline = pipeline.lib.resume_pipeline:main",
-            "transfer-completed = pipeline.lib.transfer_completed:main",
-            "unchunk-blast = pipeline.lib.unchunk_blast:main",
-            "window-stats = pipeline.lib.window_stats:main",
+            "pipeline = btk.lib.pipeline:main",
         ],
     },
     project_urls={
-        "Bug Reports": "https://github.com/blobtoolkit/blobtools2/issues",
-        "Source": "https://github.com/blobtoolkit/blobtools2",
+        "Bug Reports": "https://github.com/blobtoolkit/blobtoolkit/issues",
+        "Source": "https://github.com/blobtoolkit/blobtoolkit",
     },
     include_package_data=True,
     zip_safe=False,
diff --git a/src/api/package-lock.json b/src/api/package-lock.json
index 5e088471..aa0bc129 100644
--- a/src/api/package-lock.json
+++ b/src/api/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "blobtoolkit-api",
-  "version": "3.4.0",
+  "version": "4.0.0",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "blobtoolkit-api",
-      "version": "3.4.0",
+      "version": "4.0.0",
       "license": "MIT",
       "dependencies": {
         "app-root-path": "^3.0.0",
diff --git a/src/api/package.json b/src/api/package.json
index 99254bd5..44c25b54 100644
--- a/src/api/package.json
+++ b/src/api/package.json
@@ -1,6 +1,6 @@
 {
   "name": "blobtoolkit-api",
-  "version": "3.4.0",
+  "version": "4.0.0",
   "repository": "https://github.com/blobtoolkit/viewer",
   "description": "",
   "bin": "src/app.js",
diff --git a/src/blobtoolkit-host/MANIFEST.in b/src/blobtoolkit-host/MANIFEST.in
new file mode 100644
index 00000000..4c68b23c
--- /dev/null
+++ b/src/blobtoolkit-host/MANIFEST.in
@@ -0,0 +1,8 @@
+# MANIFEST.in
+include setup.py
+prune .cache
+prune .git
+prune build
+prune dist
+recursive-exclude *.egg-info *
+recursive-include src/data/bin *
diff --git a/src/blobtoolkit-host/pyproject.toml b/src/blobtoolkit-host/pyproject.toml
new file mode 100644
index 00000000..694671bb
--- /dev/null
+++ b/src/blobtoolkit-host/pyproject.toml
@@ -0,0 +1,26 @@
+[build-system]
+requires = [
+    'setuptools>=42',
+    'wheel',
+]
+
+[tool.pytest.ini_options]
+minversion = "6.0"
+addopts = "-ra -q"
+testpaths = [
+    "tests",
+]
+
+[tool.black]
+line-length = 88
+include = '\.pyi?$'
+exclude = '''
+/(
+    \.git
+  | \.tox
+  | \.venv
+  | _build
+  | build
+  | dist
+)/
+'''
\ No newline at end of file
diff --git a/src/blobtoolkit-host/requirements.txt b/src/blobtoolkit-host/requirements.txt
new file mode 100644
index 00000000..d04a80b3
--- /dev/null
+++ b/src/blobtoolkit-host/requirements.txt
@@ -0,0 +1,2 @@
+docopt==0.6.2
+psutil==5.9.4
\ No newline at end of file
diff --git a/src/blobtoolkit-host/setup.py b/src/blobtoolkit-host/setup.py
new file mode 100644
index 00000000..72db29cc
--- /dev/null
+++ b/src/blobtoolkit-host/setup.py
@@ -0,0 +1,127 @@
+"""
+A setuptools based setup module.
+
+See:
+https://packaging.python.org/guides/distributing-packages-using-setuptools/
+https://github.com/pypa/sampleproject
+"""
+
+import io
+from os.path import dirname
+from os.path import join
+
+from setuptools import find_namespace_packages
+from setuptools import setup
+
+
+def read(*names, **kwargs):
+    """Read file."""
+    with io.open(
+        join(dirname(__file__), *names), encoding=kwargs.get("encoding", "utf8")
+    ) as fh:
+        return fh.read()
+
+
+setup(
+    name="blobtoolkit-host",  # Required
+    version="4.0.2",
+    description="blobtoolkit-host",  # Optional
+    long_description="blobtoolkit-host",  # Optional
+    long_description_content_type="text/markdown",
+    url="https://github.com/blobtoolkit/blobtoolkit",  # Optional
+    # This should be your name or the name of the organization which owns the
+    # project.
+    author="blobtoolkit",  # Optional
+    # This should be a valid email address corresponding to the author listed
+    # above.
+    author_email="blobtoolkit@genomehubs.org",  # Optional
+    # Classifiers help users find your project by categorizing it.
+    #
+    # For a list of valid classifiers, see https://pypi.org/classifiers/
+    classifiers=[  # Optional
+        # How mature is this project? Common values are
+        #   3 - Alpha
+        #   4 - Beta
+        #   5 - Production/Stable
+        "Development Status :: 5 - Production/Stable",
+        # Indicate who your project is intended for
+        "Intended Audience :: Science/Research",
+        "Topic :: Scientific/Engineering :: Bio-Informatics",
+        # Pick your license as you wish
+        "License :: OSI Approved :: MIT License",
+        # Specify the Python versions you support here. In particular, ensure
+        # that you indicate you support Python 3. These classifiers are *not*
+        # checked by 'pip install'. See instead 'python_requires' below.
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.6",
+        "Programming Language :: Python :: 3.7",
+        "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.10",
+        "Programming Language :: Python :: 3.11",
+        "Programming Language :: Python :: 3 :: Only",
+    ],
+    # This field adds keywords for your project which will appear on the
+    # project page. What does your project relate to?
+    #
+    # Note that this is a list of additional keywords, separated
+    # by commas, to be used to assist searching for the distribution in a
+    # larger catalog.
+    keywords="bioinformatics",  # Optional
+    # When your source code is in a subdirectory under the project root, e.g.
+    # `src/`, it is necessary to specify the `package_dir` argument.
+    package_dir={"": "src"},  # Optional
+    # You can just specify package directories manually here if your project is
+    # simple. Or you can use find_packages().
+    #
+    # Alternatively, if you just want to distribute a single Python file, use
+    # the `py_modules` argument instead as follows, which will expect a file
+    # called `my_module.py` to exist:
+    #
+    py_modules=["blobtoolkit_host"],
+    #
+    # packages=find_packages(where="src"),  # Required
+    packages=find_namespace_packages(
+        where="src",
+        exclude=[],
+    ),
+    # Specify which Python versions you support. In contrast to the
+    # 'Programming Language' classifiers above, 'pip install' will check this
+    # and refuse to install the project if the version does not match. See
+    # https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires
+    python_requires=">=3.7, <3.12",
+    # This field lists other packages that your project depends on to run.
+    # Any package you put here will be installed by pip when your project is
+    # installed, so they must be valid existing projects.
+    #
+    # For an analysis of "install_requires" vs pip's requirements files see:
+    # https://packaging.python.org/en/latest/requirements.html
+    install_requires=[
+        "docopt>=0.6.2",
+        "psutil==5.9.4",
+    ],  # Optional
+    extras_require={  # Optional
+        "dev": ["pycodestyle>=2.6.0", "pydocstyle>=5.0.2", "pylint>=2.5.3"],
+        "test": [
+            "coverage>=5.1",
+            "coveralls>=2.0.0",
+            "mock>=4.0.2",
+            "pytest-cov>=2.10.0",
+            "pytest-isort>=5",
+            "pytest-mock>=3.1.1",
+            "pytest>=6.0.0",
+        ],
+    },
+    entry_points={
+        "console_scripts": [
+            "blobtoolkit-host = lib.host:cli",
+            "blobtoolkit-view = lib.view:cli",
+        ],
+    },
+    project_urls={
+        "Bug Reports": "https://github.com/blobtoolkit/blobtoolkit/issues",
+        "Source": "https://github.com/blobtoolkit/blobtoolkit",
+    },
+    include_package_data=True,
+    zip_safe=False,
+)
diff --git a/src/blobtoolkit-host/src/__init__.py b/src/blobtoolkit-host/src/__init__.py
new file mode 100644
index 00000000..b9fcfb40
--- /dev/null
+++ b/src/blobtoolkit-host/src/__init__.py
@@ -0,0 +1,3 @@
+"""BlobToolKit."""
+
+from .lib import host
diff --git a/src/blobtoolkit-host/src/__main__.py b/src/blobtoolkit-host/src/__main__.py
new file mode 100644
index 00000000..12aee6f7
--- /dev/null
+++ b/src/blobtoolkit-host/src/__main__.py
@@ -0,0 +1,6 @@
+"""GenomeHubs package."""
+
+from .blobtools import cli
+
+if __name__ == "__main__":
+    cli()
diff --git a/src/blobtoolkit-host/src/blobtoolkit_host.py b/src/blobtoolkit-host/src/blobtoolkit_host.py
new file mode 100644
index 00000000..00002167
--- /dev/null
+++ b/src/blobtoolkit-host/src/blobtoolkit_host.py
@@ -0,0 +1,3 @@
+"""BlobToolKit Host."""
+
+from lib import host
diff --git a/src/blobtoolkit-host/src/lib/__init__.py b/src/blobtoolkit-host/src/lib/__init__.py
new file mode 100755
index 00000000..b682f178
--- /dev/null
+++ b/src/blobtoolkit-host/src/lib/__init__.py
@@ -0,0 +1,1 @@
+"""This is a template package to demonstrate Continuous Integration."""
diff --git a/src/blobtoolkit-host/src/lib/host.py b/src/blobtoolkit-host/src/lib/host.py
new file mode 100755
index 00000000..60554c1c
--- /dev/null
+++ b/src/blobtoolkit-host/src/lib/host.py
@@ -0,0 +1,295 @@
+#!/usr/bin/env python3
+
+# pylint: disable=no-member, too-many-branches, too-many-statements, too-many-locals, W0603, W0703
+
+"""
+Host a collection of BlobDirs.
+
+Usage:
+    blobtoolkit-host [--port INT]  [--api-port INT]
+                     [-h|--help] [-v|--version]
+                     [--hostname STRING] DIRECTORY
+
+Arguments:
+    DIRECTORY             Directory containing one or more BlobDirs.
+
+Options:
+    --port INT            HTTP port number. [Default: 8080]
+    --api-port INT        API port number. [Default: 8000]
+    --hostname STRING     Hostname used to connect to API. [Default: localhost]
+    -h, --help            Show this
+    -v, --version         Show version number
+
+"""
+
+
+import contextlib
+import os
+import platform
+import shlex
+import signal
+import socket
+import sys
+import time
+from pathlib import Path
+from shutil import which
+from subprocess import PIPE
+from subprocess import Popen
+
+import psutil
+from docopt import docopt
+
+from .version import __version__
+
+PIDS = []
+
+
+def iter_user_procs(process):
+    """Iterate through processes owned by the current user."""
+    username = psutil.Process.username(
+        process
+    )  # get username in format used by processes
+    for proc in psutil.process_iter(["username"]):
+        if proc.info["username"] == username:
+            yield proc
+
+
+def close_ports(process, args):
+    """Close processes running on chosen ports."""
+    for proc in iter_user_procs(process):
+        try:
+            for conns in proc.connections(kind="inet"):
+                if conns.laddr.port == args["--api-port"]:
+                    proc.send_signal(signal.SIGTERM)
+                elif conns.laddr.port == args["--port"]:
+                    proc.send_signal(signal.SIGTERM)
+        except psutil.NoSuchProcess:
+            continue
+        except psutil.ZombieProcess:
+            continue
+        except psutil.AccessDenied:
+            continue
+
+
+def kill_child_processes(parent_pid, args=None, sig=signal.SIGTERM):
+    """Kill all child processes."""
+    try:
+        parent = psutil.Process(parent_pid)
+    except psutil.NoSuchProcess:
+        return
+    # if args:
+    #     close_ports(parent, args)
+    children = parent.children(recursive=True)
+    for process in children:
+        try:
+            process.send_signal(sig)
+        except psutil.NoSuchProcess:
+            continue
+        except psutil.ZombieProcess:
+            continue
+    parent.send_signal(sig)
+
+
+def test_port(port, service):
+    """Exit if port is already in use."""
+    port = int(port)
+    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as skt:
+        try:
+            skt.bind(("", port))
+        except OSError:
+            if service == "test":
+                return False
+            print("ERROR: Port %d already in use, unable to host %s." % (port, service))
+            print(
+                "       Use: `lsof -nP -iTCP:%d | grep LISTEN` to find the associated process."
+                % port
+            )
+            print(
+                f"       It may take ~30s for this port to become available when restarting {service}."
+            )
+
+            sys.exit(1)
+    return True
+
+
+def find_binary(tool):
+    """Find a binary executable for the blobtoolkit viewer or API."""
+    script_dir = os.path.dirname(os.path.realpath(__file__))
+    system = platform.system()
+    # arch, _ = platform.architecture()
+    default_binaries = {"api": "blobtoolkit-api", "viewer": "blobtoolkit-viewer"}
+    if system == "Darwin":
+        binaries = {
+            "api": "blobtoolkit-api-macos",
+            "viewer": "blobtoolkit-viewer-macos",
+        }
+    elif system == "Linux":
+        binaries = {
+            "api": "blobtoolkit-api-linux",
+            "viewer": "blobtoolkit-viewer-linux",
+        }
+    elif system == "Windows":
+        binaries = {
+            "api": "blobtoolkit-api-win.exe",
+            "viewer": "blobtoolkit-viewer-win.exe",
+        }
+        default_binaries = {
+            "api": "blobtoolkit-api.exe",
+            "viewer": "blobtoolkit-viewer.exe",
+        }
+    if which(default_binaries[tool]) is not None:
+        return default_binaries[tool]
+    if which(binaries[tool]) is not None:
+        return default_binaries[tool]
+    executable = binaries[tool]
+    executable_path = os.path.join(
+        os.path.dirname(script_dir), "data", "bin", executable
+    )
+    if os.path.isfile(executable_path):
+        return executable_path
+    print(
+        f"ERROR: {default_binaries[tool]} executable was not found. Please add {default_binaries[tool]} to your PATH."
+    )
+
+    sys.exit(1)
+
+
+def start_api(port, api_port, hostname, directory):
+    """Start BlobToolKit API."""
+    cmd = find_binary("api")
+    # cmd = "blobtoolkit-api"
+    origins = "http://localhost:%d http://localhost null" % int(port)
+    if hostname != "localhost":
+        origins += " http://%s:%d http://%s" % (hostname, int(port), hostname)
+    if directory == "_":
+        env = dict(
+            os.environ,
+            BTK_API_PORT=api_port,
+            BTK_ORIGINS=origins,
+        )
+    else:
+        env = dict(
+            os.environ,
+            BTK_API_PORT=api_port,
+            BTK_FILE_PATH=directory,
+            BTK_ORIGINS=origins,
+        )
+    return Popen(
+        shlex.split(cmd),
+        stdout=PIPE,
+        stderr=PIPE,
+        encoding="ascii",
+        env=env,
+    )
+
+
+def start_viewer(port, api_port, hostname):
+    """Start BlobToolKit viewer."""
+    cmd = find_binary("viewer")
+    # cmd = "blobtoolkit-viewer"
+    api_url = "http://%s:%d/api/v1" % (hostname, int(api_port))
+    return Popen(
+        shlex.split(cmd),
+        stdout=PIPE,
+        stderr=PIPE,
+        encoding="ascii",
+        env=dict(
+            os.environ,
+            BTK_HOST=hostname,
+            BTK_CLIENT_PORT=port,
+            BTK_API_PORT=api_port,
+            BTK_API_URL=api_url,
+        ),
+    )
+
+
+def main(args):
+    """Entrypoint for blobtools host."""
+    global PIDS
+    directory = args["DIRECTORY"]
+    if directory != "_":
+        path = Path(directory)
+        if not path.exists():
+            print("ERROR: Directory '%s' does not exist" % directory)
+            sys.exit(1)
+        if (path / "meta.json").exists():
+            print("WARNING: Directory '%s' appears to be a BlobDir." % directory)
+            print("         Hosting the parent directory instead.")
+            path = path.resolve().parent
+        directory = path.absolute()
+    test_port(args["--api-port"], "BlobtoolKit API")
+    test_port(args["--port"], "BlobtoolKit viewer")
+    api = start_api(
+        args["--port"],
+        args["--api-port"],
+        args["--hostname"],
+        directory,
+    )
+    PIDS.append(api.pid)
+    print(
+        "Starting BlobToolKit API on port %d (pid: %d)"
+        % (int(args["--api-port"]), api.pid)
+    )
+    time.sleep(2)
+    viewer = start_viewer(args["--port"], args["--api-port"], args["--hostname"])
+    PIDS.append(viewer.pid)
+    print(
+        "Starting BlobToolKit viewer on port %d (pid: %d)"
+        % (int(args["--port"]), viewer.pid)
+    )
+    time.sleep(2)
+    ready = False
+    url = "http://%s:%d" % (args["--hostname"], int(args["--port"]))
+    while True:
+        time.sleep(1)
+        if api.poll() is not None:
+            for line in api.stdout.readlines():
+                print(line.strip())
+            for line in api.stderr.readlines():
+                print(line.strip())
+            if viewer.poll() is not None:
+                for line in viewer.stdout.readlines():
+                    print(line.strip())
+                for line in viewer.stderr.readlines():
+                    print(line.strip())
+                with contextlib.suppress(ProcessLookupError):
+                    os.kill(viewer.pid, signal.SIGTERM)
+            break
+        if viewer.poll() is not None:
+            for line in viewer.stdout.readlines():
+                print(line.strip())
+            for line in viewer.stderr.readlines():
+                print(line.strip())
+            with contextlib.suppress(ProcessLookupError):
+                os.kill(api.pid, signal.SIGTERM)
+            break
+        if not ready:
+            print(f"Visit {url} to use the interactive BlobToolKit Viewer.")
+            ready = True
+        time.sleep(1)
+
+
+def cli(rename=None):
+    """Entry point."""
+    if sys.argv[0].endswith("-host"):
+        args = docopt(__doc__, version=__version__)
+    else:
+        docs = __doc__
+        if rename is not None:
+            docs = docs.replace("blobtoolkit-host", rename)
+        if len(sys.argv) == sys.argv.index(__name__.split(".")[-1]) + 1:
+            args = docopt(docs, argv=[])
+        else:
+            args = docopt(docs, version=__version__)
+    try:
+        main(args)
+    except KeyboardInterrupt:
+        pass
+    finally:
+        for pid in PIDS:
+            kill_child_processes(pid, args=args, sig=signal.SIGKILL)
+            time.sleep(2)
+
+
+if __name__ == "__main__":
+    cli()
diff --git a/src/pipeline/lib/version.py b/src/blobtoolkit-host/src/lib/version.py
similarity index 56%
rename from src/pipeline/lib/version.py
rename to src/blobtoolkit-host/src/lib/version.py
index e9b8f088..2f694234 100644
--- a/src/pipeline/lib/version.py
+++ b/src/blobtoolkit-host/src/lib/version.py
@@ -1,4 +1,4 @@
 #!/usr/bin/env python3
 """blobtoolkit version."""
 
-__version__ = "blobtoolkit v3.4.0"
+__version__ = "blobtoolkit-host v4.0.2"
diff --git a/src/blobtoolkit-pipeline/MANIFEST.in b/src/blobtoolkit-pipeline/MANIFEST.in
new file mode 100644
index 00000000..cccb2019
--- /dev/null
+++ b/src/blobtoolkit-pipeline/MANIFEST.in
@@ -0,0 +1,10 @@
+# MANIFEST.in
+include setup.py
+prune .cache
+prune .git
+prune build
+prune dist
+recursive-include src *.smk
+recursive-include src/rules *
+recursive-include src/lib *
+recursive-exclude *.egg-info *
diff --git a/src/blobtoolkit-pipeline/pyproject.toml b/src/blobtoolkit-pipeline/pyproject.toml
new file mode 100644
index 00000000..694671bb
--- /dev/null
+++ b/src/blobtoolkit-pipeline/pyproject.toml
@@ -0,0 +1,26 @@
+[build-system]
+requires = [
+    'setuptools>=42',
+    'wheel',
+]
+
+[tool.pytest.ini_options]
+minversion = "6.0"
+addopts = "-ra -q"
+testpaths = [
+    "tests",
+]
+
+[tool.black]
+line-length = 88
+include = '\.pyi?$'
+exclude = '''
+/(
+    \.git
+  | \.tox
+  | \.venv
+  | _build
+  | build
+  | dist
+)/
+'''
\ No newline at end of file
diff --git a/src/blobtoolkit-pipeline/requirements.txt b/src/blobtoolkit-pipeline/requirements.txt
new file mode 100644
index 00000000..14e7d387
--- /dev/null
+++ b/src/blobtoolkit-pipeline/requirements.txt
@@ -0,0 +1,11 @@
+docopt==0.6.2
+psutil==5.9.4
+PyYAML==6.0
+setuptools==65.6.3
+tolkein==0.5.0
+tqdm==4.64.1
+ujson==5.7.0
+
+defusedxml==0.7.1
+requests==2.28.1
+snakemake==7.19.1
\ No newline at end of file
diff --git a/src/blobtoolkit-pipeline/setup.py b/src/blobtoolkit-pipeline/setup.py
new file mode 100644
index 00000000..7281d81f
--- /dev/null
+++ b/src/blobtoolkit-pipeline/setup.py
@@ -0,0 +1,147 @@
+"""
+A setuptools based setup module.
+
+See:
+https://packaging.python.org/guides/distributing-packages-using-setuptools/
+https://github.com/pypa/sampleproject
+"""
+
+import io
+from os.path import dirname
+from os.path import join
+
+from setuptools import find_namespace_packages
+from setuptools import setup
+
+
+def read(*names, **kwargs):
+    """Read file."""
+    with io.open(
+        join(dirname(__file__), *names), encoding=kwargs.get("encoding", "utf8")
+    ) as fh:
+        return fh.read()
+
+
+setup(
+    name="blobtoolkit-pipeline",  # Required
+    version="4.0.2",
+    description="blobtoolkit-pipeline",  # Optional
+    long_description="blobtoolkit-pipeline",  # Optional
+    long_description_content_type="text/markdown",
+    url="https://github.com/blobtoolkit/blobtoolkit",  # Optional
+    # This should be your name or the name of the organization which owns the
+    # project.
+    author="blobtoolkit",  # Optional
+    # This should be a valid email address corresponding to the author listed
+    # above.
+    author_email="blobtoolkit@genomehubs.org",  # Optional
+    # Classifiers help users find your project by categorizing it.
+    #
+    # For a list of valid classifiers, see https://pypi.org/classifiers/
+    classifiers=[  # Optional
+        # How mature is this project? Common values are
+        #   3 - Alpha
+        #   4 - Beta
+        #   5 - Production/Stable
+        "Development Status :: 5 - Production/Stable",
+        # Indicate who your project is intended for
+        "Intended Audience :: Science/Research",
+        "Topic :: Scientific/Engineering :: Bio-Informatics",
+        # Pick your license as you wish
+        "License :: OSI Approved :: MIT License",
+        # Specify the Python versions you support here. In particular, ensure
+        # that you indicate you support Python 3. These classifiers are *not*
+        # checked by 'pip install'. See instead 'python_requires' below.
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.6",
+        "Programming Language :: Python :: 3.7",
+        "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.10",
+        "Programming Language :: Python :: 3.11",
+        "Programming Language :: Python :: 3 :: Only",
+    ],
+    # This field adds keywords for your project which will appear on the
+    # project page. What does your project relate to?
+    #
+    # Note that this is a list of additional keywords, separated
+    # by commas, to be used to assist searching for the distribution in a
+    # larger catalog.
+    keywords="bioinformatics",  # Optional
+    # When your source code is in a subdirectory under the project root, e.g.
+    # `src/`, it is necessary to specify the `package_dir` argument.
+    package_dir={"": "src"},  # Optional
+    # You can just specify package directories manually here if your project is
+    # simple. Or you can use find_packages().
+    #
+    # Alternatively, if you just want to distribute a single Python file, use
+    # the `py_modules` argument instead as follows, which will expect a file
+    # called `my_module.py` to exist:
+    #
+    py_modules=["blobtoolkit_pipeline"],
+    #
+    # packages=find_packages(where="src"),  # Required
+    packages=find_namespace_packages(
+        where="src",
+        exclude=[],
+    ),
+    # Specify which Python versions you support. In contrast to the
+    # 'Programming Language' classifiers above, 'pip install' will check this
+    # and refuse to install the project if the version does not match. See
+    # https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires
+    python_requires=">=3.7, <3.12",
+    # This field lists other packages that your project depends on to run.
+    # Any package you put here will be installed by pip when your project is
+    # installed, so they must be valid existing projects.
+    #
+    # For an analysis of "install_requires" vs pip's requirements files see:
+    # https://packaging.python.org/en/latest/requirements.html
+    install_requires=[
+        "docopt>=0.6.2",
+        "psutil==5.9.4",
+        "pyyaml",
+        "tolkein>=0.5.0",
+        "tqdm==4.64.1",
+        "ujson>=5.7.0",
+        "defusedxml==0.7.1",
+        "requests==2.28.1",
+        "snakemake==7.19.1",
+    ],  # Optional
+    extras_require={  # Optional
+        "dev": ["pycodestyle>=2.6.0", "pydocstyle>=5.0.2", "pylint>=2.5.3"],
+        "test": [
+            "coverage>=5.1",
+            "coveralls>=2.0.0",
+            "mock>=4.0.2",
+            "pytest-cov>=2.10.0",
+            "pytest-isort>=5",
+            "pytest-mock>=3.1.1",
+            "pytest>=6.0.0",
+        ],
+    },
+    entry_points={
+        "console_scripts": [
+            "blobtoolkit-pipeline = blobtoolkit_pipeline:cli",
+        ],
+        "blobtoolkit_pipeline.subcmd": [
+            "data = lib.data:main",
+            "run = lib.run:main",
+            "add-summary-to-metadata = lib.add_summary_to_metadata:main",
+            "chunk-fasta = lib.chunk_fasta:main",
+            "count-busco-genes = lib.count_busco_genes:main",
+            "extract-busco-genes = lib.extract_busco_genes:main",
+            "generate-config = lib.generate_config:main",
+            "generate-static-images = lib.generate_static_images:main",
+            "resume-pipeline = lib.resume_pipeline:main",
+            "transfer-completed = lib.transfer_completed:main",
+            "unchunk-blast = lib.unchunk_blast:main",
+            "window-stats = lib.window_stats:main",
+        ],
+    },
+    project_urls={
+        "Bug Reports": "https://github.com/blobtoolkit/blobtoolkit/issues",
+        "Source": "https://github.com/blobtoolkit/blobtoolkit",
+    },
+    include_package_data=True,
+    zip_safe=False,
+)
diff --git a/src/pipeline/README.md b/src/blobtoolkit-pipeline/src/README.md
similarity index 100%
rename from src/pipeline/README.md
rename to src/blobtoolkit-pipeline/src/README.md
diff --git a/src/blobtoolkit-pipeline/src/__init__.py b/src/blobtoolkit-pipeline/src/__init__.py
new file mode 100644
index 00000000..8fdf12d0
--- /dev/null
+++ b/src/blobtoolkit-pipeline/src/__init__.py
@@ -0,0 +1,1 @@
+"""BlobToolKit."""
diff --git a/src/pipeline/add_reads.smk b/src/blobtoolkit-pipeline/src/add_reads.smk
similarity index 100%
rename from src/pipeline/add_reads.smk
rename to src/blobtoolkit-pipeline/src/add_reads.smk
diff --git a/src/pipeline/blastn.smk b/src/blobtoolkit-pipeline/src/blastn.smk
similarity index 100%
rename from src/pipeline/blastn.smk
rename to src/blobtoolkit-pipeline/src/blastn.smk
diff --git a/src/pipeline/blobtoolkit.smk b/src/blobtoolkit-pipeline/src/blobtoolkit.smk
similarity index 100%
rename from src/pipeline/blobtoolkit.smk
rename to src/blobtoolkit-pipeline/src/blobtoolkit.smk
diff --git a/src/pipeline/pipeline.py b/src/blobtoolkit-pipeline/src/blobtoolkit_pipeline.py
similarity index 50%
rename from src/pipeline/pipeline.py
rename to src/blobtoolkit-pipeline/src/blobtoolkit_pipeline.py
index 557fe66d..93a3ea2f 100755
--- a/src/pipeline/pipeline.py
+++ b/src/blobtoolkit-pipeline/src/blobtoolkit_pipeline.py
@@ -3,7 +3,7 @@
 """
 BlobToolKit Pipeline.
 
-Usage: blobtools pipeline [<command>] [<args>...] [-h|--help] [--version]
+Usage: blobtoolkit-pipeline [<command>] [<args>...] [-h|--help] [-v|--version]
 
 Commands:
     data                      Fetch pipeline data
@@ -19,7 +19,7 @@
     window-stats              Pipeline helper script
     -h, --help      Show this
     -v, --version   Show version number
-See 'blobtools pipeline <command> --help' for more information on a specific command.
+See 'blobtoolkit-pipeline <command> --help' for more information on a specific command.
 
 """
 
@@ -27,23 +27,28 @@
 
 from docopt import DocoptExit
 from docopt import docopt
+from lib.version import __version__
 from pkg_resources import working_set
 
-from .lib.version import __version__
 
-
-def main():
+def cli(rename=None):
     """Entry point."""
+    # if sys.argv[1].match("-pipeline"):
+    command = sys.argv[1]
     if len(sys.argv) > 2:
-        try:
-            args = docopt(__doc__, help=False, version=__version__)
-        except DocoptExit:
-            args = {"<command>": sys.argv[2]}
-        if args["<command>"]:
-            # load <command> from entry_points
-            for entry_point in working_set.iter_entry_points("pipeline.subcmd"):
-                if entry_point.name == args["<command>"]:
-                    subcommand = entry_point.load()
-                    sys.exit(subcommand())
-    print(__doc__)
+        command = sys.argv[2]
+    docs = __doc__
+    if rename is not None:
+        docs = docs.replace("blobtoolkit-pipeline", rename)
+    try:
+        args = docopt(docs, help=False, version=__version__)
+    except DocoptExit:
+        args = {"<command>": command}
+    if args["<command>"]:
+        # load <command> from entry_points
+        for entry_point in working_set.iter_entry_points("blobtoolkit_pipeline.subcmd"):
+            if entry_point.name == args["<command>"]:
+                subcommand = entry_point.load()
+                sys.exit(subcommand(rename))
+    print(docs)
     raise DocoptExit
diff --git a/src/pipeline/blobtools.smk b/src/blobtoolkit-pipeline/src/blobtools.smk
similarity index 100%
rename from src/pipeline/blobtools.smk
rename to src/blobtoolkit-pipeline/src/blobtools.smk
diff --git a/src/pipeline/busco.smk b/src/blobtoolkit-pipeline/src/busco.smk
similarity index 100%
rename from src/pipeline/busco.smk
rename to src/blobtoolkit-pipeline/src/busco.smk
diff --git a/src/pipeline/chunk_stats.smk b/src/blobtoolkit-pipeline/src/chunk_stats.smk
similarity index 100%
rename from src/pipeline/chunk_stats.smk
rename to src/blobtoolkit-pipeline/src/chunk_stats.smk
diff --git a/src/pipeline/cov_stats.smk b/src/blobtoolkit-pipeline/src/cov_stats.smk
similarity index 100%
rename from src/pipeline/cov_stats.smk
rename to src/blobtoolkit-pipeline/src/cov_stats.smk
diff --git a/src/pipeline/diamond.smk b/src/blobtoolkit-pipeline/src/diamond.smk
similarity index 100%
rename from src/pipeline/diamond.smk
rename to src/blobtoolkit-pipeline/src/diamond.smk
diff --git a/src/pipeline/diamond_blastp.smk b/src/blobtoolkit-pipeline/src/diamond_blastp.smk
similarity index 100%
rename from src/pipeline/diamond_blastp.smk
rename to src/blobtoolkit-pipeline/src/diamond_blastp.smk
diff --git a/src/blobtoolkit-pipeline/src/lib/__init__.py b/src/blobtoolkit-pipeline/src/lib/__init__.py
new file mode 100755
index 00000000..b682f178
--- /dev/null
+++ b/src/blobtoolkit-pipeline/src/lib/__init__.py
@@ -0,0 +1,1 @@
+"""This is a template package to demonstrate Continuous Integration."""
diff --git a/src/pipeline/lib/add_summary_to_metadata.py b/src/blobtoolkit-pipeline/src/lib/add_summary_to_metadata.py
similarity index 85%
rename from src/pipeline/lib/add_summary_to_metadata.py
rename to src/blobtoolkit-pipeline/src/lib/add_summary_to_metadata.py
index 7309ca42..6297c883 100755
--- a/src/pipeline/lib/add_summary_to_metadata.py
+++ b/src/blobtoolkit-pipeline/src/lib/add_summary_to_metadata.py
@@ -3,13 +3,14 @@
 """
 Add summary data to metadata.
 
-Usage: btk pipeline add-summary-to-metadata --config FILE --out FILE
+Usage: blobtoolkit-pipeline add-summary-to-metadata --config FILE --out FILE
 
 Options:
     --config FILE   YAML format config file
     --out FILE      Output file
 """
 
+
 import logging
 import re
 import subprocess
@@ -33,7 +34,7 @@
 logging.basicConfig(**logger_config)
 logger = logging.getLogger()
 
-logger.info("Starting script: " + __file__)
+logger.info(f"Starting script: {__file__}")
 
 
 def add_pipeline_version(meta):
@@ -60,12 +61,14 @@ def add_software_versions(meta):
     logger.info("Adding software version details")
     for key, opts in programs.items():
         cmd = [key]
-        flag = opts.get("flag", "--version")
-        if flag:
+        if flag := opts.get("flag", "--version"):
             cmd.append(flag)
         try:
             p = subprocess.Popen(
-                cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8",
+                cmd,
+                stdout=subprocess.PIPE,
+                stderr=subprocess.PIPE,
+                encoding="utf-8",
             )
             (output, err) = p.communicate()
             status = p.wait()
@@ -83,16 +86,19 @@ def add_software_versions(meta):
         except FileNotFoundError:
             version = "not found"
         logger.info("%s: %s", key, version)
-        versions.update({key: version})
+        versions[key] = version
     meta["settings"]["software_versions"] = versions
 
 
-def main():
+def main(rename=None):
     """Entry point."""
+    docs = __doc__
+    if rename is not None:
+        docs = docs.replace("blobtoolkit-pipeline", rename)
     try:
-        args = docopt(__doc__)
-    except DocoptExit:
-        raise DocoptExit
+        args = docopt(docs)
+    except DocoptExit as e:
+        raise DocoptExit from e
     try:
         meta = {}
         config = args["--config"]
@@ -117,6 +123,8 @@ def main():
                 value["url"] = []
         add_pipeline_version(meta)
         add_software_versions(meta)
+        meta["version"] = config.get("version", 1)
+        meta["revision"] = config.get("revision", 0)
 
         yaml.add_representer(
             OrderedDict,
diff --git a/src/pipeline/lib/chunk_fasta.py b/src/blobtoolkit-pipeline/src/lib/chunk_fasta.py
similarity index 82%
rename from src/pipeline/lib/chunk_fasta.py
rename to src/blobtoolkit-pipeline/src/lib/chunk_fasta.py
index 981b5107..cd710d10 100755
--- a/src/pipeline/lib/chunk_fasta.py
+++ b/src/blobtoolkit-pipeline/src/lib/chunk_fasta.py
@@ -2,7 +2,7 @@
 """
 Split long sequences into chunks.
 
-Usage: btk pipeline chunk-fasta --in FASTA [--chunk INT] [--overlap INT] [--max-chunks INT]
+Usage: blobtoolkit-pipeline chunk-fasta --in FASTA [--chunk INT] [--overlap INT] [--max-chunks INT]
                 [--busco TSV] [--min-length INT] [--out FASTA] [--bed BEDFILE]
 
 Options:
@@ -260,64 +260,72 @@ def write_bedfiles(bed_data, args):
 #         pass
 
 
-def main():
+def main(rename=None):
     """Entry point."""
+    docs = __doc__
+    if rename is not None:
+        docs = docs.replace("blobtoolkit-pipeline", rename)
     try:
-        # parse_args()
-        args = docopt(__doc__)
-    except DocoptExit:
-        raise DocoptExit
+        args = docopt(docs)
+    except DocoptExit as e:
+        raise DocoptExit from e
     try:
-        busco_windows = {}
+        make_chunks(args)
+    except Exception as err:
+        logger.error(err)
+        exit(1)
+
+
+def make_chunks(args):
+    busco_windows = (
+        parse_busco_full_summary(args["--busco"])
         if (
             "--busco" in args
             and args["--busco"] is not None
             and args["--busco"] != "None"
+        )
+        else {}
+    )
+    logger.info(f'Splitting {args["--in"]} into chunks')
+    bed_data = defaultdict(list)
+    seqs = []
+    for seq in chunk_fasta(
+        args["--in"],
+        chunk=int(args["--chunk"]),
+        overlap=int(args["--overlap"]),
+        max_chunks=int(args["--max-chunks"]),
+    ):
+        if (
+            busco_windows
+            and seq["chunks"] == int(args["--max-chunks"])
+            and seq["length"] > int(args["--chunk"]) + int(args["--overlap"])
         ):
-            busco_windows = parse_busco_full_summary(args["--busco"])
-        logger.info("Splitting %s into chunks" % args["--in"])
-        bed_data = defaultdict(list)
-        seqs = []
-        for seq in chunk_fasta(
-            args["--in"],
-            chunk=int(args["--chunk"]),
-            overlap=int(args["--overlap"]),
-            max_chunks=int(args["--max-chunks"]),
-        ):
-            if (
-                busco_windows
-                and seq["chunks"] == int(args["--max-chunks"])
-                and seq["length"] > int(args["--chunk"]) + int(args["--overlap"])
-            ):
-                chunk_by_busco(seq, seqs, busco_windows, args)
-            else:
-                if "--bed" in args and args["--bed"] and args["--bed"] != "None":
-                    stats = seq_stats(seq["seq"])
-                    bed_data[seq["title"]].append(
-                        {
-                            "start": seq["start"],
-                            "end": seq["start"] + len(seq["seq"]),
-                            "stats": stats,
-                        }
-                    )
-                if check_for_unmasked_bases(seq["seq"]):
-                    seqs.append((seq))
-        if bed_data:
-            write_bedfiles(bed_data, args)
-        if args["--out"] is not None and args["--out"] != "None":
-            chunked = ""
-            for seq in seqs:
-                if seq["length"] >= int(args["--min-length"]):
-                    chunked += ">%s_-_%d\n" % (seq["title"], seq["start"])
-                    chunked += "%s\n" % seq["seq"]
-            outfile = args["--out"]
-            if outfile.startswith("."):
-                outfile = "%s%s" % (args["--in"], args["--out"])
-            with open(outfile, "w") as ofh:
-                ofh.writelines(chunked)
-    except Exception as err:
-        logger.error(err)
-        exit(1)
+            chunk_by_busco(seq, seqs, busco_windows, args)
+        else:
+            if "--bed" in args and args["--bed"] and args["--bed"] != "None":
+                stats = seq_stats(seq["seq"])
+                bed_data[seq["title"]].append(
+                    {
+                        "start": seq["start"],
+                        "end": seq["start"] + len(seq["seq"]),
+                        "stats": stats,
+                    }
+                )
+            if check_for_unmasked_bases(seq["seq"]):
+                seqs.append((seq))
+    if bed_data:
+        write_bedfiles(bed_data, args)
+    if args["--out"] is not None and args["--out"] != "None":
+        chunked = ""
+        for seq in seqs:
+            if seq["length"] >= int(args["--min-length"]):
+                chunked += ">%s_-_%d\n" % (seq["title"], seq["start"])
+                chunked += "%s\n" % seq["seq"]
+        outfile = args["--out"]
+        if outfile.startswith("."):
+            outfile = f'{args["--in"]}{args["--out"]}'
+        with open(outfile, "w") as ofh:
+            ofh.writelines(chunked)
 
 
 if __name__ == "__main__":
diff --git a/src/pipeline/lib/count_busco_genes.py b/src/blobtoolkit-pipeline/src/lib/count_busco_genes.py
similarity index 92%
rename from src/pipeline/lib/count_busco_genes.py
rename to src/blobtoolkit-pipeline/src/lib/count_busco_genes.py
index 5641ab84..1e261efa 100755
--- a/src/pipeline/lib/count_busco_genes.py
+++ b/src/blobtoolkit-pipeline/src/lib/count_busco_genes.py
@@ -2,7 +2,7 @@
 """
 Count BUSCO genes.
 
-Usage: btk pipeline count-busco-genes --in TSV... --mask TSV --out TSV
+Usage: blobtoolkit-pipeline count-busco-genes --in TSV... --mask TSV --out TSV
 
 Options:
     --in TSV      chunked summary stats tsv file.
@@ -90,13 +90,15 @@ def parse_busco_summary(filename, mask, header):
     return mask, header
 
 
-def main():
+def main(rename=None):
     """Entry point."""
+    docs = __doc__
+    if rename is not None:
+        docs = docs.replace("blobtoolkit-pipeline", rename)
     try:
-        # parse_args()
-        args = docopt(__doc__)
-    except DocoptExit:
-        raise DocoptExit
+        args = docopt(docs)
+    except DocoptExit as e:
+        raise DocoptExit from e
     try:
         mask, header = load_mask(args["--mask"])
         for buscofile in args["--in"]:
diff --git a/src/pipeline/lib/data.py b/src/blobtoolkit-pipeline/src/lib/data.py
similarity index 66%
rename from src/pipeline/lib/data.py
rename to src/blobtoolkit-pipeline/src/lib/data.py
index 393708af..3c10d52b 100755
--- a/src/pipeline/lib/data.py
+++ b/src/blobtoolkit-pipeline/src/lib/data.py
@@ -4,7 +4,7 @@
 
 NOT YET IMPLEMENTED
 
-Usage: blobtools pipeline data --config YAML
+Usage: blobtoolkit-pipeline data --config YAML
 
 Options:
     --config YAML  YAML format configuration filename.
@@ -27,9 +27,12 @@
 logger = logging.getLogger()
 
 
-def main():
+def main(rename=None):
     """Entry point."""
+    docs = __doc__
+    if rename is not None:
+        docs = docs.replace("blobtoolkit-pipeline", rename)
     try:
-        args = docopt(__doc__)
-    except DocoptExit:
-        raise DocoptExit
+        args = docopt(docs)
+    except DocoptExit as e:
+        raise DocoptExit from e
diff --git a/src/pipeline/lib/extract_busco_genes.py b/src/blobtoolkit-pipeline/src/lib/extract_busco_genes.py
similarity index 91%
rename from src/pipeline/lib/extract_busco_genes.py
rename to src/blobtoolkit-pipeline/src/lib/extract_busco_genes.py
index 8ad3022e..4c1e80a4 100755
--- a/src/pipeline/lib/extract_busco_genes.py
+++ b/src/blobtoolkit-pipeline/src/lib/extract_busco_genes.py
@@ -2,7 +2,7 @@
 """
 Extract BUSCO gene sequences and set headers.
 
-Usage: btk pipeline extract-busco-genes --busco PATH... --out FASTA
+Usage: blobtoolkit-pipeline extract-busco-genes --busco PATH... --out FASTA
 
 Options:
     --busco PATH         BUSCO full summary tsv file.
@@ -42,13 +42,15 @@
 #         sys.argv.append(value)
 
 
-def main():
+def main(rename=None):
     """Entry point."""
+    docs = __doc__
+    if rename is not None:
+        docs = docs.replace("blobtoolkit-pipeline", rename)
     try:
-        # parse_args()
-        args = docopt(__doc__)
-    except DocoptExit:
-        raise DocoptExit
+        args = docopt(docs)
+    except DocoptExit as e:
+        raise DocoptExit from e
     file_pattern = re.compile(r"busco_sequences\/(\w+?)_\w+\/(\d+at\d+).faa")
     header_pattern = re.compile(r">\S+:\d+-\d+")
     utf8reader = codecs.getreader("utf-8")
diff --git a/src/pipeline/lib/functions.py b/src/blobtoolkit-pipeline/src/lib/functions.py
similarity index 100%
rename from src/pipeline/lib/functions.py
rename to src/blobtoolkit-pipeline/src/lib/functions.py
diff --git a/src/pipeline/lib/generate_config.py b/src/blobtoolkit-pipeline/src/lib/generate_config.py
similarity index 98%
rename from src/pipeline/lib/generate_config.py
rename to src/blobtoolkit-pipeline/src/lib/generate_config.py
index 2cf92782..8f34edf9 100755
--- a/src/pipeline/lib/generate_config.py
+++ b/src/blobtoolkit-pipeline/src/lib/generate_config.py
@@ -4,7 +4,7 @@
 Generate config files for BlobToolKit pipeline.
 
 Usage:
-  btk pipeline generate-config <ACCESSION> [--coverage 30] [--download]
+  blobtoolkit-pipeline generate-config <ACCESSION> [--coverage 30] [--download]
     [--out /path/to/output/directory] [--db /path/to/database/directory]
     [--db-suffix STRING] [--reads STRING...] [--read-runs INT] [--api-key STRING]
     [--platforms STRING] [--datasets] [--protocol STRING] 
@@ -46,6 +46,7 @@
 import ujson
 import yaml
 from defusedxml import ElementTree as ET
+from docopt import DocoptExit
 from docopt import docopt
 from tolkein import tofetch
 from tolkein import tofile
@@ -488,10 +489,10 @@ def fetch_read_info(accession, per_platform):
             ):
                 platform = "ILLUMINA_XTEN"
             values[header[i]] = value
-        if "base_count" in values:
+        try:
             values["base_count"] = int(values["base_count"])
-        else:
-            values["base_count"] = 0
+        except (KeyError, ValueError):
+            continue
         try:
             per_platform[platform].append(values)
         except KeyError:
@@ -691,9 +692,15 @@ def set_defaults(opts):
     )
 
 
-def main():
+def main(rename=None):
     """Entry point."""
-    opts = docopt(__doc__)
+    docs = __doc__
+    if rename is not None:
+        docs = docs.replace("blobtoolkit-pipeline", rename)
+    try:
+        opts = docopt(docs)
+    except DocoptExit as e:
+        raise DocoptExit from e
     (
         accession,
         outdir,
diff --git a/src/pipeline/lib/generate_configs.sh b/src/blobtoolkit-pipeline/src/lib/generate_configs.sh
similarity index 100%
rename from src/pipeline/lib/generate_configs.sh
rename to src/blobtoolkit-pipeline/src/lib/generate_configs.sh
diff --git a/src/pipeline/lib/generate_static_images.py b/src/blobtoolkit-pipeline/src/lib/generate_static_images.py
similarity index 94%
rename from src/pipeline/lib/generate_static_images.py
rename to src/blobtoolkit-pipeline/src/lib/generate_static_images.py
index 7f2774fa..6b27b7a9 100755
--- a/src/pipeline/lib/generate_static_images.py
+++ b/src/blobtoolkit-pipeline/src/lib/generate_static_images.py
@@ -4,7 +4,7 @@
 Generate static image files.
 
 Usage:
-  btk pipeline generate-static-images --blobdir STRING [--coverage] [--host STRING]
+  blobtoolkit-pipeline generate-static-images --blobdir STRING [--coverage] [--host STRING]
                          [--ports RANGE] [--timeout INT] [--path PATH]
 
 Options:
@@ -54,13 +54,15 @@
 #         pass
 
 
-def main():
+def main(rename=None):
     """Entry point."""
+    docs = __doc__
+    if rename is not None:
+        docs = docs.replace("blobtoolkit-pipeline", rename)
     try:
-        # parse_args()
-        args = docopt(__doc__)
-    except DocoptExit:
-        raise DocoptExit
+        args = docopt(docs)
+    except DocoptExit as e:
+        raise DocoptExit from e
 
     blob_path = "%s/%s" % (args["--path"], args["--blobdir"])
 
diff --git a/src/pipeline/lib/lsf_wrapper.sh b/src/blobtoolkit-pipeline/src/lib/lsf_wrapper.sh
similarity index 100%
rename from src/pipeline/lib/lsf_wrapper.sh
rename to src/blobtoolkit-pipeline/src/lib/lsf_wrapper.sh
diff --git a/src/pipeline/lib/resume_pipeline.py b/src/blobtoolkit-pipeline/src/lib/resume_pipeline.py
similarity index 93%
rename from src/pipeline/lib/resume_pipeline.py
rename to src/blobtoolkit-pipeline/src/lib/resume_pipeline.py
index 49fdac3f..7cf9f4ca 100755
--- a/src/pipeline/lib/resume_pipeline.py
+++ b/src/blobtoolkit-pipeline/src/lib/resume_pipeline.py
@@ -4,7 +4,7 @@
 Restore intermediate files from partial analyses.
 
 Usage:
-  btk pipeline resume-pipeline --in PATH [--update-cov] --out PATH
+  blobtoolkit-pipeline resume-pipeline --in PATH [--update-cov] --out PATH
 
 Options:
   --in PATH              Path to input directory.
@@ -22,6 +22,7 @@
 from time import sleep
 
 import yaml
+from docopt import DocoptExit
 from docopt import docopt
 from tolkein import tofile
 from tolkein import tolog
@@ -90,9 +91,15 @@ def list_accessions(config):
     return f"{gca} {' '.join(paired+single)}"
 
 
-def main():
+def main(rename=None):
     """Entry point."""
-    opts = docopt(__doc__)
+    docs = __doc__
+    if rename is not None:
+        docs = docs.replace("blobtoolkit-pipeline", rename)
+    try:
+        opts = docopt(docs)
+    except DocoptExit as e:
+        raise DocoptExit from e
     indir = opts["--in"]
     config_file = "%s/config.yaml" % indir
     data = tofile.read_file(config_file)
diff --git a/src/pipeline/lib/run.py b/src/blobtoolkit-pipeline/src/lib/run.py
similarity index 91%
rename from src/pipeline/lib/run.py
rename to src/blobtoolkit-pipeline/src/lib/run.py
index d964f995..4a339fcb 100755
--- a/src/pipeline/lib/run.py
+++ b/src/blobtoolkit-pipeline/src/lib/run.py
@@ -2,7 +2,7 @@
 """
 Run BlobToolKit Pipeline.
 
-Usage: btk pipeline run [--dry-run] [--tool STRING] [--unlock]
+Usage: blobtoolkit-pipeline run [--dry-run] [--tool STRING] [--unlock]
                         [--workdir DIR] [--threads INT] --config YAML
 
 Options:
@@ -85,12 +85,15 @@ def run_pipeline(workdir, args):
     exit_code = run_command(cmd)
 
 
-def main():
+def main(rename=None):
     """Entry point."""
+    docs = __doc__
+    if rename is not None:
+        docs = docs.replace("blobtoolkit-pipeline", rename)
     try:
-        args = docopt(__doc__)
-    except DocoptExit:
-        raise DocoptExit
+        args = docopt(docs)
+    except DocoptExit as e:
+        raise DocoptExit from e
     try:
         config = os.path.abspath(args["--config"])
         if args["--workdir"] is not None:
diff --git a/src/pipeline/lib/run_btk_pipeline.sh b/src/blobtoolkit-pipeline/src/lib/run_btk_pipeline.sh
similarity index 100%
rename from src/pipeline/lib/run_btk_pipeline.sh
rename to src/blobtoolkit-pipeline/src/lib/run_btk_pipeline.sh
diff --git a/src/pipeline/lib/track_bioproject_accessions.py b/src/blobtoolkit-pipeline/src/lib/track_bioproject_accessions.py
similarity index 100%
rename from src/pipeline/lib/track_bioproject_accessions.py
rename to src/blobtoolkit-pipeline/src/lib/track_bioproject_accessions.py
diff --git a/src/pipeline/lib/transfer_completed.py b/src/blobtoolkit-pipeline/src/lib/transfer_completed.py
similarity index 95%
rename from src/pipeline/lib/transfer_completed.py
rename to src/blobtoolkit-pipeline/src/lib/transfer_completed.py
index 73a8158e..cad29ff2 100755
--- a/src/pipeline/lib/transfer_completed.py
+++ b/src/blobtoolkit-pipeline/src/lib/transfer_completed.py
@@ -4,7 +4,7 @@
 Transfer completed BlobDirs and intermediate files.
 
 Usage:
-  btk pipeline transfer-completed --in PATH --out PATH [--bin PATH]
+  blobtoolkit-pipeline transfer-completed --in PATH --out PATH [--bin PATH]
 
 Options:
   --in PATH              Path to input directory.
@@ -108,9 +108,15 @@ def remove_unwanted_files(indir, bindir):
         shutil.rmtree(indir)
 
 
-def main():
+def main(rename=None):
     """Entry point."""
-    opts = docopt(__doc__)
+    docs = __doc__
+    if rename is not None:
+        docs = docs.replace("blobtoolkit-pipeline", rename)
+    try:
+        opts = docopt(docs)
+    except DocoptExit as e:
+        raise DocoptExit from e
     data = tofile.read_file("%s/config.yaml" % opts["--in"])
     meta = yaml.full_load(data)
     prefix = meta["assembly"]["prefix"]
diff --git a/src/pipeline/lib/unchunk_blast.py b/src/blobtoolkit-pipeline/src/lib/unchunk_blast.py
similarity index 88%
rename from src/pipeline/lib/unchunk_blast.py
rename to src/blobtoolkit-pipeline/src/lib/unchunk_blast.py
index b1159804..9bacbdbe 100755
--- a/src/pipeline/lib/unchunk_blast.py
+++ b/src/blobtoolkit-pipeline/src/lib/unchunk_blast.py
@@ -4,7 +4,7 @@
 
 Update coordinates and remove seq name suffix from chunked blast results.
 
-Usage: btk pipeline unchunk-blast [--count INT] --in TSV --out TSV
+Usage: blobtoolkit-pipeline unchunk-blast [--count INT] --in TSV --out TSV
 
 Options:
     --in TSV     input filename.
@@ -43,13 +43,15 @@
 #         pass
 
 
-def main():
+def main(rename=None):
     """Entry point."""
+    docs = __doc__
+    if rename is not None:
+        docs = docs.replace("blobtoolkit-pipeline", rename)
     try:
-        # parse_args()
-        args = docopt(__doc__)
-    except DocoptExit:
-        raise DocoptExit
+        args = docopt(docs)
+    except DocoptExit as e:
+        raise DocoptExit from e
     try:
         lines = defaultdict(dict)
         chunk_counts = defaultdict(int)
diff --git a/src/blobtoolkit-pipeline/src/lib/version.py b/src/blobtoolkit-pipeline/src/lib/version.py
new file mode 100644
index 00000000..42ccafe8
--- /dev/null
+++ b/src/blobtoolkit-pipeline/src/lib/version.py
@@ -0,0 +1,4 @@
+#!/usr/bin/env python3
+"""blobtoolkit version."""
+
+__version__ = "blobtoolkit-pipeline v4.0.2"
diff --git a/src/pipeline/lib/window_stats.py b/src/blobtoolkit-pipeline/src/lib/window_stats.py
similarity index 95%
rename from src/pipeline/lib/window_stats.py
rename to src/blobtoolkit-pipeline/src/lib/window_stats.py
index fd0fa5a1..06d0675f 100755
--- a/src/pipeline/lib/window_stats.py
+++ b/src/blobtoolkit-pipeline/src/lib/window_stats.py
@@ -4,7 +4,7 @@
 Window stats.
 
 Usage:
-    btk pipeline window-stats --in TSV [--window FLOAT...] [--min-window-length INT] [--min-window-count INT] --out TSV
+    blobtoolkit-pipeline window-stats --in TSV [--window FLOAT...] [--min-window-length INT] [--min-window-count INT] --out TSV
 
 Options:
     --in TSV                 chunked summary stats tsv file.
@@ -154,13 +154,15 @@ def calculate_window_stats(lengths, chunks, window, interval, args):
     return values
 
 
-def main():
+def main(rename=None):
     """Entry point."""
+    docs = __doc__
+    if rename is not None:
+        docs = docs.replace("blobtoolkit-pipeline", rename)
     try:
-        # parse_args()
-        args = docopt(__doc__)
-    except DocoptExit:
-        raise DocoptExit
+        args = docopt(docs)
+    except DocoptExit as e:
+        raise DocoptExit from e
     try:
         lengths, chunks, interval = parse_chunked_values(args["--in"])
         outfile = args["--out"]
diff --git a/src/pipeline/minimap.smk b/src/blobtoolkit-pipeline/src/minimap.smk
similarity index 100%
rename from src/pipeline/minimap.smk
rename to src/blobtoolkit-pipeline/src/minimap.smk
diff --git a/src/pipeline/rules/_transfer_dataset.smk b/src/blobtoolkit-pipeline/src/rules/_transfer_dataset.smk
similarity index 100%
rename from src/pipeline/rules/_transfer_dataset.smk
rename to src/blobtoolkit-pipeline/src/rules/_transfer_dataset.smk
diff --git a/src/pipeline/rules/add_cov_to_tsv.smk b/src/blobtoolkit-pipeline/src/rules/add_cov_to_tsv.smk
similarity index 100%
rename from src/pipeline/rules/add_cov_to_tsv.smk
rename to src/blobtoolkit-pipeline/src/rules/add_cov_to_tsv.smk
diff --git a/src/pipeline/rules/add_summary_to_metadata.smk b/src/blobtoolkit-pipeline/src/rules/add_summary_to_metadata.smk
similarity index 100%
rename from src/pipeline/rules/add_summary_to_metadata.smk
rename to src/blobtoolkit-pipeline/src/rules/add_summary_to_metadata.smk
diff --git a/src/pipeline/rules/checksum_files.smk b/src/blobtoolkit-pipeline/src/rules/checksum_files.smk
similarity index 100%
rename from src/pipeline/rules/checksum_files.smk
rename to src/blobtoolkit-pipeline/src/rules/checksum_files.smk
diff --git a/src/pipeline/rules/chunk_fasta.smk b/src/blobtoolkit-pipeline/src/rules/chunk_fasta.smk
similarity index 100%
rename from src/pipeline/rules/chunk_fasta.smk
rename to src/blobtoolkit-pipeline/src/rules/chunk_fasta.smk
diff --git a/src/pipeline/rules/chunk_fasta_by_busco.smk b/src/blobtoolkit-pipeline/src/rules/chunk_fasta_by_busco.smk
similarity index 100%
rename from src/pipeline/rules/chunk_fasta_by_busco.smk
rename to src/blobtoolkit-pipeline/src/rules/chunk_fasta_by_busco.smk
diff --git a/src/pipeline/rules/chunk_nohit_fasta.smk b/src/blobtoolkit-pipeline/src/rules/chunk_nohit_fasta.smk
similarity index 100%
rename from src/pipeline/rules/chunk_nohit_fasta.smk
rename to src/blobtoolkit-pipeline/src/rules/chunk_nohit_fasta.smk
diff --git a/src/pipeline/rules/combine_stats.smk b/src/blobtoolkit-pipeline/src/rules/combine_stats.smk
similarity index 100%
rename from src/pipeline/rules/combine_stats.smk
rename to src/blobtoolkit-pipeline/src/rules/combine_stats.smk
diff --git a/src/pipeline/rules/count_busco_genes.smk b/src/blobtoolkit-pipeline/src/rules/count_busco_genes.smk
similarity index 100%
rename from src/pipeline/rules/count_busco_genes.smk
rename to src/blobtoolkit-pipeline/src/rules/count_busco_genes.smk
diff --git a/src/pipeline/rules/extract_busco_genes.smk b/src/blobtoolkit-pipeline/src/rules/extract_busco_genes.smk
similarity index 89%
rename from src/pipeline/rules/extract_busco_genes.smk
rename to src/blobtoolkit-pipeline/src/rules/extract_busco_genes.smk
index 9d64ea93..392367ae 100644
--- a/src/pipeline/rules/extract_busco_genes.smk
+++ b/src/blobtoolkit-pipeline/src/rules/extract_busco_genes.smk
@@ -7,14 +7,12 @@ rule extract_busco_genes:
     output:
         fasta = "{assembly}.busco_genes.fasta"
     params:
-        busco = lambda wc: " --busco ".join(expand("%s/%s.busco.{lineage}/full_table.tsv.gz" % (busco_path, wc.assembly), lineage=config['busco']['lineages'])),
+        busco = lambda wc: " --busco ".join(expand("%s/%s.busco.{lineage}/full_table.tsv.gz" % (busco_path, wc.assembly), lineage=get_basal_lineages(config))),
     threads: 1
     log:
         "logs/{assembly}/extract_busco_genes.log"
     benchmark:
         "logs/{assembly}/extract_busco_genes.benchmark.txt"
-    # script:
-    #     "../lib/extract_busco_genes.py"
 
     shell:
         """(btk pipeline extract-busco-genes \
diff --git a/src/pipeline/rules/extract_nohit_fasta.smk b/src/blobtoolkit-pipeline/src/rules/extract_nohit_fasta.smk
similarity index 100%
rename from src/pipeline/rules/extract_nohit_fasta.smk
rename to src/blobtoolkit-pipeline/src/rules/extract_nohit_fasta.smk
diff --git a/src/pipeline/rules/generate_images.smk b/src/blobtoolkit-pipeline/src/rules/generate_images.smk
similarity index 100%
rename from src/pipeline/rules/generate_images.smk
rename to src/blobtoolkit-pipeline/src/rules/generate_images.smk
diff --git a/src/pipeline/rules/generate_summary.smk b/src/blobtoolkit-pipeline/src/rules/generate_summary.smk
similarity index 100%
rename from src/pipeline/rules/generate_summary.smk
rename to src/blobtoolkit-pipeline/src/rules/generate_summary.smk
diff --git a/src/pipeline/rules/get_chunked_stats.smk b/src/blobtoolkit-pipeline/src/rules/get_chunked_stats.smk
similarity index 100%
rename from src/pipeline/rules/get_chunked_stats.smk
rename to src/blobtoolkit-pipeline/src/rules/get_chunked_stats.smk
diff --git a/src/pipeline/rules/get_window_stats.smk b/src/blobtoolkit-pipeline/src/rules/get_window_stats.smk
similarity index 100%
rename from src/pipeline/rules/get_window_stats.smk
rename to src/blobtoolkit-pipeline/src/rules/get_window_stats.smk
diff --git a/src/pipeline/rules/run_bamtools_stats.smk b/src/blobtoolkit-pipeline/src/rules/run_bamtools_stats.smk
similarity index 100%
rename from src/pipeline/rules/run_bamtools_stats.smk
rename to src/blobtoolkit-pipeline/src/rules/run_bamtools_stats.smk
diff --git a/src/pipeline/rules/run_blastn.smk b/src/blobtoolkit-pipeline/src/rules/run_blastn.smk
similarity index 100%
rename from src/pipeline/rules/run_blastn.smk
rename to src/blobtoolkit-pipeline/src/rules/run_blastn.smk
diff --git a/src/pipeline/rules/run_blobtools_add.smk b/src/blobtoolkit-pipeline/src/rules/run_blobtools_add.smk
similarity index 100%
rename from src/pipeline/rules/run_blobtools_add.smk
rename to src/blobtoolkit-pipeline/src/rules/run_blobtools_add.smk
diff --git a/src/pipeline/rules/run_blobtools_create.smk b/src/blobtoolkit-pipeline/src/rules/run_blobtools_create.smk
similarity index 100%
rename from src/pipeline/rules/run_blobtools_create.smk
rename to src/blobtoolkit-pipeline/src/rules/run_blobtools_create.smk
diff --git a/src/pipeline/rules/run_busco5.smk b/src/blobtoolkit-pipeline/src/rules/run_busco5.smk
similarity index 100%
rename from src/pipeline/rules/run_busco5.smk
rename to src/blobtoolkit-pipeline/src/rules/run_busco5.smk
diff --git a/src/pipeline/rules/run_diamond_blastp.smk b/src/blobtoolkit-pipeline/src/rules/run_diamond_blastp.smk
similarity index 100%
rename from src/pipeline/rules/run_diamond_blastp.smk
rename to src/blobtoolkit-pipeline/src/rules/run_diamond_blastp.smk
diff --git a/src/pipeline/rules/run_diamond_blastx.smk b/src/blobtoolkit-pipeline/src/rules/run_diamond_blastx.smk
similarity index 100%
rename from src/pipeline/rules/run_diamond_blastx.smk
rename to src/blobtoolkit-pipeline/src/rules/run_diamond_blastx.smk
diff --git a/src/pipeline/rules/run_minimap2_align.smk b/src/blobtoolkit-pipeline/src/rules/run_minimap2_align.smk
similarity index 100%
rename from src/pipeline/rules/run_minimap2_align.smk
rename to src/blobtoolkit-pipeline/src/rules/run_minimap2_align.smk
diff --git a/src/pipeline/rules/run_minimap2_index.smk b/src/blobtoolkit-pipeline/src/rules/run_minimap2_index.smk
similarity index 100%
rename from src/pipeline/rules/run_minimap2_index.smk
rename to src/blobtoolkit-pipeline/src/rules/run_minimap2_index.smk
diff --git a/src/pipeline/rules/run_mosdepth.smk b/src/blobtoolkit-pipeline/src/rules/run_mosdepth.smk
similarity index 100%
rename from src/pipeline/rules/run_mosdepth.smk
rename to src/blobtoolkit-pipeline/src/rules/run_mosdepth.smk
diff --git a/src/pipeline/rules/run_sub_pipeline.smk b/src/blobtoolkit-pipeline/src/rules/run_sub_pipeline.smk
similarity index 100%
rename from src/pipeline/rules/run_sub_pipeline.smk
rename to src/blobtoolkit-pipeline/src/rules/run_sub_pipeline.smk
diff --git a/src/pipeline/rules/run_windowmasker.smk b/src/blobtoolkit-pipeline/src/rules/run_windowmasker.smk
similarity index 100%
rename from src/pipeline/rules/run_windowmasker.smk
rename to src/blobtoolkit-pipeline/src/rules/run_windowmasker.smk
diff --git a/src/pipeline/rules/unchunk_blastn.smk b/src/blobtoolkit-pipeline/src/rules/unchunk_blastn.smk
similarity index 100%
rename from src/pipeline/rules/unchunk_blastn.smk
rename to src/blobtoolkit-pipeline/src/rules/unchunk_blastn.smk
diff --git a/src/pipeline/rules/unchunk_blastx.smk b/src/blobtoolkit-pipeline/src/rules/unchunk_blastx.smk
similarity index 100%
rename from src/pipeline/rules/unchunk_blastx.smk
rename to src/blobtoolkit-pipeline/src/rules/unchunk_blastx.smk
diff --git a/src/pipeline/rules/unzip_assembly_fasta.smk b/src/blobtoolkit-pipeline/src/rules/unzip_assembly_fasta.smk
similarity index 100%
rename from src/pipeline/rules/unzip_assembly_fasta.smk
rename to src/blobtoolkit-pipeline/src/rules/unzip_assembly_fasta.smk
diff --git a/src/pipeline/rules/validate_dataset.smk b/src/blobtoolkit-pipeline/src/rules/validate_dataset.smk
similarity index 100%
rename from src/pipeline/rules/validate_dataset.smk
rename to src/blobtoolkit-pipeline/src/rules/validate_dataset.smk
diff --git a/src/pipeline/view.smk b/src/blobtoolkit-pipeline/src/view.smk
similarity index 100%
rename from src/pipeline/view.smk
rename to src/blobtoolkit-pipeline/src/view.smk
diff --git a/src/pipeline/window_stats.smk b/src/blobtoolkit-pipeline/src/window_stats.smk
similarity index 100%
rename from src/pipeline/window_stats.smk
rename to src/blobtoolkit-pipeline/src/window_stats.smk
diff --git a/src/pipeline/windowmasker.smk b/src/blobtoolkit-pipeline/src/windowmasker.smk
similarity index 100%
rename from src/pipeline/windowmasker.smk
rename to src/blobtoolkit-pipeline/src/windowmasker.smk
diff --git a/src/blobtools/__init__.py b/src/blobtools/__init__.py
index 1f14b333..7a152e4f 100644
--- a/src/blobtools/__init__.py
+++ b/src/blobtools/__init__.py
@@ -1,10 +1,15 @@
 """BlobToolKit."""
 
+
+import contextlib
+
 from .blobtools import cli
 from .lib import add
 from .lib import filter
-from .lib import host
-from .lib import view
+
+with contextlib.suppress(ModuleNotFoundError):
+    from .lib import host
+    from .lib import view
 
 # from .field import Array, Category, Field, Identifier, MultiArray, Variable
 # from .taxdump import Taxdump
diff --git a/src/blobtools/blobtools.py b/src/blobtools/blobtools.py
index b302c679..bd34bde7 100755
--- a/src/blobtools/blobtools.py
+++ b/src/blobtools/blobtools.py
@@ -20,25 +20,25 @@
 
 examples:
     # 1. Create a new BlobDir from a FASTA file:
-    ./blobtools create --fasta examples/assembly.fasta BlobDir
+    blobtools create --fasta examples/assembly.fasta BlobDir
 
     # 2. Create a new BlobDir from a BlobDB:
-    ./blobtools create --blobdb examples/blobDB.json BlobDir
+    blobtools create --blobdb examples/blobDB.json BlobDir
 
     # 3. Add Coverage data from a BAM file:
-    ./blobtools add --cov examples/assembly.reads.bam BlobDir
+    blobtools add --cov examples/assembly.reads.bam BlobDir
 
     # 4. Assign taxonomy from BLAST hits:
-    ./blobtools add add --hits examples/blast.out --taxdump ../taxdump BlobDir
+    blobtools add add --hits examples/blast.out --taxdump ../taxdump BlobDir
 
     # 5. Add BUSCO results:
-    ./blobtools add --busco examples/busco.tsv BlobDir
+    blobtools add --busco examples/busco.tsv BlobDir
 
     # 6. Host an interactive viewer:
-    ./blobtools host BlobDir
+    blobtools host BlobDir
 
     # 7. Filter a BlobDir:
-    ./blobtools filter --param length--Min=5000 --output BlobDir_len_gt_5000 BlobDir
+    blobtools filter --param length--Min=5000 --output BlobDir_len_gt_5000 BlobDir
 """
 
 
@@ -55,6 +55,22 @@
 LOGGER = tolog.logger(__name__)
 
 
+def suggest_option(command):
+    options = {
+        "host": ["view"],
+    }
+    options_list = options.get(command, [])
+    options_list.append("full")
+    LOGGER.error(
+        f"The `blobtools {command}` command is not available, to enable this option use{' one of' if len(options_list)>1 else ''}:"
+    )
+    for option in options_list:
+        print(
+            f"                                - pip install blobtoolkit[{option}]",
+            file=sys.stderr,
+        )
+
+
 def cli():
     """Entry point."""
     args = {}
@@ -84,8 +100,12 @@ def cli():
             sys.argv[command_index] = "add"
         for entry_point in working_set.iter_entry_points("%s.subcmd" % args["<tool>"]):
             if entry_point.name == args["<command>"]:
-                subcommand = entry_point.load()
-                sys.exit(subcommand())
+                try:
+                    subcommand = entry_point.load()
+                    sys.exit(subcommand())
+                except ModuleNotFoundError:
+                    suggest_option(args["<command>"])
+                    exit(1)
         LOGGER.error(
             "'%s %s' is not a valid command", args["<tool>"], args["<command>"]
         )
diff --git a/src/blobtools/lib/add.py b/src/blobtools/lib/add.py
index 58dc4eee..6d954e46 100755
--- a/src/blobtools/lib/add.py
+++ b/src/blobtools/lib/add.py
@@ -60,7 +60,7 @@
 
 Examples:
     # 1. Add BUSCO scores to BlobDir
-    ./blobtools add --busco busco.full_table.tsv BlobDir
+    blobtools add --busco busco.full_table.tsv BlobDir
 
 """
 
diff --git a/src/blobtools/lib/bed.py b/src/blobtools/lib/bed.py
index 9d3f8d7f..1b8d63a5 100644
--- a/src/blobtools/lib/bed.py
+++ b/src/blobtools/lib/bed.py
@@ -19,12 +19,10 @@
 from tolkein import tofile
 from tqdm import tqdm
 
-from ..lib import file_io
 from .field import Array
 from .field import Identifier
 from .field import MultiArray
 from .field import Variable
-from .run_external import seqtk_subseq
 
 
 def field_settings():
diff --git a/src/blobtools/lib/cov.py b/src/blobtools/lib/cov.py
index 71c7525a..3fe74a58 100644
--- a/src/blobtools/lib/cov.py
+++ b/src/blobtools/lib/cov.py
@@ -13,11 +13,11 @@
 from pathlib import Path
 
 import pysam
+from blobtoolkit_core import filter
 from tqdm import tqdm
 
 from .field import Variable
 from .file_io import load_yaml
-from .run_external import seqtk_subseq
 
 
 def check_mapped_read(read):
@@ -132,24 +132,19 @@ def apply_filter(ids, fastq_files, **kwargs):
     """Filter FASTQ file based on read alignment file."""
     suffix = kwargs["--suffix"]
     bam_file = kwargs["--cov"]
-    read_ids = set()
-    filetype_letter = Path(bam_file).suffix[1]
-    index_file = Path("%s.csi" % bam_file)
-    if not index_file.is_file():
-        pysam.index(bam_file)
+    options = {"list": ids, "suffix": suffix, "fastq_out": True}
+    filetype = Path(bam_file).suffix
+    if filetype == ".bam":
+        options["bam"] = bam_file
+    elif filetype == ".cram":
+        options["cram"] = bam_file
     else:
-        index_file = False
-    samfile = pysam.AlignmentFile(bam_file, "r%s" % filetype_letter)
-    for seq_id in tqdm(ids):
-        for read in samfile.fetch(seq_id):
-            read_ids.add(read.query_name)
-    samfile.close()
-    if index_file:
-        os.remove(index_file)
-    for fastq_file in fastq_files:
-        path = Path(fastq_file)
-        outfile = path.parent / (path.stem + "." + suffix + path.suffix)
-        seqtk_subseq(fastq_file, "\n".join(list(read_ids)), outfile)
+        print("ERROR: Alignment file suffix %s is not suported", filetype)
+        sys.exit(1)
+    options["fastq1"] = fastq_files[0]
+    if len(fastq_files) > 1:
+        options["fastq2"] = fastq_files[1]
+    filter.fastx(options)
 
 
 def parse_json_cov(json_file, **kwargs):
@@ -276,7 +271,7 @@ def weighted_mean(values, weights, log=False):
                 for value, weight in zip(values, weights)
             ]
         ) / sum(weights)
-        mean = 10 ** mean - 0.01
+        mean = 10**mean - 0.01
     else:
         mean = sum([value * weight for value, weight in zip(values, weights)]) / sum(
             weights
diff --git a/src/blobtools/lib/fasta.py b/src/blobtools/lib/fasta.py
index fd400cfc..1bf6b3da 100644
--- a/src/blobtools/lib/fasta.py
+++ b/src/blobtools/lib/fasta.py
@@ -8,12 +8,12 @@
 from collections import Counter
 from collections import OrderedDict
 
+from blobtoolkit_core import filter
 from tqdm import tqdm
 
 from ..lib import file_io
 from .field import Identifier
 from .field import Variable
-from .run_external import seqtk_subseq
 
 
 def base_composition(seq_str):
@@ -36,9 +36,9 @@ def base_composition(seq_str):
 def apply_filter(ids, fasta_file, **kwargs):
     """Filter FASTA format assembly."""
     suffix = kwargs["--suffix"]
-    path = pathlib.Path(fasta_file)
-    outfile = path.parent / (path.stem + "." + suffix + path.suffix)
-    seqtk_subseq(fasta_file, "\n".join(ids), outfile)
+    filter.fastx(
+        {"list": ids, "fasta": fasta_file, "suffix": suffix, "fasta_out": True}
+    )
 
 
 def parse(file, **kwargs):
diff --git a/src/blobtools/lib/host.py b/src/blobtools/lib/host.py
index ab201630..63ccd800 100755
--- a/src/blobtools/lib/host.py
+++ b/src/blobtools/lib/host.py
@@ -1,284 +1,10 @@
 #!/usr/bin/env python3
 
-# pylint: disable=no-member, too-many-branches, too-many-statements, too-many-locals, W0603, W0703
-
-"""
-Host a collection of BlobDirs.
-
-Usage:
-    blobtools host [--port INT]  [--api-port INT]
-                   [--hostname STRING] DIRECTORY
-
-Arguments:
-    DIRECTORY             Directory containing one or more BlobDirs.
-
-Options:
-    --port INT            HTTP port number. [Default: 8080]
-    --api-port INT        API port number. [Default: 8000]
-    --hostname STRING     Hostname used to connect to API. [Default: localhost]
-"""
-
-
-import contextlib
-import os
-import platform
-import shlex
-import signal
-import socket
-import sys
-import time
-from pathlib import Path
-from shutil import which
-from subprocess import PIPE
-from subprocess import Popen
-
-import psutil
-from docopt import docopt
-
-from .version import __version__
-
-PIDS = []
-
-
-def iter_user_procs(process):
-    """Iterate through processes owned by the current user."""
-    username = psutil.Process.username(
-        process
-    )  # get username in format used by processes
-    for proc in psutil.process_iter(["username"]):
-        if proc.info["username"] == username:
-            yield proc
-
-
-def close_ports(process, args):
-    """Close processes running on chosen ports."""
-    for proc in iter_user_procs(process):
-        try:
-            for conns in proc.connections(kind="inet"):
-                if conns.laddr.port == args["--api-port"]:
-                    proc.send_signal(signal.SIGTERM)
-                elif conns.laddr.port == args["--port"]:
-                    proc.send_signal(signal.SIGTERM)
-        except psutil.NoSuchProcess:
-            continue
-        except psutil.ZombieProcess:
-            continue
-        except psutil.AccessDenied:
-            continue
-
-
-def kill_child_processes(parent_pid, args=None, sig=signal.SIGTERM):
-    """Kill all child processes."""
-    try:
-        parent = psutil.Process(parent_pid)
-    except psutil.NoSuchProcess:
-        return
-    # if args:
-    #     close_ports(parent, args)
-    children = parent.children(recursive=True)
-    for process in children:
-        try:
-            process.send_signal(sig)
-        except psutil.NoSuchProcess:
-            continue
-        except psutil.ZombieProcess:
-            continue
-    parent.send_signal(sig)
-
-
-def test_port(port, service):
-    """Exit if port is already in use."""
-    port = int(port)
-    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as skt:
-        try:
-            skt.bind(("", port))
-        except OSError:
-            if service == "test":
-                return False
-            print("ERROR: Port %d already in use, unable to host %s." % (port, service))
-            print(
-                "       Use: `lsof -nP -iTCP:%d | grep LISTEN` to find the associated process."
-                % port
-            )
-            print(
-                f"       It may take ~30s for this port to become available when restarting {service}."
-            )
-
-            sys.exit(1)
-    return True
-
-
-def find_binary(tool):
-    """Find a binary executable for the blobtoolkit viewer or API."""
-    script_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
-    system = platform.system()
-    # arch, _ = platform.architecture()
-    default_binaries = {"api": "blobtoolkit-api", "viewer": "blobtoolkit-viewer"}
-    if system == "Darwin":
-        binaries = {
-            "api": "blobtoolkit-api-macos",
-            "viewer": "blobtoolkit-viewer-macos",
-        }
-    elif system == "Linux":
-        binaries = {
-            "api": "blobtoolkit-api-linux",
-            "viewer": "blobtoolkit-viewer-linux",
-        }
-    elif system == "Windows":
-        binaries = {
-            "api": "blobtoolkit-api-win.exe",
-            "viewer": "blobtoolkit-viewer-win.exe",
-        }
-        default_binaries = {
-            "api": "blobtoolkit-api.exe",
-            "viewer": "blobtoolkit-viewer.exe",
-        }
-    if which(default_binaries[tool]) is not None:
-        return default_binaries[tool]
-    if which(binaries[tool]) is not None:
-        return default_binaries[tool]
-    executable = binaries[tool]
-    executable_path = os.path.join(
-        os.path.dirname(script_dir), "data", "bin", executable
-    )
-    if os.path.isfile(executable_path):
-        return executable_path
-    print(
-        f"ERROR: {default_binaries[tool]} executable was not found. Please add {default_binaries[tool]} to your PATH."
-    )
-
-    sys.exit(1)
-
-
-def start_api(port, api_port, hostname, directory):
-    """Start BlobToolKit API."""
-    cmd = find_binary("api")
-    # cmd = "blobtoolkit-api"
-    origins = "http://localhost:%d http://localhost null" % int(port)
-    if hostname != "localhost":
-        origins += " http://%s:%d http://%s" % (hostname, int(port), hostname)
-    if directory == "_":
-        env = dict(
-            os.environ,
-            BTK_API_PORT=api_port,
-            BTK_ORIGINS=origins,
-        )
-    else:
-        env = dict(
-            os.environ,
-            BTK_API_PORT=api_port,
-            BTK_FILE_PATH=directory,
-            BTK_ORIGINS=origins,
-        )
-    return Popen(
-        shlex.split(cmd),
-        stdout=PIPE,
-        stderr=PIPE,
-        encoding="ascii",
-        env=env,
-    )
-
-
-def start_viewer(port, api_port, hostname):
-    """Start BlobToolKit viewer."""
-    cmd = find_binary("viewer")
-    # cmd = "blobtoolkit-viewer"
-    api_url = "http://%s:%d/api/v1" % (hostname, int(api_port))
-    return Popen(
-        shlex.split(cmd),
-        stdout=PIPE,
-        stderr=PIPE,
-        encoding="ascii",
-        env=dict(
-            os.environ,
-            BTK_HOST=hostname,
-            BTK_CLIENT_PORT=port,
-            BTK_API_PORT=api_port,
-            BTK_API_URL=api_url,
-        ),
-    )
-
-
-def main(args):
-    """Entrypoint for blobtools host."""
-    global PIDS
-    directory = args["DIRECTORY"]
-    if directory != "_":
-        path = Path(directory)
-        if not path.exists():
-            print("ERROR: Directory '%s' does not exist" % directory)
-            sys.exit(1)
-        if (path / "meta.json").exists():
-            print("WARNING: Directory '%s' appears to be a BlobDir." % directory)
-            print("         Hosting the parent directory instead.")
-            path = path.resolve().parent
-        directory = path.absolute()
-    test_port(args["--api-port"], "BlobtoolKit API")
-    test_port(args["--port"], "BlobtoolKit viewer")
-    api = start_api(
-        args["--port"],
-        args["--api-port"],
-        args["--hostname"],
-        directory,
-    )
-    PIDS.append(api.pid)
-    print(
-        "Starting BlobToolKit API on port %d (pid: %d)"
-        % (int(args["--api-port"]), api.pid)
-    )
-    time.sleep(2)
-    viewer = start_viewer(args["--port"], args["--api-port"], args["--hostname"])
-    PIDS.append(viewer.pid)
-    print(
-        "Starting BlobToolKit viewer on port %d (pid: %d)"
-        % (int(args["--port"]), viewer.pid)
-    )
-    time.sleep(2)
-    ready = False
-    url = "http://%s:%d" % (args["--hostname"], int(args["--port"]))
-    while True:
-        time.sleep(1)
-        if api.poll() is not None:
-            for line in api.stdout.readlines():
-                print(line.strip())
-            for line in api.stderr.readlines():
-                print(line.strip())
-            if viewer.poll() is not None:
-                for line in viewer.stdout.readlines():
-                    print(line.strip())
-                for line in viewer.stderr.readlines():
-                    print(line.strip())
-                with contextlib.suppress(ProcessLookupError):
-                    os.kill(viewer.pid, signal.SIGTERM)
-            break
-        if viewer.poll() is not None:
-            for line in viewer.stdout.readlines():
-                print(line.strip())
-            for line in viewer.stderr.readlines():
-                print(line.strip())
-            with contextlib.suppress(ProcessLookupError):
-                os.kill(api.pid, signal.SIGTERM)
-            break
-        if not ready:
-            print(f"Visit {url} to use the interactive BlobToolKit Viewer.")
-            ready = True
-        time.sleep(1)
+from blobtoolkit_host import host
 
 
 def cli():
-    """Entry point."""
-    if len(sys.argv) == sys.argv.index(__name__.split(".")[-1]) + 1:
-        args = docopt(__doc__, argv=[])
-    else:
-        args = docopt(__doc__, version=__version__)
-    try:
-        main(args)
-    except KeyboardInterrupt:
-        pass
-    finally:
-        for pid in PIDS:
-            kill_child_processes(pid, args=args, sig=signal.SIGKILL)
-            time.sleep(2)
+    host.cli("blobtools host")
 
 
 if __name__ == "__main__":
diff --git a/src/blobtools/lib/remove.py b/src/blobtools/lib/remove.py
index 373ac104..dceb8762 100755
--- a/src/blobtools/lib/remove.py
+++ b/src/blobtools/lib/remove.py
@@ -22,7 +22,7 @@
 
 Examples:
     # 1. Remove BUSCO and ncount fields from a BlobDir
-    ./blobtools remove --busco --field ncount BlobDir
+    blobtools remove --busco --field ncount BlobDir
 
 """
 
diff --git a/src/blobtools/lib/run_external.py b/src/blobtools/lib/run_external.py
deleted file mode 100644
index d0fc6238..00000000
--- a/src/blobtools/lib/run_external.py
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/usr/bin/env python3
-"""Functions to run external programs."""
-
-import gzip
-import shlex
-from pathlib import Path
-from subprocess import PIPE, run
-
-
-def seqtk_subseq(infile, ids, outfile):
-    """Run seqtk subseq."""
-    cmd = "seqtk subseq %s -" % infile
-    file_open = open
-    if Path(infile).suffix == '.gz':
-        file_open = gzip.open
-    with file_open(outfile, 'wt') as ofh:
-        process = run(shlex.split(cmd),
-                      stdout=PIPE,
-                      stderr=PIPE,
-                      input=ids,
-                      encoding='ascii')
-        ofh.write(process.stdout)
-    return process.returncode
-
-
-if __name__ == '__main__':
-    import doctest
-    doctest.testmod()
diff --git a/src/blobtools/lib/version.py b/src/blobtools/lib/version.py
index e9b8f088..82995812 100644
--- a/src/blobtools/lib/version.py
+++ b/src/blobtools/lib/version.py
@@ -1,4 +1,4 @@
 #!/usr/bin/env python3
 """blobtoolkit version."""
 
-__version__ = "blobtoolkit v3.4.0"
+__version__ = "blobtoolkit v4.0.2"
diff --git a/src/blobtools/lib/view.py b/src/blobtools/lib/view.py
index a815b980..099770db 100755
--- a/src/blobtools/lib/view.py
+++ b/src/blobtools/lib/view.py
@@ -32,6 +32,7 @@
 import os
 import shlex
 import signal
+import socket
 import sys
 import time
 from pathlib import Path
@@ -51,12 +52,33 @@
 from selenium.webdriver.support.ui import WebDriverWait
 from tolkein import tolog
 
-from .host import test_port
 from .version import __version__
 
 LOGGER = tolog.logger(__name__)
 
 
+def test_port(port, service):
+    """Exit if port is already in use."""
+    port = int(port)
+    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as skt:
+        try:
+            skt.bind(("", port))
+        except OSError:
+            if service == "test":
+                return False
+            print("ERROR: Port %d already in use, unable to host %s." % (port, service))
+            print(
+                "       Use: `lsof -nP -iTCP:%d | grep LISTEN` to find the associated process."
+                % port
+            )
+            print(
+                f"       It may take ~30s for this port to become available when restarting {service}."
+            )
+
+            sys.exit(1)
+    return True
+
+
 def file_ready(file_path, timeout, callback):
     """Check if file is ready."""
     start_time = time.time()
@@ -136,12 +158,13 @@ def test_loc(args):
     loc = "%s:%d/%s" % (args["--host"], port, args["--prefix"])
     loc += f"/{dataset}/dataset/{dataset}" if level == "dataset" else "/all"
     print("Initializing viewer")
-    for _ in range(10):
+    time.sleep(2)
+    for i in range(4):
         poll = process.poll()
         if poll is None:
-            if test_port(port, "test"):
-                break
             time.sleep(1)
+            if i > 1 and test_port(port, "test"):
+                break
         else:
             print(process.stdout.read(), file=sys.stderr)
             print(process.stderr.read(), file=sys.stderr)
diff --git a/src/btk/__init__.py b/src/btk/__init__.py
index f39f3abd..f58aefd4 100644
--- a/src/btk/__init__.py
+++ b/src/btk/__init__.py
@@ -1,3 +1,8 @@
 """BlobToolKit."""
 
+import contextlib
+
 from .btk import cli
+
+with contextlib.suppress(ModuleNotFoundError):
+    from .lib import pipeline
diff --git a/src/btk/btk.py b/src/btk/btk.py
index 0295b18b..5eee4f62 100755
--- a/src/btk/btk.py
+++ b/src/btk/btk.py
@@ -28,6 +28,22 @@
 LOGGER = tolog.logger(__name__)
 
 
+def suggest_option(command):
+    options = {
+        "pipeline": ["pipeline"],
+    }
+    options_list = options.get(command, [])
+    options_list.append("full")
+    LOGGER.error(
+        f"The `btk {command}` command is not available, to enable this option use{' one of' if len(options_list)>1 else ''}:"
+    )
+    for option in options_list:
+        print(
+            f"                                - pip install blobtoolkit[{option}]",
+            file=sys.stderr,
+        )
+
+
 def cli():
     """Entry point."""
     if len(sys.argv) > 2:
@@ -49,8 +65,13 @@ def cli():
         # load <command> from entry_points
         for entry_point in working_set.iter_entry_points("%s.subcmd" % args["<tool>"]):
             if entry_point.name == args["<command>"]:
-                subcommand = entry_point.load()
-                sys.exit(subcommand())
+                if entry_point.name == args["<command>"]:
+                    try:
+                        subcommand = entry_point.load()
+                        sys.exit(subcommand())
+                    except ModuleNotFoundError:
+                        suggest_option(args["<command>"])
+                        exit(1)
         LOGGER.error(
             "'%s %s' is not a valid command", args["<tool>"], args["<command>"]
         )
diff --git a/src/btk/lib/pipeline.py b/src/btk/lib/pipeline.py
new file mode 100644
index 00000000..cb4dfb55
--- /dev/null
+++ b/src/btk/lib/pipeline.py
@@ -0,0 +1,15 @@
+#!/usr/bin/env python3
+
+"""
+BlobToolKit pipeline methods.
+"""
+
+from blobtoolkit_pipeline import cli
+
+
+def main():
+    cli("btk pipeline")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/src/btk/lib/version.py b/src/btk/lib/version.py
index e9b8f088..82995812 100644
--- a/src/btk/lib/version.py
+++ b/src/btk/lib/version.py
@@ -1,4 +1,4 @@
 #!/usr/bin/env python3
 """blobtoolkit version."""
 
-__version__ = "blobtoolkit v3.4.0"
+__version__ = "blobtoolkit v4.0.2"
diff --git a/src/data/.bumpversion.cfg b/src/data/.bumpversion.cfg
new file mode 100644
index 00000000..672f8133
--- /dev/null
+++ b/src/data/.bumpversion.cfg
@@ -0,0 +1,58 @@
+[bumpversion]
+current_version = 3.5.5
+commit = True
+tag = True
+tag_name = {new_version}
+message = "Bump version: {current_version} → {new_version}"
+
+[bumpversion:file:setup.py]
+search = version="{current_version}"
+replace = version="{new_version}"
+
+[bumpversion:file:src/blobtools/lib/version.py]
+search = blobtoolkit v{current_version}
+replace = blobtoolkit v{new_version}
+
+[bumpversion:file:src/btk/lib/version.py]
+search = blobtoolkit v{current_version}
+replace = blobtoolkit v{new_version}
+
+[bumpversion:file:src/pipeline/lib/version.py]
+search = blobtoolkit v{current_version}
+replace = blobtoolkit v{new_version}
+
+[bumpversion:file:README.md]
+search = v{current_version}
+replace = v{new_version}
+
+[bumpversion:file:src/viewer/README.md]
+search = v{current_version}
+replace = v{new_version}
+
+[bumpversion:file:src/docker/env.yaml]
+search = blobtoolkit=={current_version}
+replace = blobtoolkit=={new_version}
+
+[bumpversion:file:src/docker/blobtools/env.yaml]
+search = blobtoolkit=={current_version}
+replace = blobtoolkit=={new_version}
+
+[bumpversion:file:src/docker/Dockerfile]
+search = ARG VERSION={current_version}
+replace = ARG VERSION={new_version}
+
+[bumpversion:file:src/docker/api/Dockerfile]
+search = ARG VERSION={current_version}
+replace = ARG VERSION={new_version}
+
+[bumpversion:file:src/docker/blobtools/Dockerfile]
+search = ARG VERSION={current_version}
+replace = ARG VERSION={new_version}
+
+[bumpversion:file:src/docker/viewer/Dockerfile]
+search = ARG VERSION={current_version}
+replace = ARG VERSION={new_version}
+
+[bumpversion:file:.github/workflows/package-blobtoolkit.yml]
+search = VERSION: {current_version}
+replace = VERSION: {new_version}
diff --git a/src/data/MANIFEST.in b/src/data/MANIFEST.in
new file mode 100644
index 00000000..44337150
--- /dev/null
+++ b/src/data/MANIFEST.in
@@ -0,0 +1,8 @@
+# MANIFEST.in
+include setup.py
+prune .cache
+prune .git
+prune build
+prune dist
+recursive-exclude *.egg-info *
+recursive-include example *
diff --git a/src/data/pyproject.toml b/src/data/pyproject.toml
new file mode 100644
index 00000000..694671bb
--- /dev/null
+++ b/src/data/pyproject.toml
@@ -0,0 +1,26 @@
+[build-system]
+requires = [
+    'setuptools>=42',
+    'wheel',
+]
+
+[tool.pytest.ini_options]
+minversion = "6.0"
+addopts = "-ra -q"
+testpaths = [
+    "tests",
+]
+
+[tool.black]
+line-length = 88
+include = '\.pyi?$'
+exclude = '''
+/(
+    \.git
+  | \.tox
+  | \.venv
+  | _build
+  | build
+  | dist
+)/
+'''
\ No newline at end of file
diff --git a/src/data/requirements.txt b/src/data/requirements.txt
new file mode 100644
index 00000000..1d9dff81
--- /dev/null
+++ b/src/data/requirements.txt
@@ -0,0 +1,1 @@
+docopt==0.6.2
diff --git a/src/data/setup.py b/src/data/setup.py
new file mode 100644
index 00000000..36e29385
--- /dev/null
+++ b/src/data/setup.py
@@ -0,0 +1,86 @@
+"""
+A setuptools based setup module.
+
+See:
+https://packaging.python.org/guides/distributing-packages-using-setuptools/
+https://github.com/pypa/sampleproject
+"""
+
+import io
+from os.path import dirname
+from os.path import join
+
+from setuptools import find_namespace_packages
+from setuptools import setup
+
+
+def read(*names, **kwargs):
+    """Read file."""
+    with io.open(
+        join(dirname(__file__), *names), encoding=kwargs.get("encoding", "utf8")
+    ) as fh:
+        return fh.read()
+
+
+setup(
+    name="blobtoolkit-data",  # Required
+    version="3.5.5",
+    description="blobtoolkit-data",  # Optional
+    long_description="blobtoolkit-data",  # Optional
+    long_description_content_type="text/markdown",
+    url="https://github.com/blobtoolkit/blobtoolkit",  # Optional
+    # This should be your name or the name of the organization which owns the
+    # project.
+    author="blobtoolkit",  # Optional
+    # This should be a valid email address corresponding to the author listed
+    # above.
+    author_email="blobtoolkit@genomehubs.org",  # Optional
+    # Classifiers help users find your project by categorizing it.
+    #
+    # For a list of valid classifiers, see https://pypi.org/classifiers/
+    classifiers=[  # Optional
+        # How mature is this project? Common values are
+        #   3 - Alpha
+        #   4 - Beta
+        #   5 - Production/Stable
+        "Development Status :: 5 - Production/Stable",
+        # Indicate who your project is intended for
+        "Intended Audience :: Science/Research",
+        "Topic :: Scientific/Engineering :: Bio-Informatics",
+        # Pick your license as you wish
+        "License :: OSI Approved :: MIT License",
+        # Specify the Python versions you support here. In particular, ensure
+        # that you indicate you support Python 3. These classifiers are *not*
+        # checked by 'pip install'. See instead 'python_requires' below.
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.6",
+        "Programming Language :: Python :: 3.7",
+        "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.10",
+        "Programming Language :: Python :: 3.11",
+        "Programming Language :: Python :: 3 :: Only",
+    ],
+    keywords="bioinformatics",
+    package_dir={"": "src"},
+    py_modules=["blobtoolkit_data"],
+    packages=find_namespace_packages(
+        where="src",
+        exclude=[],
+    ),
+    python_requires=">=3.7, <=3.11",
+    install_requires=[
+        "docopt>=0.6.2",
+    ],
+    entry_points={
+        "console_scripts": [
+            "blobtoolkit-data = blobtoolkit-data:cli",
+        ],
+    },
+    project_urls={
+        "Bug Reports": "https://github.com/blobtoolkit/blobtoolkit/issues",
+        "Source": "https://github.com/blobtoolkit/blobtoolkit",
+    },
+    include_package_data=True,
+    zip_safe=False,
+)
diff --git a/src/docker/Dockerfile b/src/docker/Dockerfile
index 2d32c931..f3cdc09d 100644
--- a/src/docker/Dockerfile
+++ b/src/docker/Dockerfile
@@ -1,75 +1,28 @@
-FROM ubuntu:20.04
+FROM genomehubs/blobtoolkit-dependencies:latest
+
 LABEL maintainer="blobtoolkit@genomehubs.org"
 LABEL license="MIT"
-ARG VERSION=3.4.0
 LABEL version=$VERSION
+ARG VERSION
 ENV CONTAINER_VERSION=$VERSION
+ENV CONDA_DIR=/opt/conda
 
-RUN apt-get update \
-    && DEBIAN_FRONTEND="noninteractive" apt-get -y --no-install-recommends install \
-    aria2 \
-    build-essential \
-    dbus-x11 \
-    firefox \
-    firefox-geckodriver \
-    rsync \
-    ttf-mscorefonts-installer \
-    wget \
-    xvfb \
-    x11-utils
-
-RUN mkdir -p /blobtoolkit/conf \
-    && mkdir -p /blobtoolkit/data/assembly \
-    && mkdir -p /blobtoolkit/data/reads \
-    && mkdir -p /blobtoolkit/data/other \
-    && mkdir -p /blobtoolkit/databases/busco \
-    && mkdir -p /blobtoolkit/databases/ncbi_db \
-    && mkdir -p /blobtoolkit/databases/ncbi_taxdump \
-    && mkdir -p /blobtoolkit/databases/uniprot_db \
-    && mkdir -p /blobtoolkit/datasets \
-    && mkdir -p /blobtoolkit/output \
-    && mkdir -p /nfs \
-    && mkdir -p /lustre
-
-RUN useradd -m blobtoolkit \
-    && chown -R blobtoolkit:blobtoolkit /blobtoolkit \
-    && chown -R blobtoolkit:blobtoolkit /nfs \
-    && chown -R blobtoolkit:blobtoolkit /lustre
-
-USER blobtoolkit
-
-WORKDIR /tmp
-
-RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
-
-RUN printf '\nyes\n\n' | bash Miniconda3-latest-Linux-x86_64.sh
-
-ARG CONDA_DIR=/home/blobtoolkit/miniconda3
+USER root
 
-RUN echo ". $CONDA_DIR/etc/profile.d/conda.sh" >> ~/.bashrc
+COPY *-linux /blobtoolkit/
+COPY *.whl /tmp
+COPY setup.sh /blobtoolkit
 
-RUN $CONDA_DIR/bin/conda install mamba -n base -c conda-forge
-
-RUN mkdir -p /blobtoolkit/.conda
+RUN /blobtoolkit/setup.sh \
+    && rm /blobtoolkit/setup.sh \
+    && rm -r /tmp/*
 
 WORKDIR /blobtoolkit
 
-COPY env.yaml /tmp/env.yaml
-
-RUN $CONDA_DIR/bin/mamba env create -f /tmp/env.yaml
-
-RUN $CONDA_DIR/bin/conda clean --all --yes
-
-ENV CONDA_DEFAULT_ENV $CONDA_DIR/envs/btk_env
-
-ENV PATH $CONDA_DEFAULT_ENV/bin:$PATH
-
-ENV PYTHONPATH $CONDA_DEFAULT_ENV/lib/python3.9/site-packages:$PYTHONPATH
-
-WORKDIR /blobtoolkit
+USER blobtoolkit
 
 COPY startup.sh /blobtoolkit
 
-EXPOSE 8000 8080
+EXPOSE 8000 8001 8080
 
 CMD /blobtoolkit/startup.sh
\ No newline at end of file
diff --git a/src/pipeline/lib/__init__.py b/src/docker/_-linux
old mode 100755
new mode 100644
similarity index 100%
rename from src/pipeline/lib/__init__.py
rename to src/docker/_-linux
diff --git a/src/docker/_.whl b/src/docker/_.whl
new file mode 100644
index 00000000..e69de29b
diff --git a/src/docker/api/Dockerfile b/src/docker/api/Dockerfile
index b3f8ee05..0914f66b 100644
--- a/src/docker/api/Dockerfile
+++ b/src/docker/api/Dockerfile
@@ -1,7 +1,7 @@
 FROM alpine:3.15
 LABEL maintainer="blobtoolkit@genomehubs.org"
 LABEL license="MIT"
-ARG VERSION=3.4.0
+ARG VERSION
 LABEL version=$VERSION
 
 ENV CONTAINER_VERSION=$VERSION
diff --git a/src/docker/blobtools/Dockerfile b/src/docker/blobtools/Dockerfile
index c88a0615..fe88f135 100644
--- a/src/docker/blobtools/Dockerfile
+++ b/src/docker/blobtools/Dockerfile
@@ -1,8 +1,8 @@
 FROM ubuntu:20.04
 LABEL maintainer="blobtoolkit@genomehubs.org"
 LABEL license="MIT"
-ARG VERSION=3.4.0
 LABEL version=$VERSION
+ARG VERSION
 ENV CONTAINER_VERSION=$VERSION
 
 RUN apt-get update \
diff --git a/src/docker/blobtools/env.yaml b/src/docker/blobtools/env.yaml
index f501cdcd..653160f8 100644
--- a/src/docker/blobtools/env.yaml
+++ b/src/docker/blobtools/env.yaml
@@ -9,4 +9,4 @@ dependencies:
   - python==3.9.13
   - seqtk==1.3
   - pip:
-      - blobtoolkit==3.4.0
+      - blobtoolkit[full]==blobtoolkit==4.0.2
diff --git a/src/docker/dependencies/Dockerfile b/src/docker/dependencies/Dockerfile
new file mode 100644
index 00000000..c48c9a21
--- /dev/null
+++ b/src/docker/dependencies/Dockerfile
@@ -0,0 +1,109 @@
+ARG BUILD_ENV=dev
+
+FROM ubuntu:20.04 as base_image
+
+ONBUILD RUN apt-get update \
+    && DEBIAN_FRONTEND="noninteractive" apt-get -y --no-install-recommends install \
+    aria2 \
+    build-essential \
+    ca-certificates \
+    dbus-x11 \
+    firefox \
+    firefox-geckodriver \
+    git \
+    rsync \
+    ttf-mscorefonts-installer \
+    wget \
+    xvfb \
+    x11-utils \
+    && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
+
+WORKDIR /opt
+
+ONBUILD RUN wget -P /usr/local/src https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh \
+    && bash /usr/local/src/Miniconda3-latest-Linux-x86_64.sh -b -p /opt/conda \
+    && /opt/conda/bin/conda install -n base conda-libmamba-solver \
+    && rm /usr/local/src/Miniconda3-latest-Linux-x86_64.sh
+
+ONBUILD RUN mkdir -p /blobtoolkit \
+    && useradd -m blobtoolkit
+
+ONBUILD COPY env.yaml /tmp/env.yaml
+
+FROM base_image as build_dev
+
+ONBUILD COPY blobtoolkit-api-linux /blobtoolkit/blobtoolkit-api
+ONBUILD COPY blobtoolkit-viewer-linux /blobtoolkit/blobtoolkit-viewer
+ONBUILD COPY *-${VERSION}-*.whl /tmp
+
+ONBUILD WORKDIR /tmp
+
+ONBUILD RUN chown -R blobtoolkit:blobtoolkit /blobtoolkit \
+    && chmod 755 /blobtoolkit/blobtoolkit-api /blobtoolkit/blobtoolkit-viewer \
+    && sed -i '/- blobtoolkit/d' /tmp/env.yaml
+#    && git clone https://github.com/blobtoolkit/blobtoolkit \
+
+ONBUILD WORKDIR /blobtoolkit
+
+FROM base_image as build_prod
+
+ONBUILD WORKDIR /blobtoolkit
+
+FROM build_${BUILD_ENV}
+LABEL maintainer="blobtoolkit@genomehubs.org"
+LABEL license="MIT"
+LABEL version=$VERSION
+ARG VERSION
+ENV CONTAINER_VERSION=$VERSION
+
+RUN mkdir -p /blobtoolkit/conf \
+    && mkdir -p /blobtoolkit/data/assembly \
+    && mkdir -p /blobtoolkit/data/reads \
+    && mkdir -p /blobtoolkit/data/other \
+    && mkdir -p /blobtoolkit/databases/busco \
+    && mkdir -p /blobtoolkit/databases/ncbi_db \
+    && mkdir -p /blobtoolkit/databases/ncbi_taxdump \
+    && mkdir -p /blobtoolkit/databases/uniprot_db \
+    && mkdir -p /blobtoolkit/datasets \
+    && mkdir -p /blobtoolkit/output \
+    && mkdir -p /nfs \
+    && mkdir -p /lustre
+
+RUN chown -R blobtoolkit:blobtoolkit /blobtoolkit \
+    && chown -R blobtoolkit:blobtoolkit /nfs \
+    && chown -R blobtoolkit:blobtoolkit /lustre
+
+ARG CONDA_DIR=/opt/conda
+
+RUN $CONDA_DIR/bin/conda env create --experimental-solver=libmamba -f /tmp/env.yaml \
+    && $CONDA_DIR/bin/conda clean --all --yes
+
+COPY setup.sh /blobtoolkit
+
+RUN /blobtoolkit/setup.sh \
+    && rm /blobtoolkit/setup.sh \
+    && rm -r /tmp/*
+
+USER blobtoolkit
+
+
+# RUN echo ". $CONDA_DIR/etc/profile.d/conda.sh" >> ~/.bashrc
+
+# RUN $CONDA_DIR/bin/conda install mamba -n base -c conda-forge
+
+# RUN mkdir -p /blobtoolkit/.conda
+
+# WORKDIR /blobtoolkit
+
+
+ENV CONDA_DEFAULT_ENV $CONDA_DIR/envs/btk_env
+
+ENV PATH /blobtoolkit:$CONDA_DEFAULT_ENV/bin:$PATH
+
+ENV PYTHONPATH $CONDA_DEFAULT_ENV/lib/python3.9/site-packages:$PYTHONPATH
+
+COPY startup.sh /blobtoolkit
+
+EXPOSE 8000 8001 8080
+
+CMD /blobtoolkit/startup.sh
\ No newline at end of file
diff --git a/src/docker/dependencies/env.yaml b/src/docker/dependencies/env.yaml
new file mode 100644
index 00000000..2e05303f
--- /dev/null
+++ b/src/docker/dependencies/env.yaml
@@ -0,0 +1,19 @@
+name: btk_env
+channels:
+  - conda-forge
+  - bioconda
+  - defaults
+dependencies:
+  - blast==2.12.0
+  - busco==5.3.2
+  - ncbi-datasets-cli==14.1.0
+  - diamond==2.0.15
+  - minimap2==2.24
+  - mosdepth==0.3.3
+  - pigz==2.6
+  - pip==22.1.2
+  - python==3.9.13
+  - samtools==1.15.1
+  - seqtk==1.3
+  - pip:
+      - blobtoolkit[full]==3.5.5
diff --git a/src/docker/dependencies/setup.sh b/src/docker/dependencies/setup.sh
new file mode 100755
index 00000000..a1097596
--- /dev/null
+++ b/src/docker/dependencies/setup.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+
+ls /tmp/*.whl | while read WHEEL; do
+    $CONDA_DIR/envs/btk_env/bin/pip install $WHEEL
+done
+
+# touch /tmp/build_env.t$BUILD_ENV
+
+# # if [ "${BUILD_ENV}" == "dev" ]; then
+#   cd /tmp/blobtoolkit
+#   git fetch origin feature/split-packaging
+#   git checkout feature/split-packaging
+
+#   pip install setuptools wheel twine
+
+#   cd /tmp/blobtoolkit/src/blobtoolkit-pipeline
+#   ./pip_install_latest.sh manylinux2014_x86_64
+
+#   cd /tmp/blobtoolkit/src/blobtoolkit-host
+#   ./pip_install_latest.sh manylinux2014_x86_64
+
+#   cd /tmp/blobtoolkit
+#   ./pip_install_latest.sh manylinux2014_x86_64
+# # fi
\ No newline at end of file
diff --git a/src/docker/dependencies/startup.sh b/src/docker/dependencies/startup.sh
new file mode 100755
index 00000000..f3706b06
--- /dev/null
+++ b/src/docker/dependencies/startup.sh
@@ -0,0 +1,53 @@
+#!/bin/bash
+
+if [[ ! -z $VIEWER ]]; then
+  blobtools host --api-port 8000 --port 8080 /blobtoolkit/datasets &
+  tail -f /dev/null
+  exit
+fi
+
+if [[ ! -z $ASSEMBLY ]]; then
+
+  export WORKDIR=/blobtoolkit/datasets/$ASSEMBLY
+
+  if [[ ! -f "$WORKDIR/config.yaml" ]]; then
+    echo "ERROR: unable to locate configuration file '$WORKDIR/config.yaml'"
+    exit 1
+  fi
+  
+  if [[ -z $THREADS ]]; then
+    export THREADS=32
+  fi
+
+  if [[ ! -z $DRYRUN ]]; then
+    DRYRUN="--dry-run"
+  fi
+
+  if [[ -z $TOOL ]]; then
+    TOOL="blobtoolkit"
+  fi
+
+  if [[ ! -z $UNLOCK ]]; then
+    btk pipeline run --unlock --config $WORKDIR/config.yaml
+  fi
+
+  btk pipeline run $DRYRUN --threads $THREADS --config $WORKDIR/config.yaml
+
+  if [ $? -ne 0 ];then
+    echo "ERROR: failed to run snakemake pipeline"
+    exit 1
+  fi
+
+  if [[ ! -z $TRANSFER ]]; then
+    btk pipeline transfer-completed \
+        --in $WORKDIR \
+        --out /blobtoolkit/output
+    
+    if [ $? -ne 0 ];then
+      echo "ERROR: failed to transfer completed files"
+      exit 1
+    fi
+  fi
+fi
+
+
diff --git a/src/docker/env.yaml b/src/docker/env.yaml
index 09127a64..15b875a5 100644
--- a/src/docker/env.yaml
+++ b/src/docker/env.yaml
@@ -16,4 +16,4 @@ dependencies:
   - samtools==1.15.1
   - seqtk==1.3
   - pip:
-      - blobtoolkit==3.4.0
+      - blobtoolkit[full]==4.0.2
diff --git a/src/docker/setup.sh b/src/docker/setup.sh
new file mode 100755
index 00000000..6742884c
--- /dev/null
+++ b/src/docker/setup.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+ls /tmp/*-${VERSION}-*.whl | while read WHEEL; do
+    $CONDA_DIR/envs/btk_env/bin/pip install $WHEEL
+done
+
+if [ -s /blobtoolkit/blobtoolkit-api-linux ]; then
+    mv /blobtoolkit/blobtoolkit-api-linux /blobtoolkit/blobtoolkit-api
+    chmod 755 /blobtoolkit/blobtoolkit-api
+fi
+
+if [ -s /blobtoolkit/blobtoolkit-viewer-linux ]; then
+    mv /blobtoolkit/blobtoolkit-viewer-linux /blobtoolkit/blobtoolkit-viewer
+    chmod 755 /blobtoolkit/blobtoolkit-viewer
+fi
+
+chown blobtoolkit:blobtoolkit /blobtoolkit/blobtoolkit-{api,viewer}
\ No newline at end of file
diff --git a/src/docker/viewer/Dockerfile b/src/docker/viewer/Dockerfile
index 3c1331c5..4509226a 100644
--- a/src/docker/viewer/Dockerfile
+++ b/src/docker/viewer/Dockerfile
@@ -1,7 +1,7 @@
 FROM alpine:3.15
 LABEL maintainer="blobtoolkit@genomehubs.org"
 LABEL license="MIT"
-ARG VERSION=3.4.0
+ARG VERSION
 LABEL version=$VERSION
 
 ENV CONTAINER_VERSION=$VERSION
@@ -11,6 +11,10 @@ RUN apk add --no-cache curl gcompat libstdc++ libgcc
 RUN mkdir -p /blobtoolkit/conf \
     && mkdir -p /blobtoolkit/datasets
 
+COPY blobtoolkit-viewer-linux /blobtoolkit/blobtoolkit-viewer
+
+RUN chmod 755 /blobtoolkit/blobtoolkit-viewer
+
 RUN addgroup -S blobtoolkit \
     && adduser -S blobtoolkit -G blobtoolkit \
     && chown -R blobtoolkit:blobtoolkit /blobtoolkit
@@ -19,9 +23,6 @@ USER blobtoolkit
 
 WORKDIR /blobtoolkit
 
-RUN curl -Ls https://github.com/blobtoolkit/blobtoolkit/releases/download/${VERSION}/blobtoolkit-viewer-linux > blobtoolkit-viewer \
-    && chmod 755 blobtoolkit-viewer
-
 ENV PATH /blobtoolkit:$PATH
 
 COPY .env /blobtoolkit/
diff --git a/src/packaged-viewer/package-lock.json b/src/packaged-viewer/package-lock.json
index a021767d..37a13c7a 100644
--- a/src/packaged-viewer/package-lock.json
+++ b/src/packaged-viewer/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "blobtoolkit-viewer",
-  "version": "3.4.0",
+  "version": "4.0.0",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "blobtoolkit-viewer",
-      "version": "3.4.0",
+      "version": "4.0.0",
       "license": "MIT",
       "dependencies": {
         "ejs": "^3.1.7",
diff --git a/src/packaged-viewer/package.json b/src/packaged-viewer/package.json
index 435c78a0..3975d959 100644
--- a/src/packaged-viewer/package.json
+++ b/src/packaged-viewer/package.json
@@ -1,6 +1,6 @@
 {
   "name": "blobtoolkit-viewer",
-  "version": "3.4.0",
+  "version": "4.0.0",
   "repository": "https://github.com/blobtoolkit/viewer",
   "description": "",
   "bin": "src/app.js",
diff --git a/src/pipeline/__init__.py b/src/pipeline/__init__.py
deleted file mode 100644
index f3bac08c..00000000
--- a/src/pipeline/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-"""BlobToolKit Pipeline."""
-
-from .pipeline import main
diff --git a/src/pipeline/__main__.py b/src/pipeline/__main__.py
deleted file mode 100644
index 283422b1..00000000
--- a/src/pipeline/__main__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-"""BlobToolKit Pipeline package."""
-
-from .pipeline import main
-
-if __name__ == "__main__":
-    main()
diff --git a/src/viewer/README.md b/src/viewer/README.md
index 37df475a..c6e143ac 100644
--- a/src/viewer/README.md
+++ b/src/viewer/README.md
@@ -1,4 +1,4 @@
-# BlobToolKit Viewer (v3.4.0)
+# BlobToolKit Viewer (v3.5.5)
 
 [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.1134794.svg)](https://doi.org/10.5281/zenodo.1134794)
 
diff --git a/src/viewer/package-lock.json b/src/viewer/package-lock.json
index 492d442f..4204d288 100644
--- a/src/viewer/package-lock.json
+++ b/src/viewer/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "blobtoolkit-viewer",
-  "version": "3.4.0",
+  "version": "4.0.0",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "blobtoolkit-viewer",
-      "version": "3.4.0",
+      "version": "4.0.0",
       "license": "MIT",
       "dependencies": {
         "@fontsource/comfortaa": "^4.5.8",
diff --git a/src/viewer/package.json b/src/viewer/package.json
index 0e7c7f7f..dcda68e1 100644
--- a/src/viewer/package.json
+++ b/src/viewer/package.json
@@ -1,6 +1,6 @@
 {
   "name": "blobtoolkit-viewer",
-  "version": "3.4.0",
+  "version": "4.0.0",
   "repository": "https://github.com/blobtoolkit/viewer",
   "description": "",
   "main": "index.js",
diff --git a/src/viewer/src/client/views/components/FigureCaption.jsx b/src/viewer/src/client/views/components/FigureCaption.jsx
index f090d396..95ee5156 100644
--- a/src/viewer/src/client/views/components/FigureCaption.jsx
+++ b/src/viewer/src/client/views/components/FigureCaption.jsx
@@ -98,7 +98,7 @@ class Caption extends Component {
     let caption = "";
     let histograms;
     let equation;
-    let record = this.props.meta.record_type || "contig";
+    let record = "sequence";
     let z = labels[this.props.plot.axes.z];
     let taxrule, cat;
     if (this.props.plot.axes.cat) {
@@ -132,12 +132,14 @@ class Caption extends Component {
       }
       title = `${plot} plot of ${yaxis} against ${xaxis} for ${record}s in assembly ${this.props.datasetName}. `;
       if (this.props.windowSize) {
-        caption = `${this.windowStr(
-          this.props.windowSize
-        )} are coloured by ${cat}`;
-      } else {
+        caption = `${this.windowStr(this.props.windowSize)}`;
+        if (cat) {
+          caption += ` are coloured by ${cat}`;
+        }
+      } else if (cat) {
         caption = `${this.capitalise(record)}s are coloured by ${cat}`;
       }
+
       if (this.props.plotShape != "kite") {
         if (
           this.props.plotShape == "circle" ||
diff --git a/src/viewer/src/client/views/components/MainPlotBoundary.jsx b/src/viewer/src/client/views/components/MainPlotBoundary.jsx
index d15aa04e..959fbf27 100644
--- a/src/viewer/src/client/views/components/MainPlotBoundary.jsx
+++ b/src/viewer/src/client/views/components/MainPlotBoundary.jsx
@@ -149,7 +149,7 @@ const PlotOutline = ({ data, params, plotPaths, plotText, fill }) => {
   }
 
   return (
-    <g>
+    <g style={{ fill: "none" }}>
       <rect
         style={{
           ...plotPaths.boundary,
diff --git a/src/viewer/src/client/views/components/MenuDataset.jsx b/src/viewer/src/client/views/components/MenuDataset.jsx
index 32b968cc..0c08f431 100644
--- a/src/viewer/src/client/views/components/MenuDataset.jsx
+++ b/src/viewer/src/client/views/components/MenuDataset.jsx
@@ -61,7 +61,7 @@ class Dataset extends React.Component {
           </span>
         </a>
         <span className={styles.menu_count}>
-          {records.toLocaleString() + " " + record_type}
+          {records.toLocaleString() + " " + "sequences"}
         </span>
         <div
           data-tip
diff --git a/src/viewer/src/client/views/components/PlotLegend.jsx b/src/viewer/src/client/views/components/PlotLegend.jsx
index 8706b5ab..3a8abb0f 100644
--- a/src/viewer/src/client/views/components/PlotLegend.jsx
+++ b/src/viewer/src/client/views/components/PlotLegend.jsx
@@ -127,6 +127,7 @@ const Legend = ({
   };
   let commaFormat = d3format(",");
   if (bins) {
+    let activeBins = bins.filter((bin) => isNaN(bin.id) && bin.id != "all");
     let offset = 20;
     let w = 19;
     let h = 19;
@@ -169,11 +170,17 @@ const Legend = ({
     );
     let title = "total";
     let color = "#999";
+    if (activeBins.length == 0) {
+      color = palette.colors[bins.map((bin) => bin.id).indexOf("all")] || color;
+    }
     let numbers = [];
     let count = values.counts.all > 0;
     let legendRows = 0;
     // numbers.push(commaFormat(values.counts.all))
     if (shape == "grid" || shape == "lines") {
+      if (activeBins.length == 0) {
+        return null;
+      }
       offset = 0;
       let visibleBins = bins.filter((b) =>
         visibleCats ? visibleCats.has(b.id) : true
@@ -303,97 +310,99 @@ const Legend = ({
       );
       offset += h + gap;
     }
-    bins.forEach((bin, i) => {
-      let title = bin.id;
-      if (
-        title == "no-hit" &&
-        values.counts.all > circleLimit &&
-        view == "blob" &&
-        shape == "circle"
-      ) {
-        title += " (not shown)";
-      }
-      let color = palette.colors[i];
-      let numbers = [];
-      let count = values.counts.binned[i] > 0;
-      // numbers.push(commaFormat(values.counts.binned[i]))
-      numbers.push(format(values.counts.binned[i]));
-      if (reducer != "count") {
-        numbers.push(format(values.reduced.binned[i]));
-      }
-      if (zAxis == "length") {
-        numbers.push(format(values.n50.binned[i]));
-      }
-      if (count || shape == "lines") {
-        items.push(
-          <g
-            key={i}
-            transform={
-              "translate(" + (largeFonts ? 101 : 0) + "," + offset + ")"
-            }
-          >
-            {(largeFonts && (
-              <g>
-                <rect
-                  x={-155}
-                  y={-gap}
-                  width={325}
-                  height={h + gap}
-                  style={{ fill: "white", stroke: "none" }}
-                />
-                <text
-                  style={Object.assign({}, plotText.legend, {
-                    textAnchor: "end",
-                  })}
-                  transform={"translate(" + -gap * 2 + "," + (h - gap) + ")"}
-                >
-                  {title}
-                </text>
-                {count && (
-                  <text
-                    style={plotText.legend}
-                    transform={
-                      "translate(" + (w + gap * 2) + "," + (h - gap) + ")"
-                    }
-                  >
-                    [{numbers.join("; ")}]
-                  </text>
-                )}
-              </g>
-            )) || (
-              <g>
-                <text
-                  style={plotText.legend}
-                  transform={"translate(" + (w + gap) + "," + (h - gap) + ")"}
-                >
-                  {title}
-                </text>
-                {count && (
+    if (activeBins.length > 0) {
+      bins.forEach((bin, i) => {
+        let title = bin.id;
+        if (
+          title == "no-hit" &&
+          values.counts.all > circleLimit &&
+          view == "blob" &&
+          shape == "circle"
+        ) {
+          title += " (not shown)";
+        }
+        let color = palette.colors[i];
+        let numbers = [];
+        let count = values.counts.binned[i] > 0;
+        // numbers.push(commaFormat(values.counts.binned[i]))
+        numbers.push(format(values.counts.binned[i]));
+        if (reducer != "count") {
+          numbers.push(format(values.reduced.binned[i]));
+        }
+        if (zAxis == "length") {
+          numbers.push(format(values.n50.binned[i]));
+        }
+        if (count || shape == "lines") {
+          items.push(
+            <g
+              key={i}
+              transform={
+                "translate(" + (largeFonts ? 101 : 0) + "," + offset + ")"
+              }
+            >
+              {(largeFonts && (
+                <g>
+                  <rect
+                    x={-155}
+                    y={-gap}
+                    width={325}
+                    height={h + gap}
+                    style={{ fill: "white", stroke: "none" }}
+                  />
                   <text
                     style={Object.assign({}, plotText.legend, {
                       textAnchor: "end",
                     })}
-                    transform={
-                      "translate(" + (w + gap + 260) + "," + (h - gap) + ")"
-                    }
+                    transform={"translate(" + -gap * 2 + "," + (h - gap) + ")"}
                   >
-                    [{numbers.join("; ")}]
+                    {title}
                   </text>
-                )}
-              </g>
-            )}
-            <rect
-              x={0}
-              y={0}
-              width={w}
-              height={h}
-              style={{ fill: color, stroke: "black" }}
-            />
-          </g>
-        );
-        offset += h + gap;
-      }
-    });
+                  {count && (
+                    <text
+                      style={plotText.legend}
+                      transform={
+                        "translate(" + (w + gap * 2) + "," + (h - gap) + ")"
+                      }
+                    >
+                      [{numbers.join("; ")}]
+                    </text>
+                  )}
+                </g>
+              )) || (
+                <g>
+                  <text
+                    style={plotText.legend}
+                    transform={"translate(" + (w + gap) + "," + (h - gap) + ")"}
+                  >
+                    {title}
+                  </text>
+                  {count && (
+                    <text
+                      style={Object.assign({}, plotText.legend, {
+                        textAnchor: "end",
+                      })}
+                      transform={
+                        "translate(" + (w + gap + 260) + "," + (h - gap) + ")"
+                      }
+                    >
+                      [{numbers.join("; ")}]
+                    </text>
+                  )}
+                </g>
+              )}
+              <rect
+                x={0}
+                y={0}
+                width={w}
+                height={h}
+                style={{ fill: color, stroke: "black" }}
+              />
+            </g>
+          );
+          offset += h + gap;
+        }
+      });
+    }
   }
   return (
     <g>
diff --git a/src/viewer/src/client/views/components/SnailPlot.jsx b/src/viewer/src/client/views/components/SnailPlot.jsx
index 6198aac1..b25381f3 100644
--- a/src/viewer/src/client/views/components/SnailPlot.jsx
+++ b/src/viewer/src/client/views/components/SnailPlot.jsx
@@ -106,7 +106,7 @@ const SegmentStats = ({
 };
 
 const SnailSegment = ({ path }) => (
-  <path d={path} fill={"rgba(225,225,225,0.4)"} stroke={"none"} />
+  <path d={path} fill={"rgb(225,225,225)"} fillOpacity={0.4} stroke={"none"} />
 );
 
 class Snail extends React.Component {
@@ -185,6 +185,7 @@ class Snail extends React.Component {
     let viewbox = "0 0 " + side + " " + side;
     let pathProps = this.props.circular.pathProps;
     let paths = [];
+    let labels = [];
     let legend = this.props.circular.legend;
     let bottomLeft = (
       <SnailPlotScale
@@ -331,7 +332,9 @@ class Snail extends React.Component {
           d={d}
           key={k}
           fill={pathProps[k].fill}
+          fillOpacity={pathProps[k].fillOpacity || 1}
           stroke={pathProps[k].stroke}
+          strokeOpacity={pathProps[k].strokeOpacity || 1}
           strokeWidth={pathProps[k].strokeWidth || 2}
           strokeDasharray={pathProps[k].strokeDasharray || null}
           strokeLinecap="round"
@@ -341,7 +344,7 @@ class Snail extends React.Component {
     let axes = this.props.circular.axes;
     Object.keys(axes).forEach((k, i) => {
       let axis = axes[k];
-      paths.push(
+      labels.push(
         <path
           style={this.props.plotPaths.axis}
           d={axis.path}
@@ -353,7 +356,7 @@ class Snail extends React.Component {
       );
       if (axis.ticks) {
         axis.ticks.major.forEach((d, idx) => {
-          paths.push(
+          labels.push(
             <path
               style={this.props.plotPaths.axis}
               d={d}
@@ -365,7 +368,7 @@ class Snail extends React.Component {
           );
         });
         axis.ticks.minor.forEach((d, idx) => {
-          paths.push(
+          labels.push(
             <path
               style={this.props.plotPaths.fine}
               d={d}
@@ -389,7 +392,7 @@ class Snail extends React.Component {
               textAnchor = "end";
               startOffset = "100%";
             }
-            paths.push(
+            labels.push(
               <path
                 style={this.props.plotPaths.axis}
                 d={d.path}
@@ -399,7 +402,7 @@ class Snail extends React.Component {
                 stroke="none"
               />
             );
-            paths.push(
+            labels.push(
               <text
                 key={k + "_text_" + idx}
                 style={Object.assign({}, this.props.plotText.axisLabel, {
@@ -469,8 +472,10 @@ class Snail extends React.Component {
             </g>
             <g transform={"translate(525,550)"}>
               {paths}
+              {labels}
               {segment}
             </g>
+
             <Pointable
               tagName="g"
               onPointerMove={(e) => {
@@ -510,7 +515,8 @@ class Snail extends React.Component {
                 r={450}
                 cx={520}
                 cy={550}
-                fill="rgba(255,255,255,0)"
+                fill="rgb(255,255,255)"
+                fillOpacity={0}
                 stroke="none"
                 style={{
                   pointerEvents: "auto",
diff --git a/src/viewer/src/client/views/components/SnailPlotScale.jsx b/src/viewer/src/client/views/components/SnailPlotScale.jsx
index 53524bc7..4187dc7a 100644
--- a/src/viewer/src/client/views/components/SnailPlotScale.jsx
+++ b/src/viewer/src/client/views/components/SnailPlotScale.jsx
@@ -37,7 +37,7 @@ export const PlotTextInput = ({
 }) => {
   if (!display) return null;
   return (
-    <g>
+    <g style={{ fill: "none" }}>
       <foreignObject width={width} height={height}>
         <div xmlns="http://www.w3.org/1999/xhtml">
           <input
@@ -51,6 +51,7 @@ export const PlotTextInput = ({
               marginLeft: x,
               marginTop: y,
               padding: 0,
+              fill: "none",
               border: "3px solid",
             }}
           />
@@ -110,7 +111,8 @@ export class SnailPlotScaleComponent extends React.Component {
           width="80px"
           height={w}
           style={Object.assign({}, this.props.plotPaths.axis, {
-            fill: "rgba(255,255,255,0)",
+            fill: "rgb(255,255,255)",
+            fillOpacity: 0,
             stroke: "black",
             pointerEvents: "auto",
             cursor: "pointer",
@@ -168,7 +170,8 @@ export class SnailPlotScaleComponent extends React.Component {
           width="80px"
           height={w}
           style={Object.assign({}, this.props.plotPaths.axis, {
-            fill: "rgba(255,255,255,0)",
+            fill: "rgb(255,255,255)",
+            fillOpacity: 0,
             stroke: "black",
             pointerEvents: "auto",
             cursor: "pointer",
diff --git a/src/viewer/src/client/views/components/style/node_modules.css b/src/viewer/src/client/views/components/style/node_modules.css
index 9ce9eaca..3a130ee9 100644
--- a/src/viewer/src/client/views/components/style/node_modules.css
+++ b/src/viewer/src/client/views/components/style/node_modules.css
@@ -146,7 +146,7 @@
   -webkit-box-direction: normal;
   -ms-flex-direction: column;
   flex-direction: column;
-  overflow: auto;
+  overflow: hidden;
   padding-bottom:1em;
 }
 .ReactTable .rt-tbody .rt-tr-group {
diff --git a/src/viewer/src/client/views/reducers/plotSquareBins.js b/src/viewer/src/client/views/reducers/plotSquareBins.js
index e2c72bd0..e4961e8b 100644
--- a/src/viewer/src/client/views/reducers/plotSquareBins.js
+++ b/src/viewer/src/client/views/reducers/plotSquareBins.js
@@ -6,6 +6,7 @@ import {
   getScatterPlotData,
 } from "./plotData";
 import { getBinsForCat, getBinsForFieldId } from "./field";
+import { getCatAxis, getMainPlot } from "./plot";
 import { getDetailsForFieldId, getRawDataForFieldId } from "./field";
 import {
   getPlotResolution,
@@ -19,7 +20,6 @@ import { byIdSelectorCreator } from "./selectorCreators";
 import { createSelector } from "reselect";
 import { getColorPalette } from "./color";
 import { getFilteredDataForFieldId } from "./preview";
-import { getMainPlot } from "./plot";
 import { getSelectedRecordsAsObject } from "./select";
 import immutableUpdate from "immutable-update";
 
@@ -214,6 +214,7 @@ export const getScatterPlotDataBySquareBinByCategory = createSelector(
   getZReducer,
   getZScale,
   getPlotScale,
+  getCatAxis,
   (
     grid = {},
     scatterData,
@@ -222,7 +223,8 @@ export const getScatterPlotDataBySquareBinByCategory = createSelector(
     palette,
     reducer,
     scale,
-    plotScale
+    plotScale,
+    catAxis
   ) => {
     if (!scatterData) return undefined;
     let zScale = d3[scale]().domain(grid.range).range([0, grid.width]);
@@ -241,17 +243,29 @@ export const getScatterPlotDataBySquareBinByCategory = createSelector(
     scatterData.data.forEach((square) => {
       let squareData = [];
       bins.forEach((bin, i) => {
-        squareData[i] = {
-          id: square.id + "_" + i,
-          cellId: square.id,
-          index: i,
-          i: square.i,
-          j: square.j,
-          color: palette.colors[i],
-          ids: [],
-          zs: [],
-          indices: [],
-        };
+        if (i == 0 || isNaN(catAxis)) {
+          squareData[i] = {
+            id: square.id + "_" + i,
+            cellId: square.id,
+            index: i,
+            i: square.i,
+            j: square.j,
+            color: palette.colors[i],
+            ids: [],
+            zs: [],
+            indices: [],
+          };
+        } else {
+          squareData[i] = {
+            ...squareData[i - 1],
+            id: square.id + "_" + i,
+            cellId: square.id,
+            index: i,
+            i: square.i,
+            j: square.j,
+            color: palette.colors[i],
+          };
+        }
       });
       square.indices.forEach((index, i) => {
         let currentCell = squareData[keys[categories.values[index]]];
@@ -308,31 +322,22 @@ export const getBinnedDataByCategoryByAxis = createSelector(
       if (!iBinned[d.index][d.i]) iBinned[d.index][d.i] = { zs: [], ids: [] };
       if (!jBinned[d.index][d.j]) jBinned[d.index][d.j] = { zs: [], ids: [] };
       iBinned[d.index][d.i].zs = iBinned[d.index][d.i].zs.concat(d.zs);
-      //iBinned[d.index][d.i].ids = iBinned[d.index][d.i].ids.concat(d.ids)
-      //iBinned[d.index][d.i].indices = iBinned[d.index][d.i].indices.concat(d.indices)
       jBinned[d.index][d.j].zs = jBinned[d.index][d.j].zs.concat(d.zs);
-      //jBinned[d.index][d.j].ids = jBinned[d.index][d.j].ids.concat(d.ids)
-      //jBinned[d.index][d.j].indices = jBinned[d.index][d.j].indices.concat(d.indices)
     });
     let data = [];
     for (let index = 0; index < Math.max(bins.length, 1); index++) {
       Object.keys(iBinned[index]).forEach((i) => {
         let obj = { axis: "x", bin: i, index: index };
         obj.zs = iBinned[index][i].zs;
-        //obj.ids = iBinned[index][i].ids
-        //obj.indices = iBinned[index][i].indices
         data.push(obj);
       });
       Object.keys(jBinned[index]).forEach((j) => {
         let obj = { axis: "y", bin: j, index: index };
         obj.zs = jBinned[index][j].zs;
-        //obj.ids = jBinned[index][j].ids
-        //obj.indices = jBinned[index][j].indices
         data.push(obj);
       });
     }
     let grid = binnedData.grid;
-    console.log(data);
     return { data, grid };
   }
 );
@@ -346,7 +351,8 @@ export const getBinnedLinesByCategoryForAxis = createSelector(
   getSideMax,
   getZReducer,
   getZScale,
-  (axis, binnedData, categories, palette, side, reducer, scale) => {
+  getCatAxis,
+  (axis, binnedData, categories, palette, side, reducer, scale, catAxis) => {
     if (!binnedData) return undefined;
     let min = 0;
     let max = side;
@@ -359,7 +365,6 @@ export const getBinnedLinesByCategoryForAxis = createSelector(
       let z = reducer.func(d.zs);
       max = Math.max(max, z);
     });
-
     let zScale = d3.scaleLinear().domain([0, max]).range([0, 300]);
     // zScale = d3.scaleSqrt().clamp(1).domain([1,max]).range([0,300])
     let paths = [{ name: "all", color: "#999999", path: "M0 300" }];
@@ -367,7 +372,7 @@ export const getBinnedLinesByCategoryForAxis = createSelector(
       for (let i = 0; i < categories.keys.length; i++) {
         paths[i] = {
           name: categories.keys[i],
-          color: palette.colors[i],
+          color: isNaN(catAxis) ? palette.colors[i] : palette.colors[catAxis],
           path: "M0 300",
         };
       }
diff --git a/src/viewer/src/client/views/reducers/summary.js b/src/viewer/src/client/views/reducers/summary.js
index 2cbe8120..e8bc4b76 100644
--- a/src/viewer/src/client/views/reducers/summary.js
+++ b/src/viewer/src/client/views/reducers/summary.js
@@ -896,16 +896,16 @@ export const circularCurves = createSelector(
         )
     );
     pathProps.meanGC = { fill: palette.colors[1], stroke: "none" };
-    // paths.maxNupper = d3RadialLine()(ns.map((n,i)=>[cScale(i),oScale(n.max/2)]))
-    // pathProps.maxNupper = {fill:'none',stroke:'rgba(255,255,255,0.9)',strokeWidth:1}
     paths.maxNlower = d3RadialLine()(
       [[cScale(0), oScale(1)]]
         .concat(ns.map((n, i) => [cScale(i), oScale(1 - n.max / 2)]))
         .concat(ns.map((n, i) => [cScale(999 - i), oScale(1)]))
     );
     pathProps.maxNlower = {
-      fill: "rgba(255,255,255,0.2)",
-      stroke: "rgba(255,255,255,0.4)",
+      fill: "rgb(255,255,255)",
+      fillOpacity: 0.2,
+      stroke: "rgb(255,255,255)",
+      strokeOpacity: 0.4,
       strokeWidth: 0.5,
     };
     paths.maxNupper = d3RadialLine()(
@@ -914,8 +914,10 @@ export const circularCurves = createSelector(
         .concat(ns.map((n, i) => [cScale(999 - i), oScale(0)]))
     );
     pathProps.maxNupper = {
-      fill: "rgba(255,255,255,0.2)",
-      stroke: "rgba(255,255,255,0.4)",
+      fill: "rgb(255,255,255)",
+      fillOpacity: 0.2,
+      stroke: "rgb(255,255,255)",
+      strokeOpacity: 0.4,
       strokeWidth: 0.5,
     };
     paths.maxGC = d3RadialLine()(
@@ -923,21 +925,25 @@ export const circularCurves = createSelector(
         .concat(gc.map((n, i) => [cScale(i), oScale(n.max)]))
         .concat(gc.map((n, i) => [cScale(999 - i), oScale(gc[999 - i].mean)]))
     );
-    let stroke1 = palette.colors[1]
-      .replace("rgb", "rgba")
-      .replace(")", ",0.6)");
-    let fill1 = palette.colors[1].replace("rgb", "rgba").replace(")", ",0.4)");
-    pathProps.maxGC = { fill: fill1, stroke: stroke1, strokeWidth: 1 };
+    pathProps.maxGC = {
+      fill: palette.colors[1],
+      fillOpacity: 0.4,
+      stroke: palette.colors[1],
+      strokeOpacity: 0.6,
+      strokeWidth: 1,
+    };
     paths.minGC = d3RadialLine()(
       [[cScale(0), oScale(0)]]
         .concat(gc.map((n, i) => [cScale(i), oScale(n.min)]))
         .concat(gc.map((n, i) => [cScale(999 - i), oScale(gc[999 - i].mean)]))
     );
-    let stroke0 = palette.colors[0]
-      .replace("rgb", "rgba")
-      .replace(")", ",0.6)");
-    let fill0 = palette.colors[0].replace("rgb", "rgba").replace(")", ",0.4)");
-    pathProps.minGC = { fill: fill0, stroke: stroke0, strokeWidth: 1 };
+    pathProps.minGC = {
+      fill: palette.colors[0],
+      fillOpacity: 0.4,
+      stroke: palette.colors[0],
+      strokeOpacity: 0.6,
+      strokeWidth: 1,
+    };
     paths.meanGCstroke = d3RadialLine()(
       gc.map((n, i) => [cScale(i), oScale(n.mean)])
     );


Test patch:
diff --git a/.github/workflows/test-blobtoolkit.yml b/.github/workflows/test-blobtoolkit.yml
deleted file mode 100644
index d4f20446..00000000
--- a/.github/workflows/test-blobtoolkit.yml
+++ /dev/null
@@ -1,92 +0,0 @@
-name: test-blobtoolkit
-on: [push]
-jobs:
-  package-ui:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v3
-      - uses: actions/setup-node@v3
-        with:
-          node-version: "16"
-      - run: npm install -g pkg
-      - run: ./package-ui.sh
-      - uses: actions/upload-artifact@v3
-        with:
-          path: ./dist/*
-
-  package-api:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v3
-      - uses: actions/setup-node@v3
-        with:
-          node-version: "16"
-      - run: npm install -g pkg
-      - run: ./package-api.sh
-      - uses: actions/upload-artifact@v3
-        with:
-          path: ./dist/*
-
-  package-blobtoolkit-linux:
-    runs-on: ubuntu-latest
-    needs:
-      - package-ui
-      - package-api
-    steps:
-      - uses: actions/checkout@v3
-      - name: Collect artifacts
-        uses: actions/download-artifact@v3
-        with:
-          path: ./dist
-      - name: Install dependencies
-        run: |
-          python3 -m pip install --upgrade pip
-          pip install setuptools wheel twine
-      - name: Build and publish
-        run: |
-          mkdir -p src/data/bin
-          cp dist/artifact/*-linux src/data/bin/
-          rm -r dist/artifact
-          chmod 755 src/data/bin/*
-          ./pip_install_latest.sh manylinux2014_x86_64
-          blobtools -v
-      - uses: actions/upload-artifact@v3
-        with:
-          path: ./dist/*
-
-  test-commands:
-    runs-on: ubuntu-latest
-    needs:
-      - package-blobtoolkit-linux
-    steps:
-      - uses: actions/checkout@v3
-      - name: Collect artifacts
-        uses: actions/download-artifact@v3
-        with:
-          path: ./dist
-      - name: Pip install blobtoolkit
-        run: |
-          python3 -m pip install --upgrade pip
-          pip install dist/artifact/*linux*.whl
-          rm -r dist/artifact
-          blobtools -v
-      - name: Install dependencies
-        run: sudo apt-get install -y firefox xvfb
-      - name: Generate static plot
-        run: |
-          blobtools view \
-              --host http://localhost \
-              --timeout 60 \
-              --ports 8000-8099 \
-              --view blob \
-              --param plotShape=circle \
-              --param largeFonts=true \
-              --format png \
-              _
-      - name: Test plot exists
-        run: |
-          test -s "FXWY01.blob.circle.png" && exit 0 || exit 1
-      - uses: actions/upload-artifact@v3
-        with:
-          name: tests
-          path: FXWY01.blob.circle.png
diff --git a/pip_install_latest.sh b/pip_install_latest.sh
index 96b8d15b..ff10acb6 100755
--- a/pip_install_latest.sh
+++ b/pip_install_latest.sh
@@ -6,6 +6,7 @@ PLATFORM=$1
 
 if [ -z $PLATFORM ]; then
     echo USAGE: ./pip_install_latest.sh linux_x86_64
+    echo    OR: ./pip_install_latest.sh macosx_10_9_x86_64
     exit 1
 fi
 
diff --git a/src/blobtoolkit-host/pip_install_latest.sh b/src/blobtoolkit-host/pip_install_latest.sh
new file mode 100755
index 00000000..076ffe73
--- /dev/null
+++ b/src/blobtoolkit-host/pip_install_latest.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+
+# Build and install the latest blobtoolkit version in the current environment
+
+PLATFORM=$1
+
+if [ -z $PLATFORM ]; then
+    echo USAGE: ./pip_install_latest.sh linux_x86_64
+    echo    OR: ./pip_install_latest.sh macosx_10_9_x86_64
+    exit 1
+fi
+
+BTK_VERSION=$(
+    grep current_version `dirname "$0"`/../../.hostbumpversion.cfg \
+    | head -n 1 \
+    | awk '{print $3}')
+
+python3 setup.py sdist bdist_wheel --python-tag py3 --plat-name=$PLATFORM &&
+echo y | pip3 uninstall blobtoolkit-host &&
+pip3 install dist/blobtoolkit_host-${BTK_VERSION}-py3-none-$PLATFORM.whl &&
+blobtoolkit-host -v
diff --git a/src/blobtoolkit-pipeline/pip_install_latest.sh b/src/blobtoolkit-pipeline/pip_install_latest.sh
new file mode 100755
index 00000000..de4f90b6
--- /dev/null
+++ b/src/blobtoolkit-pipeline/pip_install_latest.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+
+# Build and install the latest blobtoolkit version in the current environment
+
+PLATFORM=$1
+
+if [ -z $PLATFORM ]; then
+    echo USAGE: ./pip_install_latest.sh linux_x86_64
+    echo    OR: ./pip_install_latest.sh macosx_10_9_x86_64
+    exit 1
+fi
+
+BTK_VERSION=$(
+    grep current_version `dirname "$0"`/../../.pipelinebumpversion.cfg \
+    | head -n 1 \
+    | awk '{print $3}')
+
+python3 setup.py sdist bdist_wheel --python-tag py3 --plat-name=$PLATFORM &&
+echo y | pip3 uninstall blobtoolkit-pipeline &&
+pip3 install dist/blobtoolkit_pipeline-${BTK_VERSION}-py3-none-$PLATFORM.whl &&
+blobtoolkit-pipeline -v
diff --git a/src/data/pip_install_latest.sh b/src/data/pip_install_latest.sh
new file mode 100755
index 00000000..7a21503f
--- /dev/null
+++ b/src/data/pip_install_latest.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+# Build and install the latest blobtoolkit version in the current environment
+
+PLATFORM=$1
+
+if [ -z $PLATFORM ]; then
+    echo USAGE: ./pip_install_latest.sh linux_x86_64
+    exit 1
+fi
+
+BTK_VERSION=$(
+    grep current_version `dirname "$0"`/.bumpversion.cfg \
+    | head -n 1 \
+    | awk '{print $3}')
+
+python3 setup.py sdist bdist_wheel --python-tag py3 --plat-name=$PLATFORM &&
+echo y | pip3 uninstall blobtoolkit-data &&
+pip3 install dist/blobtoolkit_data-${BTK_VERSION}-py3-none-$PLATFORM.whl &&
+blobtoolkit-data -v