skill
DealWeek
An open-source AI skill that turns Gmail and Outlook promotions into a weekly savings calendar, with JEV-powered filtering and deduplication. 一个开源 AI Skill,将 Gmail 和 Outlook 中的促销邮件整理成每周省钱日历,借助 JEV 智能筛
Set up
Skills are installed by placing their folder where your agent loads skills.
The download comes from github.com/FlyPig23/DealWeek itself, not from our server, so what you get is exactly what its author published.
Install instructions come from the README
README
synced Oct 21, 2026DealWeek
Weekly Deals is the savings assistant in DealWeek. Its command and skill
are named weekly-deals; its Python package is weekly_deals.
Turns promotional email into a weekly savings plan you can check: which offers are usable this week, which expire soon, and which still need confirmation — with the original wording behind every claim. Food, shopping, travel, events, and services stay in one calendar with small categories. An optional JEV pass groups repeat reminders for the same promotion into one calendar entry while keeping the source emails available.
Local-first, single user, read-only email access. Works with Gmail and
Outlook/Microsoft 365 through your agent’s mail connector, or with exported
.eml files. Runs offline on synthetic data with no API keys at all.
git clone https://github.com/FlyPig23/DealWeek.git
cd DealWeek
python3 -m venv .venv
source .venv/bin/activate
pip install .
weekly_deals_demo_dir="$(mktemp -d)"
WEEKLY_DEALS_DATA_DIR="$weekly_deals_demo_dir" weekly-deals scan --offline --mode llm-only
WEEKLY_DEALS_DATA_DIR="$weekly_deals_demo_dir" weekly-deals calendar --output ./savings-calendar.html
Open savings-calendar.html in your browser. This example scans synthetic
messages in an isolated temporary data directory and builds the calendar
without network access or credentials.
Every merchant in it is invented.
What makes it different from a list of coupons
The hard part of this problem is not finding offers. It is not overstating them.
- An unstated deadline is not "never expires". It is
time_status: unknown, and it cannot enter a plan. - A claim deadline is not a redemption deadline. "Redeemable until the 30th, but claim it by Friday" is two dates, and losing the second one loses the offer.
- A quoted term that is not in the email is not a term. Every material value must be backed by a verbatim quote the validator can locate in the normalized body. If it cannot, the offer is downgraded.
- A coupon that needs a bigger basket than you would have bought is not a saving. If the minimum spend is above your per-meal budget, it is not recommended — the point is to spend less, not to use coupons.
- "We didn't find anything" and "we couldn't finish" are different screens.
Every report states its coverage: messages matched, fetched, failed, images
left unparsed, and whether the search actually reached the end of the window.
A run capped with
--max-messagessays so; it does not report the part it saw as the whole. - Your decisions are yours. Marking an offer used or dismissed writes to a separate table. A re-sync rewrites model facts and never touches it.
Install
pip install -e . # core: offline demo, planner, reports
pip install -e ".[gmail]" # + read-only Gmail access
pip install -e ".[llm]" # + OpenAI-compatible extraction
pip install -e ".[web]" # + local web UI
pip install -e ".[mcp]" # + expose Weekly Deals to MCP hosts
pip install -e ".[all,dev]" # everything, plus tests
Requires Python 3.11+.
Install the skill
After installing the application, copy the skill into your agent's skill folder:
# Codex
mkdir -p ~/.codex/skills
cp -R skills/weekly-deals ~/.codex/skills/
# Or Claude Code
mkdir -p ~/.claude/skills
cp -R skills/weekly-deals ~/.claude/skills/
Start a new turn/session and ask the agent to use weekly-deals. If the agent
already has read access to your Gmail or Outlook/Microsoft 365 account through
a mail connector, no separate mailbox OAuth setup is needed in Weekly Deals.
Connector availability and supported account types depend on your agent. See
mail connection options.
Connect JEV once (optional)
Skill installation copies instructions; it does not open an API-key form. For JEV classification and promotion deduplication, run this in your own terminal after installing the CLI:
weekly-deals auth jev
The command prompts for your own TypeSafe API key
with hidden input, verifies it on a synthetic coupon, then asks whether to allow
email subjects/bodies to be sent to TypeSafe. It saves the key in
~/.config/weekly-deals/.env (or $XDG_CONFIG_HOME/weekly-deals/.env) with
owner-only file permissions. It works from any directory. No changes to
SKILL.md, no key in chat, and no additional mailbox API credentials are needed
in agent-host mode.
Already have TYPESAFE_API_KEY in your shell or project .env? Use
weekly-deals auth jev --from-env to verify and save it to the same user location.
An invalid key is not saved. To replace a key or change JEV email permission,
run the command again. This permission covers only the official TypeSafe API;
another cloud extractor needs its own configuration and cloud-processing consent.
weekly-deals status shows whether a key and permission are configured.
weekly-deals doctor --check-apis checks live connectivity with synthetic text
and exits with an error if a provider fails. Neither reads your mailbox.
jev_configured: true means a key is present, not that it has just been verified.
After setup, ask the host to use JEV with weekly-deals. It uses
scan --mail-dir <dir> --mode host-ingest without --offline.
JEV records promotion probabilities and category judgments. Run weekly-deals dedupe after the scan to compare likely repeats, then generate the HTML calendar.
Both JEV stages reuse the same API key and email-processing permission; no extra
key is needed. Default observe mode keeps all messages; automatic rejection
(gate) needs a recorded evaluation.
The calendar's display categories currently come from local rules.
Already completed, unchanged messages are reused; connecting JEV does not
retrospectively reprocess those messages. New and pending mail uses the configured
classifier. Check the scan's classification count, not just its provider label.
Without JEV, explicitly choose the host-only path with --offline. The skill
explains this choice on first use rather than silently bypassing a requested JEV run.
Merge repeated promotions
After fetching email through your agent's mail connector:
weekly-deals scan --mail-dir <exported-emails> --account-alias personal-gmail --mode host-ingest
weekly-deals dedupe
weekly-deals calendar --output ./savings-calendar.html
dedupe asks JEV whether candidate emails describe the same promotion, including
reminders with different wording. The calendar then shows one entry per matched
group with its source message IDs. All original emails remain stored; nothing is
deleted or changed in your mailbox. A shared merchant alone is not enough to merge two
offers. Uncertain matches and failed requests leave the messages separate.
Likely offers with upcoming deadlines are checked first; messages already scored
below the promotion-candidate threshold stay in the original-mail view. JEV
receives readable promotional text rather than email CSS and tracking URLs.
If grouped sources disagree on dates or terms, the entry is marked for review
and uses the earliest known deadline as a reminder, without treating the most
generous terms as confirmed. Unknown dates stay unknown. Review the reported
coverage and cost: deduplication uses the existing runtime.per_run_budget_usd
setting (default $1 per run), and reaching the budget can leave work unfinished.
Large mailboxes can use weekly-deals dedupe --max-comparisons 10000 to raise the
default 5,000-comparison cap while retaining the dollar budget and cached judgments.
To inspect every original entry, use weekly-deals calendar --all-messages;
the same switch works with --json. Rendering is offline and free. The dedupe
step calls JEV and is not available as a simulated offline result.
Upgrading from MealDeals
Reinstall the application and replace the old mealdeals skill folder with
skills/weekly-deals. Update scripts and MCP configurations to call
weekly-deals, and Python imports to use weekly_deals.
The environment prefix is now WEEKLY_DEALS_. Existing MEALDEALS_DATA_DIR,
MEALDEALS_EML_DIR, MEALDEALS_TIMEZONE, and MEALDEALS_LANGUAGE still work;
the corresponding new name takes priority when both are supplied.
New installations use ~/.local/share/weekly-deals (or the equivalent under
XDG_DATA_HOME). If that directory does not exist but the old mealdeals
data directory does, it is reused so saved mail, decisions and OAuth tokens
remain available. No private data is moved automatically.
Commands
weekly-deals demo # offline, synthetic, no keys
weekly-deals doctor # check configuration
weekly-deals doctor --check-apis # send one synthetic probe per provider
weekly-deals auth jev # hidden key entry, verification, private user config
weekly-deals auth gmail # read-only OAuth, token stored locally
weekly-deals scan --mail-dir ~/Desktop/test-emails --offline # real files, no keys
weekly-deals scan --mode llm-only --max-messages 30
weekly-deals scan --mode jev-observe --lookback-days 90
weekly-deals scan --mode jev-gate # only after an evaluation is recorded
weekly-deals dedupe # JEV: group repeated promotions after a scan
weekly-deals plan # show the plan in the terminal
weekly-deals offers # list stored offers and their state
weekly-deals calendar # HTML, with saved duplicate groups merged
weekly-deals calendar --json # machine-readable grouped events
weekly-deals calendar --all-messages # inspect every original email entry
weekly-deals calendar --promotion-candidates # display successfully classified likely promotions
weekly-deals savings # same HTML calendar under another command name
weekly-deals mark <offer-id> --status used
weekly-deals report --format html --output ./report.html
weekly-deals serve # local web UI on 127.0.0.1
weekly-deals mcp # MCP server over stdio
First real run
The order matters, and each step is cheap to undo. Steps 1-2 need no account, no key and no network.
-
weekly-deals demo— confirm the pipeline works end to end on synthetic mail. -
Test on real email without giving anything away. Export a dozen promotional messages from your mail client (in Gmail: open a message → the three-dot menu → "Download message"). Use RFC822/MIME
.emlfiles, not Outlook.msgfiles; renaming.msgto.emldoes not convert it. Then:weekly-deals scan --mail-dir ~/Desktop/test-emails --offline weekly-deals plan --offline weekly-deals calendar --offlineThis runs the whole pipeline over real vendor HTML, real nested MIME, real encodings — with no OAuth, no API key, and nothing leaving the machine. It is the fastest way to find out whether the normalizer copes with the mail you actually get, which is the part the synthetic corpus tests least honestly. Check the parsed dates, amounts and conditions against the originals before going further.
-
weekly-deals auth jevif using JEV, thenweekly-deals doctor --check-apis— confirm your keys and model names, using synthetic text only. Your mailbox is not touched. -
Choose a mail connection. For an agent connector, follow its host-ingest example below; steps 5–9 here describe the direct Gmail API path. Only that path needs
weekly-deals auth gmail. -
For a cloud extractor, set
privacy.cloud_processing_consent: trueinconfig.yamland pass--config config.yamlto scans. JEV-only use can grant its narrower permission throughweekly-deals auth jev. Authorising mailbox reads and agreeing to send email text to a cloud model are two separate decisions, and the application will not do the second without this. It covers every remote model that sees your mail, JEV included — running the classifier alone still sends each subject and body to TypeSafe.If you set
runtime.per_run_budget_usd, also setllm_price_input_usd_per_mtokandllm_price_output_usd_per_mtokfor your model. A cap can only be applied to spend that can be measured, so a run with a cap and no prices refuses to start rather than pretending to be capped. -
weekly-deals scan --mode llm-only --max-messages 30— a small batch. Read the output and check it against the real emails. The default Gmail query iscategory:promotions, so archived Promotions mail is included; the app does not restrict this step toin:inbox. -
Fix whatever is wrong, add a test for it, then widen to 90 days.
-
weekly-deals scan --mode jev-observe— the classifier runs but discards nothing, so you can measure what it would have filtered. -
Only once you have an evaluation record showing acceptable recall, switch to
jev-gate.
Choose how mail is connected
The application accepts standard email files; the host connector handles mailbox access. Installing the skill does not install or authorise a mail connector.
| Mail source | How it connects | Setup |
|---|---|---|
| Gmail | Agent-host connector, or the built-in Gmail API backend | Authorise the host connector, or run weekly-deals auth gmail for direct access |
| Outlook / Microsoft 365 | Agent-host connector that supports your account | Connect the mailbox in Codex, Claude Code, or your chosen host; no Microsoft API credentials go in this skill |
| Other providers | Host connector with full-message access, or exported RFC822 .eml files | Use the connector’s account setup or export the messages yourself |
Agent-host mode (recommended). The host reads the authorised mail and hands
it to Weekly Deals. Local normalisation and calendar rendering work the same way
for each provider. For the host-only path, no JEV or extractor key is needed;
choose --offline explicitly. Optional JEV classification and deduplication use
one personal key configured by weekly-deals auth jev, regardless of mail provider.
The search step is different:
- Gmail: search
category:promotionswithin the requested date range, plus any senders the user requests. Include archived matching messages; do not restrict the search to the primary inbox. - Outlook / Microsoft 365: there is no Gmail
category:promotionsquery. Focused/Other are inbox views, not promotion categories. Search received mail within the requested dates across Inbox and relevant archive/custom folders; exclude sent, drafts, deleted and junk mail by default. Use the connector’s supported query/filter syntax, then let JEV classify the authorised set. A broad received-mail scan can include personal or work messages: establish that scope before processing, and ask only if it would widen the user’s existing authorisation. Report any sender or keyword limits. With JEV, usecalendar --promotion-candidatesto show successfully classified likely promotions. The default calendar includes all imports. Without JEV, the host must select promotional messages before import and disclose that selection.
For either provider, follow pagination, fetch full message bodies rather than snippets, and report account/folder coverage, caps and missing bodies. Use only read tools. See the skill’s mail handoff guide for MIME export details.
For example, ask the host:
Use weekly-deals to scan my Gmail Promotions and received mail in my connected Microsoft 365 account from the last 30 days. Use JEV to find promotions and deduplicate them, then create one HTML savings calendar. Read only; do not send, archive, label, or delete mail.
使用 weekly-deals,读取最近 30 天的 Gmail Promotions,以及已连接的 Microsoft 365 邮箱收到的邮件,用 JEV 筛选促销并去重,生成一份 HTML 省钱日历。只读邮件。
For multiple accounts, export each to a separate empty directory. Give each
account a stable alias and keep filenames based on stable provider message IDs.
Weekly Deals namespaces imported message IDs by account alias, so the same
filename in two account directories remains distinct. Scan both into the same
WEEKLY_DEALS_DATA_DIR (the default works), then deduplicate once:
# Each directory contains stable <message-id>.eml filenames for that account.
weekly-deals scan --mail-dir <gmail-export-dir> --account-alias personal-gmail --mode host-ingest
weekly-deals scan --mail-dir <outlook-export-dir> --account-alias work-outlook --mode host-ingest
weekly-deals dedupe
weekly-deals calendar --promotion-candidates --output ./savings-calendar.html
--promotion-candidates is a display filter using successful saved classifier
judgments (normally JEV) at the configured candidate threshold; it does not delete emails or treat missing
judgments as confirmed negatives. Omit it to inspect every imported message.
--all-messages unfolds saved duplicate groups and cannot be combined with
--promotion-candidates. Mock classifier scores are not JEV judgments.
The host must select the date window before exporting: --mail-dir imports the
files supplied, and --lookback-days does not filter that directory. Keep aliases
and filenames unchanged on repeat imports so already processed mail is reused.
Existing imports under the default local-eml alias should keep that identity;
do not assign a new alias just because the option is now available.
The application records host imports as partial coverage because it cannot
independently verify the host’s mailbox search.
Direct Microsoft Graph/Outlook OAuth is not built in. Outlook support uses
an authorised host connector or .eml exports. A connector that cannot read full
messages is insufficient for complete offer terms. Live access also depends on
the connector’s account support and any organisation policies.
Self-hosted Gmail API mode. Use this when Weekly Deals itself should access Gmail from a terminal or server:
cp .env.example .env
# edit .env:
# MAIL_PROVIDER=gmail_api
# GMAIL_CLIENT_SECRET_PATH=/absolute/path/client_secret.json
./.venv/bin/weekly-deals auth gmail
./.venv/bin/weekly-deals scan --mode llm-only
./.venv/bin/weekly-deals calendar
For local processing, use --mode llm-only (or set classification.mode: "off"
in a file passed with --config) and
leave LLM_PROVIDER=mock. This uses Gmail only for reading and does not send
email text to a cloud model. The mock extractor is deterministic and is mainly
intended for testing.
Is JEV required?
No. JEV provides optional semantic classification and promotion deduplication in
both agent-host and self-hosted runs. Run weekly-deals auth jev to configure it. The default model is
jev-latest, using the official TypeSafe HTTP API.
Agent-host scans use --offline when JEV is intentionally omitted.
Subjects and normalised bodies sent to JEV leave the machine; the key is never
committed to the repository.
If the command says No module named 'weekly_deals'
On macOS with iCloud Drive's "Desktop & Documents" sync enabled, iCloud sets the
hidden flag on files it treats as internal — including the .pth file an
editable install (pip install -e .) leaves in site-packages. Python's site
module skips hidden .pth files, so the package silently disappears from the
import path.
Clearing the flag by hand does not hold — iCloud sets it again within minutes. Keep the virtualenv outside the synced folders instead:
python3 -m venv ~/.venvs/weekly-deals
~/.venvs/weekly-deals/bin/pip install "/path/to/DealWeek[gmail,llm,web]"
# ...then link it somewhere already on your PATH. Check first:
# echo $PATH | tr ':' '\n' | grep -E 'local/bin|homebrew/bin'
ln -sf ~/.venvs/weekly-deals/bin/weekly-deals /opt/homebrew/bin/weekly-deals
~/.local/bin is the conventional target, but it is not on the default macOS
PATH — add it to ~/.zshrc first if you use it, or link somewhere that already
is.
A non-editable install puts real files in site-packages and needs no .pth at
all, so it is immune regardless.
The same applies to your data: keep WEEKLY_DEALS_DATA_DIR off the Desktop and out
of Documents. The default (~/.local/share/weekly-deals) is already outside them —
pointing it at a synced folder would upload your email text to iCloud.
Configuration
Business settings are loaded with --config config.yaml (see config.example.yaml);
no configuration file is assumed when the flag is omitted. Credentials
live in private dotenv files or the environment (see .env.example) and are never
written to reports, logs or model prompts.
Credential precedence, highest first: shell environment → current-directory
.env → user ~/.config/weekly-deals/.env. Even an empty project key overrides
the user key; remove unused entries rather than leaving blank placeholders.
Business settings: defaults → explicit config file → environment → CLI flags.
How it works
Host connector / .eml / Gmail API (read-only)
└──► normalize (MIME/HTML/JSON-LD)
│
JEV classifier ──► observe | gate routing
│
LLM extractor ──► OfferDraft[] + evidence
│
validate ─ temporal ─ eligibility ─ deduplicate (deterministic)
│
JSON files ──► savings calendar / rule planner ──► report
Plain Python controls the sequence. No model decides what happens next, so the failure modes are enumerable and the whole thing is testable offline.
The generic Promotions calendar also has a separate post-scan path:
scan → dedupe (JEV) → calendar. It groups repeated campaigns without requiring
meal-offer extraction. This is separate from the deterministic offer-level
deduplication shown above; neither stage deletes source email.
docs/DECISIONS.md explains the choices people ask about most: why there is no
LLM framework here, why MCP is used to expose this application rather than to
read mail with it, and why the storage is a directory of JSON files rather than
a database.
For agent hosts
weekly-deals mcp
Exposes seven tools: get_status, sync_promotions, list_food_offers,
get_offer, build_meal_plan, render_report, set_offer_state. They call
the same service layer the CLI does.
The boundary is enforced, not advisory: read and plan freely, write only the user's own offer state, no file paths or URLs as arguments, a hard cap on messages per sync, and no way for a host to grant itself consent to spend money on cloud model calls.
Example, for a client that launches servers over stdio:
{
"mcpServers": {
"weekly-deals": { "command": "weekly-deals", "args": ["mcp"] }
}
}
Development
pip install -e ".[all,dev]"
pytest # offline regression suite; no network or mailbox
ruff check src tests
CI needs no secrets. The clock is injected everywhere, so time-dependent behaviour is tested at frozen instants rather than by waiting.
Status and limits
v0.1 is a working skeleton with a complete offline path. Known limits, stated rather than hidden:
- Offers inside images are flagged
needs_visual, not parsed. They are counted in coverage, never silently dropped. - The direct Gmail API backend uses one account per configuration; its default
scope is 90 days of
category:promotions. Agent-host imports can combine accounts and providers using stable account aliases and distinct filenames. The calendar indexes imported messages across food, shopping, travel, events, and services; dates it cannot prove stayunknownand are marked for review. gmail.readonlygrants read access to the whole mailbox at the OAuth layer. The restriction to promotional mail is enforced by this application's queries, not by Google.- No incremental history sync yet; each scan re-searches the window and skips unchanged messages by body hash. Expiry is recomputed locally on every read, so a stored offer never reports itself as live after its date has passed.
- Offers inside attached documents (a PDF flyer) are flagged
needs_visual, not read. They are counted, never silently treated as fully parsed. - One email per model call, deliberately — see
docs/DECISIONS.md§2b. Fetching is parallel where the source allows it; the paid stages are still serial because parallelising them races the budget cap. - JEV
gatemode is refused until you record an evaluation.observeis the default for a reason. - The extraction quality of the offline mock is deliberately modest — it is a deterministic rule engine for testing the pipeline, not a language model.
Your data
Everything lives in one directory — $WEEKLY_DEALS_DATA_DIR, or the platform
default — as plain JSON:
store/
messages/<key>.json one email: its text and its model results
offers.json the offers derived from them
promotions.json the generic savings calendar events
user_state.json used / dismissed / planned -- your decisions
duplicates.json near-matches waiting for you to settle
runs.json recent scans, with what they cost
plans/<id>.json plan snapshots
You can read it, grep it, correct it by hand, or delete it with rm -r. There
is no database file, and no write-ahead log keeping a copy of your mail after
you delete it.
privacy.payload_retention_days (default 180) drops stored message text once
it is that old, measured from the email's own date. Text still backing a live
offer is kept regardless — the validator's quotes have to stay checkable — and
a pruned record keeps its hash, so a re-scan still skips it without re-reading
your mailbox.
Licence
MIT. See SECURITY.md and PRIVACY.md for the threat model and what leaves
your machine.