@trygocode/notify 0.3.3 → 0.3.4

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.
@@ -59,24 +59,65 @@ export function parseCursorStopStatus(stdin) {
59
59
  return undefined;
60
60
  }
61
61
  /**
62
- * Map a Cursor `stop` status to the appropriate {@link NotifyKind} (T-CUR1 / PRD §8.5):
63
- * - `completed` `finished` (agent turned cleanly)
64
- * - `aborted` → `awaiting_input` (agent yielded back to the human)
65
- * - `error` → `error` (agent hit an error)
66
- * - `undefined` → `finished` (back-compat: absent/unrecognised)
62
+ * Map a Cursor `stop` status to the appropriate {@link NotifyKind} (T-CUR1 / PRD §8.5;
63
+ * `aborted` reclassified 2026-06-13):
64
+ * - `completed` → `finished` (agent turned cleanly)
65
+ * - `aborted` → `finished` (FALLBACK kind only — see note; the live path
66
+ * SUPPRESSES aborted before it reaches a kind)
67
+ * - `error` → `error` (agent hit an error)
68
+ * - `undefined` → `finished` (back-compat: absent/unrecognised)
69
+ *
70
+ * WHY `aborted` is special:
71
+ * Cursor's `stop` hook fires `status: "aborted"` when the user **manually presses
72
+ * the Stop button** to interrupt a turn mid-prompt — a RELIABLE "the human stopped
73
+ * it on purpose" signal (distinct from `completed`). The original T-CUR1 design
74
+ * (2026-06-08) mapped `aborted → awaiting_input` as a stand-in for a needs-input
75
+ * event, which fired a false **"Agent needs you"** question push on every manual
76
+ * interrupt (2026-06-13 bug report). Per the user's follow-up, a manual stop must
77
+ * send **NO notification at all** — so {@link shouldSuppressAbortedStop} short-
78
+ * circuits the Cursor `aborted` case in {@link onStop} BEFORE this kind is used.
79
+ * This function therefore only returns `finished` for `aborted` as a defensive
80
+ * fallback (e.g. a caller that bypasses the suppression gate) — never a question.
81
+ *
82
+ * Real "agent needs you" pings are unaffected: Claude Code still fires its
83
+ * dedicated `Notification → awaiting_input` hook (a genuine needs-input signal),
84
+ * and the correct Cursor real-question signal is the future `AskQuestion`
85
+ * `postToolUse` hook (PRD §8.5 T-CUR3), NOT this `aborted` stand-in.
67
86
  */
68
87
  export function cursorStopStatusToKind(status) {
69
88
  switch (status) {
70
89
  case "completed":
71
90
  return "finished";
72
91
  case "aborted":
73
- return "awaiting_input";
92
+ // Defensive fallback only — the live onStop path SUPPRESSES aborted before
93
+ // reaching here (shouldSuppressAbortedStop). Never a question. (2026-06-13)
94
+ return "finished";
74
95
  case "error":
75
96
  return "error";
76
97
  default:
77
98
  return "finished"; // back-compat: absent/unrecognised → finished
78
99
  }
79
100
  }
101
+ /**
102
+ * True when this stop turn is a user-initiated interrupt we should send NO
103
+ * notification for (2026-06-13). Today this is ONLY the Cursor `stop` hook
104
+ * reporting `status: "aborted"` — the one runtime that gives a reliable
105
+ * "the human pressed Stop" discriminator distinct from a normal completion.
106
+ *
107
+ * Claude Code and OpenCode are intentionally NOT suppressible here: their
108
+ * end-of-turn hooks (`Stop` / `session.idle` / `session.status`) fire identically
109
+ * for a normal completion AND a manual stop with no field to tell them apart, so
110
+ * suppressing would also eat their legitimate `finished` pings. They keep the
111
+ * `finished` fallback (the user's explicit "if not possible, just send a DONE
112
+ * notification instead"). Real questions are unaffected on every runtime (Claude's
113
+ * separate `Notification` hook; the future Cursor `AskQuestion` hook).
114
+ *
115
+ * Pure — no I/O. Returns false for any non-Cursor source or any non-aborted
116
+ * status so a normal `completed`/`error`/absent turn always notifies as before.
117
+ */
118
+ export function shouldSuppressAbortedStop(source, status) {
119
+ return source === "cursor" && status === "aborted";
120
+ }
80
121
  /** Slice the merged settings down to what the push flow consumes. */
81
122
  function toPushSettings(settings) {
82
123
  return {
@@ -468,6 +509,24 @@ export async function onStop(opts = {}) {
468
509
  detail: "autopilot loop owns the turn — per-turn ping suppressed",
469
510
  };
470
511
  }
512
+ // ── Step 0.5: user-initiated-stop gate (2026-06-13). ──
513
+ // When the user manually presses Stop/interrupt on Cursor, the stop hook
514
+ // reports `status: "aborted"` — a reliable "the human stopped it on purpose"
515
+ // signal. The user wants NO notification for that (it is not a question, and
516
+ // not a turn they want pinged). Suppress entirely BEFORE any auto-push OR
517
+ // plain-send work, so a manual interrupt never fires a push/commit either.
518
+ // Only Cursor's `aborted` qualifies (the one runtime with a reliable manual-
519
+ // stop discriminator); Claude/OpenCode have no such signal and keep their
520
+ // `finished` fallback. Real questions are unaffected on every runtime.
521
+ const earlyHookStatus = parseCursorStopStatus(opts.hookStdin);
522
+ if (shouldSuppressAbortedStop(source, earlyHookStatus)) {
523
+ await logLine(`user-initiated stop (${source} aborted) → suppressed per-turn ping (not a question; manual interrupt)`);
524
+ return {
525
+ mode: "aborted-suppressed",
526
+ settingsSource: "default",
527
+ detail: "user pressed Stop/interrupt — per-turn ping suppressed",
528
+ };
529
+ }
471
530
  // ── Step 1: derive repo identity (never throws — local fallback on failure). ──
472
531
  let repo;
473
532
  try {
@@ -509,12 +568,13 @@ export async function onStop(opts = {}) {
509
568
  }
510
569
  // ── Step 3b: auto-push off → the plain notification (legacy flow). ──
511
570
  // Derive the notification kind from the Cursor stop hook's stdin JSON (T-CUR1
512
- // / PRD §8.5). The status maps:
571
+ // / PRD §8.5; aborted handling 2026-06-13). The status maps:
513
572
  // completed → finished (agent turned cleanly)
514
- // aborted → awaiting_input (agent yielded back to the human)
573
+ // aborted → (SUPPRESSED upstream at Step 0.5 for Cursor; finished fallback)
515
574
  // error → error (agent hit an error)
516
575
  // absent → finished (back-compat: no stdin or unrecognised status)
517
- const hookStatus = parseCursorStopStatus(opts.hookStdin);
576
+ // Reuse the status already parsed at Step 0.5 (single parse per turn).
577
+ const hookStatus = earlyHookStatus;
518
578
  const sendKind = cursorStopStatusToKind(hookStatus);
519
579
  if (opts.dryRun) {
520
580
  await logLine(`dry-run: would send ${sendKind} (auto-push off, source: ${source}, settings: ${resolved.source})`);
@@ -1,2 +1,2 @@
1
1
  // Single source of truth for the CLI version. Keep in sync with package.json.
2
- export const VERSION = "0.3.3";
2
+ export const VERSION = "0.3.4";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trygocode/notify",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
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",