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.
Files changed (3) hide show
  1. package/README.md +80 -11
  2. package/bin/cli.mjs +228 -161
  3. 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 to both Claude Code and Codex automatically. Restart your agent.
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 | `idle.mp3` | Stop | agent-turn-complete |
20
- | Needs your input | `input.mp3` | PermissionRequest | approval-requested |
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 test # Play both sounds
26
- agent-noti install # Re-add hooks (if needed)
27
- agent-noti uninstall # Remove hooks
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 = SOUND_THEMES;
226
+ const themes = buildPickerThemes();
198
227
  const config = readConfig();
199
- const currentTheme = config.idle || "default";
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
- // Total lines we render (for redraw cursor math)
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(theme, event) {
246
+ function playPreview(themeName, event) {
216
247
  killPreview();
217
- const file = findThemeFile(theme, event);
218
- if (!existsSync(file)) return;
219
- nowPlaying = `${theme} ${event}`;
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 === `${theme} ${event}`) 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
- const active = config.idle === theme.name && config.input === theme.name
244
- ? " \x1b[32m(current)\x1b[0m" : "";
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
- process.stdout.write(
253
- `\x1b[2K \x1b[90m[up/down] Navigate [<] Idle [>] Input [enter] Select [q] Quit\x1b[0m${playInfo}\n`
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
- const choice = await picker();
330
- if (choice) {
331
- const config = readConfig();
332
- config.idle = choice;
333
- config.input = choice;
334
- writeConfig(config);
335
- console.log(`\n Theme set to: ${choice}\n`);
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.idle && config.idle.startsWith("/")) {
380
- console.log(` ${"(custom idle)".padEnd(maxName + 2)} ${config.idle} [idle]`);
381
- }
382
- if (config.input && config.input.startsWith("/")) {
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", config.idle || "default", config.input || "default");
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
- function set(args) {
394
- const themes = getThemeNames();
427
+ // --- Interactive add-custom ---
395
428
 
396
- if (args.length === 1) {
397
- const theme = args[0];
398
- if (!themes.includes(theme)) {
399
- console.log(`\n Unknown theme: ${theme}`);
400
- console.log(` Available: ${themes.join(", ")}\n`);
401
- return;
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
- if (args.length === 2) {
412
- const [event, theme] = args;
413
- if (event !== "idle" && event !== "input") {
414
- console.log(`\n Invalid event: ${event} (use idle or input)\n`);
415
- return;
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
- if (!themes.includes(theme)) {
418
- console.log(`\n Unknown theme: ${theme}`);
419
- console.log(` Available: ${themes.join(", ")}\n`);
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
- const config = readConfig();
423
- config[event] = theme;
424
- writeConfig(config);
425
- console.log(`\n ${event} sound set to: ${theme}\n`);
426
- return;
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
- console.log("\n Usage:");
430
- console.log(" agent-noti set <theme> Set both idle & input");
431
- console.log(" agent-noti set <idle|input> <theme> Set one event\n");
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 setCustom(args) {
435
- if (args.length < 1 || args.length > 2) {
436
- console.log("\n Usage:");
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
- // set-custom <path> — sets both events to same custom file
443
- // set-custom <event> <path> — sets one event
444
- let event, sourcePath;
445
- if (args.length === 1) {
446
- sourcePath = args[0];
447
- event = null; // both
448
- } else {
449
- [event, sourcePath] = args;
450
- if (event !== "idle" && event !== "input") {
451
- console.log(`\n Invalid event: ${event} (use idle or input)\n`);
452
- return;
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 (!existsSync(sourcePath)) {
457
- console.log(`\n File not found: ${sourcePath}\n`);
458
- return;
524
+ if (!idlePath && idleChoice !== "skip") {
525
+ config.idle = config.idle || "default";
459
526
  }
460
527
 
461
- const ext = extname(sourcePath);
462
- const config = readConfig();
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
- const events = event ? [event] : ["idle", "input"];
465
- for (const ev of events) {
466
- const destName = `custom-${ev}${ext}`;
467
- const destPath = join(CUSTOM_SOUNDS_DIR, destName);
468
- mkdirSync(CUSTOM_SOUNDS_DIR, { recursive: true });
469
- copyFileSync(sourcePath, destPath);
470
- config[ev] = destPath;
471
- console.log(`\n Copied to: ${destPath}`);
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 events have a value (fallback to default)
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(` Custom sound applied.\n`);
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
- async function pick() {
555
- const choice = await picker();
556
- if (choice) {
557
- const config = readConfig();
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": await install(); break;
573
- case "uninstall": uninstall(); break;
574
- case "test": test(); break;
575
- case "sounds": sounds(); break;
576
- case "set": set(args); break;
577
- case "set-custom": setCustom(args); break;
578
- case "preview": preview(args); break;
579
- case "reset": reset(); break;
580
- case "pick": await pick(); break;
581
- case "mute": mute(); break;
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 Add hooks + pick theme");
587
- console.log(" agent-noti uninstall Remove hooks");
588
- console.log(" agent-noti test Play current sounds");
589
- console.log(" agent-noti sounds List available themes");
590
- console.log(" agent-noti pick Interactive sound picker");
591
- console.log(" agent-noti set <theme> Set both idle & input");
592
- console.log(" agent-noti set <idle|input> <theme> Set one event");
593
- console.log(" agent-noti set-custom [idle|input] <file> Use custom sound");
594
- console.log(" agent-noti preview <theme> Preview a theme");
595
- console.log(" agent-noti volume <1-10> Set volume level");
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-noti",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Audio notifications for Claude Code & Codex — customizable sound themes",
5
5
  "bin": {
6
6
  "agent-noti": "./bin/cli.mjs"