# AGENTS.md — guidelines for coding agents ## Project **cheapo** — CLI in Go (Cobra) to fetch and observe current free game offers. Later planned: exposing the tools as an MCP server (see README.md, section "Roadmap / MCP"). ## Reference project **free-games-claimer** — https://github.com/DoSpamu/free-games-claimer (fork of https://github.com/vogler/free-games-claimer). How it gets the free-games info: - The fork (DoSpamu) is only an overlay (runner `run.js`, Discord notify, logger) on the upstream Docker image. The actual logic lives upstream in `vogler/free-games-claimer` (`epic-games.js`, `prime-gaming.js`, `gog.js`, `steam-games.js`, `aliexpress.js`). - Upstream uses **Playwright (Firefox)** with a persistent session: it opens the store page "Free Games", detects games via the locator `a:has(span:text-is("Free Now"))`, follows the links and claims them in the browser (login per cookie session, 2FA via OTP key, captcha handling). - **Important (comment in `epic-games.js`, ~line 151):** the page is built from the JSON of the public backend: `https://store-site-backend-static.ak.epicgames.com/freeGamesPromotions` (filter `data.Catalog.searchStore.elements` on active `promotions.promotionalOffers` with 100 % discount; slug preferably from `catalogNs.mappings[0].pageSlug`, since `urlSlug` is sometimes wrong). - **Our approach (Go):** we query this endpoint **directly** — no browser, no login needed, as long as we only want to *detect* (list + watch). The actual *claiming* of the games (like in the reference project) is deliberate **not** part of this project. ## Second reference project **free-games-notifier** — https://github.com/jaixmario/free-games-notifier (Python, GitHub-Actions cron every 6 h; notifications via e-mail, Telegram, Discord, WhatsApp; state in `free.json`/`free-steam.json`). How it gets the info: - **Epic** (`epic.py`): same public endpoint `store-site-backend-static.ak.epicgames.com/freeGamesPromotions?locale=en-US` (HTTP GET, no login) — confirms our Go approach. Filter: `discountSetting.discountPercentage == 0`; distinguishes **current** (`promotionalOffers`) and **upcoming** (`upcomingPromotionalOffers` — preview window for future free games). Change detection via signature (`CURRENT:t1,t2|UPCOMING:...`) + detail comparison per (title, link) keys; only notify on **new** offers, removals are silently written to state. - **Steam** (`steam.py`), two sources: 1. **Free to Keep:** HTML scrape of `https://store.steampowered.com/search/?maxprice=free&specials=1` (BeautifulSoup; `.search_result_row`, price "Free"/100 %). 2. **Free Weekend:** JSON API `https://store.steampowered.com/api/featuredcategories/` — items with "Free Weekend"/"play for free" in `name`/`body`; title fallback via `store.steampowered.com/api/appdetails?appids=`. - Useful patterns for us: **state signature + diff** (notify only on real additions, removals silently into state) and **current vs. upcoming** separation. Both could be adopted. ## Toolchain & linting - Go ≥ 1.27 (`go version`). - **Formatting:** `gofmt -w .` (mandatory before every commit; CI checks `gofmt -l .` = empty). - **Linting/analysis:** `gopls` (install: `go install golang.org/x/tools/gopls@latest`). Use in editors as LSP; additionally `go vet ./...` before every commit. - Build & test: ```bash go build ./... && go vet ./... && go test ./... gofmt -l . # must be empty ``` ## Conventions - Module path: `gogs.netdome.biz/paul/cheapo`. - CLI framework: **Cobra** (`github.com/spf13/cobra`), commands in `cmd/cheapo/main.go`. - Internal code under `internal/` (epic: store client, watch: polling/eventing). - Wrap errors with context: `fmt.Errorf("epic: ...: %w", err)`. - User-facing strings in German (CLI output, help texts, error messages), code comments in English. - **Provider pattern:** every store lives in `internal//` and implements `game.Provider` (`Name()` + `GetFreeGames(ctx) ([]game.Game, error)`); the shared type is `game.Game` (`internal/game`). New stores (e.g. GOG) follow this pattern and are registered in `newProviders()` (`cmd/cheapo/main.go`). - **Chatto client:** `internal/chatto` is a hand-built Connect-JSON client (Chatto's generated code is `internal/` and not importable). Credentials ONLY via `CHATTO_BASE_URL`/`CHATTO_TOKEN` env from the operator — the tool never fetches credentials itself, and the agent does not use its own plugin credentials for it. - New store platforms (Prime, GOG, …) follow the provider pattern (above).