@trygocode/notify 0.6.6 → 0.6.8

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
@@ -530,6 +530,35 @@ npm publish, real-device E2E), see
530
530
 
531
531
  ## Changelog
532
532
 
533
+ ### 0.6.8
534
+
535
+ - **Fixed: Claude questions inside Cursor now notify.** When Claude Code runs
536
+ inside Cursor and asks a question (permission prompt, idle/elicitation, agent
537
+ needs input), the notification was being **suppressed** because the imported
538
+ Claude `Notification` payload carries `cursor_version`. That suppression is
539
+ correct for *completions* (Cursor's own `stop` hook already fires, so the
540
+ imported Claude `Stop` would double-notify) — but **wrong for questions**:
541
+ Cursor's native question hook (`AskQuestion` via `postToolUse`) is broken
542
+ upstream and Cursor has no `Notification` event, so the imported Claude
543
+ `Notification` is the *only* question signal. `on-notification` now always
544
+ forwards a genuine input-requiring notification, even when Cursor imported it.
545
+ The non-input `notification_type` filter and per-question `--dedupe-key` still
546
+ apply, so no extra noise or duplicates.
547
+
548
+ ### 0.6.7
549
+
550
+ - **Notification project name now matches your IDE.** The per-turn completion
551
+ ping labels the notification with your **workspace folder name** (e.g.
552
+ `Cursor · OpenHandApp — Agent finished`) instead of the git `owner/repo`.
553
+ - **Fixed the `.cursor` project label.** When a stop hook ran without a piped
554
+ workspace path, the label derived from the hook's cwd (`~/.cursor`) and
555
+ rendered as “Cursor · .cursor” (the IDE name twice). `projectLabel` now walks
556
+ up past any leading-dot tooling dir (`.cursor`, `.git`, `.vscode`, …) and
557
+ stops at the home dir, so it never surfaces a tooling/home folder as the
558
+ project. When no real workspace folder is derivable it falls back to the git
559
+ repo name, and if even that is absent it shows the runtime alone
560
+ (`Cursor — Agent finished`) with no confusing suffix.
561
+
533
562
  ### 0.6.6
534
563
 
535
564
  - **Claude questions are now classified by notification type.** Setup installs
@@ -67,10 +67,9 @@ export const CLAUDE_HOOK_COMMANDS = {
67
67
  Notification: 'npx -y @trygocode/notify@latest on-notification --source claude_code --dedupe-key "$CLAUDE_SESSION_ID-notify" --quiet || true',
68
68
  };
