AGENTS.md 6.0 KB

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=<id>.
  • 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/<store>/ 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_DATE_FORMAT. 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/<store>/testdata/. - Run before every commit: go build ./... && go vet ./... && go test ./...