unsnooze 1.15.0 → 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 +68 -0
- package/README.md +87 -12
- package/bin/unsnooze.js +5 -1
- package/package.json +10 -3
- package/src/agents/claude.js +36 -4
- package/src/cli.js +11 -1
- package/src/config.js +8 -1
- package/src/design.js +209 -0
- package/src/doctor.js +43 -6
- package/src/install.js +118 -7
- package/src/launcher.js +19 -1
- package/src/multiplexer.js +22 -2
- package/src/multiplexers/headless.js +164 -0
- package/src/patterns.js +11 -0
- package/src/powershell.js +45 -0
- package/src/resumer.js +15 -2
- package/src/settings.js +23 -1
- package/src/wizard.js +14 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,73 @@
|
|
|
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
|
+
|
|
3
71
|
## 1.15.0 — 2026-08-15
|
|
4
72
|
|
|
5
73
|
- **Two new multiplexer backends: herdr and cmux.** unsnooze now watches and
|
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,6 +164,45 @@ 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
208
|
Terminal sessions are watched through the shell wrapper + tmux, Zellij, or herdr. Sessions in
|
|
@@ -509,7 +550,7 @@ password hosts on Windows; a plain `ssh <host>` prompt works with native
|
|
|
509
550
|
|
|
510
551
|
| key | default | meaning |
|
|
511
552
|
|---|---|---|
|
|
512
|
-
| `multiplexer` | `auto` | Backend to use: `auto`, `tmux`, `zellij`, `herdr`, 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. |
|
|
513
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. |
|
|
514
555
|
| `menuAutoAnswer` | `true` | May unsnooze answer Claude's limit menu (send keys in your pane)? Off = watch-only. |
|
|
515
556
|
| `notifications` | `true` | Master switch for all notifications (limit detected / session resumed / gave up). Off = silence every channel. |
|
|
@@ -518,6 +559,7 @@ password hosts on Windows; a plain `ssh <host>` prompt works with native
|
|
|
518
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`. |
|
|
519
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 ""`. |
|
|
520
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. |
|
|
521
563
|
| `agents.claude` / `agents.codex` | `true` | Which CLIs are guarded. |
|
|
522
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`. |
|
|
523
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. |
|
|
@@ -615,15 +657,50 @@ watcher stops (no pane context) always use native.
|
|
|
615
657
|
|
|
616
658
|
## Requirements
|
|
617
659
|
|
|
618
|
-
- Node ≥ 20.12
|
|
619
|
-
- macOS, Linux, or
|
|
620
|
-
|
|
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.
|
|
621
694
|
|
|
622
|
-
|
|
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.
|
|
623
701
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
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:
|
|
627
704
|
|
|
628
705
|
```sh
|
|
629
706
|
# inside your WSL distro (Ubuntu etc.)
|
|
@@ -665,9 +742,7 @@ herdr differs from tmux and Zellij in three ways that are visible in use:
|
|
|
665
742
|
|
|
666
743
|
Everything works as on Linux, including desktop notifications: inside WSL,
|
|
667
744
|
unsnooze raises **native Windows toasts** through `powershell.exe` (no
|
|
668
|
-
`notify-send` or X server needed).
|
|
669
|
-
not supported — without tmux or Zellij there is no terminal pane to watch; unsnooze will tell you
|
|
670
|
-
so and run your CLI unwatched instead of breaking it.
|
|
745
|
+
`notify-send` or X server needed).
|
|
671
746
|
|
|
672
747
|
## FAQ
|
|
673
748
|
|
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",
|
|
@@ -39,7 +39,14 @@
|
|
|
39
39
|
"ai-agents",
|
|
40
40
|
"agentic-coding",
|
|
41
41
|
"cli",
|
|
42
|
-
"developer-tools"
|
|
42
|
+
"developer-tools",
|
|
43
|
+
"windows",
|
|
44
|
+
"powershell",
|
|
45
|
+
"claude-design",
|
|
46
|
+
"mcp",
|
|
47
|
+
"headless",
|
|
48
|
+
"wsl",
|
|
49
|
+
"cmux"
|
|
43
50
|
],
|
|
44
51
|
"author": "Saaransh Menon <saaransh.dev2811@gmail.com>",
|
|
45
52
|
"license": "MIT",
|
package/src/agents/claude.js
CHANGED
|
@@ -52,8 +52,29 @@ export const patterns = {
|
|
|
52
52
|
/API Error:?\s*\(?5\d\d/i,
|
|
53
53
|
/overloaded_error/i,
|
|
54
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,
|
|
55
60
|
],
|
|
56
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
|
+
],
|
|
57
78
|
};
|
|
58
79
|
|
|
59
80
|
// --- Interactive /rate-limit-options menu (Claude Code only) ---
|
|
@@ -94,10 +115,21 @@ export default {
|
|
|
94
115
|
experimental: false,
|
|
95
116
|
patterns,
|
|
96
117
|
menu: { isPrompt: isRateLimitOptionsPrompt, stepsToWait: menuStepsToWaitOption },
|
|
97
|
-
// How to reopen a dead session.
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
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 };
|
|
101
133
|
},
|
|
102
134
|
// v1: every agent launches the bare TUI and gets the prompt typed once idle.
|
|
103
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;
|
package/src/config.js
CHANGED
|
@@ -77,7 +77,14 @@ export const LEASE_GRACE_MS = envInt('UNSNOOZE_LEASE_GRACE_MS', 60_000);
|
|
|
77
77
|
// it in either one puts the other in a temporal dead zone at import time.
|
|
78
78
|
// cmux is last: an agent can run tmux inside a cmux surface, so tmux/zellij
|
|
79
79
|
// must win detection when both are signalled (see multiplexer.js: detect()).
|
|
80
|
-
|
|
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');
|
|
81
88
|
|
|
82
89
|
// Pane scanning
|
|
83
90
|
export const PANE_SCAN_LINES = envInt('UNSNOOZE_PANE_SCAN_LINES', 12);
|
package/src/design.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// Claude Design support (issue #13).
|
|
2
|
+
//
|
|
3
|
+
// Claude Design has three surfaces: the canvas at claude.ai/design, the Claude
|
|
4
|
+
// Desktop sidebar, and — the one that matters here — an official MCP server
|
|
5
|
+
// that Claude Code drives from the terminal:
|
|
6
|
+
//
|
|
7
|
+
// claude mcp add --scope user --transport http claude-design \
|
|
8
|
+
// https://api.anthropic.com/v1/design/mcp
|
|
9
|
+
// /design-login
|
|
10
|
+
//
|
|
11
|
+
// unsnooze deliberately supports only that third surface. The canvas is a web
|
|
12
|
+
// app, and Anthropic's Consumer ToS bars accessing Claude "through automated or
|
|
13
|
+
// non-human means, whether through a bot, script, or otherwise" — driving it
|
|
14
|
+
// with a headless browser risks the user's account, and Claude Code is the
|
|
15
|
+
// documented exemption. There is no version of this file that opens a browser.
|
|
16
|
+
//
|
|
17
|
+
// Design draws from the same 5-hour and weekly pool as chat, Cowork and Claude
|
|
18
|
+
// Code (it had its own weekly allowance once; it does not now), so a design
|
|
19
|
+
// session that stops is an ordinary limit stop and the existing machinery
|
|
20
|
+
// already handles it. What was missing was setup and visibility, which is all
|
|
21
|
+
// this module adds.
|
|
22
|
+
|
|
23
|
+
import { spawnSync } from 'node:child_process';
|
|
24
|
+
import { readFileSync } from 'node:fs';
|
|
25
|
+
import { homedir } from 'node:os';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
|
|
28
|
+
export const DESIGN_MCP_NAME = 'claude-design';
|
|
29
|
+
export const DESIGN_MCP_URL = 'https://api.anthropic.com/v1/design/mcp';
|
|
30
|
+
|
|
31
|
+
export function designMcpAddArgs() {
|
|
32
|
+
return ['mcp', 'add', '--scope', 'user', '--transport', 'http',
|
|
33
|
+
DESIGN_MCP_NAME, DESIGN_MCP_URL];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function defaultRunner(args) {
|
|
37
|
+
const bin = process.env.UNSNOOZE_CLAUDE_BIN || 'claude';
|
|
38
|
+
const r = spawnSync(bin, args, { encoding: 'utf-8' });
|
|
39
|
+
if (r.error) throw r.error;
|
|
40
|
+
return `${r.stdout || ''}${r.stderr || ''}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// One row per configured MCP server, from `claude mcp list`. Real output:
|
|
44
|
+
//
|
|
45
|
+
// Checking MCP server health…
|
|
46
|
+
// [mcp-sdk] SEP-2352: stored OAuth credential has no 'issuer' stamp (…)
|
|
47
|
+
// claude.ai Notion: https://mcp.notion.com/mcp - ! Needs authentication
|
|
48
|
+
// plugin:context7:context7: https://mcp.context7.com/mcp (HTTP) - ✔ Connected
|
|
49
|
+
//
|
|
50
|
+
// Two shapes make this fussier than a split(': '): names carry both colons
|
|
51
|
+
// (plugin:context7:context7) and spaces (claude.ai Notion), and the SDK prints
|
|
52
|
+
// unrelated diagnostics that look like rows. So the row separator is the LAST
|
|
53
|
+
// ' - ' on the line, and the name is everything before the first COLON-SPACE —
|
|
54
|
+
// which no name contains, because a bare colon inside a name is never followed
|
|
55
|
+
// by a space.
|
|
56
|
+
export function parseMcpList(stdout) {
|
|
57
|
+
if (typeof stdout !== 'string' || stdout === '') return [];
|
|
58
|
+
const rows = [];
|
|
59
|
+
for (const raw of stdout.split('\n')) {
|
|
60
|
+
const line = raw.trim();
|
|
61
|
+
if (!line || line.startsWith('[')) continue; // sdk diagnostics
|
|
62
|
+
const sep = line.lastIndexOf(' - ');
|
|
63
|
+
if (sep === -1) continue;
|
|
64
|
+
const head = line.slice(0, sep);
|
|
65
|
+
const status = line.slice(sep + 3).trim();
|
|
66
|
+
const colon = head.indexOf(': ');
|
|
67
|
+
if (colon === -1) continue;
|
|
68
|
+
const name = head.slice(0, colon).trim();
|
|
69
|
+
if (!name) continue;
|
|
70
|
+
const target = head.slice(colon + 2).trim();
|
|
71
|
+
const needsAuth = /needs? authentication/i.test(status);
|
|
72
|
+
rows.push({
|
|
73
|
+
name,
|
|
74
|
+
target,
|
|
75
|
+
status,
|
|
76
|
+
connected: /connected/i.test(status) && !needsAuth,
|
|
77
|
+
needsAuth,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return rows;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Registration, answered from disk instead of over the wire.
|
|
84
|
+
//
|
|
85
|
+
// `claude mcp list` health-checks every configured server, which takes seconds
|
|
86
|
+
// — fine for an explicit `unsnooze design`, far too slow for `unsnooze doctor`,
|
|
87
|
+
// which people run casually. `--scope user` writes the TOP-LEVEL mcpServers map
|
|
88
|
+
// in ~/.claude.json; the per-project maps under .projects are a different scope
|
|
89
|
+
// and must not be mistaken for it.
|
|
90
|
+
export function designRegisteredOffline({
|
|
91
|
+
readConfig = () => readFileSync(join(homedir(), '.claude.json'), 'utf-8'),
|
|
92
|
+
} = {}) {
|
|
93
|
+
try {
|
|
94
|
+
const parsed = JSON.parse(readConfig());
|
|
95
|
+
return Boolean(parsed?.mcpServers?.[DESIGN_MCP_NAME]);
|
|
96
|
+
} catch {
|
|
97
|
+
return false; // missing, unreadable or corrupt — treat as not set up
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Is this machine ready to run Claude Design work from the terminal?
|
|
102
|
+
//
|
|
103
|
+
// Three distinguishable states, because the remedies differ completely:
|
|
104
|
+
// - claude missing/broken -> nothing to do with Design at all
|
|
105
|
+
// - not registered -> `unsnooze design setup`
|
|
106
|
+
// - registered, logged out -> `/design-login`, and note that this one is
|
|
107
|
+
// NOT a usage limit: no amount of waiting clears it.
|
|
108
|
+
export function designStatus({ runner = defaultRunner } = {}) {
|
|
109
|
+
let out;
|
|
110
|
+
try {
|
|
111
|
+
out = runner(['mcp', 'list']);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
return {
|
|
114
|
+
available: false, registered: false, connected: false, needsAuth: false,
|
|
115
|
+
hint: `could not run \`claude mcp list\` (${err.message}) — is the claude CLI installed?`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const row = parseMcpList(out).find(r => r.name === DESIGN_MCP_NAME);
|
|
119
|
+
if (!row) {
|
|
120
|
+
return {
|
|
121
|
+
available: true, registered: false, connected: false, needsAuth: false,
|
|
122
|
+
hint: 'Claude Design is not wired into Claude Code — run `unsnooze design setup`',
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (row.needsAuth || !row.connected) {
|
|
126
|
+
return {
|
|
127
|
+
available: true, registered: true, connected: false, needsAuth: true,
|
|
128
|
+
hint: 'Claude Design is registered but signed out — run `/design-login` inside Claude Code. '
|
|
129
|
+
+ 'This is not a usage limit; waiting will not clear it.',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
available: true, registered: true, connected: true, needsAuth: false,
|
|
134
|
+
hint: 'Claude Design is connected — design sessions are watched like any other Claude Code session',
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Register the MCP server. Editing the user's Claude config is not something to
|
|
139
|
+
// do quietly, so this is only ever reached from an explicit `unsnooze design
|
|
140
|
+
// setup`, and it prints exactly what it ran.
|
|
141
|
+
export function installDesignMcp({ runner = defaultRunner } = {}) {
|
|
142
|
+
const args = designMcpAddArgs();
|
|
143
|
+
try {
|
|
144
|
+
return { ok: true, output: runner(args), args };
|
|
145
|
+
} catch (err) {
|
|
146
|
+
return { ok: false, output: err.message, args };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// --- command ---------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
const USAGE = `unsnooze design — Claude Design from the terminal
|
|
153
|
+
|
|
154
|
+
unsnooze design show whether Claude Design is wired up
|
|
155
|
+
unsnooze design setup register the claude-design MCP server with Claude Code
|
|
156
|
+
|
|
157
|
+
Claude Design's canvas (claude.ai/design, Claude Desktop) is a web surface and
|
|
158
|
+
unsnooze does not automate it: Anthropic's Consumer Terms bar automated access
|
|
159
|
+
to claude.ai, and doing it anyway risks your account. The supported route is the
|
|
160
|
+
claude-design MCP server inside Claude Code, which unsnooze watches like any
|
|
161
|
+
other Claude Code session.
|
|
162
|
+
`;
|
|
163
|
+
|
|
164
|
+
export function cmdDesign(rest = [], { runner = defaultRunner, log = console.log } = {}) {
|
|
165
|
+
const sub = rest[0];
|
|
166
|
+
|
|
167
|
+
if (sub === 'help' || sub === '--help' || sub === '-h') {
|
|
168
|
+
log(USAGE);
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (sub === 'setup') {
|
|
173
|
+
const before = designStatus({ runner });
|
|
174
|
+
if (!before.available) {
|
|
175
|
+
log(`unsnooze: ${before.hint}`);
|
|
176
|
+
return 1;
|
|
177
|
+
}
|
|
178
|
+
if (before.registered) {
|
|
179
|
+
log(`unsnooze: ${DESIGN_MCP_NAME} is already registered.`);
|
|
180
|
+
log(`unsnooze: ${before.hint}`);
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
log(`unsnooze: running \`claude ${designMcpAddArgs().join(' ')}\``);
|
|
184
|
+
const result = installDesignMcp({ runner });
|
|
185
|
+
if (!result.ok) {
|
|
186
|
+
log(`unsnooze: registration failed — ${result.output}`);
|
|
187
|
+
return 1;
|
|
188
|
+
}
|
|
189
|
+
log('unsnooze: registered. Now run `/design-login` inside Claude Code to sign in.');
|
|
190
|
+
log('unsnooze: design work shares your usual 5-hour and weekly limits, so a');
|
|
191
|
+
log('unsnooze: design session that stops is resumed like any other.');
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (sub && sub !== 'status') {
|
|
196
|
+
log(`unsnooze: unknown design subcommand "${sub}"`);
|
|
197
|
+
log(USAGE);
|
|
198
|
+
return 1;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const status = designStatus({ runner });
|
|
202
|
+
log(`unsnooze: ${status.hint}`);
|
|
203
|
+
if (status.connected) {
|
|
204
|
+
log('unsnooze: tip — long design runs burn context fast; set');
|
|
205
|
+
log('unsnooze: unsnooze config set launchExtraArgs.claude "--autocompact 400000"');
|
|
206
|
+
log('unsnooze: so a context-heavy session compacts instead of stalling.');
|
|
207
|
+
}
|
|
208
|
+
return status.connected ? 0 : 1;
|
|
209
|
+
}
|
package/src/doctor.js
CHANGED
|
@@ -14,6 +14,8 @@ import { CLAUDE_SETTINGS, RESUMER_LOCK } from './config.js';
|
|
|
14
14
|
import { getMultiplexer } from './multiplexer.js';
|
|
15
15
|
import { makeLogger } from './logger.js';
|
|
16
16
|
import { shouldUseTui, formatDoctorTui } from './tui.js';
|
|
17
|
+
import { designRegisteredOffline } from './design.js';
|
|
18
|
+
import { powershellProfilePath } from './powershell.js';
|
|
17
19
|
|
|
18
20
|
const log = makeLogger('doctor');
|
|
19
21
|
|
|
@@ -104,11 +106,31 @@ function defaultHookInstalled(settingsPath = CLAUDE_SETTINGS) {
|
|
|
104
106
|
}
|
|
105
107
|
}
|
|
106
108
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
const WRAPPER_MARK = '# >>> unsnooze >>>';
|
|
110
|
+
|
|
111
|
+
function readOrEmpty(path) {
|
|
112
|
+
try { return readFileSync(path, 'utf-8'); } catch { return ''; }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// The wrapper shadowing the real CLI is unsnooze's entry point, so "are the
|
|
116
|
+
// wrappers installed" is a health question — but it has to be asked of the
|
|
117
|
+
// right file. A PowerShell user has no ~/.zshrc, so checking only the POSIX rc
|
|
118
|
+
// files made every native-Windows `unsnooze doctor` report the wrappers
|
|
119
|
+
// missing and point the user at an install they had already run.
|
|
120
|
+
function defaultWrappersInstalled({
|
|
121
|
+
platform = process.platform,
|
|
122
|
+
rcContent = readOrEmpty,
|
|
123
|
+
profileContent = null,
|
|
124
|
+
} = {}) {
|
|
125
|
+
const rcs = [join(homedir(), '.zshrc'), join(homedir(), '.bashrc')];
|
|
126
|
+
if (rcs.some(rc => rcContent(rc).includes(WRAPPER_MARK))) return true;
|
|
127
|
+
if (platform !== 'win32') return false;
|
|
128
|
+
const readProfile = profileContent
|
|
129
|
+
|| (() => {
|
|
130
|
+
const p = powershellProfilePath({ platform });
|
|
131
|
+
return p ? readOrEmpty(p) : '';
|
|
132
|
+
});
|
|
133
|
+
return readProfile().includes(WRAPPER_MARK);
|
|
112
134
|
}
|
|
113
135
|
|
|
114
136
|
function daemonRunning() {
|
|
@@ -136,8 +158,11 @@ export async function runDoctor({
|
|
|
136
158
|
csgStateDir = join(homedir(), '.claude-session-guard'),
|
|
137
159
|
csgBinPath = defaultCsgPackagePath({ runner }),
|
|
138
160
|
mux = getMultiplexer(),
|
|
161
|
+
designRegistered = designRegisteredOffline,
|
|
139
162
|
hookInstalled = defaultHookInstalled,
|
|
140
163
|
wrappersInstalled = defaultWrappersInstalled,
|
|
164
|
+
rcContent = undefined,
|
|
165
|
+
profileContent = undefined,
|
|
141
166
|
autostartDir = null,
|
|
142
167
|
} = {}) {
|
|
143
168
|
const findings = [];
|
|
@@ -188,7 +213,7 @@ export async function runDoctor({
|
|
|
188
213
|
detail: ' run: unsnooze install --yes',
|
|
189
214
|
});
|
|
190
215
|
}
|
|
191
|
-
if (!wrappersInstalled()) {
|
|
216
|
+
if (!wrappersInstalled({ platform, ...(rcContent ? { rcContent } : {}), ...(profileContent ? { profileContent } : {}) })) {
|
|
192
217
|
findings.push({
|
|
193
218
|
id: 'wrappers-missing', kind: 'health',
|
|
194
219
|
title: 'shell wrappers are not installed',
|
|
@@ -230,6 +255,18 @@ export async function runDoctor({
|
|
|
230
255
|
detail: '',
|
|
231
256
|
});
|
|
232
257
|
|
|
258
|
+
// Claude Design (issue #13). Informational only — Design is optional, so a
|
|
259
|
+
// machine without it is not unhealthy. Read from config rather than
|
|
260
|
+
// `claude mcp list`, which health-checks every server and would put seconds
|
|
261
|
+
// onto every doctor run.
|
|
262
|
+
findings.push({
|
|
263
|
+
id: 'claude-design', kind: 'info',
|
|
264
|
+
title: designRegistered()
|
|
265
|
+
? 'Claude Design: MCP server registered (run `unsnooze design` to check sign-in)'
|
|
266
|
+
: 'Claude Design: not set up (optional — `unsnooze design setup`)',
|
|
267
|
+
detail: '',
|
|
268
|
+
});
|
|
269
|
+
|
|
233
270
|
const healthy = findings.every(f => f.kind === 'info');
|
|
234
271
|
return { findings, healthy };
|
|
235
272
|
}
|