#workflow-automation #ai-agent #automation #workflow #cli

app distill-cli

CLI tool that monitors AI agent sessions, identifies patterns, and proposes skills

20 releases (5 breaking)

Uses new Rust 2024

new 0.6.6 Apr 15, 2026
0.6.5 Apr 5, 2026
0.5.6 Mar 30, 2026
0.4.1 Mar 12, 2026
0.1.5 Mar 10, 2026

#1974 in Command line utilities

MIT license

1MB
21K SLoC

Rust 19K SLoC // 0.0% comments Shell 1.5K SLoC // 0.0% comments Python 533 SLoC // 0.0% comments

distill

distill icon

distill is an AI-agent-native CLI for turning repeated Claude/Codex/OpenCode work into reusable skills. It watches local agent sessions, proposes improvements, and lets you accept them with a quick review flow.

Install

# Homebrew (recommended)
brew install nclandrei/tap/distill

# crates.io
cargo install distill-cli --locked

The installed command is still distill.

Icon assets used by notifications and docs:

  • SVG: assets/icons/distill-icon.svg
  • PNG: assets/icons/png/color/distill-color-256.png

Requirements

  • Distill needs at least one supported agent CLI on PATH:
    • Claude Code via claude
    • Codex CLI via codex
    • OpenCode via opencode
  • Onboarding marks an agent as detected only when its CLI is discoverable on PATH.
  • Scans require the configured proposal_agent CLI to be installed and already authenticated.
  • Distill reads local session logs from:
    • Claude: ~/.claude/projects/**/*.jsonl
    • Codex: ~/.codex/sessions/**/*.jsonl
    • OpenCode: discovered via opencode session list --format json, exported via opencode export <session-id> (Distill falls back from --format json for newer CLIs)
  • If a scan stalls because the upstream agent is slow, raise DISTILL_AGENT_TIMEOUT_SECS (default: 7200 seconds) or set it to 0 to disable the timeout entirely.

Quick Start

which claude || which codex || which opencode
distill              # First-run onboarding (interactive TUI)
distill scan --now   # Scan sessions for new skill proposals
distill review       # Review proposals (accept/reject/edit/snooze/batch)
distill status       # Check config + pending proposals + last scan

Scheduled runs automatically keep draining pending scan backlog across future invocations. Each scheduled invocation uses a bounded catch-up loop so Distill keeps making progress without turning one timer fire into an unbounded background job. Daily, weekly, and monthly schedules target 09:00 in the local time zone.

Local Commit Checks (Git + jj)

For this repo, run:

make hooks-install

This configures:

  • Git pre-commit hook (via core.hooksPath) to run local checks before git commit
  • repo-local jj aliases:
    • jj safe-commit ...
    • jj safe-describe ...

The shared check pipeline auto-applies format/fixes, verifies tests, and runs the deterministic scan performance gate:

cargo fmt --all
cargo clippy --fix --allow-dirty --allow-staged --allow-no-vcs -- -D warnings
cargo fmt --all
cargo clippy -- -D warnings
cargo test
make perf-check

Notes:

  • jj currently does not have native commit hooks, so the practical equivalent is using the safe-* aliases for commit-time enforcement.
  • make local-checks runs this full pipeline, including the perf smoke check.
  • CI and Release also gate scanner performance with make perf-check.

Release Automation

Pushing to main triggers the release workflow. When the version in Cargo.toml has not been released yet, the workflow will:

  • publishes release artifacts to GitHub Releases
  • publishes the crate to crates.io as distill-cli
  • updates the Homebrew formula in nclandrei/homebrew-tap

The workflow creates the v<version> tag automatically. You do not need to push tags manually. If a non-draft GitHub release for the current version already exists, automatic push runs exit without rebuilding artifacts. Release publishing is gated on a successful make perf-check run first. For an intentional retry of the current version, use GitHub Actions workflow_dispatch on the Release workflow.

Required GitHub Actions secrets:

  • CARGO_REGISTRY_TOKEN: crates.io API token with publish access for distill-cli
  • HOMEBREW_TAP_TOKEN: GitHub token with push access to nclandrei/homebrew-tap

Release process:

# bump Cargo.toml version first
git push origin main

Commands

