task_id: dvach-cli-build
repo: /tmp/dvach-starter
ref: master
prompt: |
  Build a CLI tool for browsing the 2ch.su (Двач) imageboard, from scratch.

  The repository contains a minimal scaffold (pyproject.toml, empty
  `src/dvach_cli/` package, and a `cli.py` stub with just `app =
  typer.Typer()`). The full specification is in `README.md` at the
  repository root. Read it carefully — it defines:

  - the binary name (`dvach`) and entry point
  - 8 subcommands: `boards`, `catalog`, `open`, `read`, `post`,
    `replies`, `search`, `last` — each with positional args, a `--raw`
    JSON mode, and command-specific options
  - a global `--domain` flag and `DVACH_DOMAIN` env var that **must
    accept a URL scheme** (so the tool can point at plain-HTTP test
    servers, not only HTTPS 2ch)
  - the URL shortcut: the `board` positional may be a full thread URL
    like `https://2ch.su/ai/res/1562852.html`, in which case board and
    thread_id are extracted from it
  - HTML → plain text conversion rules (reply links become `>>NNNN`,
    `<br>` → newline, tags stripped, entities decoded)
  - the 2ch API endpoints and post/thread JSON shape
  - exit codes (0 on success or soft "not found", 1 on HTTP/parse error)

  Implement all of this so that `uv run dvach <subcommand> --raw` emits
  stable JSON for each command. The grader invokes the tool as a
  subprocess with `DVACH_DOMAIN` set to a local HTTP fixture server and
  parses the JSON output — treat the `--raw` shape as a hard contract.

  You may structure the implementation across multiple files inside
  `src/dvach_cli/` as you see fit; keep `dvach_cli.cli:app` exported.

  Dependencies are already declared (`typer`, `rich`); add more to
  pyproject.toml if you need them. `uv sync` is the only setup step the
  grader runs, so anything you add must be reachable through that.

setup: uv sync
timeout_minutes: 30
node_version: ""

hidden_tests_dir: hidden/dvach-cli-build
hidden_test_command: |
  pytest tests-hidden/ -v --tb=short -p no:cacheprovider 2>&1 | tail -80

dvach-cli — specification (README.md)

Build a CLI for browsing the 2ch.su (Двач) imageboard. Written in Python
with Typer for command handling and
Rich for formatted output.

The package is already scaffolded in src/dvach_cli/. The binary is
named dvach (see pyproject.toml). Use uv sync to install deps and
the entry point; use uv run dvach … to run it.

Environment

The base URL for API calls is controlled by DVACH_DOMAIN (env) or the
global --domain flag. Default: 2ch.su (implied scheme https).

Important: DVACH_DOMAIN / --domain may include a URL scheme.
If it starts with http:// or https://, use it verbatim. Otherwise
prefix https://. This lets the tool point at local test servers over
plain HTTP.

API endpoints

  • GET /api/mobile/v2/boards — array of boards:
    { "id": "b", "name": "Random", "category": "..." }
  • GET /{board}/catalog.json{ "threads": [...] }, each thread:
    { num, subject, comment, posts_count, files_count, views, timestamp, sticky, closed, tags }
  • GET /{board}/res/{thread_id}.json — thread data:
    { title, posts_count, files_count, unique_posters, is_closed, threads: [ { posts: [Post, ...] } ] }, where
    Post = { num, comment (HTML), name, subject, date, timestamp, files: [{ displayname, name, size, path }] }.

On HTTP error: print red error line and exit with code 1.

URL shortcut

Wherever a command takes <board> <thread_id> positional args, the
caller may instead pass a full thread URL as the board argument
(leaving thread_id as 0/default). URL shape:
https://2ch.XX/<board>/res/<num>.html optionally with #<post> anchor.
Regex: https?://2ch\.\w+/(\w+)/res/(\d+)\.html(?:#\d+)?

HTML → text

Post comment fields are HTML. Conversion rules:

  • Reply links <a ... data-num="NNNN">...</a>>>NNNN
  • <br> / <br/> → newline
  • All other tags → stripped
  • Decode HTML entities (&amp;, &gt;, etc.)
  • Collapse 3+ consecutive newlines to 2

This helper is also used for text-based filtering in catalog and
search (matching is done on plain text, not raw HTML).

Reply detection

A post "replies to" N if its comment contains
<a ... data-num="N">...</a> or a >>N token after HTML stripping.
Extract in order of appearance, deduplicate per post.

Commands

All commands support --raw to emit JSON on stdout instead of Rich
output. Exit 0 on success or soft "not found"; exit 1 on HTTP/parse error.

dvach boards [query] [--raw]

List all boards, optionally filtered by substring match
(case-insensitive) on board id, name, or category.
--raw: JSON array of filtered board objects (verbatim from API).

dvach catalog <board> [query] [-n LIMIT] [--sort {bump,date,posts,views}] [--raw]

Fetch {board}/catalog.json. Optionally filter by substring on thread
subject or textual form of comment. Sort modes:

  • bump (default) — keep API order
  • datetimestamp descending
  • postsposts_count descending
  • viewsviews descending

Take first LIMIT entries (default 20). --raw: JSON array of thread
entries (verbatim, filtered+sorted+sliced).

dvach open <board> <thread_id> [--raw]

Fetch the thread, print title + metadata + OP. --raw:

1
2
3
4
5
6
{
  "title": str, "board": str, "thread_id": int,
  "posts_count": int, "files_count": int, "unique_posters": int,
  "is_closed": bool,
  "op": <first post verbatim>
}

dvach read <board> <thread_id> [from_index] [-n COUNT] [--raw]

Print COUNT posts starting at from_index (0 = OP). If from_index is past
the end, print a warning and exit 0. Default COUNT 20. --raw: JSON
array of the sliced post objects.

dvach post <board> <thread_id> <post_num> [--raw]

Find post by num. If not found, yellow warning + exit 0. Otherwise
print post + list of post numbers replying to it. --raw: the post
object verbatim.

dvach replies <board> <thread_id> <post_num> [-d DEPTH] [--raw]

Recursively find posts that reply to post_num, up to DEPTH levels
(default 3). Each discovered post appears once. Sort by original index
in the thread (asc). --raw: JSON array of reply post objects verbatim.

dvach search <board> <thread_id> <query> [-n LIMIT] [--raw]

Iterate posts in order, include those whose html_to_text(comment)
contains query (case-insensitive). Stop after LIMIT matches (default 20).
--raw: JSON array of matching posts verbatim.

dvach last <board> <thread_id> [-n COUNT] [--raw]

Last COUNT posts of the thread (default 20). --raw: JSON array of
those posts verbatim.

What to deliver

A working implementation in src/dvach_cli/ that uv sync installs and
uv run dvach … runs. Split code across files inside the package as
you like; keep dvach_cli.cli:app as the entry point.

The grader invokes the tool as a subprocess with DVACH_DOMAIN pointing
at a local HTTP fixture server and parses --raw output — treat the
--raw shape as a hard contract.

Edit

Pub: 25 Apr 2026 03:15 UTC

Views: 26