Metadata-Version: 2.5
Name: anki-cli
Version: 0.2.0
Summary: Hybrid Anki CLI for humans and agents
Project-URL: Homepage, https://github.com/ubermenchh/anki-cli
Project-URL: Repository, https://github.com/ubermenchh/anki-cli
Project-URL: Issues, https://github.com/ubermenchh/anki-cli/issues
Author: umangkaushik
License: MIT License
        
        Copyright (c) 2026 umangkaushik
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: anki,cli,education,flashcards,spaced-repetition
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Education
Classifier: Topic :: Utilities
Requires-Python: <3.14,>=3.12
Requires-Dist: betterproto<3,>=2.0.0b7
Requires-Dist: click
Requires-Dist: fsrs
Requires-Dist: httpx<1,>=0.27
Requires-Dist: markdownify>=1.2.2
Requires-Dist: pydantic
Requires-Dist: pyperclip>=1.11.0
Requires-Dist: rich
Provides-Extra: clipboard
Requires-Dist: pyperclip; extra == 'clipboard'
Provides-Extra: tui
Requires-Dist: prompt-toolkit<4,>=3; extra == 'tui'
Requires-Dist: textual<8,>=7; extra == 'tui'
Description-Content-Type: text/markdown

# anki-cli

Hybrid Anki CLI for humans and agents.

<p align="center">
  <a href="https://github.com/ubermenchh/anki-cli/actions/workflows/ci.yml"><img src="https://github.com/ubermenchh/anki-cli/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>
  <a href="https://pypi.org/project/anki-cli/"><img src="https://img.shields.io/pypi/v/anki-cli" alt="PyPI version"></a>
  <a href="https://pypi.org/project/anki-cli/"><img src="https://img.shields.io/pypi/pyversions/anki-cli" alt="Python versions"></a>
  <a href="https://pepy.tech/projects/anki-cli"><img src="https://static.pepy.tech/badge/anki-cli" alt="Total PyPI downloads"></a>
  <a href="LICENSE"><img src="https://img.shields.io/pypi/l/anki-cli" alt="License: MIT"></a>
  <a href="https://github.com/astral-sh/ruff"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff"></a>
</p>

<p align="center">
  <img src="assets/anki-cli.png" alt="anki-cli interactive REPL showing due card counts, keyboard shortcuts, and command autocomplete" width="700">
</p>

`anki-cli` supports both:

- AnkiConnect backend (use a running Anki Desktop instance)
- Direct SQLite backend (operate directly on collection files)

Alpha software: see the PyPI badge above for the current release.

## Install

From PyPI (recommended):

```bash
uv tool install anki-cli
anki --version
```

With optional TUI extras:

```bash
uv tool install "anki-cli[tui]"
```

## Quick Start

Inspect environment and backend:

```bash
anki status
anki version
```

`status` always exits 0 — it reports probe results in `data.ok`/`data.error`
rather than failing, so it works as a health check on hosts with no Anki.

List core entities:

```bash
anki decks
anki notetypes
anki tags
```

Query cards and notes:

```bash
anki cards --query "deck:Default is:due"       # full card details (first 1000; --limit 0 for all)
anki cards:ids --query "deck:Default is:due"   # ids only
anki notes --query "tag:verb"                  # note ids
anki browse --query "tag:verb"                 # interactive TUI
```

Inspect individual objects:

```bash
anki card --id 1234567890
anki note --id 1234567890
anki deck --deck "Default"
anki notetype --notetype "Basic"
```

## Backend Modes

Global backend selection:

```bash
anki --backend auto ...
anki --backend ankiconnect ...
anki --backend direct ...
```

Collection override (direct backend):

```bash
anki --backend direct --col "/path/to/collection.anki2" status
```

Anki profile selection (for multi-profile installs):

```toml
# ~/.config/anki-cli/config.toml
[collection]
anki_profile = "Work"
```

When set, collection discovery picks `<Anki2>/<anki_profile>/collection.anki2`
instead of the first profile found. A name matching no profile exits with code
3 and lists the available profiles — but only where a local collection is
actually required: on the AnkiConnect path the lookup is informational and a
miss never fails. `--col` takes precedence over it entirely.

Backend behavior:

- `auto`: detects and chooses best available backend
- `ankiconnect`: forwards search queries to `findCards` and `findNotes`
- `direct`: compiles queries to SQL and executes directly on the collection DB

The old `standalone` backend was removed: `prefer = "standalone"` in
`config.toml` and `ANKI_CLI_BACKEND=standalone` warn and run as `auto`;
`--backend standalone` is rejected at the CLI with a usage error (exit 2).

### Remote AnkiConnect

To connect to AnkiConnect running on a different machine (e.g. via Tailscale or LAN):

