@trygocode/notify 0.3.2 → 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.
- package/dist/src/claude.js +7 -2
- package/dist/src/cursor.js +9 -1
- package/dist/src/on_stop.js +80 -10
- package/dist/src/opencode.js +4 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
package/dist/src/claude.js
CHANGED
|
@@ -56,9 +56,14 @@ export const MCP_SERVER_ENTRY = {
|
|
|
56
56
|
* We intentionally register ONLY `Stop` + `Notification`. `SubagentStop` is NOT
|
|
57
57
|
* here on purpose — see {@link SUBAGENT_STOP_EVENT}.
|
|
58
58
|
*/
|
|
59
|
+
// `@latest` is REQUIRED: bare `npx -y @trygocode/notify` caches the
|
|
60
|
+
// first-resolved version forever and never auto-updates (npm/cli#6664). Pinning
|
|
61
|
+
// `@latest` makes every hook fire the newest published version, so users get our
|
|
62
|
+
// fixes without ever clearing their npx cache by hand. Matchers below key on the
|
|
63
|
+
// `@trygocode/notify` substring, which `@latest` preserves.
|
|
59
64
|
export const CLAUDE_HOOK_COMMANDS = {
|
|
60
|
-
Stop: 'npx -y @trygocode/notify on-stop --source claude_code --dedupe-key "$CLAUDE_SESSION_ID-stop" || true',
|
|
61
|
-
Notification: 'npx -y @trygocode/notify send --kind awaiting_input --source claude_code --title "Agent needs you" --dedupe-key "$CLAUDE_SESSION_ID-notify" || true',
|
|
65
|
+
Stop: 'npx -y @trygocode/notify@latest on-stop --source claude_code --dedupe-key "$CLAUDE_SESSION_ID-stop" || true',
|
|
66
|
+
Notification: 'npx -y @trygocode/notify@latest send --kind awaiting_input --source claude_code --title "Agent needs you" --dedupe-key "$CLAUDE_SESSION_ID-notify" || true',
|
|
62
67
|
};
|
|
63
68
|
/**
|
|
64
69
|
* The Claude event we DELIBERATELY do not register and actively scrub on every
|
package/dist/src/cursor.js
CHANGED
|
@@ -48,7 +48,15 @@ export const MCP_SERVER_ENTRY = {
|
|
|
48
48
|
* both old and new commands contain the {@link HOOK_MARKERS} tokens, so an old
|
|
49
49
|
* install still upgrades idempotently and uninstalls cleanly (PRD §2.2).
|
|
50
50
|
*/
|
|
51
|
-
|
|
51
|
+
// `@latest` is REQUIRED, not cosmetic: bare `npx -y @trygocode/notify` caches the
|
|
52
|
+
// first-resolved version FOREVER and never picks up a newer publish (documented
|
|
53
|
+
// npx behaviour — npm/cli#6664; the whole `clear-npx-cache` package exists for
|
|
54
|
+
// this). Without `@latest`, a user who installed an old version would keep
|
|
55
|
+
// running it after we ship a fix, with no way to update short of wiping
|
|
56
|
+
// ~/.npm/_npx by hand. Pinning `@latest` forces npx to resolve the newest
|
|
57
|
+
// version on every turn, so users self-update seamlessly. (Matchers below key on
|
|
58
|
+
// the `@trygocode/notify` substring, which `@latest` preserves.)
|
|
59
|
+
export const CURSOR_STOP_COMMAND = "npx -y @trygocode/notify@latest on-stop --source cursor --dedupe-key cursor-stop || true";
|
|
52
60
|
/**
|
|
53
61
|
* Substrings that together identify a `stop` hook entry as OURS. Used for
|
|
54
62
|
* idempotent merge (replace, don't duplicate) and for surgical uninstall (remove
|
package/dist/src/on_stop.js
CHANGED
|
@@ -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
|
-
*
|
|
64
|
-
* - `
|
|
65
|
-
* - `
|
|
66
|
-
*
|
|
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
|
-
|
|
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 {
|
|
@@ -238,7 +279,17 @@ export function ideChatIdFromHookStdin(source, cwd, hookStdin) {
|
|
|
238
279
|
const ideSessionId = str(p.conversation_id, p.conversationId, p.session_id, p.sessionId, sessionFromPath);
|
|
239
280
|
if (!ideSessionId)
|
|
240
281
|
return undefined;
|
|
241
|
-
|
|
282
|
+
// CRITICAL parity: the deep-link id hashes the workspace, and gocode-sync hashes
|
|
283
|
+
// the SAME workspace when it stores the chat — they MUST agree or a tapped push
|
|
284
|
+
// hits "Chat not found". gocode-sync resolves the workspace from Cursor's REAL
|
|
285
|
+
// field `workspace_roots` (snake_case), so we MUST read it here too. Reuse
|
|
286
|
+
// workspaceRootFromHookStdin (which already prefers workspace_roots and every
|
|
287
|
+
// spelling) so the two packages can never drift. Fall back to camelCase / cwd
|
|
288
|
+
// for older Cursor builds, then the process cwd.
|
|
289
|
+
const workspacePath = workspaceRootFromHookStdin(hookStdin) ??
|
|
290
|
+
str(p.workspace_path, Array.isArray(p.workspace_roots)
|
|
291
|
+
? p.workspace_roots[0]
|
|
292
|
+
: undefined, Array.isArray(p.workspaceRoots) ? p.workspaceRoots[0] : undefined, p.cwd, cwd);
|
|
242
293
|
if (!workspacePath)
|
|
243
294
|
return undefined;
|
|
244
295
|
// The capture side stores `source: "cursor" | "claude_code"`; the hook passes
|
|
@@ -458,6 +509,24 @@ export async function onStop(opts = {}) {
|
|
|
458
509
|
detail: "autopilot loop owns the turn — per-turn ping suppressed",
|
|
459
510
|
};
|
|
460
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
|
+
}
|
|
461
530
|
// ── Step 1: derive repo identity (never throws — local fallback on failure). ──
|
|
462
531
|
let repo;
|
|
463
532
|
try {
|
|
@@ -499,12 +568,13 @@ export async function onStop(opts = {}) {
|
|
|
499
568
|
}
|
|
500
569
|
// ── Step 3b: auto-push off → the plain notification (legacy flow). ──
|
|
501
570
|
// Derive the notification kind from the Cursor stop hook's stdin JSON (T-CUR1
|
|
502
|
-
// / PRD §8.5). The status maps:
|
|
571
|
+
// / PRD §8.5; aborted handling 2026-06-13). The status maps:
|
|
503
572
|
// completed → finished (agent turned cleanly)
|
|
504
|
-
// aborted →
|
|
573
|
+
// aborted → (SUPPRESSED upstream at Step 0.5 for Cursor; finished fallback)
|
|
505
574
|
// error → error (agent hit an error)
|
|
506
575
|
// absent → finished (back-compat: no stdin or unrecognised status)
|
|
507
|
-
|
|
576
|
+
// Reuse the status already parsed at Step 0.5 (single parse per turn).
|
|
577
|
+
const hookStatus = earlyHookStatus;
|
|
508
578
|
const sendKind = cursorStopStatusToKind(hookStatus);
|
|
509
579
|
if (opts.dryRun) {
|
|
510
580
|
await logLine(`dry-run: would send ${sendKind} (auto-push off, source: ${source}, settings: ${resolved.source})`);
|
package/dist/src/opencode.js
CHANGED
|
@@ -68,7 +68,10 @@ export const OPENCODE_MCP_ENTRY = {
|
|
|
68
68
|
* Claude/Cursor. Ends in `|| true` so a failure can never block the session, and
|
|
69
69
|
* carries a `--dedupe-key` so overlapping triggers coalesce server-side.
|
|
70
70
|
*/
|
|
71
|
-
|
|
71
|
+
// `@latest` is REQUIRED: bare `npx -y @trygocode/notify` caches the first
|
|
72
|
+
// resolved version forever and never auto-updates (npm/cli#6664). Pinning
|
|
73
|
+
// `@latest` makes the hook always fetch the newest publish so users self-update.
|
|
74
|
+
export const OPENCODE_STOP_COMMAND = "npx -y @trygocode/notify@latest on-stop --source opencode --dedupe-key opencode-idle || true";
|
|
72
75
|
/**
|
|
73
76
|
* Substrings that together identify our plugin file as OURS. Used to keep
|
|
74
77
|
* uninstall surgical: we only delete the plugin file when BOTH markers are
|
package/dist/src/version.js
CHANGED
|
@@ -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.
|
|
2
|
+
export const VERSION = "0.3.4";
|
package/package.json
CHANGED