Drum & bass bangers from another dimension.
翻译准备中
取自仓库 README
正在加载完整 README。 以下为已保存的摘录。
Drum & bass bangers from another dimension. Fluncle discovers and certifies drum & bass bangers, logs each as a finding, and keeps the full archive across the Galaxy, with fluncle.com as home base. The same archive, reachable however you like. Every surface reads the same public API and shares the same Log IDs. [README 설치·사용 부분 발췌] Install the latest standalone CLI release:查看完整 README ↗
Drum & bass bangers from another dimension.
Fluncle discovers and certifies drum & bass bangers, logs each as a finding, and keeps the full archive across the Galaxy, with fluncle.com as home base.
The same archive, reachable however you like. Every surface reads the same public API and shares the same Log IDs.
| Surface | Where | What |
|---|---|---|
| Web | https://www.fluncle.com | The public archive: cover-led, dark, fast. Also /about, /log, /log/<id> |
| Galaxy | https://galaxy.fluncle.com | The game: every finding is a star you can fly to |
| Radio | https://radio.fluncle.com | The cycling station: each finding's clean footage under its spoken observation, lean-back, forever |
| Public API | https://www.fluncle.com/api/v1/* | JSON reads and submissions; the bare /api/* answers only for the file-route carve-outs (see Web, Submission Flow) |
| RSS | https://www.fluncle.com/rss.xml | The 25 most recent findings and mixtapes, for feed readers |
| CLI | curl -fsSL https://www.fluncle.com/cli/latest.sh | sh | The archive in your terminal (see CLI) |
| DNS | dig random.dig.fluncle.com TXT +short | Findings answered over DNS TXT — latest, random, or any Log ID (see docs/dig.md) |
| SSH | ssh rave.fluncle.com | The rave terminal, a Wish/Bubble Tea app (see SSH) |
| MCP | https://www.fluncle.com/mcp | The archive as agent tools, Streamable HTTP, no auth (see MCP) |
| Tor | http://p53pc2uzfu2tnih4cd6wd42ok6zup2uttj6xdmjdccy5kqo33fyppkqd.onion | The whole site mirrored onto Tor, off the grid: the archive plus the API, RSS, and MCP (see docs/tor.md) |
Crawler and discovery surfaces (all under https://www.fluncle.com): /robots.txt, /sitemap.xml, /llms.txt, /llms-full.txt (the whole archive in one doc), /api/v1/openapi.json, /.well-known/api-catalog, /.well-known/agent-skills/index.json, and /.well-known/mcp/server-card.json. Public pages also carry schema.org JSON-LD (WebSite, MusicPlaylist, MusicGroup, FAQPage, and per-finding MusicRecording with the Log ID).
apps/cli Bun/TypeScript CLI. Thin client for public reads and admin API calls.
apps/dns Go DNS server behind dig.fluncle.com — findings answered over DNS TXT (docs/dig.md).
apps/extension Fluncle Lens, an MV3 Chrome extension. Linkifies fluncle:// coordinates on any page.
apps/mobile Expo React Native app (feed-first Stories + push). Thin client of the public API.
apps/raycast Raycast extension. Thin client that shells out to the CLI.
apps/sonar Rust in-memory exact-SIMD vector search engine — the whole MuQ corpus in RAM (docs/vector-serving.md).
apps/ssh Go Wish/Bubble Tea SSH terminal behind ssh rave.fluncle.com. Thin client of the public API.
apps/web TanStack Start public web app and server-side Fluncle API.
packages/contracts oRPC contracts for the public + admin HTTP API (the contract-first surface).
packages/live The live runtime: the glass (WebGL renderer) + the bridge (plan + fingerprint identity + supervisor + phone remote).
packages/media Remotion kit for static image assets (link-preview cards, covers).
packages/registry The typed catalog of every Fluncle surface (@fluncle/registry, the source of truth).
packages/skills Agent skills: operator runbooks + creative doctrine. Installed with `npx skills add`.
packages/sprites Fluncle's pixel-sprite family — the Galaxy game's sprites. Owns the PNGs; the web build mirrors them into public/.
packages/test-support Shared test helpers — the no-network rail and the test preload.
packages/tokens Shared design tokens (colors, typography, radii, motion) from DESIGN.md.
packages/tooling Typecheck-only workspace for the loose scripts: scripts/, docs/agents/hermes/scripts, packages/skills.
packages/ui Shared @fluncle/ui design system: the Shadcn base + the Nostalgic Cosmos tokens, consumed by apps/web.
packages/video Remotion kit for per-track social videos (the Nostalgic Cosmos).
The deployed web app owns the Spotify, Telegram, Turso, and Resend secrets. Every API route is served canonically under /api/v1/*. oRPC contract ops — the default for a public or admin HTTP surface — answer at that single prefix only; a bare /api/* contract path falls through to the router as a 404. The bare /api/* prefix is available only to the file-route carve-outs: auth/OAuth redirects, large-body or streaming media routes, and non-JSON emitters. Each carve-out mounts the same handler at both prefixes so POST bodies are preserved. Public reads are served by /api/v1/tracks (with since/until discovery windows), /api/v1/tracks/random, and /rss.xml. Newsletter signups post to /api/v1/newsletter, which the web app relays to Resend. The mobile app registers for push via /api/v1/devices (register_device / deregister_device); the web app then relays a notification to the Expo Push Service when a finding or mixtape publishes — best-effort and a no-op until EXPO_ACCESS_TOKEN is set, like the Telegram/Last.fm side-channels. Admin mutations are served by authenticated /api/v1/admin/* routes. Raycast must keep calling fluncle.
Listener submissions are accepted through public /api/v1/search and /api/v1/submissions routes, then reviewed through authenticated admin submission routes. Approval publishes through the authenticated admin add flow. Optional web accounts are private overlays on the same Log ID spine: signed-in listeners can sync Galaxy lifetime progress, save findings, see their own submissions, export data, and delete the account without changing anonymous Fluncle.
Exact runtime recipes, the concrete secret/topology map, and operator-only support are deliberately excluded from this public repo. Fluncle builds, runs, tests, and deploys without them.
The mk-repos skill maps how this repo relates to the others.
Project source code and documentation are licensed under the Apache License 2.0, with attribution notices in NOTICE.
The Fluncle name, logo, visual identity, social/profile artwork, generated media assets, track curation data, playlist identity, and other brand assets are not licensed for reuse except where explicitly stated.
Built by Maurice Kleine.
bun install
bun run dev
bun run build
bun run typecheck
bun run lint
bun run lint:fix
bun run format
bun run format:check
bun run check
bun run check:fix
Root scripts are orchestrated with Turborepo. bun run lint uses Bun canary through scripts/bun-canary.ts; bun run format uses the pinned release's bun fmt with flavor = "oxfmt" in bunfig.toml. Both run from the root with the workspace-aware .oxlintrc.json and .oxfmtrc.json configs. apps/raycast is the exception: Raycast's CLI owns extension formatting, and ray lint runs its own Prettier check over src/**.
Hosted checks and deploy builds use bun install --frozen-lockfile, so a package manifest change without a matching bun.lock fails before it can ship.
Fluncle has two environment surfaces:
~/.config/fluncle/.env.production for production CLI admin commands and ~/.config/fluncle/.env.local for local development.apps/web/.dev.vars rendered from apps/web/.dev.vars.tpl with 1Password for local Worker previews.The public CLI defaults to https://www.fluncle.com and the production profile. Admin CLI commands need FLUNCLE_API_TOKEN. Set FLUNCLE_API_BASE_URL only when pointing the CLI at a non-production API. When stdout is not a terminal the CLI stops any command after 240 s with exit code 124 and a message naming what it was waiting on; --timeout <seconds> or FLUNCLE_TIMEOUT changes that, and 0 disables it. Evidence commands refuse a rate-limit slot they could not reach before the deadline instead of queueing for it.
The CLI keeps listener sessions in a per-profile user.<profile>.json file, separate from the operator token in .env.<profile>. User requests read only the listener token; admin requests read only FLUNCLE_API_TOKEN. The listener token file is chmodded to 0600 on every write because a create-time mode does not tighten an existing file. Tests skip the operator profile when NODE_ENV=test, and subprocess tests use a loopback API, so a local test cannot issue an admin command against production. Bodyless admin POSTs omit Content-Type: application/json: oRPC rejects an empty body advertised as JSON. Multipart requests let fetch set the boundary-bearing content type. The CLI writes piped output synchronously so a large JSON response cannot be cut off when Bun exits.
Install the latest standalone CLI release:
curl -fsSL https://www.fluncle.com/cli/latest.sh | sh
bun run --cwd apps/cli fluncle recent --json
bun run --cwd apps/cli fluncle list --limit 10
bun run --cwd apps/cli fluncle open
bun run --cwd apps/cli fluncle open playlist --browser
bun run --cwd apps/cli fluncle open telegram --app
bun run --cwd apps/cli fluncle submit
bun run --cwd apps/cli fluncle submit "https://open.spotify.com/track/..."
bun run --cwd apps/cli fluncle version --check
bun run --cwd apps/cli fluncle admin tracks publish "https://open.spotify.com/track/..." --note "Absolute weapon"
bun run --cwd apps/cli fluncle admin tracks publish "https://open.spotify.com/track/..." --dry-run
bun run --cwd apps/cli fluncle admin tracks preview <track-id-or-log-id> --file preview.mp3 --source deezer:isrc --mime audio/mpeg
bun run --cwd apps/cli fluncle admin backfills previews --dry-run
bun run --cwd apps/cli fluncle admin submissions
bun run --cwd apps/cli fluncle admin submissions review <submission-id>
bun run --cwd apps/cli fluncle admin submissions reject <submission-id>
bun run --cwd apps/cli fluncle admin submissions approve <submission-id>
bun run --cwd apps/cli fluncle admin auth spotify
bun run --cwd apps/cli fluncle --env local recent --json
Database migrations are web workspace commands:
bun run --cwd apps/web db:generate
bun run --cwd apps/web db:migrate
Local migration commands read Turso credentials from apps/web/.dev.vars through apps/web/drizzle.config.ts. Everyday local dev runs against a per-worktree local libSQL server (turso dev) seeded from a production snapshot, so db:migrate only ever touches your own worktree's data. See docs/local-database.md for the full picture (dev, db:refresh-dev, db:pull-prod, and the worktree flow).
bun run --cwd apps/raycast build
bun run --cwd apps/raycast lint
bun run --cwd apps/raycast lint -- --fix
bun run --cwd apps/raycast dev
Raycast has one required preference:
Fluncle CLI Path
Raycast runs with a minimal shell environment, so this should point to a standalone binary rather than a Bun-linked script. Build and install one locally:
bun run --cwd apps/cli build:local
mkdir -p ~/.config/fluncle
install -m 755 ./apps/cli/dist/fluncle-darwin-arm64 ~/.local/bin/fluncle
For production admin Raycast commands, put only operator API settings in ~/.config/fluncle/.env.production:
FLUNCLE_API_BASE_URL=https://www.fluncle.com
FLUNCLE_API_TOKEN=<admin token>
For local development, put local API settings in ~/.config/fluncle/.env.local and run CLI commands with --env local.
Set the preference to your standalone binary, for example:
~/.local/bin/fluncle
Then verify:
fluncle recent --limit 3 --json
The rave terminal at ssh rave.fluncle.com is a Go app built on Wish and Bubble Tea. It serves the public archive over SSH: browse and submit tracks, subscribe to the newsletter, install the CLI. It reads the same public API as the other clients and owns no secrets.
Run it locally and connect from a second terminal:
cd apps/ssh
go run .
ssh -p 2222 127.0.0.1
Configuration is environment-driven: FLUNCLE_SSH_HOST and FLUNCLE_SSH_PORT (defaults 127.0.0.1:2222), FLUNCLE_API_URL (defaults to production), FLUNCLE_SSH_DATA_DIR for generated host keys (defaults to .local, gitignored), and optional FLUNCLE_GEOIP_DB pointing at a MaxMind-compatible .mmdb for the connected-crew country codes (lookups render VOID without it).
Checks:
go build -C apps/ssh ./...
gofmt -l apps/ssh
go vet -C apps/ssh ./...
Production runs as the fluncle-ssh systemd service on a dedicated VPS: the app terminates public TCP/22 while administrative OpenSSH listens on 2222 over Tailscale only. Cross-compile and deploy with the fluncle-hetzner-ops skill, which documents the public SSH host and monthly GeoIP refresh:
GOOS=linux GOARCH=amd64 go build -C apps/ssh -o dist/fluncle-ssh-linux-x64 .
SERVER_NAME=<tailscale-ip> BINARY_PATH=apps/ssh/dist/fluncle-ssh-linux-x64 \
FLUNCLE_API_URL=https://www.fluncle.com \
FLUNCLE_GEOIP_DB=/var/lib/fluncle-ssh/dbip-country-lite.mmdb \
packages/skills/fluncle-hetzner-ops/scripts/deploy-ssh-app-service.sh
bun run --cwd apps/web dev
bun run --cwd apps/web build
bun run --cwd apps/web typecheck
bun run --cwd apps/web lint
bun run --cwd apps/web preview
bun run --cwd apps/web deploy
The public app is dark-only and centered around the Fluncle cover art. The first page of tracks is server-rendered for crawlers; further pages load from /api/v1/tracks with limit/cursor pagination. Track rows open Spotify directly and use Spotify album artwork when available. Optional private accounts live at /account. On the web, joining and signing in lead with an emailed magic link (Better Auth's magicLink plugin: the link creates the account on first use, stored hashed, single-use, 15 minutes, rate-limited per IP and per address), plus Google where configured; password sign-in stays for existing accounts, and the mobile app keeps email/password. Redeeming a link on an unverified password account is Better Auth's documented account-takeover guard: it removes that unproven password and revokes the account's sessions, so the owner sets a new password (reset) before signing in with one again, on web or mobile. Email is auth, recovery, and the follow digest only, while the username (claimed after the first sign-in) is the private Galaxy identity. A signed-in listener can link the fluncle CLI to their account with fluncle login — an OAuth 2.0 Device Authorization (RFC 8628) flow (Better Auth's deviceAuthorization plugin) where the CLI prints a code, the user approves it at /device, and the CLI receives a user-scoped session token to sync their Galaxy progress and saved findings (fluncle me). That user token is stored separately from the admin FLUNCLE_API_TOKEN and is never an admin credential; fluncle logout revokes and removes it. The public RSS feed is available at /rss.xml; the crawler and discovery surfaces are the ones listed under Public Surfaces above. The .well-known maps, /llms.txt, /llms-full.txt, and the markdown homepage are served ahead of the router in apps/web/src/lib/server/agent-discovery.ts — except the MCP server card, which is served beside /mcp in apps/web/src/lib/server/mcp.ts.
Private account endpoints are intentionally separate from anonymous archive DTOs: /api/v1/me returns { ok: true, user: null } when signed out and only id, username, displayUsername, createdAt, and feature flags when signed in. /api/v1/me/csrf issues the short-lived x-fluncle-csrf token required by cookie-authenticated private account mutations. /api/v1/me/galaxy-progress, /api/v1/me/saved-findings, /api/v1/me/submissions, /api/v1/me/export, and /api/v1/me/delete require a Better Auth session and use DB-backed rate limits. Account deletion revokes sessions, deletes Better Auth credentials plus private progress and saves, marks the user deleted, and unlinks signed-in submissions while keeping the submission rows as anonymized review history. Discord/Resend copies may have their own processor retention windows.
The web app deploys as a Cloudflare Worker with the cf CLI, configured in apps/web/cloudflare.config.ts. Keep secrets out of that file; set them as Worker secrets.
The authoritative env inventory is the committed apps/web/.dev.vars.tpl — field for field, that template is the Worker secret set. Every var it carries must exist as a Worker secret for its feature to light up in production (the optional side-channels no-op cleanly when absent, so an unset one costs only that leg). The core five:
bun run --cwd apps/web wrangler login
bun run --cwd apps/web wrangler secret put TURSO_DATABASE_URL
bun run --cwd apps/web wrangler secret put TURSO_AUTH_TOKEN
bun run --cwd apps/web wrangler secret put BETTER_AUTH_SECRET
bun run --cwd apps/web wrangler secret put BETTER_AUTH_URL
bun run --cwd apps/web wrangler secret put ADMIN_SESSION_SECRET
For local Worker previews and local migration commands, read secrets from the committed apps/web/.dev.vars.tpl template. Local web secrets live in a Fluncle 1Password item with fields named exactly like the env vars; the item path is supplied by FLUNCLE_1PASSWORD_ENV_ITEM so the public template does not expose vault names. Set FLUNCLE_1PASSWORD_ENV_ITEM in your shell, then run db:secrets. A service-account wrapper selects the account; desktop-app users may also set FLUNCLE_1PASSWORD_ACCOUNT. macOS writes .dev.vars with owner-only permissions. Linux starts dev through op run without a secret file. Pass an explicit command to db:secrets for process-only mode on either platform. Leave the TURSO_DATABASE_URL pair to db:refresh-dev — it seeds a local libSQL database from a production snapshot and points that pair at the local server. The snapshot is pulled by db:pull-prod, which resolves production credentials at run time: the TURSO_* environment pair, else the 1Password item FLUNCLE_TURSO_OP_ITEM names, else a short-lived read-only token from the logged-in turso CLI; see docs/local-database.md:
bun run --cwd apps/web db:secrets
bun run --cwd apps/web db:refresh-dev
bun run --cwd apps/web preview
Production uses the fluncle Turso database through Worker secrets. Deploys run through Cloudflare Workers Builds on push to main, and migrations apply as part of the deploy step: the Cloudflare Deploy command is bun run --cwd apps/web deploy:cf, which runs the primary migration, then the required telemetry migration, then the hidden-graph-count repair (scripts/repair-hidden-graph-counts.ts), then cf deploy --prebuilt, and only afterwards activates the mixable-artists projection. Deploys do not purge the edge cache: each cached HTML page records its referenced /assets/ paths, and every client reuses it only when all those paths exist in the current client build output. Incompatible or headerless entries never serve, including stale-if-error, while unchanged assets preserve cache entries across deploys. Content writes still purge the pages they change. bun run --cwd apps/web cache:purge wipes the store by hand when an out-of-band write must reach crawlers sooner.
The primary runner validates the generated Drizzle journal against the loaded migration files, reads the target's __drizzle_migrations ledger, and applies the complete pending journal suffix as one atomic libSQL batch. Each migration's ledger stamp is part of the same transaction, so a failed statement rolls the schema changes and the stamps back together. The telemetry runner is required before publication; missing credentials and migration failures stop the command instead of leaving a new Worker ahead of its run-ledger schema. Applied migrations are never down-migrated: restore any required schema forward before rolling application code back. Local db:migrate behavior is unchanged. The complete commands, prerequisites, and rollback contract are in the local database guide.
To deploy manually from a checkout (builds locally, but does not migrate):
bun run --cwd apps/web deploy
After the first deploy, add fluncle.com in the Cloudflare Workers custom domains settings.
The web Worker also serves a small, stateless Model Context Protocol server at https://www.fluncle.com/mcp (Streamable HTTP, no sessions, no auth): the same archive the public API exposes, over the full protocol — tools, resources, and prompts. It is a thin layer over the internal functions the /api routes already use, so validation, the submission rate limit, and the submitter hash stay identical.
list_findings (the found-order feed of findings + mixtapes), list_tracks (every track Fluncle holds, newest release first, paged, with a tri-state certified filter), get_track (read one finding/mixtape by Log ID coordinate or Spotify id), get_random_track, get_status, list_fresh, search_archive, get_artist, get_label, build_set, list_similar_artists, list_album_catalogue, list_artist_catalogue, list_label_catalogue, list_artists, list_albums, list_labels, search_tracks, submit_track, subscribe_newsletter. SHARED_TOOL_SPECS in apps/web/src/lib/tool-specs.ts is the canonical inventory for every tool but search_tracks, which apps/web/src/lib/server/mcp.ts and apps/web/src/lib/webmcp.ts each declare.fluncle://finding/<logId> and each mixtape at fluncle://mixtape/<logId>, returning only the public record its /log page shows.recommend_finding (a finding for a mood), walk_recent_night, decode_coordinate.The MCP Server Card (SEP-2127) for agent discovery is at /.well-known/mcp/server-card.json. The endpoint is intercepted ahead of the router in apps/web/src/server.ts; the server lives in apps/web/src/lib/server/mcp.ts. The browser-side WebMCP surface (apps/web/src/lib/webmcp.ts) exposes the subset of tools whose spec lists the webmcp transport for agent-driving browsers (resources and prompts have no navigator.modelContext primitive, so the browser read path is the get_track tool); keep the two in step.
Point any MCP client at the endpoint, for example:
{
"mcpServers": {
"fluncle": { "type": "http", "url": "https://www.fluncle.com/mcp" }
}
}
packages/video is a Remotion kit that composes per-track social videos for Fluncle's Findings: 1080×1920 vertical clips that put one banger under the burning eclipse, on-brand with DESIGN.md and VOICE.md. It is built as a kit for a future AI agent to assemble fresh scenes per track, with NostalgicCosmos as the exemplar composition.
Render a preview for a track locally:
bun run social:preview <track-id>
This fetches the track, resolves and analyzes a preview clip, extracts the artwork palette, and renders to packages/video/out/<track-id>.mp4 (props land alongside as out/<track-id>.props.json). Add --skip-render to stop after the props JSON, --composition <Id> to render a registered composition other than the exemplar, or run bun run --cwd packages/video studio to scrub the composition live.
social:preview is the local dev loop. Production renders run unattended on the box render conductor, which wakes a scale-to-zero GPU box, renders one queued finding, and ships the bundle to R2 — see docs/agents/render-conductor.md. Publishing the finished clip is a separate leg: the server pushes to TikTok and YouTube through its Postiz integration, driven by the fluncle-publish-advance box cron, with the fluncle-publish skill as the doctrine. See packages/video/README.md for the machinery, the output contract, and the pipeline; the creative doctrine lives in the fluncle-video skill.
Build a standalone Linux binary locally, copy it to the server, and place API config in the operator user's config directory.
bun run --cwd apps/cli build:vps
scp ./apps/cli/dist/fluncle ./fluncle.env <host>:/tmp/
Run on the server:
mkdir -p ~/.config/fluncle
install -m 600 /tmp/fluncle.env ~/.config/fluncle/.env.production
sudo install -m 755 /tmp/fluncle /usr/local/bin/fluncle
rm -f /tmp/fluncle /tmp/fluncle.env
Verify:
fluncle recent --limit 1 --json
fluncle admin tracks publish calls POST /api/v1/admin/tracks with Authorization: Bearer <FLUNCLE_API_TOKEN>. The server checks Turso for duplicates by case-sensitive Spotify track id. It inserts a pending row first, then adds the track to Spotify, then posts to Telegram. Each external operation is retried three times.
If Spotify fails, Telegram is not posted. If Spotify succeeds but Telegram fails, the database row is kept with posted_to_telegram = false for later inspection or recovery.
Listeners can submit tracks from fluncle.com or with:
fluncle submit
fluncle submit "Camo & Krooked"
fluncle submit "https://open.spotify.com/track/..."
Both clients call GET /api/v1/search?q=..., select a visible candidate, then post the selected track to POST /api/v1/submissions. Submissions are stored as pending rows with hashed rate-limit keys; rejected rows are kept. Web, CLI, and SSH submissions remain anonymous-compatible; when a browser carries a valid private Better Auth session, the Worker attaches user_id server-side so the account can show its own submission history.
Operators review with:
fluncle admin submissions
fluncle admin submissions review <submission-id>
fluncle admin submissions reject <submission-id>
fluncle admin submissions approve <submission-id>
Approval fetches the submission, runs fluncle admin tracks publish "<spotify-url>" --dry-run, asks Publish this submission? (Y/n), then runs the real publish call only after confirmation and marks the submission approved.
Standalone CLI release binaries are built by .github/workflows/cli-release.yml after Quality Checks has passed for the exact pushed main SHA and the selector finds CLI shipping-source changes since the latest CLI tag. The first automated release uses v0.1.0; later CLI releases bump the minor version, for example v0.2.0, v0.3.0. The workflow uses Bun compile targets for macOS arm64/x64 and Linux arm64/x64, bakes the release version into the binary, creates the GitHub Release, and uploads the binaries as release assets. The hosted installer at https://www.fluncle.com/cli/latest.sh selects the right asset and installs it to ~/.local/bin/fluncle by default.
The CLI reports its bundled version with fluncle version. fluncle version --check compares that version against the latest GitHub Release tag.
The CLI also surfaces a passive update hint: after a command finishes it checks (at most once every 24h, cached in ~/.config/fluncle/update-check.json) whether the published fluncle npm version is newer and, if so, prints a one-line update notice to stderr with an install-method-appropriate command (npm i -g fluncle@latest, brew upgrade fluncle, or the curl installer / GitHub release for the standalone binary). The check is fire-and-forget — it never changes a command's output, exit code, or behavior, and is silent for --json, piped/non-TTY output, and CI. Set FLUNCLE_NO_UPDATE_NOTIFIER=1 to opt out.