@trygocode/notify 0.1.4 → 0.1.5

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/README.md CHANGED
@@ -109,7 +109,7 @@ or staging GoCode server.
109
109
 
110
110
  | Trigger | Mechanism | Fires when |
111
111
  |---|---|---|
112
- | **(A) Runtime hook** | Cursor `stop` / Claude Code `Stop`+`Notification`+`SubagentStop` / OpenCode `session.idle` plugin | Agent finishes a turn, goes idle, or errors — **automatic, the killer feature** |
112
+ | **(A) Runtime hook** | Cursor `stop` / Claude Code `Stop`+`Notification` / OpenCode `session.idle` plugin | The MAIN agent finishes a turn, goes idle, or errors — **automatic, the killer feature** |
113
113
  | **(B) MCP tool** | `gocode_notify` tool the agent calls | You *explicitly* ask "ping me when X is done" mid-task |
114
114
  | **(C) Loop shell hook** | one line in your loop's completion/halt path | A Ralph/Homer loop reaches `completed` / `halted` |
115
115
 
@@ -2,12 +2,16 @@
2
2
  // Claude Code. It does three things, all idempotently and without clobbering the
3
3
  // user's existing config:
4
4
  //
5
- // 1. MERGE three fire-and-forget hooks into `~/.claude/settings.json`:
6
- // Stop → "finished" (a turn completed)
5
+ // 1. MERGE two fire-and-forget hooks into `~/.claude/settings.json`:
6
+ // Stop → "finished" (the MAIN turn completed)
7
7
  // Notification → "awaiting_input" (the agent needs the user)
8
- // SubagentStop → "finished" (a subagent completed)
9
- // Each hook shells out to `gocode-notify send … || true` so a failed push
8
+ // Each hook shells out to `gocode-notify … || true` so a failed push
10
9
  // NEVER blocks the agent's turn (PRD §4.4, §5.3).
10
+ // NOTE: we deliberately do NOT register a `SubagentStop` hook — a notify
11
+ // should fire only when the MAIN agent finishes its turn, not on every
12
+ // internal subagent completion (that produced noisy "Subagent done" pings).
13
+ // Any prior `SubagentStop` hook of ours is actively stripped on re-setup
14
+ // (see {@link SUBAGENT_STOP_EVENT} + {@link mergeHooks}).
11
15
  // 2. MERGE an `mcpServers` entry pointing at `npx -y @trygocode/notify mcp`.
12
16
  // 3. WRITE the on-demand skill to `~/.claude/skills/gocode-notify/SKILL.md`
13
17
  // (the anti-double-ping rule, PRD §5.5).