69
69
  /**
70
- * Claude `Notification` types that genuinely require the user's attention.
71
- * Keep this matcher in lockstep with `CLAUDE_INPUT_NOTIFICATION_TYPES` in
72
- * `cli.ts`, which provides a defensive payload check for existing 0.6.5
73
- * installs whose matcher is still empty until setup is rerun.
70
+ * Claude `Notification` types that genuinely require the user's attention. The
71
+ * dispatcher independently applies the same values as a defensive payload check
72
+ * for existing 0.6.5 installs whose matcher stays empty until setup reruns.
74
73
  *
75
74
  * Deliberately excluded: `auth_success`, `elicitation_complete`,
76
75
  * `elicitation_response`, and `agent_completed`.
package/dist/src/cli.js CHANGED
@@ -20,7 +20,7 @@ import { serveStdio } from "./mcp.js";
20
20
  import { setup } from "./setup.js";
21
21
  import { uninstall } from "./uninstall.js";
22
22
  import { cmdConfig } from "./config.js";
23
- import { onStop, isClaudeHookImportedByCursor, parseCursorStopStatus, cursorStopStatusToKind, clickTarget, projectLabel, } from "./on_stop.js";
23
+ import { onStop, parseCursorStopStatus, cursorStopStatusToKind, clickTarget, projectLabel, } from "./on_stop.js";
24
24
  import { notifyDesktop, requestDesktopPermission, desktopDisabledByEnv, } from "./desktop_notify.js";
25
25
  import { resolveNotifySettings } from "./config.js";
26
26
  import { deriveRepoIdentity } from "./repo_key.js";
@@ -795,8 +795,23 @@ export async function cmdOnNotification(args, deps = {}) {
795
795
  const hookStdin = deps.hookStdin !== undefined
796
796
  ? deps.hookStdin
797
797
  : await (deps.readStdin ?? readStdinIfPipe)();
798
- if (isClaudeHookImportedByCursor(source, hookStdin))
799
- return 0;
798
+ // NOTE (2026-07-28): we DELIBERATELY do NOT suppress Cursor-imported Claude
799
+ // hooks here (unlike the `on-stop`/completion path). The `cursor_version`
800
+ // suppression exists to avoid DOUBLE notifications when BOTH a native Cursor
801
+ // hook AND the imported Claude hook fire for the same turn. That double-fire
802
+ // only happens for COMPLETION: Cursor's native `stop` hook works, so its
803
+ // imported Claude `Stop` would duplicate it → suppress.
804
+ //
805
+ // For QUESTIONS there is no competing native hook to dedupe against: Cursor's
806
+ // own question signal (the `AskQuestion` `postToolUse` hook) is BROKEN upstream
807
+ // (Cursor docs: "postToolUse ... excluding the AskQuestion tool, which is a
808
+ // known bug") and Cursor has NO `Notification` event at all. So when Claude
809
+ // runs inside Cursor and asks a question, the imported Claude `Notification`
810
+ // hook is the ONLY signal we get. Suppressing it (the old behaviour) silently
811
+ // dropped every in-Cursor Claude question — the exact "questions don't notify"
812
+ // regression. We now always forward a genuine input-requiring notification,
813
+ // regardless of whether Cursor imported it. The per-turn `--dedupe-key` still
814
+ // coalesces any accidental repeat of the SAME question.
800
815
  const notificationType = parseClaudeNotificationType(hookStdin);
801
816
  if (notificationType !== undefined &&
802
817
  !CLAUDE_INPUT_NOTIFICATION_TYPES.has(notificationType)) {
@@ -26,6 +26,7 @@
26
26
  //
27
27
  // Zero runtime deps — Node built-ins only, matching the package's zero-dep rule.
28
28
  import path from "node:path";
29
+ import os from "node:os";
29
30
  import { promises as fs } from "node:fs";
30
31
  import { createHash } from "node:crypto";
31
32
  import { resolveNotifySettings } from "./config.js";
@@ -223,42 +224,57 @@ function toPushSettings(settings) {
223
224
  };
224
225
  }
225
226
  /**
226
- * Resolve the `project` label for a per-turn ping (T-N4 / PRD §3.4). Prefers the
227
- * derived repo identity's `repo_label`; when that is blank — the repo-derive
228
- * threw (so `repo` is undefined) or a non-git cwd produced an empty label — falls
229
- * back to the cwd basename so the phone ALWAYS shows SOMETHING ("better-than-
230
- * nothing"). Returns `undefined` only when even the basename is empty (e.g. cwd
231
- * is the filesystem root), so the caller still omits the field gracefully.
227
+ * Resolve the `project` label for a per-turn ping (T-N4 / PRD §3.4).
228
+ *
229
+ * Preference order (user decision 2026-07-27):
230
+ * 1. The WORKSPACE FOLDER NAME the cwd basename (e.g. "OpenHandApp"), which
231
+ * matches what the user sees in the IDE title bar / file explorer. We walk
232
+ * up past any leading-dot tooling dir (`.cursor`, `.git`, `.vscode`, …) so a
233
+ * hook that runs inside a config dir still names the real project folder,
234
+ * never "Cursor · .cursor". We also reject the user's HOME dir as a label
235
+ * (it is never a meaningful project name).
236
+ * 2. The git REPO NAME — the last segment of `repo_label` (`owner/repo` →
237
+ * `repo`), used only when the folder walk yields nothing usable. Also
238
+ * dot-guarded (a `.cursor` repo label must never surface).
239
+ * 3. `undefined` — no project word at all, so the caller renders the runtime
240
+ * alone ("Cursor — Agent finished"). This is the intentional fallback when
241
+ * there is genuinely no workspace/repo signal (a rare IDE edge case); it is
242
+ * strictly better than showing a confusing dir name.
232
243
  */
