ai-notify 0.4.4 → 0.4.6
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/package.json +1 -1
- package/src/notify.mjs +17 -6
- package/src/util.mjs +28 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-notify",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
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/notify.mjs
CHANGED
|
@@ -184,11 +184,22 @@ export const emit = ({ provider = 'default', event = 'done', label = '', message
|
|
|
184
184
|
setPaneWaiting(tty, event === 'waiting'); // waiting -> yellow menu bar status; done clears it
|
|
185
185
|
const pane = readPaneSetting(tty);
|
|
186
186
|
|
|
187
|
-
// Name this pane in the read-out
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
187
|
+
// Name this pane in the read-out, most-reliable identity first:
|
|
188
|
+
// 1. $AI_NOTIFY_LABEL — set in the pane's shell, inherited by the hook even
|
|
189
|
+
// when the agent runs it detached (no tty). Always spoken: setting it is
|
|
190
|
+
// explicit intent. The reliable way to name a pane for Claude Code.
|
|
191
|
+
// 2. pane.speakName — set from the menu bar, keyed by tty. Works only when
|
|
192
|
+
// the hook resolves to the pane's tty (see controllingTty's tree walk).
|
|
193
|
+
// 3. the auto-derived label — only when speakLabel is on (else slow filler).
|
|
194
|
+
const envName = (process.env.AI_NOTIFY_LABEL || '').trim();
|
|
195
|
+
const spokenName = envName || pane.speakName || (config.speakLabel === true && label ? label : '');
|
|
196
|
+
// Join the pane name to the read-out as the SUBJECT. Japanese needs the は
|
|
197
|
+
// topic particle ("ジョンは、…") — a bare comma ("ジョン、…") reads as calling
|
|
198
|
+
// out TO John, not saying John is the one finishing / waiting. Other languages
|
|
199
|
+
// just get a comma.
|
|
200
|
+
const isJa = (s) => /[-ヿ㐀-鿿ヲ-゚]/.test(s); // kana / kanji / half-width kana
|
|
201
|
+
const joinName = (name, body) => (name ? `${name}${isJa(body) ? 'は、' : ', '}${body}` : body);
|
|
202
|
+
const speakText = joinName(spokenName, spokenBody);
|
|
192
203
|
|
|
193
204
|
// Per-pane voice (precedence: $AI_NOTIFY_* env > this pane's pick > global).
|
|
194
205
|
const tts = pane.tts || config.tts;
|
|
@@ -233,7 +244,7 @@ export const emit = ({ provider = 'default', event = 'done', label = '', message
|
|
|
233
244
|
speakTone = tsundere.axisFor(eff);
|
|
234
245
|
outVol = Math.min(2, Math.max(0, vol * tsundere.volumeMul(tier, ts.volumeBoost !== false)));
|
|
235
246
|
outText = tsundere.wrap(spokenBody, eff, tier, ts.lang || 'ja', nextCounter('tsundere'));
|
|
236
|
-
if (spokenName) outText =
|
|
247
|
+
if (spokenName) outText = joinName(spokenName, outText);
|
|
237
248
|
if (tts === 'voicevox') {
|
|
238
249
|
const sm = ts.styleMap || voicevox.resolveStyles(outSpeaker, config.voicevox?.url);
|
|
239
250
|
const axis = tsundere.axisFor(eff);
|
package/src/util.mjs
CHANGED
|
@@ -40,18 +40,34 @@ export const isEphemeralInstall = (cliPath) => /[/\\]_npx[/\\]/.test(cliPath);
|
|
|
40
40
|
|
|
41
41
|
export const MARKER = 'ai-notify'; // substring used to detect our own wiring
|
|
42
42
|
|
|
43
|
-
// The controlling terminal of
|
|
44
|
-
//
|
|
43
|
+
// The controlling terminal of the agent's pane (e.g. "/dev/ttys010"), used to
|
|
44
|
+
// scope per-pane settings. Returns null if none can be found.
|
|
45
|
+
//
|
|
46
|
+
// Agents often run the notify hook detached (Claude Code wires it `async`), so
|
|
47
|
+
// the hook process itself frequently has NO controlling tty — but its parent
|
|
48
|
+
// (the agent, e.g. `claude`) still owns the pane's terminal. So we walk up the
|
|
49
|
+
// process tree until we find a real tty, which makes the hook resolve to the
|
|
50
|
+
// SAME tty the menu bar lists the pane under (it scans the agent process).
|
|
45
51
|
export const controllingTty = () => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
let pid = process.pid;
|
|
53
|
+
for (let depth = 0; depth < 8 && pid > 1; depth++) {
|
|
54
|
+
try {
|
|
55
|
+
const line = execFileSync('ps', ['-o', 'tty=', '-o', 'ppid=', '-p', String(pid)], {
|
|
56
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
57
|
+
})
|
|
58
|
+
.toString()
|
|
59
|
+
.trim();
|
|
60
|
+
if (!line) return null;
|
|
61
|
+
// "ttys010 1234" or "?? 1234" (no controlling tty for this pid)
|
|
62
|
+
const sp = line.lastIndexOf(' ');
|
|
63
|
+
const tty = line.slice(0, sp).trim();
|
|
64
|
+
const ppid = parseInt(line.slice(sp + 1).trim(), 10);
|
|
65
|
+
if (tty && tty !== '??' && tty !== '?') return tty.startsWith('/dev/') ? tty : `/dev/${tty}`;
|
|
66
|
+
if (!Number.isFinite(ppid) || ppid <= 1) return null;
|
|
67
|
+
pid = ppid;
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
56
71
|
}
|
|
72
|
+
return null;
|
|
57
73
|
};
|