unsnooze 1.19.0 → 1.19.2
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 +218 -0
- package/README.md +41 -12
- package/bin/unsnooze.js +90 -64
- package/package.json +1 -1
- package/src/agents/codex.js +117 -12
- package/src/agents/opencode.js +2 -1
- package/src/cli.js +4 -0
- package/src/config.js +21 -0
- package/src/doctor.js +76 -0
- package/src/hook.js +6 -2
- package/src/install.js +104 -35
- package/src/launcher.js +52 -10
- package/src/monitor.js +11 -0
- package/src/multiplexers/headless.js +159 -11
- package/src/resumer.js +94 -11
- package/src/spawn.js +262 -9
- package/src/time-parser.js +6 -1
- package/src/usage.js +50 -21
- package/src/watcher.js +45 -12
- package/src/watchers/codex.js +202 -28
- package/src/which.js +63 -0
- package/src/wizard.js +8 -4
- package/src/workspace.js +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,223 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.19.2 — 2026-09-27
|
|
4
|
+
|
|
5
|
+
Setup on Windows no longer creates a Scheduled Task. That task is what
|
|
6
|
+
Microsoft Defender flagged as a Trojan.
|
|
7
|
+
|
|
8
|
+
### Website redesign
|
|
9
|
+
|
|
10
|
+
- The new night-shift design now covers the homepage, documentation, changelog,
|
|
11
|
+
and feedback page, with responsive layouts and reduced-motion support.
|
|
12
|
+
- Release metadata refreshes every five minutes so the site follows published
|
|
13
|
+
npm versions. Failed feedback-board reads now show an error instead of
|
|
14
|
+
loading indefinitely.
|
|
15
|
+
- Windows setup and troubleshooting docs describe the on-demand daemon.
|
|
16
|
+
|
|
17
|
+
### Codex wake recovery
|
|
18
|
+
|
|
19
|
+
- Team/business stops with a null credits balance now retain the exhausted
|
|
20
|
+
five-hour window's reset instead of being held as a workspace credit wall.
|
|
21
|
+
- If Codex consumes Enter as a pasted newline, verification retries Enter
|
|
22
|
+
up to three times while the exact wake prompt remains in the composer.
|
|
23
|
+
Each retry rechecks pane ownership and agent liveness. The old banner is
|
|
24
|
+
not re-recorded as a new stop, and the wake text is never pasted twice.
|
|
25
|
+
An unconfirmed submission is left visible for manual inspection.
|
|
26
|
+
|
|
27
|
+
### No Scheduled Task on Windows
|
|
28
|
+
|
|
29
|
+
A user reported that Microsoft Defender flagged `unsnooze setup` as
|
|
30
|
+
`Trojan:Win32/Commando.A!ml` as it created the `\unsnooze` Scheduled Task. It
|
|
31
|
+
was a false positive, but an understandable one. Setup ran
|
|
32
|
+
`schtasks /create /f /sc onlogon /tr "node.exe …\AppData\Roaming\npm\…\unsnooze.js daemon"`:
|
|
33
|
+
an interpreter told to run a script out of `%APPDATA%` at every logon, which is
|
|
34
|
+
exactly how malware persists, and `Commando.A!ml` is Defender's
|
|
35
|
+
machine-learning verdict on command lines like that one. A Run key or a
|
|
36
|
+
Startup-folder entry reads the same way, so the daemon uses none of them now.
|
|
37
|
+
The task rarely worked anyway: from a shell that is not elevated, `schtasks`
|
|
38
|
+
refuses an `onlogon` task with "Access is denied", and setup said it was
|
|
39
|
+
installed regardless.
|
|
40
|
+
|
|
41
|
+
On Windows the daemon is now an ordinary background process:
|
|
42
|
+
|
|
43
|
+
- **Setup starts it.** Saying yes to guarding GUI sessions (or running
|
|
44
|
+
`unsnooze install --daemon`) starts the daemon at once and records that
|
|
45
|
+
choice in `~/.unsnooze/daemon-on-demand`. Nothing is registered with Windows.
|
|
46
|
+
- **It is started again on demand.** Every agent launched through the
|
|
47
|
+
PowerShell wrappers, and every Claude Code StopFailure hook, starts the
|
|
48
|
+
daemon if it is not running. After a restart it is back as soon as you use
|
|
49
|
+
an agent.
|
|
50
|
+
- **One runs at a time.** The daemon keeps a heartbeat pidfile
|
|
51
|
+
(`~/.unsnooze/daemon.pid`), and a second daemon stands down at once. Once the
|
|
52
|
+
heartbeat is five minutes old the file no longer counts, whoever its pid
|
|
53
|
+
belongs to by then, so a pid Windows recycles after a sign-out cannot keep
|
|
54
|
+
the daemon from starting. A resumer lock left behind by a daemon that was
|
|
55
|
+
killed is cleared too, so it cannot stall every resume.
|
|
56
|
+
- **It carries no session with it.** Started from inside a Claude Code
|
|
57
|
+
session (by the hook), it used to inherit that session's environment,
|
|
58
|
+
including `CLAUDECODE=1`, Claude Code's nested-session guard. Every Claude
|
|
59
|
+
session it revived would then have refused to start. Background processes
|
|
60
|
+
now drop the variables that mark one session; your own settings, such as
|
|
61
|
+
`CLAUDE_CONFIG_DIR` and `CLAUDE_CODE_USE_BEDROCK`, are kept. They also run in
|
|
62
|
+
your home directory, not the project that happened to start them, which
|
|
63
|
+
Windows would otherwise refuse to delete or rename until sign-out. The
|
|
64
|
+
transient resumer the hook starts gets the same treatment, on every
|
|
65
|
+
platform.
|
|
66
|
+
- **No console windows.** A background daemon has no console. The `git` calls
|
|
67
|
+
it makes when a session stops and wakes, and the agent a revival starts,
|
|
68
|
+
now run hidden instead of each popping up a window of its own.
|
|
69
|
+
- **Its output goes to `~/.unsnooze/daemon.log`**, as the launchd daemon's
|
|
70
|
+
does, so a crash leaves a trace.
|
|
71
|
+
- **Re-running setup restarts it.** `unsnooze install --daemon` stops the
|
|
72
|
+
running daemon and starts a fresh one. That is how it picks up a new
|
|
73
|
+
version, or a `PATH` that now finds an agent, and `unsnooze doctor` now
|
|
74
|
+
says so instead of pointing at `schtasks`.
|
|
75
|
+
|
|
76
|
+
If you use only the Codex app or IDE on Windows, nothing you do passes through
|
|
77
|
+
a wrapper or the hook. After signing in, run `unsnooze install --daemon` (or
|
|
78
|
+
launch any wrapped agent) to start the daemon.
|
|
79
|
+
|
|
80
|
+
`unsnooze uninstall` removes the marker first, then stops the daemon, so
|
|
81
|
+
nothing can start it again halfway through; a daemon that was only starting at
|
|
82
|
+
that moment sees the marker gone and exits. It also deletes a `\unsnooze`
|
|
83
|
+
Scheduled Task left by an earlier version, after checking that one exists, so
|
|
84
|
+
a machine that never had one only ever sees a read-only query. If the delete
|
|
85
|
+
is refused (a task created from an administrator shell can need one to
|
|
86
|
+
remove), it prints the command to run. Setup does not go looking for that
|
|
87
|
+
task: that would mean running `schtasks` again. A daemon such a task already
|
|
88
|
+
started keeps running until you sign out; from the next sign-in the task only
|
|
89
|
+
starts the same on-demand daemon, which the pidfile keeps to one. macOS and
|
|
90
|
+
Linux are unchanged (launchd / systemd).
|
|
91
|
+
|
|
92
|
+
## 1.19.1 — 2026-09-21
|
|
93
|
+
|
|
94
|
+
A Codex usage reading that was averaged down, a headless Codex revival
|
|
95
|
+
that could never have worked — reported as a success — and Codex stops that
|
|
96
|
+
were never seen at all behind a proxy or in a reverted thread.
|
|
97
|
+
|
|
98
|
+
### `unsnooze usage` reports Codex's exact percentage as read
|
|
99
|
+
|
|
100
|
+
The 64% in [issue #20](https://github.com/saaranshM/unsnooze/issues/20) was
|
|
101
|
+
`(29 + 99) / 2`: a jump of more than 15 points was averaged with the reading
|
|
102
|
+
before it and still labelled `exact`. The averaging was meant to soften a
|
|
103
|
+
one-tick spike, but at the moment a session is blocked no further reading
|
|
104
|
+
arrives, so the low number stayed for the rest of the window — and 64 is
|
|
105
|
+
below the 80/95 warning thresholds, so the usage-wall notification never
|
|
106
|
+
fired. Codex's `used_percent` is the server's own number and is now shown
|
|
107
|
+
unchanged (still clamped to 0–100).
|
|
108
|
+
|
|
109
|
+
Two things could put a much lower reading in front of the latest one: Codex
|
|
110
|
+
writes one snapshot per rate-limit bucket per response, and `unsnooze usage`
|
|
111
|
+
read every bucket (`premium`, `codex_other`, …) as the account window; and
|
|
112
|
+
Codex Desktop tabs and sub-agents each write their own rollout, so "the
|
|
113
|
+
previous reading" could be another thread's minutes-old snapshot. Only the
|
|
114
|
+
account bucket feeds the 5h/weekly lines now, and burn is measured between
|
|
115
|
+
readings of the same rollout. Thanks to
|
|
116
|
+
[@mio-tsuki](https://github.com/mio-tsuki) for working out the arithmetic.
|
|
117
|
+
|
|
118
|
+
### Headless Codex revivals run, and a revival that dies says so
|
|
119
|
+
|
|
120
|
+
[Issue #25](https://github.com/saaranshM/unsnooze/issues/25): on Windows the
|
|
121
|
+
daemon's Scheduled Task could not find `codex`, every revival died with
|
|
122
|
+
`spawn codex ENOENT`, and unsnooze logged `verified resumed` anyway.
|
|
123
|
+
|
|
124
|
+
- **`codex exec resume`.** Under the headless backend a revival ran the Codex
|
|
125
|
+
TUI, which exits before touching the session when stdin is not a terminal —
|
|
126
|
+
on every OS, since the backend shipped in 1.16.0. Headless Codex revivals
|
|
127
|
+
now run `codex exec resume <id> "<prompt>"`, which continues the same
|
|
128
|
+
conversation non-interactively, the way Claude's headless revival already
|
|
129
|
+
passed its prompt in argv. It is given `--skip-git-repo-check`: `exec`
|
|
130
|
+
refuses any directory that is not a git repository, which the TUI never
|
|
131
|
+
did, and the session being revived already ran there. `resumeExtraArgs.codex`
|
|
132
|
+
goes right after `exec`, where `-s`, `-p` and `--add-dir` parse; after
|
|
133
|
+
`resume` they were rejected outright.
|
|
134
|
+
- **A dead revival is a failed attempt.** With no pane to capture, an empty
|
|
135
|
+
capture used to count as a cleared banner. The headless backend now records
|
|
136
|
+
each revival's exit, and a non-zero one puts the stop back on the ledger
|
|
137
|
+
with backoff and a `last error` that carries the child's own words — visible
|
|
138
|
+
in `unsnooze status`, and kept when unsnooze finally gives up on the session.
|
|
139
|
+
A revival still running at the 20-second check is watched for up to two
|
|
140
|
+
minutes before it counts as resumed, long enough to catch the failures that
|
|
141
|
+
take a while (an auth refresh giving up, a retry loop); exit 0 counts as
|
|
142
|
+
resumed. A revival started by a resumer the Claude hook spawned no longer
|
|
143
|
+
inherits Claude's nested-launch marker, which had made it drop
|
|
144
|
+
`launchExtraArgs` and fail with a bare "exit 1".
|
|
145
|
+
- **A revival into a deleted directory no longer takes the daemon down.** When
|
|
146
|
+
a session's directory was gone (a removed worktree, say), the headless
|
|
147
|
+
launch failed with an error nothing was listening for, and the daemon
|
|
148
|
+
crashed. It is now an ordinary failed attempt that names the reason.
|
|
149
|
+
- **`unsnooze preview` shows the headless command.** It described a typed
|
|
150
|
+
resume and the Codex TUI for sessions that dispatch revives through argv
|
|
151
|
+
and `codex exec resume`.
|
|
152
|
+
- **Windows finds the Codex runtime.** The Desktop/Store install keeps
|
|
153
|
+
`codex.exe` under a versioned directory that changes on update, and a daemon
|
|
154
|
+
started at logon keeps the PATH it was born with. Codex is now resolved at
|
|
155
|
+
launch: `UNSNOOZE_CODEX_BIN`, then `codex.exe` on PATH, then the newest
|
|
156
|
+
runtime under `%LOCALAPPDATA%\OpenAI\Codex\bin`. A `.cmd`/`.bat` shim
|
|
157
|
+
cannot be launched directly by Node; the launcher now says so and names the
|
|
158
|
+
variable to point at the `.exe` instead of failing with `EINVAL`.
|
|
159
|
+
- **`unsnooze doctor` names each agent's binary** as the launcher would resolve
|
|
160
|
+
it, and reports an agent that cannot be launched as a health problem — on
|
|
161
|
+
Windows with the note that the daemon's environment can lag the shell's and
|
|
162
|
+
how to refresh it. Only for agents that have run on the machine: Claude and
|
|
163
|
+
Codex are both on by default, and a Claude-only machine is not unhealthy for
|
|
164
|
+
having no `codex`. The lookup follows spawn's own Windows rules: an `.exe`
|
|
165
|
+
anywhere on PATH wins over an npm shim earlier on it, quoted PATH entries
|
|
166
|
+
count, and a path given without its extension finds the `.exe`.
|
|
167
|
+
- **Codex stops bind to the right reset.** A `rate_limit_reached` at a reported
|
|
168
|
+
99.x% was scheduled for the *weekly* reset (the latest one), days out; it now
|
|
169
|
+
binds the window nearest exhaustion. A workspace wall — credits depleted or
|
|
170
|
+
a spend cap, with no window spent — has no reset to wait for: it is announced
|
|
171
|
+
as needing you rather than "resumes when the limit resets", probed, and then
|
|
172
|
+
held with the remedy (add credits or raise the cap, then continue the
|
|
173
|
+
session in Codex) instead of a blind wake. A spent window (99% or more)
|
|
174
|
+
still governs when a workspace reason rides along, since its reset is what
|
|
175
|
+
brings the plan allowance back. `unsnooze status --json` shows the reason as
|
|
176
|
+
`limitReason`.
|
|
177
|
+
- **A weaker reading no longer undoes an exact reset.** A re-detected stop
|
|
178
|
+
whose only evidence is a banner ("Try again later.") used to replace the
|
|
179
|
+
exact reset the rollout snapshot had given, turning a known wake into a
|
|
180
|
+
15-minute probe; the stronger schedule now stands until it passes, the rule
|
|
181
|
+
pane detection already followed. `unsnooze usage` in the dashboard also
|
|
182
|
+
stops reading Codex burn as idle whenever the daemon is running.
|
|
183
|
+
|
|
184
|
+
The Windows resolution follows the layout in the report and wants a Windows
|
|
185
|
+
run before it is trusted; continuing an open Codex Desktop thread in place
|
|
186
|
+
(`codex queue`) is a separate follow-up. Thanks to
|
|
187
|
+
[@nwn900](https://github.com/nwn900) for the precise report.
|
|
188
|
+
|
|
189
|
+
### Codex stops behind a proxy, and in reverted threads
|
|
190
|
+
|
|
191
|
+
[Issue #27](https://github.com/saaranshM/unsnooze/issues/27) found two ways
|
|
192
|
+
a Codex desktop-app or IDE session went untracked:
|
|
193
|
+
|
|
194
|
+
- **Reverted threads are watched.** An edited, regenerated or `/undo`-ed
|
|
195
|
+
thread continues in `rollout-<ts>-<thread id>_<rollout id>.jsonl`, a name
|
|
196
|
+
the rollout watcher never opened. Those files are now watched, counted by
|
|
197
|
+
`unsnooze usage`, and resumed by the stable thread id.
|
|
198
|
+
- **A stop recorded only as an error is a stop.** Behind an OpenAI-compatible
|
|
199
|
+
proxy the rate-limit headers never reach Codex, every snapshot has empty
|
|
200
|
+
windows, and no stop was ever detected. Since codex-cli 0.145 the failed
|
|
201
|
+
turn's `task_complete` carries the limit error itself; unsnooze now reads
|
|
202
|
+
it and dates it from the banner the way a scraped pane is dated ("Try again
|
|
203
|
+
later." is probed). When the same turn also wrote an exhausted snapshot,
|
|
204
|
+
its exact reset still wins, and a workspace wall — in the snapshot or in
|
|
205
|
+
the banner's own words — stays a wall. Only the banner counts: Codex marks
|
|
206
|
+
billing and plan errors ("Quota exceeded…", "upgrade to Plus…") with the
|
|
207
|
+
same `usage_limit_exceeded` code, and those are not stops, nor is a bare 429,
|
|
208
|
+
which carries no reset time.
|
|
209
|
+
- **Codex's current banner is recognized.** Codex now writes "You’ve hit your
|
|
210
|
+
usage limit" with a typographic apostrophe; the anchor accepts both, in
|
|
211
|
+
rollouts and in panes.
|
|
212
|
+
- **A reset in the banner's own minute is not tomorrow.** Codex prints the
|
|
213
|
+
reset time without seconds, so a limit hit at 7:36:15 that resets at
|
|
214
|
+
7:36:40 says "try again at 7:36 AM" — which was read as 7:36 the next
|
|
215
|
+
morning, a day late. It now wakes at 7:37.
|
|
216
|
+
|
|
217
|
+
Thanks to [@d-jiao](https://github.com/d-jiao) for the report, the fix in
|
|
218
|
+
[PR #28](https://github.com/saaranshM/unsnooze/pull/28), and checking it
|
|
219
|
+
against 523 real rollouts.
|
|
220
|
+
|
|
3
221
|
## 1.19.0 — 2026-09-10
|
|
4
222
|
|
|
5
223
|
Fish shell support, direct help/version commands, and a fix for Codex sessions
|
package/README.md
CHANGED
|
@@ -244,6 +244,15 @@ the session files those surfaces already write:
|
|
|
244
244
|
one minute by an empty `premium` bucket with no credits, using the previous
|
|
245
245
|
future reset time. The snapshots must come from the same rollout; 99% alone
|
|
246
246
|
is not a stop signal. This works across watcher polls and daemon restarts.
|
|
247
|
+
Since codex-cli 0.145 the failed turn's `task_complete` also carries the
|
|
248
|
+
limit error itself (`codex_error_info: "usage_limit_exceeded"` plus the
|
|
249
|
+
banner text), and unsnooze reads that too — it is the only signal when
|
|
250
|
+
Codex runs behind an OpenAI-compatible proxy (`model_providers.<x>.base_url`),
|
|
251
|
+
where the rate-limit headers never reach Codex and every snapshot has empty
|
|
252
|
+
windows. The banner's time is parsed like a scraped pane; "Try again later."
|
|
253
|
+
takes the probe path. Reverted threads (the desktop app's edit/regenerate,
|
|
254
|
+
`/undo`) continue in `rollout-<ts>-<thread id>_<rollout id>.jsonl`; those
|
|
255
|
+
files are watched and resumed by the stable thread id.
|
|
247
256
|
- **Claude desktop (cowork) sessions** *(experimental, macOS)* run in
|
|
248
257
|
sandboxes under `~/Library/Application Support/Claude`; unsnooze watches
|
|
249
258
|
those too and revives with the session's isolated `CLAUDE_CONFIG_DIR`
|
|
@@ -256,7 +265,8 @@ the continued conversation stays visible in the GUI's own history. (Resuming
|
|
|
256
265
|
*inside* the GUI panel isn't possible today: no extension/app exposes an
|
|
257
266
|
IPC/URI that can send a prompt.)
|
|
258
267
|
|
|
259
|
-
Enable it in `unsnooze setup` (installs a launchd agent / systemd user unit
|
|
268
|
+
Enable it in `unsnooze setup` (installs a launchd agent / systemd user unit;
|
|
269
|
+
on Windows the daemon is started on demand instead — see [Windows](#windows)),
|
|
260
270
|
or run `unsnooze install --daemon` / `unsnooze daemon` yourself. Turn it off
|
|
261
271
|
anytime with `unsnooze config set guiWatch off`.
|
|
262
272
|
|
|
@@ -375,7 +385,8 @@ unsnooze config set <k> <v> # e.g. autoResume off
|
|
|
375
385
|
unsnooze logs [-f] # what unsnooze has been doing
|
|
376
386
|
unsnooze update # update unsnooze itself
|
|
377
387
|
unsnooze daemon # persistent GUI-session watcher (usually run
|
|
378
|
-
# by launchd/systemd via `install --daemon
|
|
388
|
+
# by launchd/systemd via `install --daemon`;
|
|
389
|
+
# on Windows, started on demand)
|
|
379
390
|
unsnooze report [agent] # capture a pane to report an undetected banner
|
|
380
391
|
unsnooze uninstall [--purge] # remove wrappers + hooks (+ state with --purge)
|
|
381
392
|
unsnooze help # full command list (also -h / --help)
|
|
@@ -719,7 +730,10 @@ herdr restores saved agent panes itself, so a revival on top would resume the
|
|
|
719
730
|
same conversation twice.
|
|
720
731
|
² Into a fresh workspace; cmux has no joinable named-session model.
|
|
721
732
|
³ As a detached process. Its output goes to `~/.unsnooze/headless/<session>.log`,
|
|
722
|
-
since there is no scrollback to read it out of later
|
|
733
|
+
since there is no scrollback to read it out of later; the prompt rides in argv
|
|
734
|
+
(`claude --resume <id> "…"`, `codex exec resume <id> "…"`), and a revival that
|
|
735
|
+
exits non-zero is retried, with its last output as the `last error` in
|
|
736
|
+
`unsnooze status`.
|
|
723
737
|
⁴ Nothing to wrap into — your own terminal is the session.
|
|
724
738
|
|
|
725
739
|
Pick one explicitly with `unsnooze config set multiplexer tmux|zellij|herdr|cmux|headless`.
|
|
@@ -755,15 +769,30 @@ Windows, WSL is still the better home.
|
|
|
755
769
|
### Windows
|
|
756
770
|
|
|
757
771
|
Native Windows works: PowerShell wrappers, a cmd-safe StopFailure hook, and a
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
772
|
+
daemon started on demand. Install as usual and run `unsnooze doctor` to confirm.
|
|
773
|
+
|
|
774
|
+
unsnooze registers nothing to run at sign-in on Windows — no Scheduled Task, Run
|
|
775
|
+
key or Startup-folder entry. (Up to 1.19.1 it created a logon Scheduled Task,
|
|
776
|
+
and Microsoft Defender flagged setup as `Trojan:Win32/Commando.A!ml` for it;
|
|
777
|
+
`unsnooze uninstall` removes one left behind.)
|
|
778
|
+
Setup starts the daemon instead, and every agent you launch from PowerShell and
|
|
779
|
+
every Claude Code StopFailure hook starts it again whenever it is not running,
|
|
780
|
+
so after a restart it is back as soon as you use an agent. If you only use the
|
|
781
|
+
Codex app or IDE, nothing you do passes through a wrapper or the hook: run
|
|
782
|
+
`unsnooze install --daemon` after signing in to start it.
|
|
783
|
+
|
|
784
|
+
Three honest caveats. Nothing supervises the daemon the way launchd and systemd
|
|
785
|
+
do, and a running daemon keeps the code it started with, so after upgrading
|
|
786
|
+
unsnooze re-run `unsnooze install --daemon` (or sign out and back in). The
|
|
787
|
+
daemon also keeps the `PATH` it was born with: an agent whose install directory
|
|
788
|
+
changes on update (the Codex Desktop/Store runtime lives under
|
|
789
|
+
`%LOCALAPPDATA%\OpenAI\Codex\bin\<hash>`) can vanish from the daemon's view
|
|
790
|
+
while your shell still finds it — unsnooze looks that directory up at launch
|
|
791
|
+
time, `unsnooze doctor` prints what each agent resolves to, and
|
|
792
|
+
`UNSNOOZE_CODEX_BIN` (or `UNSNOOZE_CLAUDE_BIN`, …) pins a path; point it at an
|
|
793
|
+
`.exe`, since Node cannot launch a `.cmd` shim directly. And stored-password
|
|
794
|
+
fleet hosts still need Git-for-Windows or WSL `ssh`; native `ssh.exe` requires
|
|
795
|
+
an `.exe` askpass helper unsnooze does not yet ship.
|
|
767
796
|
|
|
768
797
|
WSL remains the richer option, because that is where the Unix multiplexers live
|
|
769
798
|
— and where the agent CLIs often already are:
|
package/bin/unsnooze.js
CHANGED
|
@@ -223,81 +223,107 @@ async function main() {
|
|
|
223
223
|
case 'daemon': {
|
|
224
224
|
// Persistent resumer + transcript watcher: detects and revives limit
|
|
225
225
|
// stops from GUI surfaces (VS Code extension, desktop apps) where no
|
|
226
|
-
// shell wrapper or multiplexer pane exists. Run via launchd/systemd
|
|
226
|
+
// shell wrapper or multiplexer pane exists. Run via launchd/systemd, on
|
|
227
|
+
// demand on native Windows (spawn.js ensureDaemon), or from a shell.
|
|
227
228
|
// Exit 0 on load failure: with launchd KeepAlive a crash here means an
|
|
228
229
|
// instant-respawn crash-loop for the whole upgrade window.
|
|
229
230
|
const resumerMod = await safeImport('../src/resumer.js');
|
|
230
231
|
const watcherMod = await safeImport('../src/watcher.js');
|
|
231
|
-
|
|
232
|
+
const spawnMod = await safeImport('../src/spawn.js');
|
|
233
|
+
if (!resumerMod || !watcherMod || !spawnMod) return 0;
|
|
232
234
|
const { runResumer } = resumerMod;
|
|
233
235
|
const { createWatcher } = watcherMod;
|
|
234
236
|
const controller = new AbortController();
|
|
235
237
|
process.on('SIGTERM', () => controller.abort());
|
|
236
238
|
process.on('SIGINT', () => controller.abort());
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const loggerMod = await safeImport('../src/logger.js');
|
|
257
|
-
const configMod = await safeImport('../src/config.js');
|
|
258
|
-
if (loggerMod && configMod) {
|
|
259
|
-
const { join } = await import('node:path');
|
|
260
|
-
loggerMod.copyTruncateIfLarge(join(configMod.STATE_DIR, 'daemon.log'));
|
|
239
|
+
// Native Windows has no launchd/systemd to keep this a single instance,
|
|
240
|
+
// and the wrappers and the hook can both start one at once. Claim the
|
|
241
|
+
// pidfile before doing anything else: a daemon that finds a live one
|
|
242
|
+
// already running exits here, and one that later finds itself replaced
|
|
243
|
+
// stands down. --on-demand (how ensureDaemon starts it) also ties the
|
|
244
|
+
// daemon to its marker, so uninstall ends it rather than sign-out.
|
|
245
|
+
const onDemand = rest.includes('--on-demand');
|
|
246
|
+
let releaseDaemon = null;
|
|
247
|
+
if (process.platform === 'win32') {
|
|
248
|
+
const wanted = onDemand ? () => spawnMod.onDemandWanted() : () => true;
|
|
249
|
+
if (!wanted()) return 0; // uninstalled between the start and now
|
|
250
|
+
releaseDaemon = spawnMod.claimDaemon({ onLost: () => controller.abort(), wanted });
|
|
251
|
+
if (!releaseDaemon) {
|
|
252
|
+
// Reaches a terminal only when a user ran `unsnooze daemon` by hand;
|
|
253
|
+
// an on-demand start has nowhere to print.
|
|
254
|
+
const pid = spawnMod.runningDaemonPid();
|
|
255
|
+
console.error(`unsnooze: the daemon is already running${pid ? ` (pid ${pid})` : ''} — not starting another`);
|
|
256
|
+
return 0;
|
|
257
|
+
}
|
|
261
258
|
}
|
|
262
|
-
//
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
259
|
+
// From the claim on, every way out gives it back.
|
|
260
|
+
try {
|
|
261
|
+
// Self-heal pre-1.12 autostart units: they lack PATH, so this daemon
|
|
262
|
+
// cannot find tmux and every revival dies. Healing rewrites the unit
|
|
263
|
+
// and reloads it — which intentionally kills THIS process; the
|
|
264
|
+
// supervisor restarts us under the fixed unit. One-time: healed units
|
|
265
|
+
// pass the check forever after.
|
|
266
|
+
const installMod = await safeImport('../src/install.js');
|
|
267
|
+
if (installMod?.healDaemonAutostart) {
|
|
268
|
+
try {
|
|
269
|
+
const healed = installMod.healDaemonAutostart();
|
|
270
|
+
if (healed) {
|
|
271
|
+
const lm = await safeImport('../src/logger.js');
|
|
272
|
+
lm?.log('daemon', `autostart unit lacked PATH — regenerated ${healed}, reloading (self-heal)`);
|
|
273
|
+
}
|
|
274
|
+
} catch { /* heal is best-effort — a broken unit must not block the daemon */ }
|
|
275
|
+
}
|
|
276
|
+
// daemon.log is launchd/systemd-captured stdout+stderr. launchd holds
|
|
277
|
+
// an open fd on it for our whole lifetime, so rotation must be
|
|
278
|
+
// copy-truncate: renaming would leave launchd appending to the renamed
|
|
279
|
+
// inode forever, with no fresh daemon.log until the next respawn.
|
|
280
|
+
const loggerMod = await safeImport('../src/logger.js');
|
|
281
|
+
const configMod = await safeImport('../src/config.js');
|
|
282
|
+
if (loggerMod && configMod) {
|
|
283
|
+
const { join } = await import('node:path');
|
|
284
|
+
loggerMod.copyTruncateIfLarge(join(configMod.STATE_DIR, 'daemon.log'));
|
|
285
|
+
}
|
|
286
|
+
// Version-skew guard: when npm swaps the package underneath us, exit
|
|
287
|
+
// cleanly so launchd/systemd restart the daemon on the fresh code.
|
|
288
|
+
// Interval stays at 15 minutes on purpose — a tighter loop raises the
|
|
289
|
+
// odds of restarting INTO a half-installed package, which is the
|
|
290
|
+
// crash-loop the unit throttles exist to survive.
|
|
291
|
+
// Exiting is only safe under a supervisor. `unsnooze daemon` run from a
|
|
292
|
+
// shell has nothing to restart it, so there it warns and keeps watching
|
|
293
|
+
// rather than ending GUI watching silently.
|
|
294
|
+
const updMod = await safeImport('../src/update-check.js');
|
|
295
|
+
const notifyMod = await safeImport('../src/notify.js');
|
|
296
|
+
if (updMod?.hasVersionSkew) {
|
|
297
|
+
let warnedSkew = false;
|
|
298
|
+
setInterval(() => {
|
|
299
|
+
const action = updMod.daemonSkewAction({
|
|
300
|
+
skewed: updMod.hasVersionSkew(),
|
|
301
|
+
// Unknown means assume supervised: that is how the daemon is
|
|
302
|
+
// installed for all but a handful of users, and it preserves the
|
|
303
|
+
// long-standing behavior when install.js could not be loaded.
|
|
304
|
+
supervised: installMod?.isSupervised ? installMod.isSupervised() : true,
|
|
305
|
+
alreadyWarned: warnedSkew,
|
|
306
|
+
});
|
|
307
|
+
if (action === 'restart') controller.abort();
|
|
308
|
+
else if (action === 'warn') {
|
|
309
|
+
warnedSkew = true;
|
|
310
|
+
const msg = 'package was upgraded on disk but this daemon is not supervised — '
|
|
311
|
+
+ 'still running the old code; '
|
|
312
|
+
+ (onDemand ? 'run `unsnooze install --daemon` to restart it' : 'restart it to pick up the new version');
|
|
313
|
+
loggerMod?.log('daemon', msg);
|
|
314
|
+
notifyMod?.notify('unsnooze needs a restart', msg, { priority: 4 });
|
|
315
|
+
}
|
|
316
|
+
}, 15 * 60_000).unref();
|
|
317
|
+
}
|
|
318
|
+
// Daily update check from the daemon: GUI-only users never run CLI
|
|
319
|
+
// commands, so this is what gets them the "new version" desktop toast.
|
|
320
|
+
const { spawnDetached } = spawnMod;
|
|
321
|
+
spawnDetached(['_update-check']);
|
|
322
|
+
setInterval(() => spawnDetached(['_update-check']), 24 * 3_600_000).unref();
|
|
323
|
+
return await runResumer({ persistent: true, watcher: createWatcher(), signal: controller.signal });
|
|
324
|
+
} finally {
|
|
325
|
+
releaseDaemon?.();
|
|
292
326
|
}
|
|
293
|
-
// Daily update check from the daemon: GUI-only users never run CLI
|
|
294
|
-
// commands, so this is what gets them the "new version" desktop toast.
|
|
295
|
-
const spawnMod = await safeImport('../src/spawn.js');
|
|
296
|
-
if (!spawnMod) return 0;
|
|
297
|
-
const { spawnDetached } = spawnMod;
|
|
298
|
-
spawnDetached(['_update-check']);
|
|
299
|
-
setInterval(() => spawnDetached(['_update-check']), 24 * 3_600_000).unref();
|
|
300
|
-
return runResumer({ persistent: true, watcher: createWatcher(), signal: controller.signal });
|
|
301
327
|
}
|
|
302
328
|
case 'help':
|
|
303
329
|
case '-h':
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unsnooze",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.2",
|
|
4
4
|
"description": "Automatically resume Claude Code, Codex CLI, Grok, Qwen Code, Kimi CLI, OpenCode, Antigravity, and Cursor CLI 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",
|