```
├── .cargo/
├── audit.toml (100 tokens)
├── .claude/
├── skills/
├── release/
├── SKILL.md (1700 tokens)
├── .codespellrc (100 tokens)
├── .depot/
├── workflows/
├── codespell.yml (100 tokens)
├── installer.yml (200 tokens)
├── nix.yml (200 tokens)
├── rust.yml (1200 tokens)
├── shellcheck.yml (100 tokens)
├── update-nix-deps.yml (100 tokens)
├── .dockerignore
├── .gitattributes (omitted)
├── .github/
├── DISCUSSION_TEMPLATE/
├── support.yml (500 tokens)
├── FUNDING.yml (100 tokens)
├── ISSUE_TEMPLATE/
├── bug.yaml (200 tokens)
├── dependabot.yml (200 tokens)
├── pull_request_template.md (100 tokens)
├── workflows/
├── codespell.yml (100 tokens)
├── docker.yaml (300 tokens)
├── installer.yml (200 tokens)
├── nix.yml (200 tokens)
├── release.yml (2.5k tokens)
├── rust.yml (1100 tokens)
├── shellcheck.yml (100 tokens)
├── update-nix-deps.yml (100 tokens)
├── .gitignore
├── .mailmap (200 tokens)
├── .rustfmt.toml
├── AGENTS.md (700 tokens)
├── CHANGELOG.md (15.9k tokens)
├── CODE_OF_CONDUCT.md (1000 tokens)
├── CONTRIBUTING.md (900 tokens)
├── CONTRIBUTORS (1900 tokens)
├── Cargo.lock (omitted)
├── Cargo.toml (600 tokens)
├── Dockerfile (200 tokens)
├── LICENSE (omitted)
├── README.md (900 tokens)
├── atuin.nix (300 tokens)
├── atuin.plugin.zsh
├── cliff.toml (700 tokens)
├── contrib/
├── pi/
├── atuin.ts
├── crates/
├── atuin-ai/
├── Cargo.toml (400 tokens)
├── migrations/
├── 20260413000000_create_ai_sessions.sql (200 tokens)
├── render-tests.sh (200 tokens)
├── replay-states.sh (600 tokens)
├── src/
├── commands.rs (900 tokens)
├── commands/
├── init.rs (1600 tokens)
├── inline.rs (3k tokens)
├── context.rs (500 tokens)
├── context_window.rs (4k tokens)
├── event_serde.rs (2.4k tokens)
├── lib.rs
├── permissions/
├── check.rs (500 tokens)
├── file.rs (100 tokens)
├── mod.rs
├── resolver.rs (200 tokens)
├── rule.rs (600 tokens)
├── shell.rs (8k tokens)
├── walker.rs (800 tokens)
├── writer.rs (1300 tokens)
├── session.rs (3.1k tokens)
├── store.rs (3.1k tokens)
├── stream.rs (2.9k tokens)
├── tools/
├── descriptor.rs (600 tokens)
├── mod.rs (7k tokens)
├── tui/
├── components/
├── atuin_ai.rs (900 tokens)
├── input_box.rs (1500 tokens)
├── markdown.rs (1500 tokens)
├── mod.rs
├── select.rs (600 tokens)
├── session_continue.rs (300 tokens)
├── content/
├── help.md (100 tokens)
├── dispatch.rs (4.6k tokens)
├── events.rs (300 tokens)
├── mod.rs
├── slash.rs (400 tokens)
├── state.rs (4.9k tokens)
├── view/
├── mod.rs (3.9k tokens)
├── turn.rs (2.6k tokens)
├── test-renders.json (2.1k tokens)
├── atuin-client/
├── Cargo.toml (500 tokens)
├── config.toml (2.9k tokens)
├── meta-migrations/
├── 20260203030924_create_meta.sql
├── migrations/
├── 20210422143411_create_history.sql (100 tokens)
├── 20220505083406_create-events.sql (100 tokens)
├── 20220806155627_interactive_search_index.sql
├── 20230315220114_drop-events.sql
├── 20230319185725_deleted_at.sql
├── 20260224000100_history_author_intent.sql
├── record-migrations/
├── 20230531212437_create-records.sql (100 tokens)
├── 20231127090831_create-store.sql (100 tokens)
├── src/
├── api_client.rs (2.7k tokens)
├── auth.rs (3k tokens)
├── database.rs (9.6k tokens)
├── distro.rs (500 tokens)
├── encryption.rs (3.4k tokens)
├── history.rs (5.4k tokens)
├── history/
├── builder.rs (900 tokens)
├── store.rs (3k tokens)
├── hub.rs (2000 tokens)
├── import/
├── bash.rs (1300 tokens)
├── fish.rs (1100 tokens)
├── mod.rs (600 tokens)
├── nu.rs (300 tokens)
├── nu_histdb.rs (700 tokens)
├── powershell.rs (1200 tokens)
├── replxx.rs (900 tokens)
├── resh.rs (900 tokens)
├── xonsh.rs (1600 tokens)
├── xonsh_sqlite.rs (1400 tokens)
├── zsh.rs (1400 tokens)
├── zsh_histdb.rs (1700 tokens)
├── lib.rs (100 tokens)
├── login.rs (500 tokens)
├── logout.rs (100 tokens)
├── meta.rs (2.5k tokens)
├── ordering.rs (200 tokens)
├── plugin.rs (500 tokens)
├── record/
├── encryption.rs (2.6k tokens)
├── mod.rs
├── sqlite_store.rs (3.9k tokens)
├── store.rs (400 tokens)
├── sync.rs (4k tokens)
├── register.rs (100 tokens)
├── secrets.rs (1100 tokens)
├── settings.rs (13.9k tokens)
├── settings/
├── dotfiles.rs
├── kv.rs (100 tokens)
├── meta.rs (100 tokens)
├── scripts.rs (100 tokens)
├── watcher.rs (1900 tokens)
├── sync.rs (1300 tokens)
├── theme.rs (5.5k tokens)
├── utils.rs (100 tokens)
├── tests/
├── data/
├── xonsh-history.sqlite
├── xonsh/
├── xonsh-82eafbf5-9f43-489a-80d2-61c7dc6ef542.json (1500 tokens)
├── xonsh-de16af90-9148-4461-8df3-5b5659c6420d.json (1500 tokens)
├── atuin-common/
├── Cargo.toml (200 tokens)
├── src/
├── api.rs (800 tokens)
├── calendar.rs (100 tokens)
├── lib.rs (300 tokens)
├── record.rs (2.6k tokens)
├── shell.rs (1100 tokens)
├── tls.rs (100 tokens)
├── utils.rs (2.6k tokens)
├── atuin-daemon/
├── Cargo.toml (300 tokens)
├── build.rs (200 tokens)
├── proto/
├── control.proto (300 tokens)
├── history.proto (300 tokens)
├── search.proto (100 tokens)
├── src/
├── client.rs (2.8k tokens)
├── components/
├── history.rs (2.2k tokens)
├── mod.rs (100 tokens)
├── search.rs (3.1k tokens)
├── sync.rs (2000 tokens)
├── control/
├── mod.rs (100 tokens)
├── service.rs (500 tokens)
├── daemon.rs (2.9k tokens)
├── events.rs (400 tokens)
├── history/
├── mod.rs
├── lib.rs (900 tokens)
├── search/
├── index.rs (5k tokens)
├── mod.rs (100 tokens)
├── server.rs (1100 tokens)
├── tests/
├── lifecycle.rs (1600 tokens)
├── atuin-dotfiles/
├── Cargo.toml (100 tokens)
├── src/
├── lib.rs
├── shell.rs (1400 tokens)
├── shell/
├── bash.rs (400 tokens)
├── fish.rs (400 tokens)
├── powershell.rs (900 tokens)
├── xonsh.rs (400 tokens)
├── zsh.rs (400 tokens)
├── store.rs (2.6k tokens)
├── store/
├── alias.rs
├── var.rs (3.4k tokens)
├── atuin-hex/
├── Cargo.toml (100 tokens)
├── src/
├── lib.rs (3k tokens)
├── osc133.rs (4k tokens)
├── atuin-history/
├── Cargo.toml (100 tokens)
├── benches/
├── smart_sort.rs (200 tokens)
├── src/
├── lib.rs
├── sort.rs (400 tokens)
├── stats.rs (3.3k tokens)
├── atuin-kv/
├── Cargo.toml (200 tokens)
├── migrations/
├── 20250501160746_create_kv_db.down.sql
├── 20250501160746_create_kv_db.up.sql (100 tokens)
├── src/
├── database.rs (1300 tokens)
├── lib.rs
├── store.rs (1300 tokens)
├── store/
├── entry.rs
├── record.rs (1000 tokens)
├── atuin-nucleo/
├── .gitignore
├── CHANGELOG.md (300 tokens)
├── Cargo.lock (omitted)
├── Cargo.toml (100 tokens)
├── LICENSE (3.3k tokens)
├── README.md (2.9k tokens)
├── bench/
├── Cargo.toml
├── src/
├── main.rs (500 tokens)
├── matcher/
├── Cargo.toml (100 tokens)
├── LICENSE
├── fuzz.sh
├── fuzz/
├── .gitignore
├── Cargo.toml (100 tokens)
├── fuzz_targets/
├── fuzz_target_1.rs (500 tokens)
├── generate_case_fold_table.sh (100 tokens)
├── src/
├── chars.rs (1300 tokens)
├── chars/
├── case_fold.rs (3.8k tokens)
├── normalize.rs (5.9k tokens)
├── config.rs (500 tokens)
├── debug.rs (100 tokens)
├── exact.rs (2000 tokens)
├── fuzzy_greedy.rs (400 tokens)
├── fuzzy_optimal.rs (2.5k tokens)
├── lib.rs (6k tokens)
├── matrix.rs (1300 tokens)
├── pattern.rs (4.2k tokens)
├── pattern/
├── tests.rs (1300 tokens)
├── prefilter.rs (600 tokens)
├── score.rs (1300 tokens)
├── tests.rs (4.4k tokens)
├── utf32_str.rs (3k tokens)
├── utf32_str/
├── tests.rs (300 tokens)
├── src/
├── boxcar.rs (5.4k tokens)
├── lib.rs (3.8k tokens)
├── par_sort.rs (7.3k tokens)
├── pattern.rs (500 tokens)
├── pattern/
├── tests.rs (100 tokens)
├── tests.rs (1600 tokens)
├── worker.rs (3k tokens)
├── tarpaulin.toml
├── typos.toml
├── atuin-scripts/
├── Cargo.toml (200 tokens)
├── migrations/
├── 20250326160051_create_scripts.down.sql
├── 20250326160051_create_scripts.up.sql (100 tokens)
├── 20250402170430_unique_names.down.sql
├── 20250402170430_unique_names.up.sql
├── src/
├── database.rs (2.1k tokens)
├── execution.rs (1900 tokens)
├── lib.rs
├── settings.rs
├── store.rs (700 tokens)
├── store/
├── record.rs (1400 tokens)
├── script.rs (900 tokens)
├── atuin-server-database/
├── Cargo.toml (100 tokens)
├── src/
├── calendar.rs (100 tokens)
├── lib.rs (1500 tokens)
├── models.rs (200 tokens)
├── atuin-server-postgres/
├── Cargo.toml (100 tokens)
├── build.rs
├── migrations/
├── 20210425153745_create_history.sql (100 tokens)
├── 20210425153757_create_users.sql (100 tokens)
├── 20210425153800_create_sessions.sql
├── 20220419082412_add_count_trigger.sql (300 tokens)
├── 20220421073605_fix_count_trigger_delete.sql (200 tokens)
├── 20220421174016_larger-commands.sql
├── 20220426172813_user-created-at.sql
├── 20220505082442_create-events.sql (100 tokens)
├── 20220610074049_history-length.sql
├── 20230315220537_drop-events.sql
├── 20230315224203_create-deleted.sql (100 tokens)
├── 20230515221038_trigger-delete-only.sql (200 tokens)
├── 20230623070418_records.sql (200 tokens)
├── 20231202170508_create-store.sql (200 tokens)
├── 20231203124112_create-store-idx.sql
├── 20240108124837_drop-some-defaults.sql
├── 20240614104159_idx-cache.sql
├── 20240621110731_user-verified.sql
├── 20240702094825_idx_cache_index.sql
├── 20260127000000_remove-email-verification.sql
├── src/
├── lib.rs (3.9k tokens)
├── wrappers.rs (500 tokens)
├── atuin-server-sqlite/
├── Cargo.toml (100 tokens)
├── build.rs
├── migrations/
├── 20231203124112_create-store.sql (200 tokens)
├── 20240108124830_create-history.sql (100 tokens)
├── 20240108124831_create-sessions.sql
├── 20240621110730_create-users.sql (100 tokens)
├── 20240621110731_create-user-verification-token.sql
├── 20240702094825_create-store-idx-cache.sql
├── 20260127000000_remove-email-verification.sql
├── src/
├── lib.rs (2.6k tokens)
├── wrappers.rs (400 tokens)
├── atuin-server/
├── CHANGELOG.md
├── Cargo.toml (200 tokens)
├── server.toml (200 tokens)
├── src/
├── bin/
├── main.rs (400 tokens)
├── handlers/
├── health.rs (100 tokens)
├── history.rs (1400 tokens)
├── mod.rs (300 tokens)
├── record.rs (300 tokens)
├── status.rs (300 tokens)
├── user.rs (1700 tokens)
├── v0/
├── me.rs (100 tokens)
├── mod.rs
├── record.rs (600 tokens)
├── store.rs (200 tokens)
├── lib.rs (500 tokens)
├── metrics.rs (400 tokens)
├── router.rs (1100 tokens)
├── settings.rs (700 tokens)
├── utils.rs (100 tokens)
├── atuin/
├── CHANGELOG.md
├── Cargo.toml (800 tokens)
├── LICENSE (200 tokens)
├── README.md
├── build.rs (100 tokens)
├── contrib/
├── pi/
├── atuin.ts (400 tokens)
├── src/
├── command/
├── CONTRIBUTORS
├── client.rs (2.6k tokens)
├── client/
├── account.rs (300 tokens)
├── account/
├── change_password.rs (400 tokens)
├── delete.rs (300 tokens)
├── link.rs (300 tokens)
├── login.rs (2.3k tokens)
├── logout.rs
├── register.rs (1200 tokens)
├── config.rs (2.2k tokens)
├── daemon.rs (4.8k tokens)
├── default_config.rs
├── doctor.rs (2.7k tokens)
├── dotfiles.rs (100 tokens)
├── dotfiles/
├── alias.rs (1100 tokens)
├── var.rs (1100 tokens)
├── history.rs (8.6k tokens)
├── hook.rs (2.8k tokens)
├── import.rs (1300 tokens)
├── info.rs (200 tokens)
├── init.rs (1200 tokens)
├── init/
├── bash.rs (300 tokens)
├── fish.rs (600 tokens)
├── powershell.rs (200 tokens)
├── xonsh.rs (200 tokens)
├── zsh.rs (400 tokens)
├── kv.rs (800 tokens)
├── scripts.rs (4k tokens)
├── search.rs (2.5k tokens)
├── search/
├── cursor.rs (2.5k tokens)
├── duration.rs (400 tokens)
├── engines.rs (600 tokens)
├── engines/
├── daemon.rs (1700 tokens)
├── db.rs (700 tokens)
├── skim.rs (1600 tokens)
├── history_list.rs (2.9k tokens)
├── inspector.rs (2.6k tokens)
├── interactive.rs (22.7k tokens)
├── keybindings/
├── actions.rs (2.4k tokens)
├── conditions.rs (5.3k tokens)
├── defaults.rs (9.1k tokens)
├── key.rs (4.4k tokens)
├── keymap.rs (1400 tokens)
├── mod.rs (100 tokens)
├── setup.rs (500 tokens)
├── stats.rs (500 tokens)
├── store.rs (700 tokens)
├── store/
├── pull.rs (500 tokens)
├── purge.rs (100 tokens)
├── push.rs (600 tokens)
├── rebuild.rs (600 tokens)
├── rekey.rs (400 tokens)
├── verify.rs (100 tokens)
├── sync.rs (800 tokens)
├── sync/
├── status.rs (300 tokens)
├── wrapped.rs (2.4k tokens)
├── contributors.rs
├── external.rs (500 tokens)
├── gen_completions.rs (500 tokens)
├── mod.rs (300 tokens)
├── main.rs (300 tokens)
├── shell/
├── .gitattributes
├── atuin.bash (5.9k tokens)
├── atuin.fish (1000 tokens)
├── atuin.nu (700 tokens)
├── atuin.ps1 (2.1k tokens)
├── atuin.xsh (600 tokens)
├── atuin.zsh (1100 tokens)
├── sync.rs (400 tokens)
├── tests/
├── common/
├── mod.rs (700 tokens)
├── sync.rs (300 tokens)
├── users.rs (800 tokens)
├── default.nix (100 tokens)
├── demo.gif
├── deny.toml (700 tokens)
├── depot.json
├── dist-workspace.toml (200 tokens)
├── docs-i18n/
├── .gitignore
├── ru/
├── config_ru.md (800 tokens)
├── import_ru.md (100 tokens)
├── key-binding_ru.md (200 tokens)
├── list_ru.md (100 tokens)
├── search_ru.md (400 tokens)
├── server_ru.md (700 tokens)
├── shell-completions_ru.md (100 tokens)
├── stats_ru.md (200 tokens)
├── sync_ru.md (300 tokens)
├── zh-CN/
├── README.md (1000 tokens)
├── config.md (500 tokens)
├── docker.md (300 tokens)
├── import.md (100 tokens)
├── k8s.md (900 tokens)
├── key-binding.md (100 tokens)
├── list.md (100 tokens)
├── search.md (200 tokens)
├── server.md (200 tokens)
├── shell-completions.md (100 tokens)
├── stats.md (200 tokens)
├── sync.md (200 tokens)
├── docs/
├── .gitignore
├── docs/
├── ai/
├── images/
├── basic-followup-questions.png
├── basic-refine.png
├── basic.png
├── danger.png
├── question.png
├── tool_atuin_history.png
├── introduction.md (500 tokens)
├── settings.md (500 tokens)
├── tools-permissions.md (700 tokens)
├── configuration/
├── advanced-key-binding.md (3.2k tokens)
├── config.md (5.7k tokens)
├── key-binding.md (3.1k tokens)
├── faq.md (500 tokens)
├── guide/
├── advanced-usage.md (600 tokens)
├── agent-hooks.md (800 tokens)
├── basic-usage.md (300 tokens)
├── delete-history.md (1000 tokens)
├── dotfiles.md (500 tokens)
├── getting-started.md (300 tokens)
├── import.md (100 tokens)
├── installation.md (1700 tokens)
├── shell-integration.md (1700 tokens)
├── sync.md (400 tokens)
├── theming.md (1100 tokens)
├── index.md (300 tokens)
├── integrations.md (400 tokens)
├── known-issues.md (100 tokens)
├── reference/
├── config.md (600 tokens)
├── daemon.md (200 tokens)
├── doctor.md (100 tokens)
├── gen-completions.md (100 tokens)
├── hex.md (400 tokens)
├── import.md (800 tokens)
├── info.md (100 tokens)
├── list.md (200 tokens)
├── prune.md (200 tokens)
├── search.md (700 tokens)
├── stats.md (300 tokens)
├── sync.md (400 tokens)
├── self-hosting/
├── docker.md (800 tokens)
├── kubernetes.md (1700 tokens)
├── server-setup.md (500 tokens)
├── systemd.md (300 tokens)
├── usage.md (200 tokens)
├── sync-v2.md (100 tokens)
├── uninstall.md (100 tokens)
├── mkdocs.yml (700 tokens)
├── pyproject.toml (100 tokens)
├── uv.lock (omitted)
├── flake.lock (omitted)
├── flake.nix (400 tokens)
├── install.sh (1200 tokens)
├── k8s/
├── atuin.yaml (700 tokens)
├── namespaces.yaml
├── secrets.yaml (100 tokens)
├── rust-toolchain.toml
├── scripts/
├── release.sh (3.1k tokens)
├── span-table.ts (2.5k tokens)
├── systemd/
├── atuin-server.service (100 tokens)
├── atuin-server.sysusers
```
## /.cargo/audit.toml
```toml path="/.cargo/audit.toml"
[advisories]
ignore = [
# This is a vuln on RSA. RSA is in our lockfile, but not in cargo-tree.
# It is a issue with sqlx/cargo, and does not affect Atuin.
# See:
# - https://github.com/launchbadge/sqlx/issues/3211
# - https://github.com/rust-lang/cargo/issues/10801
"RUSTSEC-2023-0071"
]
```
## /.claude/skills/release/SKILL.md
---
name: release
description: >
Orchestrate a multi-step Atuin CLI release — version bumping, changelog
generation, PR creation, tagging, and crates.io publishing. Invoke with
/release or /release <version>.
disable-model-invocation: true
argument-hint: [version]
---
# Atuin CLI Release
You are orchestrating a release of the Atuin CLI. Follow the steps below
**in order**, pausing at each checkpoint for user confirmation. Do not skip
steps or combine them.
## Current State
- Workspace version: !`sed -n '/^\[workspace\.package\]/,/^\[/s/^version = "\(.*\)"/\1/p' Cargo.toml`
- Latest tag: !`git describe --tags --abbrev=0 2>/dev/null || echo "none"`
- Suggested next version: !`git-cliff --bumped-version 2>/dev/null | sed 's/^v//' || echo "(unknown)"`
---
## Step 1 — Check Dependencies
Verify these tools are installed: `git`, `gsed`, `cargo`, `gh`, `git-cliff`.
Use `command -v` for each. If any are missing, report which ones and stop.
---
## Step 2 — Determine Version
The target version may be provided as `$ARGUMENTS`. If it's empty, use
AskUserQuestion to ask for the new version (show the current state above
for reference).
After determining the version:
- If it contains a `-` (e.g. `18.15.0-beta.1`), it is a **prerelease**.
Note this — it affects changelog and publish behavior later.
- Show the user: `current → new` and whether it's a prerelease.
- **Checkpoint:** Ask the user to confirm before proceeding.
---
## Step 3 — Set Up Working Directory
Clone a fresh copy into a temp directory:
```bash
WORKDIR=$(mktemp -d)
git clone git@github.com:atuinsh/atuin.git "$WORKDIR"
```
Print the working directory path so the user can find it if needed.
All subsequent Bash commands run from `$WORKDIR`.
---
## Step 4 — Create Branch & Update Versions
1. Create a release branch named after the version (no `v` prefix):
`git checkout -b <VERSION>`
2. Replace the old version with the new one in all `Cargo.toml` files.
**Escape dots** in the old version so sed treats them literally:
```bash
VERSION_PATTERN="${OLD_VERSION//./\\.}"
find . -type f -name 'Cargo.toml' -not -path './.git/*' \
-exec gsed -i "s/$VERSION_PATTERN/$NEW_VERSION/g" {} \;
```
3. Run `cargo check` to update `Cargo.lock`.
4. Show `git diff --stat` and the version-related lines from the diff:
```bash
git diff --unified=0 -- '*.toml' | grep -E '^\+.*version' | grep -v '^\+\+\+'
```
5. Verify the workspace version was actually updated by re-reading it
from `Cargo.toml`.
6. **Checkpoint:** Show the diff summary and ask the user to confirm the
version changes look correct.
---
## Step 5 — Update Changelog
The changelog strategy differs for prereleases vs stable releases:
- **Prerelease:** Maintain a running `## [unreleased]` section containing
all changes since the last stable release. Use:
`git-cliff --unreleased --strip all`
(cliff.toml's `ignore_tags` already ignores beta/alpha tags, so
`--unreleased` spans back to the last stable release automatically.)
- **Stable release:** Generate a versioned entry that replaces the
`[unreleased]` section. Use:
`git-cliff --unreleased --tag "v<VERSION>" --strip all`
Then update `CHANGELOG.md`:
1. If an existing `## [unreleased]` or `## [Unreleased]` section exists,
**remove it entirely** (the heading and all content up to the next
`## ` heading).
2. Insert the new entry before the first existing `## ` version heading.
3. **Checkpoint:** Read and display the new changelog entry to the user.
Ask if they want any edits. If so, make the requested changes using
the Edit tool. Repeat until they're satisfied.
---
## Step 6 — Commit & Push
Stage all changes and commit:
```
chore(release): prepare for release <VERSION>
```
Push the branch with `--set-upstream origin`.
---
## Step 7 — Create PR & Wait for Merge
### Create the PR
Extract the changelog entry body (everything between the new `## ` heading
and the next one) for the PR description.
For prereleases, the heading to match is `## [unreleased]`.
For stable releases, it's `## <VERSION>` (escape dots in the awk pattern).
Create the PR:
```bash
gh pr create \
--title "chore(release): prepare for release <VERSION>" \
--body "<body with changelog>" \
--repo atuinsh/atuin
```
Show the PR URL to the user.
### Wait for merge
Start a **persistent Monitor** that polls the PR status every 30 seconds.
The monitor script must:
- **Only emit output** on meaningful state changes: all checks green, PR
merged, or PR closed. Silent polls keep the monitor quiet and avoid
flooding notifications.
- Handle transient API errors gracefully (don't crash on a single failure)
- Exit 0 on `MERGED`, exit 1 on `CLOSED`
The rollup mixes two entry shapes: `CheckRun` entries use `status` +
`conclusion`, while `StatusContext` entries use `state`. A check counts
as "passing" when it's in a terminal state with a non-failing outcome.
Treat `SUCCESS`, `SKIPPED`, and `NEUTRAL` as passing — some release
workflows (e.g. `announce`, `build-global-artifacts`) are conditional
and report `SKIPPED` on non-tag events, which is expected, not a
failure.
Example monitor script (substitute the actual PR number):
```bash
checks_passed=false
while true; do
json=$(gh pr view PR_NUM --repo atuinsh/atuin --json state,statusCheckRollup 2>/dev/null) || { sleep 30; continue; }
state=$(echo "$json" | jq -r '.state')
case "$state" in
MERGED) echo "PR #PR_NUM has been merged!"; exit 0 ;;
CLOSED) echo "PR #PR_NUM was closed without merging."; exit 1 ;;
esac
# Only notify once when all checks reach a terminal passing state.
# CheckRun entries carry `status`/`conclusion`; StatusContext entries
# carry `state`. SKIPPED and NEUTRAL count as passing.
if [ "$checks_passed" = false ]; then
counts=$(echo "$json" | jq -r '
[.statusCheckRollup[]?] as $all
| ($all | map(select(
(.status == "COMPLETED" and (.conclusion | IN("SUCCESS","SKIPPED","NEUTRAL")))
or .state == "SUCCESS"
)) | length) as $passing
| ($all | map(select(
(.status == "COMPLETED" and (.conclusion | IN("FAILURE","TIMED_OUT","CANCELLED","ACTION_REQUIRED","STALE")))
or (.state | IN("FAILURE","ERROR"))
)) | length) as $failing
| "\($all | length) \($passing) \($failing)"
' 2>/dev/null)
read -r total passing failing <<<"$counts"
if [ "${failing:-0}" -gt 0 ] 2>/dev/null; then
echo "PR #PR_NUM has $failing failing check(s) — investigate before merging."
checks_passed=true # don't re-notify
elif [ "${total:-0}" -gt 0 ] 2>/dev/null && [ "$total" = "$passing" ]; then
echo "All $total checks passed on PR #PR_NUM — ready to merge!"
checks_passed=true
fi
fi
sleep 30
done
```
Tell the user to go review and merge the PR. While the monitor runs, you
can respond to other questions — the monitor notifications will arrive
asynchronously.
When the monitor reports `MERGED`, proceed to the next step.
If it reports `CLOSED`, inform the user and stop the release.
---
## Step 8 — Tag Release
Back in the working directory:
```bash
git checkout main
git pull
git tag "v<VERSION>"
git push --tags
```
Tell the user the tag was pushed and the release CI workflow has been
triggered.
---
## Step 9 — Publish to crates.io
**If this is a prerelease**, skip this step entirely and tell the user.
**If this is a stable release**, ask the user whether to publish.
If yes, publish each crate **in dependency order** using `--no-verify`
(the code already passed CI, and verification fails when crates.io
hasn't indexed a freshly-published dependency yet):
```
atuin-common, atuin-client, atuin-ai, atuin-dotfiles, atuin-history,
atuin-nucleo/matcher, atuin-nucleo, atuin-daemon, atuin-kv,
atuin-scripts, atuin-server-database, atuin-server-postgres,
atuin-server-sqlite, atuin-server, atuin-hex, atuin
```
For each crate, run from `crates/<name>`:
```bash
cargo publish --no-verify 2>&1
```
If it fails with "already uploaded", report it as a skip (not an error) —
some crates like `atuin-nucleo` are versioned independently and may
already be published at their current version.
If it fails for any other reason, stop and report the error.
---
## Completion
Summarize what was done:
- Version released
- PR URL
- Tag name
- Which crates were published (if any)
- Working directory path and how to clean it up (`rm -rf`)
## /.codespellrc
```codespellrc path="/.codespellrc"
[codespell]
# Ref: https://github.com/codespell-project/codespell#using-a-config-file
skip = .git*,*.lock,.codespellrc,CODE_OF_CONDUCT.md,CONTRIBUTORS
check-hidden = true
# ignore-regex =
ignore-words-list = crate,ratatui,inbetween,iterm,fo,brunch
```
## /.depot/workflows/codespell.yml
```yml path="/.depot/workflows/codespell.yml"
# Depot CI Migration
# Source: .github/workflows/codespell.yml
#
# No changes were necessary.
# Codespell configuration is within .codespellrc
name: Codespell
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
codespell:
name: Check for spelling errors
runs-on: depot-ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Codespell
uses: codespell-project/actions-codespell@v2
with:
# This is regenerated from commit history
# we cannot rewrite commit history, and I'd rather not correct it
# every time
exclude_file: CHANGELOG.md
```
## /.depot/workflows/installer.yml
```yml path="/.depot/workflows/installer.yml"
# Depot CI Migration
# Source: .github/workflows/installer.yml
#
# No changes were necessary.
name: Install
on:
push:
branches: [main]
pull_request:
paths: .github/workflows/installer.yml
env:
CARGO_TERM_COLOR: always
jobs:
install:
strategy:
matrix:
os: [depot-ubuntu-24.04, macos-14]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
- name: Install zsh for ubuntu
if: matrix.os == 'depot-ubuntu-24.04'
run: |
sudo apt install zsh
- name: Test install script on bash
run: |
/bin/bash -c "$(curl --proto '=https' --tlsv1.2 -sSf https://setup.atuin.sh)"
[ -d "$HOME/.atuin" ] && source $HOME/.atuin/bin/env
atuin --help
- name: Test install script on zsh
shell: zsh {0}
run: |
/bin/bash -c "$(curl --proto '=https' --tlsv1.2 -sSf https://setup.atuin.sh)"
[ -d "$HOME/.atuin" ] && source $HOME/.atuin/bin/env
atuin --help
```
## /.depot/workflows/nix.yml
```yml path="/.depot/workflows/nix.yml"
# Depot CI Migration
# Source: .github/workflows/nix.yml
#
# No changes were necessary.
# Verify the Nix build is working
# Failures will usually occur due to an out of date Rust version
# That can be updated to the latest version in nixpkgs-unstable with `nix flake update`
name: Nix
on:
push:
branches: [main]
paths-ignore:
- 'ui/**'
pull_request:
branches: [main]
paths-ignore:
- 'ui/**'
jobs:
check:
runs-on: depot-ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: cachix/install-nix-action@v31
- name: Run nix flake check
run: nix flake check --print-build-logs
build-test:
runs-on: depot-ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: cachix/install-nix-action@v31
- name: Run nix build
run: nix build --print-build-logs
```
## /.depot/workflows/rust.yml
```yml path="/.depot/workflows/rust.yml"
# Depot CI Migration
# Source: .github/workflows/rust.yml
#
# No changes were necessary.
name: Rust
on:
push:
branches: [main]
paths-ignore:
- "ui/**"
pull_request:
branches: [main]
paths-ignore:
- "ui/**"
env:
CARGO_TERM_COLOR: always
jobs:
build:
strategy:
matrix:
os: [depot-ubuntu-24.04, macos-14, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
- name: Install rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.94.0
- uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }}
- name: Run cargo build common
run: cargo build -p atuin-common --locked --release
- name: Run cargo build client
run: cargo build -p atuin-client --locked --release
- name: Run cargo build server
run: cargo build -p atuin-server --locked --release
- name: Run cargo build main
run: cargo build --all --locked --release
cross-compile:
strategy:
matrix:
# There was an attempt to make cross-compiles also work on FreeBSD, but that failed with:
#
# warning: libelf.so.2, needed by <...>/libkvm.so, not found (try using -rpath or -rpath-link)
target: [x86_64-unknown-illumos]
runs-on: depot-ubuntu-24.04
steps:
- uses: actions/checkout@v6
- name: Install cross
uses: taiki-e/install-action@v2
with:
tool: cross
- uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ matrix.target }}-cross-compile-${{ hashFiles('**/Cargo.lock') }}
- name: Run cross build common
run: cross build -p atuin-common --locked --target ${{ matrix.target }}
- name: Run cross build client
run: cross build -p atuin-client --locked --target ${{ matrix.target }}
- name: Run cross build server
run: cross build -p atuin-server --locked --target ${{ matrix.target }}
- name: Run cross build main
run: |
cross build --all --locked --target ${{ matrix.target }}
unit-test:
strategy:
matrix:
os: [depot-ubuntu-24.04, macos-14, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
- name: Install rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.94.0
- uses: taiki-e/install-action@v2
name: Install nextest
with:
tool: cargo-nextest
- uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-debug-${{ hashFiles('**/Cargo.lock') }}
- name: Run cargo test
run: cargo nextest run --lib --bins
check:
strategy:
matrix:
os: [depot-ubuntu-24.04, macos-14, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
- name: Install rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.94.0
- uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-debug-${{ hashFiles('**/Cargo.lock') }}
- name: Run cargo check (all features)
run: cargo check --all-features --workspace
- name: Run cargo check (no features)
run: cargo check --no-default-features --workspace
- name: Run cargo check (sync)
run: cargo check --no-default-features --features sync --workspace
- name: Run cargo check (server)
run: cargo check -p atuin-server
- name: Run cargo check (client only)
run: cargo check --no-default-features --features client --workspace
integration-test:
runs-on: depot-ubuntu-24.04
services:
postgres:
image: postgres
env:
POSTGRES_USER: atuin
POSTGRES_PASSWORD: pass
POSTGRES_DB: atuin
ports:
- 5432:5432
steps:
- uses: actions/checkout@v6
- name: Install rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.94.0
- uses: taiki-e/install-action@v2
name: Install nextest
with:
tool: cargo-nextest
- uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-debug-${{ hashFiles('**/Cargo.lock') }}
- name: Run cargo test
run: cargo nextest run --test '*'
env:
ATUIN_DB_URI: postgres://atuin:pass@localhost:5432/atuin
clippy:
runs-on: depot-ubuntu-24.04
steps:
- uses: actions/checkout@v6
- name: Install latest rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.94.0
components: clippy
- uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-debug-${{ hashFiles('**/Cargo.lock') }}
- name: Run clippy
run: cargo clippy -- -D warnings -D clippy::redundant_clone
format:
runs-on: depot-ubuntu-24.04
steps:
- uses: actions/checkout@v6
- name: Install latest rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.94.0
components: rustfmt
- name: Format
run: cargo fmt -- --check
```
## /.depot/workflows/shellcheck.yml
```yml path="/.depot/workflows/shellcheck.yml"
# Depot CI Migration
# Source: .github/workflows/shellcheck.yml
#
# No changes were necessary.
name: Shellcheck
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
shellcheck:
runs-on: depot-ubuntu-24.04
steps:
- uses: actions/checkout@v6
- name: Run shellcheck
uses: ludeeus/action-shellcheck@master
env:
SHELLCHECK_OPTS: "-e SC2148"
```
## /.depot/workflows/update-nix-deps.yml
```yml path="/.depot/workflows/update-nix-deps.yml"
# Depot CI Migration
# Source: .github/workflows/update-nix-deps.yml
#
# No changes were necessary.
name: Update Nix Deps
on:
workflow_dispatch: # allows manual triggering
schedule:
- cron: '0 0 1 * *' # runs monthly on the first day of the month at 00:00
jobs:
lockfile:
runs-on: depot-ubuntu-24.04
if: github.repository == 'atuinsh/atuin'
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@main
- name: Update flake.lock
uses: DeterminateSystems/update-flake-lock@main
with:
pr-title: "chore(deps): update flake.lock"
pr-labels: |
dependencies
```
## /.dockerignore
```dockerignore path="/.dockerignore"
./target
Dockerfile
```
## /.github/DISCUSSION_TEMPLATE/support.yml
```yml path="/.github/DISCUSSION_TEMPLATE/support.yml"
body:
- type: input
attributes:
label: Operating System
description: What operating system are you using?
placeholder: "Example: macOS Big Sur"
validations:
required: true
- type: input
attributes:
label: Shell
description: What shell are you using?
placeholder: "Example: zsh 5.8.1"
validations:
required: true
- type: dropdown
attributes:
label: Version
description: What version of atuin are you running?
multiple: false
options: # how often will I forget to update this? a lot.
- v17.0.0 (Default)
- v16.0.0
- v15.0.0
- v14.0.1
- v14.0.0
- v13.0.1
- v13.0.0
- v12.0.0
- v11.0.0
- v0.10.0
- v0.9.1
- v0.9.0
- v0.8.1
- v0.8.0
- v0.7.2
- v0.7.1
- v0.7.0
- v0.6.4
- v0.6.3
default: 0
validations:
required: true
- type: checkboxes
attributes:
label: Self hosted
description: Are you self hosting atuin server?
options:
- label: I am self hosting atuin server
- type: checkboxes
attributes:
label: Search the issues
description: Did you search the issues and discussions for your problem?
options:
- label: I checked that someone hasn't already asked about the same issue
required: true
- type: textarea
attributes:
label: Behaviour
description: "Please describe the issue - what you expected to happen, what actually happened"
- type: textarea
attributes:
label: Logs
description: "If possible, please include logs from atuin, especially if you self host the server - ATUIN_LOG=debug"
- type: textarea
attributes:
label: Extra information
description: "Anything else you'd like to add?"
- type: checkboxes
attributes:
label: Code of Conduct
description: The Code of Conduct helps create a safe space for everyone. We require
that everyone agrees to it.
options:
- label: I agree to follow this project's [Code of Conduct](https://github.com/atuinsh/atuin/blob/main/CODE_OF_CONDUCT.md)
required: true
```
## /.github/FUNDING.yml
```yml path="/.github/FUNDING.yml"
# These are supported funding model platforms
github: [atuinsh]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
```
## /.github/ISSUE_TEMPLATE/bug.yaml
```yaml path="/.github/ISSUE_TEMPLATE/bug.yaml"
name: Bug Report
description: File a bug report
title: "[Bug]: "
labels: ["bug", "triage"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: textarea
id: what-expected
attributes:
label: What did you expect to happen?
placeholder: Tell us what you expected to see!
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: What happened?
placeholder: Tell us what you see!
validations:
required: true
- type: textarea
id: doctor
validations:
required: true
attributes:
label: Atuin doctor output
description: Please run 'atuin doctor' and share the output. If it fails to run, share any errors. This requires Atuin >=v18.1.0
render: yaml
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/atuinsh/atuin/blob/main/CODE_OF_CONDUCT.md)
options:
- label: I agree to follow this project's Code of Conduct
required: true
```
## /.github/dependabot.yml
```yml path="/.github/dependabot.yml"
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "cargo" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
- package-ecosystem: "docker" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
```
## /.github/pull_request_template.md
<!-- Thank you for making a PR! Bug fixes are always welcome, but if you're adding a new feature or changing an existing one, we'd really appreciate if you open an issue, post on the forum, or drop in on Discord -->
## Checks
- [ ] I am happy for maintainers to push small adjustments to this PR, to speed up the review cycle
- [ ] I have checked that there are no existing pull requests for the same thing
## /.github/workflows/codespell.yml
```yml path="/.github/workflows/codespell.yml"
# Codespell configuration is within .codespellrc
---
name: Codespell
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
codespell:
name: Check for spelling errors
runs-on: depot-ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Codespell
uses: codespell-project/actions-codespell@v2
with:
# This is regenerated from commit history
# we cannot rewrite commit history, and I'd rather not correct it
# every time
exclude_file: CHANGELOG.md
```
## /.github/workflows/docker.yaml
```yaml path="/.github/workflows/docker.yaml"
name: build-docker
on:
push:
branches: [main]
tags:
- 'v*'
jobs:
publish:
concurrency:
group: ${{ github.ref }}-docker
cancel-in-progress: true
permissions:
packages: write
contents: read
id-token: write
runs-on: depot-ubuntu-24.04
steps:
- uses: actions/checkout@v6
- name: Get Repo Owner
id: get_repo_owner
run: echo "REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]')" > $GITHUB_ENV
- uses: depot/setup-action@v1
- name: Login to container Registry
uses: docker/login-action@v3
with:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
registry: ghcr.io
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ env.REPO_OWNER }}/atuin
flavor: |
latest=false
tags: |
type=ref,event=branch
type=sha,prefix=
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Build and push
uses: depot/build-push-action@v1
with:
push: true
platforms: linux/amd64,linux/arm64
file: ./Dockerfile
context: .
provenance: false
build-args: |
Version=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] || 'dev' }}
GitCommit=${{ github.sha }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
```
## /.github/workflows/installer.yml
```yml path="/.github/workflows/installer.yml"
name: Install
on:
push:
branches: [main]
pull_request:
paths: .github/workflows/installer.yml
env:
CARGO_TERM_COLOR: always
jobs:
install:
strategy:
matrix:
os: [depot-ubuntu-24.04, macos-14]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
- name: Install zsh for ubuntu
if: matrix.os == 'depot-ubuntu-24.04'
run: |
sudo apt install zsh
- name: Test install script on bash
run: |
/bin/bash -c "$(curl --proto '=https' --tlsv1.2 -sSf https://setup.atuin.sh)"
[ -d "$HOME/.atuin" ] && source $HOME/.atuin/bin/env
atuin --help
- name: Test install script on zsh
shell: zsh {0}
run: |
/bin/bash -c "$(curl --proto '=https' --tlsv1.2 -sSf https://setup.atuin.sh)"
[ -d "$HOME/.atuin" ] && source $HOME/.atuin/bin/env
atuin --help
```
## /.github/workflows/nix.yml
```yml path="/.github/workflows/nix.yml"
# Verify the Nix build is working
# Failures will usually occur due to an out of date Rust version
# That can be updated to the latest version in nixpkgs-unstable with `nix flake update`
name: Nix
on:
push:
branches: [ main ]
paths-ignore:
- 'ui/**'
pull_request:
branches: [ main ]
paths-ignore:
- 'ui/**'
jobs:
check:
runs-on: depot-ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: cachix/install-nix-action@v31
- name: Run nix flake check
run: nix flake check --print-build-logs
build-test:
runs-on: depot-ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: cachix/install-nix-action@v31
- name: Run nix build
run: nix build --print-build-logs
```
## /.github/workflows/release.yml
```yml path="/.github/workflows/release.yml"
# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist
#
# Copyright 2022-2024, axodotdev
# SPDX-License-Identifier: MIT or Apache-2.0
#
# CI that:
#
# * checks for a Git Tag that looks like a release
# * builds artifacts with dist (archives, installers, hashes)
# * uploads those artifacts to temporary workflow zip
# * on success, uploads the artifacts to a GitHub Release
#
# Note that the GitHub Release will be created with a generated
# title/body based on your changelogs.
name: Release
permissions:
"contents": "write"
# This task will run whenever you push a git tag that looks like a version
# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc.
# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where
# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION
# must be a Cargo-style SemVer Version (must have at least major.minor.patch).
#
# If PACKAGE_NAME is specified, then the announcement will be for that
# package (erroring out if it doesn't have the given version or isn't dist-able).
#
# If PACKAGE_NAME isn't specified, then the announcement will be for all
# (dist-able) packages in the workspace with that version (this mode is
# intended for workspaces with only one dist-able package, or with all dist-able
# packages versioned/released in lockstep).
#
# If you push multiple tags at once, separate instances of this workflow will
# spin up, creating an independent announcement for each one. However, GitHub
# will hard limit this to 3 tags per commit, as it will assume more tags is a
# mistake.
#
# If there's a prerelease-style suffix to the version, then the release(s)
# will be marked as a prerelease.
on:
pull_request:
push:
tags:
- '**[0-9]+.[0-9]+.[0-9]+*'
jobs:
# Run 'dist plan' (or host) to determine what tasks we need to do
plan:
runs-on: "ubuntu-22.04"
outputs:
val: ${{ steps.plan.outputs.manifest }}
tag: ${{ !github.event.pull_request && github.ref_name || '' }}
tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }}
publishing: ${{ !github.event.pull_request }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
- name: Install dist
# we specify bash to get pipefail; it guards against the `curl` command
# failing. otherwise `sh` won't catch that `curl` returned non-0
shell: bash
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.31.0/cargo-dist-installer.sh | sh"
- name: Cache dist
uses: actions/upload-artifact@v6
with:
name: cargo-dist-cache
path: ~/.cargo/bin/dist
# sure would be cool if github gave us proper conditionals...
# so here's a doubly-nested ternary-via-truthiness to try to provide the best possible
# functionality based on whether this is a pull_request, and whether it's from a fork.
# (PRs run on the *source* but secrets are usually on the *target* -- that's *good*
# but also really annoying to build CI around when it needs secrets to work right.)
- id: plan
run: |
dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json
echo "dist ran successfully"
cat plan-dist-manifest.json
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@v6
with:
name: artifacts-plan-dist-manifest
path: plan-dist-manifest.json
# Build and packages all the platform-specific things
build-local-artifacts:
name: build-local-artifacts (${{ join(matrix.targets, ', ') }})
# Let the initial task tell us to not run (currently very blunt)
needs:
- plan
if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
strategy:
fail-fast: false
# Target platforms/runners are computed by dist in create-release.
# Each member of the matrix has the following arguments:
#
# - runner: the github runner
# - dist-args: cli flags to pass to dist
# - install-dist: expression to run to install dist on the runner
#
# Typically there will be:
# - 1 "global" task that builds universal installers
# - N "local" tasks that build each platform's binaries and platform-specific installers
matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
runs-on: ${{ matrix.runner }}
container: ${{ matrix.container && matrix.container.image || null }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
permissions:
"attestations": "write"
"contents": "read"
"id-token": "write"
steps:
- name: enable windows longpaths
run: |
git config --global core.longpaths true
- uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
- name: Install Rust non-interactively if not already installed
if: ${{ matrix.container }}
run: |
if ! command -v cargo > /dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
fi
- name: Install dist
run: ${{ matrix.install_dist.run }}
# Get the dist-manifest
- name: Fetch local artifacts
uses: actions/download-artifact@v7
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- name: Install dependencies
run: |
${{ matrix.packages_install }}
- name: Build artifacts
run: |
# Actually do builds and make zips and whatnot
dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
echo "dist ran successfully"
- name: Attest
uses: actions/attest-build-provenance@v3
with:
subject-path: "target/distrib/*${{ join(matrix.targets, ', ') }}*"
- id: cargo-dist
name: Post-build
# We force bash here just because github makes it really hard to get values up
# to "real" actions without writing to env-vars, and writing to env-vars has
# inconsistent syntax between shell and powershell.
shell: bash
run: |
# Parse out what we just built and upload it to scratch storage
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
- name: "Upload artifacts"
uses: actions/upload-artifact@v6
with:
name: artifacts-build-local-${{ join(matrix.targets, '_') }}
path: |
${{ steps.cargo-dist.outputs.paths }}
${{ env.BUILD_MANIFEST_NAME }}
# Build and package all the platform-agnostic(ish) things
build-global-artifacts:
needs:
- plan
- build-local-artifacts
runs-on: "ubuntu-22.04"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
- name: Install cached dist
uses: actions/download-artifact@v7
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/dist
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
- name: Fetch local artifacts
uses: actions/download-artifact@v7
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- id: cargo-dist
shell: bash
run: |
dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
echo "dist ran successfully"
# Parse out what we just built and upload it to scratch storage
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
- name: "Upload artifacts"
uses: actions/upload-artifact@v6
with:
name: artifacts-build-global
path: |
${{ steps.cargo-dist.outputs.paths }}
${{ env.BUILD_MANIFEST_NAME }}
# Determines if we should publish/announce
host:
needs:
- plan
- build-local-artifacts
- build-global-artifacts
# Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine)
if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
runs-on: "ubuntu-22.04"
outputs:
val: ${{ steps.host.outputs.manifest }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
- name: Install cached dist
uses: actions/download-artifact@v7
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/dist
# Fetch artifacts from scratch-storage
- name: Fetch artifacts
uses: actions/download-artifact@v7
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- id: host
shell: bash
run: |
dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
echo "artifacts uploaded and released successfully"
cat dist-manifest.json
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@v6
with:
# Overwrite the previous copy
name: artifacts-dist-manifest
path: dist-manifest.json
# Create a GitHub Release while uploading all files to it
- name: "Download GitHub Artifacts"
uses: actions/download-artifact@v7
with:
pattern: artifacts-*
path: artifacts
merge-multiple: true
- name: Cleanup
run: |
# Remove the granular manifests
rm -f artifacts/*-dist-manifest.json
- name: Create GitHub Release
env:
PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}"
ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}"
ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}"
RELEASE_COMMIT: "${{ github.sha }}"
run: |
# Write and read notes from a file to avoid quoting breaking things
echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt
gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/*
announce:
needs:
- plan
- host
# use "always() && ..." to allow us to wait for all publish jobs while
# still allowing individual publish jobs to skip themselves (for prereleases).
# "host" however must run to completion, no skipping allowed!
if: ${{ always() && needs.host.result == 'success' }}
runs-on: "ubuntu-22.04"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
```
## /.github/workflows/rust.yml
```yml path="/.github/workflows/rust.yml"
name: Rust
on:
push:
branches: [main]
paths-ignore:
- "ui/**"
pull_request:
branches: [main]
paths-ignore:
- "ui/**"
env:
CARGO_TERM_COLOR: always
jobs:
build:
strategy:
matrix:
os: [depot-ubuntu-24.04, macos-14, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
- name: Install rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.95.0
- uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }}
- name: Run cargo build common
run: cargo build -p atuin-common --locked --release
- name: Run cargo build client
run: cargo build -p atuin-client --locked --release
- name: Run cargo build server
run: cargo build -p atuin-server --locked --release
- name: Run cargo build main
run: cargo build --all --locked --release
cross-compile:
strategy:
matrix:
# There was an attempt to make cross-compiles also work on FreeBSD, but that failed with:
#
# warning: libelf.so.2, needed by <...>/libkvm.so, not found (try using -rpath or -rpath-link)
target: [x86_64-unknown-illumos]
runs-on: depot-ubuntu-24.04
steps:
- uses: actions/checkout@v6
- name: Install cross
uses: taiki-e/install-action@v2
with:
tool: cross
- uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ matrix.target }}-cross-compile-${{ hashFiles('**/Cargo.lock') }}
- name: Run cross build common
run: cross build -p atuin-common --locked --target ${{ matrix.target }}
- name: Run cross build client
run: cross build -p atuin-client --locked --target ${{ matrix.target }}
- name: Run cross build server
run: cross build -p atuin-server --locked --target ${{ matrix.target }}
- name: Run cross build main
run: |
cross build --all --locked --target ${{ matrix.target }}
unit-test:
strategy:
matrix:
os: [depot-ubuntu-24.04, macos-14, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
- name: Install rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.95.0
- uses: taiki-e/install-action@v2
name: Install nextest
with:
tool: cargo-nextest
- uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-debug-${{ hashFiles('**/Cargo.lock') }}
- name: Run cargo test
run: cargo nextest run --lib --bins
check:
strategy:
matrix:
os: [depot-ubuntu-24.04, macos-14, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
- name: Install rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.95.0
- uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-debug-${{ hashFiles('**/Cargo.lock') }}
- name: Run cargo check (all features)
run: cargo check --all-features --workspace
- name: Run cargo check (no features)
run: cargo check --no-default-features --workspace
- name: Run cargo check (sync)
run: cargo check --no-default-features --features sync --workspace
- name: Run cargo check (server)
run: cargo check -p atuin-server
- name: Run cargo check (client only)
run: cargo check --no-default-features --features client --workspace
integration-test:
runs-on: depot-ubuntu-24.04
services:
postgres:
image: postgres
env:
POSTGRES_USER: atuin
POSTGRES_PASSWORD: pass
POSTGRES_DB: atuin
ports:
- 5432:5432
steps:
- uses: actions/checkout@v6
- name: Install rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.95.0
- uses: taiki-e/install-action@v2
name: Install nextest
with:
tool: cargo-nextest
- uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-debug-${{ hashFiles('**/Cargo.lock') }}
- name: Run cargo test
run: cargo nextest run --test '*'
env:
ATUIN_DB_URI: postgres://atuin:pass@localhost:5432/atuin
clippy:
runs-on: depot-ubuntu-24.04
steps:
- uses: actions/checkout@v6
- name: Install latest rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.95.0
components: clippy
- uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-debug-${{ hashFiles('**/Cargo.lock') }}
- name: Run clippy
run: cargo clippy -- -D warnings -D clippy::redundant_clone
format:
runs-on: depot-ubuntu-24.04
steps:
- uses: actions/checkout@v6
- name: Install latest rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.95.0
components: rustfmt
- name: Format
run: cargo fmt -- --check
```
## /.github/workflows/shellcheck.yml
```yml path="/.github/workflows/shellcheck.yml"
name: Shellcheck
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
shellcheck:
runs-on: depot-ubuntu-24.04
steps:
- uses: actions/checkout@v6
- name: Run shellcheck
uses: ludeeus/action-shellcheck@master
env:
SHELLCHECK_OPTS: "-e SC2148"
```
## /.github/workflows/update-nix-deps.yml
```yml path="/.github/workflows/update-nix-deps.yml"
name: Update Nix Deps
on:
workflow_dispatch: # allows manual triggering
schedule:
- cron: '0 0 1 * *' # runs monthly on the first day of the month at 00:00
jobs:
lockfile:
runs-on: depot-ubuntu-24.04
if: github.repository == 'atuinsh/atuin'
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@main
- name: Update flake.lock
uses: DeterminateSystems/update-flake-lock@main
with:
pr-title: "chore(deps): update flake.lock"
pr-labels: |
dependencies
```
## /.gitignore
```gitignore path="/.gitignore"
.DS_Store
/target
*/target
.env
.idea/
.vscode/
result
publish.sh
.envrc
.planning/
ui/backend/target
ui/backend/gen
sqlite-server.db*
.atuin/permissions.*.toml
```
## /.mailmap
```mailmap path="/.mailmap"
networkException <git@nwex.de> <github@nwex.de>
Violet Shreve <github@shreve.io> <jacob@shreve.io>
Chris Rose <offline@offby1.net> <offbyone@github.com>
Conrad Ludgate <conradludgate@gmail.com> <conrad.ludgate@truelayer.com>
Cristian Le <github@lecris.me> <cristian.le@mpsd.mpg.de>
Dennis Trautwein <git@dtrautwein.eu> <dennis.trautwein@posteo.de>
Ellie Huxtable <ellie@atuin.sh> <e@elm.sh>
Ellie Huxtable <ellie@atuin.sh> <ellie@elliehuxtable.com>
Frank Hamand <frankhamand@gmail.com> <frank.hamand@coinbase.com>
Jakob Schrettenbrunner <dev@schrej.net> <jakob.schrettenbrunner@telekom.de>
Nemo157 <git@nemo157.com> <github@nemo157.com>
Richard de Boer <git@tubul.net> <github@tubul.net>
Sandro <sandro.jaeckel@gmail.com> <sandro.jaeckel@sap.com>
TymanWasTaken <tbeckman530@gmail.com> <ty@blahaj.land>
```
## /.rustfmt.toml
```toml path="/.rustfmt.toml"
reorder_imports = true
# uncomment once stable
#imports_granularity = "crate"
#group_imports = "StdExternalCrate"
```
## /AGENTS.md
# Atuin
Shell history tool. Replaces your shell's built-in history with a SQLite database, adds context (cwd, exit code, duration, hostname), and optionally syncs across machines with end-to-end encryption.
## Workspace crates
```
atuin CLI binary + TUI (clap, ratatui, crossterm)
atuin-client Client library: local DB, encryption, sync, settings
atuin-common Shared types, API models, utils
atuin-daemon Background gRPC daemon (tonic) for shell hooks
atuin-dotfiles Alias/var sync via record store
atuin-history Sorting algorithms, stats
atuin-kv Key-value store (synced)
atuin-scripts Script management (minijinja)
atuin-server HTTP sync server (axum) - lib + standalone binary
atuin-server-database Database trait for server
atuin-server-postgres Postgres implementation (sqlx)
atuin-server-sqlite SQLite implementation (sqlx)
```
## Two sync protocols
- **V1 (legacy)**: Syncs history entries directly. Being phased out. Toggleable via `sync_v1_enabled`.
- **V2 (current)**: Record store abstraction. All data types (history, KV, aliases, vars, scripts) share the same sync infrastructure using tagged records. Envelope-encrypted with PASETO V4 and per-record CEKs.
## Encryption
- **V1**: XSalsa20Poly1305 (secretbox). Key at `~/.local/share/atuin/key`.
- **V2**: PASETO V4 Local (XChaCha20-Poly1305 + Blake2b). Envelope encryption: each record gets a random CEK wrapped with the master key. Record metadata (id, idx, version, tag, host) is authenticated as implicit assertions.
## Databases
- **Client**: SQLite everywhere. Separate DBs for history, record store, KV, scripts. All use sqlx + WAL mode.
- **Server**: Postgres (primary) or SQLite. Auto-detected from URI prefix.
- Migrations live alongside each crate. Never modify existing migrations, only add new ones.
## Hot paths
`history start`, `history end`, and `init` skip database initialization for latency. Don't add DB calls to these without good reason.
## Conventions
- Rust 2024 edition, toolchain 1.93.1.
- Errors: `eyre::Result` in binaries, `thiserror` for typed errors in libraries.
- Async: tokio. Client uses `current_thread`; server uses `multi_thread`.
- `#![deny(unsafe_code)]` on client/common, `#![forbid(unsafe_code)]` on server.
- Clippy: `pedantic` + `nursery` on main crate. CI enforces `-D warnings -D clippy::redundant_clone`.
- Format: `cargo fmt`. Only non-default: `reorder_imports = true`.
- IDs: UUIDv7 (time-ordered), newtype wrappers (`HistoryId`, `RecordId`, `HostId`).
- Serialization: MessagePack for encrypted payloads, JSON for API, TOML for config.
- Storage traits: `Database` (client), `Store` (record store), `Database` (server) -- all `async_trait`.
- History builders: `HistoryImported`, `HistoryCaptured`, `HistoryFromDb` with compile-time field validation.
- Feature flags: `client`, `sync`, `daemon`, `clipboard`, `check-update`.
## Testing
- Unit tests inline with `#[cfg(test)]`, async via `#[tokio::test]`.
- Integration tests in `crates/atuin/tests/` need Postgres (`ATUIN_DB_URI` env var).
- Use `":memory:"` SQLite for unit tests needing a database.
- Runner: `cargo nextest`.
- Benchmarks: `divan` in `atuin-history`.
## Build and check
```sh
cargo build
cargo test
cargo clippy -- -D warnings
cargo fmt --check
```
## /CHANGELOG.md
# Changelog
All notable changes to this project will be documented in this file.
## 18.15.2
### Bug Fixes
- Tab doesn't insert suggested command ([#3420](https://github.com/atuinsh/atuin/issues/3420))
## 18.15.1
### Bug Fixes
- Enter runs suggested command when selecting permissions ([#3418](https://github.com/atuinsh/atuin/issues/3418))
## 18.15.0
### Bug Fixes
- Install script incorrectly tries to install opencode hooks ([#3410](https://github.com/atuinsh/atuin/issues/3410))
- Dependency fix ([#3414](https://github.com/atuinsh/atuin/issues/3414))
- Loss of loading spinners + tokio panic on exit ([#3415](https://github.com/atuinsh/atuin/issues/3415))
### Features
- Add OCI standard labels to Dockerfile ([#3412](https://github.com/atuinsh/atuin/issues/3412))
- Enable atuin hex for illumos ([#3413](https://github.com/atuinsh/atuin/issues/3413))
- Allow resuming previous AI sessions ([#3407](https://github.com/atuinsh/atuin/issues/3407))
### Miscellaneous Tasks
- Add release script ([#3411](https://github.com/atuinsh/atuin/issues/3411))
## 18.14.1
### Bug Fixes
- Ensure we can publish to crates ([#3403](https://github.com/atuinsh/atuin/issues/3403))
- Thread remote and content_length through system for server tool calls ([#3404](https://github.com/atuinsh/atuin/issues/3404))
### Documentation
- Add Tools & Permissions doc section ([#3402](https://github.com/atuinsh/atuin/issues/3402))
## 18.14.0
### Bug Fixes
- *(ui)* Make preview line breaking algorithm aware of CJK double-width characters ([#3360](https://github.com/atuinsh/atuin/issues/3360))
- *(ui)* When inverted, invert scroll events handling ([#3373](https://github.com/atuinsh/atuin/issues/3373))
- Replace `e>|` with `|` in nushell integration to restore history recording ([#3358](https://github.com/atuinsh/atuin/issues/3358))
- Resolve git worktrees to main repo in workspace filter ([#3366](https://github.com/atuinsh/atuin/issues/3366))
- Ensure daemon is running ([#3384](https://github.com/atuinsh/atuin/issues/3384))
### Documentation
- Remove docker-compose duplication ([#3376](https://github.com/atuinsh/atuin/issues/3376))
- Cover prefix mode properly ([#3383](https://github.com/atuinsh/atuin/issues/3383))
- Minor readability improvement to README ([#3381](https://github.com/atuinsh/atuin/issues/3381))
### Features
- Opt-in to sharing last command with ai ([#3367](https://github.com/atuinsh/atuin/issues/3367))
- Add 'atuin config' subcommand for reading and setting config values ([#3368](https://github.com/atuinsh/atuin/issues/3368))
- Option to disable mouse support ([#3372](https://github.com/atuinsh/atuin/issues/3372))
- Add support for deleting all matching commands via keybindings ([#3375](https://github.com/atuinsh/atuin/issues/3375))
- Add strip_trailing_whitespace, on by default ([#3390](https://github.com/atuinsh/atuin/issues/3390))
- Client-tool execution + permission system ([#3370](https://github.com/atuinsh/atuin/issues/3370))
- Add history tail for live monitoring view ([#3389](https://github.com/atuinsh/atuin/issues/3389))
- Track coding agent shell usage ([#3388](https://github.com/atuinsh/atuin/issues/3388))
- Remove agent search from tui ([#3397](https://github.com/atuinsh/atuin/issues/3397))
- Add pi hook installer ([#3398](https://github.com/atuinsh/atuin/issues/3398))
- Autoinstall ai shell history hooks ([#3399](https://github.com/atuinsh/atuin/issues/3399))
### Miscellaneous Tasks
- Update to eye-declare 0.3.0 ([#3365](https://github.com/atuinsh/atuin/issues/3365))
- Prepare 18.14.0-beta.1 release ([#3393](https://github.com/atuinsh/atuin/issues/3393))
### Refactor
- Rename examples -> contrib ([#3400](https://github.com/atuinsh/atuin/issues/3400))
## 18.13.6
### Bug Fixes
- *(powershell)* Handle non-FileSystem drives ([#3353](https://github.com/atuinsh/atuin/issues/3353))
- Remove unnecessary arboard/image-data default feature ([#3345](https://github.com/atuinsh/atuin/issues/3345))
- Use printf to append fish shell init block ([#3346](https://github.com/atuinsh/atuin/issues/3346))
- Set WorkingDirectory in PowerShell Invoke-AtuinSearch ([#3351](https://github.com/atuinsh/atuin/issues/3351))
### Features
- Use eye-declare for more performant and flexible AI TUI ([#3343](https://github.com/atuinsh/atuin/issues/3343))
### Miscellaneous Tasks
- *(ci)* Switch most workflows to depot ci ([#3352](https://github.com/atuinsh/atuin/issues/3352))
- Prepare 18.13.6 release ([#3356](https://github.com/atuinsh/atuin/issues/3356))
## 18.13.5
### Bug Fixes
- Atuin Hex fails to init on bash and zsh ([#3341](https://github.com/atuinsh/atuin/issues/3341))
### Documentation
- Fix duplicated word in Kubernetes guide ([#3338](https://github.com/atuinsh/atuin/issues/3338))
### Miscellaneous Tasks
- Prepare 18.13.5 ([#3342](https://github.com/atuinsh/atuin/issues/3342))
## 18.13.4
### Bug Fixes
- *(ai)* Restore url-quote-magic for ? in zsh ([#3304](https://github.com/atuinsh/atuin/issues/3304))
- Redirect tty0 when running setup
- Redirect tty0 when running setup ([#3302](https://github.com/atuinsh/atuin/issues/3302))
- Call ensure_hub_session even if primary sync endpoint is self-hosted
- Call ensure_hub_session even if primary sync endpoint is self-hosted ([#3301](https://github.com/atuinsh/atuin/issues/3301))
- Remove per-event mouse capture toggling that leaked ANSI to stdout ([#3299](https://github.com/atuinsh/atuin/issues/3299))
- Clarify what data is sent when using Atuin AI during setup (only OS and shell) ([#3290](https://github.com/atuinsh/atuin/issues/3290))
- Better tty check ([#3313](https://github.com/atuinsh/atuin/issues/3313))
- Disable features in init when that feature is explicitly disabled ([#3328](https://github.com/atuinsh/atuin/issues/3328))
- Don't run 'atuin init' in 'atuin hex init' — each must be initialized separately ([#3334](https://github.com/atuinsh/atuin/issues/3334))
### Documentation
- Fix typo in FAQ alternatives section ([#3292](https://github.com/atuinsh/atuin/issues/3292))
- Remove 'experimental' status from Atuin Daemon
- Remove 'experimental' status from Atuin Daemon ([#3295](https://github.com/atuinsh/atuin/issues/3295))
- Add inline_height_shell_up_key_binding ([#3270](https://github.com/atuinsh/atuin/issues/3270))
### Features
- Report distro name with OS for distro-specific commands ([#3289](https://github.com/atuinsh/atuin/issues/3289))
- Allow setting kv values from stdin
- Error if value not provided and no stdin
- Add a small atuin label to the ai box ([#3309](https://github.com/atuinsh/atuin/issues/3309))
- Allow running `atuin search -i` as subcommand on Windows ([#3250](https://github.com/atuinsh/atuin/issues/3250))
- Hex init nu ([#3330](https://github.com/atuinsh/atuin/issues/3330))
### Miscellaneous Tasks
- *(ci)* Tag docker images with semantic versions on tag creation ([#3316](https://github.com/atuinsh/atuin/issues/3316))
- Replace atuin-ai rendering with component-oriented system ([#3288](https://github.com/atuinsh/atuin/issues/3288))
- Refactor CLI auth flows and token storage ([#3317](https://github.com/atuinsh/atuin/issues/3317))
## 18.13.3
### Bug Fixes
- Nushell 0.111; future Nushell 0.112 support ([#3266](https://github.com/atuinsh/atuin/issues/3266))
### Features
- Call atuin setup from install script ([#3265](https://github.com/atuinsh/atuin/issues/3265))
- Allow headless account ops against Hub server ([#3280](https://github.com/atuinsh/atuin/issues/3280))
- Add custom filtering and scoring mechanisms
### Miscellaneous Tasks
- *(ci)* Migrate to depot runners ([#3279](https://github.com/atuinsh/atuin/issues/3279))
- *(ci)* Use depot to build docker images too ([#3281](https://github.com/atuinsh/atuin/issues/3281))
- *(ci)* Use github for macos
- Update changelog
- Update permissions in Docker workflow ([#3283](https://github.com/atuinsh/atuin/issues/3283))
- Change CHANGELOG format to be easier to parse
- Symlink changelog so dist can pick it up
- Vendor nucleo-ext + fork, so we can depend on our changes properly ([#3284](https://github.com/atuinsh/atuin/issues/3284))
- Update changelog
## 18.13.2
### Miscellaneous Tasks
- *(release)* Building windows aarch64 was overly optimistic
- Update changelog
## 18.13.1
### Miscellaneous Tasks
- *(release)* Update dist, remove custom runners
- Update changelog
## 18.13.0
### Bug Fixes
- *(deps)* Add use-dev-tty to crossterm in atuin-ai ([#3185](https://github.com/atuinsh/atuin/issues/3185))
- *(docs)* Update Postgres volume path in Docker as required by pg18 ([#3174](https://github.com/atuinsh/atuin/issues/3174))
- Systemd Exec for separate server binary ([#3176](https://github.com/atuinsh/atuin/issues/3176))
- Multiline commands with fish ([#3179](https://github.com/atuinsh/atuin/issues/3179))
- Silent DB failures e.g. when disk is full ([#3183](https://github.com/atuinsh/atuin/issues/3183))
- Forward $PATH to tmux popup in zsh ([#3198](https://github.com/atuinsh/atuin/issues/3198))
- Dramatically decrease daemon memory usage ([#3211](https://github.com/atuinsh/atuin/issues/3211))
- Regen cargo dist
- Clear script database before rebuild to prevent unique constraint violation ([#3232](https://github.com/atuinsh/atuin/issues/3232))
- Support Nushell 0.111 ([#3249](https://github.com/atuinsh/atuin/issues/3249))
- Ctrl-c not exiting ai ([#3256](https://github.com/atuinsh/atuin/issues/3256))
### Documentation
- Update config.md to remove NuShell support note ([#3190](https://github.com/atuinsh/atuin/issues/3190))
- Document `search.filters` ([#3195](https://github.com/atuinsh/atuin/issues/3195))
- Clean up doc references for sqlite-based self-hosting ([#3216](https://github.com/atuinsh/atuin/issues/3216))
- Document daemon-fuzzy search mode ([#3254](https://github.com/atuinsh/atuin/issues/3254))
### Features
- *(docs)* Add Shell Integration and Interoperability docs ([#3163](https://github.com/atuinsh/atuin/issues/3163))
- `switch-context` ([#3149](https://github.com/atuinsh/atuin/issues/3149))
- Add Hub authentication for future sync + extra features ([#3010](https://github.com/atuinsh/atuin/issues/3010))
- Add Atuin AI inline CLI MVP ([#3178](https://github.com/atuinsh/atuin/issues/3178))
- Add autostart and pid management to daemon ([#3180](https://github.com/atuinsh/atuin/issues/3180))
- Generate commands or ask questions with `atuin ai` ([#3199](https://github.com/atuinsh/atuin/issues/3199))
- Add history author/intent metadata and v1 record version ([#3205](https://github.com/atuinsh/atuin/issues/3205))
- In-memory search index with atuin daemon ([#3201](https://github.com/atuinsh/atuin/issues/3201))
- Update script for smoother setup ([#3230](https://github.com/atuinsh/atuin/issues/3230))
- Initial draft of atuin-shell ([#3206](https://github.com/atuinsh/atuin/issues/3206))
- Allow setting multipliers for frequency, recency, and frecency scores ([#3235](https://github.com/atuinsh/atuin/issues/3235))
- Allow running `atuin search -i` as subcommand ([#3208](https://github.com/atuinsh/atuin/issues/3208))
- Use pty proxy for rendering tui popups without clearing the terminal ([#3234](https://github.com/atuinsh/atuin/issues/3234))
- Allow authenticating with Atuin Hub ([#3237](https://github.com/atuinsh/atuin/issues/3237))
- Initialize Atuin AI by default with `atuin init` ([#3255](https://github.com/atuinsh/atuin/issues/3255))
- Add `atuin setup` ([#3257](https://github.com/atuinsh/atuin/issues/3257))
### Miscellaneous Tasks
- Update changelog
- Update changelog
- Update changelog
- Use workspace versions ([#3210](https://github.com/atuinsh/atuin/issues/3210))
- Move atuin ai subcommand into core binary ([#3212](https://github.com/atuinsh/atuin/issues/3212))
- Update changelog
- Update to Rust 1.94 ([#3247](https://github.com/atuinsh/atuin/issues/3247))
- Strip symbols in dist profile to reduce binary size
- Upgrade thiserror 1.x to 2.x to deduplicate dependency
- Upgrade axum 0.7 to 0.8 to deduplicate with tonic's axum
- Update changelog
- Update changelog
- Update changelog
- Update changelog
## 18.12.1
### Bug Fixes
- *(shell)* Fix ATUIN_SESSION errors in tmux popup ([#3170](https://github.com/atuinsh/atuin/issues/3170))
- *(tui)* Enter in vim normal mode, shift-tab keybind ([#3158](https://github.com/atuinsh/atuin/issues/3158))
- Server start commands for Docker. ([#3160](https://github.com/atuinsh/atuin/issues/3160))
### Features
- Expand keybinding system with vim motions, media keys, and inspector improvements ([#3161](https://github.com/atuinsh/atuin/issues/3161))
- Add original-input-empty keybind condition ([#3171](https://github.com/atuinsh/atuin/issues/3171))
### Miscellaneous Tasks
- Update changelog
## 18.12.0
### Bug Fixes
- *(powershell)* Preserve `$LASTEXITCODE` ([#3120](https://github.com/atuinsh/atuin/issues/3120))
- *(powershell)* Display search stderr ([#3146](https://github.com/atuinsh/atuin/issues/3146))
- *(search)* Allow hyphen-prefixed query args like `---` ([#3129](https://github.com/atuinsh/atuin/issues/3129))
- *(tui)* Space and F1-F24 keys not handled properly by new keybind system ([#3138](https://github.com/atuinsh/atuin/issues/3138))
- *(ui)* Don't draw a leading space for command
- *(ui)* Time column can take up to 9 cells
- *(ui)* Align cursor with the expand column (usually the command)
- *(ui)* Align cursor when expand column is in the middle ([#3103](https://github.com/atuinsh/atuin/issues/3103))
- Zsh import multiline issue ([#2799](https://github.com/atuinsh/atuin/issues/2799))
- Do not hit sync v1 endpoints for status
- Do not hit sync v1 endpoints for status ([#3102](https://github.com/atuinsh/atuin/issues/3102))
- Do not set ATUIN_SESSION if it is already set ([#3107](https://github.com/atuinsh/atuin/issues/3107))
- Custom data dir test on windows ([#3109](https://github.com/atuinsh/atuin/issues/3109))
- New session on shlvl change ([#3111](https://github.com/atuinsh/atuin/issues/3111))
- Larger exit column width on Windows ([#3119](https://github.com/atuinsh/atuin/issues/3119))
- Halt sync loop if server returns an empty page ([#3122](https://github.com/atuinsh/atuin/issues/3122))
- Use directories crate for home dir resolution ([#3125](https://github.com/atuinsh/atuin/issues/3125))
- Tab behaving like enter, eprintln ([#3135](https://github.com/atuinsh/atuin/issues/3135))
- Issue with shift and modifier keys ([#3143](https://github.com/atuinsh/atuin/issues/3143))
- Remove invalid IF EXISTS from sqlite drop column migration ([#3145](https://github.com/atuinsh/atuin/issues/3145))
### Documentation
- *(CONTRIBUTING)* Update links ([#3117](https://github.com/atuinsh/atuin/issues/3117))
- *(README)* Update links ([#3116](https://github.com/atuinsh/atuin/issues/3116))
- *(config)* Clarify scope of directory filter_mode ([#3082](https://github.com/atuinsh/atuin/issues/3082))
- *(configuration)* Describe new utility "atuin-bind" for Bash ([#3064](https://github.com/atuinsh/atuin/issues/3064))
- *(installation)* Add mise alternative installation method ([#3066](https://github.com/atuinsh/atuin/issues/3066))
- Various improvements to the `atuin import` docs ([#3062](https://github.com/atuinsh/atuin/issues/3062))
- Disambiguate 'setup' (noun) vs. 'set up' (verb) ([#3061](https://github.com/atuinsh/atuin/issues/3061))
- Fix punctuation and grammar in basic usage guide ([#3063](https://github.com/atuinsh/atuin/issues/3063))
- Expand and clarify usage of the history prune command ([#3084](https://github.com/atuinsh/atuin/issues/3084))
- Small edit to themes website file ([#3069](https://github.com/atuinsh/atuin/issues/3069))
- Config/ with initial uid:gid
- Add PowerShell install instructions
- Add PowerShell and Windows install instructions ([#3096](https://github.com/atuinsh/atuin/issues/3096))
- Update the `[keys]` docs ([#3114](https://github.com/atuinsh/atuin/issues/3114))
- Add history deletion guide ([#3130](https://github.com/atuinsh/atuin/issues/3130))
- Update how to use Docker to self-host ([#3148](https://github.com/atuinsh/atuin/issues/3148))
- Add IRC contact information to README
### Features
- *(dotfiles)* Add sort and filter options to alias/var list ([#3131](https://github.com/atuinsh/atuin/issues/3131))
- *(theme)* Note new default theme name and syntax ([#3080](https://github.com/atuinsh/atuin/issues/3080))
- *(tui)* Add clear-to-start/end actions ([#3141](https://github.com/atuinsh/atuin/issues/3141))
- *(ui)* Highlight fulltext search as fulltext search instead of fuzzy search
- *(ui)* Highlight fulltext search as fulltext search instead of fuzzy search ([#3098](https://github.com/atuinsh/atuin/issues/3098))
- *(ultracompact)* Adds setting for ultracompact mode ([#3079](https://github.com/atuinsh/atuin/issues/3079))
- Add custom column support ([#3089](https://github.com/atuinsh/atuin/issues/3089))
- Left arrow/backspace on empty to start edit ([#3090](https://github.com/atuinsh/atuin/issues/3090))
- Add more vim movement bindings for navigation ([#3041](https://github.com/atuinsh/atuin/issues/3041))
- Support setting a custom data dir in config ([#3105](https://github.com/atuinsh/atuin/issues/3105))
- Remove user verification functionality ([#3108](https://github.com/atuinsh/atuin/issues/3108))
- Add option to use tmux display-popup ([#3058](https://github.com/atuinsh/atuin/issues/3058))
- Move atuin-server to its own binary ([#3112](https://github.com/atuinsh/atuin/issues/3112))
- Add a parameter to the sync to specify the download/upload page ([#2408](https://github.com/atuinsh/atuin/issues/2408))
- Replace several files with a sqlite db ([#3128](https://github.com/atuinsh/atuin/issues/3128))
- Add new custom keybinding system for search TUI ([#3127](https://github.com/atuinsh/atuin/issues/3127))
### Miscellaneous Tasks
- Remove total_history from api index response ([#3094](https://github.com/atuinsh/atuin/issues/3094))
- **BREAKING**: remove total_history from api index response ([#3094](https://github.com/atuinsh/atuin/issues/3094))
- Update to rust 1.93
- Update to rust 1.93 ([#3101](https://github.com/atuinsh/atuin/issues/3101))
- Update changelog
- Update agents.md ([#3126](https://github.com/atuinsh/atuin/issues/3126))
- Update changelog
- Update changelog
- Update changelog
### Theming
- Explain how to set ANSI codes directly ([#3065](https://github.com/atuinsh/atuin/issues/3065))
### Faq
- Add alternative projects ([#3076](https://github.com/atuinsh/atuin/issues/3076))
## 18.11.0
### Bug Fixes
- *(bash)* Fix issues with intermediate key sequences in the vi editing mode ([#2977](https://github.com/atuinsh/atuin/issues/2977))
- *(bash)* Work around a keybinding bug of Bash 5.1 ([#2975](https://github.com/atuinsh/atuin/issues/2975))
- *(bash/blesh)* Suppress error message for auto-complete source ([#2976](https://github.com/atuinsh/atuin/issues/2976))
- *(powershell)* Run `atuin history end` in the background ([#3034](https://github.com/atuinsh/atuin/issues/3034))
- *(powershell)* Add error safety and cleanup ([#3040](https://github.com/atuinsh/atuin/issues/3040))
- Highlight the correct place when multibyte characters are involved ([#2965](https://github.com/atuinsh/atuin/issues/2965))
- Prevent interactive search crash when update check fails ([#3016](https://github.com/atuinsh/atuin/issues/3016))
- Move thorough search through search.filters w/ workspaces ([#2703](https://github.com/atuinsh/atuin/issues/2703))
### Documentation
- Migrate docs from separate repo to `docs` subfolder ([#3018](https://github.com/atuinsh/atuin/issues/3018))
### Features
- Support additional history filenames in replxx importer ([#3005](https://github.com/atuinsh/atuin/issues/3005))
- Add colors to --help/-h ([#3000](https://github.com/atuinsh/atuin/issues/3000))
- Add support for read replicas to postgres ([#3029](https://github.com/atuinsh/atuin/issues/3029))
- Allow disabling sync v1 ([#3030](https://github.com/atuinsh/atuin/issues/3030))
- Consider atuin dotfile aliases when calculating atuin wrapped ([#3048](https://github.com/atuinsh/atuin/issues/3048))
- Add session and uuid column support to history list ([#3049](https://github.com/atuinsh/atuin/issues/3049))
### Miscellaneous Tasks
- *(nix)* Prevent deprecation warning on evaluation ([#3006](https://github.com/atuinsh/atuin/issues/3006))
- Update changelog
- Adjust update wording ([#2974](https://github.com/atuinsh/atuin/issues/2974))
- Add Windows builds, second try ([#2966](https://github.com/atuinsh/atuin/issues/2966))
- Update to rust 1.91 ([#2981](https://github.com/atuinsh/atuin/issues/2981))
- Add Atuin Desktop information to install script
- Remove trailing whitespace ([#2985](https://github.com/atuinsh/atuin/issues/2985))
- Fix typo ([#2994](https://github.com/atuinsh/atuin/issues/2994))
- Clarify docstring of the enter_accept config key ([#3003](https://github.com/atuinsh/atuin/issues/3003))
- Fix github action syntax for variables ([#2998](https://github.com/atuinsh/atuin/issues/2998))
- Add AGENTS.md
- Update changelog
- Remove x86_64 mac from build targets ([#3052](https://github.com/atuinsh/atuin/issues/3052))
### Build
- *(nix)* Update rust toolchain hash ([#2990](https://github.com/atuinsh/atuin/issues/2990))
## 18.10.0
### Bug Fixes
- Stats ngram window size cli parsing ([#2946](https://github.com/atuinsh/atuin/issues/2946))
### Features
- *(bash)* Use Readline's accept-line for enter_accept ([#2953](https://github.com/atuinsh/atuin/issues/2953))
- Add commit to displayed version info ([#2922](https://github.com/atuinsh/atuin/issues/2922))
- Add import from PowerShell history ([#2864](https://github.com/atuinsh/atuin/issues/2864))
- Interactive Inspector ([#2319](https://github.com/atuinsh/atuin/issues/2319))
- Nu ≥ 0.106.0 support commandline accept ([#2957](https://github.com/atuinsh/atuin/issues/2957))
### Miscellaneous Tasks
- Update rusty_paseto and rusty_paserk ([#2942](https://github.com/atuinsh/atuin/issues/2942))
- Update changelog
## 18.9.0
### Bug Fixes
- *(dotfiles)* Properly escape spaces/quotes in vars
- Clippy issues on Windows ([#2856](https://github.com/atuinsh/atuin/issues/2856))
- Honor timezone in inspector stats ([#2853](https://github.com/atuinsh/atuin/issues/2853))
- Make status exit 1 if not logged in ([#2843](https://github.com/atuinsh/atuin/issues/2843))
- Match logic of theme directory with settings directory, so ATUIN_CONFIG_DIR is respected ([#2707](https://github.com/atuinsh/atuin/issues/2707))
- Expand path for daemon.socket_path ([#2870](https://github.com/atuinsh/atuin/issues/2870))
- Use fullscreen if `inline_height` is too large ([#2888](https://github.com/atuinsh/atuin/issues/2888))
- Clean up new rustc and clippy warnings on Rust 1.89
- `cargo update` and changes needed to accomodate it
- Run `cargo fmt`
- Clippy warnings I don't have on my version of clippy
- Add forgotten `rust-toolchain.toml` to match changes (oops)
- Update version in Cargo.toml + github workflows
- Clippy warnings
- Dissociate command_chaining from enter_accept
- Remove __atuin_chain_command__ prefix
- Docker compose link ([#2914](https://github.com/atuinsh/atuin/issues/2914))
- Fish up binding ([#2902](https://github.com/atuinsh/atuin/issues/2902))
### Features
- *(stats)* Add dotnet to default common subcommands
- *(tui)* Select entries using number in vim-normal mode. closes #2368 ([#2893](https://github.com/atuinsh/atuin/issues/2893))
- *(tui)* Add show_numeric_shortcuts config to hide 1-9 shortcuts ([#2766](https://github.com/atuinsh/atuin/issues/2766))
- Highlight matches in interactive search ([#2653](https://github.com/atuinsh/atuin/issues/2653))
- Add session-preload filter mode to include global history from before session start
- Add various acceptance keys ([#2928](https://github.com/atuinsh/atuin/issues/2928))
- More accurately filter secret tokens ([#2932](https://github.com/atuinsh/atuin/issues/2932))
- Add shell pipelines to command chaining ([#2938](https://github.com/atuinsh/atuin/issues/2938))
### Miscellaneous Tasks
- Update changelog
- Remove legacy Apple SDK frameworks ([#2885](https://github.com/atuinsh/atuin/issues/2885))
- Update dist workflows
- Update to Rust 1.90 ([#2916](https://github.com/atuinsh/atuin/issues/2916))
### Refactor
- Shell environment variables
### Build
- Update flake.nix with new sha256
## 18.8.0
### Bug Fixes
- *(build)* Enable sqlite feature for sqlite server ([#2848](https://github.com/atuinsh/atuin/issues/2848))
- Make login exit 1 if already logged in ([#2832](https://github.com/atuinsh/atuin/issues/2832))
- Use transaction for idx consistency checking ([#2840](https://github.com/atuinsh/atuin/issues/2840))
- Ensure the idx cache is cleaned on deletion, only insert if records are inserted ([#2841](https://github.com/atuinsh/atuin/issues/2841))
### Features
- Command chaining ([#2834](https://github.com/atuinsh/atuin/issues/2834))
- Add info for 'official' plugins ([#2835](https://github.com/atuinsh/atuin/issues/2835))
- Support multi part commands ([#2836](https://github.com/atuinsh/atuin/issues/2836)) ([#2837](https://github.com/atuinsh/atuin/issues/2837))
- Add inline_height_shell_up_key_binding option ([#2817](https://github.com/atuinsh/atuin/issues/2817))
- Add IDX_CACHE_ROLLOUT ([#2850](https://github.com/atuinsh/atuin/issues/2850))
### Miscellaneous Tasks
- Update to rust 1.88 ([#2815](https://github.com/atuinsh/atuin/issues/2815))
### Nushell
- Fix `get -i` deprecation ([#2829](https://github.com/atuinsh/atuin/issues/2829))
## 18.7.1
### Bug Fixes
- Add check for postgresql prefix ([#2825](https://github.com/atuinsh/atuin/issues/2825))
### Miscellaneous Tasks
- Update changelog
## 18.7.0
### Bug Fixes
- *(api)* Allow trailing slashes in sync_address ([#2760](https://github.com/atuinsh/atuin/issues/2760))
- *(doctor)* Mention the required ble.sh version ([#2774](https://github.com/atuinsh/atuin/issues/2774))
- *(search)* Prevent panic on malformed format strings ([#2776](https://github.com/atuinsh/atuin/issues/2776)) ([#2777](https://github.com/atuinsh/atuin/issues/2777))
- Clarify that HISTFILE, if used, must be exported ([#2758](https://github.com/atuinsh/atuin/issues/2758))
- Don't print errors in `zsh_autosuggest` helper ([#2780](https://github.com/atuinsh/atuin/issues/2780))
- `atuin.nu` enchancements ([#2778](https://github.com/atuinsh/atuin/issues/2778))
- Refuse "--dupkeep 0" ([#2807](https://github.com/atuinsh/atuin/issues/2807))
### Features
- Add sqlite server support for self-hosting ([#2770](https://github.com/atuinsh/atuin/issues/2770))
### Miscellaneous Tasks
- *(ci)* Install toolchain that matches rust-toolchain.toml ([#2759](https://github.com/atuinsh/atuin/issues/2759))
- Allow setting script DB path ([#2750](https://github.com/atuinsh/atuin/issues/2750))
## 18.6.1
### Bug Fixes
- Selection vs render issue ([#2706](https://github.com/atuinsh/atuin/issues/2706))
### Features
- *(stats)* Add jj to default common subcommands ([#2708](https://github.com/atuinsh/atuin/issues/2708))
- Delete duplicate history ([#2697](https://github.com/atuinsh/atuin/issues/2697))
- Sort `atuin store status` output ([#2719](https://github.com/atuinsh/atuin/issues/2719))
- Implement KV as a write-through cache ([#2732](https://github.com/atuinsh/atuin/issues/2732))
### Miscellaneous Tasks
- Use native github arm64 runner ([#2690](https://github.com/atuinsh/atuin/issues/2690))
- Fix typos ([#2668](https://github.com/atuinsh/atuin/issues/2668))
## 18.5.0
### Bug Fixes
- *(1289)* Clear terminal area if inline ([#2600](https://github.com/atuinsh/atuin/issues/2600))
- *(bash)* Fix preexec of child Bash session started by enter_accept ([#2558](https://github.com/atuinsh/atuin/issues/2558))
- *(build)* Change atuin-daemon build script .proto paths ([#2638](https://github.com/atuinsh/atuin/issues/2638))
- *(kv)* Filter deleted keys from `kv list` ([#2665](https://github.com/atuinsh/atuin/issues/2665))
- *(stats)* Ignore leading environment variables when calculating stats ([#2659](https://github.com/atuinsh/atuin/issues/2659))
- *(wrapped)* Fix crash when history is empty ([#2508](https://github.com/atuinsh/atuin/issues/2508))
- *(zsh)* Fix an error introduced earilier with support for bracketed paste mode ([#2651](https://github.com/atuinsh/atuin/issues/2651))
- *(zsh)* Avoid calling user-defined widgets when searching for history position ([#2670](https://github.com/atuinsh/atuin/issues/2670))
- Add .histfile as file to look for when doing atuin import zsh ([#2588](https://github.com/atuinsh/atuin/issues/2588))
- Panic when invoking delete on empty tui ([#2584](https://github.com/atuinsh/atuin/issues/2584))
- Sql files checksums ([#2601](https://github.com/atuinsh/atuin/issues/2601))
- Up binding with fish 4.0 ([#2613](https://github.com/atuinsh/atuin/issues/2613)) ([#2616](https://github.com/atuinsh/atuin/issues/2616))
- Don't save empty commands ([#2605](https://github.com/atuinsh/atuin/issues/2605))
- Improve broken symlink error handling ([#2589](https://github.com/atuinsh/atuin/issues/2589))
- Multiline command does not honour max_preview_height ([#2624](https://github.com/atuinsh/atuin/issues/2624))
- Typeerror in client sync code ([#2647](https://github.com/atuinsh/atuin/issues/2647))
- Add redundant clones to clippy and cleanup instances of it ([#2654](https://github.com/atuinsh/atuin/issues/2654))
- Allow -ve values for timezone ([#2609](https://github.com/atuinsh/atuin/issues/2609))
- Fish up binding bug ([#2677](https://github.com/atuinsh/atuin/issues/2677))
- Switch to astral cargo-dist ([#2687](https://github.com/atuinsh/atuin/issues/2687))
### Documentation
- Update logo and badges in README for zh-CN ([#2392](https://github.com/atuinsh/atuin/issues/2392))
### Features
- *(client)* Update AWS secrets env var handling checks ([#2501](https://github.com/atuinsh/atuin/issues/2501))
- *(health)* Add health check endpoint at `/healthz` ([#2549](https://github.com/atuinsh/atuin/issues/2549))
- *(kv)* Add support for 'atuin kv delete' ([#2660](https://github.com/atuinsh/atuin/issues/2660))
- *(wrapped)* Add more pkg managers ([#2503](https://github.com/atuinsh/atuin/issues/2503))
- *(zsh)* Try to go to the position in zsh's history ([#1469](https://github.com/atuinsh/atuin/issues/1469))
- *(zsh)* Re-enable bracketed paste ([#2646](https://github.com/atuinsh/atuin/issues/2646))
- Add the --print0 option to search ([#2562](https://github.com/atuinsh/atuin/issues/2562))
- Make new arrow key behavior configurable ([#2606](https://github.com/atuinsh/atuin/issues/2606))
- Use readline binding for ctrl-a when it is not the prefix ([#2626](https://github.com/atuinsh/atuin/issues/2626))
- Option to include duplicate commands when printing history commands ([#2407](https://github.com/atuinsh/atuin/issues/2407))
- Binaries as subcommands ([#2661](https://github.com/atuinsh/atuin/issues/2661))
- Support storing, syncing and executing scripts ([#2644](https://github.com/atuinsh/atuin/issues/2644))
- Add 'atuin scripts rm' and 'atuin scripts ls' aliases; allow reading from stdin ([#2680](https://github.com/atuinsh/atuin/issues/2680))
### Miscellaneous Tasks
- Remove unneeded dependencies ([#2523](https://github.com/atuinsh/atuin/issues/2523))
- Update rust toolchain to 1.85 ([#2618](https://github.com/atuinsh/atuin/issues/2618))
- Align daemon and client sync freq ([#2628](https://github.com/atuinsh/atuin/issues/2628))
- Migrate to rust 2024 ([#2635](https://github.com/atuinsh/atuin/issues/2635))
- Show host and user in inspector ([#2634](https://github.com/atuinsh/atuin/issues/2634))
- Update to rust 1.85.1 ([#2642](https://github.com/atuinsh/atuin/issues/2642))
- Update to rust 1.86 ([#2666](https://github.com/atuinsh/atuin/issues/2666))
### Performance
- Cache `SECRET_PATTERNS`'s `RegexSet` ([#2570](https://github.com/atuinsh/atuin/issues/2570))
### Styling
- Avoid calling `unwrap()` when we don't have to ([#2519](https://github.com/atuinsh/atuin/issues/2519))
### Build
- *(nix)* Bump `flake.lock` ([#2637](https://github.com/atuinsh/atuin/issues/2637))
### Flake.lock
- Update ([#2463](https://github.com/atuinsh/atuin/issues/2463))
## 18.4.0
### Bug Fixes
- *(crate)* Add missing description ([#2106](https://github.com/atuinsh/atuin/issues/2106))
- *(crate)* Add description to daemon crate ([#2107](https://github.com/atuinsh/atuin/issues/2107))
- *(daemon)* Add context to error when unable to connect ([#2394](https://github.com/atuinsh/atuin/issues/2394))
- *(deps)* Pin tiny_bip to 1.0.0 until breaking change resolved ([#2412](https://github.com/atuinsh/atuin/issues/2412))
- *(docker)* Update Dockerfile ([#2369](https://github.com/atuinsh/atuin/issues/2369))
- *(gui)* Update deps ([#2116](https://github.com/atuinsh/atuin/issues/2116))
- *(gui)* Add support for checking if the cli is installed on windows ([#2162](https://github.com/atuinsh/atuin/issues/2162))
- *(gui)* WeekInfo call on Edge ([#2252](https://github.com/atuinsh/atuin/issues/2252))
- *(gui)* Add \r for windows (shouldn't effect unix bc they should ignore it) ([#2253](https://github.com/atuinsh/atuin/issues/2253))
- *(gui)* Terminal resize overflow ([#2285](https://github.com/atuinsh/atuin/issues/2285))
- *(gui)* Kill child on block stop ([#2288](https://github.com/atuinsh/atuin/issues/2288))
- *(gui)* Do not hardcode db path ([#2309](https://github.com/atuinsh/atuin/issues/2309))
- *(gui)* Double return on mac/linux ([#2311](https://github.com/atuinsh/atuin/issues/2311))
- *(gui)* Cursor positioning on new doc creation ([#2310](https://github.com/atuinsh/atuin/issues/2310))
- *(gui)* Random ts errors ([#2316](https://github.com/atuinsh/atuin/issues/2316))
- *(history)* Logic for store_failed=false ([#2284](https://github.com/atuinsh/atuin/issues/2284))
- *(mail)* Incorrect alias and error logs ([#2346](https://github.com/atuinsh/atuin/issues/2346))
- *(mail)* Enable correct tls features for postmark client ([#2347](https://github.com/atuinsh/atuin/issues/2347))
- *(theme)* Restore original colours ([#2339](https://github.com/atuinsh/atuin/issues/2339))
- *(themes)* Restore default theme, refactor ([#2294](https://github.com/atuinsh/atuin/issues/2294))
- *(tui)* Press ctrl-a twice should jump to beginning of line ([#2246](https://github.com/atuinsh/atuin/issues/2246))
- *(tui)* Don't panic when search result is empty and up is pressed ([#2395](https://github.com/atuinsh/atuin/issues/2395))
- Cargo binstall config ([#2112](https://github.com/atuinsh/atuin/issues/2112))
- Unitless sync_frequence = 0 not parsed by humantime ([#2154](https://github.com/atuinsh/atuin/issues/2154))
- Some --help comments didn't show properly ([#2176](https://github.com/atuinsh/atuin/issues/2176))
- Ensure we cleanup all tables when deleting ([#2191](https://github.com/atuinsh/atuin/issues/2191))
- Add idx cache unique index ([#2226](https://github.com/atuinsh/atuin/issues/2226))
- Idx cache inconsistency ([#2231](https://github.com/atuinsh/atuin/issues/2231))
- Ambiguous column name ([#2232](https://github.com/atuinsh/atuin/issues/2232))
- Atuin-daemon optional dependency ([#2306](https://github.com/atuinsh/atuin/issues/2306))
- Windows build error ([#2321](https://github.com/atuinsh/atuin/issues/2321))
- Codespell config still references the ui ([#2330](https://github.com/atuinsh/atuin/issues/2330))
- Remove dbg! macro ([#2355](https://github.com/atuinsh/atuin/issues/2355))
- Disable mail by default, resolve #2404 ([#2405](https://github.com/atuinsh/atuin/issues/2405))
- Time offset display in `atuin status` ([#2433](https://github.com/atuinsh/atuin/issues/2433))
- Disable the actuated mirror on the x86 docker builder ([#2443](https://github.com/atuinsh/atuin/issues/2443))
### Documentation
- *(README)* Fix broken link ([#2206](https://github.com/atuinsh/atuin/issues/2206))
- *(gui)* Update README ([#2283](https://github.com/atuinsh/atuin/issues/2283))
- Streamline readme ([#2203](https://github.com/atuinsh/atuin/issues/2203))
- Update quickstart install command ([#2205](https://github.com/atuinsh/atuin/issues/2205))
### Features
- *(bash/blesh)* Hook into BLE_ONLOAD to resolve loading order issue ([#2234](https://github.com/atuinsh/atuin/issues/2234))
- *(client)* Add filter mode enablement and ordering configuration ([#2430](https://github.com/atuinsh/atuin/issues/2430))
- *(daemon)* Follow XDG_RUNTIME_DIR if set ([#2171](https://github.com/atuinsh/atuin/issues/2171))
- *(gui)* Automatically install and setup the cli/shell ([#2139](https://github.com/atuinsh/atuin/issues/2139))
- *(gui)* Add activity calendar to the homepage ([#2160](https://github.com/atuinsh/atuin/issues/2160))
- *(gui)* Cache zustand store in localstorage ([#2168](https://github.com/atuinsh/atuin/issues/2168))
- *(gui)* Toast with prompt for cli install, rather than auto ([#2173](https://github.com/atuinsh/atuin/issues/2173))
- *(gui)* Runbooks that run ([#2233](https://github.com/atuinsh/atuin/issues/2233))
- *(gui)* Use fancy new side nav ([#2243](https://github.com/atuinsh/atuin/issues/2243))
- *(gui)* Add runbook list, ability to create and delete, sql storage ([#2282](https://github.com/atuinsh/atuin/issues/2282))
- *(gui)* Background terminals and more ([#2303](https://github.com/atuinsh/atuin/issues/2303))
- *(gui)* Clean up home page, fix a few bugs ([#2304](https://github.com/atuinsh/atuin/issues/2304))
- *(gui)* Allow interacting with the embedded terminal ([#2312](https://github.com/atuinsh/atuin/issues/2312))
- *(gui)* Directory block, re-org of some code ([#2314](https://github.com/atuinsh/atuin/issues/2314))
- *(gui)* Folder select dialogue for directory block ([#2315](https://github.com/atuinsh/atuin/issues/2315))
- *(history)* Filter out various environment variables containing potential secrets ([#2174](https://github.com/atuinsh/atuin/issues/2174))
- *(tui)* Configurable prefix character ([#2157](https://github.com/atuinsh/atuin/issues/2157))
- *(tui)* Customizable Themes ([#2236](https://github.com/atuinsh/atuin/issues/2236))
- *(tui)* Fixed preview height option ([#2286](https://github.com/atuinsh/atuin/issues/2286))
- Use cargo-dist installer from our install script ([#2108](https://github.com/atuinsh/atuin/issues/2108))
- Add user account verification ([#2190](https://github.com/atuinsh/atuin/issues/2190))
- Add GitLab PAT to secret patterns ([#2196](https://github.com/atuinsh/atuin/issues/2196))
- Add several other GitHub access token patterns ([#2200](https://github.com/atuinsh/atuin/issues/2200))
- Add npm, Netlify and Pulumi tokens to secret patterns ([#2210](https://github.com/atuinsh/atuin/issues/2210))
- Allow advertising a fake version to clients ([#2228](https://github.com/atuinsh/atuin/issues/2228))
- Monitor idx cache consistency before switching ([#2229](https://github.com/atuinsh/atuin/issues/2229))
- Ultracompact Mode (search-only) ([#2357](https://github.com/atuinsh/atuin/issues/2357))
- Right Arrow to modify selected command ([#2453](https://github.com/atuinsh/atuin/issues/2453))
- Provide additional clarity around key management ([#2467](https://github.com/atuinsh/atuin/issues/2467))
- Add `atuin wrapped` ([#2493](https://github.com/atuinsh/atuin/issues/2493))
### Miscellaneous Tasks
- *(build)* Compile protobufs with protox ([#2122](https://github.com/atuinsh/atuin/issues/2122))
- *(ci)* Do not run current ci for ui ([#2189](https://github.com/atuinsh/atuin/issues/2189))
- *(ci)* Codespell again ([#2332](https://github.com/atuinsh/atuin/issues/2332))
- *(deps-dev)* Bump @tauri-apps/cli in /ui ([#2135](https://github.com/atuinsh/atuin/issues/2135))
- *(deps-dev)* Bump vite from 5.2.13 to 5.3.1 in /ui ([#2150](https://github.com/atuinsh/atuin/issues/2150))
- *(deps-dev)* Bump @tauri-apps/cli in /ui ([#2277](https://github.com/atuinsh/atuin/issues/2277))
- *(deps-dev)* Bump tailwindcss from 3.4.4 to 3.4.6 in /ui ([#2301](https://github.com/atuinsh/atuin/issues/2301))
- *(install)* Use posix sh, not bash ([#2204](https://github.com/atuinsh/atuin/issues/2204))
- *(nix)* De-couple atuin nix build from nixpkgs rustc version ([#2123](https://github.com/atuinsh/atuin/issues/2123))
- Add installer e2e tests ([#2110](https://github.com/atuinsh/atuin/issues/2110))
- Remove unnecessary proto import ([#2120](https://github.com/atuinsh/atuin/issues/2120))
- Update to rust 1.78
- Add audit config, ignore RUSTSEC-2023-0071 ([#2126](https://github.com/atuinsh/atuin/issues/2126))
- Setup dependabot for the ui ([#2128](https://github.com/atuinsh/atuin/issues/2128))
- Cargo and pnpm update ([#2127](https://github.com/atuinsh/atuin/issues/2127))
- Update to rust 1.79 ([#2138](https://github.com/atuinsh/atuin/issues/2138))
- Update to cargo-dist 0.16, enable attestations ([#2156](https://github.com/atuinsh/atuin/issues/2156))
- Do not use package managers in installer ([#2201](https://github.com/atuinsh/atuin/issues/2201))
- Enable record sync by default ([#2255](https://github.com/atuinsh/atuin/issues/2255))
- Remove ui directory ([#2329](https://github.com/atuinsh/atuin/issues/2329))
- Update to rust 1.80 ([#2344](https://github.com/atuinsh/atuin/issues/2344))
- Update rust to `1.80.1` ([#2362](https://github.com/atuinsh/atuin/issues/2362))
- Enable inline height and compact by default ([#2249](https://github.com/atuinsh/atuin/issues/2249))
- Update to rust 1.82 ([#2432](https://github.com/atuinsh/atuin/issues/2432))
- Update cargo-dist ([#2471](https://github.com/atuinsh/atuin/issues/2471))
### Performance
- *(search)* Benchmark smart sort ([#2202](https://github.com/atuinsh/atuin/issues/2202))
- Create idx cache table ([#2140](https://github.com/atuinsh/atuin/issues/2140))
- Write to the idx cache ([#2225](https://github.com/atuinsh/atuin/issues/2225))
### Testing
- Add env ATUIN_TEST_LOCAL_TIMEOUT to control test timeout of SQLite ([#2337](https://github.com/atuinsh/atuin/issues/2337))
### Flake.lock
- Update ([#2213](https://github.com/atuinsh/atuin/issues/2213))
- Update ([#2378](https://github.com/atuinsh/atuin/issues/2378))
- Update ([#2402](https://github.com/atuinsh/atuin/issues/2402))
## 18.3.0
### Bug Fixes
- *(bash)* Fix a workaround for bash-5.2 keybindings ([#2060](https://github.com/atuinsh/atuin/issues/2060))
- *(ci)* Release workflow ([#1978](https://github.com/atuinsh/atuin/issues/1978))
- *(client)* Better error reporting on login/registration ([#2076](https://github.com/atuinsh/atuin/issues/2076))
- *(config)* Add quotes for strategy value in comment ([#1993](https://github.com/atuinsh/atuin/issues/1993))
- *(daemon)* Do not try to sync if logged out ([#2037](https://github.com/atuinsh/atuin/issues/2037))
- *(deps)* Replace parse_duration with humantime ([#2074](https://github.com/atuinsh/atuin/issues/2074))
- *(dotfiles)* Alias import with init output ([#1970](https://github.com/atuinsh/atuin/issues/1970))
- *(dotfiles)* Fish alias import ([#1972](https://github.com/atuinsh/atuin/issues/1972))
- *(dotfiles)* More fish alias import ([#1974](https://github.com/atuinsh/atuin/issues/1974))
- *(dotfiles)* Unquote aliases before quoting ([#1976](https://github.com/atuinsh/atuin/issues/1976))
- *(dotfiles)* Allow clearing aliases, disable import ([#1995](https://github.com/atuinsh/atuin/issues/1995))
- *(stats)* Generation for commands starting with a pipe ([#2058](https://github.com/atuinsh/atuin/issues/2058))
- *(ui)* Handle being logged out gracefully ([#2052](https://github.com/atuinsh/atuin/issues/2052))
- *(ui)* Fix mistake in last pr ([#2053](https://github.com/atuinsh/atuin/issues/2053))
- Support not-mac for default shell ([#1960](https://github.com/atuinsh/atuin/issues/1960))
- Adapt help to `enter_accept` config ([#2001](https://github.com/atuinsh/atuin/issues/2001))
- Add protobuf compiler to docker image ([#2009](https://github.com/atuinsh/atuin/issues/2009))
- Add incremental rebuild to daemon loop ([#2010](https://github.com/atuinsh/atuin/issues/2010))
- Alias enable/enabled in settings ([#2021](https://github.com/atuinsh/atuin/issues/2021))
- Bogus error message wording ([#1283](https://github.com/atuinsh/atuin/issues/1283))
- Save sync time in daemon ([#2029](https://github.com/atuinsh/atuin/issues/2029))
- Redact password in database URI when logging ([#2032](https://github.com/atuinsh/atuin/issues/2032))
- Save sync time in daemon ([#2051](https://github.com/atuinsh/atuin/issues/2051))
- Replace serde_yaml::to_string with serde_json::to_string_yaml ([#2087](https://github.com/atuinsh/atuin/issues/2087))
### Documentation
- Fix "From source" `cd` command ([#1973](https://github.com/atuinsh/atuin/issues/1973))
- Add docs for store subcommand ([#2097](https://github.com/atuinsh/atuin/issues/2097))
### Features
- *(daemon)* Add support for daemon on windows ([#2014](https://github.com/atuinsh/atuin/issues/2014))
- *(doctor)* Detect active preexec framework ([#1955](https://github.com/atuinsh/atuin/issues/1955))
- *(doctor)* Report sqlite version ([#2075](https://github.com/atuinsh/atuin/issues/2075))
- *(dotfiles)* Support syncing shell/env vars ([#1977](https://github.com/atuinsh/atuin/issues/1977))
- *(gui)* Work on home page, sort state ([#1956](https://github.com/atuinsh/atuin/issues/1956))
- *(history)* Create atuin-history, add stats to it ([#1990](https://github.com/atuinsh/atuin/issues/1990))
- *(install)* Add Tuxedo OS ([#2018](https://github.com/atuinsh/atuin/issues/2018))
- *(server)* Add me endpoint ([#1954](https://github.com/atuinsh/atuin/issues/1954))
- *(ui)* Scroll history infinitely ([#1999](https://github.com/atuinsh/atuin/issues/1999))
- *(ui)* Add history explore ([#2022](https://github.com/atuinsh/atuin/issues/2022))
- *(ui)* Use correct username on welcome screen ([#2050](https://github.com/atuinsh/atuin/issues/2050))
- *(ui)* Add login/register dialog ([#2056](https://github.com/atuinsh/atuin/issues/2056))
- *(ui)* Setup single-instance ([#2093](https://github.com/atuinsh/atuin/issues/2093))
- *(ui/dotfiles)* Add vars ([#1989](https://github.com/atuinsh/atuin/issues/1989))
- Allow ignoring failed commands ([#1957](https://github.com/atuinsh/atuin/issues/1957))
- Show preview auto ([#1804](https://github.com/atuinsh/atuin/issues/1804))
- Add background daemon ([#2006](https://github.com/atuinsh/atuin/issues/2006))
- Support importing from replxx history files ([#2024](https://github.com/atuinsh/atuin/issues/2024))
- Support systemd socket activation for daemon ([#2039](https://github.com/atuinsh/atuin/issues/2039))
### Miscellaneous Tasks
- *(ci)* Don't run "Update Nix Deps" CI on forks ([#2070](https://github.com/atuinsh/atuin/issues/2070))
- *(codespell)* Ignore CODE_OF_CONDUCT ([#2044](https://github.com/atuinsh/atuin/issues/2044))
- *(install)* Log cargo and rustc version ([#2068](https://github.com/atuinsh/atuin/issues/2068))
- *(release)* V18.3.0-prerelease.1 ([#2090](https://github.com/atuinsh/atuin/issues/2090))
- Move crates into crates/ dir ([#1958](https://github.com/atuinsh/atuin/issues/1958))
- Fix atuin crate readme ([#1959](https://github.com/atuinsh/atuin/issues/1959))
- Add some more logging to handlers ([#1971](https://github.com/atuinsh/atuin/issues/1971))
- Add some more debug logs ([#1979](https://github.com/atuinsh/atuin/issues/1979))
- Clarify default config file ([#2026](https://github.com/atuinsh/atuin/issues/2026))
- Handle rate limited responses ([#2057](https://github.com/atuinsh/atuin/issues/2057))
- Add Systemd config for self-hosted server ([#1879](https://github.com/atuinsh/atuin/issues/1879))
- Switch to cargo dist for releases ([#2085](https://github.com/atuinsh/atuin/issues/2085))
- Update email, gitignore, tweak ui ([#2094](https://github.com/atuinsh/atuin/issues/2094))
- Show scope in changelog ([#2102](https://github.com/atuinsh/atuin/issues/2102))
### Performance
- *(nushell)* Use version.(major|minor|patch) if available ([#1963](https://github.com/atuinsh/atuin/issues/1963))
- Only open the database for commands if strictly required ([#2043](https://github.com/atuinsh/atuin/issues/2043))
### Refactor
- Preview_auto to use enum and different option ([#1991](https://github.com/atuinsh/atuin/issues/1991))
## 18.2.0
### Bug Fixes
- *(bash)* Do not use "return" to cancel initialization ([#1928](https://github.com/atuinsh/atuin/issues/1928))
- *(crate)* Add missing description ([#1861](https://github.com/atuinsh/atuin/issues/1861))
- *(doctor)* Detect preexec plugin using env ATUIN_PREEXEC_BACKEND ([#1856](https://github.com/atuinsh/atuin/issues/1856))
- *(install)* Install script echo ([#1899](https://github.com/atuinsh/atuin/issues/1899))
- *(nu)* Update atuin.nu to resolve 0.92 deprecation ([#1913](https://github.com/atuinsh/atuin/issues/1913))
- *(search)* Allow empty search ([#1866](https://github.com/atuinsh/atuin/issues/1866))
- *(search)* Case insensitive hostname filtering ([#1883](https://github.com/atuinsh/atuin/issues/1883))
- Pass search query in via env ([#1865](https://github.com/atuinsh/atuin/issues/1865))
- Pass search query in via env for *Nushell* ([#1874](https://github.com/atuinsh/atuin/issues/1874))
- Report non-decodable errors correctly ([#1915](https://github.com/atuinsh/atuin/issues/1915))
- Use spawn_blocking for file access during async context ([#1936](https://github.com/atuinsh/atuin/issues/1936))
### Documentation
- *(bash-preexec)* Describe the limitation of missing commands ([#1937](https://github.com/atuinsh/atuin/issues/1937))
- Add security contact ([#1867](https://github.com/atuinsh/atuin/issues/1867))
- Add install instructions for cave/exherbo linux in README.md ([#1927](https://github.com/atuinsh/atuin/issues/1927))
- Add missing cli help text ([#1945](https://github.com/atuinsh/atuin/issues/1945))
### Features
- *(bash/blesh)* Use _ble_exec_time_ata for duration even in bash < 5 ([#1940](https://github.com/atuinsh/atuin/issues/1940))
- *(dotfiles)* Add alias import ([#1938](https://github.com/atuinsh/atuin/issues/1938))
- *(gui)* Add base structure ([#1935](https://github.com/atuinsh/atuin/issues/1935))
- *(install)* Update install.sh to support KDE Neon ([#1908](https://github.com/atuinsh/atuin/issues/1908))
- *(search)* Process [C-h] and [C-?] as representations of backspace ([#1857](https://github.com/atuinsh/atuin/issues/1857))
- *(search)* Allow specifying search query as an env var ([#1863](https://github.com/atuinsh/atuin/issues/1863))
- *(search)* Add better search scoring ([#1885](https://github.com/atuinsh/atuin/issues/1885))
- *(server)* Check PG version before running migrations ([#1868](https://github.com/atuinsh/atuin/issues/1868))
- Add atuin prefix binding ([#1875](https://github.com/atuinsh/atuin/issues/1875))
- Sync v2 default for new installs ([#1914](https://github.com/atuinsh/atuin/issues/1914))
- Add 'ctrl-a a' to jump to beginning of line ([#1917](https://github.com/atuinsh/atuin/issues/1917))
- Prevents stderr from going to the screen ([#1933](https://github.com/atuinsh/atuin/issues/1933))
### Miscellaneous Tasks
- *(ci)* Add codespell support (config, workflow) and make it fix some typos ([#1916](https://github.com/atuinsh/atuin/issues/1916))
- *(gui)* Cargo update ([#1943](https://github.com/atuinsh/atuin/issues/1943))
- Add issue form ([#1871](https://github.com/atuinsh/atuin/issues/1871))
- Require atuin doctor in issue form ([#1872](https://github.com/atuinsh/atuin/issues/1872))
- Add section to issue form ([#1873](https://github.com/atuinsh/atuin/issues/1873))
### Performance
- *(dotfiles)* Cache aliases and read straight from file ([#1918](https://github.com/atuinsh/atuin/issues/1918))
## 18.1.0
### Bug Fixes
- *(bash)* Rework #1509 to recover from the preexec failure ([#1729](https://github.com/atuinsh/atuin/issues/1729))
- *(build)* Make atuin compile on non-win/mac/linux platforms ([#1825](https://github.com/atuinsh/atuin/issues/1825))
- *(client)* No panic on empty inspector ([#1768](https://github.com/atuinsh/atuin/issues/1768))
- *(doctor)* Use a different method to detect env vars ([#1819](https://github.com/atuinsh/atuin/issues/1819))
- *(dotfiles)* Use latest client ([#1859](https://github.com/atuinsh/atuin/issues/1859))
- *(import/zsh-histdb)* Missing or wrong fields ([#1740](https://github.com/atuinsh/atuin/issues/1740))
- *(nix)* Set meta.mainProgram in the package ([#1823](https://github.com/atuinsh/atuin/issues/1823))
- *(nushell)* Readd up-arrow keybinding, now with menu handling ([#1770](https://github.com/atuinsh/atuin/issues/1770))
- *(regex)* Disable regex error logs ([#1806](https://github.com/atuinsh/atuin/issues/1806))
- *(stats)* Enable multiple command stats to be shown using unicode_segmentation ([#1739](https://github.com/atuinsh/atuin/issues/1739))
- *(store-init)* Re-sync after running auto store init ([#1834](https://github.com/atuinsh/atuin/issues/1834))
- *(sync)* Check store length after sync, not before ([#1805](https://github.com/atuinsh/atuin/issues/1805))
- *(sync)* Record size limiter ([#1827](https://github.com/atuinsh/atuin/issues/1827))
- *(tz)* Attempt to fix timezone reading ([#1810](https://github.com/atuinsh/atuin/issues/1810))
- *(ui)* Don't preserve for empty space ([#1712](https://github.com/atuinsh/atuin/issues/1712))
- *(xonsh)* Add xonsh to auto import, respect $HISTFILE in xonsh import, and fix issue with up-arrow keybinding in xonsh ([#1711](https://github.com/atuinsh/atuin/issues/1711))
- Fish init ([#1725](https://github.com/atuinsh/atuin/issues/1725))
- Typo ([#1741](https://github.com/atuinsh/atuin/issues/1741))
- Check session file exists for status command ([#1756](https://github.com/atuinsh/atuin/issues/1756))
- Ensure sync time is saved for sync v2 ([#1758](https://github.com/atuinsh/atuin/issues/1758))
- Missing characters in preview ([#1803](https://github.com/atuinsh/atuin/issues/1803))
- Doctor shell wording ([#1858](https://github.com/atuinsh/atuin/issues/1858))
### Documentation
- Minor formatting updates to the default config.toml ([#1689](https://github.com/atuinsh/atuin/issues/1689))
- Update docker compose ([#1818](https://github.com/atuinsh/atuin/issues/1818))
- Use db name env variable also in uri ([#1840](https://github.com/atuinsh/atuin/issues/1840))
### Features
- *(client)* Add config option keys.scroll_exits ([#1744](https://github.com/atuinsh/atuin/issues/1744))
- *(dotfiles)* Add enable setting to dotfiles, disable by default ([#1829](https://github.com/atuinsh/atuin/issues/1829))
- *(nix)* Add update action ([#1779](https://github.com/atuinsh/atuin/issues/1779))
- *(nu)* Return early if history is disabled ([#1807](https://github.com/atuinsh/atuin/issues/1807))
- *(nushell)* Add nushell completion generation ([#1791](https://github.com/atuinsh/atuin/issues/1791))
- *(search)* Process Ctrl+m for kitty keyboard protocol ([#1720](https://github.com/atuinsh/atuin/issues/1720))
- *(stats)* Normalize formatting of default config, suggest nix ([#1764](https://github.com/atuinsh/atuin/issues/1764))
- *(stats)* Add linux sysadmin commands to common_subcommands ([#1784](https://github.com/atuinsh/atuin/issues/1784))
- *(ui)* Add config setting for showing tabs ([#1755](https://github.com/atuinsh/atuin/issues/1755))
- Use ATUIN_TEST_SQLITE_STORE_TIMEOUT to specify test timeout of SQLite store ([#1703](https://github.com/atuinsh/atuin/issues/1703))
- Add 'a', 'A', 'h', and 'l' bindings to vim-normal mode ([#1697](https://github.com/atuinsh/atuin/issues/1697))
- Add xonsh history import ([#1678](https://github.com/atuinsh/atuin/issues/1678))
- Add 'ignored_commands' option to stats ([#1722](https://github.com/atuinsh/atuin/issues/1722))
- Support syncing aliases ([#1721](https://github.com/atuinsh/atuin/issues/1721))
- Change fulltext to do multi substring match ([#1660](https://github.com/atuinsh/atuin/issues/1660))
- Add history prune subcommand ([#1743](https://github.com/atuinsh/atuin/issues/1743))
- Add alias feedback and list command ([#1747](https://github.com/atuinsh/atuin/issues/1747))
- Add PHP package manager "composer" to list of default common subcommands ([#1757](https://github.com/atuinsh/atuin/issues/1757))
- Add '/', '?', and 'I' bindings to vim-normal mode ([#1760](https://github.com/atuinsh/atuin/issues/1760))
- Add `CTRL+[` binding as `<Esc>` alias ([#1787](https://github.com/atuinsh/atuin/issues/1787))
- Add atuin doctor ([#1796](https://github.com/atuinsh/atuin/issues/1796))
- Add checks for common setup issues ([#1799](https://github.com/atuinsh/atuin/issues/1799))
- Support regex with r/.../ syntax ([#1745](https://github.com/atuinsh/atuin/issues/1745))
- Guard against ancient versions of bash where this does not work. ([#1794](https://github.com/atuinsh/atuin/issues/1794))
- Add automatic history store init ([#1831](https://github.com/atuinsh/atuin/issues/1831))
- Adds info command to show env vars and config files ([#1841](https://github.com/atuinsh/atuin/issues/1841))
### Miscellaneous Tasks
- *(ci)* Add cross-compile job for illumos ([#1830](https://github.com/atuinsh/atuin/issues/1830))
- *(ci)* Setup nextest ([#1848](https://github.com/atuinsh/atuin/issues/1848))
- Do not show history table stats when using records ([#1835](https://github.com/atuinsh/atuin/issues/1835))
### Performance
- Optimize history init-store ([#1691](https://github.com/atuinsh/atuin/issues/1691))
### Refactor
- *(alias)* Clarify operation result for working with aliases ([#1748](https://github.com/atuinsh/atuin/issues/1748))
- *(nushell)* Update `commandline` syntax, closes #1733 ([#1735](https://github.com/atuinsh/atuin/issues/1735))
- Rename atuin-config to atuin-dotfiles ([#1817](https://github.com/atuinsh/atuin/issues/1817))
## 18.0.1
### Bug Fixes
- Reorder the exit of enhanced keyboard mode ([#1694](https://github.com/atuinsh/atuin/issues/1694))
## 18.0.0
### Bug Fixes
- *(bash)* Avoid unexpected `atuin history start` for keybindings ([#1509](https://github.com/atuinsh/atuin/issues/1509))
- *(bash)* Prevent input to be interpreted as options for blesh auto-complete ([#1511](https://github.com/atuinsh/atuin/issues/1511))
- *(bash)* Work around custom IFS ([#1514](https://github.com/atuinsh/atuin/issues/1514))
- *(bash)* Fix and improve the keybinding to `up` ([#1515](https://github.com/atuinsh/atuin/issues/1515))
- *(bash)* Work around bash < 4 and introduce initialization guards ([#1533](https://github.com/atuinsh/atuin/issues/1533))
- *(bash)* Strip control chars generated by `\[\]` in PS1 with bash-preexec ([#1620](https://github.com/atuinsh/atuin/issues/1620))
- *(bash/preexec)* Erase the prompt last line before Bash renders it
- *(bash/preexec)* Erase the previous prompt before overwriting
- *(bash/preexec)* Support termcap names for tput ([#1670](https://github.com/atuinsh/atuin/issues/1670))
- *(docs)* Update repo url in CONTRIBUTING.md ([#1594](https://github.com/atuinsh/atuin/issues/1594))
- *(fish)* Integration on older fishes ([#1563](https://github.com/atuinsh/atuin/issues/1563))
- *(perm)* Set umask 077 ([#1554](https://github.com/atuinsh/atuin/issues/1554))
- *(search)* Fix invisible tab title ([#1560](https://github.com/atuinsh/atuin/issues/1560))
- *(shell)* Fix incorrect timing of child shells ([#1510](https://github.com/atuinsh/atuin/issues/1510))
- *(sync)* Save sync time when it starts, not ends ([#1573](https://github.com/atuinsh/atuin/issues/1573))
- *(tests)* Add Settings::utc() for utc settings ([#1677](https://github.com/atuinsh/atuin/issues/1677))
- *(tui)* Dedupe was removing history ([#1610](https://github.com/atuinsh/atuin/issues/1610))
- *(windows)* Disables unix specific stuff for windows ([#1557](https://github.com/atuinsh/atuin/issues/1557))
- Prevent input to be interpreted as options for zsh autosuggestions ([#1506](https://github.com/atuinsh/atuin/issues/1506))
- Disable musl deb building ([#1525](https://github.com/atuinsh/atuin/issues/1525))
- Shorten text, use ctrl-o for inspector ([#1561](https://github.com/atuinsh/atuin/issues/1561))
- Print literal control characters to non terminals ([#1586](https://github.com/atuinsh/atuin/issues/1586))
- Escape control characters in command preview ([#1588](https://github.com/atuinsh/atuin/issues/1588))
- Use existing db querying for history list ([#1589](https://github.com/atuinsh/atuin/issues/1589))
- Add acquire timeout to sqlite database connection ([#1590](https://github.com/atuinsh/atuin/issues/1590))
- Only escape control characters when writing to terminal ([#1593](https://github.com/atuinsh/atuin/issues/1593))
- Check for format errors when printing history ([#1623](https://github.com/atuinsh/atuin/issues/1623))
- Skip padding time if it will overflow the allowed prefix length ([#1630](https://github.com/atuinsh/atuin/issues/1630))
- Never overwrite the key ([#1657](https://github.com/atuinsh/atuin/issues/1657))
- Set durability for sqlite to recommended settings ([#1667](https://github.com/atuinsh/atuin/issues/1667))
- Correct download list for incremental builds ([#1672](https://github.com/atuinsh/atuin/issues/1672))
### Documentation
- *(README)* Clarify prerequisites for Bash ([#1686](https://github.com/atuinsh/atuin/issues/1686))
- *(readme)* Add repology badge ([#1494](https://github.com/atuinsh/atuin/issues/1494))
- Add forum link to contributing ([#1498](https://github.com/atuinsh/atuin/issues/1498))
- Refer to image with multi-arch support ([#1513](https://github.com/atuinsh/atuin/issues/1513))
- Remove activity graph
- Fix `Destination file already exists` in Nushell ([#1530](https://github.com/atuinsh/atuin/issues/1530))
- Clarify enter/tab usage ([#1538](https://github.com/atuinsh/atuin/issues/1538))
- Improve style ([#1537](https://github.com/atuinsh/atuin/issues/1537))
- Remove old docusaurus ([#1581](https://github.com/atuinsh/atuin/issues/1581))
- Mention environment variables for custom paths ([#1614](https://github.com/atuinsh/atuin/issues/1614))
- Create pull_request_template.md ([#1632](https://github.com/atuinsh/atuin/issues/1632))
- Update CONTRIBUTING.md ([#1633](https://github.com/atuinsh/atuin/issues/1633))
### Features
- *(bash)* Support high-resolution timing even without ble.sh ([#1534](https://github.com/atuinsh/atuin/issues/1534))
- *(search)* Introduce keymap-dependent vim-mode ([#1570](https://github.com/atuinsh/atuin/issues/1570))
- *(search)* Make cursor style configurable ([#1595](https://github.com/atuinsh/atuin/issues/1595))
- *(shell)* Bind the Atuin search to "/" in vi-normal mode ([#1629](https://github.com/atuinsh/atuin/issues/1629))
- **BREAKING**: bind the Atuin search to "/" in vi-normal mode ([#1629](https://github.com/atuinsh/atuin/issues/1629))
- *(ui)* Add redraw ([#1519](https://github.com/atuinsh/atuin/issues/1519))
- *(ui)* Vim mode ([#1553](https://github.com/atuinsh/atuin/issues/1553))
- *(ui)* When in vim-normal mode apply an alternative highlighting to the selected line ([#1574](https://github.com/atuinsh/atuin/issues/1574))
- *(zsh)* Update widget names ([#1631](https://github.com/atuinsh/atuin/issues/1631))
- Enable enhanced keyboard mode ([#1505](https://github.com/atuinsh/atuin/issues/1505))
- Rework record sync for improved reliability ([#1478](https://github.com/atuinsh/atuin/issues/1478))
- Include atuin login in secret patterns ([#1518](https://github.com/atuinsh/atuin/issues/1518))
- Make it clear what you are registering for ([#1523](https://github.com/atuinsh/atuin/issues/1523))
- Add extended help ([#1540](https://github.com/atuinsh/atuin/issues/1540))
- Add interactive command inspector ([#1296](https://github.com/atuinsh/atuin/issues/1296))
- Add better error handling for sync ([#1572](https://github.com/atuinsh/atuin/issues/1572))
- Add history rebuild ([#1575](https://github.com/atuinsh/atuin/issues/1575))
- Make deleting from the UI work with record store sync ([#1580](https://github.com/atuinsh/atuin/issues/1580))
- Add metrics counter for records downloaded ([#1584](https://github.com/atuinsh/atuin/issues/1584))
- Make store init idempotent ([#1609](https://github.com/atuinsh/atuin/issues/1609))
- Don't stop with invalid key ([#1612](https://github.com/atuinsh/atuin/issues/1612))
- Add registered and deleted metrics ([#1622](https://github.com/atuinsh/atuin/issues/1622))
- Make history list format configurable ([#1638](https://github.com/atuinsh/atuin/issues/1638))
- Add change-password command & support on server ([#1615](https://github.com/atuinsh/atuin/issues/1615))
- Automatically init history store when record sync is enabled ([#1634](https://github.com/atuinsh/atuin/issues/1634))
- Add store push ([#1649](https://github.com/atuinsh/atuin/issues/1649))
- Reencrypt/rekey local store ([#1662](https://github.com/atuinsh/atuin/issues/1662))
- Add prefers_reduced_motion flag ([#1645](https://github.com/atuinsh/atuin/issues/1645))
- Add verify command to local store
- Add store purge command
- Failure to decrypt history = failure to sync
- Add `store push --force`
- Add `store pull`
- Disable auto record store init ([#1671](https://github.com/atuinsh/atuin/issues/1671))
- Add progress bars to sync and store init ([#1684](https://github.com/atuinsh/atuin/issues/1684))
### Miscellaneous Tasks
- *(ci)* Use github m1 for release builds ([#1658](https://github.com/atuinsh/atuin/issues/1658))
- *(ci)* Re-enable test cache, add separate check step ([#1663](https://github.com/atuinsh/atuin/issues/1663))
- *(ci)* Run rust build/test/check on 3 platforms ([#1675](https://github.com/atuinsh/atuin/issues/1675))
- Remove the teapot response ([#1496](https://github.com/atuinsh/atuin/issues/1496))
- Schema cleanup ([#1522](https://github.com/atuinsh/atuin/issues/1522))
- Update funding ([#1543](https://github.com/atuinsh/atuin/issues/1543))
- Make clipboard dep optional as a feature ([#1558](https://github.com/atuinsh/atuin/issues/1558))
- Add feature to allow always disable check update ([#1628](https://github.com/atuinsh/atuin/issues/1628))
- Use resolver 2, update editions + cargo ([#1635](https://github.com/atuinsh/atuin/issues/1635))
- Disable nix tests ([#1646](https://github.com/atuinsh/atuin/issues/1646))
- Set ATUIN_ variables for development in devshell ([#1653](https://github.com/atuinsh/atuin/issues/1653))
### Refactor
- *(search)* Refactor vim mode ([#1559](https://github.com/atuinsh/atuin/issues/1559))
- *(search)* Refactor handling of key inputs ([#1606](https://github.com/atuinsh/atuin/issues/1606))
- *(shell)* Refactor and localize `HISTORY => __atuin_output` ([#1535](https://github.com/atuinsh/atuin/issues/1535))
- Use enum instead of magic numbers ([#1499](https://github.com/atuinsh/atuin/issues/1499))
- String -> HistoryId ([#1512](https://github.com/atuinsh/atuin/issues/1512))
### Styling
- *(bash)* Use consistent coding style ([#1528](https://github.com/atuinsh/atuin/issues/1528))
### Testing
- Add multi-user integration tests ([#1648](https://github.com/atuinsh/atuin/issues/1648))
### Stats
- Misc improvements ([#1613](https://github.com/atuinsh/atuin/issues/1613))
## 17.2.1
### Bug Fixes
- *(server)* Typo with default config ([#1493](https://github.com/atuinsh/atuin/issues/1493))
## 17.2.0
### Bug Fixes
- *(bash)* Fix loss of the last output line with enter_accept ([#1463](https://github.com/atuinsh/atuin/issues/1463))
- *(bash)* Improve the support for `enter_accept` with `ble.sh` ([#1465](https://github.com/atuinsh/atuin/issues/1465))
- *(bash)* Fix small issues of `enter_accept` for the plain Bash ([#1467](https://github.com/atuinsh/atuin/issues/1467))
- *(bash)* Fix error by the use of ${PS1@P} in bash < 4.4 ([#1488](https://github.com/atuinsh/atuin/issues/1488))
- *(bash,zsh)* Fix quirks on search cancel ([#1483](https://github.com/atuinsh/atuin/issues/1483))
- *(clippy)* Ignore struct_field_names ([#1466](https://github.com/atuinsh/atuin/issues/1466))
- *(docs)* Fix typo ([#1439](https://github.com/atuinsh/atuin/issues/1439))
- *(docs)* Discord link expired
- *(history)* Disallow deletion if the '--limit' flag is present ([#1436](https://github.com/atuinsh/atuin/issues/1436))
- *(import/zsh)* Zsh use a special format to escape some characters ([#1490](https://github.com/atuinsh/atuin/issues/1490))
- *(install)* Discord broken link
- *(shell)* Respect ZSH's $ZDOTDIR environment variable ([#1441](https://github.com/atuinsh/atuin/issues/1441))
- *(stats)* Don't require all fields under [stats] ([#1437](https://github.com/atuinsh/atuin/issues/1437))
- *(stats)* Time now_local not working
- *(zsh)* Zsh_autosuggest_strategy for no-unset environment ([#1486](https://github.com/atuinsh/atuin/issues/1486))
### Documentation
- *(readme)* Add actuated linkback
- *(readme)* Fix light/dark mode logo
- *(readme)* Use picture element for logo
- Add link to forum
- Align setup links in docs and readme ([#1446](https://github.com/atuinsh/atuin/issues/1446))
- Add Void Linux install instruction ([#1445](https://github.com/atuinsh/atuin/issues/1445))
- Add fish install script ([#1447](https://github.com/atuinsh/atuin/issues/1447))
- Correct link
- Add docs for zsh-autosuggestion integration ([#1480](https://github.com/atuinsh/atuin/issues/1480))
- Remove stray character from README
- Update logo ([#1481](https://github.com/atuinsh/atuin/issues/1481))
### Features
- *(bash)* Provide auto-complete source for ble.sh ([#1487](https://github.com/atuinsh/atuin/issues/1487))
- *(shell)* Support high-resolution duration if available ([#1484](https://github.com/atuinsh/atuin/issues/1484))
- Add semver checking to client requests ([#1456](https://github.com/atuinsh/atuin/issues/1456))
- Add TLS to atuin-server ([#1457](https://github.com/atuinsh/atuin/issues/1457))
- Integrate with zsh-autosuggestions ([#1479](https://github.com/atuinsh/atuin/issues/1479))
### Miscellaneous Tasks
- *(repo)* Remove issue config ([#1433](https://github.com/atuinsh/atuin/issues/1433))
- Remove issue template ([#1444](https://github.com/atuinsh/atuin/issues/1444))
### Refactor
- *(bash)* Factorize `__atuin_accept_line` ([#1476](https://github.com/atuinsh/atuin/issues/1476))
- *(bash)* Refactor and optimize `__atuin_accept_line` ([#1482](https://github.com/atuinsh/atuin/issues/1482))
## 17.1.0
### Bug Fixes
- *(fish)* Clean up the fish script options ([#1370](https://github.com/atuinsh/atuin/issues/1370))
- *(fish)* Use fish builtins for `enter_accept` ([#1373](https://github.com/atuinsh/atuin/issues/1373))
- *(fish)* Accept multiline commands ([#1418](https://github.com/atuinsh/atuin/issues/1418))
- *(nix)* Add Appkit to the package build ([#1358](https://github.com/atuinsh/atuin/issues/1358))
- *(zsh)* Bind in the most popular modes ([#1360](https://github.com/atuinsh/atuin/issues/1360))
- *(zsh)* Only trigger up-arrow on first line ([#1359](https://github.com/atuinsh/atuin/issues/1359))
- Initial list of history in workspace mode ([#1356](https://github.com/atuinsh/atuin/issues/1356))
- Make `atuin account delete` void session + key ([#1393](https://github.com/atuinsh/atuin/issues/1393))
- New clippy lints ([#1395](https://github.com/atuinsh/atuin/issues/1395))
- Reenable enter_accept for bash ([#1408](https://github.com/atuinsh/atuin/issues/1408))
- Respect ZSH's $ZDOTDIR environment variable ([#942](https://github.com/atuinsh/atuin/issues/942))
### Documentation
- Update sync.md ([#1409](https://github.com/atuinsh/atuin/issues/1409))
- Update Arch Linux package URL in advanced-install.md ([#1407](https://github.com/atuinsh/atuin/issues/1407))
- New stats config ([#1412](https://github.com/atuinsh/atuin/issues/1412))
### Features
- *(nix)* Add a nixpkgs overlay ([#1357](https://github.com/atuinsh/atuin/issues/1357))
- Add metrics server and http metrics ([#1394](https://github.com/atuinsh/atuin/issues/1394))
- Add some metrics related to Atuin as an app ([#1399](https://github.com/atuinsh/atuin/issues/1399))
- Allow configuring stats prefix ([#1411](https://github.com/atuinsh/atuin/issues/1411))
- Allow spaces in stats prefixes ([#1414](https://github.com/atuinsh/atuin/issues/1414))
### Miscellaneous Tasks
- *(readme)* Add contributor image to README ([#1430](https://github.com/atuinsh/atuin/issues/1430))
- Update to sqlx 0.7.3 ([#1416](https://github.com/atuinsh/atuin/issues/1416))
- `cargo update` ([#1419](https://github.com/atuinsh/atuin/issues/1419))
- Update rusty_paseto and rusty_paserk ([#1420](https://github.com/atuinsh/atuin/issues/1420))
- Run dependabot weekly, not daily ([#1423](https://github.com/atuinsh/atuin/issues/1423))
- Don't group deps ([#1424](https://github.com/atuinsh/atuin/issues/1424))
- Setup git cliff ([#1431](https://github.com/atuinsh/atuin/issues/1431))
## 17.0.1
### Bug Fixes
- *(bash)* Improve output of `enter_accept` ([#1342](https://github.com/atuinsh/atuin/issues/1342))
- *(enter_accept)* Clear old cmd snippet ([#1350](https://github.com/atuinsh/atuin/issues/1350))
- *(fish)* Improve output for `enter_accept` ([#1341](https://github.com/atuinsh/atuin/issues/1341))
## 17.0.0
### Bug Fixes
- *(1220)* Workspace Filtermode not handled in skim engine ([#1273](https://github.com/atuinsh/atuin/issues/1273))
- *(nu)* Disable the up-arrow keybinding for Nushell ([#1329](https://github.com/atuinsh/atuin/issues/1329))
- *(nushell)* Ignore stderr messages ([#1320](https://github.com/atuinsh/atuin/issues/1320))
- *(ubuntu/arm*)* Detect non amd64 ubuntu and handle ([#1131](https://github.com/atuinsh/atuin/issues/1131))
### Documentation
- Update `workspace` config key to `workspaces` ([#1174](https://github.com/atuinsh/atuin/issues/1174))
- Document the available format options of History list command ([#1234](https://github.com/atuinsh/atuin/issues/1234))
### Features
- *(installer)* Try installing via paru for the AUR ([#1262](https://github.com/atuinsh/atuin/issues/1262))
- *(keyup)* Configure SearchMode for KeyUp invocation #1216 ([#1224](https://github.com/atuinsh/atuin/issues/1224))
- Mouse selection support ([#1209](https://github.com/atuinsh/atuin/issues/1209))
- Copy to clipboard ([#1249](https://github.com/atuinsh/atuin/issues/1249))
### Refactor
- Duplications reduced in order to align implementations of reading history files ([#1247](https://github.com/atuinsh/atuin/issues/1247))
### Config.md
- Invert mode detailed options ([#1225](https://github.com/atuinsh/atuin/issues/1225))
## 16.0.0
### Bug Fixes
- *(docs)* List all presently documented commands ([#1140](https://github.com/atuinsh/atuin/issues/1140))
- *(docs)* Correct command overview paths ([#1145](https://github.com/atuinsh/atuin/issues/1145))
- *(server)* Teapot is a cup of coffee ([#1137](https://github.com/atuinsh/atuin/issues/1137))
- Adjust broken link to supported shells ([#1013](https://github.com/atuinsh/atuin/issues/1013))
- Fixes unix specific impl of shutdown_signal ([#1061](https://github.com/atuinsh/atuin/issues/1061))
- Nushell empty hooks ([#1138](https://github.com/atuinsh/atuin/issues/1138))
### Features
- Do not allow empty passwords durring account creation ([#1029](https://github.com/atuinsh/atuin/issues/1029))
### Skim
- Fix filtering aggregates ([#1114](https://github.com/atuinsh/atuin/issues/1114))
## 15.0.0
### Documentation
- Fix broken links in README.md ([#920](https://github.com/atuinsh/atuin/issues/920))
- Fix "From source" `cd` command ([#937](https://github.com/atuinsh/atuin/issues/937))
### Features
- Add delete account option (attempt 2) ([#980](https://github.com/atuinsh/atuin/issues/980))
### Miscellaneous Tasks
- Uuhhhhhh crypto lol ([#805](https://github.com/atuinsh/atuin/issues/805))
- Fix participle "be ran" -> "be run" ([#939](https://github.com/atuinsh/atuin/issues/939))
### Cwd_filter
- Much like history_filter, only it applies to cwd ([#904](https://github.com/atuinsh/atuin/issues/904))
## 14.0.0
### Bug Fixes
- *(client)* Always read session_path from settings ([#757](https://github.com/atuinsh/atuin/issues/757))
- *(installer)* Use case-insensitive comparison ([#776](https://github.com/atuinsh/atuin/issues/776))
- Many wins were broken :memo: ([#789](https://github.com/atuinsh/atuin/issues/789))
- Paste into terminal after switching modes ([#793](https://github.com/atuinsh/atuin/issues/793))
- Record negative exit codes ([#821](https://github.com/atuinsh/atuin/issues/821))
- Allow nix package to fetch dependencies from git ([#832](https://github.com/atuinsh/atuin/issues/832))
### Documentation
- *(README)* Fix activity graph link ([#753](https://github.com/atuinsh/atuin/issues/753))
### Features
- Add common default keybindings ([#719](https://github.com/atuinsh/atuin/issues/719))
- Add an inline view mode ([#648](https://github.com/atuinsh/atuin/issues/648))
- Add *Nushell* support ([#788](https://github.com/atuinsh/atuin/issues/788))
- Add github action to test the nix builds ([#833](https://github.com/atuinsh/atuin/issues/833))
### Miscellaneous Tasks
- Remove tui vendoring ([#804](https://github.com/atuinsh/atuin/issues/804))
- Use fork of skim ([#803](https://github.com/atuinsh/atuin/issues/803))
### Nix
- Add flake-compat ([#743](https://github.com/atuinsh/atuin/issues/743))
## 13.0.0
### Documentation
- *(README)* Add static activity graph example ([#680](https://github.com/atuinsh/atuin/issues/680))
- Remove human short flag from docs, duplicate of help -h ([#663](https://github.com/atuinsh/atuin/issues/663))
- Fix typo in zh-CN/README.md ([#666](https://github.com/atuinsh/atuin/issues/666))
### Features
- *(history)* Add new flag to allow custom output format ([#662](https://github.com/atuinsh/atuin/issues/662))
### Fish
- Fix `atuin init` for the fish shell ([#699](https://github.com/atuinsh/atuin/issues/699))
### Install.sh
- Fallback to using cargo ([#639](https://github.com/atuinsh/atuin/issues/639))
## 12.0.0
### Documentation
- Add more details about date parsing in the stats command ([#579](https://github.com/atuinsh/atuin/issues/579))
## 0.10.0
### Miscellaneous Tasks
- Allow specifiying the limited of returned entries ([#364](https://github.com/atuinsh/atuin/issues/364))
## 0.9.0
### README
- Add MacPorts installation instructions ([#302](https://github.com/atuinsh/atuin/issues/302))
## 0.8.1
### Bug Fixes
- Get install.sh working on UbuntuWSL ([#260](https://github.com/atuinsh/atuin/issues/260))
## 0.8.0
### Bug Fixes
- Resolve some issues with install.sh ([#188](https://github.com/atuinsh/atuin/issues/188))
### Features
- Login/register no longer blocking ([#216](https://github.com/atuinsh/atuin/issues/216))
## 0.7.2
### Bug Fixes
- Dockerfile with correct glibc ([#198](https://github.com/atuinsh/atuin/issues/198))
### Features
- Allow input of credentials from stdin ([#185](https://github.com/atuinsh/atuin/issues/185))
### Miscellaneous Tasks
- Some new linting ([#201](https://github.com/atuinsh/atuin/issues/201))
- Supply pre-build docker image ([#199](https://github.com/atuinsh/atuin/issues/199))
- Add more eyre contexts ([#200](https://github.com/atuinsh/atuin/issues/200))
- Improve build times ([#213](https://github.com/atuinsh/atuin/issues/213))
## 0.7.1
### Features
- Build individual crates ([#109](https://github.com/atuinsh/atuin/issues/109))
## 0.6.3
### Bug Fixes
- Help text
### Features
- Use directories project data dir
### Miscellaneous Tasks
- Use structopt wrapper instead of building clap by hand
<!-- generated by git-cliff -->
## /CODE_OF_CONDUCT.md
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
ellie@elliehuxtable.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
## /CONTRIBUTING.md
# Contributing
Thank you so much for considering contributing to Atuin! We really appreciate it <3
Development dependencies
1. A rust toolchain ([rustup](https://rustup.rs) recommended)
We commit to supporting the latest stable version of Rust - nothing more, nothing less, no nightly.
Before working on anything, we suggest taking a copy of your Atuin data directory (`~/.local/share/atuin` on most \*nix platforms). If anything goes wrong, you can always restore it!
While data directory backups are always a good idea, you can instruct Atuin to use custom path using the following environment variables:
```shell
export ATUIN_RECORD_STORE_PATH=/tmp/atuin_records.db # path to primary record store
export ATUIN_DB_PATH=/tmp/atuin_dev.db # path to materialized history database
export ATUIN_KV__DB_PATH=/tmp/atuin_kv.db # path to key-value store
export ATUIN_SCRIPTS__DB_PATH=/tmp/atuin_scripts.db # path to scripts database
export ATUIN_AI__DB_PATH=/tmp/atuin_ai_sessions.db # path to AI sessions database
export ATUIN_META__DB_PATH=/tmp/atuin_meta.db # path to meta database
```
It is also recommended to update your `$PATH` so that the pre-exec scripts would use the locally built version:
```shell
export PATH="./target/release:$PATH"
```
If you'd like to load a different configuration file, set `ATUIN_CONFIG_DIR` to a folder that contains your `config.toml` file:
```shell
export ATUIN_CONFIG_DIR=/tmp/atuin-config/
```
These variable exports can be added in a local `.envrc` file, read by [direnv](https://direnv.net/).
## PRs
It can speed up the review cycle if you consent to maintainers pushing to your branch. This will only be in the case of small fixes or adjustments, and not anything large. If you feel OK with this, please check the box on the template!
## What to work on?
Any issues labeled "bug" or "help wanted" would be fantastic, just drop a comment and feel free to ask for help!
If there's anything you want to work on that isn't already an issue, either open a feature request or get in touch on the [forum](https://forum.atuin.sh)/Discord.
## Setup
```
git clone https://github.com/atuinsh/atuin
cd atuin
cargo build
```
## Running
When iterating on a feature, it's useful to use `cargo run`
For example, if working on a search feature
```
cargo run -- search --a-new-flag
```
While iterating on the server, I find it helpful to run a new user on my system, with `sync_server` set to be `localhost`.
## Tests
Our test coverage is currently not the best, but we are working on it! Generally tests live in the file next to the functionality they are testing, and are executed just with `cargo test`.
## Logging and Debugging
### Log Files
Atuin writes logs to `~/.atuin/logs` unless configured otherwise. Log files are rotated daily and retained for 4 days by default:
- `search.log.*` - Interactive search session logs
- `daemon.log.*` - Background daemon logs
### Log Levels
You can set the `ATUIN_LOG` environment variable to override log verbosity from the config file:
```shell
ATUIN_LOG=debug atuin search # Enable debug logging
ATUIN_LOG=trace atuin search # Enable trace logging (very verbose)
```
### Span Timing (Performance Profiling)
For performance analysis, you can capture detailed span timing data as JSON:
```shell
ATUIN_SPAN=spans.json atuin search
```
This creates a JSON file with timing information for each instrumented span, including:
- `time.busy` - Time actively executing code
- `time.idle` - Time awaiting async operations (I/O, child tasks)
The `scripts/span-table.ts` script analyzes these logs:
```shell
# Summary view - shows all spans with timing stats
bun scripts/span-table.ts spans.json
# Detail view - shows individual calls for a specific span
bun scripts/span-table.ts spans.json --detail daemon_search
# Filter to specific spans
bun scripts/span-table.ts spans.json --filter "search|hydrate"
```
This is useful for comparing performance between different search implementations or identifying bottlenecks.
## Migrations
Be careful creating database migrations - once your database has migrated ahead of current stable, there is no going back
### Stickers
We try to ship anyone contributing to Atuin a sticker! Only contributors get a shiny one. Fill out [this form](https://noteforms.com/forms/contributors-stickers) if you'd like one.
## /CONTRIBUTORS
``` path="/CONTRIBUTORS"
0x4A6F <0x4A6F@users.noreply.github.com>
Aleks Bunin <sashkab@users.noreply.github.com>
Alex Hamilton <1622250+Aehmlo@users.noreply.github.com>
Alexandre GV. <contact@alexandregv.fr>
Aloxaf <bailong104@gmail.com>
Alpha Chen <alpha@kejadlen.dev>
Amos Bird <amosbird@gmail.com>
Anderson <141751473+digital-cuttlefish@users.noreply.github.com>
Andrew Aylett <andrew@aylett.co.uk>
Andrew Lee <32912555+candrewlee14@users.noreply.github.com>
Anish Pallati <anishp@duck.com>
Austin Schey <aschey13@gmail.com>
avinassh <640792+avinassh@users.noreply.github.com>
Azzam S.A <17734314+azzamsa@users.noreply.github.com>
b3nj5m1n <47924309+b3nj5m1n@users.noreply.github.com>
Baptiste <32563450+BapRx@users.noreply.github.com>
Ben J <bdavjones@gmail.com>
Benjamin Vergnaud <9599845+bvergnaud@users.noreply.github.com>
Benjamin Weinstein-Raun <b@w-r.me>
Blair Noctis <4474501+nc7s@users.noreply.github.com>
Brad Robel-Forrest <brad@bitpony.com>
Braelyn Boynton <bboynton97@gmail.com>
Brian Kung <2836167+briankung@users.noreply.github.com>
Bruce Huang <helbingxxx@gmail.com>
c-14 <git@c-14.de>
Caleb Maclennan <caleb@alerque.com>
Ch. (Chanwhi Choi) <ccwpc@hanmail.net>
Chandra Kiran G <chandra.kiran@cai-solutions.com>
chitao1234 <1139954766@qq.com>
Chris Rose <offline@offby1.net>
Conrad Ludgate <conradludgate@gmail.com>
CosmicHorror <LovecraftianHorror@pm.me>
Cristian Le <git@lecris.dev>
Cristian Le <github@lecris.me>
CULT PONY <67918945+cultpony@users.noreply.github.com>
cyqsimon <28627918+cyqsimon@users.noreply.github.com>
Dagan McGregor <d.mcgregor@gns.cri.nz>
Daniel <daniel.hub@outlook.de>
Daniel Carosone <daniel.carosone@gmail.com>
DaniPopes <57450786+DaniPopes@users.noreply.github.com>
David <drmorr@appliedcomputing.io>
David <drmorr@evokewonder.com>
David Chocholatý <chocholaty.david@protonmail.com>
David Jack Wange Olrik <david@olrik.dk>
David Legrand <1110600+davlgd@users.noreply.github.com>
Dennis Trautwein <git@dtrautwein.eu>
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Devin Buhl <onedr0p@users.noreply.github.com>
Dhruv Thakur <13575379+dhth@users.noreply.github.com>
Diego Carrasco Gubernatis <557703+dacog@users.noreply.github.com>
Dieter Eickstaedt <eickstaedt@deicon.de>
Dom Rodriguez <shymega@users.noreply.github.com>
Dongxu Wang <dongxu@apache.org>
DS/Charlie <82801887+ds-cbo@users.noreply.github.com>
Ed Ive <ed.ivve@gmail.com>
Edward Loveall <edward@edwardloveall.com>
Ellie Huxtable <ellie@atuin.sh>
Emanuele Panzeri <thepanz@gmail.com>
Eric Crosson <EricCrosson@users.noreply.github.com>
Eric Hodel <drbrain@segment7.net>
Eric Long <i@hack3r.moe>
Eric Ripa <eric@ripa.io>
Erwin Kroon <123574+ekroon@users.noreply.github.com>
eth3lbert <eth3lbert+dev@gmail.com>
Ethan Brierley <ethanboxx@gmail.com>
Evan McBeth <64177332+AtomicRobotMan0101@users.noreply.github.com>
Evan Purkhiser <evanpurkhiser@gmail.com>
Farid Zakaria <farid.m.zakaria@gmail.com>
Felix Yan <felixonmars@archlinux.org>
Frank Hamand <frankhamand@gmail.com>
frukto <fruktopus@gmail.com>
Gokul <appu.yess@gmail.com>
Hamza Hamud <53880692+hhamud@users.noreply.github.com>
Helmut K. C. Tessarek <tessarek@evermeet.cx>
Herby Gillot <herby.gillot@gmail.com>
Hesam Pakdaman <14890379+hesampakdaman@users.noreply.github.com>
Hilmar Wiegand <me@hwgnd.de>
Hunter Casten <41604962+enchantednatures@users.noreply.github.com>
Ian Manske <ian.manske@pm.me>
Ian Smith <iansmith@honeycomb.io>
Ian Smith <ismith@mit.edu>
Ilkin Bayramli <43158991+ibayramli@users.noreply.github.com>
Ivan Toriya <43750521+ivan-toriya@users.noreply.github.com>
J. Emiliano Deustua <edeustua@gmail.com>
Jakob Schrettenbrunner <dev@schrej.net>
Jakub Jirutka <jakub@jirutka.cz>
Jakub Panek <me@panekj.dev>
James Trew <66286082+jamestrew@users.noreply.github.com>
Jamie Quigley <jamie@quigley.xyz>
Jan Larres <jan@majutsushi.net>
Jannik <32144358+mozzieongit@users.noreply.github.com>
Jannik <jannik.peters@posteo.de>
Jax Young <jaxvanyang@gmail.com>
jean-santos <ewqjean@gmail.com>
jean-santos <jeanpnsantos@gmail.com>
Jeff Gould <JRGould@gmail.com>
Jeremy Cline <github@declined.dev>
Jeremy Cline <jeremy@jcline.org>
Jerome Ducret <jdiphone34@gmail.com>
jfmontanaro <jfmonty2@gmail.com>
Jinn Koriech <jinnko@users.noreply.github.com>
Jinna Kiisuo <jinna@nocturnal.fi>
Joe Ardent <nebkor@users.noreply.github.com>
Johannes Baiter <johannes.baiter@gmail.com>
Josef Friedrich <josef@friedrich.rocks>
JT <547158+jntrnr@users.noreply.github.com>
Julien P <julien@caffeine.lu>
Justin Su <injustsu@gmail.com>
János Illés <ijanos@gmail.com>
Kian-Meng Ang <kianmeng.ang@gmail.com>
Kjetil Jørgensen <kjetijor+github@gmail.com>
Klas Mellbourn <klas@mellbourn.net>
Koichi Murase <myoga.murase@gmail.com>
Korvin Szanto <Korvinszanto@gmail.com>
Krithic Kumar <30691152+notjedi@users.noreply.github.com>
Krut Patel <kroot.patel@gmail.com>
Laurent le Beau-Martin <1180863+laurentlbm@users.noreply.github.com>
lchausmann <jazz-github@zqz.dk>
LeoniePhiline <22329650+LeoniePhiline@users.noreply.github.com>
Luca Comellini <luca.com@gmail.com>
Lucas Burns <44355502+lmburns@users.noreply.github.com>
Lucas Trzesniewski <lucas.trzesniewski@gmail.com>
Lucy <lucy@absolucy.moe>
Luke Baker <lukebaker@gmail.com>
Luke Karrys <luke@lukekarrys.com>
Mag Mell <sakiiily@aosc.io>
Manel Vilar <manelvf@gmail.com>
Marcin Puc <tranzystorek.io@protonmail.com>
Marijan Smetko <msmetko@msmetko.xyz>
Mark Wotton <mwotton@gmail.com>
Martin Indra <martin.indra@mgn.cz>
Martin Junghanns <m.junghanns@mailbox.org>
Mat Jones <mat@mjones.network>
Matheus Martins <matheuscumth@gmail.com>
Matt Godbolt <matt@godbolt.org>
Matthew Berryman <matthew@acrossthecloud.net>
Matthias Beyer <mail@beyermatthias.de>
Matthieu LAURENT <matthieu.laurent69@protonmail.com>
Mattias Eriksson <mattias.eriksson@tutanota.com>
Maurice Escher <maurice.escher@sap.com>
Maxim Burgerhout <maxim@wzzrd.com>
Maxim Uvarov <maxim-uvarov@users.noreply.github.com>
mb6ockatf <104227451+mb6ockatf@users.noreply.github.com>
mentalisttraceur <mentalisttraceur@gmail.com>
Michael Bianco <iloveitaly@gmail.com>
Michael Mior <michael.mior@gmail.com>
Michael Vincent <377567+Vynce@users.noreply.github.com>
Michele Azzolari <michele@azzolari.it>
Michelle Tilley <michelle@michelletilley.net>
Mike Pastore <mwpastore@users.noreply.github.com>
Mike Tsao <mike@sowbug.com>
mmx <github@m2nx.com>
morguldir <morguldir@protonmail.com>
mundry <1453314+mundry@users.noreply.github.com>
Nelyah <Nelyah@users.noreply.github.com>
Nemo157 <git@nemo157.com>
networkException <git@nwex.de>
Nico Kokonas <nico@nicomee.com>
Niklas Hambüchen <mail@nh2.me>
noyez <noyez@ithryn.net>
Omer Katz <omer.drow@gmail.com>
onkelT2 <126604057+onkelT2@users.noreply.github.com>
Onè <43485962+c-git@users.noreply.github.com>
Orhun Parmaksız <orhunparmaksiz@gmail.com>
P T Weir <phil.weir@flaxandteal.co.uk>
Patrick <pmarschik@users.noreply.github.com>
Patrick Decat <pdecat@gmail.com>
Patrick Jackson <patrick@jackson.dev>
Pavel Ivanov <mr.pavel.ivanov@gmail.com>
Per Modin <pmodin@users.noreply.github.com>
Peter Brunner <peter@lugoues.net>
Peter Holloway <holloway.p.r@gmail.com>
Philippe Normand <phil@base-art.net>
Philippe Normand <philn@igalia.com>
Pierluigi <82404704+IoSonoPiero@users.noreply.github.com>
Plamen Dimitrov <pdimitrov@pevogam.com>
Poliorcetics <poliorcetics@users.noreply.github.com>
postmath <postmath@users.noreply.github.com>
printfn <1643883+printfn@users.noreply.github.com>
Qiming Xu <33349132+xqm32@users.noreply.github.com>
Rain <rain@sunshowers.io>
Ramses <ramses@well-founded.dev>
Remmy Cat Stock <3317423+remmycat@users.noreply.github.com>
Remo Senekowitsch <remo@buenzli.dev>
Reverier Xu <reverier.xu@outlook.com>
Richard de Boer <git@tubul.net>
Richard Jones <4550158+RichardDRJ@users.noreply.github.com>
Richard Turner <63139+zygous@users.noreply.github.com>
Robin Millette <robin@millette.info>
rriski <github@timoriski.fi>
Sam Edwards <sam@samedwards.ca>
Sam Lanning <sam@samlanning.com>
Samson <samson_gh@onepatchdown.net>
Sandro <sandro.jaeckel@gmail.com>
Satyarth Sampath <satyarth.23@gmail.com>
sdr135284 <54752759+sdr135284@users.noreply.github.com>
Shroomy <sporeventexplosion@gmail.com>
Simon <simon_bull@mckinsey.com>
Simon Elsbrock <simon@iodev.org>
slamp <slaamp@gmail.com>
Steve Kemp <steve@steve.org.uk>
Steven Xu <stevenxxiu@users.noreply.github.com>
Sven-Hendrik Haase <svenstaro@gmail.com>
Thomas Buckley-Houston <tom@tombh.co.uk>
Tobias Genannt <tobias.genannt@gmail.com>
Tobias Genannt <tobias.genannt@qbeyond.de>
Tobias Hunger <tobias.hunger@gmail.com>
Tom Cammann <cammann.tom@gmail.com>
Tom Cammann <tom.cammann@oracle.com>
Trygve Aaberge <trygveaa@gmail.com>
TymanWasTaken <tbeckman530@gmail.com>
Ubiquitous Photon <39134173+UbiquitousPhoton@users.noreply.github.com>
Violet Shreve <github@shreve.io>
Vlad Stepanov <8uk.8ak@gmail.com>
Vladislav Stepanov <8uk.8ak@gmail.com>
VuiMuich <jm.spam@gmx.net>
Webmaster At Cosmic DNA <92752640+DanielAtCosmicDNA@users.noreply.github.com>
Will Fancher <elvishjerricco@gmail.com>
Wind <WindSoilder@outlook.com>
WindSoilder <WindSoilder@outlook.com>
winston <hey@winston.sh>
wpbrz <61665187+wpbrz@users.noreply.github.com>
Xavier Vello <xavier.vello@gmail.com>
xfzv <78810647+xfzv@users.noreply.github.com>
Yannick Ulrich <yannick.ulrich@durham.ac.uk>
Yaroslav Halchenko <debian@onerussian.com>
Yolo <noah.chang@outlook.com>
Yonatan Goldschmidt <yon.goldschmidt@gmail.com>
YummyOreo <bobgim20@gmail.com>
Yuvi Panda <yuvipanda@gmail.com>
Zhanibek Adilbekov <zhanibek.adilbekov@proton.me>
ZhiHong Li <joker_lizhih@163.com>
Zhizhen He <hezhizhen.yi@gmail.com>
éclairevoyant <848000+eclairevoyant@users.noreply.github.com>
依云 <lilydjwg@gmail.com>
镜面王子 <153555712@qq.com>
```
## /Cargo.toml
```toml path="/Cargo.toml"
[workspace]
members = [
"crates/*",
"crates/atuin-nucleo/matcher",
"crates/atuin-nucleo/bench",
]
resolver = "2"
exclude = ["ui/backend", "crates/atuin-nucleo/matcher/fuzz"]
[workspace.package]
version = "18.15.2"
authors = ["Ellie Huxtable <ellie@atuin.sh>"]
rust-version = "1.95.0"
license = "MIT"
homepage = "https://atuin.sh"
repository = "https://github.com/atuinsh/atuin"
readme = "README.md"
[workspace.dependencies]
async-trait = "0.1.58"
atuin-client = { path = "crates/atuin-client", version = "18.15.2" }
atuin-common = { path = "crates/atuin-common", version = "18.15.2" }
atuin-daemon = { path = "crates/atuin-daemon", version = "18.15.2" }
atuin-dotfiles = { path = "crates/atuin-dotfiles", version = "18.15.2" }
atuin-history = { path = "crates/atuin-history", version = "18.15.2" }
atuin-kv = { path = "crates/atuin-kv", version = "18.15.2" }
atuin-scripts = { path = "crates/atuin-scripts", version = "18.15.2" }
atuin-server = { path = "crates/atuin-server", version = "18.15.2" }
atuin-server-database = { path = "crates/atuin-server-database", version = "18.15.2" }
atuin-server-postgres = { path = "crates/atuin-server-postgres", version = "18.15.2" }
atuin-server-sqlite = { path = "crates/atuin-server-sqlite", version = "18.15.2" }
atuin-nucleo = { path = "crates/atuin-nucleo", version = "0.6.0" }
atuin-nucleo-matcher = { path = "crates/atuin-nucleo/matcher", version = "0.3.1" }
base64 = "0.22"
crossterm = "0.29.0"
log = "0.4"
time = { version = "0.3.47", features = [
"serde-human-readable",
"macros",
"local-offset",
] }
clap = { version = "4.5.7", features = ["derive"] }
config = { version = "0.15.8", default-features = false, features = ["toml"] }
directories = "6.0.0"
eyre = "0.6"
fs-err = "3.1"
interim = { version = "0.2.0", features = ["time_0_3"] }
itertools = "0.14.0"
rand = { version = "0.8.5", features = ["std"] }
semver = "1.0.20"
serde = { version = "1.0.202", features = ["derive"] }
serde_json = "1.0.119"
tokio = { version = "1", features = ["full"] }
uuid = { version = "1.9", features = ["v4", "v7", "serde"] }
whoami = "2.1.0"
typed-builder = "0.18.2"
pretty_assertions = "1.3.0"
thiserror = "2"
rustix = { version = "1.1.4", features = ["process", "fs"] }
tower = "0.5"
tracing = "0.1"
ratatui = "0.30.0"
sql-builder = "3"
tempfile = { version = "3.19" }
minijinja = "2.9.0"
rustls = { version = "0.23", default-features = false, features = [
"ring",
"std",
"tls12",
] }
glob-match = "0.2.1"
vt100 = "0.16"
regex = "1.10.5"
toml_edit = "0.25.4"
[workspace.dependencies.tracing-subscriber]
version = "0.3"
features = ["ansi", "fmt", "registry", "env-filter", "json"]
[workspace.dependencies.reqwest]
version = "0.13"
features = ["json", "rustls-no-provider", "stream"]
default-features = false
[workspace.dependencies.sqlx]
version = "0.8"
features = ["runtime-tokio-rustls", "time", "postgres", "uuid"]
# The profile that 'cargo dist' will build with
[profile.dist]
inherits = "release"
lto = "thin"
strip = "symbols"
```
## /Dockerfile
``` path="/Dockerfile"
FROM lukemathwalker/cargo-chef:latest-rust-1.95.0-slim-bookworm AS chef
WORKDIR app
FROM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json
FROM chef AS builder
# Ensure working C compile setup (not installed by default in arm64 images)
RUN apt update && apt install build-essential -y
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json
COPY . .
RUN cargo build --release --bin atuin-server
FROM debian:bookworm-20260202-slim AS runtime
LABEL org.opencontainers.image.source="https://github.com/atuinsh/atuin" \
org.opencontainers.image.url="https://atuin.sh" \
org.opencontainers.image.licenses="MIT"
RUN useradd -c 'atuin user' atuin && mkdir /config && chown atuin:atuin /config
# Install ca-certificates for webhooks to work
RUN apt update && apt install ca-certificates -y && rm -rf /var/lib/apt/lists/*
WORKDIR app
USER atuin
ENV TZ=Etc/UTC
ENV RUST_LOG=atuin_server=info
ENV ATUIN_CONFIG_DIR=/config
COPY --from=builder /app/target/release/atuin-server /usr/local/bin
ENTRYPOINT ["/usr/local/bin/atuin-server"]
```
## /README.md
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/atuinsh/atuin/assets/53315310/13216a1d-1ac0-4c99-b0eb-d88290fe0efd">
<img alt="Text changing depending on mode. Light: 'So light!' Dark: 'So dark!'" src="https://github.com/atuinsh/atuin/assets/53315310/08bc86d4-a781-4aaa-8d7e-478ae6bcd129">
</picture>
</p>
<p align="center">
<em>magical shell history</em>
</p>
<hr/>
<p align="center">
<a href="https://github.com/atuinsh/atuin/actions?query=workflow%3ARust"><img src="https://img.shields.io/github/actions/workflow/status/atuinsh/atuin/rust.yml?style=flat-square" /></a>
<a href="https://crates.io/crates/atuin"><img src="https://img.shields.io/crates/v/atuin.svg?style=flat-square" /></a>
<a href="https://crates.io/crates/atuin"><img src="https://img.shields.io/crates/d/atuin.svg?style=flat-square" /></a>
<a href="https://github.com/atuinsh/atuin/blob/main/LICENSE"><img src="https://img.shields.io/crates/l/atuin.svg?style=flat-square" /></a>
<a href="https://discord.gg/Fq8bJSKPHh"><img src="https://img.shields.io/discord/954121165239115808" /></a>
<a rel="me" href="https://hachyderm.io/@atuin"><img src="https://img.shields.io/mastodon/follow/109944632283122560?domain=https%3A%2F%2Fhachyderm.io&style=social"/></a>
<a href="https://twitter.com/atuinsh"><img src="https://img.shields.io/twitter/follow/atuinsh?style=social" /></a>
</p>
[English] | [简体中文]
Atuin replaces your existing shell history with a SQLite database, and records
additional context for your commands. Additionally, it provides optional and
_fully encrypted_ synchronisation of your history between machines, via an Atuin
server.
<p align="center">
<img src="demo.gif" alt="animated" width="80%" />
</p>
<p align="center">
<em>exit code, duration, time and command shown</em>
</p>
As well as the search UI, it can do things like this:
```
# search for all successful `make` commands, recorded after 3pm yesterday
atuin search --exit 0 --after "yesterday 3pm" make
```
You may use either the server I host, or host your own! Or just don't use sync
at all. As all history sync is encrypted, I couldn't access your data even if
I wanted to. And I **really** don't want to.
## Features
- rebind `ctrl-r` and `up` (configurable) to a full screen history search UI
- store shell history in a sqlite database
- back up and sync **encrypted** shell history
- the same history across terminals, across sessions, and across machines
- log exit code, cwd, hostname, session, command duration, etc
- calculate statistics such as "most used command"
- old history file is not replaced
- quick-jump to previous items with <kbd>Alt-\<num\></kbd>
- switch filter modes via ctrl-r; search history just from the current session, directory, or globally
- enter to execute a command, tab to edit
## Documentation
- [Quickstart](#quickstart)
- [Install](https://docs.atuin.sh/guide/installation/)
- [Setting up sync](https://docs.atuin.sh/guide/sync/)
- [Import history](https://docs.atuin.sh/guide/import/)
- [Basic usage](https://docs.atuin.sh/guide/basic-usage/)
## Supported Shells
- zsh
- bash
- fish
- nushell
- xonsh
- powershell (tier 2 support)
## Community
### Forum
Atuin has a community forum, please ask here for help and support: <https://forum.atuin.sh/>
### IRC
We're also available via #atuin on libera.chat
### Discord
Atuin also has a community Discord, available [here](https://discord.gg/jR3tfchVvW)
# Quickstart
This will sign you up for the Atuin Cloud sync server. Everything is end-to-end encrypted, so your secrets are safe!
Read the [docs](https://docs.atuin.sh) for an offline setup, self-hosted server, and more.
```
curl --proto '=https' --tlsv1.2 -LsSf https://setup.atuin.sh | sh
atuin register -u <USERNAME> -e <EMAIL>
atuin import auto
atuin sync
```
Then restart your shell!
> [!NOTE]
>
> **For Bash users**: The above sets up `bash-preexec` for necessary hooks, but
> `bash-preexec` has limitations. For details, please see the
> [Bash](https://docs.atuin.sh/guide/installation/#installing-the-shell-plugin)
> section of the shell plugin documentation.
# Security
If you find any security issues, we'd appreciate it if you could alert <ellie@atuin.sh>
# Contributors
<a href="https://github.com/atuinsh/atuin/graphs/contributors">
<img src="https://contrib.rocks/image?repo=atuinsh/atuin&max=300" />
</a>
Made with [contrib.rocks](https://contrib.rocks).
[English]: ./README.md
[简体中文]: ./docs-i18n/zh-CN/README.md
## /atuin.nix
```nix path="/atuin.nix"
# Atuin package definition
#
# This file will be similar to the package definition in nixpkgs:
# https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/at/atuin/package.nix
#
# Helpful documentation: https://github.com/NixOS/nixpkgs/blob/master/doc/languages-frameworks/rust.section.md
{
lib,
stdenv,
installShellFiles,
rustPlatform,
libiconv,
}:
rustPlatform.buildRustPackage {
name = "atuin";
src = lib.cleanSource ./.;
cargoLock = {
lockFile = ./Cargo.lock;
# Allow dependencies to be fetched from git and avoid having to set the outputHashes manually
allowBuiltinFetchGit = true;
};
nativeBuildInputs = [installShellFiles];
buildInputs = lib.optionals stdenv.isDarwin [libiconv];
postInstall = ''
installShellCompletion --cmd atuin \
--bash <($out/bin/atuin gen-completions -s bash) \
--fish <($out/bin/atuin gen-completions -s fish) \
--zsh <($out/bin/atuin gen-completions -s zsh)
'';
doCheck = false;
meta = with lib; {
description = "Replacement for a shell history which records additional commands context with optional encrypted synchronization between machines";
homepage = "https://github.com/atuinsh/atuin";
license = licenses.mit;
mainProgram = "atuin";
};
}
```
## /atuin.plugin.zsh
```zsh path="/atuin.plugin.zsh"
# shellcheck disable=2148,SC2168,SC1090,SC2125
local FOUND_ATUIN=$+commands[atuin]
if [[ $FOUND_ATUIN -eq 1 ]]; then
source <(atuin init zsh)
fi
```
## /cliff.toml
```toml path="/cliff.toml"
# git-cliff ~ default configuration file
# https://git-cliff.org/docs/configuration
#
# Lines starting with "#" are comments.
# Configuration options are organized into tables and keys.
# See documentation for more information on available options.
[changelog]
# changelog header
header = """
# Changelog\n
All notable changes to this project will be documented in this file.\n
"""
# template for the changelog body
# https://keats.github.io/tera/docs/#introduction
body = """
{% if version %}\
## {{ version | trim_start_matches(pat="v") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits
| filter(attribute="scope")
| sort(attribute="scope") %}
- *({{commit.scope}})* {{ commit.message | upper_first }}
{%- if commit.breaking %}
{% raw %} {% endraw %}- **BREAKING**: {{commit.breaking_description}}
{%- endif -%}
{%- endfor -%}
{% raw %}\n{% endraw %}\
{%- for commit in commits %}
{%- if commit.scope -%}
{% else -%}
- {{ commit.message | upper_first }}
{% if commit.breaking -%}
{% raw %} {% endraw %}- **BREAKING**: {{commit.breaking_description}}
{% endif -%}
{% endif -%}
{% endfor -%}
{% raw %}\n{% endraw %}\
{% endfor %}\n
"""
# remove the leading and trailing whitespace from the template
trim = true
# changelog footer
footer = """
<!-- generated by git-cliff -->
"""
# postprocessors
postprocessors = [
{ pattern = '<REPO>', replace = "https://github.com/atuinsh/atuin" }, # replace repository URL
]
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))" }, # replace issue numbers
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "Features" },
{ message = "^fix", group = "Bug Fixes" },
{ message = "^doc", group = "Documentation" },
{ message = "^perf", group = "Performance" },
{ message = "^refactor", group = "Refactor" },
{ message = "^style", group = "Styling" },
{ message = "^test", group = "Testing" },
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^chore\\(deps\\)", skip = true },
{ message = "^chore\\(pr\\)", skip = true },
{ message = "^chore\\(pull\\)", skip = true },
{ message = "^chore|ci", group = "Miscellaneous Tasks" },
{ body = ".*security", group = "Security" },
{ message = "^revert", group = "Revert" },
]
# protect breaking changes from being skipped due to matching a skipping commit_parser
protect_breaking_commits = false
# filter out the commits that are not matched by commit parsers
filter_commits = false
# regex for matching git tags
tag_pattern = "v[0-9].*"
# regex for skipping tags
skip_tags = "v0.1.0-beta.1"
# regex for ignoring tags
ignore_tags = "prerelease|beta|alpha"
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"
# limit the number of commits included in the changelog.
# limit_commits = 42
```
## /contrib/pi/atuin.ts
```ts path="/contrib/pi/atuin.ts"
../../crates/atuin/contrib/pi/atuin.ts
```
## /crates/atuin-ai/Cargo.toml
```toml path="/crates/atuin-ai/Cargo.toml"
[package]
name = "atuin-ai"
edition = "2024"
description = "AI integration for Atuin CLI"
rust-version = { workspace = true }
version = { workspace = true }
authors = { workspace = true }
license = { workspace = true }
homepage = { workspace = true }
repository = { workspace = true }
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = []
tree-sitter = ["dep:tree-sitter-lib", "dep:tree-sitter-bash", "dep:tree-sitter-fish"]
[dependencies]
async-trait = { workspace = true }
atuin-client = { workspace = true }
atuin-common = { workspace = true }
tokio = { workspace = true }
eyre = { workspace = true }
clap = { workspace = true, features = ["derive", "env"] }
tracing = { workspace = true }
tracing-subscriber = { workspace = true, features = [
"ansi",
"fmt",
"registry",
"env-filter",
] }
directories = { workspace = true }
tracing-appender = "0.2.4"
reqwest = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
crossterm = { workspace = true, features = ["use-dev-tty", "event-stream"] }
ratatui = { workspace = true }
fs-err = { workspace = true }
futures = "0.3"
eventsource-stream = "0.2"
pulldown-cmark = "0.13.0"
async-stream = "0.3"
uuid = { workspace = true }
tui-textarea-2 = "0.10.2"
unicode-width = "0.2"
eye_declare = "0.4.3"
ratatui-core = "0.1"
ratatui-widgets = "0.3"
thiserror = { workspace = true }
glob-match = { workspace = true }
regex = { workspace = true }
time = { workspace = true }
toml = "1.1"
toml_edit = { workspace = true }
tree-sitter-lib = { package = "tree-sitter", version = "0.26.8", optional = true }
tree-sitter-bash = { version = "0.25.1", optional = true }
tree-sitter-fish = { version = "3.6.0", optional = true }
sqlx = { workspace = true, features = ["sqlite"] }
typed-builder = { workspace = true }
vt100 = { workspace = true }
chrono = "0.4"
chrono-humanize = "0.2"
[dev-dependencies]
pretty_assertions = { workspace = true }
tempfile = { workspace = true }
```
## /crates/atuin-ai/migrations/20260413000000_create_ai_sessions.sql
```sql path="/crates/atuin-ai/migrations/20260413000000_create_ai_sessions.sql"
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
head_id TEXT,
server_session_id TEXT,
directory TEXT,
git_root TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
archived_at INTEGER
);
CREATE INDEX idx_sessions_directory ON sessions(directory);
CREATE INDEX idx_sessions_git_root ON sessions(git_root);
CREATE INDEX idx_sessions_updated_at ON sessions(updated_at);
CREATE INDEX idx_sessions_created_at ON sessions(created_at);
CREATE TABLE IF NOT EXISTS session_events (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
parent_id TEXT,
invocation_id TEXT NOT NULL,
event_type TEXT NOT NULL,
event_data TEXT NOT NULL,
created_at INTEGER NOT NULL,
FOREIGN KEY (session_id) REFERENCES sessions(id)
);
CREATE INDEX idx_session_events_session_id ON session_events(session_id);
CREATE INDEX idx_session_events_parent_id ON session_events(parent_id);
CREATE INDEX idx_session_events_invocation_id ON session_events(invocation_id);
CREATE INDEX idx_session_events_created_at ON session_events(created_at);
```
## /crates/atuin-ai/render-tests.sh
```sh path="/crates/atuin-ai/render-tests.sh"
#!/bin/bash
# Render all test cases from test-renders.json
# Usage: ./render-tests.sh [test_name]
# With no args: renders all tests
# With arg: renders only matching test (e.g., ./render-tests.sh 05)
set -e
cd "$(dirname "$0")"
JSON_FILE="test-renders.json"
FILTER="${1:-}"
# Build once
cargo build -p atuin-ai --quiet
# Count tests
TOTAL=$(jq length "$JSON_FILE")
for i in $(seq 0 $((TOTAL - 1))); do
NAME=$(jq -r ".[$i].name" "$JSON_FILE")
DESC=$(jq -r ".[$i].description" "$JSON_FILE")
STATE=$(jq -c ".[$i].state" "$JSON_FILE")
# Skip if filter provided and doesn't match
if [[ -n "$FILTER" && ! "$NAME" =~ $FILTER ]]; then
continue
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "[$NAME] $DESC"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "$STATE" | cargo run -p atuin-ai --quiet -- debug-render -f plain
echo ""
done
```
## /crates/atuin-ai/replay-states.sh
```sh path="/crates/atuin-ai/replay-states.sh"
#!/bin/bash
# Replay state snapshots from a debug state JSONL file
# Usage: ./replay-states.sh <state-file.jsonl> [entry-number]
# With no entry: renders all frames in sequence (press Enter to advance)
# With entry number: renders just that frame
set -e
# cd "$(dirname "$0")"
STATE_FILE="${1:-}"
ENTRY_FILTER="${2:-}"
if [[ -z "$STATE_FILE" ]]; then
echo "Usage: $0 <state-file.jsonl> [entry-number]"
echo ""
echo "Examples:"
echo " $0 /tmp/state.jsonl # Interactive replay of all frames"
echo " $0 /tmp/state.jsonl 15 # Show just entry 15"
exit 1
fi
if [[ ! -f "$STATE_FILE" ]]; then
echo "Error: File not found: $STATE_FILE"
exit 1
fi
# Build once
cargo build -p atuin --quiet
# Count entries
TOTAL=$(wc -l < "$STATE_FILE" | tr -d ' ')
if [[ -n "$ENTRY_FILTER" ]]; then
# Show single entry
LINE=$(sed -n "${ENTRY_FILTER}p" "$STATE_FILE")
if [[ -z "$LINE" ]]; then
echo "Error: Entry $ENTRY_FILTER not found (file has $TOTAL entries)"
exit 1
fi
ENTRY=$(echo "$LINE" | jq -r '.entry')
LABEL=$(echo "$LINE" | jq -r '.label')
STATE=$(echo "$LINE" | jq -c '.state')
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "[$ENTRY/$TOTAL] $LABEL"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "$STATE" | cargo run -p atuin --quiet -- ai debug-render -f ansi
else
# Interactive replay
echo "Replaying $TOTAL frames from $STATE_FILE"
echo "Press Enter to advance, 'q' to quit, or number+Enter to jump"
echo ""
CURRENT=1
while [[ $CURRENT -le $TOTAL ]]; do
LINE=$(sed -n "${CURRENT}p" "$STATE_FILE")
ENTRY=$(echo "$LINE" | jq -r '.entry')
LABEL=$(echo "$LINE" | jq -r '.label')
STATE=$(echo "$LINE" | jq -c '.state')
clear
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "[$CURRENT/$TOTAL] $LABEL"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "$STATE" | cargo run -p atuin --quiet -- ai debug-render -f ansi
echo ""
echo "[Enter: next] [p: prev] [number: jump] [s: show state JSON] [q: quit]"
read -r INPUT
case "$INPUT" in
q|Q)
break
;;
p|P)
if [[ $CURRENT -gt 1 ]]; then
CURRENT=$((CURRENT - 1))
fi
;;
s|S)
echo ""
echo "State JSON:"
echo "$STATE" | jq .
echo ""
echo "Press Enter to continue..."
read -r
;;
''|' ')
CURRENT=$((CURRENT + 1))
;;
*[0-9]*)
if [[ "$INPUT" =~ ^[0-9]+$ ]] && [[ "$INPUT" -ge 1 ]] && [[ "$INPUT" -le $TOTAL ]]; then
CURRENT=$INPUT
else
echo "Invalid entry number (1-$TOTAL)"
sleep 1
fi
;;
esac
done
fi
```
## /crates/atuin-ai/src/commands.rs
```rs path="/crates/atuin-ai/src/commands.rs"
use std::{
fs,
path::{Path, PathBuf},
};
use atuin_common::shell::Shell;
use clap::{Args, Subcommand};
use eyre::Result;
use tracing_appender::rolling::{RollingFileAppender, Rotation};
use tracing_subscriber::{EnvFilter, Layer, fmt, layer::SubscriberExt, util::SubscriberInitExt};
pub mod init;
pub(crate) mod inline;
#[derive(Args, Debug)]
pub struct AiArgs {
/// Enable verbose logging
#[arg(short, long, global = true)]
verbose: bool,
/// Custom API endpoint; defaults to reading from the `ai.endpoint` setting.
#[arg(long, global = true)]
api_endpoint: Option<String>,
/// Custom API token; defaults to reading from the `ai.api_token` setting.
#[arg(long, global = true)]
api_token: Option<String>,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
/// Initialize shell integration
Init {
/// Shell to generate integration for; defaults to "auto"
#[arg(value_name = "SHELL", default_value = "auto")]
shell: String,
},
/// Inline completion mode with small TUI overlay
Inline {
#[command(flatten)]
args: AiArgs,
/// Current command line to complete
#[arg(value_name = "COMMAND")]
command: Option<String>,
/// Use the hook mode
#[arg(long, hide = true)]
hook: bool,
},
}
pub async fn run(
command: Commands,
settings: &atuin_client::settings::Settings,
) -> eyre::Result<()> {
match command {
Commands::Init { shell } => init::run(shell).await,
Commands::Inline {
command,
hook,
args,
..
} => {
if settings.logs.ai_enabled() {
init_logging(settings, args.verbose)?;
}
inline::run(command, args.api_endpoint, args.api_token, settings, hook).await
}
}
}
pub(crate) fn detect_shell() -> Option<String> {
Some(Shell::current().to_string())
}
/// Initializes logging for the AI commands.
fn init_logging(settings: &atuin_client::settings::Settings, verbose: bool) -> Result<()> {
// ATUIN_LOG env var overrides config file level settings
let env_log_set = std::env::var("ATUIN_LOG").is_ok();
// Base filter from env var (or empty if not set)
let base_filter =
EnvFilter::from_env("ATUIN_LOG").add_directive("sqlx_sqlite::regexp=off".parse()?);
// Use config level unless ATUIN_LOG is set
let filter = if env_log_set {
base_filter
} else {
EnvFilter::default()
.add_directive(settings.logs.ai_level().as_directive().parse()?)
.add_directive("sqlx_sqlite::regexp=off".parse()?)
};
let log_dir = PathBuf::from(&settings.logs.dir);
let ai_log_filename = settings.logs.ai.file.clone();
// Clean up old log files
cleanup_old_logs(&log_dir, &ai_log_filename, settings.logs.ai_retention());
let console_layer = if verbose {
Some(
fmt::layer()
.with_writer(std::io::stderr)
.with_ansi(true)
.with_target(false)
.with_filter(filter.clone()),
)
} else {
None
};
let file_appender = RollingFileAppender::new(Rotation::DAILY, &log_dir, &ai_log_filename);
let base = tracing_subscriber::registry().with(
fmt::layer()
.with_writer(file_appender)
.with_ansi(false)
.with_filter(filter),
);
if let Some(console_layer) = console_layer {
base.with(console_layer).init();
} else {
base.init();
};
Ok(())
}
fn cleanup_old_logs(log_dir: &Path, prefix: &str, retention_days: u64) {
let cutoff = std::time::SystemTime::now()
- std::time::Duration::from_secs(retention_days * 24 * 60 * 60);
let Ok(entries) = fs::read_dir(log_dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
continue;
};
// Match files like "search.log.2024-02-23" or "daemon.log.2024-02-23"
if !name.starts_with(prefix) || name == prefix {
continue;
}
if let Ok(metadata) = entry.metadata()
&& let Ok(modified) = metadata.modified()
&& modified < cutoff
{
let _ = fs::remove_file(&path);
}
}
}
```
## /crates/atuin-ai/src/commands/init.rs
```rs path="/crates/atuin-ai/src/commands/init.rs"
use crate::commands::detect_shell;
pub(crate) async fn run(shell: String) -> eyre::Result<()> {
let integration = match shell.as_str() {
"zsh" => generate_zsh_integration(),
"bash" => generate_bash_integration(),
"fish" => generate_fish_integration(),
"auto" => generate_auto_integration()?,
_ => eyre::bail!("Unsupported shell: {}", shell),
};
println!("{}", integration);
Ok(())
}
fn generate_auto_integration() -> eyre::Result<&'static str> {
let shell = detect_shell();
match shell.as_deref() {
Some("zsh") => Ok(generate_zsh_integration()),
Some("bash") => Ok(generate_bash_integration()),
Some("fish") => Ok(generate_fish_integration()),
Some(s) => eyre::bail!("Unsupported shell: {}", s),
None => eyre::bail!("Could not detect shell"),
}
}
/// Generate the zsh integration function - pure function for easy testing
pub fn generate_zsh_integration() -> &'static str {
r#"
# TUI uses an alternate screen, so no explicit cleanup is needed.
_atuin_ai_cleanup() {
true
}
# Question mark at start of line - natural language mode.
# Named with 'self-' prefix so bracketed-paste-magic activates it during
# paste, allowing url-quote-magic to escape ? in pasted URLs via self-insert.
self-atuin-ai-question-mark() {
# If buffer is empty or just contains '?', trigger natural language mode
if [[ -z "$BUFFER" || "$BUFFER" == "?" ]]; then
BUFFER=""
local output
output=$(atuin ai inline --hook 3>&1 1>&2 2>&3)
# Clean up the inline viewport
_atuin_ai_cleanup
if [[ $output == __atuin_ai_print__:* ]]; then
zle -I
echo "${output#__atuin_ai_print__:}"
elif [[ $output == __atuin_ai_cancel__ ]]; then
zle reset-prompt
elif [[ $output == __atuin_ai_execute__:* ]]; then
RBUFFER=""
LBUFFER=${output#__atuin_ai_execute__:}
zle reset-prompt
zle accept-line
elif [[ $output == __atuin_ai_insert__:* ]]; then
RBUFFER=""
LBUFFER=${output#__atuin_ai_insert__:}
zle reset-prompt
elif [[ -n $output ]]; then
RBUFFER=""
LBUFFER=$output
zle reset-prompt
else
zle reset-prompt
fi
else
zle self-insert
fi
}
# Set up keybindings
zle -N self-atuin-ai-question-mark
bindkey '?' self-atuin-ai-question-mark # Question mark
"#
.trim()
}
/// Generate the bash integration function - pure function for easy testing
pub fn generate_bash_integration() -> &'static str {
r#"
# Question mark at start of line - natural language mode
_atuin_ai_question_mark() {
# If buffer is empty or just contains '?', trigger natural language mode
if [[ -z "$READLINE_LINE" || "$READLINE_LINE" == "?" ]]; then
READLINE_LINE=""
READLINE_POINT=0
local output
output=$(atuin ai inline --hook 3>&1 1>&2 2>&3)
if [[ $output == __atuin_ai_print__:* ]]; then
echo "${output#__atuin_ai_print__:}"
READLINE_LINE=""
READLINE_POINT=0
elif [[ $output == __atuin_ai_cancel__ ]]; then
READLINE_LINE=""
READLINE_POINT=0
elif [[ $output == __atuin_ai_execute__:* ]]; then
# Execute the command immediately
READLINE_LINE=${output#__atuin_ai_execute__:}
READLINE_POINT=${#READLINE_LINE}
# Note: We can't directly execute in bash bind -x, but we can
# use a workaround by binding to a macro that accepts the line
bind '"\C-x\C-a": accept-line'
bind -x '"\C-x\C-e": _atuin_ai_question_mark'
elif [[ $output == __atuin_ai_insert__:* ]]; then
# Insert the command for editing
READLINE_LINE=${output#__atuin_ai_insert__:}
READLINE_POINT=${#READLINE_LINE}
elif [[ -n $output ]]; then
# Default: insert for editing
READLINE_LINE=$output
READLINE_POINT=${#READLINE_LINE}
fi
else
# Not at empty prompt, just insert the question mark
READLINE_LINE="${READLINE_LINE:0:READLINE_POINT}?${READLINE_LINE:READLINE_POINT}"
((READLINE_POINT++))
fi
}
# Set up keybindings
# Bash requires special handling: we use bind -x for the function,
# but need a two-step approach for execute mode
__atuin_ai_accept_line=""
_atuin_ai_question_mark_wrapper() {
_atuin_ai_question_mark
if [[ -n "$__atuin_ai_accept_line" ]]; then
__atuin_ai_accept_line=""
fi
}
bind -x '"?": _atuin_ai_question_mark'
"#
.trim()
}
/// Generate the fish integration function - pure function for easy testing
pub fn generate_fish_integration() -> &'static str {
r#"
# Question mark at start of line - natural language mode
function _atuin_ai_question_mark
set -l buf (commandline -b)
# If buffer is empty or just contains '?', trigger natural language mode
if test -z "$buf" -o "$buf" = "?"
commandline -r ""
# Run atuin ai inline, swapping stdout and stderr
set -l output (atuin ai inline --hook 3>&1 1>&2 2>&3 | string collect)
if string match --quiet '__atuin_ai_print__:*' "$output"
echo (string replace "__atuin_ai_print__:" "" -- "$output" | string collect)
commandline -f repaint
else if test "$output" = "__atuin_ai_cancel__"
commandline -f repaint
else if string match --quiet '__atuin_ai_execute__:*' "$output"
# Execute the command immediately
set -l cmd (string replace "__atuin_ai_execute__:" "" -- "$output" | string collect)
commandline -r "$cmd"
commandline -f repaint
commandline -f execute
else if string match --quiet '__atuin_ai_insert__:*' "$output"
# Insert the command for editing
set -l cmd (string replace "__atuin_ai_insert__:" "" -- "$output" | string collect)
commandline -r "$cmd"
commandline -f repaint
else if test -n "$output"
# Default: insert for editing
commandline -r "$output"
commandline -f repaint
else
commandline -f repaint
end
else
# Not at empty prompt, just insert the question mark
commandline -i "?"
end
end
# Set up keybindings
bind "?" _atuin_ai_question_mark
"#
.trim()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_zsh_integration() {
let result = generate_zsh_integration();
assert!(result.contains("self-atuin-ai-question-mark"));
assert!(result.contains("bindkey"));
assert!(result.contains("atuin ai inline --hook"));
assert!(result.contains("__atuin_ai_print__"));
assert!(result.contains("__atuin_ai_cancel__"));
assert!(result.contains("__atuin_ai_execute__"));
assert!(result.contains("__atuin_ai_insert__"));
assert!(result.contains("zle self-insert"));
}
#[test]
fn test_generate_bash_integration() {
let result = generate_bash_integration();
assert!(result.contains("_atuin_ai_question_mark"));
assert!(result.contains("bind"));
assert!(result.contains("READLINE_LINE"));
assert!(result.contains("atuin ai inline --hook"));
assert!(result.contains("__atuin_ai_print__"));
assert!(result.contains("__atuin_ai_cancel__"));
assert!(result.contains("__atuin_ai_execute__"));
assert!(result.contains("__atuin_ai_insert__"));
}
#[test]
fn test_generate_fish_integration() {
let result = generate_fish_integration();
assert!(result.contains("_atuin_ai_question_mark"));
assert!(result.contains("bind"));
assert!(result.contains("commandline"));
assert!(result.contains("atuin ai inline --hook"));
assert!(result.contains("__atuin_ai_print__"));
assert!(result.contains("__atuin_ai_cancel__"));
assert!(result.contains("__atuin_ai_execute__"));
assert!(result.contains("__atuin_ai_insert__"));
}
}
```
## /crates/atuin-ai/src/commands/inline.rs
```rs path="/crates/atuin-ai/src/commands/inline.rs"
use std::path::PathBuf;
use std::sync::mpsc;
use crate::context::{AppContext, ClientContext};
use crate::session::{LocalSessionService, SessionManager, SessionService};
use crate::tui::dispatch;
use crate::tui::events::AiTuiEvent;
use crate::tui::state::{ExitAction, Session};
use crate::tui::view::ai_view;
use atuin_client::database::{Database, Sqlite};
use eye_declare::{Application, CtrlCBehavior};
use eyre::{Context as _, Result, bail};
use tracing::{debug, info};
pub(crate) async fn run(
initial_command: Option<String>,
api_endpoint: Option<String>,
api_token: Option<String>,
settings: &atuin_client::settings::Settings,
output_for_hook: bool,
) -> Result<()> {
if settings.ai.enabled == Some(false) {
return Ok(());
}
if settings.ai.enabled.is_none() {
match prompt_ai_setup()? {
SetupChoice::EnableAi => {
set_ai_enabled(true).await?;
}
SetupChoice::DisableKeybind => {
set_ai_enabled(false).await?;
emit_shell_result(Action::Cancel, output_for_hook);
return Ok(());
}
SetupChoice::Cancel => {
emit_shell_result(Action::Cancel, output_for_hook);
return Ok(());
}
}
}
let endpoint = api_endpoint.as_deref().unwrap_or(
settings
.ai
.endpoint
.as_deref()
.unwrap_or("https://hub.atuin.sh"),
);
let api_token = api_token.as_deref().or(settings.ai.api_token.as_deref());
let token = if let Some(token) = &api_token {
token.to_string()
} else {
ensure_hub_session(settings).await?
};
let history_db_path = PathBuf::from(settings.db_path.as_str());
let history_db = Sqlite::new(history_db_path, settings.local_timeout)
.await
.context("failed to open history database for AI")?;
// Support both legacy [ai] send_cwd and new [ai.opening] send_cwd
let send_cwd =
settings.ai.opening.send_cwd.unwrap_or(false) || settings.ai.send_cwd.unwrap_or(false);
let last_command = if settings.ai.opening.send_last_command.unwrap_or(false) {
history_db.last().await.ok().flatten().map(|h| h.command)
} else {
None
};
let git_root = std::env::current_dir()
.ok()
.and_then(|cwd| atuin_common::utils::in_git_repo(cwd.to_str()?));
let ctx = AppContext {
endpoint: endpoint.to_string(),
token,
send_cwd,
last_command,
history_db: std::sync::Arc::new(history_db),
git_root,
capabilities: settings.ai.capabilities.clone(),
};
let action = run_inline_tui(ctx, initial_command, settings).await?;
emit_shell_result(action, output_for_hook);
Ok(())
}
async fn ensure_hub_session(settings: &atuin_client::settings::Settings) -> Result<String> {
if let Some(token) = atuin_client::hub::get_session_token().await? {
debug!("Found Hub session, using existing token");
return Ok(token);
}
let hub_address = settings.active_hub_endpoint().unwrap_or_default();
let will_sync = settings.is_hub_sync();
info!("No Hub session found, prompting for authentication");
println!("Atuin AI requires authenticating with Atuin Hub.");
if will_sync {
println!(
"Once logged in, your shell history will be synchronized via Atuin Hub if auto_sync is enabled or when manually syncing."
);
}
println!(
"If you have an existing Atuin sync account, you can log in with your existing credentials."
);
println!("Press enter to begin (or esc to cancel).");
if !wait_for_login_confirmation()? {
bail!("authentication canceled");
}
debug!("Starting Atuin Hub authentication...");
println!("Authenticating with Atuin Hub...");
let session = atuin_client::hub::HubAuthSession::start(hub_address.as_ref()).await?;
println!("Open this URL to continue:");
println!("{}", session.auth_url);
let token = session
.wait_for_completion(
atuin_client::hub::DEFAULT_AUTH_TIMEOUT,
atuin_client::hub::DEFAULT_POLL_INTERVAL,
)
.await?;
info!("Authentication complete, saving session token");
atuin_client::hub::save_session(&token).await?;
if let Ok(meta) = atuin_client::settings::Settings::meta_store().await
&& let Ok(Some(cli_token)) = meta.session_token().await
{
debug!("CLI session found, attempting to link accounts");
if let Err(e) = atuin_client::hub::link_account(hub_address.as_ref(), &cli_token).await {
debug!("Could not link CLI account to Hub: {}", e);
} else {
info!("Successfully linked CLI account to Hub");
}
}
Ok(token)
}
// ───────────────────────────────────────────────────────────────────
async fn run_inline_tui(
ctx: AppContext,
initial_prompt: Option<String>,
settings: &atuin_client::settings::Settings,
) -> Result<Action> {
let client_ctx = ClientContext::detect();
// Open the session service and check for a resumable session
let service = LocalSessionService::open(&settings.ai.db_path, settings.local_timeout)
.await
.context("failed to open AI session database")?;
let cwd = std::env::current_dir()
.ok()
.map(|p| p.to_string_lossy().into_owned());
let git_root_str = ctx
.git_root
.as_ref()
.map(|p| p.to_string_lossy().into_owned());
let session_window_mins = settings.ai.session_continue_minutes.max(0); // treat negative values as 0 to avoid confusion
let max_age_secs: i64 = session_window_mins * 60;
let resumable = service
.find_resumable(cwd.as_deref(), git_root_str.as_deref(), max_age_secs)
.await?;
let (mut session_mgr, initial_state) = if let Some(stored) = resumable {
debug!(session_id = %stored.id, "resuming AI session");
let (mgr, events, server_sid, last_event_ts, invocation_id) =
SessionManager::resume(Box::new(service), &stored).await?;
// Only treat this as a meaningful resume if there are API-visible events
// (not just OutOfBandOutput or SystemContext).
let has_api_content = events.iter().any(|e| e.is_api_content());
if has_api_content {
let mut session = Session::new(ctx.git_root.is_some(), Some(invocation_id));
session.conversation.events = events;
session.conversation.session_id = server_sid;
// Inject an invocation boundary so the LLM knows prior messages
// are from an earlier interaction.
session.conversation.events.push(
crate::tui::state::ConversationEvent::SystemContext {
content: "[Note: The user has started a new invocation of Atuin AI. Prior messages from this session are from an earlier invocation.]".to_string(),
},
);
session.view_start_index = session.conversation.events.len();
session.is_resumed = true;
session.last_event_time =
last_event_ts.and_then(|ts| chrono::DateTime::from_timestamp(ts, 0));
(mgr, session)
} else {
// No meaningful content — treat as a fresh session
debug!("resumable session has no API-visible content, starting fresh");
(
mgr,
Session::new(ctx.git_root.is_some(), Some(invocation_id)),
)
}
} else {
debug!("creating new AI session");
let mgr =
SessionManager::create_new(Box::new(service), cwd.as_deref(), git_root_str.as_deref());
(mgr, Session::new(ctx.git_root.is_some(), None))
};
let (tx, rx) = mpsc::channel::<AiTuiEvent>();
println!();
// If there's an initial prompt, send it as a SubmitInput event
// so it flows through the same path as user-typed input.
if let Some(prompt) = initial_prompt {
let _ = tx.send(AiTuiEvent::SubmitInput(prompt));
}
let (mut app, handle) = Application::builder()
.state(initial_state)
.view(ai_view)
.ctrl_c(CtrlCBehavior::Deliver)
.keyboard_protocol(eye_declare::KeyboardProtocol::Enhanced)
.bracketed_paste(true)
.with_context(tx.clone())
.extra_newlines_at_exit(1)
.build()?;
// Event loop: receives AiTuiEvent from components, mutates state via Handle.
// The dispatch thread processes events synchronously, including async persistence
// via block_on. It signals exit via an AtomicBool rather than querying the handle
// (which would hang if the TUI thread has already stopped processing).
let h = handle.clone();
let dispatch_handle = tokio::task::spawn_blocking(move || {
let mut dctx = dispatch::DispatchContext {
handle: &h,
tx: &tx,
app_ctx: &ctx,
client_ctx: &client_ctx,
session_mgr: &mut session_mgr,
exiting: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
};
while let Ok(event) = rx.recv() {
if !dispatch::dispatch(&mut dctx, event) {
break;
}
}
});
let run_result = app.run_loop().await;
// Wait for the dispatch thread to finish its final persist before the
// tokio runtime tears down. This prevents panics from block_on calls
// racing with runtime shutdown — including on the error path.
let _ = dispatch_handle.await;
run_result?;
// Map exit action to return value
let result = match app.state().exit_action {
Some(ExitAction::Execute(ref cmd)) => Action::Execute(cmd.clone()),
Some(ExitAction::Insert(ref cmd)) => Action::Insert(cmd.clone()),
_ => Action::Cancel,
};
Ok(result)
}
// ───────────────────────────────────────────────────────────────────
// Helpers
// ───────────────────────────────────────────────────────────────────
enum SetupChoice {
EnableAi,
DisableKeybind,
Cancel,
}
fn prompt_ai_setup() -> Result<SetupChoice> {
use crossterm::{
cursor,
event::{self, Event, KeyCode},
terminal,
};
let options = ["Enable Atuin AI", "Disable ? Keybind", "Cancel"];
let mut selected: usize = 0;
let mut stdout = std::io::stdout();
// Print header before raw mode so newlines render correctly.
// Use stdout because the shell hook swaps stdout/stderr — stdout goes
// to the terminal in both hook and non-hook modes.
println!();
println!(" Atuin AI is not yet configured.");
println!();
terminal::enable_raw_mode().context("failed to enable raw mode")?;
struct Guard;
impl Drop for Guard {
fn drop(&mut self) {
let _ = terminal::disable_raw_mode();
}
}
let _guard = Guard;
crossterm::execute!(stdout, cursor::Hide)?;
loop {
render_setup_options(&mut stdout, &options, selected)?;
let ev = event::read().context("failed to read key event")?;
crossterm::execute!(stdout, cursor::MoveUp(options.len() as u16))?;
if let Event::Key(key) = ev {
match key.code {
KeyCode::Up | KeyCode::Char('k') => {
selected = selected.saturating_sub(1);
}
KeyCode::Down | KeyCode::Char('j') if selected < options.len() - 1 => {
selected += 1;
}
KeyCode::Enter => break,
KeyCode::Esc => {
selected = 2;
break;
}
_ => {}
}
}
}
// Final render with selection visible
render_setup_options(&mut stdout, &options, selected)?;
crossterm::execute!(stdout, cursor::Show)?;
Ok(match selected {
0 => SetupChoice::EnableAi,
1 => SetupChoice::DisableKeybind,
_ => SetupChoice::Cancel,
})
}
fn render_setup_options(
w: &mut impl std::io::Write,
options: &[&str],
selected: usize,
) -> Result<()> {
use crossterm::{
style::Stylize,
terminal::{Clear, ClearType},
};
for (i, option) in options.iter().enumerate() {
if i == selected {
write!(w, "\r {}", format!("> {option}").bold().cyan())?;
} else {
write!(w, "\r {option}")?;
}
crossterm::execute!(w, Clear(ClearType::UntilNewLine))?;
write!(w, "\r\n")?;
}
w.flush()?;
Ok(())
}
async fn set_ai_enabled(enabled: bool) -> Result<()> {
let config_file = atuin_client::settings::Settings::get_config_path()?;
let config_str = tokio::fs::read_to_string(&config_file).await?;
let mut doc = config_str.parse::<toml_edit::DocumentMut>()?;
if !doc.contains_key("ai") {
doc["ai"] = toml_edit::table();
}
doc["ai"]["enabled"] = toml_edit::value(enabled);
tokio::fs::write(&config_file, doc.to_string()).await?;
if !enabled {
println!(
"Atuin AI keybind disabled. You can re-enable with `atuin config set ai.enabled true`.",
);
println!("Restart your shell for changes to take effect.");
// Two printlns to ensure the message is visible above the shell prompt after program ends.
println!();
println!();
}
Ok(())
}
fn wait_for_login_confirmation() -> Result<bool> {
use crossterm::{
event::{self, Event, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode},
};
enable_raw_mode().context("failed enabling raw mode for login prompt")?;
struct Guard;
impl Drop for Guard {
fn drop(&mut self) {
let _ = disable_raw_mode();
}
}
let _guard = Guard;
loop {
let ev = event::read().context("failed to read login confirmation key")?;
if let Event::Key(key) = ev {
match key.code {
KeyCode::Enter => return Ok(true),
KeyCode::Esc => return Ok(false),
_ => {}
}
}
}
}
#[derive(Clone)]
enum Action {
Execute(String),
Insert(String),
Cancel,
}
fn emit_shell_result(action: Action, output_for_hook: bool) {
if output_for_hook {
match action {
Action::Execute(output) => eprintln!("__atuin_ai_execute__:{output}"),
Action::Insert(output) => eprintln!("__atuin_ai_insert__:{output}"),
Action::Cancel => eprintln!("__atuin_ai_cancel__"),
}
} else {
match action {
Action::Execute(output) | Action::Insert(output) => {
println!("{output}");
}
Action::Cancel => {}
}
}
}
```
## /crates/atuin-ai/src/context.rs
```rs path="/crates/atuin-ai/src/context.rs"
use std::path::PathBuf;
use std::sync::Arc;
use atuin_client::distro::detect_linux_distribution;
use atuin_client::settings::AiCapabilities;
/// Session-scoped context for the AI chat session.
/// Holds the API configuration and client settings needed by the event loop and stream task.
#[derive(Clone, Debug)]
pub(crate) struct AppContext {
pub endpoint: String,
pub token: String,
pub send_cwd: bool,
pub last_command: Option<String>,
pub history_db: Arc<atuin_client::database::Sqlite>,
/// Git root of the current working directory, if inside a git repo.
/// Resolves through worktrees to the main repo root.
pub git_root: Option<PathBuf>,
pub capabilities: AiCapabilities,
}
/// Machine identity — computed once per session.
#[derive(Clone, Debug)]
pub(crate) struct ClientContext {
pub os: String,
pub shell: Option<String>,
pub distro: Option<String>,
}
impl ClientContext {
pub(crate) fn detect() -> Self {
let os = detect_os();
let shell = crate::commands::detect_shell();
let distro = if os == "linux" {
Some(detect_linux_distribution())
} else {
None
};
Self { os, shell, distro }
}
/// Serialize to the JSON format the API expects for the "context" field.
/// The `pwd` field is always dynamic (current working directory), so it's
/// computed fresh on each call if `send_cwd` is true.
pub(crate) fn to_json(&self, send_cwd: bool, last_command: Option<&str>) -> serde_json::Value {
let mut ctx = serde_json::json!({
"os": self.os,
"shell": self.shell,
"pwd": if send_cwd {
std::env::current_dir().ok().map(|p| p.to_string_lossy().into_owned())
} else {
None
},
"last_command": last_command,
});
if let Some(ref distro) = self.distro {
ctx["distro"] = serde_json::json!(distro);
}
ctx
}
}
/// Move the `detect_os` function here since it's about client identity.
fn detect_os() -> String {
match std::env::consts::OS {
"macos" => "macos".to_string(),
"linux" => "linux".to_string(),
"windows" => "windows".to_string(),
other => format!("Other: {other}"),
}
}
```
## /crates/atuin-ai/src/context_window.rs
```rs path="/crates/atuin-ai/src/context_window.rs"
//! Context window management for API requests.
//!
//! Full conversation events are always persisted to disk. This module handles
//! truncation at send time so the API payload stays within a character budget.
//!
//! Strategy: **frozen prefix + live tail**. The first N turns form a stable
//! prefix that stays identical across requests (maximizing prompt cache hits).
//! The most recent turns form the live tail. When the total exceeds the budget,
//! turns between prefix and tail are dropped with a truncation marker. The
//! prefix never shifts, avoiding cache invalidation.
use std::ops::Range;
use crate::tui::{ConversationEvent, events_to_messages};
/// Default character budget for the context window.
/// Roughly ~50K tokens at ~4 chars/token — generous enough that truncation
/// only kicks in for genuinely long sessions.
const DEFAULT_BUDGET_CHARS: usize = 200_000;
/// Number of initial turns to freeze as the stable prefix.
const FROZEN_PREFIX_TURNS: usize = 1;
/// Builds API messages from conversation events while respecting a character
/// budget using frozen prefix + live tail truncation.
pub(crate) struct ContextWindowBuilder {
budget: usize,
}
impl ContextWindowBuilder {
pub fn new(budget: usize) -> Self {
Self { budget }
}
pub fn with_default_budget() -> Self {
Self::new(DEFAULT_BUDGET_CHARS)
}
/// Build API messages from conversation events, applying the context
/// window budget. Returns the messages to send in the API request.
pub fn build(&self, events: &[ConversationEvent]) -> Vec<serde_json::Value> {
if events.is_empty() {
return Vec::new();
}
let turns = group_into_turns(events);
// Convert each turn's events to API messages independently.
// This is safe because the combining logic (Text + ToolCall merging)
// only operates within a single assistant response, which never
// spans turn boundaries.
let turn_messages: Vec<Vec<serde_json::Value>> = turns
.iter()
.map(|range| events_to_messages(&events[range.clone()]))
.collect();
let turn_chars: Vec<usize> = turn_messages.iter().map(|m| estimate_chars(m)).collect();
let total_chars: usize = turn_chars.iter().sum();
if total_chars <= self.budget {
return turn_messages.into_iter().flatten().collect();
}
// --- Over budget: apply frozen prefix + live tail ---
let prefix_count = FROZEN_PREFIX_TURNS.min(turns.len());
let prefix_chars: usize = turn_chars[..prefix_count].iter().sum();
let marker = truncation_marker();
let marker_chars = estimate_chars(std::slice::from_ref(&marker));
let mut remaining = self.budget.saturating_sub(prefix_chars + marker_chars);
// Work backwards from the end, accumulating tail turns that fit.
let mut tail_start = turns.len();
for i in (prefix_count..turns.len()).rev() {
if turn_chars[i] <= remaining {
remaining -= turn_chars[i];
tail_start = i;
} else {
break;
}
}
// Always include at least the most recent turn, even if it alone
// exceeds the budget — sending something is better than nothing.
if tail_start >= turns.len() && turns.len() > prefix_count {
tail_start = turns.len() - 1;
}
let mut result = Vec::new();
// Frozen prefix
for msgs in &turn_messages[..prefix_count] {
result.extend(msgs.iter().cloned());
}
// Truncation marker (only if turns were actually dropped)
if tail_start > prefix_count {
result.push(marker);
}
// Live tail
for msgs in &turn_messages[tail_start..] {
result.extend(msgs.iter().cloned());
}
result
}
}
/// Marker message inserted where turns were dropped. Uses user role since
/// the preceding prefix typically ends with an assistant message.
fn truncation_marker() -> serde_json::Value {
serde_json::json!({
"role": "user",
"content": "[Earlier conversation context was omitted to fit within the context window. The conversation continues below.]"
})
}
/// Group conversation events into turns. A new turn starts at each
/// `UserMessage` or `SystemContext` event. Everything between boundaries
/// belongs to the preceding turn (assistant text, tool calls, tool results,
/// out-of-band output).
fn group_into_turns(events: &[ConversationEvent]) -> Vec<Range<usize>> {
let mut turns = Vec::new();
let mut start = 0;
for (i, event) in events.iter().enumerate() {
if i > start
&& matches!(
event,
ConversationEvent::UserMessage { .. } | ConversationEvent::SystemContext { .. }
)
{
turns.push(start..i);
start = i;
}
}
if start < events.len() {
turns.push(start..events.len());
}
turns
}
/// Rough character-count estimate for a set of messages. Uses the JSON
/// serialization length as a proxy — not exact tokens, but proportional
/// and cheap to compute.
fn estimate_chars(messages: &[serde_json::Value]) -> usize {
messages.iter().map(|m| m.to_string().len()).sum()
}
#[cfg(test)]
mod tests {
use super::*;
fn user(content: &str) -> ConversationEvent {
ConversationEvent::UserMessage {
content: content.to_string(),
}
}
fn text(content: &str) -> ConversationEvent {
ConversationEvent::Text {
content: content.to_string(),
}
}
fn tool_call(id: &str, name: &str) -> ConversationEvent {
ConversationEvent::ToolCall {
id: id.to_string(),
name: name.to_string(),
input: serde_json::json!({"command": "ls"}),
}
}
fn tool_result(tool_use_id: &str, content: &str) -> ConversationEvent {
ConversationEvent::ToolResult {
tool_use_id: tool_use_id.to_string(),
content: content.to_string(),
is_error: false,
remote: false,
content_length: None,
}
}
fn system_context(content: &str) -> ConversationEvent {
ConversationEvent::SystemContext {
content: content.to_string(),
}
}
fn oob(content: &str) -> ConversationEvent {
ConversationEvent::OutOfBandOutput {
name: "test".to_string(),
command: None,
content: content.to_string(),
}
}
// --- group_into_turns ---
#[test]
fn empty_events_produce_no_turns() {
assert!(group_into_turns(&[]).is_empty());
}
#[test]
fn single_user_message_is_one_turn() {
let events = vec![user("hello")];
let turns = group_into_turns(&events);
assert_eq!(turns, vec![0..1]);
}
#[test]
fn user_assistant_is_one_turn() {
let events = vec![user("hello"), text("hi there")];
let turns = group_into_turns(&events);
assert_eq!(turns, vec![0..2]);
}
#[test]
fn two_turns_split_at_user_message() {
let events = vec![
user("first"),
text("response 1"),
user("second"),
text("response 2"),
];
let turns = group_into_turns(&events);
assert_eq!(turns, vec![0..2, 2..4]);
}
#[test]
fn tool_calls_and_results_stay_in_same_turn() {
let events = vec![
user("list files"),
text("Let me check"),
tool_call("tc1", "suggest_command"),
tool_result("tc1", "file1\nfile2"),
text("Here are your files"),
];
let turns = group_into_turns(&events);
assert_eq!(turns, vec![0..5]);
}
#[test]
fn system_context_starts_new_turn() {
let events = vec![
user("hello"),
text("hi"),
system_context("invocation boundary"),
user("next question"),
text("answer"),
];
let turns = group_into_turns(&events);
assert_eq!(turns, vec![0..2, 2..3, 3..5]);
}
#[test]
fn oob_events_stay_in_current_turn() {
let events = vec![user("hello"), oob("some output"), text("response")];
let turns = group_into_turns(&events);
assert_eq!(turns, vec![0..3]);
}
#[test]
fn leading_text_without_user_message() {
// Edge case: events start with assistant text (shouldn't happen
// normally but handle gracefully)
let events = vec![text("orphaned"), user("hello"), text("hi")];
let turns = group_into_turns(&events);
assert_eq!(turns, vec![0..1, 1..3]);
}
// --- ContextWindowBuilder ---
#[test]
fn empty_events_produce_empty_messages() {
let builder = ContextWindowBuilder::with_default_budget();
assert!(builder.build(&[]).is_empty());
}
#[test]
fn under_budget_returns_all_messages() {
let events = vec![user("hello"), text("hi"), user("how are you"), text("good")];
let builder = ContextWindowBuilder::with_default_budget();
let messages = builder.build(&events);
// Should produce 4 messages (2 user + 2 assistant)
assert_eq!(messages.len(), 4);
assert_eq!(messages[0]["role"], "user");
assert_eq!(messages[0]["content"], "hello");
assert_eq!(messages[1]["role"], "assistant");
assert_eq!(messages[1]["content"], "hi");
assert_eq!(messages[2]["role"], "user");
assert_eq!(messages[2]["content"], "how are you");
assert_eq!(messages[3]["role"], "assistant");
assert_eq!(messages[3]["content"], "good");
}
#[test]
fn over_budget_truncates_middle_turns() {
// Create events where each turn has known content. Use a tiny
// budget so truncation is triggered with just a few turns.
let events = vec![
user("turn-1-user"),
text("turn-1-assistant"),
user("turn-2-user"),
text("turn-2-assistant"),
user("turn-3-user"),
text("turn-3-assistant"),
user("turn-4-user"),
text("turn-4-assistant-final"),
];
// Calculate sizes to set budget that keeps turn 1 (prefix) + turn 4 (tail)
// but drops turns 2 and 3.
let all_messages = events_to_messages(&events);
let total_chars: usize = all_messages.iter().map(|m| m.to_string().len()).sum();
// Set budget to roughly half — enough for prefix + last turn + marker
let turn1_msgs = events_to_messages(&events[0..2]);
let turn4_msgs = events_to_messages(&events[6..8]);
let marker_chars = estimate_chars(std::slice::from_ref(&truncation_marker()));
let needed = estimate_chars(&turn1_msgs) + estimate_chars(&turn4_msgs) + marker_chars;
// Budget allows prefix + marker + last turn but not the middle turns
assert!(
needed < total_chars,
"test setup: needed ({needed}) should be less than total ({total_chars})"
);
let builder = ContextWindowBuilder::new(needed + 10); // small margin
let messages = builder.build(&events);
// Should have: turn 1 (2 msgs) + marker (1 msg) + turn 4 (2 msgs) = 5
assert_eq!(messages.len(), 5, "expected prefix + marker + tail");
assert_eq!(messages[0]["content"], "turn-1-user");
assert_eq!(messages[1]["content"], "turn-1-assistant");
assert!(
messages[2]["content"].as_str().unwrap().contains("omitted"),
"middle message should be truncation marker"
);
assert_eq!(messages[3]["content"], "turn-4-user");
assert_eq!(messages[4]["content"], "turn-4-assistant-final");
}
#[test]
fn very_tight_budget_keeps_prefix_and_last_turn() {
let events = vec![
user("first"),
text("response-1"),
user("second"),
text("response-2"),
user("third"),
text("response-3"),
];
// Budget of 1 — forces the "always include last turn" fallback
let builder = ContextWindowBuilder::new(1);
let messages = builder.build(&events);
// Should have prefix (turn 1) + marker + last turn (turn 3)
assert!(
messages.len() >= 3,
"should have at least prefix + marker + tail"
);
// First message should be from turn 1
assert_eq!(messages[0]["content"], "first");
// Last messages should be from the final turn
let last = messages.last().unwrap();
assert_eq!(last["content"], "response-3");
}
#[test]
fn single_turn_always_returned() {
let events = vec![user("hello"), text("hi there")];
// Even with a tiny budget, the single turn must be returned
let builder = ContextWindowBuilder::new(1);
let messages = builder.build(&events);
assert_eq!(messages.len(), 2);
}
#[test]
fn tool_calls_preserved_through_truncation() {
let events = vec![
// Turn 1: simple exchange
user("turn 1"),
text("response 1"),
// Turn 2: with tool calls (will be dropped)
user("turn 2"),
text("checking"),
tool_call("tc1", "suggest_command"),
tool_result("tc1", "output"),
text("done"),
// Turn 3: final turn (kept in tail)
user("turn 3"),
text("final response"),
];
// Budget that fits turn 1 + turn 3 + marker but not turn 2
let turn1 = events_to_messages(&events[0..2]);
let turn3 = events_to_messages(&events[7..9]);
let marker_cost = estimate_chars(std::slice::from_ref(&truncation_marker()));
let budget = estimate_chars(&turn1) + estimate_chars(&turn3) + marker_cost + 10;
let builder = ContextWindowBuilder::new(budget);
let messages = builder.build(&events);
// Verify turn 2 (the tool call turn) was dropped
let has_tool_use = messages.iter().any(|m| {
m["content"]
.as_array()
.is_some_and(|arr| arr.iter().any(|b| b["type"] == "tool_use"))
});
assert!(!has_tool_use, "tool call turn should have been truncated");
// Verify first and last turns present
assert_eq!(messages[0]["content"], "turn 1");
assert_eq!(messages.last().unwrap()["content"], "final response");
}
#[test]
fn tail_accumulates_multiple_turns_when_budget_allows() {
// Use long content so turn sizes dwarf the truncation marker.
let padding = "x".repeat(500);
let events = vec![
user(&format!("turn-1-user-{padding}")),
text(&format!("turn-1-response-{padding}")),
user(&format!("turn-2-user-{padding}")),
text(&format!("turn-2-response-{padding}")),
user(&format!("turn-3-user-{padding}")),
text(&format!("turn-3-response-{padding}")),
user(&format!("turn-4-user-{padding}")),
text(&format!("turn-4-response-{padding}")),
];
// Budget that fits everything except turn 2
let all = events_to_messages(&events);
let total = estimate_chars(&all);
let turn2 = events_to_messages(&events[2..4]);
let turn2_chars = estimate_chars(&turn2);
let marker_cost = estimate_chars(std::slice::from_ref(&truncation_marker()));
let budget = total - turn2_chars + marker_cost + 5;
assert!(
budget < total,
"budget must be less than total for truncation to trigger"
);
let builder = ContextWindowBuilder::new(budget);
let messages = builder.build(&events);
// Should have: prefix (t1: 2 msgs) + marker (1 msg) + t3 (2 msgs) + t4 (2 msgs) = 7
// (turn 2 dropped)
assert_eq!(messages.len(), 7);
assert!(
messages[0]["content"]
.as_str()
.unwrap()
.starts_with("turn-1-user-")
);
assert!(
messages[1]["content"]
.as_str()
.unwrap()
.starts_with("turn-1-response-")
);
assert!(messages[2]["content"].as_str().unwrap().contains("omitted"));
assert!(
messages[3]["content"]
.as_str()
.unwrap()
.starts_with("turn-3-user-")
);
assert!(
messages[4]["content"]
.as_str()
.unwrap()
.starts_with("turn-3-response-")
);
assert!(
messages[5]["content"]
.as_str()
.unwrap()
.starts_with("turn-4-user-")
);
assert!(
messages[6]["content"]
.as_str()
.unwrap()
.starts_with("turn-4-response-")
);
}
#[test]
fn no_marker_when_no_turns_dropped() {
// Two turns, both fit in budget
let events = vec![user("a"), text("b"), user("c"), text("d")];
let builder = ContextWindowBuilder::with_default_budget();
let messages = builder.build(&events);
// No truncation marker
assert_eq!(messages.len(), 4);
assert!(
!messages
.iter()
.any(|m| m["content"].as_str().is_some_and(|s| s.contains("omitted")))
);
}
#[test]
fn tool_use_and_tool_result_never_split() {
// Invariant: a tool_use and its matching tool_result must always
// end up in the same turn, so truncation can't orphan one from
// the other. This test verifies that ToolResult does NOT start
// a new turn boundary.
let padding = "x".repeat(500);
let events = vec![
// Turn 1 (prefix)
user(&format!("turn-1-{padding}")),
text(&format!("resp-1-{padding}")),
// Turn 2: contains a tool_use → tool_result pair (will be dropped)
user(&format!("turn-2-{padding}")),
text("checking"),
tool_call("tc1", "suggest_command"),
tool_result("tc1", &format!("output-{padding}")),
text(&format!("done-{padding}")),
// Turn 3 (tail)
user(&format!("turn-3-{padding}")),
text(&format!("resp-3-{padding}")),
];
// Budget that fits turn 1 + turn 3 + marker, but not turn 2
let turn1 = events_to_messages(&events[0..2]);
let turn3 = events_to_messages(&events[7..9]);
let marker_cost = estimate_chars(std::slice::from_ref(&truncation_marker()));
let budget = estimate_chars(&turn1) + estimate_chars(&turn3) + marker_cost + 10;
let builder = ContextWindowBuilder::new(budget);
let messages = builder.build(&events);
// Verify: every tool_use has a matching tool_result, and vice versa
let tool_use_ids: Vec<&str> = messages
.iter()
.filter_map(|m| m["content"].as_array())
.flatten()
.filter(|b| b["type"] == "tool_use")
.filter_map(|b| b["id"].as_str())
.collect();
let tool_result_ids: Vec<&str> = messages
.iter()
.filter_map(|m| m["content"].as_array())
.flatten()
.filter(|b| b["type"] == "tool_result")
.filter_map(|b| b["tool_use_id"].as_str())
.collect();
assert_eq!(
tool_use_ids, tool_result_ids,
"every tool_use must have a matching tool_result (and vice versa)"
);
// Turn 2 was dropped entirely, so no tool IDs should be present
assert!(
!tool_use_ids.contains(&"tc1"),
"dropped turn's tool_use should not appear"
);
}
}
```
## /crates/atuin-ai/src/event_serde.rs
```rs path="/crates/atuin-ai/src/event_serde.rs"
//! Manual serialization for ConversationEvent to/from storage format.
//!
//! The storage format is decoupled from the Rust enum so the two can evolve
//! independently. Each event is stored as an `(event_type, event_data)` pair
//! where `event_data` is a JSON string.
use eyre::{Result, eyre};
use serde_json::Value;
use crate::tui::ConversationEvent;
/// Serialize a ConversationEvent into an (event_type, event_data_json) pair
/// suitable for database storage.
pub(crate) fn serialize_event(event: &ConversationEvent) -> (String, String) {
match event {
ConversationEvent::UserMessage { content } => (
"user_message".to_string(),
serde_json::json!({ "content": content }).to_string(),
),
ConversationEvent::Text { content } => (
"text".to_string(),
serde_json::json!({ "content": content }).to_string(),
),
ConversationEvent::ToolCall { id, name, input } => (
"tool_call".to_string(),
serde_json::json!({
"id": id,
"name": name,
"input": input,
})
.to_string(),
),
ConversationEvent::ToolResult {
tool_use_id,
content,
is_error,
remote,
content_length,
} => (
"tool_result".to_string(),
serde_json::json!({
"tool_use_id": tool_use_id,
"content": content,
"is_error": is_error,
"remote": remote,
"content_length": content_length,
})
.to_string(),
),
ConversationEvent::OutOfBandOutput {
name,
command,
content,
} => (
"out_of_band_output".to_string(),
serde_json::json!({
"name": name,
"command": command,
"content": content,
})
.to_string(),
),
ConversationEvent::SystemContext { content } => (
"system_context".to_string(),
serde_json::json!({ "content": content }).to_string(),
),
}
}
/// Deserialize an (event_type, event_data_json) pair from storage back into a
/// ConversationEvent.
pub(crate) fn deserialize_event(event_type: &str, event_data: &str) -> Result<ConversationEvent> {
let data: Value = serde_json::from_str(event_data)
.map_err(|e| eyre!("failed to parse event_data JSON: {e}"))?;
match event_type {
"user_message" => Ok(ConversationEvent::UserMessage {
content: json_string(&data, "content")?,
}),
"text" => Ok(ConversationEvent::Text {
content: json_string(&data, "content")?,
}),
"tool_call" => Ok(ConversationEvent::ToolCall {
id: json_string(&data, "id")?,
name: json_string(&data, "name")?,
input: data
.get("input")
.cloned()
.ok_or_else(|| eyre!("tool_call missing 'input' field"))?,
}),
"tool_result" => Ok(ConversationEvent::ToolResult {
tool_use_id: json_string(&data, "tool_use_id")?,
content: json_string(&data, "content")?,
is_error: data
.get("is_error")
.and_then(Value::as_bool)
.ok_or_else(|| eyre!("tool_result missing 'is_error' field"))?,
remote: data.get("remote").and_then(Value::as_bool).unwrap_or(false),
content_length: data
.get("content_length")
.and_then(Value::as_u64)
.map(|v| v as usize),
}),
"out_of_band_output" => Ok(ConversationEvent::OutOfBandOutput {
name: json_string(&data, "name")?,
command: data
.get("command")
.and_then(|v| if v.is_null() { None } else { v.as_str() })
.map(String::from),
content: json_string(&data, "content")?,
}),
"system_context" => Ok(ConversationEvent::SystemContext {
content: json_string(&data, "content")?,
}),
other => Err(eyre!("unknown event type: {other}")),
}
}
fn json_string(data: &Value, field: &str) -> Result<String> {
data.get(field)
.and_then(Value::as_str)
.map(String::from)
.ok_or_else(|| eyre!("missing or non-string field '{field}'"))
}
#[cfg(test)]
mod tests {
use super::*;
fn round_trip(event: &ConversationEvent) -> ConversationEvent {
let (event_type, event_data) = serialize_event(event);
deserialize_event(&event_type, &event_data).unwrap()
}
#[test]
fn test_user_message() {
let event = ConversationEvent::UserMessage {
content: "hello world".to_string(),
};
let result = round_trip(&event);
assert!(
matches!(result, ConversationEvent::UserMessage { content } if content == "hello world")
);
}
#[test]
fn test_text() {
let event = ConversationEvent::Text {
content: "response text".to_string(),
};
let result = round_trip(&event);
assert!(
matches!(result, ConversationEvent::Text { content } if content == "response text")
);
}
#[test]
fn test_tool_call() {
let input = serde_json::json!({"command": "ls -la", "danger": "low"});
let event = ConversationEvent::ToolCall {
id: "tc_123".to_string(),
name: "suggest_command".to_string(),
input: input.clone(),
};
let result = round_trip(&event);
match result {
ConversationEvent::ToolCall {
id,
name,
input: result_input,
} => {
assert_eq!(id, "tc_123");
assert_eq!(name, "suggest_command");
assert_eq!(result_input, input);
}
_ => panic!("expected ToolCall"),
}
}
#[test]
fn test_tool_result() {
let event = ConversationEvent::ToolResult {
tool_use_id: "tc_123".to_string(),
content: "file contents here".to_string(),
is_error: false,
remote: false,
content_length: None,
};
let result = round_trip(&event);
match result {
ConversationEvent::ToolResult {
tool_use_id,
content,
is_error,
remote,
content_length,
} => {
assert_eq!(tool_use_id, "tc_123");
assert_eq!(content, "file contents here");
assert!(!is_error);
assert!(!remote);
assert!(content_length.is_none());
}
_ => panic!("expected ToolResult"),
}
}
#[test]
fn test_tool_result_error() {
let event = ConversationEvent::ToolResult {
tool_use_id: "tc_456".to_string(),
content: "permission denied".to_string(),
is_error: true,
remote: false,
content_length: None,
};
let result = round_trip(&event);
match result {
ConversationEvent::ToolResult { is_error, .. } => assert!(is_error),
_ => panic!("expected ToolResult"),
}
}
#[test]
fn test_tool_result_remote() {
let event = ConversationEvent::ToolResult {
tool_use_id: "tc_789".to_string(),
content: "ref:abc123".to_string(),
is_error: false,
remote: true,
content_length: Some(4096),
};
let result = round_trip(&event);
match result {
ConversationEvent::ToolResult {
remote,
content_length,
..
} => {
assert!(remote);
assert_eq!(content_length, Some(4096));
}
_ => panic!("expected ToolResult"),
}
}
#[test]
fn test_tool_result_backwards_compat() {
// Old stored data without remote/content_length fields should deserialize
// with defaults (remote=false, content_length=None)
let event = deserialize_event(
"tool_result",
r#"{"tool_use_id":"tc_old","content":"old result","is_error":false}"#,
)
.unwrap();
match event {
ConversationEvent::ToolResult {
remote,
content_length,
..
} => {
assert!(!remote);
assert!(content_length.is_none());
}
_ => panic!("expected ToolResult"),
}
}
#[test]
fn test_out_of_band_with_command() {
let event = ConversationEvent::OutOfBandOutput {
name: "System".to_string(),
command: Some("/help".to_string()),
content: "help text".to_string(),
};
let result = round_trip(&event);
match result {
ConversationEvent::OutOfBandOutput {
name,
command,
content,
} => {
assert_eq!(name, "System");
assert_eq!(command.as_deref(), Some("/help"));
assert_eq!(content, "help text");
}
_ => panic!("expected OutOfBandOutput"),
}
}
#[test]
fn test_out_of_band_without_command() {
let event = ConversationEvent::OutOfBandOutput {
name: "System".to_string(),
command: None,
content: "some output".to_string(),
};
let result = round_trip(&event);
match result {
ConversationEvent::OutOfBandOutput { command, .. } => {
assert!(command.is_none());
}
_ => panic!("expected OutOfBandOutput"),
}
}
#[test]
fn test_unknown_event_type() {
let result = deserialize_event("banana", "{}");
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("unknown event type")
);
}
#[test]
fn test_invalid_json() {
let result = deserialize_event("text", "not json");
assert!(result.is_err());
}
#[test]
fn test_missing_field() {
let result = deserialize_event("text", "{}");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("content"));
}
#[test]
fn test_text_with_special_characters() {
let event = ConversationEvent::Text {
content: "line1\nline2\ttab \"quotes\" \\backslash 🎉".to_string(),
};
let result = round_trip(&event);
assert!(
matches!(result, ConversationEvent::Text { content } if content == "line1\nline2\ttab \"quotes\" \\backslash 🎉")
);
}
#[test]
fn test_tool_call_with_nested_input() {
let input = serde_json::json!({
"command": "echo 'hello'",
"nested": { "a": [1, 2, 3], "b": null }
});
let event = ConversationEvent::ToolCall {
id: "tc_1".to_string(),
name: "execute_shell_command".to_string(),
input: input.clone(),
};
let result = round_trip(&event);
match result {
ConversationEvent::ToolCall {
input: result_input,
..
} => {
assert_eq!(result_input, input);
}
_ => panic!("expected ToolCall"),
}
}
#[test]
fn test_system_context() {
let event = ConversationEvent::SystemContext {
content: "[system: new invocation started]".to_string(),
};
let result = round_trip(&event);
assert!(
matches!(result, ConversationEvent::SystemContext { content } if content == "[system: new invocation started]")
);
}
}
```
## /crates/atuin-ai/src/lib.rs
```rs path="/crates/atuin-ai/src/lib.rs"
pub mod commands;
pub(crate) mod context;
pub(crate) mod context_window;
pub(crate) mod event_serde;
pub(crate) mod permissions;
pub(crate) mod session;
pub(crate) mod store;
pub(crate) mod stream;
pub(crate) mod tools;
pub(crate) mod tui;
```
## /crates/atuin-ai/src/permissions/check.rs
```rs path="/crates/atuin-ai/src/permissions/check.rs"
use eyre::Result;
use crate::{permissions::file::RuleFile, tools::PermissableToolCall};
pub(crate) struct PermissionRequest<'t> {
call: &'t (dyn PermissableToolCall + Send + Sync),
}
impl<'t> PermissionRequest<'t> {
pub fn new(call: &'t (dyn PermissableToolCall + Send + Sync)) -> Self {
Self { call }
}
}
pub(crate) enum PermissionResponse {
Allowed,
Denied,
Ask,
}
pub(crate) struct PermissionChecker {
files: Vec<RuleFile>,
}
impl PermissionChecker {
pub fn new(files: Vec<RuleFile>) -> Self {
Self { files }
}
pub async fn check<'t>(
&self,
request: &'t PermissionRequest<'t>,
) -> Result<PermissionResponse> {
// Files are in order from deepest to shallowest, so we can stop at the first match.
// Within a file, the priority is ask -> deny -> allow
// The first rule type that matches is the one that applies, even if a later rule would contradict it.
for file in &self.files {
for rule in &file.content.permissions.ask {
if request.call.matches_rule(rule) {
tracing::debug!(
"Permission 'ASK' by rule: {} in file: {}",
rule,
file.path.display()
);
return Ok(PermissionResponse::Ask);
}
}
for rule in &file.content.permissions.deny {
if request.call.matches_rule(rule) {
tracing::debug!(
"Permission 'DENY' by rule: {} in file: {}",
rule,
file.path.display()
);
return Ok(PermissionResponse::Denied);
}
}
for rule in &file.content.permissions.allow {
if request.call.matches_rule(rule) {
tracing::debug!(
"Permission 'ALLOW' by rule: {} in file: {}",
rule,
file.path.display()
);
return Ok(PermissionResponse::Allowed);
}
}
}
Ok(PermissionResponse::Ask)
}
}
```
## /crates/atuin-ai/src/permissions/file.rs
```rs path="/crates/atuin-ai/src/permissions/file.rs"
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::permissions::rule::Rule;
#[derive(Debug, Clone)]
pub(crate) struct RuleFile {
pub path: PathBuf,
pub content: RuleFileContent,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct RuleFileContent {
pub permissions: RuleFilePermissions,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct RuleFilePermissions {
#[serde(default)]
pub allow: Vec<Rule>,
#[serde(default)]
pub deny: Vec<Rule>,
#[serde(default)]
pub ask: Vec<Rule>,
}
```
## /crates/atuin-ai/src/permissions/mod.rs
```rs path="/crates/atuin-ai/src/permissions/mod.rs"
pub(crate) mod check;
pub(crate) mod file;
pub(crate) mod resolver;
pub(crate) mod rule;
pub(crate) mod shell;
pub(crate) mod walker;
pub(crate) mod writer;
```
## /crates/atuin-ai/src/permissions/resolver.rs
```rs path="/crates/atuin-ai/src/permissions/resolver.rs"
use std::path::PathBuf;
use eyre::Result;
use crate::permissions::check::{PermissionChecker, PermissionRequest, PermissionResponse};
use crate::permissions::walker::PermissionWalker;
use crate::permissions::writer;
use crate::tools::ClientToolCall;
/// Resolves permissions for client tool calls by walking the filesystem to find permission files,
pub(crate) struct PermissionResolver {
checker: PermissionChecker,
}
impl PermissionResolver {
/// Create a new resolver that walks from `working_dir` to root for project
/// permissions, and also checks the global permissions file.
pub async fn new(working_dir: PathBuf) -> Result<Self> {
let global_file = writer::global_permissions_path();
let mut walker = PermissionWalker::new(working_dir, Some(global_file));
walker.walk().await?;
let checker = PermissionChecker::new(walker.rules().to_owned());
Ok(Self { checker })
}
/// Check whether `tool` is allowed, denied, or needs user confirmation.
pub async fn check(&self, tool: &ClientToolCall) -> Result<PermissionResponse> {
let request = PermissionRequest::new(tool);
self.checker.check(&request).await
}
}
```
## /crates/atuin-ai/src/permissions/rule.rs
```rs path="/crates/atuin-ai/src/permissions/rule.rs"
use std::sync::OnceLock;
use regex::Regex;
use serde::{Deserialize, Serialize};
static RULE_RE: OnceLock<Regex> = OnceLock::new();
#[derive(Debug, thiserror::Error)]
pub(crate) enum RuleError {
#[error("invalid rule format: {0}")]
InvalidRule(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Rule {
pub tool: String,
pub scope: Option<String>,
}
impl std::fmt::Display for Rule {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.scope.as_ref() {
Some(scope) => write!(f, "{}({})", self.tool, scope),
None => write!(f, "{}", self.tool),
}
}
}
impl Serialize for Rule {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for Rule {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Self::try_from(s.as_str()).map_err(serde::de::Error::custom)
}
}
impl TryFrom<&str> for Rule {
type Error = RuleError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let value = value.trim();
let re = RULE_RE.get_or_init(|| Regex::new(r"^(\w+)(?:\((.*)\))?{{contextString}}quot;).unwrap());
let caps = re
.captures(value)
.ok_or(RuleError::InvalidRule(value.to_string()))?;
let tool = caps.get(1).unwrap().as_str().to_string();
let scope = caps.get(2).map(|m| m.as_str().to_string());
Ok(Rule { tool, scope })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rule_try_from() {
assert_eq!(
Rule::try_from("Read").unwrap(),
Rule {
tool: "Read".to_string(),
scope: None
}
);
assert_eq!(
Rule::try_from("Read(*)").unwrap(),
Rule {
tool: "Read".to_string(),
scope: Some("*".to_string())
}
);
assert_eq!(
Rule::try_from("Write(*.md)").unwrap(),
Rule {
tool: "Write".to_string(),
scope: Some("*.md".to_string())
}
);
assert_eq!(
Rule::try_from("Shell(git commit *)").unwrap(),
Rule {
tool: "Shell".to_string(),
scope: Some("git commit *".to_string())
}
);
assert_eq!(
Rule::try_from("Shell(echo ())").unwrap(),
Rule {
tool: "Shell".to_string(),
scope: Some("echo ()".to_string())
}
);
assert!(Rule::try_from("Shell(git commit *").is_err());
assert!(Rule::try_from("Shell(git commit *)!").is_err());
}
}
```
## /crates/atuin-ai/src/permissions/walker.rs
```rs path="/crates/atuin-ai/src/permissions/walker.rs"
use std::path::{Path, PathBuf};
use eyre::Result;
use tokio::task::JoinSet;
use crate::permissions::file::{RuleFile, RuleFileContent};
#[derive(Debug)]
struct FoundRuleFile {
depth: usize,
file: RuleFile,
}
pub(crate) struct PermissionWalker {
start: PathBuf,
/// Direct path to the global permissions file (e.g. `~/.config/atuin/permissions.ai.toml`).
global_permissions_file: Option<PathBuf>,
rules: Vec<RuleFile>,
}
impl PermissionWalker {
pub fn new(start: PathBuf, global_permissions_file: Option<PathBuf>) -> Self {
Self {
start,
global_permissions_file,
rules: Vec::new(),
}
}
pub fn rules(&self) -> &[RuleFile] {
&self.rules
}
/// Walks the filesystem starting from the start path and collecting permission files along the way.
/// Walks to the root, then checks the global permissions file, if any.
pub async fn walk(&mut self) -> Result<()> {
let dirs_to_check: Vec<PathBuf> = self.start.ancestors().map(PathBuf::from).collect();
let dir_count = dirs_to_check.len();
let mut set: JoinSet<Result<Option<FoundRuleFile>>> = JoinSet::new();
for (index, path) in dirs_to_check.into_iter().enumerate() {
set.spawn(async move {
match check_dir_for_permissions(&path).await {
Ok(Some(rule_file)) => Ok(Some(FoundRuleFile {
depth: index,
file: rule_file,
})),
Ok(None) => Ok(None),
Err(e) => Err(e),
}
});
}
// Check the global file separately (it's a direct file path, not a dir/.atuin/ pattern)
if let Some(global_path) = self.global_permissions_file.clone() {
let depth = dir_count; // sorts after all directory-walk entries
set.spawn(async move {
match load_permissions_file(&global_path).await {
Ok(Some(rule_file)) => Ok(Some(FoundRuleFile {
depth,
file: rule_file,
})),
Ok(None) => Ok(None),
Err(e) => Err(e),
}
});
}
let capacity = dir_count + usize::from(self.global_permissions_file.is_some());
let mut found = Vec::with_capacity(capacity);
while let Some(result) = set.join_next().await {
let result = result?; // JoinErrors result in failure to walk the filesystem
match result {
Ok(Some(FoundRuleFile { depth, file })) => {
found.push((depth, file));
}
Ok(None) => {
continue;
}
Err(e) => {
tracing::error!(
"Error while walking filesystem for permissions check; skipping: {}",
e
);
continue;
}
}
}
// join_next() returns in order of completion, not order of spawn
found.sort_by_key(|(depth, _)| *depth);
self.rules = found.into_iter().map(|(_, file)| file).collect();
Ok(())
}
}
/// Checks a directory for `.atuin/permissions.ai.toml` and returns the RuleFile if found.
async fn check_dir_for_permissions(path: &Path) -> Result<Option<RuleFile>> {
let file_path = path.join(".atuin").join("permissions.ai.toml");
load_permissions_file(&file_path).await
}
/// Load a permissions file from an exact path. Returns None if the file doesn't exist.
async fn load_permissions_file(file_path: &Path) -> Result<Option<RuleFile>> {
if !tokio::fs::try_exists(file_path).await? {
return Ok(None);
}
let raw = tokio::fs::read_to_string(file_path).await?;
let content: RuleFileContent = toml::from_str(&raw)?;
// Use the file's parent as the rule file path (for logging/debugging)
let path = file_path
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| file_path.to_path_buf());
Ok(Some(RuleFile { path, content }))
}
```
## /crates/atuin-ai/src/tui/components/mod.rs
```rs path="/crates/atuin-ai/src/tui/components/mod.rs"
pub(crate) mod atuin_ai;
pub(crate) mod input_box;
pub(crate) mod markdown;
pub(crate) mod select;
pub(crate) mod session_continue;
```
## /crates/atuin-ai/src/tui/content/help.md
Welcome to Atuin AI, an AI assistant in your terminal. You can ask it to generate a shell command for you, or ask general terminal or software questions.
Commands:
{commands}
For more information, see [https://docs.atuin.sh/cli/ai/introduction/](https://docs.atuin.sh/cli/ai/introduction/)
## /crates/atuin-ai/src/tui/mod.rs
```rs path="/crates/atuin-ai/src/tui/mod.rs"
pub(crate) mod components;
pub(crate) mod dispatch;
pub(crate) mod events;
pub(crate) mod slash;
pub(crate) mod state;
pub(crate) mod view;
pub(crate) use state::{ConversationEvent, Session, events_to_messages};
```
## /crates/atuin-dotfiles/src/store/alias.rs
```rs path="/crates/atuin-dotfiles/src/store/alias.rs"
```
The content has been capped at 50000 tokens. The user could consider applying other filters to refine the result. The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.