```toml
# ~/.config/anki-cli/config.toml
[backend]
prefer = "ankiconnect"
ankiconnect_url = "http://192.168.1.100:8765"
allow_non_localhost = true
```

The remote Anki Desktop must have AnkiConnect configured to accept non-localhost connections. Set `"webBindAddress"` to a specific interface address (e.g. your Tailscale IP) or `"0.0.0.0"` for all interfaces. If binding to all interfaces, consider restricting access with firewall rules.

Without `allow_non_localhost = true` the remote URL is never contacted — the
detection probe and all data operations refuse non-localhost hosts.

## Search Query Language

Filters follow Anki's own semantics:

- `deck:NAME` — the deck and its subdecks (`deck:Lang` covers `Lang::Spanish`), including cards visiting a filtered deck; `deck:*` (all), `deck:filtered` (cards in filtered decks); supports `*` glob. Name matching is case-insensitive for ASCII.
- `notetype:NAME` (or Anki's spelling `note:NAME`)
- `tag:NAME` — the tag and its children (`tag:verb` covers `verb::irregular`); `tag:none` for untagged notes, `tag:*` for every note; supports `*` glob
- `is:new` / `is:review` (by card type, so a suspended new card is still `is:new`), `is:learn`, `is:due` (learning/review cards whose due time has passed; never new cards), `is:suspended`, `is:buried`
- `added:N` — cards created in the last N scheduling days (`added:1` = since the last rollover)
- `flag:N`
- `prop:ivl>N`, `prop:due>N`, `prop:reps>N`, `prop:lapses>N` (`<`, `<=`, `=`, `>=`, `>`)
- `nid:ID`, `cid:ID`
- bare text and quoted text (`"specific text"`); escape a literal colon as `\:`

Any other `prefix:` (for example `card:`, `rated:`, `mid:`, `deck:current`, field searches like
`front:dog`) is rejected with `INVALID_INPUT` rather than silently searched as text.

The `--deck` option on `review`, `decks` and `deck` follows the same rule: it covers the named deck
and its subdecks, so a parent row in `decks` includes its children's counts.

Logical syntax:

- implicit `AND` from whitespace
- explicit `OR`
- unary `-` and `NOT`
- parentheses for grouping

Examples:

```bash
anki cards --query "deck:Japanese is:due"
anki cards --query "tag:verb -is:suspended"
anki notes --query "\"specific text\""
anki cards --query "(tag:a OR tag:b) is:new"
```

## Common Commands

### Cards

```bash
anki cards --query "deck:Default"          # details for every match
anki cards:ids --query "deck:Default"      # ids only
anki card --id 123
anki card:suspend --query "is:due"
anki card:unsuspend --id 123
anki card:move --query "tag:to-move" --deck "Archive"
anki card:flag --query "is:review" --flag 3
anki card:bury --query "deck:Default"        # direct backend only
anki card:unbury --deck "Default"           # direct backend only
anki card:reschedule --query "tag:reset-me" --days 3
anki card:reset --query "tag:relearn"
anki card:revlog --id 123 --limit 20
```

### Notes and tags

```bash
anki notes --query "deck:Default"
anki note --id 123
anki note:add --deck "Default" --notetype "Basic" --Front "Q" --Back "A"
anki note:add --deck "Default" --notetype "Basic" --Front "Q" --Back "A2" --allow-duplicate
anki note:edit --id 123 --Front "Updated Q" --Back "Updated A"
anki note:fields --id 123
anki note:delete --id 123 --yes
anki tag --tag "verb"
anki tag:add --query "deck:Default" --tag "important"
anki tag:remove --id 123 --tag "important"
anki tag:rename --from "old" --to "new"
```

Like Anki's own Add dialog, `note:add` refuses a note whose first field already exists in
the same notetype (exit 1) or is empty. `--allow-duplicate` lifts the duplicate check; it
also works on `note:bulk`, where refused items otherwise come back as `null` ids.

### Decks and notetypes

```bash
anki deck --deck "Default"
anki deck:create --deck "Japanese::Vocab"
anki deck:rename --from "Old" --to "New"
anki deck:delete --deck "Temporary" --yes
anki deck:config --deck "Default"
anki deck:config:set --deck "Default" --new-per-day 20 --reviews-per-day 200

anki notetypes
anki notetype --notetype "Basic"
anki notetype:create --notetype "MyType" --field "Front" --field "Back"
anki notetype:field:add --notetype "Basic" --field "Extra"
anki --yes notetype:field:remove --notetype "Basic" --field "Extra"   # deletes the field from every note
anki notetype:css --notetype "Basic" --set ".card { font-size: 18px; }"
```

### Review

```bash
anki review
anki review:next
anki review:show
anki review:preview --id 123
anki review:answer --id 123 --rating good
anki review:undo
```

Interactive TUI review (requires TUI extras, direct backend):

```bash
anki review:start --deck "Japanese"
```

## Output and Exit Codes

Global output formats:

```bash
anki --format json ...
anki --format table ...
anki --format md ...
anki --format csv ...
anki --format plain ...
```

Every command names its target the same way: `--deck`, `--notetype`, `--tag`, `--id`,
`--query`; `--field` is repeatable. Older spellings (`deck:create --name`,
`notetype:create --name`, `tag:add --tags`, `note:fields --fields a,b`) still work as
aliases. `anki commands --format json` lists every command, option, exit and error code.

Global options (`--format`, `--backend`, `--col`, `--yes`, `--copy`, `--no-color`) may be
written before or after the subcommand: `anki --yes note:delete --id 1` and
`anki note:delete --id 1 --yes` are the same command.

Every failure — including a mistyped option or command name — is reported through the
same envelope, so `--format json` always yields parseable stderr. Set `ANKI_CLI_DEBUG=1`
to append a Python traceback after an `INTERNAL_ERROR` envelope.

Exit codes:

- `0`: Success
- `1`: Backend operation failed (including unexpected errors)
- `2`: Invalid input, usage error, confirmation required, or unsupported operation
- `3`: No Anki backend found (auto mode could not reach AnkiConnect or find a collection)
- `4`: Entity not found
- `7`: Backend unavailable or collection locked
- `130`: Interrupted (Ctrl-C)

`status` is the exception: it reports detection failures as
`{"ok": true, "data": {"ok": false, "error": "..."}}` and still exits 0.

## AI Agent Integration

This repo ships a [`SKILL.md`](SKILL.md) at the project root — an [agent skill](https://agentskills.io) that AI coding agents can consume to operate `anki-cli` without human guidance. It covers the full command reference, JSON output structure, search query syntax, error codes, and common workflows.

### Any supported agent (recommended)

```bash
npx skills add ubermenchh/anki-cli
```

Automatically detects your installed agents and places the skill in the correct directory. Works with Claude Code, Codex, Cursor, Copilot, OpenCode, and others.

### Manual

Copy or symlink `SKILL.md` into your agent's skills directory:

| Agent | Skill directory |
|-------|-----------------|
| Claude Code | `~/.claude/skills/anki-cli/SKILL.md` |
| Codex | `~/.codex/skills/anki-cli/SKILL.md` |
| Cursor | `~/.cursor/skills/anki-cli/SKILL.md` |
| OpenCode | `~/.agents/skills/anki-cli/SKILL.md` |
| Project-level (any) | `.agents/skills/anki-cli/SKILL.md` |

```bash
mkdir -p ~/.claude/skills/anki-cli
cp SKILL.md ~/.claude/skills/anki-cli/SKILL.md
```

For other AI coding agents, point them at `SKILL.md` in the repo root or include it in your agent's context/system prompt.

## Safety Notes

- Use `--yes` for destructive operations (`note:delete`, `deck:delete`, `notetype:field:remove`).
- In direct mode, avoid write operations while Anki Desktop is open.
- If Anki Desktop is running, prefer `--backend ankiconnect`.
- `review:undo` (direct mode only) restores the card's previous state and deletes the revlog row written by the undone answer, matching Anki's own undo.
- In direct mode, `csum` for notes with HTML in the first field now matches Anki. Rows written by older anki-cli versions keep their old csum until Tools ▸ Check Database recomputes them; duplicate detection may miss them until then.

## Development

```bash
uv sync --group dev
uv run ruff check .
uv run ty check
uv run pytest
```

With optional TUI dependencies:

```bash
uv sync --group dev --extra tui
uv run pytest -m tui
```

### Adding a command

Commands live in `anki_cli/cli/commands/` and are declared with `@anki_command`
(`anki_cli/cli/command.py`), which owns the output envelope, the backend session, and
`BACKEND_UNAVAILABLE`. The body returns the success `data` and declares which other
exceptions become which error code:

```python
@anki_command("deck", errors={LookupError: ("ENTITY_NOT_FOUND", 4)})
@click.option("--deck", "deck_name", required=True)
def deck_cmd(cmd: CommandContext, deck_name: str) -> JSONValue:
    name = deck_name.strip()
    if not name:
        raise cmd.invalid("Deck name cannot be empty.")
    with cmd.errors(details={"deck": name}):
        return cmd.backend.get_deck(name)
```

Anything not mapped falls through to the entry-point mapper, which still renders an
envelope. `anki commands --format json` lists the result; the docs-sync tests check that
README and SKILL.md only mention commands and options that exist.

## License

MIT. See `LICENSE`.