unsnooze 1.2.0 → 1.3.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,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.3.0 — 2026-07-11
4
+
5
+ - **Per-session wake messages**: `unsnooze message <id|--all> "<text>"` sets a
6
+ custom resume message for specific tracked sessions (`--clear` reverts).
7
+ Precedence: per-session → per-agent (`resumeMessages.<id>`) → global
8
+ `resumeMessage`. Applies on both wake paths — typed into a live pane, or
9
+ carried in argv for `codex resume` — and sessions with a custom message
10
+ show a `msg: "…"` marker in `unsnooze status`.
11
+
3
12
  ## 1.2.0 — 2026-07-10
4
13
 
5
14
  ### GUI surfaces: VS Code extension, desktop apps
package/README.md CHANGED
@@ -128,6 +128,7 @@ claude / codex / grok # normal usage — wrapped automatically
128
128
  unsnooze status # tracked sessions + reset countdowns
129
129
  unsnooze resume-now [id|--all] # don't wait for the reset time
130
130
  unsnooze cancel [id|--all] # stop tracking a session
131
+ unsnooze message <id> "text" # per-session wake message (--clear to reset)
131
132
  unsnooze config list # settings (see below)
132
133
  unsnooze config set <k> <v> # e.g. autoResume off
133
134
  unsnooze logs [-f] # what unsnooze has been doing
@@ -149,7 +150,7 @@ unsnooze help # full command list (also -h / --help)
149
150
  | `menuAutoAnswer` | `true` | May unsnooze answer Claude's limit menu (send keys in your pane)? Off = watch-only. |
150
151
  | `notifications` | `true` | Desktop notification on limit detected / session resumed / gave up. |
151
152
  | `guiWatch` | `true` | May the daemon watch session files for GUI-surface stops (VS Code extension, desktop apps)? Needs the daemon running (`unsnooze install --daemon`). |
152
- | `resumeMessage` | *"Continue where you left off…"* | The message sent to wake a session. |
153
+ | `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`. |
153
154
  | `resumeMessages.claude` / `.codex` / `.grok` | `""` | Per-agent override of `resumeMessage`. Empty = use the global message; clear one with `unsnooze config set resumeMessages.claude ""`. |
154
155
  | `agents.claude` / `agents.codex` / `agents.grok` | `true` / `true` / `false` | Which CLIs are guarded. |
155
156
 
package/bin/unsnooze.js CHANGED
@@ -30,6 +30,10 @@ async function main() {
30
30
  const { cmdReport } = await import('../src/report.js');
31
31
  return cmdReport(rest);
32
32
  }
