# scripts Operational scripts for Luc's self-hosted environment. Each script collection is an independent, self-documenting folder at the repo root. There is no shared runtime, no build step, and no top-level package manifest — this is a repo of standalone tools, not an application. --- ## The one hard rule: collections are self-contained Every script collection lives in **its own top-level folder** and carries **all of its own documentation, configuration, and dependencies**. A collection must be understandable, runnable, and deletable on its own. ``` scripts/ ├── CLAUDE.md # this file — repo-wide context ├── README.md # human-facing index of collections ├── _template/ # scaffold for a new collection (leading _ = not a collection) └── / ├── README.md # REQUIRED — what it does, prerequisites, usage ├── CLAUDE.md # OPTIONAL — agent context, only if non-obvious ├── .env.example # REQUIRED if the collection reads secrets ├── bin/ # executable entrypoints (chmod +x, shebang) ├── lib/ # sourced helpers, not directly executable └── docs/ # anything longer than the README warrants ``` **Do not** create shared/, common/, or utils/ at the repo root. If two collections need the same helper, copy it. Duplication across collections is explicitly preferred over coupling — a collection that depends on a sibling is no longer independently deletable, and these scripts get deployed to different places at different times. Folders prefixed with `_` are repo infrastructure, not collections. ### Adding a collection Copy `_template/` to the new name, fill in its README, and add one line to the root `README.md` index. That is the whole process. --- ## The environment **odin** — the Unraid server; the single host everything self-hosted runs on. Reached from this box over **Tailscale** (`100.101.253.105`, LAN `192.168.90.103`). > **See [docs/odin-access.md](docs/odin-access.md)** for the access path, the WebGUI/API > endpoint, TLS caveats, and the Unraid GraphQL API — all verified from this box. Verified characteristics: | Aspect | Detail | | --- | --- | | Host OS | Unraid | | Container runtime | Docker + Docker Compose (stacks defined per-app) | | Database | MariaDB 11.x — **PostgreSQL is not available**, treat as a hard constraint | | Cache / queue | Redis (available, used optionally) | | Auth | Authelia — OIDC/OAuth2 provider for internal apps | | Ingress | Pangolin/Newt tunnel — **no open inbound ports** | | DNS | Split-DNS on `bergerhouse.net`; private IPs resolve internally | | Git forge | Self-hosted Gitea/Forgejo at `git.bergerhouse.net` (user `luckberg`, SSH port 222) | | CI | Gitea Actions with a self-hosted runner | | Mail / calendar | Fastmail (paid) — JMAP/CalDAV, the source of truth for calendars | | Remote access | Tailscale; odin `100.101.253.105`, subnet route for `192.168.90.0/24` | | Unraid API | GraphQL at `{myunraid.net host}:8443/graphql`, `x-api-key` auth (Unraid 7.2+) | | Automation | n8n on odin — drives the Docker-manager Telegram bot | Known service hostnames: `git.bergerhouse.net`, `familysync.bergerhouse.net`, `familysync-dev.bergerhouse.net`. **Not on odin:** GitHub (`gh` is not installed — use `tea` for the Gitea forge), PostgreSQL, any cloud provider. ### This box vs. odin The machine this repo is edited on is `dev` (Tailscale `100.94.16.46`) — a **separate workstation**, and in fact a VPS with a public IP, not a machine on the home LAN. Scripts written here usually *target* odin. Never assume a script runs on the same host it was authored on: take the target as a parameter or read it from config. Tailscale is the **only** path from `dev` to odin — if it is down, nothing below resolves. `?` — still unconfirmed: the SSH login to odin (no `~/.ssh/config` entry on this box) and where deployed scripts are expected to live on the Unraid host. ### Sibling repos on `git.bergerhouse.net` `luckberg/` holds `scripts` (this repo), `familysync`, `unraid-docker-manager`, `azure-epac`, `_cargo-index`. Two are load-bearing context: - **`unraid-docker-manager`** — n8n + Telegram bot managing odin's containers. The authoritative source on the Unraid GraphQL API and container polling; distilled into [docs/odin-access.md](docs/odin-access.md). - **`familysync`** — the origin of most of the stack facts in the table above. --- ## Prior context: MemPalace Much of what is known about this environment lives in **MemPalace**, a local semantic memory store — not in this repo. Query it before asking Luc to re-explain something, and before assuming an infrastructure detail is undocumented. | Aspect | Detail | | --- | --- | | Version | `mempalace` v3.4.1, installed as a `uv` tool | | Palace path | `/home/luc/.mempalace/palace` | | Backend | ChromaDB (vector search) + SQLite (metadata); fully local, no API key | | Access | MCP server (`mempalace-mcp`) → 19 `mempalace_*` tools; plus the `mempalace` CLI | | Size | ~32k drawers | **Read the docs, don't guess the CLI.** MemPalace ships its own instructions: ```bash mempalace instructions ``` Structure is **Wings → Rooms → Closets → Drawers** — wings are projects/people, rooms are topics, closets are summaries, drawers are verbatim memories. *Halls* connect rooms within a wing; *tunnels* connect rooms across wings. Existing wings: `familysync` (~21.6k), `sessions` (~10.6k — mined agent transcripts), `wing_familysync`. ### This repo's wing Initialized with `mempalace init . --yes` and mined with `mempalace mine .`. - **Wing: `odin-scripts`** — set explicitly in `mempalace.yaml`, *not* auto-detected. Auto-detection uses the directory basename, and a wing called `scripts` would collide with any other corpus in a folder of that name. - **Rooms:** `environment`, `documentation`, `collections`, `general`. - Room routing matches keywords against the file **path**, so never use `scripts` as a room keyword here — it matches this repo's own path and swallows every file. (Learned the hard way; the comment in `mempalace.yaml` says so too.) - `mempalace.yaml` and `entities.json` are gitignored — that is MemPalace's own convention (issue #185), applied by `init`. Re-mine after adding a collection: `mempalace mine . --agent claude`. Mining is idempotent — already-filed files are skipped. Practical notes: - `mempalace_search` is semantic — the `query` field takes **keywords only**, max 250 chars. Put background in `context`, not in the query, or the embedding gets diluted. - Filter with `wing`/`room` when you know where the answer lives; the `sessions` wing is raw transcript chunks and is noisy for factual lookups. - **BM25 scores of 0 with mid-range similarity means no lexical match** — a bare-keyword search for a proper noun that returns only semantic neighbours is a *negative* result, not a weak positive. This is how the `odin` gap above was established. - `mempalace_kg_query` hits the temporal knowledge graph for point-in-time facts; prefer it over `search` for "what is X currently" questions, and use `kg_supersede` rather than invalidate-then-add when a single-valued fact changes. - Auto-save hooks are active: a **Stop** hook saves every 15 human messages, and a **PreCompact** hook force-saves before context compaction. Complementary and **separate** from MemPalace: - **Per-project agent memory** — `~/.claude/projects//memory/*.md`, indexed by that directory's `MEMORY.md`. This repo's is currently empty; FamilySync's holds the CI-gate, dev-stack, and Gitea-CI notes that much of this file's environment section derives from. - **Project docs on disk** — `~/projects/familysync/CLAUDE.md` and `docs/ARCHITECTURE.md` are the authoritative written source for the odin stack. --- ## Conventions These apply to every collection unless its own README documents a deliberate exception. ### Shell - `#!/usr/bin/env bash` and `set -euo pipefail` at the top of every bash entrypoint. - Quote every expansion. Prefer `[[ ]]` over `[ ]`. - Scripts must be **idempotent** — safe to re-run. Assume cron or a retry will do so. - Accept `--dry-run` for anything that mutates state, deletes, or sends. Default to the safe path when a flag is ambiguous. - Log to stdout, errors to stderr. No log files unless the collection documents rotation. ### Secrets - **Never commit secrets.** Read them from the environment or a file path passed in. - Every collection that needs secrets ships a `.env.example` with dummy values and a README line naming where the real values live. - Note the FamilySync-side precedent: gitleaks runs as a blocking CI gate on that repo. Assume any secret committed here is treated the same way — as a leak to be rotated, not a mistake to be amended away. ### Portability - Target the interpreters actually present: bash, Python 3, Node 22. Check before adding a dependency on anything else. - Unraid's userland is BusyBox-leaning in places — prefer POSIX-portable invocations of `sed`/`awk`/`date` over GNU-only flags when the script runs on the server. --- ## Working agreements for Claude - **Scope changes to one collection.** A request about collection X should not touch collection Y. If it must, say so explicitly rather than doing it quietly. - **Update the collection's README in the same change as the code.** Docs living beside the script is the point of this repo's layout; a code change that leaves its README stale defeats it. - **Don't invent infrastructure.** If a script needs a host, path, port, or credential that isn't documented above, ask — do not guess a plausible value. Wrong infra assumptions in an ops script fail in production, not in review. - **Destructive operations need confirmation** before they're run against odin, even when the script itself is finished and correct. Writing the script is not authorization to execute it.