233
244
  export function projectLabel(repo, cwd) {
234
- const label = repo?.repo_label?.trim();
235
- // The user wants the plain project/folder name (e.g. "alerc8"), not the
236
- // `owner/repo` form. `repo_label` from a git origin is `owner/repo`, and the
237
- // `local:` fallback already strips the prefix in deriveRepoIdentity so take
238
- // the LAST path segment as the display name. Guard against a trailing slash.
239
- if (label) {
240
- const lastSeg = label.replace(/\/+$/, "").split("/").pop()?.trim();
241
- if (lastSeg)
242
- return lastSeg;
243
- return label;
244
- }
245
- // Fall back to the cwd basename — but SKIP tool/config dot-dirs. The stop hook
246
- // can run with a cwd inside `.cursor` (or `.git`, `.vscode`, …), whose basename
247
- // would otherwise become the project name and render as "Cursor · .cursor"
248
- // (the IDE name shown twice). Walk up past any leading-dot segment to the first
249
- // real project folder so the label is the actual repo dir, not its tooling dir.
245
+ const home = path.resolve(os.homedir());
246
+ // Directories that are never a meaningful project name, even if we land on
247
+ // them while walking up: the home dir itself and its parent chain (`/Users`,
248
+ // `/home`, filesystem root). Reaching any of these means "no project folder".
249
+ const isNonProjectDir = (d) => d === home || home === d + path.sep || home.startsWith(d + path.sep) || path.dirname(d) === d;
250
+ // (1) Prefer the workspace FOLDER name — walk up past leading-dot tooling dirs
251
+ // (`.cursor`, `.git`, `.vscode`, ) to the first REAL project folder. We STOP
252
+ // if we reach the home dir or any ancestor of it (so a hook run with cwd =
253
+ // `~/.cursor` and no workspace path bails to runtime-only rather than climbing
254
+ // into "Users"/"home"/root).
250
255
  let dir = path.resolve(cwd);
251
- for (let i = 0; i < 6; i++) {
256
+ for (let i = 0; i < 8; i++) {
257
+ if (isNonProjectDir(dir))
258
+ break; // home / its ancestors / root → no folder label
252
259
  const base = path.basename(dir).trim();
253
- if (!base)
254
- break; // reached filesystem root
255
- if (!base.startsWith("."))
256
- return base; // first non-dot folder wins
260
+ if (base && !base.startsWith(".")) {
261
+ return base; // first real (non-dot) project folder wins
262
+ }
257
263
  const parent = path.dirname(dir);
258
264
  if (parent === dir)
259
265
  break; // no more parents
260
266
  dir = parent;
261
267
  }
268
+ // (2) Fall back to the git REPO name (last segment of `owner/repo`), dot-guarded.
269
+ const label = repo?.repo_label?.trim();
270
+ if (label) {
271
+ const lastSeg = label.replace(/\/+$/, "").split("/").pop()?.trim();
272
+ if (lastSeg && !lastSeg.startsWith("."))
273
+ return lastSeg;
274
+ if (!lastSeg && !label.startsWith("."))
275
+ return label;
276
+ }
277
+ // (3) No usable signal → runtime-only (undefined omits the project word).
262
278
  return undefined;
263
279
  }
264
280
  /**
@@ -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.6.6";
2
+ export const VERSION = "0.6.8";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trygocode/notify",
3
- "version": "0.6.6",
3
+ "version": "0.6.8",
4
4
  "description": "Free phone + branded desktop notifications for any coding agent (Cursor, Claude Code, OpenCode, Ralph/Homer) via the GoCode app.",
5
5
  "license": "MIT",
6
6
  "type": "module",