33
+ case 'message': {
34
+ const { cmdMessage } = await import('../src/cli.js');
35
+ return cmdMessage(rest);
36
+ }
33
37
  case 'config': {
34
38
  const { cmdConfig } = await import('../src/cli.js');
35
39
  return cmdConfig(rest);
@@ -83,6 +87,7 @@ Usage:
83
87
  unsnooze status list tracked sessions + reset countdowns
84
88
  unsnooze resume-now [id|--all] resume stopped session(s) immediately
85
89
  unsnooze cancel [id|--all] stop tracking session(s)
90
+ unsnooze message <id|--all> <t> set a per-session wake message (--clear to reset)
86
91
  unsnooze logs [-f] show (or follow) the unsnooze log
87
92
  unsnooze daemon persistent watcher for GUI sessions (VS Code
88
93
  extension, desktop apps) — no tmux needed to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unsnooze",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Wakes every limit-stopped AI coding session (Claude Code, Codex CLI, Grok) in tmux the moment the usage limit resets.",
5
5
  "keywords": [
6
6
  "claude-code",
package/src/cli.js CHANGED
@@ -30,16 +30,19 @@ export function cmdStatus() {
30
30
  const id = s.sessionId ? s.sessionId.slice(0, 8) : '(no id)';
31
31
  const reset = s.resetAt ? `${new Date(s.resetAt).toLocaleString()} (${fmtCountdown(s.resetAt - now)})` : '?';
32
32
  const origin = s.origin ?? (s.pane ? 'cli' : '?');
33
+ const msg = s.resumeMessage
34
+ ? ` · msg: "${s.resumeMessage.length > 44 ? s.resumeMessage.slice(0, 44) + '…' : s.resumeMessage}"`
35
+ : '';
33
36
  console.log(` [${s.status.toUpperCase().padEnd(9)}] ${id} ${(s.agent || 'claude').padEnd(6)} ${s.limitType?.padEnd(7) ?? 'unknown'} ${s.cwd}`);
34
- console.log(` pane ${s.pane ?? '-'} · via ${origin} · resets ${reset} · attempts ${s.attempts ?? 0}/${MAX_RESUME_ATTEMPTS}${s.lastError ? ` · last error: ${s.lastError}` : ''}`);
37
+ console.log(` pane ${s.pane ?? '-'} · via ${origin} · resets ${reset} · attempts ${s.attempts ?? 0}/${MAX_RESUME_ATTEMPTS}${s.lastError ? ` · last error: ${s.lastError}` : ''}${msg}`);
35
38
  }
36
39
  return 0;
37
40
  }
38
41
 
39
- function selectKeys(state, idOrAll) {
40
- const stopped = Object.values(state.sessions).filter(s => s.status === 'stopped');
41
- if (idOrAll === '--all' || idOrAll === undefined) return stopped.map(s => s.key);
42
- const match = stopped.filter(s => s.key.startsWith(idOrAll) || (s.sessionId || '').startsWith(idOrAll));
42
+ function selectKeys(state, idOrAll, statuses = ['stopped']) {
43
+ const candidates = Object.values(state.sessions).filter(s => statuses.includes(s.status));
44
+ if (idOrAll === '--all' || idOrAll === undefined) return candidates.map(s => s.key);
45
+ const match = candidates.filter(s => s.key.startsWith(idOrAll) || (s.sessionId || '').startsWith(idOrAll));
43
46
  return match.map(s => s.key);
44
47
  }
45
48
 
@@ -82,6 +85,33 @@ export function cmdLogs(follow) {
82
85
  return 0;
83
86
  }
84
87
 
88
+ // `unsnooze message <id|--all> <text...>` — set (or --clear) the wake message
89
+ // for specific sessions; the resumer prefers it over the global setting.
90
+ export function cmdMessage(rest) {
91
+ const [idOrAll, ...textParts] = rest;
92
+ const clear = textParts[0] === '--clear';
93
+ const text = textParts.join(' ').trim();
94
+ if (!idOrAll || (!clear && !text)) {
95
+ console.error('unsnooze message <id|--all> <text...> (or --clear to revert to the default)');
96
+ return 2;
97
+ }
98
+ const state = readState();
99
+ // stopped OR resuming: editing the message before a retry is legitimate.
100
+ const keys = selectKeys(state, idOrAll, ['stopped', 'resuming']);
101
+ if (keys.length === 0) { console.log('unsnooze: no matching active sessions.'); return 1; }
102
+ updateState(s => {
103
+ for (const key of keys) {
104
+ if (!s.sessions[key]) continue;
105
+ if (clear) delete s.sessions[key].resumeMessage;
106
+ else s.sessions[key].resumeMessage = text;
107
+ }
108
+ });
109
+ console.log(clear
110
+ ? `unsnooze: cleared custom message on ${keys.length} session(s) (global default applies).`
111
+ : `unsnooze: ${keys.length} session(s) will wake with: "${text}"`);
112
+ return 0;
113
+ }
114
+
85
115
  // `unsnooze config list | get <key> | set <key> <value>`
86
116
  export function cmdConfig(rest) {
87
117
  const [action, key, ...valueParts] = rest;
package/src/resumer.js CHANGED
@@ -72,8 +72,10 @@ function selfCommand() {
72
72
  export async function dispatchOne(rec, { tmux = realTmux, resumeMessage, selfCmd = selfCommand() } = {}) {
73
73
  const key = rec.key;
74
74
  const agent = getAgent(rec.agent);
75
- // Explicit option wins; otherwise the agent's own message (or the global).
76
- resumeMessage = resumeMessage ?? resolveResumeMessage(agent.id);
75
+ // Wake-message precedence: per-session (`unsnooze message <id> "..."`)
76
+ // explicit option per-agent (`resumeMessages.<id>`) → global. Applies to
77
+ // both the live-pane sendText and the argv reopen path.
78
+ resumeMessage = rec.resumeMessage ?? resumeMessage ?? resolveResumeMessage(agent.id);
77
79
 
78
80
  // Live-pane path: only if the pane still exists AND the agent CLI is its
79
81
  // foreground command (pane ids get recycled — never inject into a random