Establish this repo as a collection of independent script folders for the self-hosted environment (odin, an Unraid host). - CLAUDE.md: repo conventions, the odin stack, and MemPalace usage. The infrastructure facts are carried over from the FamilySync project, where they are documented and verified; the mapping of the name "odin" to that host is assumed and flagged for confirmation, along with the SSH/deploy gaps marked "?". - Core rule: each collection is a self-contained top-level folder owning its own docs, config, and dependencies. No shared/ or utils/ at the root — duplication is preferred over coupling so a collection stays independently deletable. - _template/: scaffold making that rule concrete. The bash entrypoint ships strict mode, --dry-run, and a required-env guard (all four paths tested). - mempalace.yaml: wing "odin-scripts", set explicitly because basename auto-detection would produce the colliding wing "scripts". Tracked rather than gitignored so a fresh clone keeps the config; entities.json stays ignored. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
9.1 KiB
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)
└── <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
⚠ Confirm the gaps marked
?below, then delete this warning. The infrastructure facts are carried over from the FamilySync project, where they are documented and verified. Only the mapping of the nameodinto that host is assumed — the name appears in no memory, transcript, or config on this box.
odin — the Unraid server; the single host everything self-hosted runs on. 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) |
| CI | Gitea Actions with a self-hosted runner |
| Mail / calendar | Fastmail (paid) — JMAP/CalDAV, the source of truth for calendars |
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 — a separate workstation, not the server.
Scripts written here usually target odin (over SSH, or by being deployed into a
container/user-script on it). Never assume a script runs on the same host it was authored
on: take the target as a parameter or read it from config.
? — SSH access method to odin (no ~/.ssh/config entry exists on this box yet), and
where deployed scripts are expected to live on the Unraid host.
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:
mempalace instructions <help|init|mine|search|status>
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 inmempalace.yaml, not auto-detected. Auto-detection uses the directory basename, and a wing calledscriptswould 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
scriptsas a room keyword here — it matches this repo's own path and swallows every file. (Learned the hard way; the comment inmempalace.yamlsays so too.) mempalace.yamlandentities.jsonare gitignored — that is MemPalace's own convention (issue #185), applied byinit.
Re-mine after adding a collection: mempalace mine . --agent claude. Mining is
idempotent — already-filed files are skipped.
Practical notes:
mempalace_searchis semantic — thequeryfield takes keywords only, max 250 chars. Put background incontext, not in the query, or the embedding gets diluted.- Filter with
wing/roomwhen you know where the answer lives; thesessionswing 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
odingap above was established. mempalace_kg_queryhits the temporal knowledge graph for point-in-time facts; prefer it oversearchfor "what is X currently" questions, and usekg_supersederather 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/<slug>/memory/*.md, indexed by that directory'sMEMORY.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.mdanddocs/ARCHITECTURE.mdare 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 bashandset -euo pipefailat 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-runfor 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.examplewith 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/dateover 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.