Commits
Move realwallpaper service and lassul.us website from prism to neoprism.
DNS for lassul.us already points at neoprism.
bgt-bot is commented out on neoprism for now; can be enabled later if it
goes down on prism (which currently isn't deploying anyway).
Drop the now-redundant lassul.us -> orange.r proxy in orange-host.nix and
the lassul.us acme fallback on prism.
Wraps noctalia-shell so declarative bits are merged into the user's
~/.config/noctalia/settings.json on every start. The file stays
writable; only fields covered by 'settings' (deep-merge) and
'settingsPatches' (jq filters) are pinned, so user GUI tweaks to
other keys survive.
Also moves the sway workspace patch (named/global workspaces,
click-to-current-output) into the wrapper behind 'swayWorkspaceFix',
and ships a default config that pins the dark-mode hook and shows
workspace names in the bar.
Hetzner rotated robotns2.second-ns.de's addresses on 2023-12-04;
the acl block was updated then but the remote/NOTIFY targets were
missed, so pushes to that slave were going to dead IPs.
The built-in `auto` permission mode uses a classifier to gate per-action
approvals — covers the routine-action auto-approve role GouvernAI was
filling. Drops --dangerously-skip-permissions (no longer needed since
the mode handles approvals) and the GouvernAI plugin + fetchFromGitHub.
Sets permissions.defaultMode = "auto" in the wrapper's --settings so
every session boots into auto mode without a CLI flag.
Adds projectStateWipeKeys (default [ "mcpServers" ]). preHook rewrites
~/.claude.json via jq on every invocation, deleting the listed keys
from each projects.<dir> entry. Purges stale `claude mcp add`
registrations that pin GC-able /nix/store paths and shadow the
canonical set the wrapper writes via --settings. Empty list disables
the step.
GouvernAI's hook returns allow/deny/ask_user decisions, but Claude Code's
native permission prompts still fire on top, so safe T1 actions get a
duplicate prompt. With native prompts off, the guardrails hook becomes the
sole gate — silent on T1, notify on T2, ask on T3/T4, hard-block on
NEVER rules.
- new skills/tmux documents the claude- session prefix, run-and-wait
marker pattern, output capture, and parallel sessions
- drops the tmux-mcp MCP server and the 5pkgs/tmux-mcp wrapper package
- adds python3 + tmux to the wrapper's extraPackages so plugin hooks
resolve a real python3 inside Nix dev shells where DEVELOPER_DIR
points at a Nix Apple SDK (otherwise /usr/bin/python3 -> xcrun ->
"tool 'python3' not found")
Make every bot response a thread reply instead of a fresh top-level
message. send_notice gains a thread_root kwarg that adds m.relates_to
with rel_type=m.thread plus the m.in_reply_to fallback. on_message
picks the thread root via thread_root_for(): if the user's message is
already inside a thread we reply into that one, otherwise we start a
new thread rooted on their message.
For Radarr/Sonarr download notifications to thread back to the
original request, tracked_requests.json now stores the request's
event_id alongside the room. load_tracked normalizes legacy bare-
string entries so existing tracked items keep working (just without
threading on the eventual notification).
Three problems were stacked on top of each other:
1. matrix-nio's client.join doesn't expose ?server_name=, so when our
homeserver hadn't fully processed the federation invite by the time
we turned around to join, it returned M_FORBIDDEN with no retry.
2. Even with retry, on_invite only fires once per sync delivery; if
joining failed we never tried again.
3. client.invited_rooms is populated incrementally from the persisted
next_batch, so synapse won't re-send invites that arrived in a prior
session, which means the startup loop was a no-op for stuck invites.
Add a try_join helper that calls /join/{room_id}?server_name=<inviter>
directly via _send and retries with exponential backoff, and a
fetch_pending_invites helper that does a fresh /sync (no since token)
to ask the server for the real invite set. Use both in on_invite and
in the startup loop.
MegolmEvent has no `.message` attribute (the real field is `.ciphertext`),
so the on_megolm callback added in 54d7388 raised AttributeError on the
first undecryptable event and took the bot down. Drop the field from the
log line — sender + session_id are enough.
Also re-raise after logging in the sync_forever wrapper so the process
exits non-zero and systemd's Restart=on-failure actually triggers; the
swallowed exception was why the bot stayed dead for 5 days instead of
restarting.
The previous translation of the dovecot 2.3 plugin/namespace blocks didn't
survive doveconf parsing or the runtime check for virtual namespaces:
- `acl = vfile` is a named-list-filter in 2.4. The driver moved to
top-level `acl_driver`, and per-identifier defaults live in
`acl <identifier> { acl_rights = ...; }` filters keyed on the ACL id
(owner/anyone/user=...). The filter name itself is parsed by
acl_identifier_parse, which is why earlier attempts with `acl vfile`
or `acl primary` failed with "Invalid identifier".
- `acl_shared_dict` was removed; the bot subscribes to shared paths
explicitly so we don't need it.
- Per-namespace `location = "driver:path:opts"` was split into separate
mail_driver / mail_path / mail_index_path settings.
- The virtual namespace also needs `mailbox_list_layout = fs` to override
the global Maildir++ - otherwise dovecot 2.4 refuses to initialise the
namespace, which broke both LMTP delivery and IMAP login.
Verified on neoprism with `doveconf -n` and a live LMTP delivery.
Dovecot 2.4 (now in nixpkgs) removed services.dovecot2.extraConfig and
mailPlugins. Translate the plugin/namespace blocks into the structured
settings tree, hoist ACL plugin keys to top-level (no more `plugin {}`
section), and update the variable-substitution syntax (%n -> %{user |
username}, %u -> %{user}).
Also drop the explicit master ref on nixos-mailserver since the project
no longer uses that branch name as default tracking.
claude is already provided via tools.s (and the default tools include
s now), so drop the explicit claude entry and add s for direct access.
Use the new wrapper plugins option to install the GouvernAI plugin from
GitHub, providing the /guardrails command and gouvernai flow-state skill.
Allow declaratively adding extra Claude Code plugin directories alongside
the bundled one. Each entry becomes a repeatable --plugin-dir flag.
Add MegolmEvent callback that warns the user when a message can't be
decrypted and requests the missing room key. Also process invited_rooms
from the initial sync so invites that arrived while the bot was offline
are accepted instead of being silently ignored.
- Package from llm.claude-code (llm-agents.nix)
- Skills: websearch (kagi-search), nix-workflow, hedgedoc,
reverse-engineering
- Comprehensive permission allow-list: nix/clan, git, common
CLI tools, file operations, dev tools
- Extra packages on PATH: kagi-search, curl, jq, ripgrep, fd, tree
- wrapperModules/claude-code: declarative options for skills, commands,
agents, mcpServers, hooks, permissions, settings, and claudeMd
- Assembles a plugin directory (--plugin-dir) and settings.json
(--settings) injected into every claude-code invocation
- claudeMd lines are fuzzy-merged into ~/.claude/CLAUDE.md using
Levenshtein distance (only adds, never removes user content)
- 5pkgs/levenshtein: small C tool for edit distance, threshold check,
and line-level file merge
- tools/claude: rewritten to consume the new wrapper module
Use pinentry-rofi on linux and pinentry_mac on darwin directly,
removing the now-unused pinentry-auto wrapper.
Directory names like "Searching (2018)" broke the space-separated
inotifywait output parsing, causing file events inside those
directories to be silently dropped.
Replace watch_new_dir (which spawned a redundant background inotifywait
per directory and silently lost files) with a synchronous scan_new_dir
that immediately finds and adds all files. The main recursive watcher
already handles future events in new subdirectories.
The grep patterns (',.*S' and '-v ,.*S') matched Dovecot's size
metadata (,S=7520) instead of the actual Maildir Seen flag after :2,.
This prevented read status from propagating from notmuch to IMAP.
Replace grep-based filtering with case statements that check the
flags field after :2, correctly.
Move realwallpaper service and lassul.us website from prism to neoprism.
DNS for lassul.us already points at neoprism.
bgt-bot is commented out on neoprism for now; can be enabled later if it
goes down on prism (which currently isn't deploying anyway).
Drop the now-redundant lassul.us -> orange.r proxy in orange-host.nix and
the lassul.us acme fallback on prism.
Wraps noctalia-shell so declarative bits are merged into the user's
~/.config/noctalia/settings.json on every start. The file stays
writable; only fields covered by 'settings' (deep-merge) and
'settingsPatches' (jq filters) are pinned, so user GUI tweaks to
other keys survive.
Also moves the sway workspace patch (named/global workspaces,
click-to-current-output) into the wrapper behind 'swayWorkspaceFix',
and ships a default config that pins the dark-mode hook and shows
workspace names in the bar.
The built-in `auto` permission mode uses a classifier to gate per-action
approvals — covers the routine-action auto-approve role GouvernAI was
filling. Drops --dangerously-skip-permissions (no longer needed since
the mode handles approvals) and the GouvernAI plugin + fetchFromGitHub.
Sets permissions.defaultMode = "auto" in the wrapper's --settings so
every session boots into auto mode without a CLI flag.
Adds projectStateWipeKeys (default [ "mcpServers" ]). preHook rewrites
~/.claude.json via jq on every invocation, deleting the listed keys
from each projects.<dir> entry. Purges stale `claude mcp add`
registrations that pin GC-able /nix/store paths and shadow the
canonical set the wrapper writes via --settings. Empty list disables
the step.
- new skills/tmux documents the claude- session prefix, run-and-wait
marker pattern, output capture, and parallel sessions
- drops the tmux-mcp MCP server and the 5pkgs/tmux-mcp wrapper package
- adds python3 + tmux to the wrapper's extraPackages so plugin hooks
resolve a real python3 inside Nix dev shells where DEVELOPER_DIR
points at a Nix Apple SDK (otherwise /usr/bin/python3 -> xcrun ->
"tool 'python3' not found")
Make every bot response a thread reply instead of a fresh top-level
message. send_notice gains a thread_root kwarg that adds m.relates_to
with rel_type=m.thread plus the m.in_reply_to fallback. on_message
picks the thread root via thread_root_for(): if the user's message is
already inside a thread we reply into that one, otherwise we start a
new thread rooted on their message.
For Radarr/Sonarr download notifications to thread back to the
original request, tracked_requests.json now stores the request's
event_id alongside the room. load_tracked normalizes legacy bare-
string entries so existing tracked items keep working (just without
threading on the eventual notification).
Three problems were stacked on top of each other:
1. matrix-nio's client.join doesn't expose ?server_name=, so when our
homeserver hadn't fully processed the federation invite by the time
we turned around to join, it returned M_FORBIDDEN with no retry.
2. Even with retry, on_invite only fires once per sync delivery; if
joining failed we never tried again.
3. client.invited_rooms is populated incrementally from the persisted
next_batch, so synapse won't re-send invites that arrived in a prior
session, which means the startup loop was a no-op for stuck invites.
Add a try_join helper that calls /join/{room_id}?server_name=<inviter>
directly via _send and retries with exponential backoff, and a
fetch_pending_invites helper that does a fresh /sync (no since token)
to ask the server for the real invite set. Use both in on_invite and
in the startup loop.
MegolmEvent has no `.message` attribute (the real field is `.ciphertext`),
so the on_megolm callback added in 54d7388 raised AttributeError on the
first undecryptable event and took the bot down. Drop the field from the
log line — sender + session_id are enough.
Also re-raise after logging in the sync_forever wrapper so the process
exits non-zero and systemd's Restart=on-failure actually triggers; the
swallowed exception was why the bot stayed dead for 5 days instead of
restarting.
The previous translation of the dovecot 2.3 plugin/namespace blocks didn't
survive doveconf parsing or the runtime check for virtual namespaces:
- `acl = vfile` is a named-list-filter in 2.4. The driver moved to
top-level `acl_driver`, and per-identifier defaults live in
`acl <identifier> { acl_rights = ...; }` filters keyed on the ACL id
(owner/anyone/user=...). The filter name itself is parsed by
acl_identifier_parse, which is why earlier attempts with `acl vfile`
or `acl primary` failed with "Invalid identifier".
- `acl_shared_dict` was removed; the bot subscribes to shared paths
explicitly so we don't need it.
- Per-namespace `location = "driver:path:opts"` was split into separate
mail_driver / mail_path / mail_index_path settings.
- The virtual namespace also needs `mailbox_list_layout = fs` to override
the global Maildir++ - otherwise dovecot 2.4 refuses to initialise the
namespace, which broke both LMTP delivery and IMAP login.
Verified on neoprism with `doveconf -n` and a live LMTP delivery.
Dovecot 2.4 (now in nixpkgs) removed services.dovecot2.extraConfig and
mailPlugins. Translate the plugin/namespace blocks into the structured
settings tree, hoist ACL plugin keys to top-level (no more `plugin {}`
section), and update the variable-substitution syntax (%n -> %{user |
username}, %u -> %{user}).
- wrapperModules/claude-code: declarative options for skills, commands,
agents, mcpServers, hooks, permissions, settings, and claudeMd
- Assembles a plugin directory (--plugin-dir) and settings.json
(--settings) injected into every claude-code invocation
- claudeMd lines are fuzzy-merged into ~/.claude/CLAUDE.md using
Levenshtein distance (only adds, never removes user content)
- 5pkgs/levenshtein: small C tool for edit distance, threshold check,
and line-level file merge
- tools/claude: rewritten to consume the new wrapper module