agent-noti 1.1.0 → 1.3.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/README.md +80 -11
- package/bin/cli.mjs +228 -161
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,21 +10,94 @@ Works on macOS, Linux, and Windows.
|
|
|
10
10
|
npm i -g agent-noti
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
That's it. Hooks are added
|
|
13
|
+
That's it. Hooks are added automatically and the interactive sound picker launches so you can choose a theme. Restart your agent.
|
|
14
14
|
|
|
15
15
|
## What it does
|
|
16
16
|
|
|
17
17
|
| Event | Sound | Claude Code | Codex |
|
|
18
18
|
|---|---|---|---|
|
|
19
|
-
| Agent finished |
|
|
20
|
-
| Needs your input |
|
|
19
|
+
| Agent finished | idle sound | Stop | agent-turn-complete |
|
|
20
|
+
| Needs your input | input sound | PermissionRequest | approval-requested |
|
|
21
|
+
|
|
22
|
+
## Sound themes
|
|
23
|
+
|
|
24
|
+
Each theme includes a separate idle and input sound.
|
|
25
|
+
|
|
26
|
+
| Theme | Description |
|
|
27
|
+
|---|---|
|
|
28
|
+
| default | Original notification |
|
|
29
|
+
| cow | Moo! |
|
|
30
|
+
| goose | Honk! |
|
|
31
|
+
| duck | Quack quack |
|
|
32
|
+
| car | Vroom vroom |
|
|
33
|
+
| slide-whistle | Wheee! |
|
|
34
|
+
| video-game | Retro gaming |
|
|
35
|
+
| digital-glass | Sleek & modern |
|
|
21
36
|
|
|
22
37
|
## Commands
|
|
23
38
|
|
|
24
39
|
```sh
|
|
25
|
-
agent-noti
|
|
26
|
-
agent-noti
|
|
27
|
-
agent-noti
|
|
40
|
+
agent-noti install # Add hooks + pick theme (i)
|
|
41
|
+
agent-noti uninstall # Remove hooks
|
|
42
|
+
agent-noti test # Play current sounds (t)
|
|
43
|
+
agent-noti sounds # List available themes (s)
|
|
44
|
+
agent-noti pick # Interactive sound picker (p)
|
|
45
|
+
agent-noti add-custom # Use your own sound files (ac)
|
|
46
|
+
agent-noti volume <1-10> # Set volume level (v)
|
|
47
|
+
agent-noti mute # Mute notifications (m)
|
|
48
|
+
agent-noti unmute # Unmute notifications (u)
|
|
49
|
+
agent-noti reset # Reset everything (r)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Every command has a short alias shown in parentheses — e.g. `agent-noti v 5` instead of `agent-noti volume 5`.
|
|
53
|
+
|
|
54
|
+
## Interactive picker
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
agent-noti pick
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Navigate with arrow keys, preview sounds before selecting:
|
|
61
|
+
|
|
62
|
+
- **Up / Down** — navigate themes
|
|
63
|
+
- **Left** — play idle sound
|
|
64
|
+
- **Right** — play input sound
|
|
65
|
+
- **Enter** — select theme
|
|
66
|
+
- **q** — quit
|
|
67
|
+
|
|
68
|
+
The picker also includes **+ Add custom** at the bottom, which walks you through importing your own sound files. Once added, your custom sounds appear in the picker below default.
|
|
69
|
+
|
|
70
|
+
## Custom sounds
|
|
71
|
+
|
|
72
|
+
Run `agent-noti add-custom` (or select **+ Add custom** in the picker) for an interactive flow:
|
|
73
|
+
|
|
74
|
+
1. Choose idle sound — enter a file path or skip (use default)
|
|
75
|
+
2. Choose input sound — enter a file path, use same as idle, or skip
|
|
76
|
+
|
|
77
|
+
Custom files are copied to `~/.agent-noti/sounds/` so they persist across package updates.
|
|
78
|
+
|
|
79
|
+
## Volume & mute
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
agent-noti volume 5 # Set volume 1-10
|
|
83
|
+
agent-noti volume # Show current volume
|
|
84
|
+
agent-noti mute # Silence all notifications
|
|
85
|
+
agent-noti unmute # Re-enable notifications
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Setting volume while muted auto-unmutes. Volume works across all platforms.
|
|
89
|
+
|
|
90
|
+
## Config
|
|
91
|
+
|
|
92
|
+
All settings are stored in `~/.agent-noti/config.json`:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"idle": "cow",
|
|
97
|
+
"input": "cow",
|
|
98
|
+
"volume": 10,
|
|
99
|
+
"muted": false
|
|
100
|
+
}
|
|
28
101
|
```
|
|
29
102
|
|
|
30
103
|
## Uninstall
|
|
@@ -35,16 +108,12 @@ npm uninstall -g agent-noti
|
|
|
35
108
|
|
|
36
109
|
Hooks are removed automatically.
|
|
37
110
|
|
|
38
|
-
## Custom sounds
|
|
39
|
-
|
|
40
|
-
Replace `sounds/idle.mp3` and `sounds/input.mp3` in the package directory with your own files.
|
|
41
|
-
|
|
42
111
|
## Platform support
|
|
43
112
|
|
|
44
113
|
| OS | Audio player |
|
|
45
114
|
|---|---|
|
|
46
115
|
| macOS | `afplay` (built-in) |
|
|
47
|
-
| Linux | `ffplay`, `paplay`, or `mpv` |
|
|
116
|
+
| Linux | `ffplay`, `paplay`, or `mpv` (tries in order) |
|
|
48
117
|
| Windows | PowerShell MediaPlayer |
|
|
49
118
|
|
|
50
119
|
## License
|
package/bin/cli.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from "fs";
|
|
4
4
|
import { join, dirname, extname } from "path";
|
|
5
5
|
import { execSync, spawn } from "child_process";
|
|
6
|
+
import { createInterface } from "readline";
|
|
6
7
|
import { fileURLToPath } from "url";
|
|
7
8
|
import { homedir, platform } from "os";
|
|
8
9
|
|
|
@@ -33,10 +34,6 @@ const SOUND_THEMES = [
|
|
|
33
34
|
{ name: "digital-glass", desc: "Sleek & modern" },
|
|
34
35
|
];
|
|
35
36
|
|
|
36
|
-
function getThemeNames() {
|
|
37
|
-
return SOUND_THEMES.map((s) => s.name);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
37
|
// --- Sound file resolution ---
|
|
41
38
|
|
|
42
39
|
function findThemeFile(theme, event) {
|
|
@@ -187,6 +184,38 @@ function uninstallCodex() {
|
|
|
187
184
|
|
|
188
185
|
// --- Interactive picker ---
|
|
189
186
|
|
|
187
|
+
function isCustomPath(val) {
|
|
188
|
+
return val && (val.startsWith("/") || /^[A-Z]:\\/.test(val));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function buildPickerThemes() {
|
|
192
|
+
const config = readConfig();
|
|
193
|
+
const themes = [SOUND_THEMES[0]]; // default first
|
|
194
|
+
|
|
195
|
+
// Show custom option below default if custom sounds have been configured
|
|
196
|
+
if (config.customIdle || config.customInput) {
|
|
197
|
+
themes.push({ name: "custom", desc: "Your custom sounds" });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Regular themes
|
|
201
|
+
themes.push(...SOUND_THEMES.slice(1));
|
|
202
|
+
|
|
203
|
+
// Add custom trigger at the bottom
|
|
204
|
+
themes.push({ name: "+ Add custom", desc: "Import your own sounds" });
|
|
205
|
+
|
|
206
|
+
return themes;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function resolvePickerPreview(themeName, event, config) {
|
|
210
|
+
if (themeName === "custom") {
|
|
211
|
+
const path = event === "idle" ? config.customIdle : config.customInput;
|
|
212
|
+
if (path && existsSync(path)) return path;
|
|
213
|
+
return join(SOUNDS_DIR, `${event}.mp3`); // fallback
|
|
214
|
+
}
|
|
215
|
+
if (themeName === "+ Add custom") return null;
|
|
216
|
+
return findThemeFile(themeName, event);
|
|
217
|
+
}
|
|
218
|
+
|
|
190
219
|
function picker() {
|
|
191
220
|
return new Promise((resolve) => {
|
|
192
221
|
if (!process.stdin.isTTY) {
|
|
@@ -194,16 +223,18 @@ function picker() {
|
|
|
194
223
|
return;
|
|
195
224
|
}
|
|
196
225
|
|
|
197
|
-
const themes =
|
|
226
|
+
const themes = buildPickerThemes();
|
|
198
227
|
const config = readConfig();
|
|
199
|
-
|
|
228
|
+
|
|
229
|
+
// Determine current theme for pre-selection
|
|
230
|
+
const currentTheme = isCustomPath(config.idle) ? "custom" : (config.idle || "default");
|
|
200
231
|
let selected = Math.max(0, themes.findIndex((t) => t.name === currentTheme));
|
|
232
|
+
|
|
201
233
|
let nowPlaying = "";
|
|
202
234
|
let previewProc = null;
|
|
203
235
|
const maxName = Math.max(...themes.map((s) => s.name.length));
|
|
204
236
|
|
|
205
|
-
|
|
206
|
-
const totalLines = themes.length + 5; // blank + title + blank + themes + blank + footer
|
|
237
|
+
const totalLines = themes.length + 5;
|
|
207
238
|
|
|
208
239
|
function killPreview() {
|
|
209
240
|
if (previewProc) {
|
|
@@ -212,14 +243,14 @@ function picker() {
|
|
|
212
243
|
}
|
|
213
244
|
}
|
|
214
245
|
|
|
215
|
-
function playPreview(
|
|
246
|
+
function playPreview(themeName, event) {
|
|
216
247
|
killPreview();
|
|
217
|
-
const file =
|
|
218
|
-
if (!existsSync(file)) return;
|
|
219
|
-
nowPlaying = `${
|
|
248
|
+
const file = resolvePickerPreview(themeName, event, config);
|
|
249
|
+
if (!file || !existsSync(file)) return;
|
|
250
|
+
nowPlaying = `${themeName} ${event}`;
|
|
220
251
|
previewProc = spawnPlayer(file);
|
|
221
252
|
previewProc.on("close", () => {
|
|
222
|
-
if (nowPlaying === `${
|
|
253
|
+
if (nowPlaying === `${themeName} ${event}`) nowPlaying = "";
|
|
223
254
|
render();
|
|
224
255
|
});
|
|
225
256
|
render();
|
|
@@ -227,7 +258,6 @@ function picker() {
|
|
|
227
258
|
|
|
228
259
|
function render(firstTime) {
|
|
229
260
|
if (!firstTime) {
|
|
230
|
-
// Move cursor up to start of our block and clear
|
|
231
261
|
process.stdout.write(`\x1b[${totalLines}A`);
|
|
232
262
|
}
|
|
233
263
|
|
|
@@ -240,18 +270,27 @@ function picker() {
|
|
|
240
270
|
const arrow = isSelected ? "\x1b[36m> " : " ";
|
|
241
271
|
const color = isSelected ? "\x1b[36m" : "\x1b[90m";
|
|
242
272
|
const reset = "\x1b[0m";
|
|
243
|
-
|
|
244
|
-
|
|
273
|
+
|
|
274
|
+
let active = "";
|
|
275
|
+
if (theme.name === "custom" && isCustomPath(config.idle)) {
|
|
276
|
+
active = " \x1b[32m(current)\x1b[0m";
|
|
277
|
+
} else if (theme.name !== "custom" && theme.name !== "+ Add custom"
|
|
278
|
+
&& config.idle === theme.name && config.input === theme.name) {
|
|
279
|
+
active = " \x1b[32m(current)\x1b[0m";
|
|
280
|
+
}
|
|
281
|
+
|
|
245
282
|
process.stdout.write(
|
|
246
283
|
`\x1b[2K ${arrow}${color}${theme.name.padEnd(maxName + 2)}${theme.desc}${reset}${active}\n`
|
|
247
284
|
);
|
|
248
285
|
});
|
|
249
286
|
|
|
250
287
|
process.stdout.write("\x1b[2K\n");
|
|
288
|
+
const isAddCustom = themes[selected].name === "+ Add custom";
|
|
251
289
|
const playInfo = nowPlaying ? ` \x1b[33m♪ ${nowPlaying}\x1b[0m` : "";
|
|
252
|
-
|
|
253
|
-
`\x1b[2K \x1b[90m[up/down] Navigate [
|
|
254
|
-
|
|
290
|
+
const controls = isAddCustom
|
|
291
|
+
? `\x1b[2K \x1b[90m[up/down] Navigate [enter] Add custom [q] Quit\x1b[0m${playInfo}\n`
|
|
292
|
+
: `\x1b[2K \x1b[90m[up/down] Navigate [<] Play idle [>] Play input [enter] Select [q] Quit\x1b[0m${playInfo}\n`;
|
|
293
|
+
process.stdout.write(controls);
|
|
255
294
|
}
|
|
256
295
|
|
|
257
296
|
const stdin = process.stdin;
|
|
@@ -269,14 +308,12 @@ function picker() {
|
|
|
269
308
|
}
|
|
270
309
|
|
|
271
310
|
function onKey(key) {
|
|
272
|
-
// Ctrl+C
|
|
273
311
|
if (key === "\x03") {
|
|
274
312
|
cleanup();
|
|
275
313
|
console.log("");
|
|
276
314
|
process.exit(0);
|
|
277
315
|
}
|
|
278
316
|
|
|
279
|
-
// q = quit
|
|
280
317
|
if (key === "q" || key === "Q") {
|
|
281
318
|
cleanup();
|
|
282
319
|
console.log("\n No changes made.\n");
|
|
@@ -284,28 +321,23 @@ function picker() {
|
|
|
284
321
|
return;
|
|
285
322
|
}
|
|
286
323
|
|
|
287
|
-
// Enter = confirm
|
|
288
324
|
if (key === "\r" || key === "\n") {
|
|
289
325
|
cleanup();
|
|
290
326
|
resolve(themes[selected].name);
|
|
291
327
|
return;
|
|
292
328
|
}
|
|
293
329
|
|
|
294
|
-
// Arrow up
|
|
295
330
|
if (key === "\x1b[A" || key === "k") {
|
|
296
331
|
selected = (selected - 1 + themes.length) % themes.length;
|
|
297
332
|
render();
|
|
298
333
|
}
|
|
299
|
-
// Arrow down
|
|
300
334
|
else if (key === "\x1b[B" || key === "j") {
|
|
301
335
|
selected = (selected + 1) % themes.length;
|
|
302
336
|
render();
|
|
303
337
|
}
|
|
304
|
-
// Arrow left = preview idle
|
|
305
338
|
else if (key === "\x1b[D") {
|
|
306
339
|
playPreview(themes[selected].name, "idle");
|
|
307
340
|
}
|
|
308
|
-
// Arrow right = preview input
|
|
309
341
|
else if (key === "\x1b[C") {
|
|
310
342
|
playPreview(themes[selected].name, "input");
|
|
311
343
|
}
|
|
@@ -326,13 +358,14 @@ async function install() {
|
|
|
326
358
|
console.log("");
|
|
327
359
|
|
|
328
360
|
if (process.stdin.isTTY) {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
361
|
+
while (true) {
|
|
362
|
+
const choice = await picker();
|
|
363
|
+
if (choice === "+ Add custom") {
|
|
364
|
+
await addCustom();
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (choice) applyPickerChoice(choice);
|
|
368
|
+
break;
|
|
336
369
|
}
|
|
337
370
|
} else {
|
|
338
371
|
console.log(" Run 'agent-noti pick' to choose a sound theme.\n");
|
|
@@ -376,136 +409,159 @@ function sounds() {
|
|
|
376
409
|
console.log(` ${name.padEnd(maxName + 2)} ${desc}${marker}`);
|
|
377
410
|
}
|
|
378
411
|
|
|
379
|
-
if (config.
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
console.log(` ${"(custom input)".padEnd(maxName + 2)} ${config.input} [input]`);
|
|
412
|
+
if (config.customIdle || config.customInput) {
|
|
413
|
+
const isActive = isCustomPath(config.idle) || isCustomPath(config.input);
|
|
414
|
+
const marker = isActive ? " [active]" : "";
|
|
415
|
+
console.log(` ${"custom".padEnd(maxName + 2)} Your custom sounds${marker}`);
|
|
384
416
|
}
|
|
385
417
|
|
|
418
|
+
const idleLabel = isCustomPath(config.idle) ? "custom" : (config.idle || "default");
|
|
419
|
+
const inputLabel = isCustomPath(config.input) ? "custom" : (config.input || "default");
|
|
386
420
|
console.log("");
|
|
387
|
-
console.log(" Theme: idle=%s, input=%s",
|
|
421
|
+
console.log(" Theme: idle=%s, input=%s", idleLabel, inputLabel);
|
|
388
422
|
const volBar = "#".repeat(vol) + "-".repeat(10 - vol);
|
|
389
423
|
console.log(` Volume: [${volBar}] ${vol}/10${muted ? " (MUTED)" : ""}`);
|
|
390
424
|
console.log("");
|
|
391
425
|
}
|
|
392
426
|
|
|
393
|
-
|
|
394
|
-
const themes = getThemeNames();
|
|
427
|
+
// --- Interactive add-custom ---
|
|
395
428
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if (!
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
429
|
+
function selectOption(title, options) {
|
|
430
|
+
return new Promise((resolve) => {
|
|
431
|
+
if (!process.stdin.isTTY) { resolve(null); return; }
|
|
432
|
+
|
|
433
|
+
let selected = 0;
|
|
434
|
+
const totalLines = options.length + 4; // blank + title + blank + options + blank
|
|
435
|
+
|
|
436
|
+
function render(firstTime) {
|
|
437
|
+
if (!firstTime) process.stdout.write(`\x1b[${totalLines}A`);
|
|
438
|
+
process.stdout.write("\x1b[2K\n");
|
|
439
|
+
process.stdout.write(`\x1b[2K \x1b[1m${title}\x1b[0m\n`);
|
|
440
|
+
process.stdout.write("\x1b[2K\n");
|
|
441
|
+
options.forEach((opt, i) => {
|
|
442
|
+
const arrow = i === selected ? "\x1b[36m> " : " ";
|
|
443
|
+
const color = i === selected ? "\x1b[36m" : "\x1b[90m";
|
|
444
|
+
process.stdout.write(`\x1b[2K ${arrow}${color}${opt.label}\x1b[0m\n`);
|
|
445
|
+
});
|
|
446
|
+
process.stdout.write("\x1b[2K\n");
|
|
402
447
|
}
|
|
403
|
-
const config = readConfig();
|
|
404
|
-
config.idle = theme;
|
|
405
|
-
config.input = theme;
|
|
406
|
-
writeConfig(config);
|
|
407
|
-
console.log(`\n Both idle & input set to: ${theme}\n`);
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
448
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
449
|
+
const stdin = process.stdin;
|
|
450
|
+
stdin.setRawMode(true);
|
|
451
|
+
stdin.resume();
|
|
452
|
+
stdin.setEncoding("utf8");
|
|
453
|
+
render(true);
|
|
454
|
+
|
|
455
|
+
function cleanup() {
|
|
456
|
+
stdin.removeListener("data", onKey);
|
|
457
|
+
stdin.setRawMode(false);
|
|
458
|
+
stdin.pause();
|
|
416
459
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
return;
|
|
460
|
+
|
|
461
|
+
function onKey(key) {
|
|
462
|
+
if (key === "\x03") { cleanup(); console.log(""); process.exit(0); }
|
|
463
|
+
if (key === "q" || key === "Q") { cleanup(); resolve(null); return; }
|
|
464
|
+
if (key === "\r" || key === "\n") { cleanup(); resolve(options[selected].value); return; }
|
|
465
|
+
if (key === "\x1b[A" || key === "k") { selected = (selected - 1 + options.length) % options.length; render(); }
|
|
466
|
+
else if (key === "\x1b[B" || key === "j") { selected = (selected + 1) % options.length; render(); }
|
|
421
467
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
468
|
+
stdin.on("data", onKey);
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function promptPath(label) {
|
|
473
|
+
return new Promise((resolve) => {
|
|
474
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
475
|
+
rl.question(` ${label}`, (answer) => {
|
|
476
|
+
rl.close();
|
|
477
|
+
resolve(answer.trim());
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
}
|
|
428
481
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
482
|
+
function copyCustomSound(sourcePath, event) {
|
|
483
|
+
const ext = extname(sourcePath);
|
|
484
|
+
const destName = `custom-${event}${ext}`;
|
|
485
|
+
const destPath = join(CUSTOM_SOUNDS_DIR, destName);
|
|
486
|
+
mkdirSync(CUSTOM_SOUNDS_DIR, { recursive: true });
|
|
487
|
+
copyFileSync(sourcePath, destPath);
|
|
488
|
+
return destPath;
|
|
432
489
|
}
|
|
433
490
|
|
|
434
|
-
function
|
|
435
|
-
if (
|
|
436
|
-
console.log("\n
|
|
437
|
-
console.log(" agent-noti set-custom <path> Set for both idle & input");
|
|
438
|
-
console.log(" agent-noti set-custom <idle|input> <path> Set for one event\n");
|
|
491
|
+
async function addCustom() {
|
|
492
|
+
if (!process.stdin.isTTY) {
|
|
493
|
+
console.log("\n This command requires an interactive terminal.\n");
|
|
439
494
|
return;
|
|
440
495
|
}
|
|
441
496
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
497
|
+
const config = readConfig();
|
|
498
|
+
let idlePath = null;
|
|
499
|
+
|
|
500
|
+
// Step 1: Idle sound
|
|
501
|
+
const idleChoice = await selectOption("Idle sound (when agent finishes):", [
|
|
502
|
+
{ label: "Enter file path", value: "path" },
|
|
503
|
+
{ label: "Skip (use default)", value: "skip" },
|
|
504
|
+
]);
|
|
505
|
+
|
|
506
|
+
if (idleChoice === null) { console.log("\n No changes made.\n"); return; }
|
|
507
|
+
|
|
508
|
+
if (idleChoice === "path") {
|
|
509
|
+
console.log("");
|
|
510
|
+
const p = await promptPath("Path to idle sound: ");
|
|
511
|
+
if (!p) {
|
|
512
|
+
console.log(" No path provided, using default.\n");
|
|
513
|
+
} else if (!existsSync(p)) {
|
|
514
|
+
console.log(` File not found: ${p} — using default.\n`);
|
|
515
|
+
} else {
|
|
516
|
+
idlePath = p;
|
|
517
|
+
const dest = copyCustomSound(p, "idle");
|
|
518
|
+
config.idle = dest;
|
|
519
|
+
config.customIdle = dest;
|
|
520
|
+
console.log(` Copied to: ${dest}\n`);
|
|
453
521
|
}
|
|
454
522
|
}
|
|
455
523
|
|
|
456
|
-
if (!
|
|
457
|
-
|
|
458
|
-
return;
|
|
524
|
+
if (!idlePath && idleChoice !== "skip") {
|
|
525
|
+
config.idle = config.idle || "default";
|
|
459
526
|
}
|
|
460
527
|
|
|
461
|
-
|
|
462
|
-
const
|
|
528
|
+
// Step 2: Input sound
|
|
529
|
+
const inputOptions = [
|
|
530
|
+
{ label: "Enter file path", value: "path" },
|
|
531
|
+
...(idlePath ? [{ label: "Same as idle", value: "same" }] : []),
|
|
532
|
+
{ label: "Skip (use default)", value: "skip" },
|
|
533
|
+
];
|
|
534
|
+
|
|
535
|
+
const inputChoice = await selectOption("Input sound (when agent needs approval):", inputOptions);
|
|
463
536
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
537
|
+
if (inputChoice === null) { console.log("\n No changes made.\n"); return; }
|
|
538
|
+
|
|
539
|
+
if (inputChoice === "path") {
|
|
540
|
+
console.log("");
|
|
541
|
+
const p = await promptPath("Path to input sound: ");
|
|
542
|
+
if (!p) {
|
|
543
|
+
console.log(" No path provided, using default.\n");
|
|
544
|
+
} else if (!existsSync(p)) {
|
|
545
|
+
console.log(` File not found: ${p} — using default.\n`);
|
|
546
|
+
} else {
|
|
547
|
+
const dest = copyCustomSound(p, "input");
|
|
548
|
+
config.input = dest;
|
|
549
|
+
config.customInput = dest;
|
|
550
|
+
console.log(` Copied to: ${dest}\n`);
|
|
551
|
+
}
|
|
552
|
+
} else if (inputChoice === "same" && idlePath) {
|
|
553
|
+
const dest = copyCustomSound(idlePath, "input");
|
|
554
|
+
config.input = dest;
|
|
555
|
+
config.customInput = dest;
|
|
556
|
+
console.log(`\n Input set to same as idle.\n`);
|
|
472
557
|
}
|
|
473
558
|
|
|
474
|
-
// Ensure both
|
|
559
|
+
// Ensure both have values
|
|
475
560
|
if (!config.idle) config.idle = "default";
|
|
476
561
|
if (!config.input) config.input = "default";
|
|
477
562
|
|
|
478
563
|
writeConfig(config);
|
|
479
|
-
console.log(
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
function preview(args) {
|
|
483
|
-
if (args.length < 1) {
|
|
484
|
-
console.log("\n Usage: agent-noti preview <theme>\n");
|
|
485
|
-
return;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
const theme = args[0];
|
|
489
|
-
const themes = getThemeNames();
|
|
490
|
-
|
|
491
|
-
if (!themes.includes(theme)) {
|
|
492
|
-
console.log(`\n Unknown theme: ${theme}`);
|
|
493
|
-
console.log(` Available: ${themes.join(", ")}\n`);
|
|
494
|
-
return;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
console.log("");
|
|
498
|
-
for (const event of ["idle", "input"]) {
|
|
499
|
-
const file = findThemeFile(theme, event);
|
|
500
|
-
console.log(` Playing ${theme} ${event}...`);
|
|
501
|
-
try {
|
|
502
|
-
execSync(`node "${PLAY_SCRIPT}" --file "${file}"`, { stdio: "inherit" });
|
|
503
|
-
execSync(process.platform === "win32" ? "timeout /t 2 >nul" : "sleep 2");
|
|
504
|
-
} catch {
|
|
505
|
-
console.log(` Could not play ${theme}-${event}`);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
console.log("");
|
|
564
|
+
console.log(" Custom sounds applied.\n");
|
|
509
565
|
}
|
|
510
566
|
|
|
511
567
|
function mute() {
|
|
@@ -551,10 +607,14 @@ function reset() {
|
|
|
551
607
|
console.log("\n Reset to defaults (theme=default, volume=10, unmuted).\n");
|
|
552
608
|
}
|
|
553
609
|
|
|
554
|
-
|
|
555
|
-
const
|
|
556
|
-
if (choice) {
|
|
557
|
-
|
|
610
|
+
function applyPickerChoice(choice) {
|
|
611
|
+
const config = readConfig();
|
|
612
|
+
if (choice === "custom") {
|
|
613
|
+
config.idle = config.customIdle || "default";
|
|
614
|
+
config.input = config.customInput || "default";
|
|
615
|
+
writeConfig(config);
|
|
616
|
+
console.log(`\n Theme set to: custom\n`);
|
|
617
|
+
} else {
|
|
558
618
|
config.idle = choice;
|
|
559
619
|
config.input = choice;
|
|
560
620
|
writeConfig(config);
|
|
@@ -562,6 +622,18 @@ async function pick() {
|
|
|
562
622
|
}
|
|
563
623
|
}
|
|
564
624
|
|
|
625
|
+
async function pick() {
|
|
626
|
+
while (true) {
|
|
627
|
+
const choice = await picker();
|
|
628
|
+
if (choice === "+ Add custom") {
|
|
629
|
+
await addCustom();
|
|
630
|
+
continue; // restart picker to show updated custom entry
|
|
631
|
+
}
|
|
632
|
+
if (choice) applyPickerChoice(choice);
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
565
637
|
// --- Main ---
|
|
566
638
|
|
|
567
639
|
const cmd = process.argv[2];
|
|
@@ -569,33 +641,28 @@ const args = process.argv.slice(3);
|
|
|
569
641
|
|
|
570
642
|
async function main() {
|
|
571
643
|
switch (cmd) {
|
|
572
|
-
case "install":
|
|
573
|
-
case "uninstall":
|
|
574
|
-
case "test":
|
|
575
|
-
case "sounds":
|
|
576
|
-
case "
|
|
577
|
-
case "
|
|
578
|
-
case "
|
|
579
|
-
case "
|
|
580
|
-
case "
|
|
581
|
-
case "
|
|
582
|
-
case "unmute": unmute(); break;
|
|
583
|
-
case "volume": volume(args); break;
|
|
644
|
+
case "install": case "i": await install(); break;
|
|
645
|
+
case "uninstall": uninstall(); break;
|
|
646
|
+
case "test": case "t": test(); break;
|
|
647
|
+
case "sounds": case "s": sounds(); break;
|
|
648
|
+
case "pick": case "p": await pick(); break;
|
|
649
|
+
case "add-custom": case "ac": await addCustom(); break;
|
|
650
|
+
case "volume": case "v": volume(args); break;
|
|
651
|
+
case "mute": case "m": mute(); break;
|
|
652
|
+
case "unmute": case "u": unmute(); break;
|
|
653
|
+
case "reset": case "r": reset(); break;
|
|
584
654
|
default:
|
|
585
655
|
console.log("");
|
|
586
|
-
console.log(" agent-noti install
|
|
587
|
-
console.log(" agent-noti uninstall
|
|
588
|
-
console.log(" agent-noti test
|
|
589
|
-
console.log(" agent-noti sounds
|
|
590
|
-
console.log(" agent-noti pick
|
|
591
|
-
console.log(" agent-noti
|
|
592
|
-
console.log(" agent-noti
|
|
593
|
-
console.log(" agent-noti
|
|
594
|
-
console.log(" agent-noti
|
|
595
|
-
console.log(" agent-noti
|
|
596
|
-
console.log(" agent-noti mute Mute notifications");
|
|
597
|
-
console.log(" agent-noti unmute Unmute notifications");
|
|
598
|
-
console.log(" agent-noti reset Reset everything");
|
|
656
|
+
console.log(" agent-noti install (i) Add hooks + pick theme");
|
|
657
|
+
console.log(" agent-noti uninstall Remove hooks");
|
|
658
|
+
console.log(" agent-noti test (t) Play current sounds");
|
|
659
|
+
console.log(" agent-noti sounds (s) List available themes");
|
|
660
|
+
console.log(" agent-noti pick (p) Interactive sound picker");
|
|
661
|
+
console.log(" agent-noti add-custom(ac) Use your own sound files");
|
|
662
|
+
console.log(" agent-noti volume (v) Set volume <1-10>");
|
|
663
|
+
console.log(" agent-noti mute (m) Mute notifications");
|
|
664
|
+
console.log(" agent-noti unmute (u) Unmute notifications");
|
|
665
|
+
console.log(" agent-noti reset (r) Reset everything");
|
|
599
666
|
console.log("");
|
|
600
667
|
}
|
|
601
668
|
}
|