chore: Claude/agent guidance and workspace files
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
2baad92089
commit
a8f1045518
4 changed files with 1167 additions and 0 deletions
173
CLAUDE.md
Normal file
173
CLAUDE.md
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Omnixient is a declarative NixOS configuration that builds a Hyprland-based desktop environment. It uses Nix flakes, home-manager, and a custom module system with feature flags and presets.
|
||||
|
||||
**Repository**: https://github.com/TheArctesian/omnixy
|
||||
|
||||
## Relationship to `~/dev` and `~/.claude` (read first)
|
||||
|
||||
This dir configures **bohm** (the local dev box). It is NOT
|
||||
`~/dev/deploy/server-deploy/` — that's a separate NixOS flake for the
|
||||
deployed server fleet (host1, host2, host3, host4, host5, host6,
|
||||
…). When the user says "rebuild" / "deploy" without context, ask
|
||||
whether they mean local (this dir, `omni-rebuild`) or remote
|
||||
(server-deploy + `deploy.sh`).
|
||||
|
||||
Most files here are user-owned (no sudo needed). `hardware-configuration.nix`
|
||||
is generated — don't hand-edit.
|
||||
|
||||
### What this config provides to `~/dev` work
|
||||
|
||||
The Omnixient config is the source of everything `~/dev` *expects* to be
|
||||
on this machine:
|
||||
|
||||
- **Global git pre-commit hook** — `modules/dev-env/scripts/git-hooks/pre-commit`,
|
||||
wired into every repo via `core.hooksPath`. Secret-scanner with
|
||||
`pragma: allowlist secret` markers. See user-global
|
||||
`~/.claude/CLAUDE.md` for the false-positive workflow.
|
||||
- **Home-manager systemd user units** — declared in `home.nix`
|
||||
under `systemd.user.{services,timers}.*`. The refs weekly refresh
|
||||
(`refs-refresh.timer`) lives here.
|
||||
- **PATH, env, shell tooling** — `modules/dev-env/`, `home.nix`
|
||||
`programs.*`, and `home.packages` / `environment.systemPackages`.
|
||||
- **SSH host aliases** — `home.nix` `programs.ssh.matchBlocks`
|
||||
(or wherever ssh is declared).
|
||||
- **lazyvim-nix** — pinned in `flake.nix`, configured under
|
||||
`programs.lazyvim` in `home.nix`. User-global CLAUDE.md has the
|
||||
full plugin/config conventions.
|
||||
- **`~/dev/CLAUDE.md`** — workspace-level Claude instructions for
|
||||
the aiolabs / AIO stack. `~/dev/` itself isn't a git repo, so the
|
||||
file lives at `files/dev-CLAUDE.md` here and is surfaced as a
|
||||
mutable symlink via `home.file."dev/CLAUDE.md".source =
|
||||
config.lib.file.mkOutOfStoreSymlink ...` in `home.nix`. Edit at
|
||||
either path; commit from this repo. Use `files/` for any other
|
||||
documents that conceptually belong outside `/etc/nixos` but have
|
||||
no other repo to live in.
|
||||
|
||||
### Cross-references
|
||||
|
||||
- **`~/.claude/CLAUDE.md`** — user-global preferences, including a
|
||||
"Machine config — `/etc/nixos`" section that mirrors this overview
|
||||
but oriented for sessions started *outside* this dir. Loaded in
|
||||
every session regardless of cwd.
|
||||
- **`~/dev/CLAUDE.md`** — workspace orientation for the aiolabs / AIO
|
||||
stack under `~/dev/`. Loaded when working there.
|
||||
|
||||
## Key Commands
|
||||
|
||||
```bash
|
||||
# Build without switching (validate changes)
|
||||
nixos-rebuild build --flake .#omni
|
||||
|
||||
# Build and switch
|
||||
sudo nixos-rebuild switch --flake .#omni
|
||||
|
||||
# Test in VM
|
||||
nixos-rebuild build-vm --flake .#omni
|
||||
|
||||
# Check flake evaluation
|
||||
nix flake check
|
||||
|
||||
# Format / lint Nix code
|
||||
nixfmt *.nix modules/*.nix # or: make fmt (find -name '*.nix' | xargs nixfmt)
|
||||
statix check .
|
||||
deadnix .
|
||||
|
||||
# Build ISO
|
||||
nix build .#iso
|
||||
```
|
||||
|
||||
## Sandboxed claude sessions
|
||||
|
||||
Two scripts launch claude under an allowlist+deny policy, so it can
|
||||
work unattended on a target dir without being able to reach bohm's
|
||||
running system:
|
||||
|
||||
- **`scripts/sandbox-claude.sh [<dir>]`** — generalized form. Targets
|
||||
`<dir>` if given (any repo path on the machine), else the parent of
|
||||
the script dir. Remaining args pass through to claude. Use this when
|
||||
iterating on a separate repo (e.g. a new public template under
|
||||
development).
|
||||
- **`scripts/refactor-claude.sh`** — backwards-compatible wrapper for
|
||||
the `~/nixos-refactor/` worktree of this repo. Equivalent to
|
||||
`sandbox-claude.sh` with no args.
|
||||
|
||||
Both bootstrap `<target>/.claude/settings.json` fresh from
|
||||
`scripts/sandbox-settings.json` on every launch — policy evolves in
|
||||
the main `/etc/nixos` checkout and propagates on next launch.
|
||||
Hand-editing the runtime copy is pointless (overwritten on relaunch);
|
||||
`<target>/.gitignore` should list `.claude/` (the script warns if not).
|
||||
|
||||
### Policy (`scripts/sandbox-settings.json`)
|
||||
|
||||
- `defaultMode=acceptEdits` — file edits silent.
|
||||
- **allow**: narrow Bash set — `nix build|check|eval|run`, `nixfmt`,
|
||||
`statix`, `deadnix`, in-worktree git (status/diff/log/add/commit/
|
||||
checkout/restore/stash/rev-parse/worktree list), `find`/`xargs`, and
|
||||
basic inspection (`ls`, `pwd`, `which`, `type`, `env`, `echo`,
|
||||
`file`, `stat`, `wc`, `head`, `tail`, `cat`).
|
||||
- **ask**: `rm`, `rmdir`, `mv`, `cp`, `chmod`, `chown` — prompt every
|
||||
time.
|
||||
- **deny**: sudo, `nixos-rebuild switch|test|boot`, `omni-rebuild`,
|
||||
`nh`, `git push`, `git remote`, `git reset --hard`, curl, wget, ssh,
|
||||
scp, rsync, nc, systemctl, docker, podman, WebFetch, WebSearch.
|
||||
|
||||
`deny` is hard-reject and cannot be overridden mid-session. To loosen,
|
||||
edit `scripts/sandbox-settings.json` in main and relaunch.
|
||||
|
||||
### Verification signal for an autonomous run
|
||||
|
||||
- `nix flake check -L` — all checks
|
||||
- `nix build .#checks.x86_64-linux.refactor-smoke -L` — headless VM
|
||||
boot test scaffold at `tests/refactor-smoke.nix`
|
||||
- `nixos-rebuild build --flake .#omni` — build only, no switch
|
||||
|
||||
Merging a passing refactor is a manual step done from the main
|
||||
`/etc/nixos` checkout, not from the sandbox.
|
||||
|
||||
`omni-rebuild` itself now wraps `nh os switch --hostname omni
|
||||
/etc/nixos` (see `60bd3f75`); the surface for users is unchanged but
|
||||
`nh` is now also a system-wide command via `programs.nh`.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Entry Points
|
||||
- **settings.nix** — Single source of truth for user, gitName, gitEmail, hostName, timeZone. Imported by flake.nix and passed via `specialArgs` to all modules.
|
||||
- **flake.nix** — Inputs (nixpkgs, home-manager, hyprland, nix-colors, stylix, neovim-nightly, NUR), outputs. Passes `settings` to NixOS and home-manager modules.
|
||||
- **configuration.nix** — Thin entry point: imports all modules, sets per-host nix/networking/locale config, and `omni.*` options. Receives `settings` via specialArgs.
|
||||
- **home.nix** — Home-manager user config. Uses `osConfig.omni.features` to gate packages by feature flag. Receives `settings` via extraSpecialArgs.
|
||||
- **iso.nix** — Live ISO config (overrides user to "nixos").
|
||||
|
||||
### Module System (`modules/`)
|
||||
All modules use `config.omni.*` options defined in `modules/core.nix`:
|
||||
- **core.nix** — Defines options: `user`, `theme`, `preset`, `features.*`, `displayManager`, `colorScheme`, `wallpaper`. Presets auto-enable feature combinations via `mkDefault`.
|
||||
- **lib.nix** — Exposes shared helpers via `config.omni.lib`: `isEnabled`, `userPath`, `getColor`, `withFeature`, `filterPackages`, `makeScript`, `paths`, `colors`. This is the only place helpers are defined — all modules access them through `config.omni.lib`.
|
||||
- **packages.nix** — System packages gated by `cfg.features.*` and `cfg.packages.categories.*`.
|
||||
- **services.nix** — Canonical location for all system services (pipewire, openssh, greetd, fstrim, thermald, avahi, etc.). Also owns polkit and AppArmor config.
|
||||
- **boot.nix** — Boot loader, kernel, Plymouth, console config.
|
||||
- **hardware/** — Conditional modules for Intel, AMD, NVIDIA, audio, bluetooth, touchpad. `default.nix` is the entry point with common hardware support.
|
||||
|
||||
### Desktop (`modules/desktop/`)
|
||||
- **hyprland.nix** — Uses home-manager's `wayland.windowManager.hyprland.settings` for structured, mergeable config. Also configures Waybar.
|
||||
- **hyprland/autostart.nix** — `exec-once`/`exec` lists (themes and feature modules can append).
|
||||
- **hyprland/bindings.nix** — Keybindings as `bind`/`binde`/`bindm`/`bindl` lists.
|
||||
- **hyprland/idle.nix** — Uses HM `services.hypridle` and `programs.hyprlock` modules.
|
||||
|
||||
### Theme System (`modules/themes/`)
|
||||
Each theme is a standalone module that merges config into HM settings:
|
||||
- Hyprland colors via `wayland.windowManager.hyprland.settings.general."col.active_border"` etc.
|
||||
- Waybar CSS via `programs.waybar.style`
|
||||
- Terminal colors (alacritty, kitty), editor themes (neovim, vscode), starship palette, lazygit, mako notifications, fzf colors, Firefox userChrome, GTK
|
||||
- Only one theme is imported at a time (selected by `currentTheme` in configuration.nix)
|
||||
- Use tokyo-night.nix as the template for new themes
|
||||
|
||||
### Key Patterns
|
||||
- **Feature flags**: `config.omni.features.coding`, etc. — guarded with `lib.mkIf` or `lib.optionals`. Both system packages (packages.nix) and user packages (home.nix) respect these.
|
||||
- **Helpers access**: Always via `config.omni.lib` (never `import ./helpers.nix`).
|
||||
- **User reference**: Always via `config.omni.user` in NixOS modules, `settings.user` in home.nix/flake.nix.
|
||||
- **Theme switching**: Change `currentTheme` in configuration.nix, then rebuild.
|
||||
- **Structured Hyprland config**: Themes merge attrsets into `wayland.windowManager.hyprland.settings` — no raw config files in `/etc/`.
|
||||
130
INTEGRATION_NOTES.md
Normal file
130
INTEGRATION_NOTES.md
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
# nixos-dev-setup integration notes
|
||||
|
||||
Status: wired in, evaluates cleanly, ready to build once a pre-existing
|
||||
unrelated bug is fixed.
|
||||
|
||||
## What was integrated
|
||||
|
||||
From `~/Work/tries/2026-04-06-nixos-dev-setup` (tagged `v0.1.0`):
|
||||
|
||||
- `lib/mksystem.nix` — uniform host constructor
|
||||
- `lib/overlays.nix` — nixpkgs-unstable pinning for fast-moving pkgs
|
||||
- `modules/cache.nix` — substituters + trusted-public-keys
|
||||
- `modules/dev-env/` — full dev environment module (options, config,
|
||||
lib, bash helpers, aiolabs preset, git hooks, runbooks)
|
||||
- `Makefile` — `switch`/`test`/`build`/`cache`/`check`/`clean`/`fmt`
|
||||
- `hosts/omni/default.nix` — host-specific dev-env settings
|
||||
- `users/user/home-manager.nix` — shim forwarder for home.nix
|
||||
|
||||
## What changed in existing omni files
|
||||
|
||||
- `flake.nix`
|
||||
- Added `nixpkgs-unstable` and `deploy-flake` inputs.
|
||||
- Outputs rewritten to use `mkSystem` for the `omni` host.
|
||||
- `omni-iso` left as a direct `nixosSystem` call (variant, not host).
|
||||
- Overlays (lib/overlays.nix + nur + neovim-nightly) now applied to
|
||||
nixosConfigurations as well as devShells/packages/apps.
|
||||
|
||||
- Nothing else was edited in place. `configuration.nix` and `home.nix`
|
||||
are still at the top level, reached via shim files in `hosts/omni/`
|
||||
and `users/user/`.
|
||||
|
||||
## Integration commit sequence
|
||||
|
||||
```
|
||||
2f875a9 enable dev-env + aiolabs preset; fix options.nix interpolation escapes (phase E)
|
||||
81ecc6c wire flake.nix outputs through mkSystem (phase D)
|
||||
103fe0e extend mksystem with extraHmArgs for host-specific HM extras
|
||||
dc9d70b add mksystem shims for hosts/ and users/ (phase C)
|
||||
2fe2ba0 add nixpkgs-unstable + deploy-flake inputs (phase B)
|
||||
c9b7db5 import nixos-dev-setup module (phase A: additive copy)
|
||||
f18d2c0 fix: VM boot support and documentation update ← pre-dev-env-integration tag
|
||||
```
|
||||
|
||||
Rollback: `git reset --hard pre-dev-env-integration`
|
||||
|
||||
## Verification
|
||||
|
||||
Last successful eval sequence on current master:
|
||||
|
||||
```bash
|
||||
# Top-level host name — verifies module tree assembles
|
||||
nix eval .#nixosConfigurations.omni.config.system.name
|
||||
# → "omni"
|
||||
|
||||
# Project list from aiolabs preset — verifies dev-env schema loads
|
||||
nix eval --json \
|
||||
.#nixosConfigurations.omni.config.dev-env.projects \
|
||||
--apply 'builtins.attrNames'
|
||||
# → 18 projects incl. lightning-pub, lnbits, webapp, etc.
|
||||
|
||||
# Nested worktree paths — verifies submodule wiring
|
||||
nix eval --json \
|
||||
.#nixosConfigurations.omni.config.dev-env.projects.lightning-pub.worktrees
|
||||
# → 7 worktree entries with computed path/branch/remote
|
||||
|
||||
# Runtime config file rendering — verifies config.nix
|
||||
nix build --no-link --print-out-paths \
|
||||
'.#nixosConfigurations.omni.config.environment.etc."dev-env/config.sh".source'
|
||||
# → /nix/store/.../dev-env-config.sh with DEV_ROOT, FORGEJO_*, etc.
|
||||
|
||||
# Project manifest rendering — verifies dev-env-bootstrap input
|
||||
nix build --no-link --print-out-paths \
|
||||
'.#nixosConfigurations.omni.config.environment.etc."dev-env/projects.json".source'
|
||||
# → /nix/store/.../dev-env-projects.json — full project tree with
|
||||
# bare paths, remotes, and worktree paths resolved
|
||||
```
|
||||
|
||||
All the above work end-to-end. The dev-env pipeline (options → config
|
||||
rendering → runtime files) is validated.
|
||||
|
||||
## Known pre-existing issue: blocks final build
|
||||
|
||||
`modules/desktop/hyprland.nix:112` references `thunar` as an unqualified
|
||||
package name inside a `with pkgs;` scope. `thunar` is not a top-level
|
||||
attribute — it lives at `pkgs.xfce.thunar` — so evaluation of
|
||||
`config.system.build.toplevel` fails with:
|
||||
|
||||
error: undefined variable 'thunar'
|
||||
|
||||
This bug exists at the `pre-dev-env-integration` tag too. It is
|
||||
**not caused by this integration**. Verified by:
|
||||
|
||||
```bash
|
||||
git stash -u
|
||||
git checkout pre-dev-env-integration -- .
|
||||
nix eval .#nixosConfigurations.omni.config.system.build.toplevel.drvPath
|
||||
# → same 'undefined variable thunar' error
|
||||
git checkout master -- .
|
||||
git stash pop
|
||||
```
|
||||
|
||||
To move past it, fix that line (change `thunar` to `xfce.thunar` or
|
||||
remove it if unused). That is out of scope for the dev-env integration.
|
||||
|
||||
## After the thunar fix
|
||||
|
||||
Once the pre-existing issue is resolved, run:
|
||||
|
||||
```bash
|
||||
make test HOST=omni # nixos-rebuild test, no boot entry change
|
||||
make switch HOST=omni # actual switch
|
||||
dev-env-bootstrap --dry-run
|
||||
dev-env-bootstrap
|
||||
lb dev # navigate to ~/user/dev/lnbits/dev (won't exist until bootstrap runs)
|
||||
```
|
||||
|
||||
## Open follow-ups (not blocking)
|
||||
|
||||
- [ ] Fix `thunar` → `xfce.thunar` in `modules/desktop/hyprland.nix`
|
||||
- [ ] Rename `users/user/` → `users/<real-username>/` once `settings.user`
|
||||
is set to something other than `"user"`
|
||||
- [ ] Inline the shim content (move `configuration.nix` body into
|
||||
`hosts/omni/default.nix`, `home.nix` body into
|
||||
`users/<user>/home-manager.nix`) — cosmetic cleanup
|
||||
- [ ] Push `optimize-deploys/unified` to forgejo so `deploy-flake.url`
|
||||
can move from `git+file://` to `git+ssh://forgejo@.../deploy-unified`
|
||||
- [ ] Set up the `aiolabs-nix` cachix cache and uncomment the
|
||||
substituter + trusted-public-key lines in `modules/cache.nix`
|
||||
- [ ] Add a smoke-test flake to the staging repo so options.nix
|
||||
interpolation bugs (or similar) don't escape again
|
||||
136
files/anki-README.md
Normal file
136
files/anki-README.md
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
# ~/Anki — spaced-repetition source tree
|
||||
|
||||
This directory is a **source/staging area** for Anki cards, not Anki's
|
||||
collection. The actual collection (SQLite DB, media, scheduling state) lives
|
||||
at `~/.local/share/Anki2/<profile>/collection.anki2` and is managed by Anki
|
||||
itself. Files here are plain-text card sources you author/edit/diff, then
|
||||
import into Anki when you want to sync them up.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
~/Anki/
|
||||
├── README.md this file (symlinked from /etc/nixos/files/anki-README.md)
|
||||
├── dev/
|
||||
│ └── cards.csv developer jargon + concepts (GitHub PM lingo, TLV, …)
|
||||
├── french/
|
||||
│ └── cards.csv French vocab + phrases, tagged for export by topic
|
||||
├── protocols/
|
||||
│ └── cards.csv decentralized protocols (BTC, Lightning, Nostr — BIPs, NIPs, …)
|
||||
├── exports/ .apkg files generated to share with others
|
||||
└── sources/ raw clippings/articles you're mining cards from (scratch)
|
||||
```
|
||||
|
||||
Add more top-level deck dirs as new topics show up (`mkdir ~/Anki/<topic> &&
|
||||
cp ~/Anki/dev/cards.csv ~/Anki/<topic>/cards.csv` to clone the header).
|
||||
|
||||
## CSV format
|
||||
|
||||
Each `cards.csv` starts with Anki import directives so File → Import in Anki
|
||||
needs zero clicks to configure:
|
||||
|
||||
```
|
||||
#separator:Comma
|
||||
#html:false
|
||||
#deck:Dev
|
||||
#notetype:Basic
|
||||
#tags column:3
|
||||
```
|
||||
|
||||
Then one row per card: `Front,Back,Tags` — tags are space-separated within
|
||||
the column. Quote fields that contain commas.
|
||||
|
||||
Example rows:
|
||||
|
||||
```
|
||||
What does "cutting a release" mean?,Creating a new versioned release \
|
||||
branch/tag from main — the point at which the codebase is frozen for that \
|
||||
version.,jargon github-pm
|
||||
TLV (in protocol design),"Type-Length-Value: a framing scheme where each \
|
||||
record carries its type tag, byte length, then payload. Self-describing, \
|
||||
extensible, used in BER/DER, Lightning Network messages, etc.",concepts \
|
||||
networking
|
||||
```
|
||||
|
||||
## Adding cards via Claude
|
||||
|
||||
Ask Claude in natural language:
|
||||
|
||||
> Claude, add a card explaining "dogfooding" to dev/cards.csv
|
||||
|
||||
Claude appends a properly-escaped row with sensible tags. You review the
|
||||
diff, commit if you version this dir, then re-import into Anki when ready.
|
||||
|
||||
## Importing into Anki
|
||||
|
||||
1. Open Anki.
|
||||
2. **File → Import** → pick `~/Anki/<topic>/cards.csv`.
|
||||
3. The header directives drive the import — confirm the deck name and
|
||||
notetype shown in the dialog, then click Import.
|
||||
4. Anki uses the **first field** (Front) as the duplicate key, so re-importing
|
||||
the same CSV after appending new rows updates existing notes and adds new
|
||||
ones — no duplicates.
|
||||
|
||||
## Exporting a tag-filtered subset (e.g. French → farming, for an employee)
|
||||
|
||||
Anki's main File → Export dialog targets whole decks, not tags. To slice by
|
||||
tag:
|
||||
|
||||
1. **Tools → Create Filtered Deck** (Ctrl+Alt+N).
|
||||
2. Search: `deck:French tag:farming` (combine tags with `OR` /`-tag:` as
|
||||
needed).
|
||||
3. Anki creates a temporary deck containing just those cards. Filtered decks
|
||||
are references, not copies — originals are untouched.
|
||||
4. **File → Export** → pick the filtered deck → **Anki Deck Package
|
||||
(.apkg)** → **uncheck "Include scheduling information"** so the recipient
|
||||
starts fresh.
|
||||
5. Save to `~/Anki/exports/french-farming.apkg`. Hand to the recipient; they
|
||||
double-click or File → Import into their own Anki.
|
||||
6. Delete the filtered deck after (Decks page → cog → Delete). Originals
|
||||
stay put.
|
||||
|
||||
## Decks vs tags — which to use
|
||||
|
||||
- **Decks** are the hierarchy you see on Anki's home screen. Cards live in
|
||||
exactly one deck. Use a deck for **a domain you study as a unit** (Dev,
|
||||
French).
|
||||
- **Tags** are orthogonal labels. A card can have many. Use tags for
|
||||
**slices within a deck** you'll want to filter on: `farming`, `livestock`,
|
||||
`gardening`, `jargon`, `concepts`, `networking`, `github-pm`.
|
||||
|
||||
Splitting `french/farming/`, `french/livestock/` into separate folders would
|
||||
duplicate this structure outside Anki and lose the cross-cutting filter
|
||||
power — keep one CSV per language/topic and let tags do the slicing.
|
||||
|
||||
## Suggested tag vocabulary
|
||||
|
||||
- **dev**: `jargon`, `concepts`, `github-pm`, `networking`, `nix`, `git`,
|
||||
`security` — add as you go.
|
||||
- **french**: `general`, `phrases`, `farming`, `livestock`, `gardening`,
|
||||
`kitchen`, `weather` — pick one per row at minimum, add specifics as
|
||||
needed.
|
||||
- **protocols**: tag every card with **at least one ecosystem tag** plus
|
||||
optionally a **spec tag** and a **topic tag**:
|
||||
- ecosystem: `bitcoin`, `lightning`, `nostr` (a card on a Lightning BOLT
|
||||
that touches both Bitcoin and LN can carry both)
|
||||
- spec: `bip-NN` (e.g. `bip-32`, `bip-39`, `bip-340`), `nip-NN` (e.g.
|
||||
`nip-01`, `nip-04`, `nip-46`), `bolt-NN` for Lightning BOLTs
|
||||
- topic: `cryptography`, `consensus`, `mempool`, `script`, `signing`,
|
||||
`relay`, `keys`, `events`, `bunker`
|
||||
- Bitcoin + Nostr overlap a lot (Schnorr/`bip-340`, key derivation, hash
|
||||
functions). Cross-ecosystem cards get both ecosystem tags so they
|
||||
surface from either filter.
|
||||
|
||||
Be conservative: every new tag is a slice you commit to maintaining. Reuse
|
||||
existing ones unless a new dimension genuinely needs splitting.
|
||||
|
||||
## Versioning this dir (optional)
|
||||
|
||||
`~/Anki/` is not under version control by default. If you want history:
|
||||
|
||||
```
|
||||
cd ~/Anki && git init && echo 'exports/*.apkg' >> .gitignore
|
||||
```
|
||||
|
||||
The CSV headers + plain-text rows diff cleanly. `.apkg` exports are
|
||||
artifacts and don't belong in git.
|
||||
728
files/dev-CLAUDE.md
Normal file
728
files/dev-CLAUDE.md
Normal file
|
|
@ -0,0 +1,728 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
> **Source-of-truth path:** `/etc/nixos/files/dev-CLAUDE.md`. `~/dev/CLAUDE.md`
|
||||
> is a home-manager `mkOutOfStoreSymlink` to here (`/etc/nixos/home.nix`).
|
||||
> `~/dev/` is not a git repo. Edits at either path take effect immediately
|
||||
> (no rebuild needed), but **commit from `/etc/nixos`**.
|
||||
|
||||
## What this directory is
|
||||
|
||||
`~/dev/` is **not** a single repo — it's a workspace of sibling projects for
|
||||
the aiolabs / atitlan.io stack (Lightning + Nostr). Most projects are
|
||||
forgejo-hosted at `git.atitlan.io`. Per-project CLAUDE.md files are the
|
||||
source of truth for each repo; this file holds workspace-level facts that
|
||||
cross-cut multiple repos.
|
||||
|
||||
## Layout convention
|
||||
|
||||
Bare repos live under `repos/<name>.git/` and are checked out as
|
||||
**worktrees** named after the branch (or purpose): `lnbits/main/`,
|
||||
`lnbits/dev/`, `lamassu-next/lightning-pub/`, etc. Single-worktree projects
|
||||
typically use `<project>/main/` (`host5-home/main`, `quartz-module/main`,
|
||||
`webapp-module/main`, `fava-module/main`).
|
||||
|
||||
## Per-project CLAUDE.md (defer to these)
|
||||
|
||||
- `webapp/CLAUDE.md` — Vue 3 + Vite + Electron client (Nostr + Lightning).
|
||||
Heavy guidance on modular DI architecture, Shadcn forms, mobile file-input
|
||||
defenses. Read before touching anything under `webapp/`.
|
||||
- `bitspire/bitspire/CLAUDE.md`
|
||||
- `quartz-module/main/CLAUDE.md`
|
||||
- `docs/castle-docs/CLAUDE.md`
|
||||
|
||||
## Key cross-cutting locations
|
||||
|
||||
- **`deploy/server-deploy/`** — Unified NixOS infrastructure flake. Consumes
|
||||
project repos here as flake inputs (`url = "git+ssh://...aiolabs/<x>"`).
|
||||
See `flake.nix` for the canonical map of which repos feed which hosts.
|
||||
- **`local/docker/regtest/`** — Local multi-node Lightning regtest stack
|
||||
(LND/CLN/Eclair + bitcoind/electrs/boltz/fava). See `local/README.md`.
|
||||
**Default lnbits dev path is FakeWallet — no docker needed.** Only spin up
|
||||
regtest when testing real channels/payments.
|
||||
|
||||
**`LNBITS_SRC` branch awareness:** the dev compose builds lnbits from
|
||||
`${LNBITS_SRC:-~/dev/lnbits/main}`. If `LNBITS_SRC` points elsewhere
|
||||
(e.g. a feature branch), commits to `lnbits/main` **don't reach the
|
||||
dev image even after `--no-cache` rebuild**. Verify resolved context
|
||||
with `docker compose config | grep -A2 lnbits` before assuming a
|
||||
rebuild picked up your patch.
|
||||
|
||||
**Extension folder = LNbits install target:** the compose mounts
|
||||
`~/dev/shared/extensions/` at `/shared` and sets `LNBITS_EXTENSIONS_PATH=
|
||||
/shared`, so the extension git checkout *is* the installed extension.
|
||||
Clicking "Upgrade" in the LNbits UI extracts the catalog archive
|
||||
directly over the checkout, wiping `.git`. Mitigation: aio semver
|
||||
MUST beat upstream tag (see catalog rules).
|
||||
- **`refs/`** — Curated mirrors of upstream reference codebases (Beancount,
|
||||
Lemmy, LNbits, LND/CLN/Eclair, nostr-tools, khatru, …) plus weekly
|
||||
digests. Manifest `refs.toml`; refresh via `bin/refresh`, digest via
|
||||
`bin/digest`. Layout: `repos/<group>/<owner>/<name>`. Use for grep/browse
|
||||
— *don't* import for builds; pin via flake input or submodule instead.
|
||||
- **`shared/extensions`, `shared/lamassu-server`, `shared/nix-bitcoin`** —
|
||||
shared paths used across multiple worktrees.
|
||||
- **`upstream-prs/`** — branches staged for upstream contribution.
|
||||
- **`lnbits-extensions/`** — extension catalog (`extensions.json`).
|
||||
|
||||
## Working in this workspace
|
||||
|
||||
- Treat each subdirectory as its own repo with its own conventions, branches,
|
||||
remotes. Don't assume changes in one project belong with changes in another.
|
||||
- Many lnbits worktrees share the same bare repo — be mindful which branch
|
||||
you're on, especially when running migrations or touching shared state.
|
||||
- The `webapp` repo's `dev` branch is the staging channel for
|
||||
`demo.aiolabs.dev`; `main` feeds production. Dev = staging, not "demo".
|
||||
|
||||
### Webapp release flow
|
||||
|
||||
1. Push to `aiolabs/webapp` **`dev`** (not main).
|
||||
2. Bump `webapp-dev` input in `deploy/server-deploy/flake.lock`
|
||||
(`nix flake lock --update-input webapp-dev`). Tracks `dev`.
|
||||
3. Deploy to `host4` (`./deploy.sh host4`). Smoke as staging vet.
|
||||
4. Once vetted: fast-forward `aiolabs/webapp` **`main`** to dev's commit
|
||||
+ bump the `webapp` (not `webapp-dev`) input in `flake.lock`.
|
||||
|
||||
**Never push runtime webapp changes directly to main.** If you find
|
||||
runtime commits on main that should have gone through dev, **stop and
|
||||
notify the user** — recovery is destructive and needs explicit auth.
|
||||
|
||||
**Carve-out: non-runtime / tooling-only changes can go direct to main +
|
||||
rebase dev on top.** Root-level dotfiles and dev tooling that don't ship
|
||||
in the Vite/Electron bundle (`.mcp.json`, editorconfig, CLAUDE.md, root
|
||||
README) have nothing to vet on staging. Pattern: commit on main, push,
|
||||
then `cd ~/dev/webapp/dev && git fetch && git rebase origin/main &&
|
||||
git push --force-with-lease`. Still PR for anything touching `src/`,
|
||||
`electron/`, `package.json`/`pnpm-lock.yaml`, build config.
|
||||
|
||||
- Production-bound changes need a corresponding `flake.lock` bump in
|
||||
`deploy/server-deploy/` to actually reach a host.
|
||||
- **Git staging discipline:** never `git add -A` / `git add .` — list
|
||||
filenames explicitly. Hard rule from a 2026-05-14 incident that leaked
|
||||
an LNbits auth key.
|
||||
|
||||
---
|
||||
|
||||
## aiolabs Forgejo conventions
|
||||
|
||||
### Fork versioning (lnbits + extensions)
|
||||
|
||||
Universal across all LNbits-derived aio forks:
|
||||
|
||||
- **Tag scheme:** `v<upstream-version>-aio.<patch-number>`. Examples:
|
||||
`v1.5.4-aio.1`, `v1.3.0-aio.2`. Bump upstream segment on rebase, reset
|
||||
patch counter to `1`.
|
||||
- **Hyphen pre-release suffix on tags + extension `config.json`**, NOT
|
||||
`+aio.N` build-metadata. Confirmed across multiple bumps.
|
||||
- **EXCEPTION: lnbits `pyproject.toml` requires PEP 440 → `+aio.N` there
|
||||
only.** Hyphen-suffix isn't PEP 440-valid. Same release, two spellings:
|
||||
git tag `v1.5.4-aio.1`, `pyproject.toml` `1.5.4+aio.1`. The latter is
|
||||
what `importlib.metadata` returns and the UI footer displays.
|
||||
- **Tag at deploy-ready boundaries, not every commit.** Tags mark vetted
|
||||
states; direct-commit-to-main is fine, but don't tag broken intermediate
|
||||
commits.
|
||||
- **Lint pipeline** for aio forks: black + mypy + prettier + ruff.
|
||||
|
||||
For `aiolabs/lnbits`: `deploy/server-deploy` pulls lnbits as a flake input
|
||||
pinned by commit. The tag is a human-readable label, not a functional
|
||||
dependency — but makes `git log --decorate` and archaeology vastly clearer.
|
||||
|
||||
### `aiolabs/lnbits-extensions` catalog rules
|
||||
|
||||
`extensions.json` is consumed at runtime by LNbits via
|
||||
`LNBITS_EXTENSIONS_MANIFESTS`; archives are sha256-pinned per entry.
|
||||
|
||||
**The catalog repo is NOT tagged.** It's a curated manifest, not a fork.
|
||||
Deploy reads it live from `…/raw/branch/main/extensions.json`; audit trail
|
||||
is `git log` on main, rollback is `git revert`.
|
||||
|
||||
- **Production points ONLY at our manifest, never upstream.** The deployed
|
||||
`LNBITS_EXTENSIONS_MANIFESTS` points at
|
||||
`git.atitlan.io/aiolabs/lnbits-extensions/raw/branch/main/extensions.json`
|
||||
(`deploy/server-deploy/modules/services/lnbits.nix:73`). Production
|
||||
users can only install / upgrade what we've curated. Cycle: upstream
|
||||
releases → vet in dev → rebase to aio → update our manifest. Never add
|
||||
`github.com/lnbits/<ext>` URLs for an extension with an active aio fork.
|
||||
- **Don't overwrite a version entry in place.** When bumping, *add a new
|
||||
entry* alongside the old — instances running the previous version need
|
||||
it to remain resolvable.
|
||||
- **Aio semver MUST beat the upstream tag we're forking** (dev hygiene).
|
||||
Regtest compose doesn't override `LNBITS_EXTENSIONS_MANIFESTS`, so dev
|
||||
sees LNbits's built-in default catalog AND our manifest. LNbits's
|
||||
"upgrade" picks highest semver across all visible entries; if upstream
|
||||
outranks our `-aio.N`, clicking Upgrade wipes our `.git`. Always bump
|
||||
aio fork to `v<upstream>-aio.1` immediately on rebase.
|
||||
- **Catalog bumps don't require a `flake.lock` bump.** LNbits fetches the
|
||||
manifest live; nix doesn't bake archives in. Explicit exception to the
|
||||
"production-bound changes need a flake.lock bump" rule.
|
||||
|
||||
### Extension version-bump procedure
|
||||
|
||||
When user says "bump <ext> and update lnbits-extensions":
|
||||
|
||||
1. Sanity-check ext repo: clean tree, on `main`, `git log <last-tag>..main`
|
||||
matches intended release.
|
||||
2. Choose semver bump from the diff.
|
||||
3. **Push the branch first** (force-push after rebase OK for solo-maintainer
|
||||
forks). Origin/main is recoverable; the tag isn't.
|
||||
4. **Test locally on dev LNbits BEFORE tagging.** Restart regtest to load
|
||||
new on-disk code (or uninstall+reinstall via UI); smoke happy paths,
|
||||
watch logs for migrations + exceptions. Once installs pull the catalog,
|
||||
bad tags are hand-fix-every-install messy.
|
||||
5. Tag at HEAD and push: `git tag v… && git push origin v…`.
|
||||
6. Fetch archive, compute sha256: `curl -sL .../archive/v….zip | sha256sum`.
|
||||
7. Add a new entry to `aiolabs/lnbits-extensions/extensions.json` (don't
|
||||
overwrite the old).
|
||||
8. Commit + push catalog. Live immediately — no flake.lock bump.
|
||||
|
||||
### Forgejo issue labels (aiolabs/webapp)
|
||||
|
||||
When creating issues on `aiolabs/webapp`, apply labels via
|
||||
`add_issue_labels` immediately on creation. Two axes:
|
||||
- **App scope:** `app:activities`, `app:webapp`, `app:marketplace`,
|
||||
`app:wallet`, etc.
|
||||
- **Issue type:** `type:bug`, `type:feat`, `type:chore`, etc.
|
||||
|
||||
### PR flow on aiolabs-forked extensions (default since 2026-06-12)
|
||||
|
||||
For aio LNbits ext forks (`aiolabs/lnurlp`, `aiolabs/withdraw`,
|
||||
`aiolabs/events`, `aiolabs/libra`, …), **feature-branch + PR is the
|
||||
default flow** — these extensions move real money and feed production
|
||||
instances, putting them in the same review category as `aiolabs/lnbits`
|
||||
and `aiolabs/webapp`.
|
||||
|
||||
**Why the flip** (was direct-commit-to-main while single-maintainer):
|
||||
a libra refactor regression (typed entry links breaking the approval
|
||||
flow, libra-#42) shipped straight to a production-feeding main on
|
||||
2026-06-12. The PR gate buys a review pause + a place for CI/tests
|
||||
before main moves.
|
||||
|
||||
Flow: branch → push → open PR (creation via MCP is fine) → **hand off
|
||||
to the user to merge via the Forgejo web UI** (see PR-merge rule in
|
||||
user-global CLAUDE.md — the MCP merge endpoint is unreliable and the
|
||||
user has a final-review ritual). Trivial typo/docs-only commits can
|
||||
still ride direct-to-main at the user's discretion — ask if unsure.
|
||||
|
||||
(L3 Forgejo branch protection on main for these repos is the parked
|
||||
enforcement follow-up, same as for lnbits/webapp.)
|
||||
|
||||
### Issue closing — manual, not commit-keyword auto-close
|
||||
|
||||
Do NOT use Forgejo closing keywords (`Closes #N`, `Fixes #N`) in commit
|
||||
messages on aiolabs repos. Reference issues as prose (`(libra-#42)` in
|
||||
the subject, plain `#42` in the body) and **close the issue manually**
|
||||
after the fix lands.
|
||||
|
||||
**Why:** auto-close fires when the commit hits main, but on this stack
|
||||
"on main" ≠ "deployed" — issues should stay open (or at least be closed
|
||||
deliberately) until the fix actually reaches the affected instance.
|
||||
Confirmed by user 2026-06-12 (libra-#42).
|
||||
|
||||
### aiolabs identity vs upstream
|
||||
|
||||
Forks live on Forgejo at `git.atitlan.io/aiolabs/*`. User's handle there
|
||||
is `padreug`. Fork-internal references (contributors, repo URLs, PR/issue
|
||||
links) use Forgejo + `padreug`. GitHub handle (`your-github-username`) is separate
|
||||
— only for upstream PRs. Identity-rewrite details in user-global
|
||||
`~/.claude/CLAUDE.md`.
|
||||
|
||||
---
|
||||
|
||||
## LNbits extension development
|
||||
|
||||
### Auth decorators
|
||||
|
||||
| Decorator | Auth scope | Returns |
|
||||
|---|---|---|
|
||||
| `require_invoice_key` | Wallet invoice key — read access | Wallet |
|
||||
| `require_admin_key` | Wallet admin key — write to **own wallet only** | Wallet |
|
||||
| `check_admin` | **LNbits admin user** (super_user + lnbits_admin_users), Bearer | Account |
|
||||
| `check_super_user` | LNbits super user only, Bearer | Account |
|
||||
|
||||
`require_admin_key` is **easily confused** with `check_admin` and means
|
||||
something very different. The first is a wallet-level write key (any user
|
||||
can have one for their own wallet); the second is LNbits-instance admin
|
||||
access. Use `check_admin` for cross-user / admin-only operations.
|
||||
|
||||
### Testing
|
||||
|
||||
Use FakeWallet (`LNBITS_BACKEND_WALLET_CLASS=FakeWallet`) for extension
|
||||
CRUD / API / UI tests. Spin up full regtest only when end-to-end Lightning
|
||||
payment behavior is actually under test (regtest costs build + container
|
||||
time and adds no fidelity for non-payment flows).
|
||||
|
||||
### Fork-migrations pattern (`migrations_fork.py`)
|
||||
|
||||
Per `aiolabs/lnbits#8` — fork-only schema deltas go in `migrations_fork.py`
|
||||
alongside the upstream-tracked `migrations.py`, loaded by our patched
|
||||
`migrate_extension_database()` under `<ext.id>_fork` in `dbversions`. Keeps
|
||||
upstream rebases conflict-free for the migration file.
|
||||
|
||||
**Architecture facts:**
|
||||
|
||||
- `dbversions` lives in the **core LNbits DB** (`database.sqlite3`), not
|
||||
per-extension. Schema: `(db TEXT PRIMARY KEY, version INT)`;
|
||||
`update_migration_version` is INSERT-OR-UPDATE so a new `<ext>_fork`
|
||||
row appears on first run with no core-side migration.
|
||||
- Extension data tables live in `ext_<id>.sqlite3` (SQLite) or a Postgres
|
||||
schema named after `<id>`. Created lazily on first `Database.connect()`.
|
||||
- **No cross-DB atomicity.** Extension migration commits to
|
||||
`ext_<id>.sqlite3`; `dbversions` upsert commits to `database.sqlite3`.
|
||||
If extension write succeeds and dbversions fails, migration is orphaned
|
||||
and re-runs on next startup. **Every migration MUST be idempotent**
|
||||
(`_alter_add_column_safe`, `CREATE TABLE IF NOT EXISTS`, etc.).
|
||||
|
||||
**Squash recipe for adopting the pattern on existing forks:**
|
||||
|
||||
1. Restore `migrations.py` to upstream-byte-identical (drop fork-only
|
||||
functions and any helpers added for them).
|
||||
2. Create `migrations_fork.py` with a single `m001_aio_<ext>_schema`
|
||||
that idempotently applies every fork-only delta the old migrations did.
|
||||
3. Use `_alter_add_column_safe` per ALTER and `CREATE TABLE IF NOT EXISTS`
|
||||
per table — no-ops cleanly on installs that already ran old migrations.
|
||||
|
||||
**One-time fix on installs adopting the pattern AFTER previously running
|
||||
old fork migrations:** their `dbversions['<ext>']` row is ahead of upstream
|
||||
(e.g. `events|11`). After moving to `migrations_fork`, the next upstream
|
||||
rebase that adds e.g. `m007` would compare `7 > 11 → false` and silently
|
||||
skip. **Reset the row before the rebase lands:**
|
||||
|
||||
```sql
|
||||
-- Run against the core DB, not the extension DB.
|
||||
UPDATE dbversions SET version = <upstream-max> WHERE db = '<ext>';
|
||||
```
|
||||
|
||||
Containerized: `docker compose exec lnbits python3 -c "..."` since the
|
||||
file is root-owned inside the container.
|
||||
|
||||
**Upstream-overlap at rebase time:** if upstream eventually adds a schema
|
||||
change we already carry in `migrations_fork.py`, fresh installs work (our
|
||||
guards swallow dups) but **existing installs crash** when upstream's
|
||||
non-idempotent migration runs. Mitigation:
|
||||
|
||||
1. **Prune the redundant block from `migrations_fork.py`** so future fresh
|
||||
installs get the column from upstream.
|
||||
2. **Pre-deploy `dbversions` surgery on affected installs:**
|
||||
`UPDATE dbversions SET version = <upstream-max> WHERE db = '<ext>'`.
|
||||
|
||||
Don't patch upstream's migration with idempotency guards in our fork —
|
||||
breaks the "migrations.py == upstream byte-identical" property.
|
||||
|
||||
**Upstream PR follow-up:** the extension loader change in
|
||||
`migrate_extension_database()` is upstreamable as a sibling to the
|
||||
in-flight `core_fork` PR (`your-github-username/lnbits @ add-fork-migrations-namespace`).
|
||||
Fork-internal patch for now.
|
||||
|
||||
### Settings precedence — env seeds DB on first boot, then DB wins
|
||||
|
||||
LNbits has two sources of truth for settings depending on lifecycle, and
|
||||
the switch happens automatically. Verified 2026-05-24 against `~/dev/lnbits/main`.
|
||||
|
||||
**On boot with `lnbits_admin_ui=True`** (`lnbits/core/services/users.py:
|
||||
231-247`, `check_admin_settings`):
|
||||
1. Read DB row via `get_super_settings()`.
|
||||
2. If DB empty → seed from `.env` via `init_admin_settings()` (first-boot
|
||||
only).
|
||||
3. `update_cached_settings(settings_db.dict())` overwrites in-memory
|
||||
`Settings` with the DB row. **`.env` values loaded by Pydantic at
|
||||
startup are clobbered.**
|
||||
|
||||
**Practical consequence:** once an instance has booted once, editing
|
||||
`.env` and restarting **changes nothing** for editable fields. Change via
|
||||
Admin UI (`PUT /api/v1/settings`, gated by `check_admin`) or by clearing
|
||||
relevant `system_settings` table rows.
|
||||
|
||||
**Exceptions where `.env` still wins every boot:**
|
||||
- `super_user` — env overrides DB at `users.py:243-245`.
|
||||
- `lnbits_admin_ui=False` — DB-load block skipped entirely.
|
||||
- All `ReadOnlySettings` fields (`settings.py:1132` + parents): `host`,
|
||||
`port`, `lnbits_extensions_path`, `lnbits_path`, `lnbits_title` (API
|
||||
title — NOT `lnbits_site_title` which is editable), `lnbits_data_folder`,
|
||||
`lnbits_database_url`, `auth_secret_key`, `first_install_token`,
|
||||
`lnbits_admin_ui`, `lnbits_allowed_funding_sources`.
|
||||
`update_cached_settings` skips any key in `readonly_variables`. Editable
|
||||
settings (site title/tagline, theme, watchdog, fee defaults, rate limits,
|
||||
per-funding-source credentials, the whole Admin UI form) get DB-frozen.
|
||||
|
||||
**`LNBITS_FIRST_INSTALL_TOKEN` rotation does NOT reset settings to env.**
|
||||
It creates a new super_user account with a fresh UUID and sets
|
||||
`settings.first_install = True`, re-enabling `/first_install` for a
|
||||
locked-out admin to re-claim the instance. No env values flow back through.
|
||||
|
||||
**Deploy-side consequence for `deploy/server-deploy/modules/services/
|
||||
lnbits.nix`:** editable env vars only take effect on fresh install
|
||||
(empty `settings` table). For existing deploys, "set it in nix, redeploy,
|
||||
done" only works for `ReadOnlySettings` fields.
|
||||
|
||||
---
|
||||
|
||||
## Nostr architecture
|
||||
|
||||
### Patterns reference is the source of truth
|
||||
|
||||
Before writing or reviewing any Nostr-related code in `~/dev/webapp/`,
|
||||
read **`docs/nostr-patterns/`** first. After implementing or refining a
|
||||
pattern (or fixing a subtle Nostr bug), **update the relevant topic file
|
||||
in the same commit**. If new, add to the index. Drift between reference
|
||||
and code defeats the purpose.
|
||||
|
||||
### Reference implementations (curated mirror)
|
||||
|
||||
`~/dev/refs/repos/nostr/<owner>/<repo>/` holds upstream nostr codebases
|
||||
worth grepping for patterns: `nostr-protocol/nips`, `fiatjaf/khatru`,
|
||||
`fiatjaf/nostr-tools`, etc. Refresh weekly via `~/dev/refs/bin/refresh`;
|
||||
digest under `~/dev/refs/digests/`. For new features: check
|
||||
`docs/nostr-patterns/` (internal) → then `refs/repos/nostr/` (external).
|
||||
|
||||
### Key in-flight initiatives
|
||||
|
||||
- **LNbits nostr transport** — `aiolabs/lnbits` PR #4
|
||||
(`nostr-native-transport` branch). NIP-44 v2 encrypted kind-21000 RPC
|
||||
over relays, modeled after Lightning.Pub. Lets HTTP-allergic clients
|
||||
(ATMs, kiosks behind NAT) reach LNbits through commodity relays.
|
||||
- **Lightning.Pub reference** — `~/dev/lamassu-next/lightning-pub/`. Key files:
|
||||
`extension-loader/proto/autogenerated/ts/nostr_transport.ts`,
|
||||
`extension-loader/src/services/nostr/nostrPool.ts`. Patterns: dual NIP-44
|
||||
v1+v2 support, content sharding for large messages.
|
||||
|
||||
### Long-term direction
|
||||
|
||||
The eventual goal is webapp + LNbits extensions communicating **exclusively
|
||||
over Nostr** — eliminating HTTP. Prefer architectural decisions that move
|
||||
toward Nostr-native. Don't create HTTP-only patterns that will need to be
|
||||
ripped out. The nostr transport PR (#4) is the first concrete step.
|
||||
|
||||
### Bunker for everything: no nsec at rest on LNbits
|
||||
|
||||
Once `aiolabs/lnbits#18` (NIP-46 bunker integration) lands, **every nsec
|
||||
on the LNbits host gets retired** — operator users AND the server identity.
|
||||
There is no two-tier endgame where users go through bunker and the server
|
||||
keeps `NOSTR_TRANSPORT_PRIVATE_KEY` on disk.
|
||||
|
||||
The plausible-looking carve-outs (server "conceptually different," boot
|
||||
bootstrap complexity, latency, failure modes) don't survive the threat
|
||||
model: the LNbits host runs extension code, payment plumbing, a public API
|
||||
— disk/root access there must NOT equal Nostr-identity compromise.
|
||||
|
||||
**Concrete rules:**
|
||||
- Every signing call routes through the signer abstraction
|
||||
(`lnbits.core.signers.resolve_signer` from `#17`). Impls: `LocalSigner`
|
||||
(envelope-encrypted at rest, **transitional**), `ClientSideOnlySigner`
|
||||
(operator-driven, no server signing), `RemoteBunkerSigner` (NIP-46 via
|
||||
`#18` — endgame).
|
||||
- No `if server_is_special:` branch anywhere. Server identity is one more
|
||||
account from the signer's perspective.
|
||||
- `NOSTR_TRANSPORT_PRIVATE_KEY` is acceptable as a transitional env-pinned
|
||||
source for a `LocalSigner` adapter, but with an **explicit sunset**.
|
||||
- For extension code signing on behalf of operators: same abstraction. The
|
||||
hybrid pattern at `aiolabs/spirekeeper` commit `e13178d` is the
|
||||
template (try-import `resolve_signer`, fall back to direct prvkey on
|
||||
pre-`#17` lnbits, both produce identical signed events).
|
||||
|
||||
If tempted to keep static nsec "because it's just the server" or "because
|
||||
bunker isn't ready yet," push back: the whole point of bunker is removing
|
||||
static nsec from the LNbits host.
|
||||
|
||||
### Respect protocol semantics over friction reduction
|
||||
|
||||
When picking a transport / event-kind / message-format for a new feature,
|
||||
the protocol's intended use case wins over "an existing listener happens to
|
||||
fire." Quick-fix transport choices get locked in by callers and are harder
|
||||
to migrate later.
|
||||
|
||||
**How to apply** — before wiring a new feature onto an existing listener:
|
||||
|
||||
1. Name the relationship the feature serves (ATM↔Customer, ATM↔LNbits,
|
||||
ATM↔Operator, Operator↔User, LNbits↔Bunker, …).
|
||||
2. Ask "what is this protocol / kind / RPC *for*?"
|
||||
3. If answers don't align, design or pick the right primitive even if a
|
||||
wrong-purpose listener already exists.
|
||||
|
||||
Exception: explicit, time-bounded stopgaps with a written migration path.
|
||||
|
||||
**Worked example (2026-05-29 / `aiolabs/bitspire#56`).** Cassette-config
|
||||
publishing was nearly routed through `clink.onManagement` (kind-21003)
|
||||
because the listener existed. CLINK is a payment-flow protocol; repurposing
|
||||
would have conflated payment + operator-config concerns. The right
|
||||
primitive was kind-30078 (NIP-78 replaceable, NIP-44 encrypted,
|
||||
`["p", atm_npub]`-tagged).
|
||||
|
||||
### Nostr kind allocations — avoid the CLINK band (21001–21003)
|
||||
|
||||
The Nostr ephemeral range (20000–29999) is technically unallocated but
|
||||
several application-protocol families have squatted on specific kinds.
|
||||
**CLINK has claimed 21001 / 21002 / 21003** for Offers / Debits / Manage
|
||||
(see `refs/repos/shocknet/shocknet/CLINK/`). We want CLINK-compat preserved
|
||||
as we adopt it for ATM cash-in/cash-out flows.
|
||||
|
||||
**Rule: kind:21001–kind:21099 is OFF-LIMITS for aiolabs-specific events.**
|
||||
CLINK may add adjacent kinds; on-the-wire collisions with
|
||||
`["clink_version"]`-tagged events aren't worth the namespace-squatting
|
||||
savings.
|
||||
|
||||
**Suggested aiolabs band: 22000–22099** for non-replaceable application
|
||||
events specific to our stack. Pick incrementally; document each allocation
|
||||
here so future sessions don't reuse a number.
|
||||
|
||||
**Settlement-receipt rotation (2026-06-02):** kind:21001 was originally
|
||||
locked in for bitSpire settlement receipts (`aiolabs/lnbits#22` +
|
||||
`aiolabs/spirekeeper#11`) before CLINK was in scope. Collision found
|
||||
during a CLINK primer review. Settlement receipts will land on a non-21001
|
||||
kind before either PR ships; rotation plan tracked on `aiolabs/spirekeeper#20`.
|
||||
|
||||
**Replaceable events (kind:30000–39999) are a separate namespace.**
|
||||
Kind:30078 (NIP-78 application-specific data) is fine for fee_config,
|
||||
cassette state, fleet roster — by-design replaceable state-of-the-world
|
||||
events.
|
||||
|
||||
### Standalone app pattern (webapp)
|
||||
|
||||
Modules can be deployed as standalone PWAs alongside the main webapp via a
|
||||
**second Vite entry point**. Used for `activities` (sortir); may replicate
|
||||
for marketplace / wallet / others.
|
||||
|
||||
For a new standalone app from an existing module:
|
||||
1. **HTML entry**: `<app-name>.html` at project root → `src/<app-name>-app/main.ts`.
|
||||
2. **App shell**: `src/<app-name>-app/{main.ts, App.vue}`.
|
||||
3. **Vite config**: `vite.<app-name>.config.ts` with its own build target
|
||||
and PWA scoping (see service worker rules in `webapp/CLAUDE.md`).
|
||||
4. **npm script**: `dev:<app-name>` and (for the demo host) a roll-up.
|
||||
|
||||
The standalone Vue frontend lives in `webapp/` alongside other AIO
|
||||
standalone apps (chat, forum, market, tasks, wallet, activities, libra) —
|
||||
the corresponding extension repo (`~/dev/shared/extensions/<name>/` if any)
|
||||
holds only the LNbits backend Python code.
|
||||
|
||||
---
|
||||
|
||||
## Matrix homeserver: Continuwuity
|
||||
|
||||
Self-hosted Matrix homeserver in the Rust/conduwuit lineage. Lives in
|
||||
`deploy/server-deploy/modules/services/matrix.nix` for castle hosts that
|
||||
opt into `services.matrix-stack.enable`. Sibling stack: LiveKit SFU,
|
||||
lk-jwt-service, Element Web, Element Call SPA, `.well-known/matrix/`
|
||||
delegation on the apex.
|
||||
|
||||
### Required config to avoid quiet breakage
|
||||
|
||||
- **`well_known.client` + `well_known.server`** — without these,
|
||||
Continuwuity uses `server_name` (the apex) when generating user-facing
|
||||
URLs like password reset links. Set to `https://matrix.${domain}` and
|
||||
`matrix.${domain}:443` to mirror the JSON nginx serves at
|
||||
`/.well-known/matrix/` on the apex.
|
||||
|
||||
- **`allow_registration = true` is safe.** It does NOT mean open
|
||||
registration. Without `registration_token`/`_file` AND without the
|
||||
`yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
|
||||
flag, Continuwuity rejects self-serve signups.
|
||||
|
||||
### First-user bootstrap
|
||||
|
||||
When no admin exists yet, Continuwuity auto-generates a one-time
|
||||
registration token at startup and prints it to journalctl. This
|
||||
**overrides** any `registration_token_file` you might wire up. After the
|
||||
first user registers, they're auto-promoted to admin.
|
||||
|
||||
### Admin commands
|
||||
|
||||
All admin ops happen in the auto-created control room via `!admin <group>
|
||||
<cmd>`. Groups: `token`, `users`, `appservice`, `rooms`, `federation`,
|
||||
`media`, `server`. Full reference:
|
||||
<https://continuwuity.org/reference/admin/index.html>.
|
||||
|
||||
Token issuance flags are **mutually exclusive** — pick exactly one of
|
||||
`--once`, `--max-uses N`, `--max-age <30s|5m|7d>`, `--immortal`.
|
||||
|
||||
### Bridges + bots
|
||||
|
||||
Continuwuity registers appservices via admin commands (`!admin appservice
|
||||
register` with pasted YAML), not via `app_service_config_files` like
|
||||
Synapse. The NixOS `services.mautrix-*.registerToSynapse` shortcut does
|
||||
NOT apply — bridges go through a manual paste step. Bot-only flows
|
||||
(maubot, custom matrix-nio scripts) don't need appservice registration.
|
||||
|
||||
## Maubot plugin development
|
||||
|
||||
Maubot is the standard Python plugin framework for Matrix bots; one
|
||||
running maubot daemon hosts many plugin-bots. Plugin source at
|
||||
`~/dev/maubot-plugins/<name>/`.
|
||||
|
||||
### `maubot.yaml` — `database_type` is API style, not storage backend
|
||||
|
||||
Valid values: `asyncpg` (modern, what `mautrix.util.async_db` provides) or
|
||||
`sqlalchemy` (legacy). NOT `sqlite` or `postgres` — that's the storage
|
||||
backend, chosen at the daemon level via `plugin_databases.sqlite` /
|
||||
`plugin_databases.postgres`. Wrong value fails at instance start with
|
||||
`RuntimeError: Unrecognized database type sqlite`.
|
||||
|
||||
### Parent command + freeform text + subcommands
|
||||
|
||||
For `!journal <text>` (parent) plus `!journal show [@user]` / `!journal
|
||||
today` (subcommands), `@command.new` needs **both** flags:
|
||||
|
||||
```python
|
||||
@command.new("journal", require_subcommand=False, arg_fallthrough=False)
|
||||
@command.argument("text", pass_raw=True, required=False)
|
||||
async def journal(self, evt, text=""): ...
|
||||
|
||||
@journal.subcommand("show", help="...")
|
||||
@command.argument("user", required=False)
|
||||
async def show(self, evt, user=None): ...
|
||||
```
|
||||
|
||||
Without `require_subcommand=False`, non-subcommand input shows auto-help
|
||||
instead of recording. Without `arg_fallthrough=False`, `pass_raw=True`
|
||||
greedily consumes the rest, so `!journal show @alice` gets recorded as an
|
||||
entry. Same pattern as upstream `maubot/reminder`.
|
||||
|
||||
### Multi-line freeform parent commands need `@command.passive`
|
||||
|
||||
The above only works when **everything is on one line**. For multi-line
|
||||
content (`!journal\n- item one\n- item two`), `@command.new` silently
|
||||
drops it — maubot's parser only treats *space* as the command/args
|
||||
delimiter, so a newline immediately after the command name makes maubot
|
||||
fail to recognise the command at all. No handler invoked, no error logged.
|
||||
|
||||
**Fix:** use `@command.passive` with a regex admitting any whitespace,
|
||||
then dispatch subcommands manually:
|
||||
|
||||
```python
|
||||
_JOURNAL_RE = re.compile(r"^!journal(?:[ \t\r\n]+(.*))?$", re.DOTALL)
|
||||
|
||||
@command.passive(regex=_JOURNAL_RE)
|
||||
async def journal(self, evt, match):
|
||||
rest = (match[1] or "").strip()
|
||||
if not rest: ...
|
||||
first_token, _, after = rest.partition("\n")[0].partition(" ")
|
||||
if first_token == "show": ...
|
||||
else: ... # record `rest` verbatim — multi-line preserved
|
||||
```
|
||||
|
||||
You lose `@parent.subcommand("...")` ergonomics but gain reliability for
|
||||
prose-style inputs. **Rule of thumb:** for commands whose dominant use is
|
||||
free-text that may span lines, default to passive.
|
||||
|
||||
### Database access (asyncpg style)
|
||||
|
||||
```python
|
||||
from mautrix.util.async_db import UpgradeTable, Connection
|
||||
|
||||
upgrade_table = UpgradeTable()
|
||||
|
||||
@upgrade_table.register(description="Initial schema")
|
||||
async def upgrade_v1(conn: Connection) -> None:
|
||||
await conn.execute("CREATE TABLE ...")
|
||||
|
||||
class MyBot(Plugin):
|
||||
@classmethod
|
||||
def get_db_upgrade_table(cls) -> UpgradeTable:
|
||||
return upgrade_table
|
||||
|
||||
@command.new("foo")
|
||||
async def foo(self, evt):
|
||||
await self.database.execute("INSERT INTO entries VALUES ($1, $2)", a, b)
|
||||
rows = await self.database.fetch("SELECT ... LIMIT 10")
|
||||
```
|
||||
|
||||
Placeholders are `$1, $2, ...` regardless of backend; `async_db` normalizes
|
||||
across asyncpg/aiosqlite.
|
||||
|
||||
### Iteration loop
|
||||
|
||||
Edit + bump `version` in `maubot.yaml`, then:
|
||||
|
||||
```
|
||||
cd ~/dev/maubot-plugins/<plugin>
|
||||
zip -j ../<plugin>.mbp maubot.yaml <plugin>.py
|
||||
```
|
||||
|
||||
Upload via Plugins → click existing plugin → upload new `.mbp`. Instance
|
||||
reload requires hitting **Save** on the instance after upload — toggling
|
||||
Enabled and walking away doesn't persist.
|
||||
|
||||
### Wiping a plugin's data
|
||||
|
||||
Each plugin has its own SQLite DB under `plugin_databases.sqlite`. Cleanest
|
||||
reset is the maubot UI's per-instance Database tab — `DELETE FROM <table>`
|
||||
runs against the live DB without restart. Nuking the file works but loses
|
||||
migration version tracking.
|
||||
|
||||
### Pyright false positives
|
||||
|
||||
`maubot` / `mautrix` imports unresolved + `.subcommand` "unknown attribute"
|
||||
warnings are expected — the SDK is dynamic and pyright can't introspect
|
||||
the decorators. Ignore.
|
||||
|
||||
---
|
||||
|
||||
## Upstream lnbits PR conventions
|
||||
|
||||
For PRs to `github.com/lnbits/lnbits`:
|
||||
|
||||
- **Base branch is `dev`, not `main`.** 100% of recent merged PRs target
|
||||
`dev`. `main` gets release commits (`Merge branch 'dev'` + version bumps).
|
||||
- **Commit titles: lowercase conventional commits.** `feat:`, `fix:`,
|
||||
`chore:`, `chore(deps):`, `docs:`, `ci:`. Not capitalized.
|
||||
- **Identity:** push under GitHub identity (`your-github-username`), not Forgejo.
|
||||
See user-global `~/.claude/CLAUDE.md` for re-author details.
|
||||
|
||||
Verified against last 25 merged PRs on 2026-04-28.
|
||||
|
||||
---
|
||||
|
||||
## Documentation discipline
|
||||
|
||||
For any aiolabs repo with a structured `docs/` tree:
|
||||
|
||||
**Any commit that materially changes a database table, an API endpoint,
|
||||
order/event flow, Nostr publishing convention, or CMS structure must
|
||||
update the relevant note in `docs/` in the same commit.** Architecture
|
||||
decisions get ADRs at `docs/adr-NNNN-<slug>.md`.
|
||||
|
||||
Drift between docs and code defeats the purpose. Don't park doc updates
|
||||
for "later" — they get forgotten and docs lose signal.
|
||||
|
||||
---
|
||||
|
||||
## LNbits + Quasar UMD — frontend gotchas
|
||||
|
||||
Applies to anything rendering into LNbits' page shell: lnbits core
|
||||
(`~/dev/lnbits/*`) and every extension (`~/dev/shared/extensions/*`,
|
||||
`~/dev/lnbits-extensions/`). All use **Vue 3 + Quasar 2 as UMD globals** —
|
||||
no build step, Jinja templates with per-page JS.
|
||||
|
||||
**No self-closing tags.** Per Quasar's UMD rules
|
||||
(https://quasar.dev/start/umd/#usage), components need explicit-close:
|
||||
|
||||
```html
|
||||
<!-- correct -->
|
||||
<q-input v-model="foo" label="Foo"></q-input>
|
||||
|
||||
<!-- wrong — silently broken in UMD/no-build mode -->
|
||||
<q-input v-model="foo" label="Foo" />
|
||||
```
|
||||
|
||||
Self-closing is fine in `.vue` SFCs (build step rewrites them), but
|
||||
UMD-loaded templates are parsed by the browser's HTML parser, which
|
||||
doesn't honor self-close on non-void elements — close tag gets implied at
|
||||
the wrong place, nesting breaks silently, subsequent siblings end up
|
||||
inside the prior component.
|
||||
|
||||
**CSS specificity trap.** LNbits applies theme overrides on Quasar
|
||||
typography utilities (`.text-caption`, `.text-grey-*`) with `!important`.
|
||||
Class-based CSS in an extension — *even with `!important`* — loses unless
|
||||
the selector is strictly more specific. Inline `style` attrs (static or
|
||||
via Vue `:style`) win without an arms race.
|
||||
|
||||
**Rule:** for per-element typography/color overrides on LNbits pages, use
|
||||
Vue `:style` bindings, not `<style>` blocks targeting utility classes.
|
||||
Background/border tweaks at card-level are fine via classes.
|
||||
|
||||
**Cache busting.** Static assets served with `?v={server_startup_time}`
|
||||
(`lnbits/helpers.py: static_url_for`). Bumping JS requires server restart;
|
||||
Jinja templates re-render every request. If browser keeps serving stale
|
||||
JS after restart, hard-refresh (Ctrl+Shift+R) to bypass HTTP cache.
|
||||
|
||||
**Dark-mode color discipline.** Pale `bg-{color}-1` utilities render
|
||||
white-on-cream under dark theme — pair every pale background with an
|
||||
explicit dark text class (`bg-red-1 text-grey-9` etc).
|
||||
Loading…
Add table
Add a link
Reference in a new issue