loops-routines 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.
- loops_routines-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +30 -0
- loops_routines-0.1.0/.github/ISSUE_TEMPLATE/config.yml +5 -0
- loops_routines-0.1.0/.github/dependabot.yml +20 -0
- loops_routines-0.1.0/.github/pull_request_template.md +9 -0
- loops_routines-0.1.0/.github/workflows/ci.yml +39 -0
- loops_routines-0.1.0/.github/workflows/release.yml +70 -0
- loops_routines-0.1.0/.gitignore +5 -0
- loops_routines-0.1.0/CHANGELOG.md +24 -0
- loops_routines-0.1.0/CONTRIBUTING.md +39 -0
- loops_routines-0.1.0/LICENSE +21 -0
- loops_routines-0.1.0/PKG-INFO +220 -0
- loops_routines-0.1.0/README.md +194 -0
- loops_routines-0.1.0/SECURITY.md +31 -0
- loops_routines-0.1.0/docs/screenshot.jpg +0 -0
- loops_routines-0.1.0/loops_routines/__init__.py +6 -0
- loops_routines-0.1.0/loops_routines/__main__.py +3 -0
- loops_routines-0.1.0/loops_routines/cli.py +282 -0
- loops_routines-0.1.0/loops_routines/client.py +69 -0
- loops_routines-0.1.0/loops_routines/core/__init__.py +0 -0
- loops_routines-0.1.0/loops_routines/core/claude_cli.py +61 -0
- loops_routines-0.1.0/loops_routines/core/clock.py +19 -0
- loops_routines-0.1.0/loops_routines/core/config.py +217 -0
- loops_routines-0.1.0/loops_routines/core/deliver.py +50 -0
- loops_routines-0.1.0/loops_routines/core/evaluate.py +80 -0
- loops_routines-0.1.0/loops_routines/core/integrations.py +368 -0
- loops_routines-0.1.0/loops_routines/core/notify.py +124 -0
- loops_routines-0.1.0/loops_routines/core/paths.py +51 -0
- loops_routines-0.1.0/loops_routines/core/probe.py +61 -0
- loops_routines-0.1.0/loops_routines/core/routines.py +213 -0
- loops_routines-0.1.0/loops_routines/core/runner.py +489 -0
- loops_routines-0.1.0/loops_routines/core/store.py +233 -0
- loops_routines-0.1.0/loops_routines/core/triggers.py +178 -0
- loops_routines-0.1.0/loops_routines/core/usage.py +119 -0
- loops_routines-0.1.0/loops_routines/core/windows.py +111 -0
- loops_routines-0.1.0/loops_routines/daemon/__init__.py +0 -0
- loops_routines-0.1.0/loops_routines/daemon/app.py +267 -0
- loops_routines-0.1.0/loops_routines/daemon/auth.py +90 -0
- loops_routines-0.1.0/loops_routines/daemon/main.py +21 -0
- loops_routines-0.1.0/loops_routines/daemon/mcp.py +235 -0
- loops_routines-0.1.0/loops_routines/defaults/README.md +37 -0
- loops_routines-0.1.0/loops_routines/defaults/routines/cleanup.md +23 -0
- loops_routines-0.1.0/loops_routines/defaults/routines/monitor-logs.md +29 -0
- loops_routines-0.1.0/loops_routines/defaults/routines/weekly-summary.md +21 -0
- loops_routines-0.1.0/loops_routines/install.py +144 -0
- loops_routines-0.1.0/loops_routines/service.py +810 -0
- loops_routines-0.1.0/loops_routines/ui/__init__.py +0 -0
- loops_routines-0.1.0/loops_routines/ui/web/index.html +450 -0
- loops_routines-0.1.0/pyproject.toml +61 -0
- loops_routines-0.1.0/tests/conftest.py +126 -0
- loops_routines-0.1.0/tests/test_api.py +80 -0
- loops_routines-0.1.0/tests/test_evaluate.py +87 -0
- loops_routines-0.1.0/tests/test_install_cli.py +93 -0
- loops_routines-0.1.0/tests/test_mcp.py +81 -0
- loops_routines-0.1.0/tests/test_notify.py +28 -0
- loops_routines-0.1.0/tests/test_routines.py +92 -0
- loops_routines-0.1.0/tests/test_service.py +382 -0
- loops_routines-0.1.0/tests/test_triggers.py +71 -0
- loops_routines-0.1.0/uv.lock +844 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Bug report
|
|
2
|
+
description: Something doesn't work as described
|
|
3
|
+
labels: [bug]
|
|
4
|
+
body:
|
|
5
|
+
- type: textarea
|
|
6
|
+
id: what
|
|
7
|
+
attributes:
|
|
8
|
+
label: What happened
|
|
9
|
+
description: What you did, what you expected, what happened instead.
|
|
10
|
+
validations:
|
|
11
|
+
required: true
|
|
12
|
+
- type: textarea
|
|
13
|
+
id: routine
|
|
14
|
+
attributes:
|
|
15
|
+
label: The routine
|
|
16
|
+
description: The routine file's header (between the +++ lines), if a routine is involved. Remove anything private.
|
|
17
|
+
render: toml
|
|
18
|
+
- type: textarea
|
|
19
|
+
id: logs
|
|
20
|
+
attributes:
|
|
21
|
+
label: Logs
|
|
22
|
+
description: "`loops-routines status`, `loops-routines get RUN`, the end of ~/Library/Logs/loops_routines/daemon.log. Remove tokens and private data."
|
|
23
|
+
render: text
|
|
24
|
+
- type: input
|
|
25
|
+
id: versions
|
|
26
|
+
attributes:
|
|
27
|
+
label: Versions
|
|
28
|
+
description: loops-routines (`loops-routines status`), `claude --version`, macOS
|
|
29
|
+
validations:
|
|
30
|
+
required: true
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: uv
|
|
4
|
+
directory: /
|
|
5
|
+
schedule:
|
|
6
|
+
interval: weekly
|
|
7
|
+
cooldown:
|
|
8
|
+
default-days: 7
|
|
9
|
+
groups:
|
|
10
|
+
python:
|
|
11
|
+
patterns: ["*"]
|
|
12
|
+
- package-ecosystem: github-actions
|
|
13
|
+
directory: /
|
|
14
|
+
schedule:
|
|
15
|
+
interval: weekly
|
|
16
|
+
cooldown:
|
|
17
|
+
default-days: 7
|
|
18
|
+
groups:
|
|
19
|
+
actions:
|
|
20
|
+
patterns: ["*"]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
## What and why
|
|
2
|
+
|
|
3
|
+
<!-- What changes for a user, and the problem it solves. Link the issue if there is one. -->
|
|
4
|
+
|
|
5
|
+
## Checks
|
|
6
|
+
|
|
7
|
+
- [ ] A test that fails without this change
|
|
8
|
+
- [ ] `uv run pytest` and `uv run ruff check .` pass
|
|
9
|
+
- [ ] A line in `CHANGELOG.md` under "Unreleased", if a user would notice
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
concurrency:
|
|
12
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
13
|
+
cancel-in-progress: true
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
lint:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
with:
|
|
21
|
+
fetch-depth: 0
|
|
22
|
+
- uses: astral-sh/setup-uv@v6
|
|
23
|
+
- run: uv run --frozen ruff check .
|
|
24
|
+
|
|
25
|
+
test:
|
|
26
|
+
runs-on: macos-latest
|
|
27
|
+
strategy:
|
|
28
|
+
fail-fast: false
|
|
29
|
+
matrix:
|
|
30
|
+
python: ["3.12", "3.13", "3.14"]
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
with:
|
|
34
|
+
fetch-depth: 0 # hatch-vcs reads the version from git tags
|
|
35
|
+
- uses: astral-sh/setup-uv@v6
|
|
36
|
+
with:
|
|
37
|
+
python-version: ${{ matrix.python }}
|
|
38
|
+
enable-cache: true
|
|
39
|
+
- run: uv run --frozen pytest -q
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Push a tag vX.Y.Z on main: the tests run, the package is built with that version, published to PyPI
|
|
4
|
+
# through Trusted Publishing (no API token) with attestations, and a GitHub Release is created.
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
tags: ["v*.*.*"]
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
runs-on: macos-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
with:
|
|
19
|
+
fetch-depth: 0
|
|
20
|
+
- uses: astral-sh/setup-uv@v6
|
|
21
|
+
with:
|
|
22
|
+
python-version: "3.14"
|
|
23
|
+
- run: uv run --frozen ruff check .
|
|
24
|
+
- run: uv run --frozen pytest -q
|
|
25
|
+
|
|
26
|
+
build:
|
|
27
|
+
needs: test
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/checkout@v4
|
|
31
|
+
with:
|
|
32
|
+
fetch-depth: 0 # hatch-vcs reads the version from the tag
|
|
33
|
+
- uses: astral-sh/setup-uv@v6
|
|
34
|
+
- name: Check the tag matches the built version
|
|
35
|
+
run: |
|
|
36
|
+
uv build
|
|
37
|
+
ls dist/*-"${GITHUB_REF_NAME#v}"-py3-none-any.whl
|
|
38
|
+
- uses: actions/upload-artifact@v4
|
|
39
|
+
with:
|
|
40
|
+
name: dist
|
|
41
|
+
path: dist/
|
|
42
|
+
|
|
43
|
+
publish:
|
|
44
|
+
needs: build
|
|
45
|
+
runs-on: ubuntu-latest
|
|
46
|
+
environment:
|
|
47
|
+
name: pypi
|
|
48
|
+
url: https://pypi.org/project/loops-routines/
|
|
49
|
+
permissions:
|
|
50
|
+
id-token: write # Trusted Publishing
|
|
51
|
+
steps:
|
|
52
|
+
- uses: actions/download-artifact@v4
|
|
53
|
+
with:
|
|
54
|
+
name: dist
|
|
55
|
+
path: dist/
|
|
56
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
57
|
+
|
|
58
|
+
github-release:
|
|
59
|
+
needs: publish
|
|
60
|
+
runs-on: ubuntu-latest
|
|
61
|
+
permissions:
|
|
62
|
+
contents: write
|
|
63
|
+
steps:
|
|
64
|
+
- uses: actions/download-artifact@v4
|
|
65
|
+
with:
|
|
66
|
+
name: dist
|
|
67
|
+
path: dist/
|
|
68
|
+
- env:
|
|
69
|
+
GH_TOKEN: ${{ github.token }}
|
|
70
|
+
run: gh release create "$GITHUB_REF_NAME" dist/* --repo "$GITHUB_REPOSITORY" --title "$GITHUB_REF_NAME" --generate-notes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-11-08
|
|
10
|
+
|
|
11
|
+
The first release.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- The daemon: routines on a cron line or an interval, within the plan's 5-hour and 7-day usage limits,
|
|
16
|
+
with priorities, parallel slots, grace periods and time windows.
|
|
17
|
+
- REST API with live events, an MCP server for Claude Code sessions, a CLI and a web page.
|
|
18
|
+
- `run_routine_now` (MCP) and `loops-routines run --now`: start a routine outside the queue.
|
|
19
|
+
- Agent tools: `report_status`, `report_blocker`, `notify` (macOS banners) and `ask_user` (dialogs with
|
|
20
|
+
the agent's own buttons).
|
|
21
|
+
- `loops-routines install --launchd --mcp` and `loops-routines restart`.
|
|
22
|
+
|
|
23
|
+
[Unreleased]: https://github.com/Melevir/loops-routines/compare/v0.1.0...HEAD
|
|
24
|
+
[0.1.0]: https://github.com/Melevir/loops-routines/releases/tag/v0.1.0
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for looking. Issues with a clear "what I did / what I expected / what happened" are as welcome as code.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
Needs macOS, [uv](https://docs.astral.sh/uv/) and Python 3.12+.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uv sync
|
|
11
|
+
uv run pytest
|
|
12
|
+
uv run ruff check .
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The tests never touch your real setup: they run in temporary folders, start the real runner against a fake
|
|
16
|
+
`claude` script (`tests/conftest.py`), and replace notifications with a fake notifier. Keep it that way: a test
|
|
17
|
+
must not call `launchctl`, `osascript` or a real `claude`.
|
|
18
|
+
|
|
19
|
+
To try a change against a real `claude` without disturbing an installed daemon, run a separate instance on
|
|
20
|
+
another port:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
uv run loops-routines --root /tmp/ct install
|
|
24
|
+
# set [daemon] port in /tmp/ct/home/config.toml, then:
|
|
25
|
+
uv run loops-routines --root /tmp/ct daemon
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Changes
|
|
29
|
+
|
|
30
|
+
- One change per pull request, with a test that fails without it.
|
|
31
|
+
- `service.py` holds the behaviour; REST (`daemon/app.py`), MCP (`daemon/mcp.py`) and the CLI stay thin
|
|
32
|
+
over it. A new action goes into `Service` first, then into whichever transports need it.
|
|
33
|
+
- MCP tools are `async def`: the MCP SDK runs sync tools in a worker thread, where the daemon's event loop
|
|
34
|
+
isn't available.
|
|
35
|
+
- Add a line under "Unreleased" in `CHANGELOG.md` for anything a user would notice.
|
|
36
|
+
|
|
37
|
+
## Releases
|
|
38
|
+
|
|
39
|
+
Maintainers tag `vX.Y.Z` on `main`; CI builds and publishes to PyPI.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ilia (Melevir)
|
|
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.
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: loops-routines
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A daemon that runs Claude Code routines on a schedule and within the plan's usage limits, with an API, MCP and CLI
|
|
5
|
+
Project-URL: Homepage, https://github.com/Melevir/loops-routines
|
|
6
|
+
Project-URL: Issues, https://github.com/Melevir/loops-routines/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/Melevir/loops-routines/blob/main/CHANGELOG.md
|
|
8
|
+
Author-email: Ilia <melevir@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agents,claude-code,cron,launchd,macos,mcp,scheduler
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Software Development
|
|
21
|
+
Classifier: Topic :: System :: Monitoring
|
|
22
|
+
Requires-Python: >=3.12
|
|
23
|
+
Requires-Dist: httpx>=0.28
|
|
24
|
+
Requires-Dist: mcp>=2.3.0
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# loops-routines
|
|
28
|
+
|
|
29
|
+
[](https://github.com/Melevir/loops-routines/actions/workflows/ci.yml)
|
|
30
|
+
[](https://pypi.org/project/loops-routines/)
|
|
31
|
+
[](LICENSE)
|
|
32
|
+
|
|
33
|
+
> An independent project, not made or endorsed by Anthropic. "Claude" and "Claude Code" are Anthropic's
|
|
34
|
+
> trademarks; loops-routines only starts the `claude` command you already have.
|
|
35
|
+
|
|
36
|
+
A daemon that runs Claude Code **routines** (on a cron line or an interval) **within your plan's usage
|
|
37
|
+
limits**. One process serves everything: the scheduler, the REST API with live events, the MCP server
|
|
38
|
+
for your Claude sessions, and a small web page. Agents tell you things with **macOS notifications** and
|
|
39
|
+
ask you questions in **system dialogs with their own buttons**, waiting for your click.
|
|
40
|
+
|
|
41
|
+
Examples it is made for:
|
|
42
|
+
|
|
43
|
+
- a weekly summary of the changes in several repositories (`0 9 * * mon`);
|
|
44
|
+
- a cleanup every 2 hours, but only with tokens the 5-hour window would lose anyway (`profile = "spare"`);
|
|
45
|
+
- a log monitor every 30 minutes that stays quiet until something new breaks (`notify = "on_alert"`).
|
|
46
|
+
|
|
47
|
+

|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
Needs macOS, [uv](https://docs.astral.sh/uv/) and [Claude Code](https://claude.com/claude-code) (`claude`
|
|
52
|
+
on your `PATH`, signed in).
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uv tool install loops-routines # from PyPI: the `loops-routines` command in ~/.local/bin
|
|
56
|
+
loops-routines install --launchd --mcp # ~/.loops_routines, the daemon as a LaunchAgent, the MCP server in Claude Code
|
|
57
|
+
loops-routines status
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`--launchd` writes `~/Library/LaunchAgents/com.loops-routines.daemon.plist` and loads it: the daemon starts now
|
|
61
|
+
and at every login, and launchd restarts it if it stops. It runs as you (a LaunchAgent, not a system
|
|
62
|
+
LaunchDaemon), so agents use your Claude sign-in, keychain and tools. Run the install from a terminal
|
|
63
|
+
whose `PATH` finds `claude` and whatever your routines call: the LaunchAgent keeps that `PATH`.
|
|
64
|
+
|
|
65
|
+
Upgrade, then restart the daemon so it runs the new code:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
uv tool upgrade loops-routines
|
|
69
|
+
loops-routines restart
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Remove: `loops-routines uninstall` (the LaunchAgent; `~/.loops_routines` is kept), then `uv tool uninstall loops-routines`.
|
|
73
|
+
|
|
74
|
+
### Before you start
|
|
75
|
+
|
|
76
|
+
- **macOS only.** Notifications, dialogs and the LaunchAgent are macOS's; the scheduler itself is not, but
|
|
77
|
+
nothing else is supported yet.
|
|
78
|
+
- **Routines spend your plan.** Every run is a `claude -p` session on your own sign-in and counts against
|
|
79
|
+
your 5-hour and 7-day windows. Limit profiles (`spare`, `keep_free`) decide how much a routine may take;
|
|
80
|
+
start with `profile = "spare"` for anything that doesn't have to run.
|
|
81
|
+
- **Agents act as you.** They run with your files, keychain and `gh` login, limited by the allowed tools in
|
|
82
|
+
`config.toml` and the routine. Read [SECURITY.md](SECURITY.md) before switching on a routine that pushes,
|
|
83
|
+
posts or reads untrusted text.
|
|
84
|
+
- The example routines in `~/.loops_routines/routines/` are switched off and name placeholder repositories and
|
|
85
|
+
services: edit them first.
|
|
86
|
+
|
|
87
|
+
## Quick start (from a checkout)
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
uv sync
|
|
91
|
+
uv run loops-routines install # ~/.loops_routines: config.toml, README, three example routines (off)
|
|
92
|
+
uv run loops-routines daemon # or: loops-routines install --launchd (keeps it running, starts at login)
|
|
93
|
+
uv run loops-routines status
|
|
94
|
+
uv run loops-routines open # the web page in the browser
|
|
95
|
+
uv run loops-routines install --mcp # let your Claude Code sessions see and steer the routines
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
A routine is a file `~/.loops_routines/routines/<name>.md`: a TOML header between `+++` lines, then the
|
|
99
|
+
prompt. See `loops_routines/defaults/README.md` (copied to `~/.loops_routines/README.md`) for every key, and
|
|
100
|
+
`loops-routines check <file>` to validate one and see its next three runs.
|
|
101
|
+
|
|
102
|
+
## Architecture
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
launchd (KeepAlive)
|
|
106
|
+
└─ loops-routines daemon ───────────────────────────────────────────────────────────┐
|
|
107
|
+
Service.tick (every 30 s): reload files → wind down runs past keep_free → │
|
|
108
|
+
evaluate each routine → expire / wait / launch by priority → probe usage │
|
|
109
|
+
Run × max_parallel → `claude -p` stream-json, own process group, own folder │
|
|
110
|
+
Store (SQLite, the daemon's only) deliver: reports/*.md │
|
|
111
|
+
REST /api/* + SSE /api/events MCP /mcp (user) MCP /agent/mcp (runs) │
|
|
112
|
+
/ui/ — a web page on the same API │
|
|
113
|
+
────────────────────────────────────────────────────────────────────────────────────┘
|
|
114
|
+
▲ REST + SSE ▲ MCP ▲ REST
|
|
115
|
+
browser Claude Code CLI
|
|
116
|
+
|
|
117
|
+
run ── /agent/mcp notify ──▶ daemon ──▶ banner (Notification Center)
|
|
118
|
+
run ── /agent/mcp ask_user ─▶ daemon ──▶ dialog with buttons ──┐ first answer wins:
|
|
119
|
+
◀── answer ── web page / CLI / MCP ┘ the dialog, or any of these
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
| Module | What it does |
|
|
123
|
+
|---|---|
|
|
124
|
+
| `core/triggers.py` | cron (5 fields, names, steps, `@daily`) and `every 30m`; `next_after(moment)` |
|
|
125
|
+
| `core/windows.py` | time windows `mon-fri 09:00-19:00` (from Loops) |
|
|
126
|
+
| `core/routines.py` | routine files → `Routine`; broken files are kept with their error |
|
|
127
|
+
| `core/evaluate.py` | the pure decision: idle / wait (why) / launch / expire for one routine |
|
|
128
|
+
| `core/usage.py`, `core/probe.py` | 5-hour and 7-day snapshots, the limit gates, the one-request probe (from Loops) |
|
|
129
|
+
| `core/integrations.py` | plugins, MCP servers, skills a routine names → `claude` flags (from Loops) |
|
|
130
|
+
| `core/runner.py` | one run: clones, per-run config files, stream-json in/out, stop, wrap-up, result file |
|
|
131
|
+
| `core/store.py` | SQLite: routine state, runs (and skipped occurrences), messages from agents |
|
|
132
|
+
| `service.py` | the scheduler tick and every query and action; REST, MCP and CLI are thin over it |
|
|
133
|
+
| `daemon/app.py`, `daemon/mcp.py`, `daemon/auth.py` | HTTP transports and the token check |
|
|
134
|
+
| `core/notify.py` | macOS banners and button dialogs through osascript; the prompt rules per `notify` |
|
|
135
|
+
| `ui/web/index.html` | the web page (`loops-routines open`) |
|
|
136
|
+
|
|
137
|
+
### Scheduling rules
|
|
138
|
+
|
|
139
|
+
- Occurrences come from the trigger; only the **latest** due one counts, so a Mac that slept for a day
|
|
140
|
+
runs a routine once, not 48 times.
|
|
141
|
+
- An occurrence may wait for its window and limits until its deadline: `grace` after it, or (without
|
|
142
|
+
`grace`) until the next occurrence. Past it, it is recorded as **skipped** with the reason it waited for.
|
|
143
|
+
- Limits: `min_free` and `before_reset` decide the start; `keep_free` / `keep_free_weekly` also wind a
|
|
144
|
+
running run down (it gets `wind_down_minutes` to write its result, then it is stopped).
|
|
145
|
+
- When numbers are older than `max_snapshot_age_minutes`, the daemon spends one haiku request on a
|
|
146
|
+
probe (at most every `probe_cooldown_minutes`). Every run also refreshes them from its own events.
|
|
147
|
+
- Several due routines: higher `priority` first, up to `max_parallel` at once; a routine never overlaps
|
|
148
|
+
itself. "Run now" does not use up an occurrence.
|
|
149
|
+
- A routine switched off (in its file or the UI) owes nothing for the time it was off.
|
|
150
|
+
|
|
151
|
+
### Results
|
|
152
|
+
|
|
153
|
+
The agent ends by writing `{"status": "ok | alert | nothing", "summary", "report"}` to its result file.
|
|
154
|
+
The daemon stores it and writes `reports/<routine>/<run>.md`. Without a result file, the agent's last
|
|
155
|
+
words become the summary with status `unknown`. While it works, the agent may post `report_status`
|
|
156
|
+
lines through `/agent/mcp`, and you can message it (`loops-routines say RUN TEXT`).
|
|
157
|
+
|
|
158
|
+
### Notifications and questions
|
|
159
|
+
|
|
160
|
+
Agents use two tools of `/agent/mcp`, as their routine's `notify` allows (`always`: the outcome when it
|
|
161
|
+
finishes; `on_alert`: only an alert; `never`: neither tool):
|
|
162
|
+
|
|
163
|
+
- `notify(text, kind)` — a Notification Center banner titled with the routine; `kind` is info, success,
|
|
164
|
+
warning or error (error and warning play `[notify] error_sound`).
|
|
165
|
+
- `ask_user(question, buttons, timeout_minutes)` — a system dialog with 1–3 buttons of the agent's wording
|
|
166
|
+
(the recommended one is the default, on the right) and waits. The first answer wins: a click in the
|
|
167
|
+
dialog, the web page, `loops-routines answer`, or `answer_question` over MCP; the dialog then closes. No
|
|
168
|
+
answer in time, or the run ends: the agent gets `timed_out` / `cancelled` and takes the safe path.
|
|
169
|
+
|
|
170
|
+
The daemon notifies by itself only what no agent can: a run that failed or timed out, an alert the agent
|
|
171
|
+
did not send, and a `report_blocker`. Everything is kept with the run.
|
|
172
|
+
|
|
173
|
+
Why a dialog and not buttons on the banner: macOS gives notification actions only to a signed app with its
|
|
174
|
+
own notification permission; osascript banners can't have buttons. The first banner may make macOS ask
|
|
175
|
+
whether "Script Editor" may notify — allow it in System Settings → Notifications. `loops-routines
|
|
176
|
+
notify-test` (and `--ask`) checks both.
|
|
177
|
+
|
|
178
|
+
### Security
|
|
179
|
+
|
|
180
|
+
The daemon listens on 127.0.0.1 only and checks the Host header. `/api` and `/mcp` need the token in
|
|
181
|
+
`~/Library/Application Support/loops_routines/token` (0600); `/agent/mcp` accepts only a live run's own token,
|
|
182
|
+
so an agent can't post as another run. The UI page gets the token from its URL fragment (never sent to
|
|
183
|
+
the server) and keeps it for the tab only.
|
|
184
|
+
|
|
185
|
+
## API
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
GET /health GET /api/summary
|
|
189
|
+
GET /api/routines GET /api/routines/{name}
|
|
190
|
+
POST /api/routines/{name}/run {force, out_of_queue}
|
|
191
|
+
POST /api/routines/{name}/enabled {enabled: true|false|null}
|
|
192
|
+
GET /api/runs?routine=&limit=&before=&skipped=0
|
|
193
|
+
GET /api/runs/{id} GET /api/runs/{id}/log?lines=
|
|
194
|
+
POST /api/runs/{id}/stop POST /api/runs/{id}/message {text}
|
|
195
|
+
POST /api/runs/seen {ids}
|
|
196
|
+
GET /api/usage POST /api/usage/probe
|
|
197
|
+
GET /api/integrations POST /api/integrations/scan
|
|
198
|
+
GET /api/questions POST /api/questions/{id}/answer {answer}
|
|
199
|
+
POST /api/notify/test {kind, ask}
|
|
200
|
+
POST /api/reload GET /api/events (SSE)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
MCP `/mcp`: `list_routines`, `routine_status`, `run_routine`, `run_routine_now` (outside the queue: on top
|
|
204
|
+
of `max_parallel`, whatever its limits and window say), `set_routine_enabled`, `list_runs`,
|
|
205
|
+
`get_run`, `run_log`, `stop_run`, `message_run`, `usage`, `pending_questions`, `answer_question`.
|
|
206
|
+
MCP `/agent/mcp` (runs only): `report_status`, `report_blocker`, `notify`, `ask_user`.
|
|
207
|
+
|
|
208
|
+
## Development
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
uv run pytest
|
|
212
|
+
uv run loops-routines --root /tmp/ct daemon # a separate instance: /tmp/ct/home, state, logs
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Tests run the real runner against a fake `claude` script (`tests/conftest.py`). See
|
|
216
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) for the rest, and [CHANGELOG.md](CHANGELOG.md) for what changed.
|
|
217
|
+
|
|
218
|
+
## License
|
|
219
|
+
|
|
220
|
+
[MIT](LICENSE).
|