cwarm 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cwarm-0.1.0/.github/workflows/ci.yml +21 -0
- cwarm-0.1.0/.github/workflows/publish.yml +108 -0
- cwarm-0.1.0/.gitignore +13 -0
- cwarm-0.1.0/CHANGELOG.md +22 -0
- cwarm-0.1.0/LICENSE +21 -0
- cwarm-0.1.0/PKG-INFO +277 -0
- cwarm-0.1.0/README.md +263 -0
- cwarm-0.1.0/config.example.json +15 -0
- cwarm-0.1.0/cwarm/__init__.py +3 -0
- cwarm-0.1.0/cwarm/agent.py +56 -0
- cwarm-0.1.0/cwarm/cli.py +228 -0
- cwarm-0.1.0/cwarm/config.py +226 -0
- cwarm-0.1.0/cwarm/cron.py +64 -0
- cwarm-0.1.0/cwarm/cswap.py +123 -0
- cwarm-0.1.0/cwarm/log.py +58 -0
- cwarm-0.1.0/cwarm/schedule.py +128 -0
- cwarm-0.1.0/cwarm/warmup.py +95 -0
- cwarm-0.1.0/pyproject.toml +44 -0
- cwarm-0.1.0/systemd/cwarm.service +25 -0
- cwarm-0.1.0/tests/test_batch.py +99 -0
- cwarm-0.1.0/tests/test_config.py +138 -0
- cwarm-0.1.0/tests/test_cron.py +51 -0
- cwarm-0.1.0/tests/test_cswap_parsing.py +67 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python-version: ["3.12", "3.13"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v5
|
|
16
|
+
- uses: actions/setup-python@v6
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python-version }}
|
|
19
|
+
- run: pip install -e ".[dev]"
|
|
20
|
+
- run: ruff check .
|
|
21
|
+
- run: python -m pytest -q
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
name: Release & Publish
|
|
2
|
+
|
|
3
|
+
# Run from the Actions tab ("Run workflow"). In one run this:
|
|
4
|
+
# 1. lints + tests,
|
|
5
|
+
# 2. bumps the version and updates CHANGELOG.md with Commitizen,
|
|
6
|
+
# 3. creates the git tag + GitHub Release,
|
|
7
|
+
# 4. builds and publishes to PyPI via Trusted Publishing (OIDC, no token).
|
|
8
|
+
#
|
|
9
|
+
# The file is named publish.yml on purpose: the PyPI Trusted Publisher is
|
|
10
|
+
# registered against this filename. Doing bump+release+publish in one workflow
|
|
11
|
+
# avoids the GitHub limitation where a release created by GITHUB_TOKEN does not
|
|
12
|
+
# trigger a separate publish workflow.
|
|
13
|
+
on:
|
|
14
|
+
workflow_dispatch:
|
|
15
|
+
inputs:
|
|
16
|
+
increment:
|
|
17
|
+
description: "Version bump (auto = infer from Conventional Commits)"
|
|
18
|
+
type: choice
|
|
19
|
+
default: auto
|
|
20
|
+
options: [auto, PATCH, MINOR, MAJOR]
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
release:
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
permissions:
|
|
26
|
+
contents: write # push the bump commit/tag and create the release
|
|
27
|
+
outputs:
|
|
28
|
+
version: ${{ steps.bump.outputs.version }}
|
|
29
|
+
released: ${{ steps.bump.outputs.released }}
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v5
|
|
32
|
+
with:
|
|
33
|
+
fetch-depth: 0 # commitizen needs full history + tags
|
|
34
|
+
- uses: actions/setup-python@v6
|
|
35
|
+
with:
|
|
36
|
+
python-version: "3.12"
|
|
37
|
+
- run: pip install -e ".[dev]"
|
|
38
|
+
- name: Lint and test
|
|
39
|
+
run: |
|
|
40
|
+
ruff check .
|
|
41
|
+
python -m pytest -q
|
|
42
|
+
- name: Configure git identity
|
|
43
|
+
run: |
|
|
44
|
+
git config user.name "github-actions[bot]"
|
|
45
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
46
|
+
- name: Bump version, changelog, and tag
|
|
47
|
+
id: bump
|
|
48
|
+
run: |
|
|
49
|
+
set -euo pipefail
|
|
50
|
+
increment="${{ inputs.increment }}"
|
|
51
|
+
if git describe --tags --abbrev=0 >/dev/null 2>&1; then
|
|
52
|
+
extra=""
|
|
53
|
+
if [ "$increment" != "auto" ]; then extra="--increment $increment"; fi
|
|
54
|
+
if cz bump --yes --changelog --changelog-to-stdout $extra > notes.md; then
|
|
55
|
+
echo "released=true" >> "$GITHUB_OUTPUT"
|
|
56
|
+
else
|
|
57
|
+
echo "No eligible commits since the last release — nothing to publish."
|
|
58
|
+
echo "released=false" >> "$GITHUB_OUTPUT"
|
|
59
|
+
exit 0
|
|
60
|
+
fi
|
|
61
|
+
else
|
|
62
|
+
# First release: tag the current version as-is (no bump).
|
|
63
|
+
version="$(cz version -p)"
|
|
64
|
+
cz changelog "$version" --dry-run > notes.md || true
|
|
65
|
+
git tag -a "v$version" -m "Release v$version"
|
|
66
|
+
echo "released=true" >> "$GITHUB_OUTPUT"
|
|
67
|
+
fi
|
|
68
|
+
echo "version=v$(cz version -p)" >> "$GITHUB_OUTPUT"
|
|
69
|
+
- name: Push commit and tag
|
|
70
|
+
if: steps.bump.outputs.released == 'true'
|
|
71
|
+
run: |
|
|
72
|
+
git push origin HEAD:${{ github.ref_name }}
|
|
73
|
+
git push origin "${{ steps.bump.outputs.version }}"
|
|
74
|
+
- name: Create GitHub Release
|
|
75
|
+
if: steps.bump.outputs.released == 'true'
|
|
76
|
+
env:
|
|
77
|
+
GH_TOKEN: ${{ github.token }}
|
|
78
|
+
run: |
|
|
79
|
+
if [ -s notes.md ]; then
|
|
80
|
+
gh release create "${{ steps.bump.outputs.version }}" \
|
|
81
|
+
--title "${{ steps.bump.outputs.version }}" --notes-file notes.md
|
|
82
|
+
else
|
|
83
|
+
gh release create "${{ steps.bump.outputs.version }}" \
|
|
84
|
+
--title "${{ steps.bump.outputs.version }}" --generate-notes
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
publish:
|
|
88
|
+
needs: release
|
|
89
|
+
if: needs.release.outputs.released == 'true'
|
|
90
|
+
runs-on: ubuntu-latest
|
|
91
|
+
environment:
|
|
92
|
+
name: pypi
|
|
93
|
+
url: https://pypi.org/p/cwarm
|
|
94
|
+
permissions:
|
|
95
|
+
id-token: write # required for Trusted Publishing
|
|
96
|
+
steps:
|
|
97
|
+
- uses: actions/checkout@v5
|
|
98
|
+
with:
|
|
99
|
+
ref: ${{ needs.release.outputs.version }} # build the freshly-tagged version
|
|
100
|
+
- uses: actions/setup-python@v6
|
|
101
|
+
with:
|
|
102
|
+
python-version: "3.12"
|
|
103
|
+
- name: Build sdist and wheel
|
|
104
|
+
run: |
|
|
105
|
+
python -m pip install --upgrade build
|
|
106
|
+
python -m build
|
|
107
|
+
- name: Publish to PyPI
|
|
108
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
cwarm-0.1.0/.gitignore
ADDED
cwarm-0.1.0/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes are documented here. This file is managed by
|
|
4
|
+
[Commitizen](https://commitizen-tools.github.io/commitizen/) — run `cz bump`
|
|
5
|
+
to cut a version and prepend the next entry from Conventional Commits.
|
|
6
|
+
|
|
7
|
+
## v0.1.0 (2026-06-21)
|
|
8
|
+
|
|
9
|
+
Initial release.
|
|
10
|
+
|
|
11
|
+
### Feat
|
|
12
|
+
|
|
13
|
+
- Stagger-warm multiple coding-agent accounts on per-account cron schedules, so
|
|
14
|
+
their rolling usage windows open early and reset at different times.
|
|
15
|
+
- Generic agent layer (a small data table): Claude Code via claude-swap today,
|
|
16
|
+
with a pluggable account-switcher seam for other CLIs. Selected per account.
|
|
17
|
+
- Commands: `init`, `validate`, `list`, `run`, `daemon`.
|
|
18
|
+
- APScheduler daemon with correct crontab day-of-week handling, multi-schedule
|
|
19
|
+
accounts, `skip_if_warm`, and save/restore of the active account (even on
|
|
20
|
+
failure).
|
|
21
|
+
- systemd unit, structured per-attempt logging, PyPI trusted-publishing and CI
|
|
22
|
+
workflows.
|
cwarm-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 wonderbyte
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
cwarm-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cwarm
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Stagger-warm coding-agent accounts (Claude Code, Codex, ...) so their usage windows open early and reset at different times.
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Requires-Dist: apscheduler<4,>=3.10
|
|
8
|
+
Requires-Dist: tzdata; platform_system == 'Windows'
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: commitizen>=3; extra == 'dev'
|
|
11
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
12
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# cwarm
|
|
16
|
+
|
|
17
|
+
[](https://github.com/wonderbyte/cwarm/actions/workflows/ci.yml)
|
|
18
|
+
[](https://pypi.org/project/cwarm/)
|
|
19
|
+
[](https://pypi.org/project/cwarm/)
|
|
20
|
+
[](LICENSE)
|
|
21
|
+
|
|
22
|
+
Stagger-warm multiple **coding-agent accounts** so their rolling usage windows
|
|
23
|
+
open early in the day and reset at different times. Today it warms **Claude Code**
|
|
24
|
+
accounts (paired with [`claude-swap`](https://pypi.org/project/claude-swap/),
|
|
25
|
+
`cswap`); the agent layer is generic, so other CLIs (e.g. Codex) can be added.
|
|
26
|
+
When the active account exhausts its window, another is already warm to switch
|
|
27
|
+
into.
|
|
28
|
+
|
|
29
|
+
This tool **stores no credentials** — the account-switcher (`claude-swap`) is the
|
|
30
|
+
source of truth for accounts and tokens. cwarm only orchestrates it.
|
|
31
|
+
|
|
32
|
+
## How it works
|
|
33
|
+
|
|
34
|
+
Claude Code's 5-hour window starts on an account's first message and resets
|
|
35
|
+
exactly 5 hours later. It's a fixed budget, not free capacity — warming only
|
|
36
|
+
*relocates* the dead/regeneration time so it lands outside your working hours.
|
|
37
|
+
Staggering the warmups (e.g. 05:00, 07:30, 10:00) keeps at least one fresh
|
|
38
|
+
account available through the day.
|
|
39
|
+
|
|
40
|
+
For each due account, cwarm:
|
|
41
|
+
|
|
42
|
+
1. switches to it (for Claude: `cswap --switch-to <id>`),
|
|
43
|
+
2. waits `settle_seconds` for the credential swap to land,
|
|
44
|
+
3. sends one minimal message via the agent's command (for Claude: `claude -p "Hi"`)
|
|
45
|
+
— anchoring that account's window,
|
|
46
|
+
|
|
47
|
+
then restores whichever account you had active before the batch (always, even
|
|
48
|
+
if a warmup fails).
|
|
49
|
+
|
|
50
|
+
> A **weekly cap** is shared across web, app, and Claude Code. Warming several
|
|
51
|
+
> accounts daily consumes some of it. Tracking that cap is out of scope.
|
|
52
|
+
|
|
53
|
+
## Agents
|
|
54
|
+
|
|
55
|
+
An "agent" is just two things: a **command** that sends a non-interactive prompt
|
|
56
|
+
(`<cli> -p "Hi"`) and an optional **account-switcher**. They live as a small data
|
|
57
|
+
table in `cwarm/agent.py` — not a class per agent:
|
|
58
|
+
|
|
59
|
+
| agent | command | switcher |
|
|
60
|
+
| --- | --- | --- |
|
|
61
|
+
| `claude` | `claude -p` | `cswap` (claude-swap) |
|
|
62
|
+
|
|
63
|
+
Each account picks its agent via the `agent` field (default `claude`). To add a
|
|
64
|
+
coding agent, add one row. Only a *new* switcher (something other than `cswap`)
|
|
65
|
+
needs code — a sibling module to `cwarm/cswap.py`. An agent with no switcher
|
|
66
|
+
warms whatever account that CLI currently has active (no multi-account swapping).
|
|
67
|
+
|
|
68
|
+
## Requirements
|
|
69
|
+
|
|
70
|
+
- For the `claude` agent: `claude-swap` installed and configured with every
|
|
71
|
+
target account added (`cswap --add-account` / `cswap --add-token sk-ant-oat01-…`),
|
|
72
|
+
and Claude Code (`claude`) runnable non-interactively.
|
|
73
|
+
- Python 3.12+.
|
|
74
|
+
|
|
75
|
+
## Platform support
|
|
76
|
+
|
|
77
|
+
cwarm is pure Python and runs anywhere the agent's CLIs (`claude`, `cswap`) do:
|
|
78
|
+
|
|
79
|
+
- **Linux** — fully supported, with the bundled `systemd` user service.
|
|
80
|
+
- **macOS** — the tool and `cwarm daemon` work the same; for boot persistence
|
|
81
|
+
use `launchd` or `cron` instead of systemd.
|
|
82
|
+
- **Windows** — works too; `tzdata` is pulled in automatically (Windows has no
|
|
83
|
+
system IANA tz database). Use Task Scheduler or run `cwarm daemon` as a
|
|
84
|
+
service instead of systemd.
|
|
85
|
+
|
|
86
|
+
`cwarm run`/`daemon` are cross-platform; only the deployment recipe differs.
|
|
87
|
+
|
|
88
|
+
## Install
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
cd ~/workspace/apps/cwarm
|
|
92
|
+
uv venv
|
|
93
|
+
uv pip install -e .
|
|
94
|
+
cp config.example.json config.json # then edit ids/schedules
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`config.json` is gitignored — it holds your real account ids.
|
|
98
|
+
|
|
99
|
+
## Configuration (`config.json`)
|
|
100
|
+
|
|
101
|
+
No tokens. Accounts are referenced by the handle their agent uses — for Claude, a
|
|
102
|
+
`cswap` **slot number** or **email**.
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"defaults": {
|
|
107
|
+
"agent": "claude",
|
|
108
|
+
"message": "Hi",
|
|
109
|
+
"timezone": "Asia/Kolkata",
|
|
110
|
+
"settle_seconds": 3,
|
|
111
|
+
"skip_if_warm": true
|
|
112
|
+
},
|
|
113
|
+
"accounts": [
|
|
114
|
+
{ "id": "work@example.com", "enabled": true, "schedules": ["0 5 * * 1-5", "0 11 * * 1-5", "0 21 * * 1-5"] },
|
|
115
|
+
{ "id": "2", "enabled": true, "schedule": "30 7 * * 1-5" },
|
|
116
|
+
{ "id": "personal@x.com", "enabled": true, "schedules": ["0 10 * * *", "0 18 * * *"] },
|
|
117
|
+
{ "id": "4", "enabled": false, "schedule": "30 12 * * *" }
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
An account can warm at **several times a day** — give it a `schedules` array
|
|
123
|
+
(e.g. 05:00, 11:00, 21:00). Use the singular `schedule` string for a single
|
|
124
|
+
time. Both keys may be present; their union (de-duplicated) is used. Each cron
|
|
125
|
+
time becomes its own daemon job, fired in the account's `timezone`.
|
|
126
|
+
|
|
127
|
+
| Field | Required | Default | Notes |
|
|
128
|
+
| --- | --- | --- | --- |
|
|
129
|
+
| `id` | yes | — | unique; the agent's account handle (cswap slot or email) |
|
|
130
|
+
| `schedule` / `schedules` | yes | — | one (string) or many (array) 5-field cron times, read in the account's `timezone` |
|
|
131
|
+
| `agent` | no | `defaults` / `claude` | which coding agent warms this account |
|
|
132
|
+
| `enabled` | no | `true` | `false` skips the account entirely |
|
|
133
|
+
| `message` | no | `defaults` / `"Hi"` | the warmup message |
|
|
134
|
+
| `timezone` | no | `defaults` / `Asia/Kolkata` | IANA tz name |
|
|
135
|
+
| `settle_seconds` | no | `defaults` / `3` | delay after switching before sending |
|
|
136
|
+
| `skip_if_warm` | no | `defaults` / `false` | skip if the window is already open |
|
|
137
|
+
|
|
138
|
+
## Usage
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
cwarm init # write a starter config.json
|
|
142
|
+
cwarm validate # check config + agents; sends nothing
|
|
143
|
+
cwarm list # show accounts, live window state, next run
|
|
144
|
+
cwarm run # warm all enabled accounts now
|
|
145
|
+
cwarm run --account work@example.com # warm just one
|
|
146
|
+
cwarm daemon # long-lived; fires each account on its cron
|
|
147
|
+
|
|
148
|
+
# global flags
|
|
149
|
+
cwarm --config /path/to/config.json --log-file /path/to/cwarm.log <command>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
- **`init`** — writes a starter `config.json` (won't clobber an existing one
|
|
153
|
+
without `--force`).
|
|
154
|
+
- **`validate`** — confirms the JSON matches the schema, each account's agent CLI
|
|
155
|
+
(and its switcher) is installed, and every configured `id` exists. Exits
|
|
156
|
+
non-zero and sends nothing on any problem.
|
|
157
|
+
- **`list`** — read-only table of every account: agent, enabled, live window
|
|
158
|
+
state (warm/cold), next scheduled run, and its cron times. Sends nothing.
|
|
159
|
+
- **`run`** — warms enabled accounts immediately (one batch, one save/restore per
|
|
160
|
+
switcher). Good for testing or a system `crontab`. Non-zero if any warmup failed.
|
|
161
|
+
- **`daemon`** — schedules one job per account per cron time, in the account's
|
|
162
|
+
timezone. Warmups are always serial.
|
|
163
|
+
|
|
164
|
+
### Logging
|
|
165
|
+
|
|
166
|
+
Every attempt emits one structured line to stderr (and the log file if set):
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
2026-06-21T05:00:03+0530 INFO save-active switcher=cswap account=work@example.com ref=1
|
|
170
|
+
2026-06-21T05:00:09+0530 INFO warmup account=work@example.com outcome=ok reset=10:00
|
|
171
|
+
2026-06-21T05:00:10+0530 INFO restore-active switcher=cswap account=work@example.com ref=1 outcome=ok
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Outcomes: `ok` (with the window `reset` time), `failed` (with an `error`
|
|
175
|
+
summary), `skipped` (`skip_if_warm` and already warm).
|
|
176
|
+
|
|
177
|
+
## Deployment
|
|
178
|
+
|
|
179
|
+
### systemd (recommended)
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
mkdir -p ~/.config/systemd/user ~/.local/state/cwarm
|
|
183
|
+
cp systemd/cwarm.service ~/.config/systemd/user/
|
|
184
|
+
systemctl --user daemon-reload
|
|
185
|
+
systemctl --user enable --now cwarm
|
|
186
|
+
loginctl enable-linger "$USER" # run without an active login session
|
|
187
|
+
journalctl --user -u cwarm -f
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Alternative: system crontab
|
|
191
|
+
|
|
192
|
+
One line per warmup time invoking the one-shot mode (repeat a line per account
|
|
193
|
+
to warm it several times a day):
|
|
194
|
+
|
|
195
|
+
```cron
|
|
196
|
+
0 5 * * 1-5 cd ~/workspace/apps/cwarm && .venv/bin/cwarm run --account work@example.com
|
|
197
|
+
0 11 * * 1-5 cd ~/workspace/apps/cwarm && .venv/bin/cwarm run --account work@example.com
|
|
198
|
+
0 21 * * 1-5 cd ~/workspace/apps/cwarm && .venv/bin/cwarm run --account work@example.com
|
|
199
|
+
30 7 * * 1-5 cd ~/workspace/apps/cwarm && .venv/bin/cwarm run --account 2
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Energy
|
|
203
|
+
|
|
204
|
+
The daemon does **not** poll — it sleeps on an event until the next scheduled
|
|
205
|
+
warmup, so idle cost is ~25 MB RAM and effectively 0% CPU (measured: 1 voluntary
|
|
206
|
+
context switch over 3 s idle). There is no busy-loop to optimise.
|
|
207
|
+
|
|
208
|
+
The real per-warmup energy is the `claude -p "Hi"` call: it boots the Node
|
|
209
|
+
Claude Code CLI **and** sends a real LLM inference request to anchor the window.
|
|
210
|
+
That's irreducible — anchoring *requires* a server-side message. So the only
|
|
211
|
+
meaningful lever is **not sending redundant ones**:
|
|
212
|
+
|
|
213
|
+
- **`skip_if_warm: true`** (the example default) — before switching, parse
|
|
214
|
+
`cswap --list`; if the account's window is already open, log `skipped` and send
|
|
215
|
+
nothing. This skips the entire heavy `claude -p` call, the single biggest
|
|
216
|
+
energy saving available.
|
|
217
|
+
- **Stagger, don't stack** — overlapping schedules waste warmups (and the shared
|
|
218
|
+
weekly cap). One warmup per window per account is enough to anchor it.
|
|
219
|
+
|
|
220
|
+
For literally zero idle footprint, use systemd timers or cron instead of the
|
|
221
|
+
daemon — no process is resident between warmups — but the saving over the
|
|
222
|
+
sleeping daemon is marginal.
|
|
223
|
+
|
|
224
|
+
## System restart
|
|
225
|
+
|
|
226
|
+
Yes. Via the systemd **user** service plus linger:
|
|
227
|
+
|
|
228
|
+
- `systemctl --user enable` + `loginctl enable-linger "$USER"` → the daemon
|
|
229
|
+
**starts on boot**, with no login session required.
|
|
230
|
+
- `Restart=always` (RestartSec=10) → if the process ever exits — crash or
|
|
231
|
+
otherwise — systemd brings it straight back.
|
|
232
|
+
- On every (re)start the schedule is **rebuilt fresh from `config.json`**
|
|
233
|
+
(in-memory jobstore; the config is the single source of truth — no stale
|
|
234
|
+
persisted state to reconcile).
|
|
235
|
+
- **Short downtime is tolerated:** `misfire_grace_time=3600` + `coalesce=True`
|
|
236
|
+
mean a warmup missed by under an hour still fires once on recovery.
|
|
237
|
+
- **Long power-off is *not* caught up by design:** a 05:00 warmup missed because
|
|
238
|
+
the machine was off until noon is skipped, not fired late — firing it hours
|
|
239
|
+
late would defeat the staggering. Edit `config.json` and
|
|
240
|
+
`systemctl --user restart cwarm` to re-plan.
|
|
241
|
+
|
|
242
|
+
## Safety
|
|
243
|
+
|
|
244
|
+
- No credentials stored or logged — the switcher (`claude-swap`) owns them.
|
|
245
|
+
- Switching changes your local active account; run warmups at off-hours. The
|
|
246
|
+
save/restore guarantees your default is unchanged after a batch.
|
|
247
|
+
- Only configure accounts you legitimately own or are authorised to use.
|
|
248
|
+
|
|
249
|
+
## Contributing / releasing
|
|
250
|
+
|
|
251
|
+
Commits follow [Conventional Commits](https://www.conventionalcommits.org/)
|
|
252
|
+
(`feat:`, `fix:`, `docs:`, `ci:`, …). Lint and tests run in CI:
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
ruff check .
|
|
256
|
+
pytest
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Versioning and the changelog are managed by
|
|
260
|
+
[Commitizen](https://commitizen-tools.github.io/commitizen/).
|
|
261
|
+
|
|
262
|
+
**To release:** run the **Release & Publish** workflow from the Actions tab
|
|
263
|
+
("Run workflow", optionally choosing the bump size). In one run it bumps the
|
|
264
|
+
version (`pyproject.toml` + `cwarm/__init__.py`), updates `CHANGELOG.md`, tags
|
|
265
|
+
and creates the GitHub Release, then builds and publishes to PyPI via Trusted
|
|
266
|
+
Publishing — no token stored. The next version is inferred from the Conventional
|
|
267
|
+
Commits since the last release.
|
|
268
|
+
|
|
269
|
+
Or do it locally:
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
cz bump # bump version + changelog + create the vX.Y.Z tag
|
|
273
|
+
git push --follow-tags
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
The project is in `0.x` (`major_version_zero`), so breaking changes bump the
|
|
277
|
+
minor until `1.0.0`.
|