- Go 98.2%
- Nix 1.8%
V1.4 — addresses the 'how big is it?' question for items where the size isn't in the price column. Two complementary features: 1) Regex-based size extraction (option A, the cheap path) Adds items.size_text column, populated by ExtractSize() on insert and backfilled on migration (v2→v3) for every existing row. Recognised: ml, cl, l, g, kg, oz, lb — case-insensitive, case preserved. Handles decimals (1,5 L), ranges (33-50 G), and no-space forms (500ML). Returns '' when the name has no size, so the caller can fall back to the image. Covers ~17% of items directly (e.g. Maxi PC products, packaged goods with size in name). The remaining 83% get a verifiable image URL instead. 2) --show-url flag When set, prints the cutout_image_url (the per-product photo from f.wishabi.net) under each item in --flat and grouped mode, so the user can visually verify the size/format that wasn't in the name. Default off — keeps the usual output clean. JSON output also picks up 'size' and 'image_url' fields with omitempty, so scripts that want them can jq for them. Schema migration is idempotent (ALTER TABLE ... 'duplicate column' is treated as success) and the backfill is idempotent too (re-running changes 0 rows on a clean DB). Tests: 4 new test functions, 17 ExtractSize subtests, 5 render subtests, 2 JSON subtests. All green; full ./go test passes. Real-data smoke (J5V1A5, this week): $ circulaires search 'ML' --merchants Maxi --min-price 1 --flat shows [695 ML], [425-500 ML], [1,89 L], [320-350 G] inline. $ circulaires search 'sirop' --show-url prints the cutout URL under each SIROP D'ÉRABLE / MÉLANGE NESTLÉ match. |
||
|---|---|---|
| cmd/circulaires | ||
| data | ||
| docs | ||
| internal | ||
| skills | ||
| .gitignore | ||
| flake.lock | ||
| flake.nix | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| README.md | ||
circulaires
Generic CLI for fetching weekly flyers from grocery stores and retailers via the Flipp (formerly Reebee) public API.
circulaires lets you track weekly specials from any store Flipp supports,
for any postal code in Canada (or ZIP in the US). It stores everything locally
in SQLite so you can track price evolution over time.
Features
- 🔎 Fetch all active flyers near a postal code
- 🏪 Filter by merchant (IGA, Maxi, Metro, Sobeys, Walmart, …)
- 💾 Store everything in a local SQLite database for history
- 📊 Show current specials in a nice terminal table
- 🇫🇷🇨🇦 French & English locale support (per postal code)
- 🛠️ Single static binary, no runtime dependencies
- 🧪 Pure Go SQLite (no CGO)
Quick start
# Enter the dev shell (requires Nix with flakes enabled)
nix develop
# Build
go build -o bin/circulaires ./cmd/circulaires
# Fetch this week's flyers for the North Pole, QC
./bin/circulaires fetch --postal "H0H 0H0"
# Show current specials (defaults to your saved postal code)
./bin/circulaires show
# Show only specific merchants, sorted by price
./bin/circulaires show --merchants "IGA,Maxi,Metro" --sort price-asc
Configuration
On first run, circulaires creates ~/.config/circulaires/config.yaml:
postal_code: "H0H 0H0" # default postal code (spaces are fine — normalised before use)
locale: fr # fr-ca | en-ca | en-us
db_path: ~/.local/share/circulaires/specials.db
merchants: [] # empty = all merchants
Override any of these via flags (--postal, --locale, etc.) or edit the file
directly with circulaires config edit.
Commands
| Command | Description |
|---|---|
circulaires fetch |
Fetch current flyers & items, store in DB |
circulaires show |
Display current specials from DB |
circulaires config |
View/edit config (path, show, edit, set) |
circulaires version |
Print version info |
Run circulaires <command> --help for full flag lists.
How it works
circulaires is a thin client over the unauthenticated public Flipp API that
flipp.com's own web client uses. Two endpoints:
GET /api/flipp/data?locale=<l>&postal_code=<p>&sid=<random>— list of all active flyers near a postal code.GET /api/flipp/flyers/<id>/flyer_items?locale=<l>&sid=<random>— items in a specific flyer.
The sid is just a random 16-digit session ID; no key, no login, no cookies.
See docs/API.md for full reverse-engineering notes.
Caveats
- The Flipp API returns every text fragment on a flyer as an "item" —
product names and decorative copy ("Offers Scene + fr",
"service magasin poissonnerie", "activer vos offres", …). For V1 we
surface them all and let you filter via
show --search. A future version will usedisplay_typeto separate real products from decorative text. - The same flyer can appear twice (current + next week). The DB
dedupes via
(flyer_id, valid_from, valid_to). - Locale matters: with
--locale fr-cayou get "YOGOURT À BOIRE"; with--locale en-cayou get "DRINKABLE YOGURT". Pick the one your users expect. - This is an unofficial client. Flipp's terms apply; be polite.
Roadmap
- Filter out decorative text items (use
display_type) - Price comparison across merchants
- Watchlist (notify when specific products go on sale)
- Telegram / email digest
- Export to CSV / JSON
- Web UI (maybe)
Contributing
PRs welcome. Code style: gofmt + golangci-lint. Tests live next to the code
they cover. Run go test ./... before pushing.
AI agent skills
This repo ships with an agent skill under skills/circulaires-cli/
for future agents (or humans) who want a quick orientation without reading
Go source. The skill also documents how circulaires integrates with the
mealie-cli tool for a complete "flyers → recipe → meal plan → shopping
list" weekly workflow.
If you change the CLI's interface, please update the skill too. The
underlying Flipp API knowledge (endpoints, sid, locale, mixed-type
JSON) is in the global flipp-flyers skill — don't duplicate it here,
link to it instead.
License
MIT — see LICENSE.