Compare commits

...

7 Commits

Author SHA1 Message Date
Joakim Persson f0918ba915 Bump opencode to 1.14.31 and split multi-arch publish across runners
Validate / docs-check (push) Successful in 26s
Publish Docker Image / smoke-base (push) Failing after 11m1s
Publish Docker Image / build-base (linux/amd64) (push) Has been skipped
Publish Docker Image / build-base (linux/arm64) (push) Has been skipped
Publish Docker Image / merge-base (push) Has been skipped
Validate / validate-base (push) Failing after 13m48s
Validate / validate-omos (push) Failing after 15m23s
Publish Docker Image / smoke-omos (push) Failing after 16m20s
Publish Docker Image / build-omos (linux/amd64) (push) Has been skipped
Publish Docker Image / build-omos (linux/arm64) (push) Has been skipped
Publish Docker Image / merge-omos (push) Has been skipped
Publish Docker Image / update-description (push) Has been skipped
The v1.14.30b publish failed on both variants with 'No space left on
device' — arm64 QEMU-emulated layers were stored alongside amd64 on the
same ~40 GB runner, and the mempalace-toolkit bake-in from v1.14.30b
tipped peak disk over the edge during the nodejs dpkg unpack and the
git-lfs layer export.

Refactor docker-publish.yml to the canonical push-by-digest +
manifest-merge pattern: smoke test (amd64) runs on its own runner, each
(variant x 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 roughly one-quarter of the old single-job peak. The four
Docker Hub tags per release are unchanged. As a bonus, amd64 and arm64
now build in parallel.

No image-level changes beyond the opencode bump.
2026-05-01 08:43:08 +00:00
Joakim Persson 1683650240 Bake mempalace-toolkit wrappers into the image
Validate / docs-check (push) Successful in 14s
Validate / validate-base (push) Successful in 14m44s
Validate / validate-omos (push) Successful in 17m51s
Publish Docker Image / build-omos (push) Failing after 25m7s
Publish Docker Image / build-base (push) Failing after 55m51s
Publish Docker Image / update-description (push) Has been skipped
The scheduler templates in mempalace-toolkit's 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 container recreate,
because the only way to get them was a post-hoc install.sh 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: host-side systemd
unit ran cleanly at 16:15 today, then the container was rebuilt
and recreated, and the wrappers were gone. The rebuild produced
an image that the scheduler template's own documented precondition
did not hold for.

Fix: 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/, asserts both respond
to --help before the layer succeeds. Gated by
INSTALL_MEMPALACE_TOOLKIT=true (defaults on, depends on
INSTALL_MEMPALACE=true). Floated ref via MEMPALACE_TOOLKIT_REF=main
for auto-picking-up toolkit updates; override for reproducible
builds once the toolkit starts tagging releases.

Smoke test gains three assertions (mempalace-session --help,
mempalace-docs --help, symlink target check). Resolved-versions
preamble logs the toolkit git short-SHA alongside the other
floated components, so CI logs always record what got baked in.

README gains a Scheduled mining (mempalace-toolkit) subsection
and a build-args row. DOCKER_HUB.md regenerated; sync-check passes.
2026-04-30 20:56:58 +00:00
joakimp 9d7c3e5ad8 Bump opencode to 1.14.30
Validate / docs-check (push) Successful in 1m51s
Validate / validate-base (push) Successful in 13m37s
Validate / validate-omos (push) Successful in 23m0s
Publish Docker Image / build-base (push) Successful in 54m42s
Publish Docker Image / build-omos (push) Successful in 1h25m20s
Publish Docker Image / update-description (push) Successful in 20s
2026-04-30 15:11:49 +02:00
joakimp 23bae2ab7d Use mempalace-mcp entry point directly, drop redundant wrapper
Validate / docs-check (push) Successful in 20s
Validate / validate-base (push) Successful in 11m32s
Validate / validate-omos (push) Successful in 15m18s
Publish Docker Image / build-base (push) Successful in 53m5s
Publish Docker Image / build-omos (push) Successful in 1h11m3s
Publish Docker Image / update-description (push) Successful in 15s
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. Our hand-rolled wrapper at
/usr/local/bin/mempalace-mcp-server was duplicating what uv installs
for free — one less file to maintain.

Fixes the MCP error users saw after the v1.14.28b → v1.14.29 upgrade
path: custom opencode.json files typically had the pre-v1.14.29
command ['python3', '-m', 'mempalace.mcp_server'] which worked with
the old pip install but fails silently after the uv-tool migration
because system python3 cannot import from the venv. Opencode surfaced
this as 'MCP error -32000: connection closed'.

- generate-config.py now emits ['mempalace-mcp'] and keys its detect
  on shutil.which('mempalace-mcp').
- Dockerfile drops 'COPY rootfs/usr/local/bin/' and the chmod of the
  wrapper. Build shrinks from 30 to 29 stages.
- rootfs/usr/local/bin/ removed entirely.
- Smoke test asserts /usr/local/bin/mempalace-mcp is executable and
  prints its symlink target.
- README's MemPalace section shows ['mempalace-mcp'] and explicitly
  warns against the old pattern with the observed failure mode.
- CHANGELOG adds a v1.14.29c entry.
2026-04-29 15:27:30 +02:00
joakimp e0b6c2082f Add apt-get upgrade to core packages layer
Validate / docs-check (push) Successful in 12s
Validate / validate-base (push) Successful in 12m18s
Validate / validate-omos (push) Successful in 17m2s
Publish Docker Image / build-base (push) Successful in 52m23s
Publish Docker Image / build-omos (push) Successful in 1h8m34s
Publish Docker Image / update-description (push) Successful in 17s
Pair 'apt-get upgrade -y --no-install-recommends' with the existing
update + install in the first RUN step. Picks up security/CVE fixes
that land in the Debian repos between base-image rebuilds. Same layer
as the install to avoid bloating history; combined with apt-get clean
and rm -rf /var/lib/apt/lists/* at the end so no index cache is kept.

Today this is a no-op (debian:trixie-slim is current: 0 upgraded).
Future-proofs against the lag between a CVE fix being published and
the next base-image rebuild.
2026-04-29 10:25:36 +02:00
joakimp 2c889b472e Add --retry to all Dockerfile curl invocations
Validate / docs-check (push) Successful in 18s
Validate / validate-omos (push) Has been cancelled
Validate / validate-base (push) Has been cancelled
Publish Docker Image / build-base (push) Has been cancelled
Publish Docker Image / build-omos (push) Has been cancelled
Publish Docker Image / update-description (push) Has been cancelled
The first v1.14.29b build attempt failed with an HTTP 502 from
GitHub's release CDN mid-download of zoxide. Single-shot curl had no
retry, so one transient 502 failed the entire OMOS build.

- curl --retry 5 --retry-delay 5 --retry-all-errors on every tool
  download (both -fsSL GETs and -sI HEAD redirect lookups).
- [ -n "$V" ] assertion after each version-resolution step, so a
  failed HEAD lookup fails fast with a clear message instead of
  producing an empty tag that then 404s on the download URL.
- Same hardening applied to the optional Go install block and the
  nodesource setup_22.x pipe.
2026-04-29 10:14:42 +02:00
joakimp 349bb633ff Fix OMOS bunx detection
Validate / docs-check (push) Successful in 1m13s
Validate / validate-omos (push) Successful in 15m30s
Validate / validate-base (push) Successful in 17m11s
Publish Docker Image / build-omos (push) Failing after 14m43s
Publish Docker Image / update-description (push) Has been cancelled
Publish Docker Image / build-base (push) Has been cancelled
entrypoint-user.sh gated OMOS auto-install on 'command -v bunx', but
neither upstream bun installer nor our Dockerfile creates a bunx
symlink — only the bun binary exists on PATH. The check always failed
on a fresh OMOS image, printing '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.

Fixes:
- Gate on 'command -v bun' instead of bunx.
- Call 'bun x oh-my-opencode-slim@latest install ...' (bun x is the
  real subcommand that actually works with only the bun binary).
- Add 'ln -sf bun /usr/local/bin/bunx' in the Dockerfile OMOS block
  so interactive users can still type bunx by habit.
- Smoke test asserts the bunx symlink exists on the OMOS variant.

Also verify 'test -L /usr/local/bin/bunx' as a build-time sanity check.
Can't use 'bunx --help' for that — bun exits 1 on help output even
though it prints the usage correctly.
2026-04-29 09:01:23 +02:00
10 changed files with 413 additions and 113 deletions
+226 -58
View File
@@ -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:
+2 -3
View File
@@ -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.
+50
View File
@@ -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
View File
@@ -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
View File
@@ -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
+21 -2
View File
@@ -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
View File
@@ -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}" \
-10
View File
@@ -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
View File
@@ -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"