unsnooze 1.14.4 → 1.16.0
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/CHANGELOG.md +127 -0
- package/README.md +122 -17
- package/bin/unsnooze.js +5 -1
- package/package.json +12 -4
- package/src/agents/claude.js +43 -4
- package/src/cli.js +12 -1
- package/src/config.js +21 -0
- package/src/design.js +209 -0
- package/src/doctor.js +43 -6
- package/src/fleet.js +5 -1
- package/src/hook.js +23 -8
- package/src/install.js +118 -7
- package/src/launcher.js +85 -14
- package/src/lease.js +15 -1
- package/src/monitor.js +62 -12
- package/src/multiplexer.js +34 -4
- package/src/multiplexers/cmux.js +153 -0
- package/src/multiplexers/headless.js +164 -0
- package/src/multiplexers/herdr.js +613 -0
- package/src/multiplexers/session-name.js +30 -0
- package/src/multiplexers/shell-quote.js +96 -0
- package/src/multiplexers/tmux.js +1 -0
- package/src/multiplexers/zellij.js +1 -0
- package/src/mux-sessions.js +68 -0
- package/src/notify.js +2 -1
- package/src/patterns.js +72 -2
- package/src/powershell.js +45 -0
- package/src/prompt-queue.js +12 -7
- package/src/reap.js +47 -12
- package/src/report.js +16 -3
- package/src/resumer.js +95 -14
- package/src/settings.js +81 -3
- package/src/state.js +24 -0
- package/src/wizard.js +14 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,132 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.16.0 — 2026-08-16
|
|
4
|
+
|
|
5
|
+
- **Claude Design, and native Windows along the way.** ([#13](https://github.com/saaranshM/unsnooze/issues/13))
|
|
6
|
+
Claude Design shares your 5-hour and weekly limits with everything else, so a
|
|
7
|
+
long design run stops exactly like any other session — the obstacle was never
|
|
8
|
+
the stop, it was that Design's canvas is a web app with no pane to watch, and
|
|
9
|
+
that the reporter was on Windows, where unsnooze watched nothing at all.
|
|
10
|
+
`unsnooze design setup` wires up the official `claude-design` MCP server so
|
|
11
|
+
the work runs inside Claude Code, where it is watched like anything else;
|
|
12
|
+
`unsnooze design` reports whether it is registered and signed in, keeping
|
|
13
|
+
those two apart because an expired `/design-login` is *not* a usage limit and
|
|
14
|
+
waiting will never clear it. unsnooze does not automate the web canvas and
|
|
15
|
+
will not: Anthropic's Consumer Terms bar automated access to claude.ai, and
|
|
16
|
+
accounts have been terminated over it.
|
|
17
|
+
|
|
18
|
+
- **Watching without a multiplexer (`headless`).** A pane was only one of three
|
|
19
|
+
detection channels — the StopFailure hook and the transcript watcher never
|
|
20
|
+
needed one. What was missing was somewhere to put a revived agent, so there
|
|
21
|
+
is now a backend where a "pane" is a pid and a revive is a detached process
|
|
22
|
+
logging to `~/.unsnooze/headless/`. That makes native Windows, bare servers
|
|
23
|
+
and CI work. It is picked only when no multiplexer is installed, never ahead
|
|
24
|
+
of one, and it is honestly weaker: no limit-menu answering, no busy
|
|
25
|
+
detection, no live pane to attach to.
|
|
26
|
+
|
|
27
|
+
- **Native Windows is supported.** Previously unsnooze printed *"native Windows
|
|
28
|
+
is not supported; run inside WSL"* and ran your CLI unwatched. Two things
|
|
29
|
+
were broken beneath that: the StopFailure hook command was POSIX
|
|
30
|
+
(`test -f …`), which cmd.exe has no `test` for, so the hook failed on every
|
|
31
|
+
turn; and the shell wrapper only ever reached `~/.zshrc` and `~/.bashrc`,
|
|
32
|
+
files a PowerShell user does not have — so nothing routed through unsnooze in
|
|
33
|
+
the first place. Both are fixed, the daemon autostarts from a logon-triggered
|
|
34
|
+
Scheduled Task, and `unsnooze doctor` stops reporting wrappers as missing
|
|
35
|
+
when they are installed. WSL remains the richer option.
|
|
36
|
+
|
|
37
|
+
- **A server throttle is no longer mistaken for your usage limit.** Claude Code
|
|
38
|
+
says *"API Error: Server is temporarily limiting requests (not your usage
|
|
39
|
+
limit)"* — and `usage limit` matched the parenthetical. Since the TUI never
|
|
40
|
+
clears old banners from scrollback, any stale *"resets 3pm"* nearby was enough
|
|
41
|
+
to turn a throttle that clears in seconds into an hours-long scheduled wait.
|
|
42
|
+
It is now read as what it is and handled on the transient overload ladder.
|
|
43
|
+
|
|
44
|
+
- **Non-resetting Claude stops are notified, not scheduled.** The claude adapter
|
|
45
|
+
gained the `terminalPatterns` every other adapter already had. An expired
|
|
46
|
+
Claude Design credential and an exhausted credit balance are stops no amount
|
|
47
|
+
of waiting clears, so unsnooze says so once instead of booking a wake and
|
|
48
|
+
burning the attempt cap. This matters most for unattended runs: a revived
|
|
49
|
+
headless session has no interactive terminal, so `/design-login` cannot even
|
|
50
|
+
be offered there. All patterns are anchored to the CLI's real error phrasings
|
|
51
|
+
rather than to the bare command name, so an agent explaining `/design-login`
|
|
52
|
+
is not a stop.
|
|
53
|
+
|
|
54
|
+
- **Claude's own auto-continue is recognised, not raced.** Claude Code shipped
|
|
55
|
+
in-process auto-continue on 2026-08-14 (desktop; the CLI carries it behind a
|
|
56
|
+
gate that currently defaults off). unsnooze was already guarded — every resume
|
|
57
|
+
path checks whether the session moved on its own, and the 60s reset margin
|
|
58
|
+
means Claude gets there first — but a session that woke itself was recorded
|
|
59
|
+
identically to one unsnooze woke, so the deliberate stand-aside was invisible.
|
|
60
|
+
Records now carry who resumed them and `unsnooze status` says *"resumed itself
|
|
61
|
+
(unsnooze stood aside)"*. Note the native feature covers only the 5-hour
|
|
62
|
+
limit: weekly stops, closed apps, headless boxes and every non-Claude CLI
|
|
63
|
+
remain unsnooze's job.
|
|
64
|
+
|
|
65
|
+
- **`launchExtraArgs.<agent>`.** The launch-side twin of `resumeExtraArgs`, for
|
|
66
|
+
flags that must hold for a whole session rather than just a revival —
|
|
67
|
+
`unsnooze config set launchExtraArgs.claude "--autocompact 400000"` keeps a
|
|
68
|
+
context-heavy run compacting instead of stalling. Revivals inherit them, so
|
|
69
|
+
the flag survives the wake.
|
|
70
|
+
|
|
71
|
+
## 1.15.0 — 2026-08-15
|
|
72
|
+
|
|
73
|
+
- **Two new multiplexer backends: herdr and cmux.** unsnooze now watches and
|
|
74
|
+
revives sessions in [herdr](https://herdr.dev) and [cmux](https://cmux.dev)
|
|
75
|
+
alongside tmux and Zellij, chosen automatically from the multiplexer you are
|
|
76
|
+
already inside or explicitly with `unsnooze config set multiplexer herdr`.
|
|
77
|
+
Both are terminals built for running coding agents, and both start a command
|
|
78
|
+
by *typing* it into a pane rather than exec'ing it, which is a materially
|
|
79
|
+
different contract from tmux and Zellij — a revival's arguments are now
|
|
80
|
+
shell-quoted for them, and an argument that cannot be typed at all (a
|
|
81
|
+
multi-line resume message, say) travels through the pane's environment
|
|
82
|
+
instead of being mangled or refused. herdr needs 0.8.0 or newer; Homebrew
|
|
83
|
+
still ships 0.7.3, so install the release binary. Original herdr backend by
|
|
84
|
+
Wave Consulting (@walt-verweij) with follow-up work by @gaoflow; cmux backend
|
|
85
|
+
by @echarrod.
|
|
86
|
+
|
|
87
|
+
- **Per-model limits are no longer invisible.** Claude Code can stop with
|
|
88
|
+
*"You've reached your Fable 5 limit. Run /usage-credits to continue or switch
|
|
89
|
+
models with /model"* — a banner with no reset time, which the previous
|
|
90
|
+
detection (a limit phrase paired with a time) could never match, and which
|
|
91
|
+
fires no StopFailure hook. The session simply stopped, untracked and
|
|
92
|
+
unannounced. It is now detected and tracked. Because only a human clears it,
|
|
93
|
+
unsnooze never types at such a pane: it probes until the banner goes, and at
|
|
94
|
+
the ceiling records a visible failure naming both remedies rather than
|
|
95
|
+
pretending a wake is coming. Detection is deliberately conservative — an
|
|
96
|
+
agent *quoting* the banner in its own output is not a stop. (@gaoflow)
|
|
97
|
+
|
|
98
|
+
- **Four reset-scheduling bugs found in production.** A banner left on screen
|
|
99
|
+
after its reset passed was re-parsed every few seconds as "now + margin", so
|
|
100
|
+
the wake slid forward forever and the resumer never fired. An undated
|
|
101
|
+
"resets 10:30pm" re-read after midnight resolved to *tomorrow*, 22 hours out.
|
|
102
|
+
Accumulated pane-snapshot records with no session id each revived their own
|
|
103
|
+
copy of the same conversation — 23 records produced roughly 8 clones in one
|
|
104
|
+
repo at a weekly reset; at most one now revives per agent and project. And a
|
|
105
|
+
stop arriving after an old record was abandoned is no longer held back by it.
|
|
106
|
+
(@gaoflow)
|
|
107
|
+
|
|
108
|
+
- **New setting: `resumeExtraArgs.<agent>`.** A revival spawns the agent binary
|
|
109
|
+
directly, so flags that normally come from a shell alias or wrapper do not
|
|
110
|
+
apply — a user who always runs `claude --dangerously-skip-permissions` got a
|
|
111
|
+
permission-prompting revival, which is about as useful as none. Set them per
|
|
112
|
+
agent and they are appended to launches unsnooze performs itself. Quoting is
|
|
113
|
+
respected (`--append-system-prompt "stay in this repo"` is two arguments, not
|
|
114
|
+
five), and `config.json` accepts an array if you would rather not think about
|
|
115
|
+
quoting at all.
|
|
116
|
+
|
|
117
|
+
- **A failed launch no longer leaves a monitor scraping forever.** The monitor
|
|
118
|
+
starts before the agent does, and when the agent failed to start at all there
|
|
119
|
+
was no lease to notice its absence — so the monitor watched an ordinary shell
|
|
120
|
+
prompt indefinitely, one more process per failed launch. It now writes the
|
|
121
|
+
lease unconditionally (it was previously skipped wherever process start times
|
|
122
|
+
are unavailable, including Windows) and gives up if none appears. Reported by
|
|
123
|
+
@echarrod.
|
|
124
|
+
|
|
125
|
+
- **Node 20.12 is now the minimum.** `engines` promised `>=20` while a
|
|
126
|
+
dependency needed `styleText` from `node:util`, which arrived in 20.12 — so
|
|
127
|
+
installs on 20.0–20.11 succeeded and then crashed on startup. The floor is
|
|
128
|
+
honest now, and CI tests that exact version so it cannot drift again.
|
|
129
|
+
|
|
3
130
|
## 1.14.4 — 2026-08-08
|
|
4
131
|
|
|
5
132
|
- **Windows: the daemon PATH check split on the wrong separator.** The check
|
package/README.md
CHANGED
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
**Claude Code · Codex CLI · Grok · Qwen · Kimi · OpenCode · Antigravity** — when they hit the 5-hour or weekly usage limit
|
|
17
17
|
("You've hit your usage limit"), your session just… stops.<br/>
|
|
18
18
|
unsnooze auto-resumes them: it tracks **every** limit-stopped session across all
|
|
19
|
-
your projects and **wakes each one up
|
|
19
|
+
your projects and **wakes each one up the moment the usage limit resets** — in
|
|
20
|
+
tmux, Zellij, herdr or cmux, and on **macOS, Linux and Windows**, with or
|
|
21
|
+
without a terminal multiplexer.
|
|
20
22
|
|
|
21
23
|
```sh
|
|
22
24
|
npm install -g unsnooze && unsnooze setup
|
|
@@ -162,9 +164,48 @@ explicit setup is:
|
|
|
162
164
|
headroom install apply --scope provider --providers manual --target claude --target codex
|
|
163
165
|
```
|
|
164
166
|
|
|
167
|
+
## Claude Design
|
|
168
|
+
|
|
169
|
+
Claude Design shares your usual 5-hour and weekly limits with chat, Cowork and
|
|
170
|
+
Claude Code — so a long design run stops the same way everything else does, and
|
|
171
|
+
unsnooze resumes it the same way. What it needs first is a terminal to run in.
|
|
172
|
+
|
|
173
|
+
Claude Design has three surfaces. The canvas at `claude.ai/design` and the
|
|
174
|
+
Claude Desktop sidebar are web apps. The third is an official MCP server that
|
|
175
|
+
Claude Code drives from your terminal, and that is the one unsnooze supports:
|
|
176
|
+
|
|
177
|
+
```sh
|
|
178
|
+
unsnooze design setup # registers the claude-design MCP server
|
|
179
|
+
# then, inside Claude Code:
|
|
180
|
+
/design-login
|
|
181
|
+
unsnooze design # confirms it is registered and signed in
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
For long design runs, give the session room to compact rather than stall:
|
|
185
|
+
|
|
186
|
+
```sh
|
|
187
|
+
unsnooze config set launchExtraArgs.claude "--autocompact 400000"
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**unsnooze does not automate the web canvas, and will not.** Anthropic's
|
|
191
|
+
Consumer Terms bar accessing Claude "through automated or non-human means,
|
|
192
|
+
whether through a bot, script, or otherwise", and accounts have been terminated
|
|
193
|
+
for it. Claude Code is the documented exemption — which is exactly what
|
|
194
|
+
unsnooze drives. A browser-driving "Design adapter" is not a missing feature
|
|
195
|
+
here; it is one this project declines.
|
|
196
|
+
|
|
197
|
+
Two things worth knowing:
|
|
198
|
+
|
|
199
|
+
- A signed-out `/design-login` is **not** a usage limit. Waiting will never
|
|
200
|
+
clear it, so unsnooze reports it separately instead of letting it look like a
|
|
201
|
+
stop that will resolve itself.
|
|
202
|
+
- Design used to have its own weekly allowance. It does not now — everything
|
|
203
|
+
draws from one shared pool, which is why `unsnooze usage` already accounts
|
|
204
|
+
for design work without knowing it is design work.
|
|
205
|
+
|
|
165
206
|
## GUI surfaces (VS Code extension, desktop apps)
|
|
166
207
|
|
|
167
|
-
Terminal sessions are watched through the shell wrapper + tmux or
|
|
208
|
+
Terminal sessions are watched through the shell wrapper + tmux, Zellij, or herdr. Sessions in
|
|
168
209
|
**Claude Code's VS Code extension / desktop app** and **Codex's IDE
|
|
169
210
|
extension / desktop app** have no pane to scrape — so `unsnooze daemon` tails
|
|
170
211
|
the session files those surfaces already write:
|
|
@@ -417,7 +458,8 @@ pressing **`R`** (uppercase — lowercase `r` refreshes) only marks it due — t
|
|
|
417
458
|
remote's own daemon does the actual keystrokes, under the same
|
|
418
459
|
ownership/liveness/menu gates as a local session.
|
|
419
460
|
Resumed and stopped sessions print an attach hint
|
|
420
|
-
(`ssh -t <host> 'tmux new -A -s <session>'` / `zellij attach <session>`
|
|
461
|
+
(`ssh -t <host> 'tmux new -A -s <session>'` / `zellij attach <session>` /
|
|
462
|
+
`ssh -t <host> 'herdr session attach <session>'`) so
|
|
421
463
|
you can hop over and watch. An unreachable or out-of-date host shows
|
|
422
464
|
`unreachable`/`skew` rather than blocking the rest of the fleet, and falls
|
|
423
465
|
back to its last-known state (marked `stale`) for up to 24h.
|
|
@@ -508,7 +550,7 @@ password hosts on Windows; a plain `ssh <host>` prompt works with native
|
|
|
508
550
|
|
|
509
551
|
| key | default | meaning |
|
|
510
552
|
|---|---|---|
|
|
511
|
-
| `multiplexer` | `auto` | Backend to use: `auto`, `tmux`, or `
|
|
553
|
+
| `multiplexer` | `auto` | Backend to use: `auto`, `tmux`, `zellij`, `herdr`, `cmux`, or `headless`. `auto` prefers the current multiplexer (tmux/zellij ambient env, then `CMUX_SOCKET_PATH`); with no ambient env it falls back to whichever of tmux/zellij is installed, tmux as the tie-breaker — cmux is only selected via its ambient env or an explicit setting, never by this installed-backend fallback, since it has no attach-from-outside path to wrap into. cmux support covers resume-in-place (detect, capture, send); it has no joinable named-session model, so idle-session reaping (`unsnooze reap`) does not cover cmux panes, and no attach hint is shown for cmux sessions. `headless` is the no-multiplexer mode (see [Watching without a multiplexer](#watching-without-a-multiplexer)) — `auto` falls back to it only when no multiplexer is installed at all, never ahead of one. |
|
|
512
554
|
| `autoResume` | `true` | Master switch. Off = stops are still tracked, but nothing is resumed until you run `unsnooze resume-now` or turn it back on. |
|
|
513
555
|
| `menuAutoAnswer` | `true` | May unsnooze answer Claude's limit menu (send keys in your pane)? Off = watch-only. |
|
|
514
556
|
| `notifications` | `true` | Master switch for all notifications (limit detected / session resumed / gave up). Off = silence every channel. |
|
|
@@ -516,6 +558,8 @@ password hosts on Windows; a plain `ssh <host>` prompt works with native
|
|
|
516
558
|
| `guiWatch` | `true` | May the daemon watch session files for GUI-surface stops (VS Code extension, desktop apps)? Needs the daemon running (`unsnooze install --daemon`). |
|
|
517
559
|
| `resumeMessage` | *"Continue where you left off…"* | The message sent to wake a session. Override it for a single session with `unsnooze message <id> "…"` — visible in `unsnooze status`. |
|
|
518
560
|
| `resumeMessages.claude` / `.codex` / `.grok` / `.qwen` / `.kimi` / `.opencode` / `.agy` | `""` | Per-agent override of `resumeMessage`. Empty = use the global message; clear one with `unsnooze config set resumeMessages.claude ""`. |
|
|
561
|
+
| `resumeExtraArgs.claude` / `.codex` / `.grok` / `.qwen` / `.kimi` / `.opencode` / `.agy` | `""` | Extra flags for launches unsnooze performs itself. A revival spawns the agent binary directly, so flags you normally get from a shell alias or wrapper (`--dangerously-skip-permissions`, `--model …`) do not apply unless you put them here. Quoting is respected: `--append-system-prompt "stay in this repo"` is two arguments, not five. `config.json` may also hold an array (`["--append-system-prompt", "stay in this repo"]`), which skips parsing entirely. |
|
|
562
|
+
| `launchExtraArgs.claude` / `.codex` / `.grok` / `.qwen` / `.kimi` / `.opencode` / `.agy` | `""` | Extra flags for the sessions **you** start through the shell wrapper — the launch-side twin of `resumeExtraArgs`. Use it for flags that have to hold for the whole session: `unsnooze config set launchExtraArgs.claude "--autocompact 400000"` keeps a long, context-heavy run compacting instead of stalling. Flags are placed before your own arguments, since `claude "do the thing"` puts a positional prompt there. Revivals inherit them too, so a flag survives the wake. |
|
|
519
563
|
| `agents.claude` / `agents.codex` | `true` | Which CLIs are guarded. |
|
|
520
564
|
| `agents.grok` / `agents.qwen` / `agents.kimi` / `agents.opencode` / `agents.agy` | `false` | Experimental adapters — off by default; enable in `unsnooze setup` or `unsnooze config set agents.qwen on`. |
|
|
521
565
|
| `workspaceGuard` | `inform` | Repo changed while a session slept? `inform` wakes it with a heads-up in the message; `pause` holds it (desktop notification, diff shown on `resume-now`); `off` disables. |
|
|
@@ -613,32 +657,92 @@ watcher stops (no pane context) always use native.
|
|
|
613
657
|
|
|
614
658
|
## Requirements
|
|
615
659
|
|
|
616
|
-
- Node ≥ 20
|
|
617
|
-
- macOS, Linux, or
|
|
618
|
-
|
|
660
|
+
- Node ≥ 20.12
|
|
661
|
+
- macOS, Linux, or Windows — natively on all three (see
|
|
662
|
+
[Watching without a multiplexer](#watching-without-a-multiplexer))
|
|
663
|
+
- tmux ≥ 3.2, Zellij, **or** herdr ≥ 0.8.0 for pane-level watching. Optional:
|
|
664
|
+
without one, unsnooze runs `headless` and still catches and resumes stops.
|
|
665
|
+
- zsh or bash (wrappers go into `~/.zshrc` / `~/.bashrc`), or PowerShell
|
|
666
|
+
(wrappers go into `$PROFILE.CurrentUserAllHosts`)
|
|
667
|
+
|
|
668
|
+
### Watching without a multiplexer
|
|
669
|
+
|
|
670
|
+
A terminal pane is only one of unsnooze's three detection channels. The Claude
|
|
671
|
+
Code **StopFailure hook** and the **session transcript watcher** need no pane at
|
|
672
|
+
all — which is what makes native Windows, bare servers and CI work.
|
|
673
|
+
|
|
674
|
+
When no multiplexer is installed, `multiplexer: auto` resolves to `headless`:
|
|
675
|
+
|
|
676
|
+
| | with tmux/Zellij/herdr | `headless` |
|
|
677
|
+
|---|---|---|
|
|
678
|
+
| detects limit stops | pane scrape + hook + transcript | hook + transcript |
|
|
679
|
+
| resumes at reset | yes | yes |
|
|
680
|
+
| answers Claude's limit menu | yes | no |
|
|
681
|
+
| waits out a busy pane | yes | no |
|
|
682
|
+
| revived session | live pane you can attach to | detached process, output in `~/.unsnooze/headless/` |
|
|
683
|
+
|
|
684
|
+
`headless` is never chosen ahead of a real multiplexer — only when none is
|
|
685
|
+
installed, or when you ask for it with
|
|
686
|
+
`unsnooze config set multiplexer headless`. If you want the full behaviour on
|
|
687
|
+
Windows, WSL is still the better home.
|
|
688
|
+
|
|
689
|
+
### Windows
|
|
690
|
+
|
|
691
|
+
Native Windows works: PowerShell wrappers, a cmd-safe StopFailure hook, and a
|
|
692
|
+
logon-triggered Scheduled Task for the daemon. Install as usual and run
|
|
693
|
+
`unsnooze doctor` to confirm.
|
|
619
694
|
|
|
620
|
-
|
|
695
|
+
Two honest caveats. The daemon is started at logon but not restarted if it
|
|
696
|
+
dies — Task Scheduler is not a supervisor the way launchd and systemd are — so
|
|
697
|
+
after upgrading unsnooze, log out and back in (or run `unsnooze daemon`
|
|
698
|
+
yourself). And stored-password fleet hosts still need Git-for-Windows or WSL
|
|
699
|
+
`ssh`; native `ssh.exe` requires an `.exe` askpass helper unsnooze does not yet
|
|
700
|
+
ship.
|
|
621
701
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
agent CLIs live on Windows anyway:
|
|
702
|
+
WSL remains the richer option, because that is where the Unix multiplexers live
|
|
703
|
+
— and where the agent CLIs often already are:
|
|
625
704
|
|
|
626
705
|
```sh
|
|
627
706
|
# inside your WSL distro (Ubuntu etc.)
|
|
628
707
|
sudo apt install tmux # or install Zellij: https://zellij.dev/documentation/installation
|
|
708
|
+
# install herdr from https://herdr.dev (requires herdr >= 0.8.0)
|
|
629
709
|
npm install -g unsnooze && unsnooze setup
|
|
630
710
|
```
|
|
631
711
|
|
|
632
712
|
On macOS, install either backend with `brew install tmux` or
|
|
633
|
-
`brew install zellij
|
|
634
|
-
|
|
635
|
-
|
|
713
|
+
`brew install zellij`; install herdr from [herdr.dev](https://herdr.dev).
|
|
714
|
+
herdr >= 0.8.0, which is the version the backend is verified against; older 0.x
|
|
715
|
+
releases are refused by available(). Homebrew currently ships 0.7.3, so install
|
|
716
|
+
the release binary from herdr.dev or GitHub rather than `brew install herdr`.
|
|
717
|
+
In `auto` mode unsnooze uses the multiplexer you are currently inside; choose one
|
|
718
|
+
explicitly with `unsnooze config set multiplexer tmux`, `zellij`, or `herdr`.
|
|
719
|
+
|
|
720
|
+
#### herdr specifics
|
|
721
|
+
|
|
722
|
+
herdr differs from tmux and Zellij in three ways that are visible in use:
|
|
723
|
+
|
|
724
|
+
- **unsnooze never restarts a stopped herdr session.** herdr restores saved
|
|
725
|
+
agent panes when a session restarts (`resume_agents_on_restore` is on by
|
|
726
|
+
default), so restarting one can bring your agent back by itself — and a
|
|
727
|
+
revival on top of that would resume the same conversation twice. Reviving
|
|
728
|
+
into a stopped session therefore creates `unsnooze-2` (and so on) rather than
|
|
729
|
+
reusing the name. If you restart a stopped session yourself and herdr brings
|
|
730
|
+
the agent back, unsnooze may still open a fresh pane for it; that case is not
|
|
731
|
+
yet detected.
|
|
732
|
+
- **Multi-line resume messages are carried, not typed.** herdr starts a pane
|
|
733
|
+
command by typing it into the pane's shell, where a newline would submit half
|
|
734
|
+
a command and a tab would be a completion request. Any such argument is passed
|
|
735
|
+
through the workspace environment instead and referenced by the typed line, so
|
|
736
|
+
a multi-paragraph `resumeMessage` reaches the agent as one argument, byte for
|
|
737
|
+
byte. (A NUL byte is still refused — an environment value cannot hold one.)
|
|
738
|
+
- **A custom `HERDR_SOCKET_PATH` that does not match the session being
|
|
739
|
+
addressed disables watching for that pane.** herdr pane ids are per-server,
|
|
740
|
+
so acting on one from the wrong server could type into an unrelated terminal.
|
|
741
|
+
The agent still runs; unsnooze says why it is not watching.
|
|
636
742
|
|
|
637
743
|
Everything works as on Linux, including desktop notifications: inside WSL,
|
|
638
744
|
unsnooze raises **native Windows toasts** through `powershell.exe` (no
|
|
639
|
-
`notify-send` or X server needed).
|
|
640
|
-
not supported — without tmux or Zellij there is no terminal pane to watch; unsnooze will tell you
|
|
641
|
-
so and run your CLI unwatched instead of breaking it.
|
|
745
|
+
`notify-send` or X server needed).
|
|
642
746
|
|
|
643
747
|
## FAQ
|
|
644
748
|
|
|
@@ -711,6 +815,7 @@ npm test # unit tests (node:test)
|
|
|
711
815
|
./scripts/e2e-simulate.sh # full detect → wait → re-open cycle in a
|
|
712
816
|
# scratch tmux session (no real limits needed)
|
|
713
817
|
bash -n scripts/e2e-zellij.sh # syntax-check the reserved-session Zellij smoke test
|
|
818
|
+
node scripts/e2e-herdr.mjs # drives src/multiplexers/herdr.js against a real herdr (not run by CI)
|
|
714
819
|
vhs demo/demo.tape # regenerate assets/demo.gif (brew install vhs)
|
|
715
820
|
```
|
|
716
821
|
|
package/bin/unsnooze.js
CHANGED
|
@@ -48,7 +48,7 @@ function runAgentFallback(agentId, args) {
|
|
|
48
48
|
|
|
49
49
|
// Only human-facing commands may print update notices — never the wrapper
|
|
50
50
|
// passthrough, hooks, or daemons (their output lands in agent panes/logs).
|
|
51
|
-
const USER_FACING = new Set(['status', 'resume-now', 'cancel', 'message', 'config', 'logs', 'report', 'sessions', 'reap', 'doctor', 'preview', 'usage', 'dashboard', 'hosts', 'fleet', 'prompt', 'help', '-h', '--help', '--help-unsnooze']);
|
|
51
|
+
const USER_FACING = new Set(['status', 'resume-now', 'cancel', 'message', 'config', 'logs', 'report', 'sessions', 'reap', 'doctor', 'preview', 'usage', 'dashboard', 'hosts', 'fleet', 'prompt', 'design', 'help', '-h', '--help', '--help-unsnooze']);
|
|
52
52
|
|
|
53
53
|
// Every named subcommand; anything else (or no args) is an agent launch.
|
|
54
54
|
const NAMED_COMMANDS = new Set([
|
|
@@ -142,6 +142,10 @@ async function main() {
|
|
|
142
142
|
const { cmdDoctor } = await import('../src/doctor.js');
|
|
143
143
|
return cmdDoctor(rest);
|
|
144
144
|
}
|
|
145
|
+
case 'design': {
|
|
146
|
+
const { cmdDesign } = await import('../src/design.js');
|
|
147
|
+
return cmdDesign(rest);
|
|
148
|
+
}
|
|
145
149
|
case 'preview': {
|
|
146
150
|
const { cmdPreview } = await import('../src/cli.js');
|
|
147
151
|
return cmdPreview(rest);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unsnooze",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Automatically resume Claude Code, Codex CLI, Grok, Qwen Code, Kimi CLI, OpenCode, and Antigravity sessions when 5-hour or weekly usage limits reset—
|
|
3
|
+
"version": "1.16.0",
|
|
4
|
+
"description": "Automatically resume Claude Code, Codex CLI, Grok, Qwen Code, Kimi CLI, OpenCode, and Antigravity sessions when 5-hour or weekly usage limits reset—on macOS, Linux and Windows, with or without a terminal multiplexer.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
7
7
|
"claude-code",
|
|
@@ -35,10 +35,18 @@
|
|
|
35
35
|
"retry",
|
|
36
36
|
"tmux",
|
|
37
37
|
"zellij",
|
|
38
|
+
"herdr",
|
|
38
39
|
"ai-agents",
|
|
39
40
|
"agentic-coding",
|
|
40
41
|
"cli",
|
|
41
|
-
"developer-tools"
|
|
42
|
+
"developer-tools",
|
|
43
|
+
"windows",
|
|
44
|
+
"powershell",
|
|
45
|
+
"claude-design",
|
|
46
|
+
"mcp",
|
|
47
|
+
"headless",
|
|
48
|
+
"wsl",
|
|
49
|
+
"cmux"
|
|
42
50
|
],
|
|
43
51
|
"author": "Saaransh Menon <saaransh.dev2811@gmail.com>",
|
|
44
52
|
"license": "MIT",
|
|
@@ -65,7 +73,7 @@
|
|
|
65
73
|
"test": "node --test"
|
|
66
74
|
},
|
|
67
75
|
"engines": {
|
|
68
|
-
"node": ">=20"
|
|
76
|
+
"node": ">=20.12"
|
|
69
77
|
},
|
|
70
78
|
"dependencies": {
|
|
71
79
|
"@clack/prompts": "^1.7.0",
|
package/src/agents/claude.js
CHANGED
|
@@ -31,6 +31,13 @@ export const patterns = {
|
|
|
31
31
|
/limit.*(?:this|per)\s+week/i,
|
|
32
32
|
],
|
|
33
33
|
fiveHourPatterns: [/\d+-hour limit/i, /session limit/i],
|
|
34
|
+
// Per-model limits carry NO reset time and are not cleared by waiting:
|
|
35
|
+
// "You've reached your Fable 5 limit. Run /usage-credits to continue
|
|
36
|
+
// or switch models with /model."
|
|
37
|
+
// The remedy hint is required, not optional — it is what separates a real
|
|
38
|
+
// banner from an agent discussing usage limits in its own output.
|
|
39
|
+
modelLimitPatterns: [/(?:reached|hit)\s+(?:your|the)\s+[\w.\s-]{0,24}?\blimit\b/i],
|
|
40
|
+
modelRemedyPatterns: [/\/usage-credits\b/i, /switch\s+models?\b/i, /\/model\b/i],
|
|
34
41
|
// While Claude is streaming ("esc to interrupt") or running its own internal
|
|
35
42
|
// retries ("Retrying in 5s · attempt 3/10"), never inject keys.
|
|
36
43
|
busyPatterns: [
|
|
@@ -45,8 +52,29 @@ export const patterns = {
|
|
|
45
52
|
/API Error:?\s*\(?5\d\d/i,
|
|
46
53
|
/overloaded_error/i,
|
|
47
54
|
/API Error:?\s*\(?429/i,
|
|
55
|
+
// "API Error: Server is temporarily limiting requests (not your usage
|
|
56
|
+
// limit)" — a server-side throttle. It clears in seconds, so it belongs on
|
|
57
|
+
// this ladder; patterns.js separately refuses to read its parenthetical as
|
|
58
|
+
// a usage limit.
|
|
59
|
+
/temporarily limiting requests/i,
|
|
48
60
|
],
|
|
49
61
|
transientPatterns: [], // claude's transient errors are the overload set
|
|
62
|
+
// Stops that waiting cannot clear: notify once, never enter the ledger
|
|
63
|
+
// (monitor.js). Scheduling a wake for these would burn the attempt cap on a
|
|
64
|
+
// condition only a human can fix.
|
|
65
|
+
//
|
|
66
|
+
// All verbatim from the shipped 2.1.233 bundle. The Claude Design ones matter
|
|
67
|
+
// most for headless/unattended runs: a revived session has no interactive
|
|
68
|
+
// terminal, so an expired design credential produces a stop that can never
|
|
69
|
+
// self-resolve. Anchored to the error phrasings rather than to the bare
|
|
70
|
+
// command name, so an agent explaining "/design-login" is not a stop.
|
|
71
|
+
terminalPatterns: [
|
|
72
|
+
/rejected your \/design-login credential/i,
|
|
73
|
+
/could not refresh the design access token/i,
|
|
74
|
+
/could not save the design credential/i,
|
|
75
|
+
/\/design-login requires an interactive terminal/i,
|
|
76
|
+
/credit balance is too low/i,
|
|
77
|
+
],
|
|
50
78
|
};
|
|
51
79
|
|
|
52
80
|
// --- Interactive /rate-limit-options menu (Claude Code only) ---
|
|
@@ -87,10 +115,21 @@ export default {
|
|
|
87
115
|
experimental: false,
|
|
88
116
|
patterns,
|
|
89
117
|
menu: { isPrompt: isRateLimitOptionsPrompt, stepsToWait: menuStepsToWaitOption },
|
|
90
|
-
// How to reopen a dead session.
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
118
|
+
// How to reopen a dead session.
|
|
119
|
+
//
|
|
120
|
+
// Default (a real pane): the prompt is typed into the TUI once it's ready.
|
|
121
|
+
// That path is load-bearing on tmux/zellij/herdr and stays exactly as it was.
|
|
122
|
+
//
|
|
123
|
+
// canType: false (headless — no pane to type into): the prompt rides in argv.
|
|
124
|
+
// `claude --resume <id> "<prompt>"` resumes that session id and acts on the
|
|
125
|
+
// prompt, verified against claude 2.1.233 on 2026-08-16. Without a TTY it
|
|
126
|
+
// runs to completion non-interactively, which is what an unattended overnight
|
|
127
|
+
// resume wants anyway.
|
|
128
|
+
resumeArgs(sessionId, message, { canType = true } = {}) {
|
|
129
|
+
const args = sessionId ? ['--resume', sessionId] : ['-c'];
|
|
130
|
+
if (canType) return { args, messageViaPane: true };
|
|
131
|
+
if (message) args.push(message);
|
|
132
|
+
return { args, messageViaPane: false };
|
|
94
133
|
},
|
|
95
134
|
// v1: every agent launches the bare TUI and gets the prompt typed once idle.
|
|
96
135
|
launchArgs(message) { return { args: [], messageViaPane: true }; },
|
package/src/cli.js
CHANGED
|
@@ -94,6 +94,11 @@ export async function cmdStatus(args = []) {
|
|
|
94
94
|
mux: s.mux ?? null, pane: s.pane ?? null, muxSession: s.muxSession ?? null,
|
|
95
95
|
attempts: s.attempts ?? 0, lastError: s.lastError ?? null,
|
|
96
96
|
workspaceHold: !!s.workspaceHold,
|
|
97
|
+
// 'unsnooze' | 'native' | null. Since 2026-08-14 Claude Code can resume
|
|
98
|
+
// a five_hour stop by itself, so "resumed" alone no longer says who did
|
|
99
|
+
// it — and standing aside for the provider is a deliberate outcome, not
|
|
100
|
+
// a missing one.
|
|
101
|
+
resumedBy: s.resumedBy ?? null,
|
|
97
102
|
})),
|
|
98
103
|
promptQueue: queueList(),
|
|
99
104
|
}, null, 2));
|
|
@@ -175,8 +180,13 @@ export async function cmdStatus(args = []) {
|
|
|
175
180
|
let attach = '';
|
|
176
181
|
const hint = await attachHintFor(s);
|
|
177
182
|
if (hint) attach = ` · attach: ${hint}`;
|
|
183
|
+
// Claude Code can now resume a five_hour stop itself. When it did, say so:
|
|
184
|
+
// otherwise standing aside (the correct outcome, and the reason nothing was
|
|
185
|
+
// double-resumed) is indistinguishable from unsnooze having done the work.
|
|
186
|
+
const by = s.status === 'resumed' && s.resumedBy === 'native'
|
|
187
|
+
? ' · resumed itself (unsnooze stood aside)' : '';
|
|
178
188
|
console.log(` [${s.status.toUpperCase().padEnd(9)}] ${id} ${(s.agent || 'claude').padEnd(6)} ${s.limitType?.padEnd(7) ?? 'unknown'} ${s.cwd}`);
|
|
179
|
-
console.log(` mux ${s.mux ?? '-'} · pane ${pane} · session ${s.muxSession ?? '-'} · via ${origin} · resets ${reset} · attempts ${s.attempts ?? 0}/${MAX_RESUME_ATTEMPTS}${s.lastError ? ` · last error: ${s.lastError}` : ''}${msg}${ctx}${hold}${attach}`);
|
|
189
|
+
console.log(` mux ${s.mux ?? '-'} · pane ${pane} · session ${s.muxSession ?? '-'} · via ${origin} · resets ${reset} · attempts ${s.attempts ?? 0}/${MAX_RESUME_ATTEMPTS}${s.lastError ? ` · last error: ${s.lastError}` : ''}${msg}${ctx}${hold}${attach}${by}`);
|
|
180
190
|
}
|
|
181
191
|
printPromptQueueSection(pendingPromptEntries());
|
|
182
192
|
return 0;
|
|
@@ -349,6 +359,7 @@ export async function cmdPreview(rest = [], {
|
|
|
349
359
|
'give-up': () => 'would GIVE UP (attempt cap reached)',
|
|
350
360
|
verifying: () => 'verifying an in-flight resume',
|
|
351
361
|
retry: () => 'would RETRY later (pane capture failed)',
|
|
362
|
+
superseded: p => `would SKIP — record ${p.target?.key} revives the same conversation`,
|
|
352
363
|
none: () => 'nothing to do',
|
|
353
364
|
};
|
|
354
365
|
let actionable = false;
|
package/src/config.js
CHANGED
|
@@ -65,6 +65,27 @@ export const READY_TIMEOUT_MS = envInt('UNSNOOZE_READY_TIMEOUT_MS', 60_000);
|
|
|
65
65
|
export const EVENT_MARKER_TTL_MS = envInt('UNSNOOZE_EVENT_MARKER_TTL_MS', 120_000);
|
|
66
66
|
export const WATCH_FRESHNESS_MS = envInt('UNSNOOZE_WATCH_FRESHNESS_MS', 15 * 60_000);
|
|
67
67
|
|
|
68
|
+
// How long a monitor waits for its agent's lease to appear before concluding
|
|
69
|
+
// the launch failed and exiting. The launcher writes the lease immediately
|
|
70
|
+
// after spawn(), so this only ever elapses when no agent was started at all.
|
|
71
|
+
export const LEASE_GRACE_MS = envInt('UNSNOOZE_LEASE_GRACE_MS', 60_000);
|
|
72
|
+
|
|
73
|
+
// Multiplexer backends, in detection order. The single source of truth: the
|
|
74
|
+
// factory, the `multiplexer` setting enum and reap's session sweep all read
|
|
75
|
+
// this list. It lives here, in a module that imports nothing but node builtins,
|
|
76
|
+
// because settings.js and multiplexer.js already import each other — declaring
|
|
77
|
+
// it in either one puts the other in a temporal dead zone at import time.
|
|
78
|
+
// cmux is last: an agent can run tmux inside a cmux surface, so tmux/zellij
|
|
79
|
+
// must win detection when both are signalled (see multiplexer.js: detect()).
|
|
80
|
+
// headless is last of all and never auto-detected from the environment: it is
|
|
81
|
+
// the no-multiplexer fallback (native Windows, servers, CI), and a real pane
|
|
82
|
+
// must always beat it.
|
|
83
|
+
export const MUX_NAMES = ['tmux', 'zellij', 'herdr', 'cmux', 'headless'];
|
|
84
|
+
|
|
85
|
+
// Where a headless revive tees the agent's output. There is no pane to scroll
|
|
86
|
+
// back through, so the log is the only record of what an unattended run did.
|
|
87
|
+
export const HEADLESS_LOG_DIR = join(STATE_DIR, 'headless');
|
|
88
|
+
|
|
68
89
|
// Pane scanning
|
|
69
90
|
export const PANE_SCAN_LINES = envInt('UNSNOOZE_PANE_SCAN_LINES', 12);
|
|
70
91
|
export const CAPTURE_LINES = envInt('UNSNOOZE_CAPTURE_LINES', 200);
|