unsnooze 1.0.0 → 1.1.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 +25 -0
- package/README.md +2 -0
- package/bin/unsnooze.js +6 -2
- package/package.json +1 -1
- package/src/cli.js +4 -2
- package/src/install.js +2 -1
- package/src/resumer.js +4 -2
- package/src/settings.js +25 -4
- package/src/wizard.js +33 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.0 — 2026-07-10
|
|
4
|
+
|
|
5
|
+
### Per-agent resume messages
|
|
6
|
+
|
|
7
|
+
- **`resumeMessages.claude` / `.codex` / `.grok`**: optional per-agent override
|
|
8
|
+
of the global `resumeMessage` — `unsnooze config set resumeMessages.codex
|
|
9
|
+
"..."` or `UNSNOOZE_RESUME_MESSAGE_CODEX`. Empty means "use the global
|
|
10
|
+
message"; clear with `unsnooze config set resumeMessages.codex ""`.
|
|
11
|
+
Specificity beats source: a per-agent file value outranks a global env var.
|
|
12
|
+
- **Setup wizard** asks for per-agent messages, prefills every message prompt
|
|
13
|
+
from the existing config, and merges over the config file — re-runs no
|
|
14
|
+
longer clobber values set via `unsnooze config`.
|
|
15
|
+
- Blank or whitespace-only messages are never sent: resolution falls through
|
|
16
|
+
to the global message, then the built-in default.
|
|
17
|
+
|
|
18
|
+
### CLI & fixes
|
|
19
|
+
|
|
20
|
+
- **`-h` / `--help`** now print the unsnooze help (previously only
|
|
21
|
+
`unsnooze help`), and the usage documents every command.
|
|
22
|
+
- **Install**: `unsnooze setup` / `install` no longer crashes with ENOENT on
|
|
23
|
+
machines without a `~/.claude/` directory.
|
|
24
|
+
- `unsnooze config set <key> ""` is a valid way to clear string overrides, and
|
|
25
|
+
a config file holding non-object JSON is treated as empty instead of
|
|
26
|
+
corrupting later writes.
|
|
27
|
+
|
|
3
28
|
## 1.0.0 — 2026-07-10
|
|
4
29
|
|
|
5
30
|
First public release (previously the private `claude-session-guard`/`csg`).
|
package/README.md
CHANGED
|
@@ -103,6 +103,7 @@ unsnooze config set <k> <v> # e.g. autoResume off
|
|
|
103
103
|
unsnooze logs [-f] # what unsnooze has been doing
|
|
104
104
|
unsnooze report [agent] # capture a pane to report an undetected banner
|
|
105
105
|
unsnooze uninstall [--purge] # remove wrappers + hooks (+ state with --purge)
|
|
106
|
+
unsnooze help # full command list (also -h / --help)
|
|
106
107
|
```
|
|
107
108
|
|
|
108
109
|
## Settings
|
|
@@ -116,6 +117,7 @@ unsnooze uninstall [--purge] # remove wrappers + hooks (+ state with --purge)
|
|
|
116
117
|
| `menuAutoAnswer` | `true` | May unsnooze answer Claude's limit menu (send keys in your pane)? Off = watch-only. |
|
|
117
118
|
| `notifications` | `true` | Desktop notification on limit detected / session resumed / gave up. |
|
|
118
119
|
| `resumeMessage` | *"Continue where you left off…"* | The message sent to wake a session. |
|
|
120
|
+
| `resumeMessages.claude` / `.codex` / `.grok` | `""` | Per-agent override of `resumeMessage`. Empty = use the global message; clear one with `unsnooze config set resumeMessages.claude ""`. |
|
|
119
121
|
| `agents.claude` / `agents.codex` / `agents.grok` | `true` / `true` / `false` | Which CLIs are guarded. |
|
|
120
122
|
|
|
121
123
|
Every setting also has a `UNSNOOZE_*` env override (see `src/settings.js`), and
|
package/bin/unsnooze.js
CHANGED
|
@@ -61,6 +61,8 @@ async function main() {
|
|
|
61
61
|
return runResumer();
|
|
62
62
|
}
|
|
63
63
|
case 'help':
|
|
64
|
+
case '-h':
|
|
65
|
+
case '--help':
|
|
64
66
|
case '--help-unsnooze': {
|
|
65
67
|
console.log(`unsnooze — wakes every limit-stopped AI coding session when the limit resets
|
|
66
68
|
|
|
@@ -71,11 +73,13 @@ Usage:
|
|
|
71
73
|
unsnooze resume-now [id|--all] resume stopped session(s) immediately
|
|
72
74
|
unsnooze cancel [id|--all] stop tracking session(s)
|
|
73
75
|
unsnooze logs [-f] show (or follow) the unsnooze log
|
|
74
|
-
unsnooze config [list|get|set] view or change settings (toggles,
|
|
76
|
+
unsnooze config [list|get|set] view or change settings (toggles, global +
|
|
77
|
+
per-agent resume messages)
|
|
75
78
|
unsnooze setup interactive setup wizard (agents + toggles)
|
|
76
79
|
unsnooze install [--yes] wire up shell wrappers + hooks (non-interactive)
|
|
77
80
|
unsnooze uninstall [--purge] remove wrappers + hooks (and state with --purge)
|
|
78
|
-
unsnooze report [agent] [pane] capture a pane to report an undetected banner
|
|
81
|
+
unsnooze report [agent] [pane] capture a pane to report an undetected banner
|
|
82
|
+
unsnooze help show this help (also -h / --help)`);
|
|
79
83
|
return 0;
|
|
80
84
|
}
|
|
81
85
|
default: {
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -89,7 +89,7 @@ export function cmdConfig(rest) {
|
|
|
89
89
|
const listed = listConfig();
|
|
90
90
|
console.log(`unsnooze settings (${CONFIG_FILE()}):\n`);
|
|
91
91
|
for (const [k, v] of Object.entries(listed)) {
|
|
92
|
-
console.log(` ${k.padEnd(
|
|
92
|
+
console.log(` ${k.padEnd(24)} ${JSON.stringify(v)}`);
|
|
93
93
|
}
|
|
94
94
|
return 0;
|
|
95
95
|
}
|
|
@@ -100,7 +100,9 @@ export function cmdConfig(rest) {
|
|
|
100
100
|
}
|
|
101
101
|
if (action === 'set') {
|
|
102
102
|
const value = valueParts.join(' ');
|
|
103
|
-
|
|
103
|
+
// An explicit "" is a valid value (clears string overrides); only a
|
|
104
|
+
// genuinely missing value is a usage error.
|
|
105
|
+
if (!key || valueParts.length === 0) { console.error('unsnooze config set <key> <value>'); return 2; }
|
|
104
106
|
const applied = setConfigValue(key, value);
|
|
105
107
|
console.log(`unsnooze: ${key} = ${JSON.stringify(applied)}`);
|
|
106
108
|
return 0;
|
package/src/install.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// - ~/.grok/hooks/unsnooze.json when the grok agent is enabled
|
|
9
9
|
// --settings <path> / --zshrc <path> override targets (used by tests).
|
|
10
10
|
|
|
11
|
-
import { readFileSync, writeFileSync, renameSync, existsSync, copyFileSync, rmSync } from 'node:fs';
|
|
11
|
+
import { readFileSync, writeFileSync, renameSync, existsSync, copyFileSync, rmSync, mkdirSync } from 'node:fs';
|
|
12
12
|
import { homedir } from 'node:os';
|
|
13
13
|
import { join, dirname } from 'node:path';
|
|
14
14
|
import { CLAUDE_SETTINGS, STATE_DIR } from './config.js';
|
|
@@ -38,6 +38,7 @@ function parseArgs(rest) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
function atomicWrite(path, content) {
|
|
41
|
+
mkdirSync(dirname(path), { recursive: true }); // e.g. ~/.claude may not exist yet
|
|
41
42
|
const tmp = join(dirname(path), `.${Date.now()}.tmp`);
|
|
42
43
|
writeFileSync(tmp, content);
|
|
43
44
|
renameSync(tmp, path);
|
package/src/resumer.js
CHANGED
|
@@ -16,7 +16,7 @@ import { detectLimit, isBusy } from './patterns.js';
|
|
|
16
16
|
import { getAgent } from './agents/index.js';
|
|
17
17
|
import { parseResetTime, resetAtMs } from './time-parser.js';
|
|
18
18
|
import { readState, updateState, setStatus, dueSessions, activeStopped } from './state.js';
|
|
19
|
-
import { getConfig } from './settings.js';
|
|
19
|
+
import { getConfig, resolveResumeMessage } from './settings.js';
|
|
20
20
|
import { notify } from './notify.js';
|
|
21
21
|
import { UNSNOOZE_BIN } from './spawn.js';
|
|
22
22
|
import { makeLogger } from './logger.js';
|
|
@@ -68,9 +68,11 @@ function selfCommand() {
|
|
|
68
68
|
|
|
69
69
|
// Decide how to act on one due record. Pure-ish; tmux injectable.
|
|
70
70
|
// Returns: 'sent' | 'reopened' | 'deferred' | 'skip' | 'failed'
|
|
71
|
-
export async function dispatchOne(rec, { tmux = realTmux, resumeMessage
|
|
71
|
+
export async function dispatchOne(rec, { tmux = realTmux, resumeMessage, selfCmd = selfCommand() } = {}) {
|
|
72
72
|
const key = rec.key;
|
|
73
73
|
const agent = getAgent(rec.agent);
|
|
74
|
+
// Explicit option wins; otherwise the agent's own message (or the global).
|
|
75
|
+
resumeMessage = resumeMessage ?? resolveResumeMessage(agent.id);
|
|
74
76
|
|
|
75
77
|
// Live-pane path: only if the pane still exists AND the agent CLI is its
|
|
76
78
|
// foreground command (pane ids get recycled — never inject into a random
|
package/src/settings.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// User-facing settings: ~/.unsnooze/config.json, managed by `unsnooze config`
|
|
2
|
-
// and the setup wizard. Precedence: env var > config file > default —
|
|
3
|
-
// stays the power-user/test escape hatch, the file is what the wizard
|
|
2
|
+
// and the setup wizard. Precedence per key: env var > config file > default —
|
|
3
|
+
// env stays the power-user/test escape hatch, the file is what the wizard
|
|
4
|
+
// writes. One cross-key exception: the resume message resolves by specificity
|
|
5
|
+
// first, so a per-agent resumeMessages.<id> value (from either source) beats
|
|
6
|
+
// the global resumeMessage even when the global came from an env var (see
|
|
7
|
+
// resolveResumeMessage).
|
|
4
8
|
|
|
5
9
|
import { readFileSync, writeFileSync, renameSync, mkdirSync } from 'node:fs';
|
|
6
10
|
import { join, dirname } from 'node:path';
|
|
@@ -13,6 +17,7 @@ export const DEFAULTS = {
|
|
|
13
17
|
menuAutoAnswer: true, // may unsnooze drive Claude's limit menu (send keys)?
|
|
14
18
|
notifications: true, // desktop notifications on detect/resume
|
|
15
19
|
resumeMessage: 'Continue where you left off. The session was interrupted by a usage limit which has now reset — pick up the task you were working on and finish it.',
|
|
20
|
+
resumeMessages: { claude: '', codex: '', grok: '' }, // per-agent override; '' = use resumeMessage
|
|
16
21
|
agents: { claude: true, codex: true, grok: false }, // grok is experimental
|
|
17
22
|
};
|
|
18
23
|
|
|
@@ -22,6 +27,9 @@ const ENV_NAMES = {
|
|
|
22
27
|
menuAutoAnswer: 'UNSNOOZE_MENU_AUTO_ANSWER',
|
|
23
28
|
notifications: 'UNSNOOZE_NOTIFICATIONS',
|
|
24
29
|
resumeMessage: 'UNSNOOZE_RESUME_MESSAGE',
|
|
30
|
+
'resumeMessages.claude': 'UNSNOOZE_RESUME_MESSAGE_CLAUDE',
|
|
31
|
+
'resumeMessages.codex': 'UNSNOOZE_RESUME_MESSAGE_CODEX',
|
|
32
|
+
'resumeMessages.grok': 'UNSNOOZE_RESUME_MESSAGE_GROK',
|
|
25
33
|
'agents.claude': 'UNSNOOZE_AGENT_CLAUDE',
|
|
26
34
|
'agents.codex': 'UNSNOOZE_AGENT_CODEX',
|
|
27
35
|
'agents.grok': 'UNSNOOZE_AGENT_GROK',
|
|
@@ -35,9 +43,11 @@ function parseBool(raw) {
|
|
|
35
43
|
return null;
|
|
36
44
|
}
|
|
37
45
|
|
|
38
|
-
function readFileConfig() {
|
|
46
|
+
export function readFileConfig() {
|
|
39
47
|
try {
|
|
40
|
-
|
|
48
|
+
const parsed = JSON.parse(readFileSync(CONFIG_FILE(), 'utf-8'));
|
|
49
|
+
// Wrong-typed but valid JSON (a bare string/array) counts as corrupt too.
|
|
50
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
41
51
|
} catch {
|
|
42
52
|
return {}; // missing or corrupt file → defaults (never crash the hook path)
|
|
43
53
|
}
|
|
@@ -63,6 +73,17 @@ export function getConfig(key) {
|
|
|
63
73
|
return fromFile !== undefined ? fromFile : def;
|
|
64
74
|
}
|
|
65
75
|
|
|
76
|
+
// The message to send a given agent: its resumeMessages.<id> override when
|
|
77
|
+
// set, else the global resumeMessage. Unknown agent ids fall back to global;
|
|
78
|
+
// blank values (empty or whitespace-only) fall through to the next level so a
|
|
79
|
+
// blank message is never sent.
|
|
80
|
+
export function resolveResumeMessage(agentId) {
|
|
81
|
+
const key = `resumeMessages.${agentId}`;
|
|
82
|
+
const set = v => (typeof v === 'string' && v.trim() ? v : '');
|
|
83
|
+
const perAgent = agentId && KNOWN_KEYS.includes(key) ? getConfig(key) : '';
|
|
84
|
+
return set(perAgent) || set(getConfig('resumeMessage')) || DEFAULTS.resumeMessage;
|
|
85
|
+
}
|
|
86
|
+
|
|
66
87
|
export function setConfigValue(key, rawValue) {
|
|
67
88
|
if (!KNOWN_KEYS.includes(key)) throw new Error(`unsnooze: unknown setting "${key}" (known: ${KNOWN_KEYS.join(', ')})`);
|
|
68
89
|
const def = dig(DEFAULTS, key);
|
package/src/wizard.js
CHANGED
|
@@ -6,7 +6,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
6
6
|
import * as p from '@clack/prompts';
|
|
7
7
|
import { listAgents } from './agents/index.js';
|
|
8
8
|
import { isCommunityGrokCli } from './agents/grok.js';
|
|
9
|
-
import { DEFAULTS, writeConfig } from './settings.js';
|
|
9
|
+
import { DEFAULTS, writeConfig, readFileConfig } from './settings.js';
|
|
10
10
|
|
|
11
11
|
export function detectInstalledAgents({ which = defaultWhich } = {}) {
|
|
12
12
|
return listAgents().map(agent => ({ agent, installed: which(agent.bin) }));
|
|
@@ -69,25 +69,55 @@ export async function runWizard() {
|
|
|
69
69
|
});
|
|
70
70
|
if (p.isCancel(notifications)) return cancelled();
|
|
71
71
|
|
|
72
|
+
// Seed message prompts from the existing config so a setup re-run shows —
|
|
73
|
+
// and keeps — values previously set here or via `unsnooze config`.
|
|
74
|
+
const existing = readFileConfig();
|
|
75
|
+
const existingMsgs = existing.resumeMessages
|
|
76
|
+
&& typeof existing.resumeMessages === 'object' && !Array.isArray(existing.resumeMessages)
|
|
77
|
+
? existing.resumeMessages : {};
|
|
78
|
+
|
|
72
79
|
const customizeMsg = await p.confirm({
|
|
73
80
|
message: 'Customize the message sent to resume a session?',
|
|
74
81
|
initialValue: false,
|
|
75
82
|
});
|
|
76
83
|
if (p.isCancel(customizeMsg)) return cancelled();
|
|
77
84
|
|
|
78
|
-
let resumeMessage =
|
|
85
|
+
let resumeMessage = typeof existing.resumeMessage === 'string' && existing.resumeMessage.trim()
|
|
86
|
+
? existing.resumeMessage : DEFAULTS.resumeMessage;
|
|
79
87
|
if (customizeMsg) {
|
|
80
88
|
const msg = await p.text({
|
|
81
89
|
message: 'Resume message:',
|
|
82
|
-
initialValue:
|
|
90
|
+
initialValue: resumeMessage,
|
|
83
91
|
validate: v => (v.trim() ? undefined : 'message cannot be empty'),
|
|
84
92
|
});
|
|
85
93
|
if (p.isCancel(msg)) return cancelled();
|
|
86
94
|
resumeMessage = msg;
|
|
87
95
|
}
|
|
88
96
|
|
|
97
|
+
const customizePerAgent = await p.confirm({
|
|
98
|
+
message: 'Set a different resume message for specific agents?',
|
|
99
|
+
initialValue: false,
|
|
100
|
+
});
|
|
101
|
+
if (p.isCancel(customizePerAgent)) return cancelled();
|
|
102
|
+
|
|
103
|
+
const resumeMessages = {};
|
|
104
|
+
if (customizePerAgent) {
|
|
105
|
+
for (const agent of listAgents().filter(a => agents.includes(a.id))) {
|
|
106
|
+
const msg = await p.text({
|
|
107
|
+
message: `Message for ${agent.name} (leave empty to use the global message):`,
|
|
108
|
+
initialValue: typeof existingMsgs[agent.id] === 'string' ? existingMsgs[agent.id] : '',
|
|
109
|
+
});
|
|
110
|
+
if (p.isCancel(msg)) return cancelled();
|
|
111
|
+
resumeMessages[agent.id] = typeof msg === 'string' && msg.trim() ? msg : '';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Merge over the existing file so a setup re-run keeps settings the wizard
|
|
116
|
+
// doesn't ask about (e.g. per-agent messages set via `unsnooze config`).
|
|
89
117
|
writeConfig({
|
|
118
|
+
...existing,
|
|
90
119
|
autoResume, menuAutoAnswer, notifications, resumeMessage,
|
|
120
|
+
resumeMessages: { ...existingMsgs, ...resumeMessages },
|
|
91
121
|
agents: Object.fromEntries(listAgents().map(a => [a.id, agents.includes(a.id)])),
|
|
92
122
|
});
|
|
93
123
|
|