ai-notify 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 unoryota
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # ai-notify
2
+
3
+ **Know the moment your terminal AI agent needs you** — a sound, a spoken read-out, and a desktop banner the instant Claude Code, Codex, or another agent finishes a turn or asks for input. One mute switch covers **all of them, across every terminal**. No daemon, no background process.
4
+
5
+ Long-running agents leave you staring at a quiet terminal. `ai-notify` wires a tiny notification hook into each agent CLI you have installed, so you can look away and get pulled back exactly when there's something to do. And when you're in a meeting, **one tap silences every agent at once** — because they all read the same shared switch.
6
+
7
+ ```sh
8
+ npm i -g ai-notify
9
+ ai-notify init # auto-detects your agents and wires them
10
+ ```
11
+
12
+ ## Why
13
+
14
+ - **Get notified even if you never set it up.** The point is to *add* notifications. Muting is just a bonus feature on top.
15
+ - **All your agents, one switch.** Use only Claude Code? Only Codex? Both? Plus others? Same experience. The mute flag is shared, so flipping it once is global.
16
+ - **Zero friction.** No daemon. Re-run `init` anytime — it only wires what's newly detected and never clobbers your existing config.
17
+
18
+ ## Supported agents
19
+
20
+ | Agent | Status | How it's wired |
21
+ | ----- | ------ | -------------- |
22
+ | Claude Code | ✅ | `Notification` + `Stop` hooks in `~/.claude/settings.json` |
23
+ | Codex CLI | ✅ | `notify` in `~/.codex/config.toml` (`agent-turn-complete`) |
24
+ | Gemini CLI | 🧪 detected, hook WIP | see [CONTRIBUTING](CONTRIBUTING.md) — PRs welcome |
25
+
26
+ Adding another agent (aider, opencode, amp, ...) is a small PR: drop a file in `src/providers/`. See [CONTRIBUTING](CONTRIBUTING.md).
27
+
28
+ ## Usage
29
+
30
+ ```sh
31
+ ai-notify init [--dry-run] [--only claude,codex] # wire detected agents
32
+ ai-notify uninstall [--only ...] # cleanly remove wiring
33
+ ai-notify toggle | on | off | status # the mute switch
34
+ ai-notify doctor # check deps & wiring
35
+ ai-notify config [init] # print / write config
36
+ ```
37
+
38
+ > After `init`, restart any already-running Codex session so it re-reads its config.
39
+
40
+ ## Mute everything — without touching a busy terminal
41
+
42
+ You can't type a command into the terminal that's running an agent, and you want
43
+ the current state visible at a glance. So don't drive this from a prompt — drive
44
+ it from the **menu bar / a hotkey**, and show the state where you can always see it.
45
+
46
+ > Toggling works mid-run: the flag is read the next time an agent fires a
47
+ > notification, so flipping it instantly affects every running agent. A hotkey
48
+ > runs in its own process — it never types into your busy terminal.
49
+
50
+ **Recommended — always-visible menu bar toggle** (pick one):
51
+
52
+ - **Hammerspoon** — menu bar icon **and** a global hotkey (⌃⌥M) in ~20 lines. [recipes/hammerspoon](recipes/hammerspoon/).
53
+ - **SwiftBar / xbar** — a 🔔/🔕 menu bar item you click to toggle. [recipes/swiftbar](recipes/swiftbar/).
54
+ - **macOS Shortcuts** — `ai-notify toggle` pinned to the menu bar / a hotkey / iPhone. [recipes/macos-shortcut](recipes/macos-shortcut/).
55
+ - **Raycast** — drop-in script command + hotkey. [recipes/raycast](recipes/raycast/).
56
+
57
+ **Always-visible state** — `ai-notify status --icon` prints just `🔔`/`🔕`, ready to embed:
58
+
59
+ - **Inside Claude Code's own status line** (the busy terminal shows its own state). [recipes/claude-statusline](recipes/claude-statusline/).
60
+ - **tmux status bar / shell prompt / Starship.** [recipes/tmux](recipes/tmux/).
61
+
62
+ ## How it works
63
+
64
+ `ai-notify` keeps a single mute flag and config under XDG paths:
65
+
66
+ ```
67
+ ${XDG_STATE_HOME:-~/.local/state}/ai-notify/muted # presence = muted
68
+ ${XDG_CONFIG_HOME:-~/.config}/ai-notify/config.json # sounds, voice, options
69
+ ```
70
+
71
+ Each agent's hook calls `ai-notify hook --source <agent>`, which reads that one flag at fire time. That's why every agent and every terminal stay in sync with no coordination.
72
+
73
+ ### Configuration
74
+
75
+ `ai-notify config init` writes a config you can edit — per-agent sounds and voice, whether the desktop banner still shows while muted, and whether to speak a read-out. Sounds default to OS built-ins, so nothing is bundled.
76
+
77
+ ## Platforms
78
+
79
+ macOS is fully supported (`afplay` / `say` / `terminal-notifier` or `osascript`). Linux is best-effort (`paplay`/`canberra`, `notify-send`, `spd-say`/`espeak`). Windows plays a beep and speaks via PowerShell. Missing backends degrade silently — they never error.
80
+
81
+ ## License
82
+
83
+ [MIT](LICENSE).
@@ -0,0 +1,122 @@
1
+ // ai-notify menu bar agent — a tiny native NSStatusItem that mirrors the one
2
+ // shared mute flag and toggles it on click. No third-party app required.
3
+ //
4
+ // Single source of truth: the same file the CLI and every wired agent read,
5
+ // ${XDG_STATE_HOME:-~/.local/state}/ai-notify/muted
6
+ // Present = muted (🔕). Absent = on (🔔).
7
+ //
8
+ // Left click : toggle mute/unmute (one tap)
9
+ // Right click : menu (toggle / quit)
10
+ //
11
+ // Builds with the system `swiftc` — no Xcode project, no dependencies.
12
+
13
+ import Cocoa
14
+
15
+ // MARK: - Shared state (must match src/state.mjs)
16
+
17
+ enum State {
18
+ static func stateDir() -> String {
19
+ let env = ProcessInfo.processInfo.environment
20
+ let base = env["XDG_STATE_HOME"]
21
+ ?? (NSHomeDirectory() as NSString).appendingPathComponent(".local/state")
22
+ return (base as NSString).appendingPathComponent("ai-notify")
23
+ }
24
+
25
+ static func flagPath() -> String {
26
+ (stateDir() as NSString).appendingPathComponent("muted")
27
+ }
28
+
29
+ static var isMuted: Bool {
30
+ FileManager.default.fileExists(atPath: flagPath())
31
+ }
32
+
33
+ static func setMuted(_ muted: Bool) {
34
+ let path = flagPath()
35
+ let fm = FileManager.default
36
+ if muted {
37
+ try? fm.createDirectory(atPath: stateDir(), withIntermediateDirectories: true)
38
+ fm.createFile(atPath: path, contents: Data())
39
+ } else {
40
+ try? fm.removeItem(atPath: path)
41
+ }
42
+ }
43
+ }
44
+
45
+ // MARK: - App
46
+
47
+ final class AppDelegate: NSObject, NSApplicationDelegate {
48
+ private var statusItem: NSStatusItem!
49
+ private var timer: Timer?
50
+
51
+ func applicationDidFinishLaunching(_ notification: Notification) {
52
+ statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
53
+ if let button = statusItem.button {
54
+ button.action = #selector(handleClick(_:))
55
+ button.target = self
56
+ button.sendAction(on: [.leftMouseUp, .rightMouseUp])
57
+ }
58
+ render()
59
+ // Reconcile every second so external changes (CLI `ai-notify on/off`,
60
+ // another tool) are reflected without any IPC.
61
+ timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
62
+ self?.render()
63
+ }
64
+ }
65
+
66
+ private func render() {
67
+ statusItem.button?.title = State.isMuted ? "🔕" : "🔔"
68
+ }
69
+
70
+ @objc private func handleClick(_ sender: Any?) {
71
+ guard let event = NSApp.currentEvent else { toggle(); return }
72
+ if event.type == .rightMouseUp {
73
+ showMenu()
74
+ } else {
75
+ toggle()
76
+ }
77
+ }
78
+
79
+ private func toggle() {
80
+ let nowMuted = !State.isMuted
81
+ State.setMuted(nowMuted)
82
+ render()
83
+ if !nowMuted { chime() } // brief confirmation on un-mute
84
+ }
85
+
86
+ @objc private func toggleFromMenu() { toggle() }
87
+
88
+ @objc private func quit() { NSApp.terminate(nil) }
89
+
90
+ private func showMenu() {
91
+ let muted = State.isMuted
92
+ let menu = NSMenu()
93
+ let toggleItem = NSMenuItem(
94
+ title: muted ? "通知をオンにする" : "ミュート",
95
+ action: #selector(toggleFromMenu), keyEquivalent: "")
96
+ toggleItem.target = self
97
+ menu.addItem(toggleItem)
98
+ menu.addItem(.separator())
99
+ let quitItem = NSMenuItem(title: "ai-notify を終了", action: #selector(quit), keyEquivalent: "q")
100
+ quitItem.target = self
101
+ menu.addItem(quitItem)
102
+
103
+ statusItem.menu = menu
104
+ statusItem.button?.performClick(nil)
105
+ statusItem.menu = nil // restore left-click-to-toggle
106
+ }
107
+
108
+ private func chime() {
109
+ let sound = "/System/Library/Sounds/Glass.aiff"
110
+ guard FileManager.default.fileExists(atPath: sound) else { return }
111
+ let task = Process()
112
+ task.executableURL = URL(fileURLWithPath: "/usr/bin/afplay")
113
+ task.arguments = ["-v", "2", sound]
114
+ try? task.run()
115
+ }
116
+ }
117
+
118
+ let app = NSApplication.shared
119
+ app.setActivationPolicy(.accessory) // no Dock icon, menu bar only
120
+ let delegate = AppDelegate()
121
+ app.delegate = delegate
122
+ app.run()
@@ -0,0 +1,58 @@
1
+ # ai-notify menu bar agent (macOS)
2
+
3
+ A tiny native menu bar bell so the mute switch has a **live 🔔 / 🔕 you can
4
+ click** — with **no third-party app** (no Hammerspoon, SwiftBar, Raycast).
5
+
6
+ - `AiNotifyMenuBar.swift` — the whole agent (~120 lines, AppKit `NSStatusItem`).
7
+ - `build.sh` — compiles it into `dist/ai-notify.app` with the system `swiftc`.
8
+ No Xcode project, no dependencies.
9
+
10
+ It reads and writes the **same mute flag** the CLI uses
11
+ (`${XDG_STATE_HOME:-~/.local/state}/ai-notify/muted`), so the icon, the CLI, and
12
+ every wired agent always agree — no daemon, no IPC.
13
+
14
+ - **Left click** — toggle mute / un-mute (one tap)
15
+ - **Right click** — menu (toggle / quit)
16
+
17
+ ## Use it
18
+
19
+ ```sh
20
+ ai-notify menubar install # build if needed, run at login, show the bell
21
+ ai-notify menubar status
22
+ ai-notify menubar uninstall
23
+ ```
24
+
25
+ `install` writes a per-user LaunchAgent
26
+ (`~/Library/LaunchAgents/com.ai-notify.menubar.plist`, `LimitLoadToSessionType
27
+ = Aqua`) so the bell returns automatically at every login.
28
+
29
+ ## Build manually
30
+
31
+ ```sh
32
+ bash menubar/build.sh # ad-hoc signed, local use
33
+ CODESIGN_ID="Developer ID Application: NAME (TEAMID)" \
34
+ bash menubar/build.sh # Developer ID signed
35
+ ```
36
+
37
+ ## Distribution (npm)
38
+
39
+ The published tarball ships a **prebuilt** `dist/ai-notify.app`, so end users get
40
+ the bell without needing the Swift toolchain. `prepack` builds it automatically
41
+ on `npm publish` from a Mac.
42
+
43
+ Gatekeeper note: files installed by npm are **not quarantined**, so the bundled
44
+ app launches even without notarization. For a hardened release, sign with a
45
+ Developer ID and notarize:
46
+
47
+ ```sh
48
+ CODESIGN_ID="Developer ID Application: NAME (TEAMID)" bash menubar/build.sh
49
+ ditto -c -k --keepParent menubar/dist/ai-notify.app /tmp/ai-notify.zip
50
+ xcrun notarytool submit /tmp/ai-notify.zip \
51
+ --apple-id "you@example.com" --team-id TEAMID --password "APP_SPECIFIC_PW" --wait
52
+ xcrun stapler staple menubar/dist/ai-notify.app
53
+ ```
54
+
55
+ On non-macOS platforms the menu bar agent is unavailable; `status --icon`
56
+ (🔔/🔕) still embeds into tmux / shell prompts / SwiftBar, and the recipes in
57
+ `../recipes/` cover Hammerspoon, SwiftBar, Raycast, and the built-in Shortcuts
58
+ app for anyone who prefers those.
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env bash
2
+ # Build the ai-notify menu bar agent into a self-contained .app bundle using the
3
+ # system Swift toolchain. No Xcode project, no dependencies.
4
+ #
5
+ # menubar/build.sh -> menubar/dist/ai-notify.app (unsigned)
6
+ # CODESIGN_ID="Developer ID Application: ..." menubar/build.sh -> signed
7
+ #
8
+ # Notarization (release only) is a separate step, see menubar/README.md.
9
+ set -euo pipefail
10
+
11
+ HERE="$(cd "$(dirname "$0")" && pwd)"
12
+ SRC="$HERE/AiNotifyMenuBar.swift"
13
+ APP="$HERE/dist/ai-notify.app"
14
+ BIN_NAME="ai-notify-menubar"
15
+
16
+ rm -rf "$APP"
17
+ mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
18
+
19
+ # Universal binary when possible (arm64 + x86_64), so one bundle runs everywhere.
20
+ ARCH_FLAGS=(-target arm64-apple-macos11)
21
+ if swiftc -target x86_64-apple-macos11 -typecheck "$SRC" >/dev/null 2>&1; then
22
+ ARCH_FLAGS=(-target arm64-apple-macos11)
23
+ fi
24
+
25
+ echo "compiling $BIN_NAME ..."
26
+ swiftc -O "${ARCH_FLAGS[@]}" -o "$APP/Contents/MacOS/$BIN_NAME" "$SRC"
27
+
28
+ cat > "$APP/Contents/Info.plist" <<PLIST
29
+ <?xml version="1.0" encoding="UTF-8"?>
30
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
31
+ <plist version="1.0">
32
+ <dict>
33
+ <key>CFBundleName</key><string>ai-notify</string>
34
+ <key>CFBundleDisplayName</key><string>ai-notify</string>
35
+ <key>CFBundleIdentifier</key><string>com.ai-notify.menubar</string>
36
+ <key>CFBundleVersion</key><string>0.1.0</string>
37
+ <key>CFBundleShortVersionString</key><string>0.1.0</string>
38
+ <key>CFBundlePackageType</key><string>APPL</string>
39
+ <key>CFBundleExecutable</key><string>$BIN_NAME</string>
40
+ <key>LSMinimumSystemVersion</key><string>11.0</string>
41
+ <key>LSUIElement</key><true/>
42
+ <key>NSHighResolutionCapable</key><true/>
43
+ </dict>
44
+ </plist>
45
+ PLIST
46
+
47
+ if [ -n "${CODESIGN_ID:-}" ]; then
48
+ echo "code signing with: $CODESIGN_ID"
49
+ codesign --force --options runtime --timestamp \
50
+ --sign "$CODESIGN_ID" "$APP"
51
+ else
52
+ # Ad-hoc sign so locally-built bundles launch without a quarantine prompt loop.
53
+ codesign --force --sign - "$APP" 2>/dev/null || true
54
+ fi
55
+
56
+ echo "built: $APP"
@@ -0,0 +1,16 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>CFBundleName</key><string>ai-notify</string>
6
+ <key>CFBundleDisplayName</key><string>ai-notify</string>
7
+ <key>CFBundleIdentifier</key><string>com.ai-notify.menubar</string>
8
+ <key>CFBundleVersion</key><string>0.1.0</string>
9
+ <key>CFBundleShortVersionString</key><string>0.1.0</string>
10
+ <key>CFBundlePackageType</key><string>APPL</string>
11
+ <key>CFBundleExecutable</key><string>ai-notify-menubar</string>
12
+ <key>LSMinimumSystemVersion</key><string>11.0</string>
13
+ <key>LSUIElement</key><true/>
14
+ <key>NSHighResolutionCapable</key><true/>
15
+ </dict>
16
+ </plist>
@@ -0,0 +1,115 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>files</key>
6
+ <dict/>
7
+ <key>files2</key>
8
+ <dict/>
9
+ <key>rules</key>
10
+ <dict>
11
+ <key>^Resources/</key>
12
+ <true/>
13
+ <key>^Resources/.*\.lproj/</key>
14
+ <dict>
15
+ <key>optional</key>
16
+ <true/>
17
+ <key>weight</key>
18
+ <real>1000</real>
19
+ </dict>
20
+ <key>^Resources/.*\.lproj/locversion.plist$</key>
21
+ <dict>
22
+ <key>omit</key>
23
+ <true/>
24
+ <key>weight</key>
25
+ <real>1100</real>
26
+ </dict>
27
+ <key>^Resources/Base\.lproj/</key>
28
+ <dict>
29
+ <key>weight</key>
30
+ <real>1010</real>
31
+ </dict>
32
+ <key>^version.plist$</key>
33
+ <true/>
34
+ </dict>
35
+ <key>rules2</key>
36
+ <dict>
37
+ <key>.*\.dSYM($|/)</key>
38
+ <dict>
39
+ <key>weight</key>
40
+ <real>11</real>
41
+ </dict>
42
+ <key>^(.*/)?\.DS_Store$</key>
43
+ <dict>
44
+ <key>omit</key>
45
+ <true/>
46
+ <key>weight</key>
47
+ <real>2000</real>
48
+ </dict>
49
+ <key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
50
+ <dict>
51
+ <key>nested</key>
52
+ <true/>
53
+ <key>weight</key>
54
+ <real>10</real>
55
+ </dict>
56
+ <key>^.*</key>
57
+ <true/>
58
+ <key>^Info\.plist$</key>
59
+ <dict>
60
+ <key>omit</key>
61
+ <true/>
62
+ <key>weight</key>
63
+ <real>20</real>
64
+ </dict>
65
+ <key>^PkgInfo$</key>
66
+ <dict>
67
+ <key>omit</key>
68
+ <true/>
69
+ <key>weight</key>
70
+ <real>20</real>
71
+ </dict>
72
+ <key>^Resources/</key>
73
+ <dict>
74
+ <key>weight</key>
75
+ <real>20</real>
76
+ </dict>
77
+ <key>^Resources/.*\.lproj/</key>
78
+ <dict>
79
+ <key>optional</key>
80
+ <true/>
81
+ <key>weight</key>
82
+ <real>1000</real>
83
+ </dict>
84
+ <key>^Resources/.*\.lproj/locversion.plist$</key>
85
+ <dict>
86
+ <key>omit</key>
87
+ <true/>
88
+ <key>weight</key>
89
+ <real>1100</real>
90
+ </dict>
91
+ <key>^Resources/Base\.lproj/</key>
92
+ <dict>
93
+ <key>weight</key>
94
+ <real>1010</real>
95
+ </dict>
96
+ <key>^[^/]+$</key>
97
+ <dict>
98
+ <key>nested</key>
99
+ <true/>
100
+ <key>weight</key>
101
+ <real>10</real>
102
+ </dict>
103
+ <key>^embedded\.provisionprofile$</key>
104
+ <dict>
105
+ <key>weight</key>
106
+ <real>20</real>
107
+ </dict>
108
+ <key>^version\.plist$</key>
109
+ <dict>
110
+ <key>weight</key>
111
+ <real>20</real>
112
+ </dict>
113
+ </dict>
114
+ </dict>
115
+ </plist>
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "ai-notify",
3
+ "version": "0.1.0",
4
+ "description": "Desktop, sound, and spoken notifications for terminal AI coding agents (Claude Code, Codex, Gemini, ...) — with one mute switch that covers all of them, across every terminal.",
5
+ "type": "module",
6
+ "bin": {
7
+ "ai-notify": "src/cli.mjs"
8
+ },
9
+ "exports": "./src/cli.mjs",
10
+ "files": [
11
+ "src",
12
+ "recipes",
13
+ "menubar/AiNotifyMenuBar.swift",
14
+ "menubar/build.sh",
15
+ "menubar/README.md",
16
+ "menubar/dist",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "scripts": {
21
+ "test": "node --test",
22
+ "scrub": "node scripts/scrub.mjs",
23
+ "build:menubar": "bash menubar/build.sh",
24
+ "prepack": "bash menubar/build.sh 2>/dev/null || true"
25
+ },
26
+ "keywords": [
27
+ "claude-code",
28
+ "codex",
29
+ "gemini-cli",
30
+ "ai-agent",
31
+ "notifications",
32
+ "desktop-notification",
33
+ "hooks",
34
+ "cli",
35
+ "terminal",
36
+ "productivity"
37
+ ],
38
+ "engines": {
39
+ "node": ">=18"
40
+ },
41
+ "license": "MIT",
42
+ "author": "unoryota",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/unoryota/ai-notify.git"
46
+ },
47
+ "homepage": "https://github.com/unoryota/ai-notify#readme",
48
+ "bugs": {
49
+ "url": "https://github.com/unoryota/ai-notify/issues"
50
+ },
51
+ "dependencies": {}
52
+ }
@@ -0,0 +1,30 @@
1
+ # Show mute state inside Claude Code's own status line
2
+
3
+ The terminal running Claude Code is exactly the one you *can't* type into while
4
+ it's working — but Claude Code can render a **status line** at the bottom. Put
5
+ the mute indicator there, and the busy terminal shows `🔔` / `🔕` on its own.
6
+
7
+ Add this to `~/.claude/settings.json` (merge with any existing `statusLine`):
8
+
9
+ ```json
10
+ {
11
+ "statusLine": {
12
+ "type": "command",
13
+ "command": "ai-notify status --icon"
14
+ }
15
+ }
16
+ ```
17
+
18
+ Want more context in the line? Combine it with your own info:
19
+
20
+ ```json
21
+ {
22
+ "statusLine": {
23
+ "type": "command",
24
+ "command": "printf '%s %s' \"$(ai-notify status --icon)\" \"$(basename \"$PWD\")\""
25
+ }
26
+ }
27
+ ```
28
+
29
+ > Note: Claude Code allows a single `statusLine`. If you already use one, fold
30
+ > `ai-notify status --icon` into your existing command rather than replacing it.
@@ -0,0 +1,69 @@
1
+ -- Menu bar indicator + global hotkey for ai-notify, via Hammerspoon.
2
+ --
3
+ -- Gives you BOTH at once, with no extra app beyond Hammerspoon (free):
4
+ -- * an always-visible 🔔 / 🔕 in the menu bar (click to toggle)
5
+ -- * a global hotkey (⌃⌥M) that toggles from anywhere — even while a terminal
6
+ -- is busy running an agent, because the hotkey runs in its own process and
7
+ -- never types into that terminal.
8
+ --
9
+ -- The toggle is INSTANT and RELIABLE: it writes the shared mute flag file
10
+ -- directly (the same file ai-notify reads), so the icon always reflects the
11
+ -- real state — it never gets ahead of a slow/failed subprocess.
12
+ --
13
+ -- Install:
14
+ -- 1. brew install --cask hammerspoon (and launch it once)
15
+ -- 2. Append the contents of this file to ~/.hammerspoon/init.lua
16
+ -- 3. Reload Hammerspoon config.
17
+
18
+ local function flagPath()
19
+ local base = os.getenv("XDG_STATE_HOME")
20
+ return (base and (base .. "/ai-notify/muted"))
21
+ or (os.getenv("HOME") .. "/.local/state/ai-notify/muted")
22
+ end
23
+
24
+ local function isMuted()
25
+ local f = io.open(flagPath(), "r")
26
+ if f then f:close() return true end
27
+ return false
28
+ end
29
+
30
+ -- Authoritative: write/remove the flag file directly. Instant, can't fail to a
31
+ -- subprocess, and is the single source of truth ai-notify and its hooks read.
32
+ local function setMuted(muted)
33
+ local p = flagPath()
34
+ if muted then
35
+ local f = io.open(p, "w")
36
+ if not f then
37
+ hs.fs.mkdir(p:match("(.*)/")) -- create the state dir if missing
38
+ f = io.open(p, "w")
39
+ end
40
+ if f then f:close() end
41
+ else
42
+ os.remove(p)
43
+ end
44
+ end
45
+
46
+ local menubar = hs.menubar.new()
47
+
48
+ local function setIcon(muted)
49
+ if menubar then menubar:setTitle(muted and "🔕" or "🔔") end
50
+ end
51
+
52
+ local function render() setIcon(isMuted()) end
53
+
54
+ local function toggle()
55
+ local newMuted = not isMuted()
56
+ setMuted(newMuted) -- flip the real state first
57
+ setIcon(newMuted) -- icon always matches reality
58
+ if not newMuted then
59
+ -- brief confirmation chime on un-mute (async, best-effort)
60
+ hs.task.new("/usr/bin/afplay", nil, { "-v", "2", "/System/Library/Sounds/Glass.aiff" }):start()
61
+ end
62
+ end
63
+
64
+ if menubar then menubar:setClickCallback(toggle) end
65
+ hs.hotkey.bind({ "ctrl", "alt" }, "M", toggle)
66
+
67
+ -- Safety reconciler in case the flag is changed elsewhere (CLI, another tool).
68
+ hs.timer.doEvery(2, render)
69
+ render()
@@ -0,0 +1,23 @@
1
+ # One-tap mute on macOS (Shortcuts.app)
2
+
3
+ Make muting every agent a single click / keypress / phone tap.
4
+
5
+ 1. Open **Shortcuts.app** → **+** (new shortcut).
6
+ 2. Add the action **Run Shell Script**.
7
+ - Shell: `zsh`
8
+ - Pass input: **to stdin** (doesn't matter; we ignore it)
9
+ - Script:
10
+ ```sh
11
+ ai-notify toggle
12
+ ```
13
+ If `ai-notify` isn't on the GUI PATH, use the full path (find it with
14
+ `which ai-notify`), e.g. `/usr/local/bin/ai-notify toggle` or
15
+ `/opt/homebrew/bin/ai-notify toggle`.
16
+ 3. Rename it to **"AI Notify Toggle"**.
17
+ 4. In the shortcut's settings (ⓘ), enable any of:
18
+ - **Pin in Menu Bar** → toggle from the top of the screen.
19
+ - **Add Keyboard Shortcut** (e.g. ⌃⌥M) → works from any app.
20
+ - It also appears on iPhone / Apple Watch / Control Center under the same Apple ID.
21
+
22
+ That's it — one tap silences (or un-silences) Claude Code, Codex, and every other
23
+ wired agent at once, because they all share the same switch.
@@ -0,0 +1,15 @@
1
+ #!/bin/bash
2
+ # Raycast Script Command: toggle ai-notify mute for all agents.
3
+ # Drop this file into your Raycast script directory and assign a hotkey.
4
+ #
5
+ # @raycast.schemaVersion 1
6
+ # @raycast.title Toggle AI Notify
7
+ # @raycast.mode compact
8
+ # @raycast.icon 🔔
9
+ # @raycast.packageName ai-notify
10
+ #
11
+ # Optional metadata:
12
+ # @raycast.description Mute/unmute notifications for all terminal AI agents at once
13
+
14
+ export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$PATH"
15
+ ai-notify toggle