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 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 in tmux or Zellij the moment the usage limit resets.**
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 Zellij. Sessions in
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>`) so
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 `zellij`. `auto` prefers the current multiplexer, then the only installed backend, with tmux as the tie-breaker. |
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 and tmux ≥ 3.2 **or** Zellij
617
- - macOS, Linux, or **Windows via WSL** (see below)
618
- - zsh or bash (the wrappers are installed into `~/.zshrc` / `~/.bashrc`)
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
- ### Windows / WSL
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
- tmux and Zellij are Unix tools, so on Windows unsnooze runs inside
623
- [WSL](https://learn.microsoft.com/windows/wsl/install) — which is where the
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`. In `auto` mode unsnooze uses the multiplexer you are
634
- currently inside; choose one explicitly with
635
- `unsnooze config set multiplexer tmux` or `zellij`.
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). Native Windows (PowerShell/cmd, no WSL) is
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.14.4",
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—across tmux, Zellij, VS Code, and desktop apps.",
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",
@@ -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. messageViaPane: the resume prompt is typed
91
- // into the TUI once it's ready (claude has no resume-with-prompt argv form).
92
- resumeArgs(sessionId) {
93
- return { args: sessionId ? ['--resume', sessionId] : ['-c'], messageViaPane: true };
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);