# 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 "MCP server"). ## Reference projects **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). - **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 `providers.New()` (`internal/providers`) — that is the only place CLI and MCP server share. - **MCP server:** `internal/mcpserver` wraps only the tool `list_offers` (equivalent to `cheapo list`, mode via the `mode` argument) over `github.com/modelcontextprotocol/go-sdk`. No store logic there; responses are structured (`structuredContent`) and use the wire type `offer` (RFC-3339 strings instead of `time.Time` zero values). Primary transport is HTTP (`cheapo mcp --http`), stdio only as fallback. The server is stateless — deduplication ("what is new?") is the client's job, not the server's (no `seen` set, no state file). - **Shared list logic:** the modes `free`/`upcoming`/`deals` (including capability interfaces `game.UpcomingProvider`/`game.DealProvider` and warn-instead-of-error) live in `internal/ops` and are shared by CLI, MCP and chatto-send. New list modes only there, not in `cmd/` or `internal/mcpserver`. - User-facing strings in English (CLI output, help texts, error messages), code comments in English. - **Unambiguous date outputs:** prefer ISO-8601 (RFC-3339 / `2006-01-02`) for machine-readable/structured surfaces (CLI `--json`, MCP `offer` wire type); compact locale-style formats (e.g. `02.01.`) are only for human-facing chat messages and must be overridable via `CHEAPO_CHATTO_OUTPUT_DATE_FORMAT`. The CLI's human- readable "Free until:" line honors `CHEAPO_CLI_OUTPUT_DATE_FORMAT` (default RFC-3339). Avoid ambiguous `DD.MM.`/`MM/DD` outputs without a year. - Wrap errors with context: `fmt.Errorf("epic: ...: %w", err)`. - New store platforms (Prime, GOG, …) follow the provider pattern (above). - **Chatto client:** `internal/chatto` is a hand-built Connect-JSON client (Chatto's generated Go code is `internal/` in the Chatto repo and not importable from outside). Credentials ONLY via `CHATTO_BASE_URL`/`CHATTO_TOKEN` env (set by the operator) — the tool never fetches credentials itself, and the agent does not use its own plugin credentials for it. ## Testing - Tests use table-driven tests where possible; live fixtures for HTML/API responses live in `internal//testdata/`. - Run before every commit: `go build ./... && go vet ./... && go test ./...`