Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f0918ba915 | |||
| 1683650240 | |||
| 9d7c3e5ad8 | |||
| 23bae2ab7d | |||
| e0b6c2082f | |||
| 2c889b472e | |||
| 349bb633ff |
@@ -5,8 +5,20 @@ on:
|
|||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
|
|
||||||
|
# Runner disk pressure notes:
|
||||||
|
# Gitea Actions runners use `catthehacker/ubuntu:act-latest` on a shared host
|
||||||
|
# with limited overlay space (~40 GB, often 70%+ used at start). Building both
|
||||||
|
# architectures of both variants on a single runner exhausted disk around the
|
||||||
|
# nodejs dpkg unpack / git-lfs layer export. To fix this:
|
||||||
|
# * smoke test (amd64 only, load into daemon) runs on its own runner
|
||||||
|
# * each push target (variant × arch) runs on its own runner, pushes by
|
||||||
|
# digest (no local image store), uploads digest as an artifact
|
||||||
|
# * a merge job composes the multi-arch manifest with `imagetools create`
|
||||||
|
# Per-runner disk pressure is now one-quarter of the old single-job peak.
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-base:
|
# ── Smoke test (amd64 only, gates the push jobs) ────────────────────
|
||||||
|
smoke-base:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: catthehacker/ubuntu:act-latest
|
image: catthehacker/ubuntu:act-latest
|
||||||
@@ -15,30 +27,13 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Force IPv4 for Docker Hub
|
- name: Force IPv4 for Docker Hub
|
||||||
run: |
|
run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
|
||||||
# Prefer IPv4 to avoid intermittent IPv6 connectivity failures
|
|
||||||
echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v4
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@v4
|
||||||
with:
|
with:
|
||||||
driver-opts: network=host
|
driver-opts: network=host
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@v4
|
|
||||||
with:
|
|
||||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Extract version from tag
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
VERSION=${GITHUB_REF#refs/tags/}
|
|
||||||
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Build and load amd64 image for smoke test
|
- name: Build and load amd64 image for smoke test
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
@@ -49,20 +44,9 @@ jobs:
|
|||||||
tags: opencode-devbox:smoke-base
|
tags: opencode-devbox:smoke-base
|
||||||
|
|
||||||
- name: Smoke test (amd64)
|
- name: Smoke test (amd64)
|
||||||
run: |
|
run: bash scripts/smoke-test.sh opencode-devbox:smoke-base --variant base
|
||||||
bash scripts/smoke-test.sh opencode-devbox:smoke-base --variant base
|
|
||||||
|
|
||||||
- name: Build and push (base)
|
smoke-omos:
|
||||||
uses: docker/build-push-action@v7
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:${{ steps.version.outputs.version }}
|
|
||||||
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:latest
|
|
||||||
|
|
||||||
build-omos:
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: catthehacker/ubuntu:act-latest
|
image: catthehacker/ubuntu:act-latest
|
||||||
@@ -71,30 +55,13 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Force IPv4 for Docker Hub
|
- name: Force IPv4 for Docker Hub
|
||||||
run: |
|
run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
|
||||||
# Prefer IPv4 to avoid intermittent IPv6 connectivity failures
|
|
||||||
echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v4
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@v4
|
||||||
with:
|
with:
|
||||||
driver-opts: network=host
|
driver-opts: network=host
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@v4
|
|
||||||
with:
|
|
||||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Extract version from tag
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
VERSION=${GITHUB_REF#refs/tags/}
|
|
||||||
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Build and load amd64 image for smoke test
|
- name: Build and load amd64 image for smoke test
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
@@ -107,24 +74,225 @@ jobs:
|
|||||||
tags: opencode-devbox:smoke-omos
|
tags: opencode-devbox:smoke-omos
|
||||||
|
|
||||||
- name: Smoke test (amd64)
|
- name: Smoke test (amd64)
|
||||||
run: |
|
run: bash scripts/smoke-test.sh opencode-devbox:smoke-omos --variant omos
|
||||||
bash scripts/smoke-test.sh opencode-devbox:smoke-omos --variant omos
|
|
||||||
|
|
||||||
- name: Build and push (omos)
|
# ── Per-arch push (by digest, no local image) ───────────────────────
|
||||||
|
build-base:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: smoke-base
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
platform:
|
||||||
|
- linux/amd64
|
||||||
|
- linux/arm64
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Force IPv4 for Docker Hub
|
||||||
|
run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
|
||||||
|
|
||||||
|
- name: Derive platform slug
|
||||||
|
id: platform
|
||||||
|
run: |
|
||||||
|
PLATFORM_PAIR="${{ matrix.platform }}"
|
||||||
|
echo "pair=${PLATFORM_PAIR//\//-}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
if: matrix.platform != 'linux/amd64'
|
||||||
|
uses: docker/setup-qemu-action@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v4
|
||||||
|
with:
|
||||||
|
driver-opts: network=host
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push by digest
|
||||||
|
id: build
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: ${{ matrix.platform }}
|
||||||
push: true
|
outputs: type=image,name=${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox,push-by-digest=true,name-canonical=true,push=true
|
||||||
|
|
||||||
|
- name: Export digest
|
||||||
|
run: |
|
||||||
|
mkdir -p /tmp/digests
|
||||||
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
|
touch "/tmp/digests/${digest#sha256:}"
|
||||||
|
|
||||||
|
- name: Upload digest
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: digests-base-${{ steps.platform.outputs.pair }}
|
||||||
|
path: /tmp/digests/*
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
build-omos:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: smoke-omos
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
platform:
|
||||||
|
- linux/amd64
|
||||||
|
- linux/arm64
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Force IPv4 for Docker Hub
|
||||||
|
run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
|
||||||
|
|
||||||
|
- name: Derive platform slug
|
||||||
|
id: platform
|
||||||
|
run: |
|
||||||
|
PLATFORM_PAIR="${{ matrix.platform }}"
|
||||||
|
echo "pair=${PLATFORM_PAIR//\//-}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
if: matrix.platform != 'linux/amd64'
|
||||||
|
uses: docker/setup-qemu-action@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v4
|
||||||
|
with:
|
||||||
|
driver-opts: network=host
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push by digest
|
||||||
|
id: build
|
||||||
|
uses: docker/build-push-action@v7
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: ${{ matrix.platform }}
|
||||||
build-args: |
|
build-args: |
|
||||||
INSTALL_OMOS=true
|
INSTALL_OMOS=true
|
||||||
tags: |
|
outputs: type=image,name=${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox,push-by-digest=true,name-canonical=true,push=true
|
||||||
|
|
||||||
|
- name: Export digest
|
||||||
|
run: |
|
||||||
|
mkdir -p /tmp/digests
|
||||||
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
|
touch "/tmp/digests/${digest#sha256:}"
|
||||||
|
|
||||||
|
- name: Upload digest
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: digests-omos-${{ steps.platform.outputs.pair }}
|
||||||
|
path: /tmp/digests/*
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
# ── Merge per-arch digests into multi-arch tags ─────────────────────
|
||||||
|
merge-base:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build-base
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
steps:
|
||||||
|
- name: Force IPv4 for Docker Hub
|
||||||
|
run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
|
||||||
|
|
||||||
|
- name: Download digests
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: /tmp/digests
|
||||||
|
pattern: digests-base-*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v4
|
||||||
|
with:
|
||||||
|
driver-opts: network=host
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract version from tag
|
||||||
|
id: version
|
||||||
|
run: echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Create manifest list and push
|
||||||
|
working-directory: /tmp/digests
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create \
|
||||||
|
-t ${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:${{ steps.version.outputs.version }} \
|
||||||
|
-t ${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:latest \
|
||||||
|
$(printf '${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox@sha256:%s ' *)
|
||||||
|
|
||||||
|
- name: Inspect image
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools inspect \
|
||||||
|
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:${{ steps.version.outputs.version }}
|
||||||
|
|
||||||
|
merge-omos:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build-omos
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
steps:
|
||||||
|
- name: Force IPv4 for Docker Hub
|
||||||
|
run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
|
||||||
|
|
||||||
|
- name: Download digests
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: /tmp/digests
|
||||||
|
pattern: digests-omos-*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v4
|
||||||
|
with:
|
||||||
|
driver-opts: network=host
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract version from tag
|
||||||
|
id: version
|
||||||
|
run: echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Create manifest list and push
|
||||||
|
working-directory: /tmp/digests
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create \
|
||||||
|
-t ${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:${{ steps.version.outputs.version }}-omos \
|
||||||
|
-t ${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:latest-omos \
|
||||||
|
$(printf '${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox@sha256:%s ' *)
|
||||||
|
|
||||||
|
- name: Inspect image
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools inspect \
|
||||||
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:${{ steps.version.outputs.version }}-omos
|
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:${{ steps.version.outputs.version }}-omos
|
||||||
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:latest-omos
|
|
||||||
|
|
||||||
update-description:
|
update-description:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [build-base, build-omos]
|
needs: [merge-base, merge-omos]
|
||||||
container:
|
container:
|
||||||
image: catthehacker/ubuntu:act-latest
|
image: catthehacker/ubuntu:act-latest
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ Docker image packaging [opencode](https://opencode.ai) into a production-ready d
|
|||||||
- `Dockerfile` — single multi-stage build for both variants. OMOS variant is controlled by `INSTALL_OMOS=true` build arg; mempalace is controlled by `INSTALL_MEMPALACE` (default `true`). All GitHub-sourced binaries are pinned with version ARGs.
|
- `Dockerfile` — single multi-stage build for both variants. OMOS variant is controlled by `INSTALL_OMOS=true` build arg; mempalace is controlled by `INSTALL_MEMPALACE` (default `true`). All GitHub-sourced binaries are pinned with version ARGs.
|
||||||
- `entrypoint.sh` — runs as root: UID/GID adjustment, SSH permissions, volume ownership fixes (skipped via `.devbox-owner` sentinel when ownership is already correct). Then drops to developer via gosu.
|
- `entrypoint.sh` — runs as root: UID/GID adjustment, SSH permissions, volume ownership fixes (skipped via `.devbox-owner` sentinel when ownership is already correct). Then drops to developer via gosu.
|
||||||
- `entrypoint-user.sh` — runs as developer: git config, opencode.json generation (delegated to `generate-config.py`), OMOS setup.
|
- `entrypoint-user.sh` — runs as developer: git config, opencode.json generation (delegated to `generate-config.py`), OMOS setup.
|
||||||
- `rootfs/usr/local/lib/opencode-devbox/generate-config.py` — generates `~/.config/opencode/opencode.json` from env vars. Never overwrites an existing config. Auto-registers MCP servers for detected tools (mempalace via the wrapper, gitea-mcp).
|
- `rootfs/usr/local/lib/opencode-devbox/generate-config.py` — generates `~/.config/opencode/opencode.json` from env vars. Never overwrites an existing config. Auto-registers MCP servers for detected tools (mempalace via the `mempalace-mcp` entry point, gitea-mcp).
|
||||||
- `rootfs/usr/local/bin/mempalace-mcp-server` — wrapper that exec's the mempalace uv-tool venv's python with `-m mempalace.mcp_server`. Needed because system `python3` can't import from the isolated venv created by `uv tool install`.
|
|
||||||
- `scripts/smoke-test.sh` — post-build image verification. Asserts binary presence, opencode startup, entrypoint correctness, config generation idempotency, and image size thresholds. Used by both CI workflows.
|
- `scripts/smoke-test.sh` — post-build image verification. Asserts binary presence, opencode startup, entrypoint correctness, config generation idempotency, and image size thresholds. Used by both CI workflows.
|
||||||
- `scripts/generate-dockerhub-md.py` — generates `DOCKER_HUB.md` from `README.md` using explicit section rules. `--check` fails if the committed file is out of sync (enforced by the `validate` workflow).
|
- `scripts/generate-dockerhub-md.py` — generates `DOCKER_HUB.md` from `README.md` using explicit section rules. `--check` fails if the committed file is out of sync (enforced by the `validate` workflow).
|
||||||
- `DOCKER_HUB.md` — **auto-generated** from README. Do not edit directly. Pushed to Docker Hub description via CI API call. Must stay under 25 kB. Short description field must be ≤100 bytes.
|
- `DOCKER_HUB.md` — **auto-generated** from README. Do not edit directly. Pushed to Docker Hub description via CI API call. Must stay under 25 kB. Short description field must be ≤100 bytes.
|
||||||
@@ -38,7 +37,7 @@ When bumping the opencode version, also bump `OPENCODE_VERSION` in `Dockerfile`
|
|||||||
- **GitHub/Gitea-sourced binaries float by default** — gosu, fzf, git-lfs, nvim, bat, eza, zoxide, uv, gitea-mcp, Go, oh-my-opencode-slim all default to `latest`. Each build-time install step reads the `/releases/latest` Location redirect (or the go.dev JSON feed for Go) and derives the concrete version. Use the same `ARCH` case-switch pattern for multi-arch support (amd64/arm64). Intentional pins: `OPENCODE_VERSION` (drives the image tag), `NODE_VERSION=22` (major pin), `DEBIAN_VERSION=trixie-slim` (OS base). Adding a new upstream tool: follow the existing floated-version pattern, don't hardcode a specific tag.
|
- **GitHub/Gitea-sourced binaries float by default** — gosu, fzf, git-lfs, nvim, bat, eza, zoxide, uv, gitea-mcp, Go, oh-my-opencode-slim all default to `latest`. Each build-time install step reads the `/releases/latest` Location redirect (or the go.dev JSON feed for Go) and derives the concrete version. Use the same `ARCH` case-switch pattern for multi-arch support (amd64/arm64). Intentional pins: `OPENCODE_VERSION` (drives the image tag), `NODE_VERSION=22` (major pin), `DEBIAN_VERSION=trixie-slim` (OS base). Adding a new upstream tool: follow the existing floated-version pattern, don't hardcode a specific tag.
|
||||||
- **Resolved versions are logged by the smoke test** — `scripts/smoke-test.sh` prints a "Resolved component versions" table as its first step. CI logs always capture what got baked into a given image even when ARGs default to `latest`.
|
- **Resolved versions are logged by the smoke test** — `scripts/smoke-test.sh` prints a "Resolved component versions" table as its first step. CI logs always capture what got baked into a given image even when ARGs default to `latest`.
|
||||||
- **Shell scripts use `set -euo pipefail`** — both entrypoints are strict. Errors in volume chown or SSH permission operations are intentionally suppressed with `|| true`.
|
- **Shell scripts use `set -euo pipefail`** — both entrypoints are strict. Errors in volume chown or SSH permission operations are intentionally suppressed with `|| true`.
|
||||||
- **MemPalace install path** — installed via `uv tool install` into `/opt/uv-tools/mempalace/`. The `mempalace` CLI is symlinked onto `PATH` by uv; the MCP server is reached via the `mempalace-mcp-server` wrapper. Do not use `pip install --break-system-packages` — that was the previous approach and has been removed.
|
- **MemPalace install path** — installed via `uv tool install` into `/opt/uv-tools/mempalace/`. Both the `mempalace` CLI and the `mempalace-mcp` MCP server binary are shipped as entry points by the mempalace package itself and placed on PATH by uv as shims whose shebangs point at the venv's Python. No hand-rolled wrapper is needed. Do not use `pip install --break-system-packages` — that was the previous approach and has been removed. Do not use `["python3", "-m", "mempalace.mcp_server"]` in `opencode.json` — system Python can't import from the uv venv.
|
||||||
- **generate-config.py idempotency** — the script MUST never overwrite an existing `opencode.json`. Users bind-mount their config directory or persist it across container recreations; accidentally clobbering that file would destroy hand-edits. The smoke test asserts this.
|
- **generate-config.py idempotency** — the script MUST never overwrite an existing `opencode.json`. Users bind-mount their config directory or persist it across container recreations; accidentally clobbering that file would destroy hand-edits. The smoke test asserts this.
|
||||||
- **Docker Hub description update** — uses `/v2/auth/token` endpoint (not the deprecated `/v2/users/login`). Auth uses `identifier`/`secret` fields, returns `access_token`, sent as `Bearer`. Short description must be ≤100 bytes.
|
- **Docker Hub description update** — uses `/v2/auth/token` endpoint (not the deprecated `/v2/users/login`). Auth uses `identifier`/`secret` fields, returns `access_token`, sent as `Bearer`. Short description must be ≤100 bytes.
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,56 @@ Tags follow `v{opencode_version}[letter]` — bare tag for the first build on a
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## v1.14.31 — 2026-05-01
|
||||||
|
|
||||||
|
Bump opencode to 1.14.31.
|
||||||
|
|
||||||
|
**CI infrastructure: split multi-arch publish across separate runners.**
|
||||||
|
|
||||||
|
- **Fix:** The `publish` workflow exhausted runner disk space on `v1.14.30b` and would have hit the same wall on any subsequent release. Both variants built both architectures on a single `catthehacker/ubuntu:act-latest` container with ~40 GB of shared overlay space, and the peak disk footprint during the nodejs dpkg unpack / git-lfs layer export pushed it over the edge (`No space left on device`). The mempalace-toolkit bake-in from v1.14.30b added the final straw; the underlying issue is that QEMU-emulated arm64 layers were stored alongside the amd64 build on the same runner.
|
||||||
|
- `docker-publish.yml` refactored to the canonical `push-by-digest` + manifest-merge pattern: smoke test (amd64) runs on its own runner, each `(variant × arch)` push target runs on its own fresh runner with `outputs: type=image,...,push-by-digest=true,push=true` (no local image store), then a tiny merge job assembles the multi-arch manifest with `docker buildx imagetools create` from digest artifacts.
|
||||||
|
- Per-runner disk peak is now roughly one-quarter of the old single-job peak. The four Docker Hub tags produced per release (`vX.Y.Z[n]`, `latest`, `vX.Y.Z[n]-omos`, `latest-omos`) are unchanged.
|
||||||
|
- Also parallelizes the amd64 and arm64 builds, so wall-clock time for a release should drop noticeably despite the added merge hop.
|
||||||
|
|
||||||
|
## v1.14.30b — 2026-04-30
|
||||||
|
|
||||||
|
**Bake mempalace-toolkit wrappers into the image.**
|
||||||
|
|
||||||
|
- **Fix:** The scheduler templates in [mempalace-toolkit's `contrib/`](https://gitea.jordbo.se/joakimp/mempalace-toolkit/src/branch/main/contrib) assume `mempalace-session` is available inside the container, but the image never actually installed it. Users following the `*-devbox` scheduler docs would silently lose the wrappers on every `docker compose up --force-recreate`, because the only way to get them was a post-hoc `./install.sh --yes` inside the container — which lives in the ephemeral layer. The host-side systemd timer would then fire, `docker exec` in, and hit `mempalace-session: command not found`. Caught during runtime validation on 2026-04-30.
|
||||||
|
- New Dockerfile block clones `mempalace-toolkit` at build time (depth-1) to `/opt/mempalace-toolkit/`, symlinks `bin/mempalace-session` and `bin/mempalace-docs` into `/usr/local/bin/`, and asserts both respond to `--help` before the layer succeeds.
|
||||||
|
- Gated by `ARG INSTALL_MEMPALACE_TOOLKIT=true` (defaults on, depends on `INSTALL_MEMPALACE=true`).
|
||||||
|
- Floated ref via `ARG MEMPALACE_TOOLKIT_REF=main` — override for reproducible builds once the toolkit starts tagging releases.
|
||||||
|
- **Tests:** Smoke test gains three toolkit assertions (`mempalace-session --help`, `mempalace-docs --help`, symlink target check). The resolved-versions preamble now logs the toolkit git short-SHA alongside the other floated components.
|
||||||
|
- **Docs:** README's MemPalace section gains a `Scheduled mining (mempalace-toolkit)` subsection covering the new wrappers and pointing at `contrib/` for scheduling. New build-args table entry for `INSTALL_MEMPALACE_TOOLKIT`.
|
||||||
|
|
||||||
|
## v1.14.30 — 2026-04-30
|
||||||
|
|
||||||
|
Bump opencode to 1.14.30.
|
||||||
|
|
||||||
|
## v1.14.29c — 2026-04-29
|
||||||
|
|
||||||
|
**Drop redundant mempalace-mcp-server wrapper, use the entry point mempalace ships.**
|
||||||
|
|
||||||
|
- **Fix:** MCP integration with mempalace was still broken for users with custom `opencode.json` files because they typically had `["python3", "-m", "mempalace.mcp_server"]` from v1.14.28b and earlier. With the uv-tool install path, system python3 can't import mempalace and the MCP server subprocess exits immediately — opencode surfaced this as `MCP error -32000: connection closed`. Users should migrate to `["mempalace-mcp"]`. The auto-generated config in new containers already emits the new form.
|
||||||
|
- **Cleanup:** Remove the hand-rolled `/usr/local/bin/mempalace-mcp-server` wrapper. The mempalace Python package ships a `mempalace-mcp` console entry point; `uv tool install` places it on PATH as a shim whose shebang points at the isolated venv's Python. The wrapper was duplicating what uv installs for free. Removed `rootfs/usr/local/bin/` and its COPY + chmod lines from the Dockerfile.
|
||||||
|
- **Docs:** README's MemPalace section now shows `["mempalace-mcp"]` and explicitly warns against `["python3", "-m", "mempalace.mcp_server"]` with the observed failure mode.
|
||||||
|
- **Tests:** Smoke test asserts `/usr/local/bin/mempalace-mcp` is executable and prints its symlink target, replacing the previous wrapper-present check.
|
||||||
|
|
||||||
|
## v1.14.29b — 2026-04-29
|
||||||
|
|
||||||
|
**Fix OMOS `bunx` detection + CI build reliability.**
|
||||||
|
|
||||||
|
- **Fix:** `entrypoint-user.sh` checked `command -v bunx` to gate the OMOS auto-install, but the OMOS image only ships the `bun` binary — upstream's bun installer never creates a `bunx` symlink and neither did our Dockerfile. The check always failed on a fresh OMOS image, so `bun x oh-my-opencode-slim@latest install` never ran and first-start OMOS setup would have printed `ENABLE_OMOS=true but bun is not installed.` even though bun was right there. Latent until now because the only exercised path had a persisted `oh-my-opencode-slim.json` from a prior install.
|
||||||
|
- Changed the gate to `command -v bun`.
|
||||||
|
- Changed both install invocations from `bunx oh-my-opencode-slim@latest install ...` to `bun x oh-my-opencode-slim@latest install ...`.
|
||||||
|
- Added `ln -sf bun /usr/local/bin/bunx` to the Dockerfile's OMOS block so interactive users can still type `bunx` by habit, and verified the symlink at build time (`test -L /usr/local/bin/bunx`).
|
||||||
|
- Smoke test now asserts the `bunx` symlink is present on the OMOS variant.
|
||||||
|
- **Fix:** CI build robustness against transient GitHub/Gitea CDN failures. The first attempt at building v1.14.29b tripped on a single HTTP 502 from GitHub's release CDN mid-download (`zoxide-0.9.9-x86_64-unknown-linux-musl.tar.gz`), failing the entire OMOS build with no retry. Fix applied to every tool-download curl in the Dockerfile:
|
||||||
|
- `curl --retry 5 --retry-delay 5 --retry-all-errors` on both the `-fsSL` GET requests and the `-sI` HEAD requests used for `/releases/latest` redirect resolution. 5 attempts with 5 s back-off eats most transient CDN hiccups without failing the build.
|
||||||
|
- Added `[ -n "$V" ]` assertion after each version-resolution step. If the HEAD redirect ever fails to produce a tag name, the build fails fast with an empty-version message rather than trying to download `.../v//...` and producing a confusing 404.
|
||||||
|
- Same hardening applied to the optional Go install block (go.dev JSON feed + tarball download) and the nodesource apt-repo setup script.
|
||||||
|
- **Security:** Added `apt-get upgrade -y` to the core-packages RUN step. Picks up any security/CVE fixes published between `debian:trixie-slim` base-image rebuilds. Paired with the existing `update` and `install` in the same layer so image history isn't bloated. Today this produced `0 upgraded` (base image is current), but it future-proofs against the next CVE drop.
|
||||||
|
|
||||||
## v1.14.29 — 2026-04-28
|
## v1.14.29 — 2026-04-28
|
||||||
|
|
||||||
**Opencode 1.14.29 + infrastructure and maintainability pass.**
|
**Opencode 1.14.29 + infrastructure and maintainability pass.**
|
||||||
|
|||||||
+20
-2
@@ -421,13 +421,13 @@ Add mempalace as an MCP server in your `opencode.json` (inside `~/.config/openco
|
|||||||
"mcp": {
|
"mcp": {
|
||||||
"mempalace": {
|
"mempalace": {
|
||||||
"type": "local",
|
"type": "local",
|
||||||
"command": ["mempalace-mcp-server"]
|
"command": ["mempalace-mcp"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> The image installs mempalace into an isolated `uv tool` venv at `/opt/uv-tools/mempalace`. The `mempalace-mcp-server` wrapper on `PATH` exec's the venv's Python with the `mempalace.mcp_server` module — you don't need to know about the venv to use it.
|
> The image installs mempalace into an isolated `uv tool` venv at `/opt/uv-tools/mempalace/`. `uv tool install` places `mempalace-mcp` on `PATH` as a shim whose shebang points at the venv's Python, so MCP clients can invoke it as a normal binary without worrying about the venv. Do **not** use `["python3", "-m", "mempalace.mcp_server"]` — the system Python cannot import from the uv-managed venv and you'll get `ModuleNotFoundError` / `MCP error -32000: connection closed`.
|
||||||
|
|
||||||
This gives opencode access to 29 MCP tools for searching memory, querying the knowledge graph, managing wings/rooms/drawers, and agent diaries.
|
This gives opencode access to 29 MCP tools for searching memory, querying the knowledge graph, managing wings/rooms/drawers, and agent diaries.
|
||||||
|
|
||||||
@@ -449,6 +449,24 @@ mempalace wake-up
|
|||||||
|
|
||||||
Each workspace gets its own isolated "wing" — memories never leak between projects.
|
Each workspace gets its own isolated "wing" — memories never leak between projects.
|
||||||
|
|
||||||
|
### Scheduled mining (mempalace-toolkit)
|
||||||
|
|
||||||
|
The image bakes in [mempalace-toolkit](https://gitea.jordbo.se/joakimp/mempalace-toolkit), a small set of bash wrappers that pair with mempalace for two common routines:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Mine opencode session history (reads ~/.local/share/opencode/opencode.db, stages JSONL, mines into wing_conversations)
|
||||||
|
mempalace-session
|
||||||
|
|
||||||
|
# Mine a project's docs into a dedicated wing
|
||||||
|
mempalace-docs /workspace/my-project
|
||||||
|
```
|
||||||
|
|
||||||
|
Both wrappers are idempotent and dedup-aware — re-running them on unchanged input is a cheap no-op.
|
||||||
|
|
||||||
|
For weekly automated runs, the toolkit ships ready-to-use scheduler templates (systemd user timer, launchd user agent, cron) in its [`contrib/`](https://gitea.jordbo.se/joakimp/mempalace-toolkit/src/branch/main/contrib) directory. The `*-devbox` variants are designed for this container: host-side schedulers that `docker exec` into the running opencode-devbox.
|
||||||
|
|
||||||
|
Disable the toolkit (keeps mempalace itself) with `--build-arg INSTALL_MEMPALACE_TOOLKIT=false`. Pin to a specific ref with `--build-arg MEMPALACE_TOOLKIT_REF=v0.3.0` once tagged releases exist.
|
||||||
|
|
||||||
### Storage
|
### Storage
|
||||||
|
|
||||||
Two separate named volumes keep different data classes apart:
|
Two separate named volumes keep different data classes apart:
|
||||||
|
|||||||
+69
-28
@@ -5,7 +5,7 @@ ARG DEBIAN_VERSION=trixie-slim
|
|||||||
FROM debian:${DEBIAN_VERSION} AS base
|
FROM debian:${DEBIAN_VERSION} AS base
|
||||||
|
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG OPENCODE_VERSION=1.14.29
|
ARG OPENCODE_VERSION=1.14.31
|
||||||
|
|
||||||
LABEL maintainer="joakimp"
|
LABEL maintainer="joakimp"
|
||||||
LABEL description="Portable opencode developer container"
|
LABEL description="Portable opencode developer container"
|
||||||
@@ -15,7 +15,12 @@ LABEL org.opencontainers.image.source="https://gitea.jordbo.se/joakimp/opencode-
|
|||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
# ── Core system packages ─────────────────────────────────────────────
|
# ── Core system packages ─────────────────────────────────────────────
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
# apt-get upgrade picks up any security/CVE fixes published between
|
||||||
|
# debian:trixie-slim base-image rebuilds. Paired with the index update
|
||||||
|
# and the install in the same layer so we don't bloat image history.
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get upgrade -y --no-install-recommends && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
wget \
|
wget \
|
||||||
@@ -45,6 +50,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
python3-pip \
|
python3-pip \
|
||||||
python3-venv \
|
python3-venv \
|
||||||
&& ln -s /usr/bin/fdfind /usr/local/bin/fd \
|
&& ln -s /usr/bin/fdfind /usr/local/bin/fd \
|
||||||
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# ── Go-compiled tools (install from GitHub to avoid CVEs in Debian's old Go builds)
|
# ── Go-compiled tools (install from GitHub to avoid CVEs in Debian's old Go builds)
|
||||||
@@ -68,11 +74,12 @@ ARG GOSU_VERSION=latest
|
|||||||
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \
|
||||||
V="${GOSU_VERSION}" && \
|
V="${GOSU_VERSION}" && \
|
||||||
if [ "$V" = "latest" ]; then \
|
if [ "$V" = "latest" ]; then \
|
||||||
V=$(curl -sI "https://github.com/tianon/gosu/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/tianon/gosu/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
||||||
fi && \
|
fi && \
|
||||||
V="${V#v}" && \
|
V="${V#v}" && \
|
||||||
|
[ -n "$V" ] && \
|
||||||
echo "Installing gosu ${V}" && \
|
echo "Installing gosu ${V}" && \
|
||||||
curl -fsSL "https://github.com/tianon/gosu/releases/download/${V}/gosu-${ARCH}" -o /usr/local/bin/gosu && \
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/tianon/gosu/releases/download/${V}/gosu-${ARCH}" -o /usr/local/bin/gosu && \
|
||||||
chmod +x /usr/local/bin/gosu && \
|
chmod +x /usr/local/bin/gosu && \
|
||||||
gosu --version
|
gosu --version
|
||||||
|
|
||||||
@@ -81,11 +88,12 @@ ARG FZF_VERSION=latest
|
|||||||
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \
|
||||||
V="${FZF_VERSION}" && \
|
V="${FZF_VERSION}" && \
|
||||||
if [ "$V" = "latest" ]; then \
|
if [ "$V" = "latest" ]; then \
|
||||||
V=$(curl -sI "https://github.com/junegunn/fzf/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/junegunn/fzf/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
||||||
fi && \
|
fi && \
|
||||||
V="${V#v}" && \
|
V="${V#v}" && \
|
||||||
|
[ -n "$V" ] && \
|
||||||
echo "Installing fzf ${V}" && \
|
echo "Installing fzf ${V}" && \
|
||||||
curl -fsSL "https://github.com/junegunn/fzf/releases/download/v${V}/fzf-${V}-linux_${ARCH}.tar.gz" | tar -xz -C /usr/local/bin fzf && \
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/junegunn/fzf/releases/download/v${V}/fzf-${V}-linux_${ARCH}.tar.gz" | tar -xz -C /usr/local/bin fzf && \
|
||||||
fzf --version
|
fzf --version
|
||||||
|
|
||||||
# git-lfs — Git Large File Storage
|
# git-lfs — Git Large File Storage
|
||||||
@@ -93,11 +101,12 @@ ARG GIT_LFS_VERSION=latest
|
|||||||
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \
|
||||||
V="${GIT_LFS_VERSION}" && \
|
V="${GIT_LFS_VERSION}" && \
|
||||||
if [ "$V" = "latest" ]; then \
|
if [ "$V" = "latest" ]; then \
|
||||||
V=$(curl -sI "https://github.com/git-lfs/git-lfs/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/git-lfs/git-lfs/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
||||||
fi && \
|
fi && \
|
||||||
V="${V#v}" && \
|
V="${V#v}" && \
|
||||||
|
[ -n "$V" ] && \
|
||||||
echo "Installing git-lfs ${V}" && \
|
echo "Installing git-lfs ${V}" && \
|
||||||
curl -fsSL "https://github.com/git-lfs/git-lfs/releases/download/v${V}/git-lfs-linux-${ARCH}-v${V}.tar.gz" | tar -xz -C /tmp && \
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/git-lfs/git-lfs/releases/download/v${V}/git-lfs-linux-${ARCH}-v${V}.tar.gz" | tar -xz -C /tmp && \
|
||||||
install /tmp/git-lfs-${V}/git-lfs /usr/local/bin/git-lfs && \
|
install /tmp/git-lfs-${V}/git-lfs /usr/local/bin/git-lfs && \
|
||||||
rm -rf /tmp/git-lfs-${V} && \
|
rm -rf /tmp/git-lfs-${V} && \
|
||||||
git lfs install --system && \
|
git lfs install --system && \
|
||||||
@@ -108,11 +117,12 @@ ARG NVIM_VERSION=latest
|
|||||||
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "arm64" ;; *) echo "x86_64" ;; esac) && \
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "arm64" ;; *) echo "x86_64" ;; esac) && \
|
||||||
V="${NVIM_VERSION}" && \
|
V="${NVIM_VERSION}" && \
|
||||||
if [ "$V" = "latest" ]; then \
|
if [ "$V" = "latest" ]; then \
|
||||||
V=$(curl -sI "https://github.com/neovim/neovim/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/neovim/neovim/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
||||||
fi && \
|
fi && \
|
||||||
V="${V#v}" && \
|
V="${V#v}" && \
|
||||||
|
[ -n "$V" ] && \
|
||||||
echo "Installing neovim ${V}" && \
|
echo "Installing neovim ${V}" && \
|
||||||
curl -fsSL "https://github.com/neovim/neovim/releases/download/v${V}/nvim-linux-${ARCH}.tar.gz" | tar -xz -C /opt && \
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/neovim/neovim/releases/download/v${V}/nvim-linux-${ARCH}.tar.gz" | tar -xz -C /opt && \
|
||||||
ln -s /opt/nvim-linux-${ARCH}/bin/nvim /usr/local/bin/nvim && \
|
ln -s /opt/nvim-linux-${ARCH}/bin/nvim /usr/local/bin/nvim && \
|
||||||
nvim --version | head -1
|
nvim --version | head -1
|
||||||
|
|
||||||
@@ -121,11 +131,12 @@ ARG BAT_VERSION=latest
|
|||||||
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
||||||
V="${BAT_VERSION}" && \
|
V="${BAT_VERSION}" && \
|
||||||
if [ "$V" = "latest" ]; then \
|
if [ "$V" = "latest" ]; then \
|
||||||
V=$(curl -sI "https://github.com/sharkdp/bat/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/sharkdp/bat/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
||||||
fi && \
|
fi && \
|
||||||
V="${V#v}" && \
|
V="${V#v}" && \
|
||||||
|
[ -n "$V" ] && \
|
||||||
echo "Installing bat ${V}" && \
|
echo "Installing bat ${V}" && \
|
||||||
curl -fsSL "https://github.com/sharkdp/bat/releases/download/v${V}/bat-v${V}-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /tmp && \
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/sharkdp/bat/releases/download/v${V}/bat-v${V}-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /tmp && \
|
||||||
install /tmp/bat-v${V}-${ARCH}-unknown-linux-musl/bat /usr/local/bin/bat && \
|
install /tmp/bat-v${V}-${ARCH}-unknown-linux-musl/bat /usr/local/bin/bat && \
|
||||||
rm -rf /tmp/bat-v${V}-* && \
|
rm -rf /tmp/bat-v${V}-* && \
|
||||||
bat --version
|
bat --version
|
||||||
@@ -135,11 +146,12 @@ ARG EZA_VERSION=latest
|
|||||||
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
||||||
V="${EZA_VERSION}" && \
|
V="${EZA_VERSION}" && \
|
||||||
if [ "$V" = "latest" ]; then \
|
if [ "$V" = "latest" ]; then \
|
||||||
V=$(curl -sI "https://github.com/eza-community/eza/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/eza-community/eza/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
||||||
fi && \
|
fi && \
|
||||||
V="${V#v}" && \
|
V="${V#v}" && \
|
||||||
|
[ -n "$V" ] && \
|
||||||
echo "Installing eza ${V}" && \
|
echo "Installing eza ${V}" && \
|
||||||
curl -fsSL "https://github.com/eza-community/eza/releases/download/v${V}/eza_${ARCH}-unknown-linux-gnu.tar.gz" | tar -xz -C /usr/local/bin && \
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/eza-community/eza/releases/download/v${V}/eza_${ARCH}-unknown-linux-gnu.tar.gz" | tar -xz -C /usr/local/bin && \
|
||||||
eza --version | head -1
|
eza --version | head -1
|
||||||
|
|
||||||
# zoxide — smarter cd command
|
# zoxide — smarter cd command
|
||||||
@@ -147,11 +159,12 @@ ARG ZOXIDE_VERSION=latest
|
|||||||
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
||||||
V="${ZOXIDE_VERSION}" && \
|
V="${ZOXIDE_VERSION}" && \
|
||||||
if [ "$V" = "latest" ]; then \
|
if [ "$V" = "latest" ]; then \
|
||||||
V=$(curl -sI "https://github.com/ajeetdsouza/zoxide/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/ajeetdsouza/zoxide/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
||||||
fi && \
|
fi && \
|
||||||
V="${V#v}" && \
|
V="${V#v}" && \
|
||||||
|
[ -n "$V" ] && \
|
||||||
echo "Installing zoxide ${V}" && \
|
echo "Installing zoxide ${V}" && \
|
||||||
curl -fsSL "https://github.com/ajeetdsouza/zoxide/releases/download/v${V}/zoxide-${V}-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /usr/local/bin zoxide && \
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/ajeetdsouza/zoxide/releases/download/v${V}/zoxide-${V}-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /usr/local/bin zoxide && \
|
||||||
zoxide --version
|
zoxide --version
|
||||||
|
|
||||||
# uv — fast Python package manager (replaces pip, venv, pyenv)
|
# uv — fast Python package manager (replaces pip, venv, pyenv)
|
||||||
@@ -160,11 +173,12 @@ ARG UV_VERSION=latest
|
|||||||
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
||||||
V="${UV_VERSION}" && \
|
V="${UV_VERSION}" && \
|
||||||
if [ "$V" = "latest" ]; then \
|
if [ "$V" = "latest" ]; then \
|
||||||
V=$(curl -sI "https://github.com/astral-sh/uv/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/astral-sh/uv/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
||||||
fi && \
|
fi && \
|
||||||
V="${V#v}" && \
|
V="${V#v}" && \
|
||||||
|
[ -n "$V" ] && \
|
||||||
echo "Installing uv ${V}" && \
|
echo "Installing uv ${V}" && \
|
||||||
curl -fsSL "https://github.com/astral-sh/uv/releases/download/${V}/uv-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /tmp && \
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/astral-sh/uv/releases/download/${V}/uv-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /tmp && \
|
||||||
install /tmp/uv-${ARCH}-unknown-linux-musl/uv /usr/local/bin/uv && \
|
install /tmp/uv-${ARCH}-unknown-linux-musl/uv /usr/local/bin/uv && \
|
||||||
install /tmp/uv-${ARCH}-unknown-linux-musl/uvx /usr/local/bin/uvx && \
|
install /tmp/uv-${ARCH}-unknown-linux-musl/uvx /usr/local/bin/uvx && \
|
||||||
rm -rf /tmp/uv-* && \
|
rm -rf /tmp/uv-* && \
|
||||||
@@ -193,12 +207,37 @@ RUN if [ "${INSTALL_MEMPALACE}" = "true" ]; then \
|
|||||||
/opt/uv-tools/mempalace/bin/python -c "import mempalace; print('mempalace', mempalace.__version__ if hasattr(mempalace, '__version__') else 'installed')" ; \
|
/opt/uv-tools/mempalace/bin/python -c "import mempalace; print('mempalace', mempalace.__version__ if hasattr(mempalace, '__version__') else 'installed')" ; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── mempalace-toolkit — bash wrappers for session/docs mining ────────
|
||||||
|
# Thin wrappers (`mempalace-session`, `mempalace-docs`) that delegate to
|
||||||
|
# the mempalace Python CLI for two common scheduled tasks:
|
||||||
|
# - mempalace-session: mines opencode's SQLite session history into
|
||||||
|
# the palace (wing_conversations). Referenced by contrib/ scheduler
|
||||||
|
# templates (systemd user timer, cron) in the toolkit repo.
|
||||||
|
# - mempalace-docs: mines project docs into a per-project wing.
|
||||||
|
# Repo source of truth: https://gitea.jordbo.se/joakimp/mempalace-toolkit
|
||||||
|
#
|
||||||
|
# Requires INSTALL_MEMPALACE=true (wrappers shell out to `mempalace`).
|
||||||
|
# Disable with --build-arg INSTALL_MEMPALACE_TOOLKIT=false if you don't
|
||||||
|
# use the scheduled-mining workflow.
|
||||||
|
ARG INSTALL_MEMPALACE_TOOLKIT=true
|
||||||
|
ARG MEMPALACE_TOOLKIT_REF=main
|
||||||
|
RUN if [ "${INSTALL_MEMPALACE}" = "true" ] && [ "${INSTALL_MEMPALACE_TOOLKIT}" = "true" ]; then \
|
||||||
|
git clone --depth 1 --branch "${MEMPALACE_TOOLKIT_REF}" \
|
||||||
|
https://gitea.jordbo.se/joakimp/mempalace-toolkit.git /opt/mempalace-toolkit && \
|
||||||
|
ln -sf /opt/mempalace-toolkit/bin/mempalace-session /usr/local/bin/mempalace-session && \
|
||||||
|
ln -sf /opt/mempalace-toolkit/bin/mempalace-docs /usr/local/bin/mempalace-docs && \
|
||||||
|
chmod +x /opt/mempalace-toolkit/bin/mempalace-session /opt/mempalace-toolkit/bin/mempalace-docs && \
|
||||||
|
mempalace-session --help >/dev/null && \
|
||||||
|
mempalace-docs --help >/dev/null && \
|
||||||
|
echo "mempalace-toolkit installed at $(cd /opt/mempalace-toolkit && git rev-parse --short HEAD)" ; \
|
||||||
|
fi
|
||||||
|
|
||||||
# rustup — Rust toolchain manager
|
# rustup — Rust toolchain manager
|
||||||
# Installs the rustup-init binary only. Users bootstrap Rust with:
|
# Installs the rustup-init binary only. Users bootstrap Rust with:
|
||||||
# rustup-init -y && source ~/.cargo/env
|
# rustup-init -y && source ~/.cargo/env
|
||||||
# Toolchains persist via devbox-rustup and devbox-cargo volumes.
|
# Toolchains persist via devbox-rustup and devbox-cargo volumes.
|
||||||
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
||||||
curl -fsSL "https://static.rust-lang.org/rustup/dist/${ARCH}-unknown-linux-gnu/rustup-init" -o /usr/local/bin/rustup-init && \
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://static.rust-lang.org/rustup/dist/${ARCH}-unknown-linux-gnu/rustup-init" -o /usr/local/bin/rustup-init && \
|
||||||
chmod +x /usr/local/bin/rustup-init
|
chmod +x /usr/local/bin/rustup-init
|
||||||
|
|
||||||
# gitea-mcp — MCP server for Gitea API (official, Go binary, hosted on gitea.com)
|
# gitea-mcp — MCP server for Gitea API (official, Go binary, hosted on gitea.com)
|
||||||
@@ -206,11 +245,12 @@ ARG GITEA_MCP_VERSION=latest
|
|||||||
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "arm64" ;; *) echo "x86_64" ;; esac) && \
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "arm64" ;; *) echo "x86_64" ;; esac) && \
|
||||||
V="${GITEA_MCP_VERSION}" && \
|
V="${GITEA_MCP_VERSION}" && \
|
||||||
if [ "$V" = "latest" ]; then \
|
if [ "$V" = "latest" ]; then \
|
||||||
V=$(curl -sI "https://gitea.com/gitea/gitea-mcp/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://gitea.com/gitea/gitea-mcp/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
||||||
fi && \
|
fi && \
|
||||||
V="${V#v}" && \
|
V="${V#v}" && \
|
||||||
|
[ -n "$V" ] && \
|
||||||
echo "Installing gitea-mcp ${V}" && \
|
echo "Installing gitea-mcp ${V}" && \
|
||||||
curl -fsSL "https://gitea.com/gitea/gitea-mcp/releases/download/v${V}/gitea-mcp_Linux_${ARCH}.tar.gz" \
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://gitea.com/gitea/gitea-mcp/releases/download/v${V}/gitea-mcp_Linux_${ARCH}.tar.gz" \
|
||||||
| tar -xz -C /usr/local/bin/ gitea-mcp && \
|
| tar -xz -C /usr/local/bin/ gitea-mcp && \
|
||||||
chmod +x /usr/local/bin/gitea-mcp && \
|
chmod +x /usr/local/bin/gitea-mcp && \
|
||||||
gitea-mcp --version
|
gitea-mcp --version
|
||||||
@@ -226,7 +266,7 @@ ENV PATH="/home/developer/.local/bin:/home/developer/.cargo/bin:${PATH}"
|
|||||||
|
|
||||||
# ── Node.js (required for opencode v1.x install + MCP servers) ──────
|
# ── Node.js (required for opencode v1.x install + MCP servers) ──────
|
||||||
ARG NODE_VERSION=22
|
ARG NODE_VERSION=22
|
||||||
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - && \
|
RUN curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - && \
|
||||||
apt-get install -y --no-install-recommends nodejs && \
|
apt-get install -y --no-install-recommends nodejs && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
@@ -241,7 +281,7 @@ RUN ARCH=$(case "${TARGETARCH}" in \
|
|||||||
arm64) echo "aarch64" ;; \
|
arm64) echo "aarch64" ;; \
|
||||||
*) echo "x86_64" ;; \
|
*) echo "x86_64" ;; \
|
||||||
esac) && \
|
esac) && \
|
||||||
curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-${ARCH}.zip" -o /tmp/awscli.zip && \
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://awscli.amazonaws.com/awscli-exe-linux-${ARCH}.zip" -o /tmp/awscli.zip && \
|
||||||
unzip -q /tmp/awscli.zip -d /tmp && \
|
unzip -q /tmp/awscli.zip -d /tmp && \
|
||||||
/tmp/aws/install && \
|
/tmp/aws/install && \
|
||||||
rm -rf /tmp/aws /tmp/awscli.zip && \
|
rm -rf /tmp/aws /tmp/awscli.zip && \
|
||||||
@@ -257,11 +297,12 @@ RUN if [ "${INSTALL_GO}" = "true" ]; then \
|
|||||||
GOARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \
|
GOARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \
|
||||||
V="${GO_VERSION}" && \
|
V="${GO_VERSION}" && \
|
||||||
if [ "$V" = "latest" ]; then \
|
if [ "$V" = "latest" ]; then \
|
||||||
V=$(curl -fsSL "https://go.dev/dl/?mode=json" | \
|
V=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://go.dev/dl/?mode=json" | \
|
||||||
awk -F'"' '/"version":/ { sub(/^go/,"",$4); print $4; exit }'); \
|
awk -F'"' '/"version":/ { sub(/^go/,"",$4); print $4; exit }'); \
|
||||||
fi && \
|
fi && \
|
||||||
|
[ -n "$V" ] && \
|
||||||
echo "Installing Go ${V}" && \
|
echo "Installing Go ${V}" && \
|
||||||
curl -fsSL "https://go.dev/dl/go${V}.linux-${GOARCH}.tar.gz" | tar -C /usr/local -xz && \
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://go.dev/dl/go${V}.linux-${GOARCH}.tar.gz" | tar -C /usr/local -xz && \
|
||||||
ln -s /usr/local/go/bin/go /usr/local/bin/go && \
|
ln -s /usr/local/go/bin/go /usr/local/bin/go && \
|
||||||
ln -s /usr/local/go/bin/gofmt /usr/local/bin/gofmt; \
|
ln -s /usr/local/go/bin/gofmt /usr/local/bin/gofmt; \
|
||||||
fi
|
fi
|
||||||
@@ -280,12 +321,14 @@ RUN if [ "${INSTALL_OMOS}" = "true" ]; then \
|
|||||||
elif [ "$ARCH" = "aarch64" ]; then \
|
elif [ "$ARCH" = "aarch64" ]; then \
|
||||||
BUN_ARCH="aarch64"; \
|
BUN_ARCH="aarch64"; \
|
||||||
fi && \
|
fi && \
|
||||||
curl -fsSL "https://github.com/oven-sh/bun/releases/latest/download/bun-linux-${BUN_ARCH}.zip" -o /tmp/bun.zip && \
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/oven-sh/bun/releases/latest/download/bun-linux-${BUN_ARCH}.zip" -o /tmp/bun.zip && \
|
||||||
unzip -o /tmp/bun.zip -d /tmp/bun && \
|
unzip -o /tmp/bun.zip -d /tmp/bun && \
|
||||||
mv /tmp/bun/bun-linux-${BUN_ARCH}/bun /usr/local/bin/bun && \
|
mv /tmp/bun/bun-linux-${BUN_ARCH}/bun /usr/local/bin/bun && \
|
||||||
chmod +x /usr/local/bin/bun && \
|
chmod +x /usr/local/bin/bun && \
|
||||||
|
ln -sf bun /usr/local/bin/bunx && \
|
||||||
rm -rf /tmp/bun /tmp/bun.zip && \
|
rm -rf /tmp/bun /tmp/bun.zip && \
|
||||||
bun --version && \
|
bun --version && \
|
||||||
|
test -L /usr/local/bin/bunx && \
|
||||||
npm install -g oh-my-opencode-slim@${OMOS_VERSION}; \
|
npm install -g oh-my-opencode-slim@${OMOS_VERSION}; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -322,11 +365,9 @@ COPY rootfs/home/developer/.inputrc /etc/skel-devbox/.inputrc
|
|||||||
|
|
||||||
# ── Entrypoint ────────────────────────────────────────────────────────
|
# ── Entrypoint ────────────────────────────────────────────────────────
|
||||||
COPY rootfs/usr/local/lib/opencode-devbox/ /usr/local/lib/opencode-devbox/
|
COPY rootfs/usr/local/lib/opencode-devbox/ /usr/local/lib/opencode-devbox/
|
||||||
COPY rootfs/usr/local/bin/ /usr/local/bin/
|
|
||||||
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||||
COPY entrypoint-user.sh /usr/local/bin/entrypoint-user.sh
|
COPY entrypoint-user.sh /usr/local/bin/entrypoint-user.sh
|
||||||
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/entrypoint-user.sh \
|
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/entrypoint-user.sh \
|
||||||
/usr/local/bin/mempalace-mcp-server \
|
|
||||||
/usr/local/lib/opencode-devbox/*.py
|
/usr/local/lib/opencode-devbox/*.py
|
||||||
|
|
||||||
# Start as root — entrypoint adjusts UID/GID then drops to developer
|
# Start as root — entrypoint adjusts UID/GID then drops to developer
|
||||||
|
|||||||
@@ -339,6 +339,7 @@ docker compose build --build-arg NVIM_VERSION=0.12.1 # pin to a specific versi
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `INSTALL_GO` | `false` | Go toolchain (resolves latest stable from go.dev when `GO_VERSION=latest`) |
|
| `INSTALL_GO` | `false` | Go toolchain (resolves latest stable from go.dev when `GO_VERSION=latest`) |
|
||||||
| `INSTALL_MEMPALACE` | `true` | [MemPalace](https://github.com/MemPalace/mempalace) local AI memory system (~300 MB — disable to shrink image if you don't need MCP memory) |
|
| `INSTALL_MEMPALACE` | `true` | [MemPalace](https://github.com/MemPalace/mempalace) local AI memory system (~300 MB — disable to shrink image if you don't need MCP memory) |
|
||||||
|
| `INSTALL_MEMPALACE_TOOLKIT` | `true` | [mempalace-toolkit](https://gitea.jordbo.se/joakimp/mempalace-toolkit) bash wrappers (`mempalace-session`, `mempalace-docs`). Cloned at build time from `MEMPALACE_TOOLKIT_REF` (default `main`). Requires `INSTALL_MEMPALACE=true`. |
|
||||||
| `INSTALL_OMOS` | `false` | [oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) multi-agent orchestration (installs Bun and plugin) |
|
| `INSTALL_OMOS` | `false` | [oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) multi-agent orchestration (installs Bun and plugin) |
|
||||||
| `OPENCODE_VERSION` | *(pinned per release)* | opencode npm version. Drives the image tag and is intentionally not floated. |
|
| `OPENCODE_VERSION` | *(pinned per release)* | opencode npm version. Drives the image tag and is intentionally not floated. |
|
||||||
| `NODE_VERSION` | `22` | Node.js major version. Pinned to protect against upstream breaking changes across majors. |
|
| `NODE_VERSION` | `22` | Node.js major version. Pinned to protect against upstream breaking changes across majors. |
|
||||||
@@ -474,13 +475,13 @@ Add mempalace as an MCP server in your `opencode.json` (inside `~/.config/openco
|
|||||||
"mcp": {
|
"mcp": {
|
||||||
"mempalace": {
|
"mempalace": {
|
||||||
"type": "local",
|
"type": "local",
|
||||||
"command": ["mempalace-mcp-server"]
|
"command": ["mempalace-mcp"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> The image installs mempalace into an isolated `uv tool` venv at `/opt/uv-tools/mempalace`. The `mempalace-mcp-server` wrapper on `PATH` exec's the venv's Python with the `mempalace.mcp_server` module — you don't need to know about the venv to use it.
|
> The image installs mempalace into an isolated `uv tool` venv at `/opt/uv-tools/mempalace/`. `uv tool install` places `mempalace-mcp` on `PATH` as a shim whose shebang points at the venv's Python, so MCP clients can invoke it as a normal binary without worrying about the venv. Do **not** use `["python3", "-m", "mempalace.mcp_server"]` — the system Python cannot import from the uv-managed venv and you'll get `ModuleNotFoundError` / `MCP error -32000: connection closed`.
|
||||||
|
|
||||||
This gives opencode access to 29 MCP tools for searching memory, querying the knowledge graph, managing wings/rooms/drawers, and agent diaries.
|
This gives opencode access to 29 MCP tools for searching memory, querying the knowledge graph, managing wings/rooms/drawers, and agent diaries.
|
||||||
|
|
||||||
@@ -502,6 +503,24 @@ mempalace wake-up
|
|||||||
|
|
||||||
Each workspace gets its own isolated "wing" — memories never leak between projects.
|
Each workspace gets its own isolated "wing" — memories never leak between projects.
|
||||||
|
|
||||||
|
### Scheduled mining (mempalace-toolkit)
|
||||||
|
|
||||||
|
The image bakes in [mempalace-toolkit](https://gitea.jordbo.se/joakimp/mempalace-toolkit), a small set of bash wrappers that pair with mempalace for two common routines:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Mine opencode session history (reads ~/.local/share/opencode/opencode.db, stages JSONL, mines into wing_conversations)
|
||||||
|
mempalace-session
|
||||||
|
|
||||||
|
# Mine a project's docs into a dedicated wing
|
||||||
|
mempalace-docs /workspace/my-project
|
||||||
|
```
|
||||||
|
|
||||||
|
Both wrappers are idempotent and dedup-aware — re-running them on unchanged input is a cheap no-op.
|
||||||
|
|
||||||
|
For weekly automated runs, the toolkit ships ready-to-use scheduler templates (systemd user timer, launchd user agent, cron) in its [`contrib/`](https://gitea.jordbo.se/joakimp/mempalace-toolkit/src/branch/main/contrib) directory. The `*-devbox` variants are designed for this container: host-side schedulers that `docker exec` into the running opencode-devbox.
|
||||||
|
|
||||||
|
Disable the toolkit (keeps mempalace itself) with `--build-arg INSTALL_MEMPALACE_TOOLKIT=false`. Pin to a specific ref with `--build-arg MEMPALACE_TOOLKIT_REF=v0.3.0` once tagged releases exist.
|
||||||
|
|
||||||
### Storage
|
### Storage
|
||||||
|
|
||||||
Two separate named volumes keep different data classes apart:
|
Two separate named volumes keep different data classes apart:
|
||||||
|
|||||||
+3
-3
@@ -53,7 +53,7 @@ OMOS_CONFIG="$CONFIG_DIR/oh-my-opencode-slim.json"
|
|||||||
OMOS_CONFIG="$CONFIG_DIR/oh-my-opencode-slim.json"
|
OMOS_CONFIG="$CONFIG_DIR/oh-my-opencode-slim.json"
|
||||||
|
|
||||||
if [ "${ENABLE_OMOS:-false}" = "true" ]; then
|
if [ "${ENABLE_OMOS:-false}" = "true" ]; then
|
||||||
if ! command -v bunx &>/dev/null; then
|
if ! command -v bun &>/dev/null; then
|
||||||
echo "WARNING: ENABLE_OMOS=true but bun is not installed."
|
echo "WARNING: ENABLE_OMOS=true but bun is not installed."
|
||||||
echo "Rebuild with: docker compose build --build-arg INSTALL_OMOS=true"
|
echo "Rebuild with: docker compose build --build-arg INSTALL_OMOS=true"
|
||||||
elif [ ! -f "$OMOS_CONFIG" ]; then
|
elif [ ! -f "$OMOS_CONFIG" ]; then
|
||||||
@@ -70,7 +70,7 @@ if [ "${ENABLE_OMOS:-false}" = "true" ]; then
|
|||||||
OMOS_SKILLS_FLAG="no"
|
OMOS_SKILLS_FLAG="no"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
bunx oh-my-opencode-slim@latest install \
|
bun x oh-my-opencode-slim@latest install \
|
||||||
--no-tui \
|
--no-tui \
|
||||||
--tmux="${OMOS_TMUX_FLAG}" \
|
--tmux="${OMOS_TMUX_FLAG}" \
|
||||||
--skills="${OMOS_SKILLS_FLAG}"
|
--skills="${OMOS_SKILLS_FLAG}"
|
||||||
@@ -87,7 +87,7 @@ if [ "${ENABLE_OMOS:-false}" = "true" ]; then
|
|||||||
OMOS_SKILLS_FLAG="yes"
|
OMOS_SKILLS_FLAG="yes"
|
||||||
[ "${OMOS_SKILLS:-true}" = "false" ] && OMOS_SKILLS_FLAG="no"
|
[ "${OMOS_SKILLS:-true}" = "false" ] && OMOS_SKILLS_FLAG="no"
|
||||||
|
|
||||||
bunx oh-my-opencode-slim@latest install \
|
bun x oh-my-opencode-slim@latest install \
|
||||||
--no-tui \
|
--no-tui \
|
||||||
--tmux="${OMOS_TMUX_FLAG}" \
|
--tmux="${OMOS_TMUX_FLAG}" \
|
||||||
--skills="${OMOS_SKILLS_FLAG}" \
|
--skills="${OMOS_SKILLS_FLAG}" \
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# Launcher for the MemPalace MCP server.
|
|
||||||
#
|
|
||||||
# MemPalace is installed via `uv tool install` into an isolated venv
|
|
||||||
# under /opt/uv-tools/. System python3 cannot import mempalace directly,
|
|
||||||
# so this wrapper exec's the venv's python with the mcp_server module.
|
|
||||||
#
|
|
||||||
# Used by opencode.json:
|
|
||||||
# "command": ["mempalace-mcp-server"]
|
|
||||||
exec /opt/uv-tools/mempalace/bin/python -m mempalace.mcp_server "$@"
|
|
||||||
@@ -75,14 +75,15 @@ def register_mcp_servers(config: dict) -> list[str]:
|
|||||||
servers: dict[str, dict] = {}
|
servers: dict[str, dict] = {}
|
||||||
|
|
||||||
# MemPalace — local-first AI memory (if installed).
|
# MemPalace — local-first AI memory (if installed).
|
||||||
# Uses the mempalace-mcp-server wrapper rather than invoking
|
# `mempalace-mcp` is the entry-point binary shipped by the mempalace
|
||||||
# `python3 -m mempalace.mcp_server` directly, because mempalace
|
# Python package. `uv tool install mempalace` places it on PATH as a
|
||||||
# lives in an isolated uv tool venv that system python3 cannot
|
# shim whose shebang points at the isolated venv's Python, so system
|
||||||
# import from. The wrapper exec's the right interpreter.
|
# `python3 -m mempalace.mcp_server` (which would fail — system
|
||||||
if shutil.which("mempalace") and shutil.which("mempalace-mcp-server"):
|
# python3 can't import from the uv venv) is unnecessary here.
|
||||||
|
if shutil.which("mempalace-mcp"):
|
||||||
servers["mempalace"] = {
|
servers["mempalace"] = {
|
||||||
"type": "local",
|
"type": "local",
|
||||||
"command": ["mempalace-mcp-server"],
|
"command": ["mempalace-mcp"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Gitea — self-hosted Git forge API (if installed).
|
# Gitea — self-hosted Git forge API (if installed).
|
||||||
|
|||||||
+15
-1
@@ -71,6 +71,9 @@ docker run --rm --entrypoint="" "$IMAGE" sh -c '
|
|||||||
if command -v mempalace >/dev/null 2>&1; then
|
if command -v mempalace >/dev/null 2>&1; then
|
||||||
printf " %-15s %s\n" "mempalace" "$(mempalace --version 2>&1 | head -1 || echo installed)"
|
printf " %-15s %s\n" "mempalace" "$(mempalace --version 2>&1 | head -1 || echo installed)"
|
||||||
fi
|
fi
|
||||||
|
if command -v mempalace-session >/dev/null 2>&1 && [ -d /opt/mempalace-toolkit ]; then
|
||||||
|
printf " %-15s %s\n" "toolkit" "$(git -C /opt/mempalace-toolkit rev-parse --short HEAD 2>/dev/null || echo installed)"
|
||||||
|
fi
|
||||||
'
|
'
|
||||||
echo
|
echo
|
||||||
echo "-- Core binaries --"
|
echo "-- Core binaries --"
|
||||||
@@ -99,14 +102,25 @@ echo "-- Optional / variant-gated --"
|
|||||||
# mempalace: present unless built with INSTALL_MEMPALACE=false
|
# mempalace: present unless built with INSTALL_MEMPALACE=false
|
||||||
if docker run --rm --entrypoint="" "$IMAGE" sh -c "command -v mempalace" >/dev/null 2>&1; then
|
if docker run --rm --entrypoint="" "$IMAGE" sh -c "command -v mempalace" >/dev/null 2>&1; then
|
||||||
run "mempalace" "mempalace --help | head -1"
|
run "mempalace" "mempalace --help | head -1"
|
||||||
run "mempalace-mcp-server" "test -x /usr/local/bin/mempalace-mcp-server && echo wrapper-present"
|
run "mempalace-mcp" "test -x /usr/local/bin/mempalace-mcp && readlink /usr/local/bin/mempalace-mcp"
|
||||||
else
|
else
|
||||||
echo " - mempalace not installed (INSTALL_MEMPALACE=false)"
|
echo " - mempalace not installed (INSTALL_MEMPALACE=false)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# mempalace-toolkit wrappers: present unless built with INSTALL_MEMPALACE_TOOLKIT=false
|
||||||
|
# Gated on mempalace presence — wrappers are useless without the CLI.
|
||||||
|
if docker run --rm --entrypoint="" "$IMAGE" sh -c "command -v mempalace && command -v mempalace-session" >/dev/null 2>&1; then
|
||||||
|
run "mempalace-session (toolkit)" "mempalace-session --help | head -1"
|
||||||
|
run "mempalace-docs (toolkit)" "mempalace-docs --help | head -1"
|
||||||
|
run "toolkit symlink target" "test -L /usr/local/bin/mempalace-session && readlink /usr/local/bin/mempalace-session"
|
||||||
|
elif docker run --rm --entrypoint="" "$IMAGE" sh -c "command -v mempalace" >/dev/null 2>&1; then
|
||||||
|
echo " - mempalace-toolkit not installed (INSTALL_MEMPALACE_TOOLKIT=false)"
|
||||||
|
fi
|
||||||
|
|
||||||
# bun: only in the omos variant
|
# bun: only in the omos variant
|
||||||
if [ "$VARIANT" = "omos" ]; then
|
if [ "$VARIANT" = "omos" ]; then
|
||||||
run "bun (omos)" "bun --version"
|
run "bun (omos)" "bun --version"
|
||||||
|
run "bunx symlink (omos)" "test -L /usr/local/bin/bunx && readlink /usr/local/bin/bunx"
|
||||||
# oh-my-opencode-slim is npm-installed globally (not a bun install);
|
# oh-my-opencode-slim is npm-installed globally (not a bun install);
|
||||||
# verify it shows up in the global module list.
|
# verify it shows up in the global module list.
|
||||||
run "oh-my-opencode-slim" "npm ls -g --depth=0 2>/dev/null | grep oh-my-opencode-slim"
|
run "oh-my-opencode-slim" "npm ls -g --depth=0 2>/dev/null | grep oh-my-opencode-slim"
|
||||||
|
|||||||
Reference in New Issue
Block a user