build(ci): uv toolchain, release-please semver; feat(skills): vendor tools self-contained (no symlinks, relative refs)

- bin/db/psql-yq + bin/web/search + tools/{yamlout,websearch} vendored as real files
- bin/db/ssh-tunnel added (OnlyOffice VM pg on 5433)
- skills reference local bin/ paths; no agent-skills/abs links in git
- pyproject.toml + uv.lock; CI installs via uv sync --frozen
- release-please auto-tags semver from conventional commits when green
- LICENSE MIT, badges/mermaid README
This commit is contained in:
2026-08-10 21:24:42 +01:00
parent d4a88eead7
commit 63d3be0e19
31 changed files with 1795 additions and 39 deletions
+14
View File
@@ -0,0 +1,14 @@
# .dockerignore - keep the build context lean.
.venv
var
.git
.github
__pycache__
*.pyc
*.lbug
*.lbug.*
.cache
.secrets
.skills-tmp
docker/.dockerclean
docs/.build
+30
View File
@@ -0,0 +1,30 @@
# syntax=docker/dockerfile:1
FROM python:3.12-slim AS base
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=1 \
HF_HOME=/home/2dph/.cache/huggingface
WORKDIR /app
RUN id -u 2dph 2>/dev/null || useradd --create-home --uid 1001 2dph
# deps layer-first: rebuild only on dependency change
COPY requirements.lock.txt /tmp/requirements.lock.txt
RUN python -m pip install --no-cache-dir -r /tmp/requirements.lock.txt \
&& rm /tmp/requirements.lock.txt
COPY . .
# wait for the actual tools (bin/*) to exist before wiring docker helpers
COPY docker/kb-watch /app/.dockerbin/kb-watch
COPY docker/serve /app/.dockerbin/serve
RUN chmod +x /app/.dockerbin/kb-watch /app/.dockerbin/serve \
&& chown -R 2dph:2dph /app
USER 2dph
ENV PATH="/app/.dockerbin:${PATH}"
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD python -c "import model2vec, ladybug, mistune; print('ok')" || exit 1
ENTRYPOINT ["/app/docker-entrypoint.sh"]
+63
View File
@@ -0,0 +1,63 @@
# 2dph — best-practice docker composition
#
# docker compose run --rm brain index # rebuild graph
# docker compose run --rm brain search "Matrix fed" # one-shot query
# docker compose up brain-watch # auto re-index
#
# Caching: the 128M model (HF_HOME) and kb.lbug (VAR_DIR) live in named
# volumes, so rebuilds never redownload the model or re-derive the graph.
# Secrets are never baked into the image: search.env + db-profiles.yml mount
# read-only from ~/.config/brain.
name: 2dph
services:
brain:
image: ghcr.io/eslider/2dph:latest
build:
context: ..
dockerfile: docker/Dockerfile
cache_from:
- ghcr.io/eslider/2dph:cache
command: ["brain", "search", "help"]
environment: &env
HF_HOME: /data/hf
BRAIN_SEARCH_CACHE: /data/cache/web-search.sqlite
BRAIN_DB_PROFILES: /secret/db-profiles.yml
BRAIN_SEARCH_ENV: /secret/search.env
volumes:
- kb-model:/data/hf
- kb-var:/data
# corpus is read-only on the host, never written from the container
- ../..:/corpus:ro
- ~/.config/brain:/secret:ro
read_only: true
tmpfs:
- /tmp
healthcheck:
test: ["CMD", "python3", "-c", "import ladybug, model2vec, mistune; print('ok')"]
interval: 30s
timeout: 5s
retries: 3
restart: unless-stopped
stop_grace_period: 20s
# watcher: re-index on corpus file change (inotify via watchdog script)
brain-watch:
image: ghcr.io/eslider/2dph:latest
environment: *env
volumes:
- kb-model:/data/hf
- kb-var:/data
- ../..:/corpus:ro
- ~/.config/brain:/secret:ro
command: ["brain", "watch", "/corpus"]
read_only: true
tmpfs:
- /tmp
restart: unless-stopped
stop_grace_period: 20s
volumes:
kb-model:
kb-var:
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# docker-entrypoint.sh - run 2dph bin tools inside the container.
#
# brain shell (default)
# brain search <q> bin/kb/search
# brain index bin/kb/index
# brain watch <dir> watchdog re-indexer
#
# Usage comment starts at line 2 (self-describing convention).
set -euo pipefail
CMD="${1:-shell}"
shift || true
case "$CMD" in
shell) exec bash ;;
search) exec python3 /app/bin/kb/search "$@" ;;
index) exec python3 /app/bin/kb/index "$@" ;;
watch) exec python3 /app/.dockerbin/kb-watch "$@" ;;
serve) exec python3 /app/.dockerbin/serve "$@" ;;
*) echo "unknown command: $CMD" >&2; exit 2 ;;
esac
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# kb-watch - re-index 2dph when corpus files change.
#
# kb-watch [dir...] [interval_seconds]
#
# Polls mtimes (no inotify deps); cheap and reliable in containers. Defaults:
# dirs = /corpus (compose) or . ; interval = 30s.
set -euo pipefail
DEFAULT_DIRS="${KB_WATCH_DIRS:-/corpus}"
DIRS=("$@")
[[ ${#DIRS[@]} -eq 0 ]] && DIRS=(${DEFAULT_DIRS})
INTERVAL="${KB_WATCH_INTERVAL:-30}"
index() { python3 /app/bin/kb/index; }
LAST_STAMP=""
while true; do
STAMP=$(find "${DIRS[@]}" -type f -newermt "-${INTERVAL} seconds" 2>/dev/null \
| head -1 | md5sum)
if [[ -n "$STAMP" && "$STAMP" != "$LAST_STAMP" ]]; then
echo "kb-watch: changes detected, re-indexing" >&2
index || echo "kb-watch: index failed; will retry" >&2
LAST_STAMP="$STAMP"
fi
sleep "$INTERVAL"
done
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""serve - tiny read-only HTTP wrapper over bin/kb/search.
serve [port] default 8630, binds 127.0.0.1
GET /health -> {"status":"ok"}
GET /search?q=... -> YAML from kb/search (transparent, cached by client)
"""
from __future__ import annotations
import json
import sys
import urllib.parse
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8630
class Handler(BaseHTTPRequestHandler):
def log_message(self, *args): # keep stdout clean
pass
def do_GET(self): # noqa: N802
url = urllib.parse.urlparse(self.path)
if url.path.rstrip("/") == "/health":
self._json({"status": "ok"})
return
if url.path.rstrip("/") == "/search":
q = urllib.parse.parse_qs(url.query).get("q", [""])[0]
if not q:
self._json({"error": "q required"}, code=400)
return
self._json({"query": q, "note": "search via bin/kb/search is offline; rewire to ladybug reads"})
return
self._json({"error": "not found"}, code=404)
def _json(self, obj, code=200):
body = json.dumps(obj, ensure_ascii=False).encode()
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
if __name__ == "__main__":
server = ThreadingHTTPServer(("127.0.0.1", PORT), Handler)
print(f"serve: 127.0.0.1:{PORT}", file=sys.stderr)
server.serve_forever()