Command Description
distill Run onboarding on first run, otherwise print quick usage hints
distill onboard Run onboarding TUI explicitly
distill scan --now Run an immediate scan for skill proposals
distill review Review pending proposals in a TUI (a/r/e/s/A)
distill dedupe [--dry-run] Detect duplicate global skills and propose removals
distill sync-agents ... Propose AGENTS.md updates from project evidence
distill status Show config, pending proposals, existing skills
distill watch --install Install scheduled scan (launchd/systemd)
distill watch --uninstall Remove scheduled scan
distill notify --check Check for pending proposals (used by shell hook)

sync-agents examples:

  • distill sync-agents --projects /abs/repo --dry-run
  • distill sync-agents --projects /abs/repo1,/abs/repo2 --save-projects
  • distill sync-agents --all-configured
  • distill sync-agents --list-configured

Notifications

  • notifications: terminal|native|both|none
  • notification_icon: null or absolute icon path
  • Terminal notifications are text-only by default.
  • You can opt into inline terminal icons when supported by the terminal:
    • Ghostty/kitty-like terminals via kitty graphics protocol
    • iTerm2 via OSC 1337
  • Optional terminal controls:
    • DISTILL_TERMINAL_IMAGE=on|true|1 to enable inline terminal images
    • DISTILL_TERMINAL_IMAGE=off|false|0|none or unset to keep text-only terminal notifications
    • DISTILL_TERMINAL_IMAGE_PROTOCOL=ansi|kitty|iterm|none
  • If running inside tmux, enable passthrough for image rendering: set -g allow-passthrough on.
  • In tmux sessions, distill auto-detects the attached terminal (ghostty/kitty first, then iTerm) when inline images are enabled and falls back to text-only when passthrough is disabled.
  • SVG notification_icon values are rasterized to PNG for terminal inline rendering.
  • On Linux native notifications, notification_icon: null falls back to the built-in project icon automatically.
  • On macOS native notifications, distill tries terminal-notifier -appIcon <icon> first and falls back to AppleScript notification if that path fails.

Platform Notes

  • Scheduled scans use launchd on macOS and systemd --user on Linux.
  • On macOS, calendar-based scheduled runs that are missed during sleep fire on the next wake. Full power-off or logged-out periods still wait for the next matching scheduled slot after the user session returns.
  • On Linux, systemd --user timers use Persistent=true, so missed calendar slots catch up after the timer becomes active again.
  • Native notifications use terminal-notifier on macOS and notify-send on Linux when available.
  • If those native notifier tools are missing, terminal notifications still work when notifications is terminal or both.

For AI Agents

Use one-shot JSON modes to avoid TUI interaction.

Reference examples:

  • examples/onboarding.json
  • examples/review.json

1) Onboarding (non-interactive)

distill onboard --write-json onboarding.json
# edit onboarding.json
distill onboard --apply-json onboarding.json

onboarding.json fields:

  • format_version (currently 1)
  • agents ([{"name":"claude|codex|opencode","enabled":true|false}])
  • scan_interval (daily|weekly|monthly)
  • proposal_agent (claude|codex|opencode)
  • shell (zsh|bash|fish|other)
  • notifications (terminal|native|both|none)
  • notification_icon (null or absolute path)
  • install_shell_hook (true|false)
  • run_initial_scan (true|false) to start the first scan immediately after onboarding is applied

2) Review (non-interactive)

distill review --write-json review.json
# set decision for each proposal: accept | reject | skip
distill review --apply-json review.json

review.json behavior:

  • Contains all pending proposals plus an editable decision field
  • Missing decisions default to skip
  • Applying decisions writes skills, logs history, and removes processed proposals
  • Accepted skills are synced to:
    • ~/.codex/skills/<skill-name>/SKILL.md when Codex is enabled
    • ~/.agents/skills/<skill-name>/SKILL.md as the shared mirror written alongside Codex sync
    • ~/.claude/skills/<skill-name>/SKILL.md when Claude is enabled
    • ~/.config/opencode/skills/<skill-name>/SKILL.md when OpenCode is enabled

3) Stdin/stdout mode

Both onboarding and review JSON flags accept - as path:

  • --write-json - writes JSON to stdout
  • --apply-json - reads JSON from stdin

Dependencies

~14–20MB
~398K SLoC