Problem: docs referenced the old file-based credential backend
and `credentials_backend` config option that no longer exist.
Solution: add git 1.7.9+ to optional requirements, update
credentials section to document `git credential` as the sole
storage mechanism, remove `credentials_backend` from config
fields.
Problem: all healthcheck items were under a single `[required]`
section, including optional tools like uv and git.
Solution: move uv/nix/git checks into a new `[optional]` section.
Add git version check asserting >= 1.7.9 with the minimum and
installed versions shown side by side. Show neovim version the
same way.
Problem: `credentials.lua`, `submit.lua`, and `scraper.lua` all
routed through `cache.get/set/clear_credentials`, which no longer
exists.
Solution: replace all `cache` credential calls with direct
`git_credential.get/store/reject` calls. Remove unused `cache`
imports where applicable.
Problem: `cache.lua` stored credentials in `_credentials` keys
inside the JSON cache file, with a versioned migration system
(`CACHE_VERSION`) that only existed to move credentials between
formats.
Solution: remove `get_credentials`, `set_credentials`,
`clear_credentials`, the v1→v2 migration, and `CACHE_VERSION`.
The cache file is now a plain JSON blob with no version field.
Problem: credentials were stored as plaintext JSON in the cache
file, with no integration with system credential managers.
Solution: add `lua/cp/git_credential.lua` wrapping the
`git credential fill/approve/reject` protocol. Maps each platform
to its host, handles CSES token as a second entry with
`path=api-token`.
## Problem
Credential and cookie files were world-readable (0644), passwords
transited via `CP_CREDENTIALS` env var (visible in `/proc/PID/environ`),
and Kattis/USACO echoed passwords back through stdout unnecessarily.
## Solution
Set 0600 permissions on `cp-nvim.json` and `cookies.json` after every
write, pass credentials via stdin pipe instead of env var, and stop
emitting passwords in ndjson from Kattis/USACO `LoginResult` (CSES token
emission unchanged).
## Problem
Wrong credentials produced garbled error messages (`"login failed: Login
failed: bad_credentials"`) and stale credentials remained cached after
failure, causing silent re-use on the next invocation.
## Solution
Standardize all scrapers to emit `"bad_credentials"` as a plain error
code, mapped to a human-readable string via `LOGIN_ERRORS` in
`constants.lua`. Fix `credentials.lua` to clear cached credentials on
failure in both the fresh-prompt and re-prompt paths. For AtCoder and
Codeforces, replace `wait_for_url` with `wait_for_function` to detect
the login error element immediately rather than sitting the full 10s
navigation timeout. Add "Remember me" checkbox on Codeforces login.
## Problem
Loading a problem whose source file already exists silently overwrites
user code. Submitting an empty file sends a blank submission to the
platform. Two ruff lint violations existed in the scrapers.
## Solution
- `setup.lua`: when the target source file exists on the filesystem
(`vim.uv.fs_stat`), show an inline `Overwrite? [y/N]:` prompt. Declining
keeps the existing file open and registers state normally. Skipped when
the file is already loaded in a buffer.
- `submit.lua`: resolve path to absolute, use `vim.uv.fs_stat` to verify
existence, abort with WARN if `stat.size == 0` ("Submit aborted: source
file has no content").
- `scrapers/atcoder.py`: remove unused `pathlib.Path` import (F401).
- `scrapers/base.py`: move local imports to top of file (E402).
Closes#364, #365.
## Problem
Scraper cookie handling was fragmented across per-platform files with no
shared access, httpx scrapers lacked `checking_login` fast paths on
login, and several re-auth edge cases (CodeChef submit, CF cookie guard,
AtCoder cookie persistence) caused unnecessary full re-logins or silent
failures.
## Solution
Centralize all cookie storage into a single `cookies.json` via helpers
in `base.py`. Add `checking_login` fast paths to `kattis.py` (using the
`x-username` response header as a session probe), `usaco.py`, and
`cses.py` login flows. Fix `kattis.py` submit to emit `checking_login`
only after loading cookies. Remove AtCoder cookie persistence from login
entirely — always do a fresh session. Harden CodeChef and CF reauth
with consistent status logging and cookie guard checks.
## Problem
httpx scrapers (CSES, Kattis, USACO) always ran full login flows even
with valid cached sessions. \`credentials.lua\` always prompted before
trying cached credentials. \`default_filename\` doubled the slug for
Kattis single-problem mode (e.g. \`addtwonumbersaddtwonumbers.cc\`).
## Solution
Added token/cookie fast paths to CSES \`login()\` and USACO
\`login()\`/\`submit()\`.
Hardened Kattis reactive re-auth trigger to check status code first.
Refactored \`credentials.lua\` to try cached credentials before
prompting.
Fixed \`default_filename\` to not concatenate when \`contest_id ==
problem_id\`.
## Problem
On CSES, Kattis, and USACO, `:CP <platform> login` always prompted
for credentials and ran a full web login even when a valid session was
already cached. Submit also had weak stale-session detection.
## Solution
`credentials.lua` now tries cached credentials first before prompting,
delegating fast-path detection to each scraper. CSES `login()` checks
the cached API token and returns immediately if valid. USACO `login()`
and `submit()` call `_check_usaco_login()` upfront. Kattis `submit()`
emits `checking_login` consistently and also triggers re-auth on HTTP
400/403, not just on the `"Request validation failed"` text match.
The premature `Submitting...` log emitted by Lua before the scraper
started is removed — Python's own status events are sufficient.
## Problem
Starting a race on a contest more than 1 hour away showed no feedback —
`should_notify` only fires at 15-minute intervals, so the first tick
produced no message and the UI appeared frozen during the contest list
fetch.
## Solution
Log the initial countdown immediately after the race timer is set up,
before the first `should_notify` tick fires.
## Problem
After the initial CodeChef implementation, submit silently swallowed
"contest not available for submission" errors, and the submit flow used
blind `wait_for_timeout` delays. AtCoder had an unnecessary 500ms settle
delay after file upload.
## Solution
Replace the initial page-load sleep in CodeChef submit with
`wait_for_selector`, replace the 3s post-click sleep with a proper
`wait_for_selector` on the result dialog, and extend the practice
fallback
check to catch both dialog variants. Remove AtCoder's
`BROWSER_SETTLE_DELAY`
and the constant from `timeouts.py`.
## Problem
CodeChef had no working login, submit, or contest list. The browser
selectors were wrong, the contest list was missing present/past
contests,
and problem/contest URLs were unset.
## Solution
Fix login and submit selectors for the Drupal-based site. Paginate
`/api/list/contests/past` to collect all 228 Starters, then expand each
parent contest into individual division entries (e.g. `START228 (Div.
4)`).
Add language IDs, correct `url`/`contest_url`/`standings_url` in
metadata,
and make `:CP <platform>` open the contest picker directly.
## Problem
Language version coverage was incomplete across all platforms, AtCoder
submit used a stale cookie fast-path that caused silent failures, and
raw
`vim.notify` calls throughout the codebase produced inconsistent or
missing `[cp.nvim]:` prefixes.
## Solution
Remove cookie persistence from AtCoder login/submit (always fresh
login),
increase the submit nav timeout to 40s, and switch to in-memory buffer
upload with the correct per-language extension from a full
`_LANGUAGE_ID_EXTENSION`
map covering all 116 AtCoder languages. Expand `LANGUAGE_VERSIONS` in
`constants.lua` with all AtCoder languages, 15 new CF languages with
full
version variants, and 50+ Kattis languages. Fix AtCoder `prolog` ID
(`6079`→`6081`, was Pony) and remove the non-existent `racket` entry.
Replace all raw `vim.notify` calls with `logger.log`. Simplify the
submit
language doc to point at `constants.lua` rather than maintaining a
static table.
## Problem
Codeforces language version IDs were wrong (pointed at old compiler
versions), `LANGUAGE_VERSIONS` was incomplete for most platforms, the
contest picker crashed when `supports_countdown` was stored as a
non-table entry, and `:CP stress` restored a broken IO view on exit.
## Solution
Correct CF `programTypeId` values and default to C++20. Add
`LANGUAGE_VERSIONS` entries for all six platforms. Guard
`get_contest_summaries` against non-table cache entries. Call
`ensure_io_view()` after stress session restore. Shorten the stress
terminal buffer name to a readable `term://stress.py` format.
## Problem
`LANGUAGE_VERSIONS` only covered cpp and python. Several platform IDs
were wrong — CodeChef used `C++ 17`/`Python 3` (correct: `C++`/`PYTH
3`), USACO listed nonexistent c++20/c++23 options, and CSES only had
C++17.
## Solution
Verify every platform's submit page and update all language ID tables.
Add java and rust entries where supported, fix incorrect CodeChef and
USACO IDs, and expand CSES `CSES_LANGUAGES` dict with
C++11/C++20/PyPy3/Java/Rust variants.
## Problem
`LANGUAGE_VERSIONS` mapped CF `c++17` to `89` (actually GNU G++20) and
defaulted to `c++17`, which didn't match any CF entry. Helpdocs listed
CF as having only `c++17`.
## Solution
Fix CF version map to `c++17 → 54`, `c++20 → 89`, `c++23 → 91`, add
`pypy3 → 70`. Change `DEFAULT_VERSIONS.cpp` to `c++20` (no behavioral
change — `89` was already the submit default). Update helpdocs. Fix
StyLua formatting in `race.lua`.
## Problem
Race countdown had three reliability issues:
- Notified every second regardless of remaining time (noisy for
hours-long waits)
- Never re-fetched start times (stale if contest rescheduled)
- No retry on setup failure at T=0 — if the scraper failed at the most
critical moment, the entire countdown was wasted
## Solution
- **Notification tiers**: >1h every 15m, >5m every 1m, >1m every 10s,
≤60s every 1s
- **Drift correction**: re-fetch contest list every 10 minutes, update
`start_time` if changed
- **T=0 retry**: `race_try_setup` calls `scrape_contest_metadata` with
the new `on_error` callback, retrying up to 15 times at 3s intervals
(~45s). Caches metadata on success, then calls `setup_contest` on the
fast (cached) path. Token guard invalidates stale retries after
cancellation.
- **`on_error` for `scrape_contest_metadata`**: optional 4th param,
backward-compatible
## Problem
Race was the only command using `:CP <action> <platform> <contest>`
syntax while every other platform+contest command uses `:CP <platform>
<contest> [flags]`.
## Solution
Make race a `--race` flag on the existing contest setup flow. Remove
`:CP race stop` — starting a new race auto-cancels any active one.
Remove `<Plug>(cp-race-stop)` keymap. Update tab completion and docs
accordingly.
## Problem
Two gaps in the plugin: (1) contest pickers have no way to know whether
a contest supports countdown (race), so the UI can't surface that
affordance; (2) `:CP submit` hardcodes a single language ID per platform
with no way to choose C++ standard or override the platform ID.
## Solution
**Race countdown** (`4e709c8`): Add `supports_countdown` boolean to
`ContestListResult` and wire it through CSES/USACO scrapers, cache, race
module, and pickers.
**Language version selection** (`b90ac67`): Add `LANGUAGE_VERSIONS` and
`DEFAULT_VERSIONS` tables in `constants.lua`. Config gains `version` and
`submit_id` fields validated at setup time. `submit.lua` resolves the
effective config to a platform ID (priority: `submit_id` > `version` >
default). Helpdocs add `*cp-submit-language*` section. Tests cover
`LANGUAGE_IDS` completeness.
## Problem
`:CP <platform> login` short-circuited on cached cookies/tokens — if an
old session was still valid, the new credentials were never tested. The
user got "login successful" even with wrong input. USACO submit also
wasted a roundtrip on `_check_usaco_login` every time.
## Solution
Always validate credentials in the login path. Remove cookie/token
loading from `_login_headless` (AtCoder), `_login_headless_cf` (CF),
`_login_headless_codechef` (CodeChef), and `login` (CSES). For USACO
submit, replace the `_check_usaco_login` roundtrip with cookie trust +
retry-on-auth-failure (the Kattis pattern). Submit paths are unchanged —
cookie fast-paths remain for contest speed.
Closes#331
## Problem
Kattis and USACO had zero offline test coverage — no fixtures, no
conftest
routers, and no entries in the test matrix. The `precision` field and
error
paths were also unverified across all platforms.
## Solution
Add HTML fixtures for both platforms and wire up `httpx.AsyncClient.get`
routers in `conftest.py` following the existing CSES/CodeChef pattern.
Extend the test matrix from 12 to 23 parametrized cases. Add a dedicated
test for the Kattis contest-vs-slug fallback path (verifying
`contest_url`
and `standings_url`), three parametrized metadata error cases, and a
targeted assertion that `extract_precision` returns a non-`None` float
for
problems with floating-point tolerance hints.
Closes#281.
## Problem
`:CP open` accepts `problem`, `contest`, and `standings` but offered no
tab completion for them.
## Solution
Add an `args[2] == 'open'` branch in the `plugin/cp.lua` completer
returning the three valid subcommands.
## Problem
No quick way to reach a platform's registration page from within Neovim.
## Solution
Add `signup` as a platform subcommand that calls `vim.ui.open` on the
platform's registration URL. URLs live in a new `SIGNUP_URLS` table in
`constants.lua`. Works even when the platform is disabled. Tab
completion and vimdoc updated.
## Problem
Supplying any `platforms` table silently dropped all unlisted platforms,
making it easy to accidentally disable platforms. Invoking a disabled
platform produced no user-facing error.
## Solution
Switch to a merge model: all six platforms are enabled by default and
user entries are deep-merged on top. Set a platform key to `false` to
disable it explicitly. Add a `check_platform_enabled` guard in
`handle_command` for contest fetch, login, logout, and race actions.
## Problem
Kattis and USACO problem tests never extracted float precision, so
epsilon problems got no tolerance. Kattis `scrape_contest_metadata`
omitted `contest_url` and `standings_url`, breaking `:CP open
contest/standings`. Kattis submit always returned success even when the
server responded with an error (e.g. "You need to join the contest").
## Solution
Call `extract_precision` on problem HTML in both scrapers and emit it in
the JSON payload. Set `contest_url` and `standings_url` on Kattis
metadata paths. After Kattis submit, check for `Submission ID:` in the
response and surface the error text if absent.
## Problem
`codeforces.py` used `curl_cffi` to bypass Cloudflare when fetching
contest problem HTML, making it unavailable in the nix python env and
requiring an extra dependency across `pyproject.toml` and `flake.nix`.
## Solution
Rewrite `_fetch_problems_html` to use scrapling `StealthySession` with
`solve_cloudflare=True`, matching the existing CF submit pattern. Extend
`needs_browser` in `scraper.lua` to route CF `metadata` and `tests`
through the FHS env on NixOS. Remove `curl-cffi` from `pyproject.toml`,
`flake.nix`, and test mocks.
## Problem
`scrape_contest_list` made O(N) requests — one per Starters number up to
~200 — to discover division sub-contests via `child_contests`. `run_one`
also fetched problem HTML via `curl_cffi` solely to extract the memory
limit, which is unavailable in the nix python env.
## Solution
Use `/api/list/contests/all` directly: filter to `^START\d+$` codes and
map to `ContestSummary` in a single request. Remove `_fetch_html_sync`,
`MEMORY_LIMIT_RE`, and `_extract_memory_limit`; hardcode `memory_mb =
256.0` and `precision = None` in `run_one`.
## Problem
`ContestSummary.display_name` defaults to `None`, which serializes to
JSON `null` → Lua `vim.NIL`. The contest picker displayed "vim.NIL" for
every entry. Additionally, `start_time` was always stored even when
null, because `vim.NIL` is truthy in Lua.
## Solution
Pass `display_name=name` explicitly in `_parse_contests_page` so JSON
never emits `null`. In `cache.lua` `set_contest_summaries`, coerce
`display_name` via a `~= vim.NIL` guard and apply the same guard before
storing `start_time`.
## Problem
Kattis and USACO login and submit were broken in multiple ways
discovered
during manual end-to-end testing. Neither platform could successfully
authenticate or submit through the plugin.
## Solution
**Kattis:** switch login from `POST /login/email` (requires CSRF fetch)
to
`POST /login` with `script=true` (200 = success, 403 = bad credentials);
remove `_check_kattis_login` entirely since Kattis blocks all GET
requests
from httpx; add submit retry on `"Request validation failed"` to handle
expired sessions; fix language ID `"C++17"` → `"C++"`.
**USACO:** fix login field `user` → `uname`; fix success check to
`code==1`;
fix submit endpoint to `submit-solution.php`, file field to
`sourcefile`,
hidden field extraction off-by-one (`group(2)` → `group(1)`); fix
`_pick_lang_option` loop order (keywords outer, options inner) so
specific
keywords like `"c++17"` match before broad ones like `"c++"`.
**`submit.lua`:** absolutize source file path via `fnamemodify(...,
':p')`
before passing to the scraper — Python is spawned with `cwd=plugin_path`
so relative paths silently fail with `FileNotFoundError`.
**Both platforms:** remove cookie fast path from `login` subcommand so
credentials are always validated, preventing stale cookies from masking
wrong credentials.
## Problem
Login and submit were stub-only for USACO, Kattis, and CodeChef, leaving
three platforms without a working solve loop.
## Solution
Three commits, one per platform:
**USACO** — httpx-based login via `login-session.php` with cookie cache.
Submit fetches the problem page and parses the form dynamically (action
URL, hidden fields, language select) before POSTing multipart with
`sub_file[]`.
**Kattis** — httpx-based login via `/login/email` (official Kattis CLI
API). Submit is a multipart POST to `/submit`; the `contest` field is
included only when `contest_id != problem_id`. `scrape_contest_list`
URL updated to filter
`kattis_original=on&kattis_recycled=off&user_created=off`.
**CodeChef** — StealthySession browser-based login and submit following
the AtCoder/CF pattern. Login checks `__NEXT_DATA__` for current user
and fills the email/password form. Submit navigates to
`/{contest_id}/submit/{problem_id}`, selects language (standard
`<select>` first, custom dropdown fallback), sets code via
Monaco/CodeMirror/textarea JS, and clicks submit. Retry-on-relogin
pattern matches existing CF behaviour.
Language IDs added to `language_ids.py` for all three platforms.
## Problem
`open_url` automatically opened the browser on contest load and problem
change, which is now redundant with `:CP open`.
## Solution
Remove the `open_url` field from `cp.Config`, its default, its
validation, and the call site in `setup_contest`. Remove documentation
from `cp.nvim.txt`.
## Problem
There was no way to open a problem, contest, or standings page in the
browser from within the plugin.
## Solution
Add `contest_url` and `standings_url` to `MetadataResult` and persist
them in the cache. Add `cache.get_open_urls` to resolve all three URLs.
Wire up `:CP open [problem|contest|standings]` in `commands/init.lua`
to call `vim.ui.open`, warning when a URL is unavailable (e.g. CSES
has no standings). Closes#315.
## Problem
CSES `_web_login` used bare dict indexing on the API response, raising
an opaque `KeyError` on missing fields. `_check_token` swallowed all
exceptions as `False`, conflating transient network errors with invalid
tokens. CF persisted cookies unconditionally and silently swallowed
`_solve_turnstile` failures in `submit_action`.
## Solution
CSES API fields now use `.get()` with a descriptive `RuntimeError` on
absence. `_check_token` re-raises `httpx` network/timeout exceptions
so callers see real failures. CF cookie writes are guarded by an
`X-User-Handle` check (the CF auth cookie). `_solve_turnstile` errors
propagate to the outer error handler instead of being silenced.
## Problem
The `CONTEST_ACTIONS` list in `handle_command` was a manually maintained
parallel of `ACTIONS` that had already drifted (`pick` was incorrectly
included, breaking `:CP pick` cold). `navigate_problem` only cancelled
the `'run'` panel on `next`/`prev`, leaving interactive and stress
terminals alive and in-flight `run_io_view` callbacks unguarded.
`pick` and `cache` also appeared twice in tab-completion when a contest
was active.
## Solution
Replace `CONTEST_ACTIONS` with a `requires_context` field on
`ParsedCommand`,
set at each parse-site in `parse_command` so the semantics live with the
action definition. Expand `navigate_problem` to call `cancel_io_view()`
and
dispatch `cancel_interactive`/`stress.cancel` for all panel types,
mirroring
the contest-switch logic. Filter already-present `pick` and `cache` from
the
context-conditional completion candidates.
## Problem
Switching contests while a run, interactive session, or stress test was
active left orphaned callbacks and terminal jobs alive. Running `:CP
run`
twice also let the first run's stale output overwrite the buffer.
## Solution
Replace the `io_view_running` bool in `views.lua` with a generation
counter so concurrent `run_io_view` calls self-cancel via stale-gen
checks in async callbacks. Add `cancel_io_view`, `cancel_interactive`,
and `stress.cancel` for forceful teardown, and call them in
`setup_contest` whenever `is_new_contest` is true.
## Problem
Two gaps in `commands/init.lua`. Codeforces contest IDs were passed
through raw, so full URLs (e.g.
`https://codeforces.com/contest/1933/problem/A`) or problem IDs with
trailing letters (e.g. `1933A`) caused scraper URL construction to fail.
Separately, action commands like `:CP run` silently failed when invoked
with no active contest instead of attempting to recover from the current
file's cached state.
## Solution
Add `canonicalize_cf_contest` to normalize URLs and strip trailing
problem letters in `parse_command`. Add a guard in `handle_command` that
calls `restore_from_current_file()` before dispatching any
contest-requiring action when no platform is active, returning early
only if no cached state is found.
Closes#306. Closes#308.
## Problem
`setup_problem` only cleared the output buffer when `old_problem_id ~=
problem_id`. If two different contests share a problem with the same ID
(e.g. both have `a`), the condition is false and stale output from the
previous contest remains visible.
## Solution
Clear the output buffer at the top of `proceed()` in `setup_contest`
whenever `is_new_contest` is true, before any problem setup runs.
Closes#303.
## Problem
The submit path in `codeforces.py` guarded `cookie_cache.write_text` on
the presence of a `JSESSIONID` cookie. Codeforces does not use
`JSESSIONID`, so the cookie file was never written after submit,
breaking the fast-path on subsequent submits.
## Solution
Replace the name-specific guard with a non-empty check (`if
browser_cookies:`), matching the unconditional save already used in the
login path.
Closes#301.