Skip to content

v0.2.0

Latest

Choose a tag to compare

@ubermenchh ubermenchh released this 14 Oct 16:42
c347a96

anki-cli 0.2.0

The largest release so far: about 40 merged pull requests since 0.1.5. The direct (SQLite) backend now follows Anki's own rules much more closely, several data-loss bugs are fixed, the CLI always answers with a JSON envelope, and the TUI got a layout and performance pass.

Upgrade note: if you use the direct backend, close Anki Desktop before the first command after upgrading. 0.2.0 rewrites hierarchical deck names in your collection once, to the format Anki itself uses (see "Deck names" below). While Anki is running the migration is skipped and retried next time.

Breaking changes

  • anki cards no longer opens the TUI. It is now the full-detail JSON lister. The interactive browser is anki browse (b in the REPL; c still means cards). search stays as a hidden alias of cards. (#66)
  • Removed CLI surface. The init command, --quiet / --verbose, and the never-constructible standalone backend are gone. --backend standalone is a usage error (exit 2); prefer = "standalone" or ANKI_CLI_BACKEND=standalone warns and runs as auto. (#52)
  • Config changes. The backup.*, review.* and display.day_boundary_hour keys no longer exist; the next config:set rewrites config.toml without them. collection.anki_profile is now live, so multi-profile installs may resolve a different collection than before: pin one with --col or collection.path. Stale defaults written by older builds are ignored at load. (#52)
  • status payload. It now returns ok, backend, collection, reason and profile (or ok: false with error), always exits 0, and drops message. config:path drops backups and standalone_default. (#52)
  • Auto mode exits 3 when no backend is found. (#52)
  • Search semantics now match Anki. deck: and tag: (and --deck on review, decks, deck) include child decks and child tags. is:due no longer matches new cards. added:N means "created since the last rollover". is:new / is:review go by card type. Queries with an unknown prefix: now fail with INVALID_INPUT (exit 2) instead of returning wrong results; escape a literal colon as \:. (#63)
  • Duplicate and empty notes are refused on the direct backend, like AnkiConnect: note:add of an empty first field, or of a duplicate without --allow-duplicate, fails with BACKEND_OPERATION_FAILED (exit 1); in note:bulk such items come back as null ids. Duplicate lookup is scoped by notetype. (#61, #64)
  • Unopenable collections fail closed. A --col that is a directory or not a SQLite file now fails with BACKEND_UNAVAILABLE (exit 7) instead of proceeding. (#50)
  • AnkiConnect fields changes shape: from a map to a list, with field_names as the replacement. Cards and notes also gain keys (notetype_name, id, due_info, ...); nothing else is removed. (#69)
  • AnkiConnect bury / unbury now exit 7 (BACKEND_UNAVAILABLE, "Use --backend direct") with no request sent. They never worked over AnkiConnect; they used to exit 1. (#77)
  • review:undo reports revlog_deleted instead of revlog_id, and review:answer gains revlog_id (direct backend). (#49)
  • Detection errors now carry the real meta.command instead of "bootstrap", and details = {"backend": "none", "requested_backend": ...}. (#76)
  • cards:ids also returns total.

Deck names

Hierarchical deck names are now stored the way Anki stores them (\x1f between components) and shown and typed with ::. Existing collections are migrated once, before the first connection. The migration skips while Anki is running, leaves rows with empty components or an existing native twin alone (logged), and marks changed rows for sync. Known limits: a new name typed with ::: is read as :: plus :, and deck: search cannot resolve names containing :::. (#91)

Data safety (direct backend)

  • Deleting a filtered deck no longer hard-deletes the cards visiting it; they return to their home deck. Deleting a normal deck also removes cards on loan to a filtered deck. Deck 1 cannot be deleted. (#40)
  • Removing a notetype field now rewrites every note of that notetype (field slots, sfld, csum, sort field, requirements). Adding a field appends an empty slot to every note. (#40, #47)
  • col.mod is bumped on every write, and col.scm on schema changes, so Anki Desktop actually sees and syncs CLI edits. Schema-changing results report full_sync_required and a warning. (#47)
  • The write guard fails closed. A ?, # or % in the collection path no longer disables the lock probe, and an inconclusive probe refuses to write. (#50)
  • Collections older than schema 18 are refused with an upgrade hint instead of a mid-command no such table crash. (#54)
  • Deck subtree matching is literal and case-folded like Anki. Before, _ and % in a deck name acted as wildcards: deleting A_B could also delete AXB::child and its cards. (#74)
  • Filtered-deck odid/odue are honoured when answering, resetting, rescheduling and moving cards. (#53)

Scheduling fidelity (direct backend)

  • The scheduling day is computed like Anki: rollover hour, timezone and creation offset, ported from rslib with its own test vectors. (#55)
  • Day-learn cards (queue 3) are treated as day indexes, not epochs, so they no longer count as due immediately or come back from unsuspend due in 1970. Unsuspend and unbury restore the right queue. (#48)
  • FSRS: the learning step is recovered correctly, 17- and 19-parameter sets from Anki 24.06 to 24.10 no longer crash, preview and answer use the same fuzz, review-Again is logged correctly, early reviews log as filtered, and the non-Anki lrt key is no longer written. (#56)
  • Empty learning or relearning step lists are respected, as in Anki's preset editor. (#58)
  • add_note generates cards from the templates, as Anki does, instead of one per template, and reqs are recomputed on every notetype change. (#59)
  • The checksum, emptiness test, duplicate confirmation and sfld all use markup-stripped text like Anki, so <b>Q</b> and Q are duplicates. Notes written by older anki-cli builds keep their old checksum until Tools > Check Database. (#50, #61, #64)
  • The next-card picker no longer mixes learning units, so a day-learn card cannot always beat an intraday one. (#69)

CLI and agents

  • A JSON envelope for every exit. Unknown commands and options, locked collections (COLLECTION_LOCKED), Ctrl-C (INTERRUPTED, exit 130) and internal bugs (INTERNAL_ERROR, with ANKI_CLI_DEBUG=1 for the traceback) all produce an envelope. Global options such as --format and --yes work after the subcommand. (#65)
  • anki commands --format json describes every command, option, global option, and the exit and error codes. (#66)
  • note:bulk accepts the documented flat shape and the nested {fields, tags} shape. Canonical flags are --deck, --notetype, --tags and repeatable --field; the old spellings still work. decks has one shape across formats. review:answer warnings go to meta.warnings, so stderr is empty on success. (#66)
  • Option values containing = are no longer rewritten as key=value sugar. (#60)
  • Docs drift guard: every anki ... example in README and SKILL.md is checked against the real command registry in CI. (#65, #66)
  • One card/note/deck/notetype shape on both backends, with an "Object Shapes" section in SKILL.md. (#69)
  • --offset for cards / search, and --limit / --offset for cards:ids. (#92)
  • Bare anki opens the REPL again, config lockouts are cured, and config:set rejects values that would break the next run. (#52)

AnkiConnect backend

  • Detection is lazy and happens once per run, with a single version probe and the lock probe before pgrep. A detection failure keeps its exit code (3 or 7). (#76)
  • Connection reuse with a single retry on a dropped keep-alive socket, and split timeouts (2 s connect, 30 s read). (#77)
  • Only documented request shapes are sent; the invented fallbacks are gone. renameDeck is emulated correctly and no longer ends half-applied.
  • Far fewer round trips through multi: get_notetype 4 to 1, get_due_counts 3 to 1, get_notetypes 1 + 2n to 2, and set_card_flag is one request. (#77)
  • Batch reads: get_cards and get_notes are one cardsInfo / notesInfo request. (#92)
  • Remote AnkiConnect configuration is documented. (#12)

Performance

  • The direct backend fetches many cards or notes in one SELECT ... IN (...) over one connection, instead of opening a connection per card. cards and search use it. (#92)
  • The browse TUI loads cards in batches of 200 with progress and cancels a superseded load. The 0.12 s polling timer is replaced by RowHighlighted, and suspend and delete run in worker threads. (#93)
  • The review TUI renders each card once. Flipping the answer no longer re-fetches the card, note, notetype, rating previews and due counts. (#93)

TUI

  • Layout audit fixes: title bar, toolbar, key hints and error messages no longer overlap; the Due and Ivl columns stay on screen at any width; the review rate row fits at 80x24; keys work at startup and Esc leaves the search box; card text containing [ ] is no longer eaten as markup; a crashed TUI no longer exits 0. (#89)
  • Hint bars are derived from the real key bindings, so no more phantom edit, mark or add hints. Rating buttons show real newlines. A failed answer no longer leaves a stale undo entry. Undo deletes exactly the revlog row it wrote, and undo.json is written atomically. (#49)

Internals, tests and CI

  • anki_direct.py was split into db/ modules, the db-to-backends import cycle was broken, and scheduler introspection is on the backend Protocol. One @anki_command decorator now owns the envelope, the session and BACKEND_UNAVAILABLE. (#31, #67, #68, #72)
  • Shared test fixtures build collections with Anki's real schema-18 DDL and protobuf-correct rows. The suite now has about 2,300 tests. (#70, #71)
  • CI runs the TUI tests on every push, checks ruff format, and pins textual>=7,<8 and prompt-toolkit>=3,<4. (#90)
  • Ruff W rules enabled. (#51)
  • README status badges, and npx skills add documented as an install method. (#15, #94, #95)

Full list: v0.1.5...v0.2.0