awewarm: Subscription Window Warmer
Keep AI coding-plan windows warm with one minimal request.
Connect once; awewarm detects what your Claude Code / Codex account or subscription endpoint can do, then makes sure the next usage window is always already open.
English · 简体中文
Connect once, then never think about 5-hour resets again: awewarm detects what it can do and only then keeps the next window open.
awewarm manages two kinds of connections:
- Account — your local
claude/codexCLI logins. awewarm reuses their login state and sends one minimal headless request (Reply with exactly: ok). No credentials are stored. - Subscription plan — any OpenAI Chat / OpenAI Responses / Anthropic-compatible endpoint with a base URL + API key. The key is stored in
~/.config/awewarm/secrets.json(chmod 600) so the background scheduler can always read it.
It schedules those requests in two modes — fixed and interval — explained in Scheduling Modes below. Interval-style renewal stays locked until the window semantics are verified or user-confirmed; fixed is always safe.
Install
Requires Python ≥ 3.9:
pip3 install awewarm
The background scheduler installs on macOS (launchd), Windows (Task Scheduler), and Linux (systemd user timer — loginctl enable-linger $USER first on headless/SSH accounts). Where systemd is unavailable, cron the tick: * * * * * awewarm tick.
All keys live in secrets.json — env-var references were removed because the background scheduler (launchd/systemd/Task Scheduler) cannot read shell variables and would silently fail with "API key unavailable".
Quick Start
Let an AI agent set it up
Working in Claude Code, Codex, or another coding agent? Tell it:
Read https://github.com/wehuman01/awewarm/blob/main/README.ai.md and follow it to install and configure awewarm.
The agent installs the CLI, scans your local accounts (read-only), and tunes schedules on request. Onboarding itself (awewarm init, awewarm config add) stays in your terminal — it prompts for choices and API keys. After setup you can ask things like "when is the next warm-up?" or "set claude-code to 06:35 and 12:35".
Manual setup
awewarm init # scan local accounts, pick a schedule, install the scheduler
awewarm status # see what will happen next
Every added connection — account or endpoint — gets one test request during setup, so a broken model or bad key surfaces immediately instead of at 6 a.m.
For a subscription endpoint instead:
awewarm config add
You will be asked for the protocol, API base URL, API key, and model; awewarm tests the endpoint with one minimal request, then stores the key in secrets.json. The same command also re-adds a local claude / codex account you removed earlier — it lists whatever is detected on this machine.
Companion Tools
awewarm is part of a small tool family for AI coding agents:
- aweswitch — agent profile switcher for Claude Code, Codex, and OpenCode. aweswitch manages which provider a session launches with; awewarm keeps that provider's subscription window open underneath. If you launch coding-plan profiles with aweswitch, awewarm is the piece that keeps those 5-hour windows from going cold overnight.
- aweskill — CLI skill package manager for AI agents (47+ agents).
- aweshelf — session bookmark manager for Claude Code and Codex.
- awerouter — smart LLM router: flash/pro split by structural signals.
Scheduling Modes
Both modes send the same one minimal request — what differs is when it fires. Switch with awewarm config set <id> --mode fixed|interval; see the current mode and next due moment with awewarm status. The old hybrid mode was removed — a fixed grid spaced one window apart already keeps windows chained all day, with calendar wake coverage interval cannot offer. Existing hybrid configs migrate to fixed on first load.
| Mode | Fires when | Needs a verified window | Best for |
|---|---|---|---|
fixed |
fixed times each day | no | predictable hours; unverified plans; sleeping Macs |
interval |
window + grace after each success | yes | 24/7 warmth on an always-on machine |
fixed — absolute times, always safe
One request at each fixed local time (weekday or every-day); each hit opens a fresh window.
- If the machine was asleep at the slot time, the slot still fires late within the catch-up window (default 45 min); past that it is recorded as skipped.
- A slot landing within 30 min of a previous success is skipped — never pay for two windows at once.
- The only mode that works while window semantics are unknown, which is why unverified plans start here.
- During setup, when the window duration is known, awewarm asks for the plan's daily quota reset time and offers a full-day grid anchored on it — one slot per window, spaced window + 5 min apart (e.g. reset 01:14 + a 5 h window → 01:14, 06:19, 11:24, 16:29, 21:34). Declining keeps just the time you entered. Plans added in fixed mode are asked for the window duration first (default 300) — it spaces the grid and is recorded as a user-confirmed window that unlocks interval mode.
awewarm config set claude-code --times 06:35,11:40,16:45 # 5 h + 5 min apart: windows chain across a workday
awewarm config set claude-code --mode fixed
Example — a laptop that sleeps at night: slots at 06:35 / 11:40 / 16:45 keep a window open from 06:35 to ~21:45 every weekday. The machine only needs to be awake within 30 min of each slot.
interval — rolling renewal
After each success the next request is scheduled window + grace later (default 300 min + 75 s, plus up to 30 s jitter). The grace runs after the old window has closed — firing earlier would land inside the old window and start nothing. With no success recorded yet, one request fires immediately as the first anchor — unless you defer that start with --start HH:MM: no request fires before that moment (today, or tomorrow if it has passed), the first tick after it opens the chain, and the gate clears on the first success.
awewarm run my-plan # 1. one minimal request, timestamped
# ...watch when the plan's quota resets, note the elapsed minutes...
awewarm config set my-plan --window 300 # 2. record the window (unlocks interval)
awewarm config set my-plan --mode interval # 3. rolling renewal
A manual run <id> never shifts the renewal chain — the next due moment stays as scheduled. Add --reset-due to restart the chain from this run instead.
Example — an always-on machine you want warm around the clock, nights and weekends included: no wake machinery needed, renewal just keeps rolling.
Quick Templates
Common scheduling patterns to get started:
# Standard workday (morning + afternoon)
awewarm config set <id> --times 06:00,11:05,16:10
# With evening overtime
awewarm config set <id> --times 06:00,11:05,16:10,21:15
# Weekday only
awewarm config set <id> --times 08:00,13:05 --days weekday
# Interval (verified 5h window)
awewarm config set <id> --mode interval --window 300 --anchor 11:05
All fixed-time slots above are 5 h 5 min apart — the subscription window (5 h) plus a 5 min buffer for scheduling imprecision. Each slot fires once and opens a fresh window. With four slots (06:00, 11:05, 16:10, 21:15) you get coverage across the whole day: morning, afternoon, evening, and late night for overtime. Three slots (06:00, 11:05, 16:10) cover a standard workday. --days weekday limits fires to weekdays. Two-slot chains work well for half-day or intermittent use.
When requests fail — the health ladder
Both modes share one ladder: connected → failing → degraded → auto-disabled.
connected ──first failure──▶ failing ──N consecutive lost──▶ degraded ──N more lost──▶ auto-disabled
▲ │ │ │
└──────── any success (node/catch-up/manual run) ──────┘ │
│
└────── --on / run ──┘
- A failed node (a fixed slot, or an interval renewal moment) enters failing and gets catch-up retries — by default 5 attempts within 30 minutes, spaced ~5 minutes apart (defaults via
awewarm config settings; one connection via--catchup-attempts/--catchup-minutes). - 3 consecutive lost nodes (default 3, via
awewarm config settings --degrade-after-nodesor a per-connection--degrade-after-nodes) drop the connection to degraded: single shot per node, no more catch-up. interval probes once per window; fixed fires each slot exactly once. - The same count again while degraded stops it entirely: auto-disabled, silent until you resume with
awewarm config set <id> --on(or a manualrun <id>that succeeds). - Any success — node attempt, catch-up retry, manual run — resets the whole ladder. Manual attempts never count as nodes, and a slot the machine slept through (zero attempts) is not a lost node.
statusshows the rung plus details (Health: failing — 1/3 nodes lost, catch-up attempt 2/5), and prints the last failure with its error right under the last activation.
Sleeping Macs — calendar wake (macOS)
scheduler install writes one StartCalendarInterval entry per fixed slot into the launchd agent. launchd wakes the Mac from sleep — lid closed and deep sleep included — and runs the tick at the exact slot time. No sudo, and every slot is protected. Entries fire every day regardless of the slot's day rule: the tick itself decides whether today is an active day, so a weekend wake for a weekday-only slot is a harmless no-op. Editing times/mode updates the entries immediately; the first tick after any edit heals drift automatically.
Per connection, schedule.wakeWhenAsleep: false opts out (asked during setup; change later with awewarm config set <id> --no-wake). Missed slots still fire late within the catch-up window once the machine wakes. A fully shut down Mac stays off — power it on and the first tick catches up anything still inside the catch-up window.
Sleeping PCs — wake tasks (Windows)
The macOS design, mirrored: scheduler install registers one extra Task Scheduler task per fixed slot — a daily trigger at the slot time with Wake to run enabled, running awewarm tick. The per-minute tick task itself never wakes the machine (a waking tick would keep it from ever staying asleep); only slot times do. schtasks.exe cannot set Wake to run, so the tasks are registered through PowerShell's Register-ScheduledTask. The setup flow asks whether fixed slots may wake the machine (same prompt as macOS), awewarm config set <id> --no-wake opts a connection out, and install/uninstall/refresh/self-heal keep the task set in sync with the config.
Always-on servers (Linux)
No wake machinery exists or is needed on a machine that never sleeps — awewarm scheduler install sets up the systemd user timer directly (tick every minute; Persistent=true fires a missed tick at boot). Copy config.json and secrets.json over (or re-run init), and note that CLI-based connections need their CLI installed on the server. loginctl enable-linger $USER first on headless/SSH accounts. Linux simply cannot wake a suspended machine: the setup flow never asks, connections default to wakeWhenAsleep: false, and missed slots catch up within their catch-up windows once the machine wakes.
Remote Server — delegation to a 24/7 box
A sleeping laptop only wakes for fixed slots; an interval chain drifts while it is closed. For around-the-clock warmth, delegate subscription connections to an awewarm serve process on any always-on machine (VPS, NAS, Raspberry Pi). CLI-account connections cannot be delegated — their login lives on your machine and keeps ticking locally.
The server holds no secrets on disk. The pairing token and your API keys stay in the local secrets.json and are pushed over the wire; the server keeps them in RAM only. Restart it and the local machine re-claims and re-pushes automatically the next time it is online. A slot that came due while its key was missing is held, not failed — it still fires inside the catch-up window once the key returns, exactly like a machine that was asleep; past the window it is recorded as skipped.
Set up the server (once):
ssh my-server
pip3 install awewarm
awewarm serve --data-dir ~/awewarm-server # listens on 127.0.0.1:8790
Keep it running with a systemd user unit (~/.config/systemd/user/awewarm.service):
[Unit]
Description=awewarm serve
After=network-online.target
[Service]
ExecStart=awewarm serve --data-dir %h/awewarm-server
Restart=on-failure
[Install]
WantedBy=default.target
systemctl --user enable --now awewarm (with loginctl enable-linger $USER on headless boxes). Expose it through a cloudflared tunnel — free TLS, no open inbound ports, your origin IP stays hidden:
cloudflared tunnel create awewarm
cloudflared tunnel route dns awewarm warm.example.com
cloudflared tunnel run --url http://127.0.0.1:8790 awewarm
Delegate from the laptop:
awewarm remote connect https://warm.example.com # token generated + stored locally, server claimed
awewarm config set glm --remote # the server takes over this connection
awewarm status # merged view: local + delegated truth
--remote only lands after the server accepted the push, so a connection is never left with nobody ticking it. Everything keeps working on delegated connections: config set pushes schedule edits automatically (offline edits stay local and pending; awewarm remote push reconciles later), awewarm run glm fires on the server and reports back, and awewarm config set glm --local takes a connection back — server state is pulled first so local scheduling resumes where the server left off. awewarm remote disconnect forgets the server and refuses while anything is still delegated. Fixed times run in the delegating machine's timezone (it travels with the push); wake-from-sleep does not apply on a server that never sleeps.
Config
Users never hand-edit config; init / config add generate it at ~/.config/awewarm/config.json (state at ~/.local/state/awewarm/state.json). The shape, for reference:
{
"version": 2,
"connections": {
"claude-code": {
"label": "Claude Code",
"cli": "/usr/local/bin/claude",
"model": "haiku",
"windowMinutes": 300,
"mode": "fixed",
"times": ["06:35"],
"days": "weekday"
},
"glm": {
"label": "glm",
"url": "https://open.bigmodel.cn/api/coding/paas/v4",
"protocol": "openai-chat",
"apiKey": "file:glm",
"model": "GLM-5-Turbo",
"windowMinutes": 300,
"mode": "fixed",
"times": ["06:00"],
"days": "every-day",
"location": "remote"
}
},
"remote": {
"url": "https://warm.example.com",
"tokenRef": "file:remote-token"
}
}
A connection with url + apiKey is a subscription; one with cli is a local account. apiKey is file:<id> — the pasted key lives in ~/.config/awewarm/secrets.json (chmod 600), readable by the background scheduler. location: "remote" (absent = local) marks a connection ticked by the paired awewarm serve server, whose URL and token ref live in the top-level remote block. windowMinutes present means the window is verified/user-confirmed (interval renewal unlocked). Tuning knobs (catch-up, grace, jitter) stay at code defaults unless changed. v1 config files upgrade to this format automatically on first load.
Commands
awewarm init # interactive onboarding: scan accounts, pick schedules, install scheduler
awewarm discover # read-only scan of local CLIs and logins
awewarm config add # add a connection: a detected account or a subscription endpoint
awewarm config set <id> [flags] # show or change settings: --times, --days, --mode, --on/--off, --anchor, --start, --window
awewarm config remove <id> # delete a connection, its state, and its stored API key
awewarm config show / edit # print the on-disk config / open it in $EDITOR (validated on exit)
awewarm config path # config / state / log locations
awewarm status [<id>] [--json] # summary; one connection in detail; redacted machine-readable dump
awewarm run # fire every enabled connection now, ignoring the schedule
awewarm run <id> [--reset-due] # fire one connection now (schedule untouched unless --reset-due)
awewarm scheduler install / uninstall # background scheduler (launchd / Task Scheduler / systemd)
awewarm serve # run the always-on server that ticks delegated connections
awewarm remote connect <url> # pair with a server (token generated + stored locally)
awewarm remote status # server view: uptime, last tick, delegated connections
awewarm remote push [<id>] # re-sync delegated connections to the server (config + keys)
awewarm remote disconnect # forget the server (refuses while delegations exist)
awewarm update [--check] # upgrade to the latest PyPI release
Commands from pre-0.3 releases (add plan, times, enable, disable, verify, anchor, activate, remove, install, uninstall, inspect, self-update) still work as hidden aliases; they print their new spelling and will be removed in v1.0.
Self-Update
awewarm checks PyPI in the background — at most once a day, and never during scheduler ticks. When a newer release exists, interactive commands print a reminder to stderr.
awewarm update # upgrade to the latest release
awewarm update --check # show versions only
To disable the background check:
export AWEWARM_NO_UPDATE_CHECK=1
Development
pip install -e .
python3 -m unittest discover -s tests
See docs/CONTRIBUTING.md for the engineering doctrine and docs/CHANGELOG.md for release history.
Support
If awewarm saves your quota, consider supporting it:
- ⭐ Star the repo — it helps others find it.
- ☕ Ko-fi — buy me a coffee.
- 💬 WeChat — scan the QR code below.
awewarm is free and open source. Sponsors keep it maintained — thank you.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file awewarm-0.4.0.tar.gz.
File metadata
- Download URL: awewarm-0.4.0.tar.gz
- Upload date:
- Size: 129.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
75ec802dcb8fb6bbeb9a1762818f2dcab6df9fe3fa8266df60e9a01248af5f6a
|
|
| MD5 |
598f75d7991364b67330b573c107fd7b
|
|
| BLAKE2b-256 |
569195a5f2cda13e75540dbba50d708362077d257095c553f0a00006afc83a03
|
File details
Details for the file awewarm-0.4.0-py3-none-any.whl.
File metadata
- Download URL: awewarm-0.4.0-py3-none-any.whl
- Upload date:
- Size: 69.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
71633adc9ea11e721e23bacadd3a593754ed6a3d19d2848ed7248f0ceab40f08
|
|
| MD5 |
06213c547adb83abb202a146959d5ada
|
|
| BLAKE2b-256 |
1e59b59074e231add6f1042bb0081943305fdaad0b7e478e4361c7ac60cc59ff
|