ai-notify 0.4.3 → 0.4.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/README.ja.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  [English](README.md) · **日本語**
4
4
 
5
+ [![npm](https://img.shields.io/npm/v/ai-notify?color=cb3837&logo=npm)](https://www.npmjs.com/package/ai-notify)
6
+ [![downloads](https://img.shields.io/npm/dw/ai-notify?color=cb3837)](https://www.npmjs.com/package/ai-notify)
7
+ [![license](https://img.shields.io/npm/l/ai-notify?color=blue)](./LICENSE)
8
+ ![platform](https://img.shields.io/badge/macOS%20%C2%B7%20Linux-zero--dep-success)
9
+ [![stars](https://img.shields.io/github/stars/unoryota/ai-notify?style=social)](https://github.com/unoryota/ai-notify)
10
+
5
11
  **ターミナルのAIエージェントが「あなたを必要とした瞬間」を逃さない** — Claude Code・Codex などのエージェントがターンを終えた/入力を求めた瞬間に、音・読み上げ・デスクトップ通知で知らせます。**全エージェント・全ターミナルを1つのスイッチで一括ミュート**。デーモンも常駐プロセスも無し。
6
12
 
7
13
  ![ai-notify demo](https://raw.githubusercontent.com/unoryota/ai-notify/main/assets/hero-ja.gif)
@@ -13,6 +19,10 @@ brew install unoryota/tap/ai-notify # macOS(Homebrew)
13
19
  ai-notify init # インストール済みのエージェントを自動検出して配線
14
20
  ```
15
21
 
22
+ セットアップはこれだけ。`init` が Claude Code / Codex / Gemini を見つけてフックを配線します。あとは1つのスイッチで全部を操作できます:
23
+
24
+ ![ai-notify の使い方: init・status・一括ミュート・声の切替](https://raw.githubusercontent.com/unoryota/ai-notify/main/assets/usage.gif)
25
+
16
26
  ## 何が違うか
17
27
 
18
28
  エージェントは数分間も沈黙しがち。ai-notify は適切な瞬間に呼び戻します。とくに **AIを並列でたくさん動かす運用**のために作られています:
@@ -23,6 +33,10 @@ ai-notify init # インストール済みのエージェントを自動
23
33
  - 🔕 **1スイッチで全部ミュート。** 全エージェント・全ターミナルが同じフラグを読むので、会議中はワンタップで全部静かに。
24
34
  - 🔔 **ネイティブのメニューバーも内蔵。** `ai-notify menubar install` — Hammerspoon/SwiftBar 不要。
25
35
 
36
+ エージェントの返答を翻訳・VOICEVOX キャラの一覧・ツンデレ口調のクイックツアー:
37
+
38
+ ![ai-notify の機能: 翻訳・VOICEVOXボイス・ツンデレ](https://raw.githubusercontent.com/unoryota/ai-notify/main/assets/features.gif)
39
+
26
40
  ## 対応エージェント
27
41
 
28
42
  | エージェント | 状態 | 配線方法 |
@@ -68,9 +82,17 @@ ai-notify menubar install # ネイティブのメニューバーアプリ・
68
82
 
69
83
  モノクロの波形アイコンが**状態を色で**表します(Adobe風):通常はシルエットのみ、入力待ちがあると**黄ドット**、ミュート中は**赤+斜線**。
70
84
 
71
- - **左クリック** → メニュー:**音量スライダー**、**ツンデレ**トグル+デレ⇄ツンスライダー、**声の一覧**(システム+VOICEVOX)、**ペイン別**設定(開いている各ターミナルに個別の声と音量)。
85
+ - **左クリック** → メニュー:**音量スライダー**、**ツンデレ**トグル+デレ⇄ツンスライダー、**声の一覧**(システム+VOICEVOX)、**ペイン別**設定。開いている各ターミナルに、**読み上げ名**(どのペインが終わったか声で分かる)・**声**・**音量**を個別設定でき、各行にそのペインの声が一覧表示されます。
72
86
  - **右クリック** → 即ミュート切替。
73
87
 
88
+ <p>
89
+ <img alt="ai-notify メニュー — 音量・ツンデレ・声" src="https://raw.githubusercontent.com/unoryota/ai-notify/main/assets/menubar.png" width="250">
90
+ &nbsp;&nbsp;
91
+ <img alt="ペインごとの読み上げ名と声(1ターミナル1行)" src="https://raw.githubusercontent.com/unoryota/ai-notify/main/assets/menubar-panes.png" width="250">
92
+ </p>
93
+
94
+ *左:全体の音量/ツンデレ/声。右:各ペインに名前と声を割り当て(🗣 バックエンド → Kyoko、infra → ずんだもん)。*
95
+
74
96
  第三者アプリ不要。別の方法が好みなら、**Hammerspoon**・**SwiftBar/xbar**・**Raycast**・標準の**ショートカット**用レシピが [`recipes/`](recipes/) にあります。`ai-notify status --icon` は `🔔`/`🔕` だけを出力するので、tmux・プロンプト・Claude Code のステータスラインに埋め込めます。
75
97
 
76
98
  > 切替は実行中でも効きます:次にエージェントが発火した時にフラグを読むので、トグルした瞬間に全稼働エージェントへ反映されます。
package/README.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  **English** · [日本語](README.ja.md)
4
4
 
5
+ [![npm](https://img.shields.io/npm/v/ai-notify?color=cb3837&logo=npm)](https://www.npmjs.com/package/ai-notify)
6
+ [![downloads](https://img.shields.io/npm/dw/ai-notify?color=cb3837)](https://www.npmjs.com/package/ai-notify)
7
+ [![license](https://img.shields.io/npm/l/ai-notify?color=blue)](./LICENSE)
8
+ ![platform](https://img.shields.io/badge/macOS%20%C2%B7%20Linux-zero--dep-success)
9
+ [![stars](https://img.shields.io/github/stars/unoryota/ai-notify?style=social)](https://github.com/unoryota/ai-notify)
10
+
5
11
  **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.
6
12
 
7
13
  ![ai-notify demo](https://raw.githubusercontent.com/unoryota/ai-notify/main/assets/hero-en.gif)
@@ -13,6 +19,11 @@ brew install unoryota/tap/ai-notify # macOS (Homebrew)
13
19
  ai-notify init # auto-detects your agents and wires them
14
20
  ```
15
21
 
22
+ That's the whole setup — `init` finds Claude Code / Codex / Gemini and wires
23
+ their hooks. From then on you control everything with one switch:
24
+
25
+ ![ai-notify usage: init, status, one-switch mute, voices](https://raw.githubusercontent.com/unoryota/ai-notify/main/assets/usage.gif)
26
+
16
27
  ## What makes it different
17
28
 
18
29
  Plenty of agents go quiet for minutes. ai-notify pulls you back at the right moment — and is built for **running many agents at once**:
@@ -23,6 +34,10 @@ Plenty of agents go quiet for minutes. ai-notify pulls you back at the right mom
23
34
  - 🔕 **One switch mutes everything.** Every agent in every terminal reads the same flag — one tap silences them all for a meeting.
24
35
  - 🔔 **A real menu bar bell, built in.** `ai-notify menubar install` — no Hammerspoon/SwiftBar required.
25
36
 
37
+ A quick tour — translate an agent's reply, list VOICEVOX character voices, and the tsundere persona:
38
+
39
+ ![ai-notify features: translate, VOICEVOX voices, tsundere](https://raw.githubusercontent.com/unoryota/ai-notify/main/assets/features.gif)
40
+
26
41
  ## Supported agents
27
42
 
28
43
  | Agent | Status | How it's wired |
@@ -68,9 +83,17 @@ ai-notify menubar install # native menu bar app, starts at login
68
83
 
69
84
  A monochrome waveform icon shows status by color (Adobe-style): plain when idle, a **yellow** dot when an agent is waiting for you, **red + slash** when muted.
70
85
 
71
- - **Left-click** → menu: a **volume slider**, a **tsundere** toggle + デレ⇄ツン slider, the **voice list** (system + VOICEVOX), and **per-pane** controls each open terminal gets its own voice *and* volume.
86
+ - **Left-click** → menu: a **volume slider**, a **tsundere** toggle + デレ⇄ツン slider, the **voice list** (system + VOICEVOX), and **per-pane** controls. Each open terminal gets its own **spoken name** (read out so you know *which* pane finished), **voice**, and **volume** — the row shows each pane's voice at a glance.
72
87
  - **Right-click** → instant mute toggle.
73
88
 
89
+ <p>
90
+ <img alt="ai-notify menu — volume, tsundere, and voice controls" src="https://raw.githubusercontent.com/unoryota/ai-notify/main/assets/menubar.png" width="250">
91
+ &nbsp;&nbsp;
92
+ <img alt="per-pane spoken name and voice, one row per terminal" src="https://raw.githubusercontent.com/unoryota/ai-notify/main/assets/menubar-panes.png" width="250">
93
+ </p>
94
+
95
+ *Left: global volume / tsundere / voices. Right: each pane named and given its own voice (🗣 バックエンド → Kyoko, infra → ずんだもん).*
96
+
74
97
  No third-party app needed. Prefer something else? There are drop-in recipes for **Hammerspoon**, **SwiftBar/xbar**, **Raycast**, and the built-in **macOS Shortcuts** in [`recipes/`](recipes/). `ai-notify status --icon` prints just `🔔`/`🔕` to embed in tmux / your prompt / Claude Code's status line.
75
98
 
76
99
  > Toggling works mid-run: the flag is read the next time an agent fires, so flipping it instantly affects every running agent.
@@ -113,6 +113,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
113
113
  }
114
114
  render()
115
115
  timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in self?.render() }
116
+
117
+ var shotPath = ProcessInfo.processInfo.environment["AI_NOTIFY_SHOT"]
118
+ let args = CommandLine.arguments
119
+ if let i = args.firstIndex(of: "--shot"), i + 1 < args.count { shotPath = args[i + 1] }
120
+ if let shot = shotPath, !shot.isEmpty {
121
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in self?.captureMenuShot(shot) }
122
+ }
116
123
  }
117
124
 
118
125
  // Black/white waveform silhouette (template, auto-adapting) when idle; a
@@ -283,7 +290,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
283
290
  return it
284
291
  }
285
292
 
286
- private func showMenu() {
293
+ private func buildMenu() -> NSMenu {
287
294
  let menu = NSMenu()
288
295
 
289
296
  // Parse menu-json once.
@@ -403,6 +410,59 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
403
410
  quitItem.target = self
404
411
  menu.addItem(quitItem)
405
412
 
413
+ return menu
414
+ }
415
+
416
+ private func showMenu() {
417
+ let menu = buildMenu()
418
+ if let button = statusItem.button {
419
+ menu.popUp(positioning: nil, at: NSPoint(x: 0, y: button.bounds.height + 4), in: button)
420
+ }
421
+ }
422
+
423
+ // Screenshot mode (env AI_NOTIFY_SHOT=/path): open the menu, capture just its
424
+ // window via `screencapture`, then quit. Used to regenerate the README image
425
+ // headlessly — never reached in normal operation.
426
+ private var shotMenu: NSMenu?
427
+ func captureMenuShot(_ path: String) {
428
+ let menu = buildMenu()
429
+ shotMenu = menu
430
+ let pid = ProcessInfo.processInfo.processIdentifier
431
+ // An open NSMenu runs the runloop in event-tracking mode, so the timer
432
+ // must be registered in .common mode to fire while the menu is on screen.
433
+ // The menu window can take a few ticks to register with the window
434
+ // server, so poll for it rather than assuming a single fixed delay.
435
+ var ticks = 0
436
+ let t = Timer(timeInterval: 0.25, repeats: true) { [weak self] timer in
437
+ ticks += 1
438
+ let infos = (CGWindowListCopyWindowInfo([.optionOnScreenOnly], kCGNullWindowID) as? [[String: Any]]) ?? []
439
+ var bestId: CGWindowID = 0
440
+ var bestArea: CGFloat = 0
441
+ for w in infos {
442
+ guard (w[kCGWindowOwnerPID as String] as? pid_t) == pid else { continue }
443
+ guard let b = w[kCGWindowBounds as String] as? [String: CGFloat],
444
+ let wd = b["Width"], let ht = b["Height"],
445
+ let num = w[kCGWindowNumber as String] as? Int else { continue }
446
+ // The menu is far taller than the status-bar button window.
447
+ let area = wd * ht
448
+ if ht > 120 && area > bestArea { bestArea = area; bestId = CGWindowID(num) }
449
+ }
450
+ if bestId == 0 && ticks < 16 { return } // menu window not up yet — keep polling
451
+ if bestId != 0 {
452
+ // -l captures the window at native resolution and crops tight to
453
+ // it; -o drops the drop-shadow for a clean asset.
454
+ let p = Process()
455
+ p.launchPath = "/usr/sbin/screencapture"
456
+ p.arguments = ["-x", "-o", "-l", String(bestId), path]
457
+ try? p.run(); p.waitUntilExit()
458
+ }
459
+ timer.invalidate()
460
+ menu.cancelTracking()
461
+ self?.shotMenu = nil
462
+ NSApp.terminate(nil)
463
+ }
464
+ RunLoop.main.add(t, forMode: .common)
465
+ NSApp.activate(ignoringOtherApps: true) // a popUp only shows for the active app
406
466
  if let button = statusItem.button {
407
467
  menu.popUp(positioning: nil, at: NSPoint(x: 0, y: button.bounds.height + 4), in: button)
408
468
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-notify",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
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
5
  "type": "module",
6
6
  "bin": {
package/src/cli.mjs CHANGED
@@ -32,6 +32,7 @@ import {
32
32
  readPanes,
33
33
  readPaneSetting,
34
34
  updatePaneSetting,
35
+ firstRunNudge,
35
36
  } from './state.mjs';
36
37
 
37
38
  // Single source of truth: read the version from package.json so `--version`
@@ -138,7 +139,15 @@ const cmds = {
138
139
  }
139
140
  if (!any) log(' No supported agents detected (looked for Claude Code, Codex, Gemini).');
140
141
  log(`\nMute toggle: ai-notify toggle Status: ai-notify status`);
141
- if (!dryRun) log('Restart already-running Codex sessions to pick up the change.');
142
+ if (!dryRun) {
143
+ log('Restart already-running Codex sessions to pick up the change.');
144
+ // A quiet, one-time nudge — `init` is a setup command, run once. Shown
145
+ // only on the first successful wiring so it never nags on re-runs.
146
+ if (any && firstRunNudge()) {
147
+ log('\n⭐ Useful? A GitHub star really helps it reach others:');
148
+ log(' https://github.com/unoryota/ai-notify');
149
+ }
150
+ }
142
151
  },
143
152
 
144
153
  uninstall() {
package/src/state.mjs CHANGED
@@ -159,6 +159,23 @@ export const nextCounter = (name) => {
159
159
  return n;
160
160
  };
161
161
 
162
+ // One-time UI nudges (e.g. the post-`init` star hint). Returns true the FIRST
163
+ // time it's called for a given key, then records a marker so it never fires
164
+ // again — so setup hints inform once without ever nagging on re-runs.
165
+ export const firstRunNudge = (key = 'star') => {
166
+ const p = join(stateDir(), `nudged-${key}`);
167
+ if (existsSync(p)) return false;
168
+ try {
169
+ ensureDir(stateDir());
170
+ writeFileSync(p, '');
171
+ } catch {
172
+ /* best-effort: if we can't persist, don't nag repeatedly is preferred, so
173
+ treat a write failure as "already shown". */
174
+ return false;
175
+ }
176
+ return true;
177
+ };
178
+
162
179
  // --- Per-pane state --------------------------------------------------------
163
180
  // Recently-active terminal panes (so the menu bar can offer per-pane voices),
164
181
  // and a per-tty voice override. Both are small JSON files in the state dir.