* docs: mark M5 complete (feature surface done), set M6 as next
* docs: correct HANDOFF service count to eight
---------
Co-authored-by: James Maes <jmaes@dmdbrands.com>
SendMessage previously always sent from the first configured account
with no way to choose, so it silently used the wrong identity the
moment a second account existed. Add an optional account_id field to
SendMessageRequest; when set, resolve that specific account and fail
with a new AccountNotFound error if it does not exist, rather than
falling back to a different account. Omitting account_id preserves
the prior first-account behavior. Thread the selector through
ComposeRequest, resolve_sending_account, the CLI's mail send --account
flag, and regenerate the nuncio.v1 descriptor golden.
Refs #196, #170
Co-authored-by: James Maes <jmaes@dmdbrands.com>
* feat(store): add per-folder sync-state checkpoint table + CRUD
Adds folder_sync_state(account_id, folder_id, state) with
get/save methods and a round-trip test, giving inbound sync a place
to persist a per-mailbox resumption token between runs.
Refs #195#184
* feat(mail): honor since_state for incremental IMAP fetch + bound FETCH stalls
ImapEngine now derives a real UIDNEXT-based checkpoint after selecting a
folder and narrows the UID FETCH to {uidnext}:* when a checkpoint is
supplied, instead of always re-fetching 1:* and returning a state
fabricated from emails.len(). An unparseable checkpoint falls back to a
full fetch but is surfaced distinctly. Each per-item FETCH read is bounded
by a named timeout, surfacing MailError::FetchStalled instead of hanging.
MockMailBackend captures the since_state arg and simulates an incremental
(empty) second fetch. Session-level duplex tests assert the narrowed FETCH
range and the bounded stall.
Refs #195#184#183
* feat(sync): persist and thread the folder checkpoint, publish sync progress
fetch_and_persist now reads the folder's stored checkpoint, passes it to
the backend for a narrowed fetch, and persists the returned checkpoint only
after that folder's messages land -- so the second sync of an account/folder
is genuinely incremental. Adds CoreEvent::SyncProgress (per folder) as an
internal signal; map_core_event drops it from the gRPC stream since it has
no wire-contract case (proto unchanged).
Refs #195#184
* fix(mail): track UIDVALIDITY in sync checkpoint to prevent mail loss
A checkpoint stored only UIDNEXT, so a mailbox renumber (UIDVALIDITY
change, UIDs restart low) would drive {old_uidnext}:* against the new low
UIDs -- per RFC 3501 n:* returns only the single highest message and
silently skips the rest, permanently dropping mail. The checkpoint now
encodes {uidvalidity}:{uidnext}; the incremental narrowing applies only
when the stored UIDVALIDITY still matches the mailbox's current one. A
mismatch (renumber), a missing current UIDVALIDITY, or a legacy
bare-number/unparseable checkpoint falls back to a safe full 1:* re-fetch
with a debug-visible warn, then rewrites the checkpoint fresh.
Refs #195#184
---------
Co-authored-by: James Maes <jmaes@dmdbrands.com>
* feat(store): persist per-account IMAP/SMTP TLS modes; add account CRUD
Adds a backfill-safe imap_tls_mode/smtp_tls_mode column migration,
binds+reads both modes in save_account/list_accounts (previously
validated then silently discarded and hardcoded back to ImplicitTls),
and adds get_account/delete_account. Regression test proves the modes
survive a save/list/get round-trip.
Refs #194#182
* feat(mail): branch IMAP/SMTP transport on TlsMode; add connection probes
Threads TlsMode through ImapEngine/ImapDualSocketManager and
SmtpTransportEngine so implicit TLS, STARTTLS, and plain are genuine
transport paths (a unified MailStream plus a real IMAP STARTTLS
negotiation), not validate-and-discard. Adds bounded connect timeout
and per-engine probe_connection backing TestAccountConnection, with
mock/loopback-only setup tests for each mode.
Refs #194#182
* feat(accounts): edit/delete/test account lifecycle over gRPC
Implements Accounts UpdateAccount/RemoveAccount/TestAccountConnection in
the daemon (update mirrors add_account's keyring rollback discipline;
test probes real IMAP+SMTP via an injectable tester, honoring persisted
TLS modes), wires the configured TLS modes into build_mail_backend/
build_smtp_sender, and replaces the fabricated CLI account
show/edit/delete/test handlers with real thin gRPC clients. Adds an
offline full-daemon lifecycle E2E and unauth-rejection coverage for all
three new RPCs.
Refs #194#182
* fix(accounts): restore prior credential on update rollback
A password rotation that fails to persist previously deleted the
just-written secret, destroying the working credential the key already
held and orphaning the still-present account row. Snapshot the prior
secret first and restore it on save failure (falling back to delete
only when none existed). Regression tests force save_account to fail
and assert the original secret survives.
Refs #194#182
* docs(accounts): scope TestAccountConnection SMTP claim to reachability
The SMTP leg only dials and negotiates TLS/STARTTLS; it performs no AUTH
(lettre exposes no clean auth-only probe on AsyncSmtpTransport), while
IMAP genuinely logs in. Clarify the proto doc for smtp_ok/smtp_error and
the CLI output wording so nothing implies SMTP authentication was
verified. Doc-only proto change; golden unchanged.
Refs #194#182
---------
Co-authored-by: James Maes <jmaes@dmdbrands.com>
If set_secret succeeds but the subsequent save_account fails, the
credential was left in the keyring with no account row referencing it.
add_account now deletes the just-written secret before returning,
appending any rollback-delete failure to the original error message
without masking it.
Refs #197, #169
Co-authored-by: James Maes <jmaes@dmdbrands.com>
* docs: mark M1-M4 complete, set M5 as next milestone
Phase 3 feature completeness (Calendar, Contacts, JMAP, live Filters)
is done and merged to dev; update ROADMAP.md, the rendered roadmap
page, and HANDOFF.md to reflect it and point at M5 next.
* docs: correct service count to eight (Calendar+Contacts added)
---------
Co-authored-by: James Maes <jmaes@dmdbrands.com>
Adds a server-streaming Triage RPC that walks the whole message store
in bounded keyset chunks, evaluates the live FilterEngine against each
message via the shared apply_filter_actions helper, and streams real
cumulative progress -- so the active rule set can be applied to mail
synced before those rules existed. Wires the CLI's first streaming-RPC
consumer (filter triage) and regenerates the proto contract golden.
Refs #193
Co-authored-by: James Maes <jmaes@dmdbrands.com>
Adds UpdateRule/ExportRules/ImportRules/GetExecutionLogs to the
authenticated Filters gRPC service and rewires the CLI's
filter edit/export/import/logs subcommands onto it, replacing their
throwaway ephemeral local-db reads with the daemon's real, persistent
store and live FilterEngine. ImportRules now reports per-line parse
failures instead of silently dropping them.
Regenerates the nuncio-proto contract-stability golden for the
additive proto changes.
Refs #192
Co-authored-by: James Maes <jmaes@dmdbrands.com>
The account condition field lumped in with folder and compared against
email.folder_id, so WHERE account = '...' would false-positive on any
message sharing a folder with the target account. Split the arm so it
reads email.account_id instead.
header[...] conditions silently evaluated to false forever because Email
has no parsed-headers field to match against. Reject header conditions
at validation time with an honest FieldTypeMismatch instead of accepting
a rule that can never fire; the runtime fallback is now documented as an
unreachable defensive branch.
Refs #191
Co-authored-by: James Maes <jmaes@dmdbrands.com>
* feat(filters): wire FilterEngine into live inbound sync
Every synced message is now evaluated against the daemon's live
FilterEngine after it is persisted. MARK READ/UNREAD apply immediately
via set_message_read; MOVE/COPY/FLAG/UNFLAG/DELETE/FORWARD/CALL WEBHOOK
enqueue a real PendingRemoteMutation through the existing outbox for
the background worker to drain. Every match appends a
FilterExecutionLog entry, so a rule match now produces a genuine,
persisted, observable side effect instead of nothing.
The evaluate-and-route logic lives in a standalone apply_filter_actions
helper (not inlined into fetch_and_persist) so other sync paths can
reuse the exact same routing instead of re-deriving it.
* fix(filters): fire filter actions exactly once per new message
Sync is not incremental yet, so every re-sync re-fetches and
re-persists messages the backend still reports. Applying filter
actions unconditionally on every persisted message meant a rule kept
re-firing on every sync cycle for a message that arrived once,
enqueueing a fresh outbox mutation and execution log entry each time.
Guard fetch_and_persist so it only routes a message through
apply_filter_actions when the message did not already exist in the
store before this sync pass (DatabaseError::is_not_found, checked
before save_email's INSERT OR REPLACE erases the distinction). Also
makes the immediate-action match in apply_filter_actions exhaustive
over the remote action variants instead of a wildcard, so a future
immediate-style action can't silently fall through to remote routing.
---------
Co-authored-by: James Maes <jmaes@dmdbrands.com>
Replace JmapEngine's canned session/mailbox/message data with real
RFC 8620/8621 HTTP session discovery and Mailbox/get + Email/query +
Email/get JSON-RPC calls over reqwest, gated behind with_credentials
so uncredentialed callers keep the deterministic no-network fallback.
Fix the camelCase wire bug on JmapEmail (receivedAt/isUnread/
bodySnippet were silently defaulting) and add a regression test.
Wire nunciod::sync::build_mail_backend to construct the JMAP engine
with the account's real host/address/keyring password, mirroring the
IMAP arm, and rewrite the sync.rs tests that previously relied on
JmapEngine's canned data to run against wiremock servers instead.
Co-authored-by: James Maes <jmaes@dmdbrands.com>
Publish the M2-S1 contacts engine over the daemon's authenticated gRPC
API, mirroring the Calendar vertical: add nuncio.v1.Contacts (Sync,
ListContacts, GetContact, CreateContact) plus Contact/ContactEmail/
ContactPhone messages to the proto contract and regenerate the golden
descriptor.
Wire ContactsEngineOverrides + ContactsGrpcService into nunciod,
mounted behind its own BearerAuthInterceptor exactly like every other
service. ListContacts/GetContact always read genuinely persisted data
from DatabaseEngine; Sync fetches from an injected ContactsBackend
(MockContactsBackend in tests) and persists via
contacts_sync::sync_with_backend, mirroring calendar_sync.rs; with no
backend injected, production Sync returns an honest error since
AccountConfig carries no per-account CardDAV configuration yet.
Add connect_contacts/AuthenticatedContactsClient to nuncio-proto's
shared client helpers. Replace the CLI's Commands::Contact handlers
(previously a disposable in-memory ContactsDatabase.in_memory() per
invocation) with real gRPC calls against the daemon, so a contact
added via `contact add` persists and is visible to a later, separate
CLI invocation.
Deviations from the issue's literal proto sketch, flagged for review:
- Added CreateContact (not in the issue's message list): the
acceptance criteria requires `contact add` to persist a locally-
authored contact across CLI invocations, but the daemon has no
per-account CardDAV credential to write back to a real server. This
RPC only ever writes to the daemon's own local store -- never a
fabricated CardDAV write-back -- which is the same honest-surface
spirit as Calendar's "no create/delete" note (that note is about not
pretending to write to the remote server, not about never
persisting locally-authored data at all).
- ListContacts/CreateContact take a required account_id (the issue
sketched a bare ListContactsRequest{}), because the real
DatabaseEngine::list_contacts/save_contact are account-scoped;
ContactSubcommand::List/Search/Add/Sync now all take --account to
match.
- Contact search has no server-side RPC; `contact search` fetches the
full ListContacts result and filters client-side by display name,
organization, and email.
- Contact's created_at/updated_at/last_interacted_at are omitted from
the proto message -- no client use case identified for them yet.
Full-daemon offline E2E (contacts_e2e_test.rs) proves Sync -> List/Get
over the authenticated API, CreateContact's persistence surviving a
second, independent client connection, and an unauthenticated call
rejected with Code::Unauthenticated.
Co-authored-by: James Maes <jmaes@dmdbrands.com>
Replace CardDavClient::fetch_remote_vcards's hardcoded "Google CardDAV
Sync Contact" return with a real reqwest REPORT/addressbook-query
client that parses a genuine multistatus response. Add a real vCard
(RFC 6350) parser -- the crate previously only had a generator -- and
prove round-trip fidelity through Contact::to_vcard(). Add the
ContactsBackend trait and MockContactsBackend, mirroring
nuncio_cal::CalendarBackend/MockCalendarBackend. Distinguish transport
failures from parse failures via CardDavError::TransportFailed/
ParseFailed rather than ever silently succeeding with fabricated data.
Give nuncio-store real, tested save_contact/list_contacts/get_contact
CRUD backed by a genuine SQLite table + trigram FTS mirror, so contacts
persist through the daemon's own store instead of nuncio-contacts's
disconnected ContactsDatabase pool. This takes nuncio-store as a
narrow, deliberate dependency on nuncio-contacts::Contact.
Opt nuncio-contacts into the workspace lint gate ([lints] workspace,
#![forbid(unsafe_code)]) for parity with every other crate.
Wiremock-only, offline: no live network calls in tests.
Co-authored-by: James Maes <jmaes@dmdbrands.com>
* feat(calendar): expand RRULE recurrence in ListEvents and filter by calendar_id
Wire the previously-dead nuncio_cal::RecurrenceEngine into
CalendarGrpcService::list_events: non-recurring events pass through
unchanged, while recurring masters are expanded into real occurrences
for the requested window via a new store method
(list_recurring_calendar_events, no time filter since a master's own
timestamps can precede the window while its occurrences fall inside
it) and a pure expand_events_for_window merge helper that keeps
recurring rows sourced only from expansion, never double-counting a
master that also overlaps the window in the raw query.
Fix expand_occurrences to stop trusting an empty match as "return the
raw master": that fabricated an out-of-window result whenever a
window fell entirely outside a bounded (UNTIL/COUNT) series, and could
hang scanning an unbounded series for a match that would never come.
skip_while/take_while over the RRuleSet's monotonic iterator bounds
the walk and returns a genuinely empty result instead.
Also apply the calendar_id filter list_events accepted but ignored,
closing the gap between it and the mock backend's own filtering.
Refs #186
* docs(nunciod): reword stale comment referencing removed rrule fallback
---------
Co-authored-by: James Maes <jmaes@dmdbrands.com>
Wire the orphaned nuncio-cal engine into nunciod as a real, authenticated
gRPC vertical mirroring Mail: add service Calendar (Sync/ListEvents/GetEvent)
to nuncio.proto with a regenerated descriptor golden, connect_calendar in
nuncio-proto's client, calendar_sync::sync_with_backend plus
CalendarEngineOverrides/CalendarGrpcService mounted behind its own
BearerAuthInterceptor in nunciod::grpc, and real `cal sync`/`cal list` gRPC
clients in nuncio-cli replacing the fabricated CLI output. ListEvents/GetEvent
always read genuinely persisted data; production Sync returns an honest error
since there is no persisted per-account CalDAV configuration yet, and only an
injected MockCalendarBackend exercises a working Sync. Adds unit, daemon
integration, and offline full-daemon E2E tests, including an
unauthenticated-call-rejected assertion for the new service.
Refs #185
Co-authored-by: James Maes <jmaes@dmdbrands.com>
The dev -> rc -> main flow left rc ungated: push/pull_request triggers only
listed main and dev, so a release candidate branch could drift without CI
ever running against it. Add rc to both branch lists.
Cargo aliases are a single command argv and cannot chain subcommands, so
`cargo verify = ["fmt --all -- --check", "check-all", "test-all"]` tried to
run a literal subcommand named "fmt --all -- --check" and always errored.
The pre-commit hook already runs the three checks as separate commands, so
it was unaffected. Drop the broken alias, drop the matching RustRover run
configuration, and reword every doc that told readers to run `cargo verify`
to instead run fmt/check-all/test-all as three commands.
Several gRPC test helpers created an ephemeral sqlite-backed
DatabaseEngine via connect_ephemeral() and then discarded the
returned TempDir guard before returning the still-live db/server to
the caller. Dropping the guard unlinks the directory backing the
sqlite file; on Windows the still-open file handle keeps the data
reachable (masking the bug locally), but on Linux the directory entry
disappears immediately, so any pool connection opened afterward fails
with "unable to open database file" -- exactly the flake CI hit in
nunciod::grpc::tests::preview_rule_dry_runs_against_a_stored_message_without_persisting.
Fixes the same pattern in nunciod::grpc's spawn_test_server and
spawn_filters_test_server helpers, nuncio-cli's HeadlessRunner
(ephemeral_with), and grpc_auth_test's start_server: each now returns
(or stores) the TempDir so it stays alive for as long as its caller
keeps using the database.
The CI/release workflows still targeted the archived presentation
shells (nuncio-gui, nuncio-tui, nuncio-mcp) and their Tauri/npm
toolchain, so every run failed before the engine crates were even
built.
- ci.yml: run on dev in addition to main, drop the GTK/WebKit/Tauri
apt packages (the engine only needs zbus-based keyring and bundled
sqlite, both pure-Rust/cc), rely on rust-toolchain.toml instead of
dtolnay/rust-toolchain@stable to avoid drift from the pinned 1.97.1
toolchain, and make the coverage job continue-on-error so it stays
informational.
- release.yml / release-nightly.yml: package only nuncio-cli and
nunciod (the shells are gone), and drop the fabricated installer/
Homebrew release notes.
- release-desktop.yml: deleted; the Tauri desktop GUI is being
rebuilt as a separate client repo and no longer belongs here.
main's only unique commit was an outdated CLAUDE.md (pre-reconstruction
11-crate description). Keep dev's reconstructed tree wholesale; record main as
merged so dev -> main promotion is clean.
Set an explicit top-level permissions block (contents: read for CI;
contents: write for wiki-sync, which pushes to the wiki) so the workflows
follow least-privilege and clear the CodeQL 'workflow does not contain
permissions' findings.
Sweep every README/doc for accuracy and remove fabricated or stale content:
- README/CLAUDE: add the live roadmap-page link and a rule to keep the rendered
page in sync with docs/ROADMAP.md; fix the stale cov alias and the
already-moved-shells note.
- .github/CONTRIBUTING+SECURITY: drop the fabricated nuncio.mx site link and the
aspirational release/security claims; state honest pre-alpha status.
- docs/RUNNING, DEVELOPING, ARCHITECTURE, BACKLOG, Home, NSQL spec, corruption
notes, proto/README: correct crate count/list, service list, CLI flags, phase
status, and remove claims of unshipped/removed things (IPC-as-current,
hardcoded-keys, unwired engines); mark superseded api-surface-draft historical.
- Remove the fabricated benchmark baked into nsql_pipeline.svg and delete the
orphaned ipc_sequence.svg (depicted the removed IPC with a fake timing).
Add docs/STORY-WORKFLOW.md (the canonical per-story execution/gate/review
protocol every roadmap issue links to) and docs/HANDOFF.md (how a new machine or
LLM picks up the work: current state, branches, the M1-M7 story index, and
coordination rules). Include the incomplete calendar API-vertical WIP as a
reference patch under docs/handoff/ (prior art for the M1 calendar story;
does not compile, apply-to-inspect only).
Rewrite ROADMAP.md as the authoritative plan from today's state (Phases 0-2
complete; Phase 3 in progress) to a frozen, versioned gRPC API that front-end
repos can consume. Records true status, folds in gaps found via dogfooding and
audit, and defines seven milestones (M1 calendar, M2 contacts, M3 JMAP, M4 live
filters, M5 mail lifecycle + incremental sync, M6 security hardening, M7 API
freeze + client-readiness gate) each with concrete exit criteria.
Fix the TZID bug in the iCalendar parser: DTSTART/DTEND values carrying a TZID
parameter hit a catch-all that zeroed the timestamp. Resolve the zone via
chrono_tz and return an error on an unknown or DST-ambiguous zone rather than
fabricating a 1970 timestamp.
Add a CalendarBackend trait mirroring MailBackend, a real reqwest CalDAV REPORT
transport (CalDavClient + CalDavAccountConfig) that surfaces network/HTTP
failures as honest errors, and make MockCalendarBackend implement the trait.
wiremock integration tests cover the REPORT path and a transport failure.
Add save/list/get calendar_events CRUD to the store. Enable recursive_triggers
so INSERT OR REPLACE fires the FTS delete trigger, keeping events_fts in sync on
upsert; verified this only strengthens the WORM-audit guards and never
double-indexes the encrypted message FTS.
Refs #178
- Enforce a rule's account scope in evaluate, evaluate_with_timeout, and
preview via matches_account; previously a rule created ON ACCOUNT one account
would fire on any account whose message matched the WHERE clause.
- Return true byte offsets from find_keyword_outside_quotes (char_indices)
instead of a char count, so the WHERE/ACTION and ON ACCOUNT splits no longer
corrupt or panic on non-ASCII text preceding a keyword.
- Add FilterOperator::NotIn evaluated as the complement of IN. Negated IN lists
previously mapped to NotEquals over a List value, which no eval arm handled,
so NOT IN was silently always false.
Refs #177
Emit a FileDescriptorSet golden (proto/nuncio/v1/descriptor.bin) at build time
and assert the freshly-compiled descriptor matches it byte-for-byte, so any
breaking change to the nuncio.v1 wire contract fails the build until the golden
is deliberately regenerated. Source info is stripped so pure doc-comment edits
do not perturb the golden.
Add proto/README.md documenting Swift/C#/TypeScript codegen from the proto,
the loopback bearer-token auth model, the System.Subscribe push stream, and the
v1 versioning policy (additive-only within v1; breaking changes go to nuncio.v2).
Refs #174
Update CLAUDE.md, ARCHITECTURE.md, and the RustRover guide: the daemon serves
only the gRPC API on 127.0.0.1:9420; the old JSON-RPC IPC (:9422) is removed. Also
corrected the stale 'shells still in the workspace' note (they moved to _reference).
Delete nuncio-core::ipc (IpcDaemonServer, IpcClient, framing, protocol -- the
demux-race transport), the daemon's :9422 boot + JSON-RPC handler, the stubbed
'nuncio-cli daemon' subcommand, and the IPC-transport test (gRPC coverage
already exists). The daemon now awaits nunciod::grpc::serve as its foreground
task; DB/keyring/sync/outbox/update remain background tasks.
Refs #173
Earlier feature commits staged only crates/, leaving the root Cargo.lock behind
when nunciod gained bytes + a sqlx dev-dependency. Re-sync so a --locked build
matches the committed manifests.
Reword ~190 source comments and user-facing strings across crates/ to explain
the code's intent/constraints instead of citing story numbers, GH issues, or
roadmap phases (that traceability lives in git history + docs). Kept RFC and
technical rationale; changed no logic. Enforces the CLAUDE.md comment convention.
One-click 'Cargo Clean' RustRover config and a 'clean-all' cargo alias
(= cargo clean, whole workspace). Document in CLAUDE.md that source comments
explain intent/constraints only -- no backlog-story/GH-issue/phase references
(those live in git history + docs).
The plain-text (non --json) listing printed only 'N account(s) registered'; it
now lists each account's id, name, email, IMAP/SMTP endpoints, and TLS. The
--json output already carried the fields. Test asserts the details appear in
plain output.
Add authenticated Export (ExportMailbox -> mbox/eml-zip/json/jsonl written to a
local file, backed by the real ExportEngine + a scoped message query) and Audit
(ListRecords, VerifyChain over the WORM ledger with a structured report) gRPC
services, each mounted with the bearer interceptor like the others. Wire CLI
'mail export' and 'system audit list/verify'. Audit records expose the record
MAC, never the signing key. Offline tests cover export content/counts, chain
verify (intact + tampered), and auth rejection.
Refs #172
Add docs/DEVELOPING-IN-RUSTROVER.md (setup, running the daemon + CLI, running and
debugging tests, quality gates, the dev loop, gotchas) and link it from CLAUDE.md.
Add one-click run configs: Run nuncio-cli (mail sync / account list / mail list).
Remove buildTarget=REMOTE from all shared run configs. That flag (inherited from
the original Antigravity setup) told RustRover to build/run on the remote WSL
Ubuntu toolchain instead of native Windows -- wrong for dogfooding, and it also
breaks the daemon (no Secret Service keyring in a headless WSL). Configs now
default to the local machine.
Add run configs for the daemon (Run nunciod), the release build
(Cargo Build Release = cargo build-release), and Cargo Verify; fix Run nuncio-cli
to the current 'system status' command (was the stale '-- status'). Document the
IDE setup in CLAUDE.md (shared run configs, rustfmt-on-save, reload-cargo-project,
pinned toolchain, clippy-as-external-linter) and refresh the now-stale gate-status
note. The .idea shared/local .gitignore split, rustfmt-on-save code style, and
Cargo project registration were already correct.
docs/RUNNING.md: verified build/run/use steps for the daemon + CLI (real gRPC +
OS keyring confirmed working). Add a 'cargo build-release' alias for the two
runnable binaries.
nunciod opens ~/.nuncio/<db>, but SQLite create_if_missing creates only the
file, not its parent directory -- so on a fresh machine (no ~/.nuncio) the
daemon failed to start with SQLITE_CANTOPEN (code 14). connect_file now
create_dir_all's the parent first. Found via dogfooding (first real daemon run);
adds a regression test.