@@ -46,14 +50,33 @@ export const MCP_SERVER_ENTRY = {
46
50
  * plain `finished` ping — exactly one notification per turn. The pre-T-C6 form
47
51
  * was `send --kind finished --source claude_code …`; both old and new commands
48
52
  * contain the {@link HOOK_MARKERS} tokens, so old installs upgrade idempotently
49
- * and uninstall cleanly. `Notification`/`SubagentStop` stay on the plain `send`
50
- * (they are not turn-completion events and never auto-push).
53
+ * and uninstall cleanly. `Notification` stays on the plain `send` (it is not a
54
+ * turn-completion event and never auto-pushes).
55
+ *
56
+ * We intentionally register ONLY `Stop` + `Notification`. `SubagentStop` is NOT
57
+ * here on purpose — see {@link SUBAGENT_STOP_EVENT}.
51
58
  */
52
59
  export const CLAUDE_HOOK_COMMANDS = {
53
60
  Stop: 'npx -y @trygocode/notify on-stop --source claude_code --dedupe-key "$CLAUDE_SESSION_ID-stop" || true',
54
61
  Notification: 'npx -y @trygocode/notify send --kind awaiting_input --source claude_code --title "Agent needs you" --dedupe-key "$CLAUDE_SESSION_ID-notify" || true',
55
- SubagentStop: 'npx -y @trygocode/notify send --kind finished --source claude_code --title "Subagent done" --dedupe-key "$CLAUDE_SESSION_ID-subagent" || true',
56
62
  };
63
+ /**
64
+ * The Claude event we DELIBERATELY do not register and actively scrub on every
65
+ * setup. We used to fire a `finished` push on `SubagentStop` ("Subagent done"),
66
+ * but a notification should only land when the MAIN agent finishes its turn —
67
+ * not for each internal subagent. `mergeHooks` strips any prior copy of OUR
68
+ * command from this event so existing installs are migrated clean on re-setup,
69
+ * and `uninstallClaudeConfig` also covers it (the uninstall strip is event-set
70
+ * agnostic). Kept as a named constant so the scrub list is single-sourced.
71
+ */
72
+ export const SUBAGENT_STOP_EVENT = "SubagentStop";
73
+ /**
74
+ * Events we make sure carry NONE of our hooks after a merge — events we no
75
+ * longer install but may have written in a prior version. `mergeHooks` walks
76
+ * this set and strips our commands from each, so re-running `setup` migrates an
77
+ * old install (with a stale "Subagent done" hook) to the current clean shape.
78
+ */
79
+ const DEPRECATED_HOOK_EVENTS = [SUBAGENT_STOP_EVENT];
57
80
  /**
58
81
  * Substrings that together identify a hook command as OURS. Used for idempotent
59
82
  * merge (replace, don't duplicate) and for surgical uninstall (remove exactly
@@ -176,12 +199,28 @@ function ourHookGroup(command) {
176
199
  */
177
200
  function mergeHooks(settings) {
178
201
  const hooks = isRecord(settings.hooks) ? settings.hooks : {};
202
+ // Install (or refresh) the events we DO want.
179
203
  for (const [event, command] of Object.entries(CLAUDE_HOOK_COMMANDS)) {
180
204
  const existing = Array.isArray(hooks[event]) ? hooks[event] : [];
181
205
  const preserved = stripOurHooks(existing).groups;
182
206
  preserved.push(ourHookGroup(command));
183
207
  hooks[event] = preserved;
184
208
  }
209
+ // Migrate old installs: strip OUR command from any event we no longer install
210
+ // (e.g. the retired `SubagentStop` "Subagent done" ping). We only remove our
211
+ // own entries — a user's own hook on the same event is preserved. An event
212
+ // left with no groups is deleted so we don't leave an empty array behind.
213
+ for (const event of DEPRECATED_HOOK_EVENTS) {
214
+ if (!Array.isArray(hooks[event]))
215
+ continue;
216
+ const cleaned = stripOurHooks(hooks[event]).groups;
217
+ if (cleaned.length === 0) {
218
+ delete hooks[event];
219
+ }
220
+ else {
221
+ hooks[event] = cleaned;
222
+ }
223
+ }
185
224
  settings.hooks = hooks;
186
225
  }
187
226
  /** Merge our MCP server entry into `settings.mcpServers`. Mutates in place. */
@@ -247,7 +286,14 @@ export async function uninstallClaudeConfig(opts) {
247
286
  let changed = false;
248
287
  if (isRecord(settings.hooks)) {
249
288
  const hooks = settings.hooks;
250
- for (const event of Object.keys(CLAUDE_HOOK_COMMANDS)) {
289
+ // Scrub our hooks from the events we install AND from deprecated events
290
+ // we no longer install but may have written in a prior version (e.g. the
291
+ // retired `SubagentStop` ping) — otherwise an uninstall would orphan it.
292
+ const eventsToScrub = [
293
+ ...Object.keys(CLAUDE_HOOK_COMMANDS),
294
+ ...DEPRECATED_HOOK_EVENTS,
295
+ ];
296
+ for (const event of eventsToScrub) {
251
297
  if (!Array.isArray(hooks[event]))
252
298
  continue;
253
299
  const { groups: kept, removed } = stripOurHooks(hooks[event]);
@@ -8,7 +8,7 @@
8
8
  // 1. The frontmatter block (Claude SKILL.md `name:`+`description:` vs Cursor
9
9
  // rule `description:`+`alwaysApply:`).
10
10
  // 2. The parenthetical naming WHICH runtime hook owns the automatic pings
11
- // (Claude fires `Stop`+`Notification`+`SubagentStop`; Cursor fires `stop`).
11
+ // (Claude fires `Stop`+`Notification`; Cursor fires `stop`).
12
12
  //
13
13
  // Centralising the prose here means the anti-double-ping invariant ("the hooks
14
14
  // own the automatic done/idle/error pings — only call the MCP tool when the user
@@ -58,7 +58,7 @@ name: gocode-notify
58
58
  description: On-demand phone notifications via the GoCode app. Use ONLY when the user EXPLICITLY asks to be pinged/notified when something finishes (e.g. "text me when the build is done").
59
59
  ---`;
60
60
  /** How Claude Code's automatic-ping hooks are named in the double-notify warning. */
61
- export const CLAUDE_HOOK_DESCRIPTION = "Claude Code `Stop` + `Notification` +\n`SubagentStop`";
61
+ export const CLAUDE_HOOK_DESCRIPTION = "Claude Code `Stop` + `Notification`";
62
62
  /**
63
63
  * Cursor rule frontmatter (PRD §5.5). `alwaysApply: false` + a trigger-y
64
64
  * description so Cursor surfaces it on "notify me / ping me / let me know when".
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trygocode/notify",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Free phone notifications for any coding agent (Cursor, Claude Code, OpenCode, Ralph/Homer) via the GoCode app.",
5
5
  "license": "MIT",
6
6
  "type": "module",