watcheroni 0.0.0-stage → 0.2.1
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.
- package/LICENSE +21 -0
- package/README.md +225 -2
- package/dist/watcheroni.mjs +1838 -0
- package/package.json +46 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 paul cannon
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,3 +1,226 @@
|
|
|
1
|
-
#
|
|
1
|
+
# watcheroni
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Wait for, stream, or act on what happens to your GitHub pull requests:
|
|
4
|
+
CI finishing or failing, reviews, comments, pushes, merges. It's built
|
|
5
|
+
for scripts and coding agents that open a PR and then need to know when
|
|
6
|
+
there's something to do about it, without polling GitHub in a loop.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
$ npx watcheroni wait
|
|
10
|
+
octo/widgets#123: 2 new events
|
|
11
|
+
- check.failed: "test (ubuntu-latest)" (failure) on 4f9c2e1
|
|
12
|
+
gh run view 18342210911 -R octo/widgets --log-failed
|
|
13
|
+
- review.submitted by mona: changes_requested
|
|
14
|
+
gh api repos/octo/widgets/pulls/123/reviews/3374120415 --jq '.state, .body'
|
|
15
|
+
|
|
16
|
+
octo/widgets#123 is still open. Run `watcheroni wait` again to keep watching.
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Requires Node.js 22 or later.
|
|
20
|
+
|
|
21
|
+
## Getting started
|
|
22
|
+
|
|
23
|
+
1. **Install the GitHub App** on the repositories you want to watch:
|
|
24
|
+
<https://github.com/apps/watcheroni/installations/new>. It asks for
|
|
25
|
+
read-only access to pull requests, checks and their metadata, and
|
|
26
|
+
sends their webhooks to the watcheroni server at
|
|
27
|
+
<https://watcheroni.dev>.
|
|
28
|
+
2. **Sign in** from your machine. This runs GitHub's device flow (a code
|
|
29
|
+
to enter at github.com) and saves an API key for the server:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
npx watcheroni login
|
|
33
|
+
```
|
|
34
|
+
3. **Wait** for something to happen to the PR for your current branch
|
|
35
|
+
(or name one: `npx watcheroni wait octo/widgets#123`):
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
npx watcheroni wait
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
It exits once there's news, printing each event with the command that
|
|
42
|
+
fetches its details, so run it again to keep watching.
|
|
43
|
+
|
|
44
|
+
To skip npx's registry check on every run, install it:
|
|
45
|
+
`npm install -g watcheroni`.
|
|
46
|
+
|
|
47
|
+
**No App, no server?** Everything also works without signing in: the CLI
|
|
48
|
+
then polls GitHub itself every 2 minutes with your `gh` login (or
|
|
49
|
+
`GH_TOKEN` / `GITHUB_TOKEN`), and that token never leaves your machine.
|
|
50
|
+
See "Local mode" below.
|
|
51
|
+
|
|
52
|
+
## Claude Code
|
|
53
|
+
|
|
54
|
+
The watcheroni plugin for Claude Code has a session keep watching the PRs
|
|
55
|
+
it opens or pushes to, in the background, and deal with failing checks
|
|
56
|
+
and review feedback as they arrive:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
claude plugin marketplace add thepaul/watcheroni
|
|
60
|
+
claude plugin install watcheroni@watcheroni
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
It runs this CLI through npx unless it's installed. See
|
|
64
|
+
[the plugin's README](https://github.com/thepaul/watcheroni/tree/main/plugin)
|
|
65
|
+
for what it does and doesn't do.
|
|
66
|
+
|
|
67
|
+
## Commands
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
watcheroni wait [target...] [--timeout 1h] [--settle 20s] [--format text|json]
|
|
71
|
+
watcheroni stream [target...]
|
|
72
|
+
watcheroni exec [target...] [--on kinds] [--batch] -- command [args...]
|
|
73
|
+
watcheroni watch [target...]
|
|
74
|
+
watcheroni unwatch <target...>
|
|
75
|
+
watcheroni ls
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Targets are `owner/repo#123` or PR URLs. With none, it's the open PR for
|
|
79
|
+
the current git branch, found through the checkout's GitHub remotes.
|
|
80
|
+
`owner/repo` watches a whole repository, in server mode only (see
|
|
81
|
+
below).
|
|
82
|
+
|
|
83
|
+
- **`wait`** polls (every 2 minutes; `--interval`) until something
|
|
84
|
+
happens, waits `--settle` for related events, prints them with the
|
|
85
|
+
command that fetches each one's content, and exits. Exit codes: `0`
|
|
86
|
+
events, `2` timeout, `3` every watched PR has ended, `4` no GitHub
|
|
87
|
+
credentials, `1` other errors.
|
|
88
|
+
- **`stream`** prints each event as a JSON line until killed.
|
|
89
|
+
- **`exec`** runs a command per event, with the event as JSON on stdin and
|
|
90
|
+
`WATCHERONI_KIND`, `_REPO`, `_PR`, `_URL` and `_EVENT_ID` in its
|
|
91
|
+
environment. An event is acknowledged only when the command exits 0.
|
|
92
|
+
`--batch` runs it once per settled batch, with a JSON array on stdin.
|
|
93
|
+
|
|
94
|
+
## Recipes
|
|
95
|
+
|
|
96
|
+
Desktop and phone notifications, Slack, only CI failures, and running
|
|
97
|
+
`exec` as a background service:
|
|
98
|
+
[docs/recipes.md](https://github.com/thepaul/watcheroni/blob/main/docs/recipes.md).
|
|
99
|
+
|
|
100
|
+
## Server mode
|
|
101
|
+
|
|
102
|
+
Sign in, which runs GitHub's device flow and saves an API key in
|
|
103
|
+
`~/.config/watcheroni/credentials.json` (mode 0600). It opens the browser
|
|
104
|
+
at GitHub's code page when you're at this machine's own terminal; over
|
|
105
|
+
SSH, or with `WATCHERONI_NO_BROWSER` set, it just prints the address:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
watcheroni login # https://watcheroni.dev
|
|
109
|
+
WATCHERONI_SERVER=https://... watcheroni login
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Once you're signed in to the default server, the CLI uses it. To use
|
|
113
|
+
another, name it in `WATCHERONI_SERVER` or `~/.config/watcheroni/config.json`
|
|
114
|
+
(`{"server": "https://..."}`). For CI, `WATCHERONI_API_KEY` stands in for
|
|
115
|
+
the credentials file. The environment wins, and `WATCHERONI_SERVER=`
|
|
116
|
+
(empty) forces local mode. `WATCHERONI_CONFIG_DIR` moves both files, and
|
|
117
|
+
`WATCHERONI_CREDENTIALS` moves just the credentials. `watcheroni logout`
|
|
118
|
+
revokes the key and forgets it.
|
|
119
|
+
|
|
120
|
+
To run your own server, with your own GitHub App, on Cloudflare's free
|
|
121
|
+
plan: `watcheroni selfhost` in a clone of the repository (see
|
|
122
|
+
[the server's README](https://github.com/thepaul/watcheroni/tree/main/packages/server#run-your-own)). It also sets
|
|
123
|
+
`server` in `config.json`.
|
|
124
|
+
|
|
125
|
+
`wait`, `stream` and `exec` then:
|
|
126
|
+
|
|
127
|
+
1. Create the inbox on the server (`PUT /v1/inboxes/{name}`), which also
|
|
128
|
+
checks the key. A rejected key exits 4.
|
|
129
|
+
2. Read it over a WebSocket (subprotocol `watcheroni.v1`: `hello` with the
|
|
130
|
+
last seq received, `ready`, `events`, `ack`, and a `ping` every 30 s).
|
|
131
|
+
If no socket can be opened but HTTP works, they long-poll
|
|
132
|
+
`GET /v1/inboxes/{name}/events?after=&wait=300` instead. Lost
|
|
133
|
+
connections are retried with backoff (1 s doubling to 60 s, with
|
|
134
|
+
jitter); close code 4001 exits 4, 4004 exits 1, and 4029 switches to
|
|
135
|
+
long-polling. Events seen before are dropped by `id`.
|
|
136
|
+
3. Acknowledge on the server as local mode does: `wait` once it has
|
|
137
|
+
printed, `stream` after each line, `exec` only after the command exits
|
|
138
|
+
0. A new process starts from the server's acknowledged position, so a
|
|
139
|
+
killed `wait` loses nothing.
|
|
140
|
+
|
|
141
|
+
Targets: each PR named on the command line (or the current branch's PR)
|
|
142
|
+
is subscribed on the server (`POST /v1/inboxes/{name}/subscriptions`),
|
|
143
|
+
which then feeds the inbox from GitHub's webhooks, and this machine stops
|
|
144
|
+
polling it. The request carries no GitHub token unless the server answers
|
|
145
|
+
`github_token_required`, meaning the repo is private; then it's sent again
|
|
146
|
+
with the GitHub token `watcheroni login` saved, renewed first through the
|
|
147
|
+
server's refresh proxy if it has expired (and saved again). The server
|
|
148
|
+
uses the token for one check and forgets it.
|
|
149
|
+
|
|
150
|
+
A PR the server won't watch stays polled from this machine, as in local
|
|
151
|
+
mode, with its events merged with the server inbox's: when the watcheroni
|
|
152
|
+
App isn't installed on the repo (stderr says where to install it), when
|
|
153
|
+
there's no usable GitHub token for a private repo (`WATCHERONI_API_KEY`
|
|
154
|
+
alone has none), or when the server refuses for another reason. Such a PR
|
|
155
|
+
is offered to the server again at most hourly. When the server ends a
|
|
156
|
+
subscription, a `watch.ended` or `watch.expired` stops the watch here too;
|
|
157
|
+
after a `watch.revoked` (the App uninstalled or the repo removed from it)
|
|
158
|
+
the PR goes back to being polled from this machine.
|
|
159
|
+
|
|
160
|
+
A whole repository (`owner/repo`) is watched by the server or not at all,
|
|
161
|
+
since local mode would have to poll every PR in it. It needs `--kinds`,
|
|
162
|
+
which the subscription carries (and which, as always, also filters what
|
|
163
|
+
`wait` prints), and may narrow to PRs against one branch (`--base main`)
|
|
164
|
+
or opened by one user (`--author alice`):
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
watcheroni wait octo/widgets --kinds pr.opened --base main
|
|
168
|
+
watcheroni wait octo/widgets --kinds check.failed --author alice
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
If the server won't take it (the App isn't installed, say) or can't be
|
|
172
|
+
reached, the command exits 1. Once subscribed it's remembered in the
|
|
173
|
+
inbox (`ls` lists it, `unwatch octo/widgets` ends every whole-repository
|
|
174
|
+
watch on it, on the server too), and while it lasts `wait` never exits 3:
|
|
175
|
+
it exits 0 on events and 2 on timeout. A `watch.*` event from the server
|
|
176
|
+
(`watch.expired` after a week unread, `watch.revoked`) ends it; name it
|
|
177
|
+
again to start over.
|
|
178
|
+
|
|
179
|
+
Because an inbox may be fed by subscriptions made elsewhere, which can't
|
|
180
|
+
be listed yet, `wait` with no PR watched here reads the inbox until
|
|
181
|
+
`--timeout` instead of exiting 3. It exits 3 when the PRs it was watching
|
|
182
|
+
have all ended.
|
|
183
|
+
|
|
184
|
+
**Fallback.** If the server can't be reached, the CLI says so on stderr
|
|
185
|
+
and polls the named PRs from this machine, subscribed or not, while it
|
|
186
|
+
keeps trying to reconnect. A `wait` with nothing to poll locally exits 1 instead of
|
|
187
|
+
waiting on a server that's down; `stream` and `exec` keep retrying.
|
|
188
|
+
|
|
189
|
+
## Local mode
|
|
190
|
+
|
|
191
|
+
Each poll is one GraphQL query per PR (cost: 1 point of GitHub's 5,000 an
|
|
192
|
+
hour). The CLI compares it with the last snapshot and turns the
|
|
193
|
+
difference into events (`src/diff.ts`). The first poll of a PR is the
|
|
194
|
+
starting point: what was already true then isn't reported, as with a
|
|
195
|
+
server subscription. A PR that has already ended is reported as
|
|
196
|
+
`watch.ended` straight away.
|
|
197
|
+
|
|
198
|
+
State lives in `~/.local/state/watcheroni/inboxes/<inbox>.json`
|
|
199
|
+
(`$WATCHERONI_STATE_DIR` to move it). Events are saved before they're
|
|
200
|
+
printed and removed only once acknowledged, so a `wait` that gets killed
|
|
201
|
+
loses nothing: the next one prints the same events. One process at a time
|
|
202
|
+
uses an inbox. The inbox is `--inbox`, or `$WATCHERONI_INBOX`, or
|
|
203
|
+
`cc-$CLAUDE_CODE_SESSION_ID` inside Claude Code, or `default`.
|
|
204
|
+
|
|
205
|
+
What local mode can't see that the server will: events between polls
|
|
206
|
+
that undo each other (a thread resolved and then unresolved within one
|
|
207
|
+
interval), more than the last 100 reviews, threads or comments, and
|
|
208
|
+
repository-wide targets.
|
|
209
|
+
|
|
210
|
+
## More
|
|
211
|
+
|
|
212
|
+
How it's built, and the API other clients can use:
|
|
213
|
+
[DESIGN.md](https://github.com/thepaul/watcheroni/blob/main/DESIGN.md),
|
|
214
|
+
[PROTOCOL.md](https://github.com/thepaul/watcheroni/blob/main/PROTOCOL.md)
|
|
215
|
+
(and its types, [`@watcheroni/protocol`](https://www.npmjs.com/package/@watcheroni/protocol)).
|
|
216
|
+
Issues: <https://github.com/thepaul/watcheroni/issues>. MIT licensed.
|
|
217
|
+
|
|
218
|
+
## Develop
|
|
219
|
+
|
|
220
|
+
From a clone of the repository, in `packages/cli`:
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
npm test # Vitest, no network; server mode runs against a fake server (test/fake-server.ts)
|
|
224
|
+
npm run dev -- wait octo/widgets#123
|
|
225
|
+
npm run build # dist/watcheroni.mjs, one file, no dependencies
|
|
226
|
+
```
|