pingthings 0.3.0 → 0.5.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 (119) hide show
  1. package/README.md +24 -0
  2. package/bin/pingthings.js +12 -0
  3. package/package.json +1 -1
  4. package/packs/0ad-civilizations/manifest.json +1 -0
  5. package/packs/7kaa-soldiers/manifest.json +1 -0
  6. package/packs/fighting-announcer/manifest.json +62 -0
  7. package/packs/fighting-announcer/sounds/begin.wav +0 -0
  8. package/packs/fighting-announcer/sounds/complete.wav +0 -0
  9. package/packs/fighting-announcer/sounds/congratulations.wav +0 -0
  10. package/packs/fighting-announcer/sounds/defeat.wav +0 -0
  11. package/packs/fighting-announcer/sounds/failure.wav +0 -0
  12. package/packs/fighting-announcer/sounds/fight.wav +0 -0
  13. package/packs/fighting-announcer/sounds/final-round.wav +0 -0
  14. package/packs/fighting-announcer/sounds/finish.wav +0 -0
  15. package/packs/fighting-announcer/sounds/game-over.wav +0 -0
  16. package/packs/fighting-announcer/sounds/get-set.wav +0 -0
  17. package/packs/fighting-announcer/sounds/go.wav +0 -0
  18. package/packs/fighting-announcer/sounds/great.wav +0 -0
  19. package/packs/fighting-announcer/sounds/high-score.wav +0 -0
  20. package/packs/fighting-announcer/sounds/new-record.wav +0 -0
  21. package/packs/fighting-announcer/sounds/no-contest.wav +0 -0
  22. package/packs/fighting-announcer/sounds/ready.wav +0 -0
  23. package/packs/fighting-announcer/sounds/sudden-death.wav +0 -0
  24. package/packs/fighting-announcer/sounds/victory.wav +0 -0
  25. package/packs/fighting-announcer/sounds/you-lose.wav +0 -0
  26. package/packs/fighting-announcer/sounds/you-win.wav +0 -0
  27. package/packs/freedoom-arsenal/manifest.json +1 -0
  28. package/packs/kenney-interface/manifest.json +58 -0
  29. package/packs/kenney-interface/sounds/back-1.wav +0 -0
  30. package/packs/kenney-interface/sounds/bong.wav +0 -0
  31. package/packs/kenney-interface/sounds/click-1.wav +0 -0
  32. package/packs/kenney-interface/sounds/close-1.wav +0 -0
  33. package/packs/kenney-interface/sounds/confirmation-1.wav +0 -0
  34. package/packs/kenney-interface/sounds/confirmation-2.wav +0 -0
  35. package/packs/kenney-interface/sounds/confirmation-3.wav +0 -0
  36. package/packs/kenney-interface/sounds/error-1.wav +0 -0
  37. package/packs/kenney-interface/sounds/error-2.wav +0 -0
  38. package/packs/kenney-interface/sounds/error-3.wav +0 -0
  39. package/packs/kenney-interface/sounds/glitch-1.wav +0 -0
  40. package/packs/kenney-interface/sounds/maximize-1.wav +0 -0
  41. package/packs/kenney-interface/sounds/maximize-2.wav +0 -0
  42. package/packs/kenney-interface/sounds/open-1.wav +0 -0
  43. package/packs/kenney-interface/sounds/pluck-1.wav +0 -0
  44. package/packs/kenney-interface/sounds/question-1.wav +0 -0
  45. package/packs/kenney-interface/sounds/question-2.wav +0 -0
  46. package/packs/kenney-interface/sounds/question-3.wav +0 -0
  47. package/packs/kenney-scifi/manifest.json +58 -0
  48. package/packs/kenney-scifi/sounds/computer-1.wav +0 -0
  49. package/packs/kenney-scifi/sounds/computer-2.wav +0 -0
  50. package/packs/kenney-scifi/sounds/computer-3.wav +0 -0
  51. package/packs/kenney-scifi/sounds/door-close.wav +0 -0
  52. package/packs/kenney-scifi/sounds/door-open.wav +0 -0
  53. package/packs/kenney-scifi/sounds/engine-1.wav +0 -0
  54. package/packs/kenney-scifi/sounds/explosion-1.wav +0 -0
  55. package/packs/kenney-scifi/sounds/explosion-2.wav +0 -0
  56. package/packs/kenney-scifi/sounds/forcefield-1.wav +0 -0
  57. package/packs/kenney-scifi/sounds/forcefield-2.wav +0 -0
  58. package/packs/kenney-scifi/sounds/impact-1.wav +0 -0
  59. package/packs/kenney-scifi/sounds/impact-2.wav +0 -0
  60. package/packs/kenney-scifi/sounds/laser-large.wav +0 -0
  61. package/packs/kenney-scifi/sounds/laser-retro.wav +0 -0
  62. package/packs/kenney-scifi/sounds/low-explosion.wav +0 -0
  63. package/packs/kenney-scifi/sounds/slime.wav +0 -0
  64. package/packs/kenney-scifi/sounds/thruster-1.wav +0 -0
  65. package/packs/kenney-scifi/sounds/thruster-2.wav +0 -0
  66. package/packs/openarena-announcer/manifest.json +1 -0
  67. package/packs/retro-8bit/manifest.json +58 -0
  68. package/packs/retro-8bit/sounds/button-1.wav +0 -0
  69. package/packs/retro-8bit/sounds/button-2.wav +0 -0
  70. package/packs/retro-8bit/sounds/button-3.wav +0 -0
  71. package/packs/retro-8bit/sounds/coin-1.wav +0 -0
  72. package/packs/retro-8bit/sounds/coin-2.wav +0 -0
  73. package/packs/retro-8bit/sounds/coin-cluster.wav +0 -0
  74. package/packs/retro-8bit/sounds/coin-double.wav +0 -0
  75. package/packs/retro-8bit/sounds/damage-1.wav +0 -0
  76. package/packs/retro-8bit/sounds/error-1.wav +0 -0
  77. package/packs/retro-8bit/sounds/error-2.wav +0 -0
  78. package/packs/retro-8bit/sounds/error-3.wav +0 -0
  79. package/packs/retro-8bit/sounds/fanfare-1.wav +0 -0
  80. package/packs/retro-8bit/sounds/fanfare-2.wav +0 -0
  81. package/packs/retro-8bit/sounds/fanfare-3.wav +0 -0
  82. package/packs/retro-8bit/sounds/negative-1.wav +0 -0
  83. package/packs/retro-8bit/sounds/negative-2.wav +0 -0
  84. package/packs/retro-8bit/sounds/powerup-1.wav +0 -0
  85. package/packs/retro-8bit/sounds/powerup-2.wav +0 -0
  86. package/packs/warzone2100-command/manifest.json +1 -0
  87. package/packs/wesnoth-combat/manifest.json +2 -1
  88. package/packs/xonotic-announcer/manifest.json +52 -0
  89. package/packs/xonotic-announcer/sounds/1fragleft.wav +0 -0
  90. package/packs/xonotic-announcer/sounds/airshot.wav +0 -0
  91. package/packs/xonotic-announcer/sounds/amazing.wav +0 -0
  92. package/packs/xonotic-announcer/sounds/awesome.wav +0 -0
  93. package/packs/xonotic-announcer/sounds/begin.wav +0 -0
  94. package/packs/xonotic-announcer/sounds/botlike.wav +0 -0
  95. package/packs/xonotic-announcer/sounds/electrobitch.wav +0 -0
  96. package/packs/xonotic-announcer/sounds/headshot.wav +0 -0
  97. package/packs/xonotic-announcer/sounds/impressive.wav +0 -0
  98. package/packs/xonotic-announcer/sounds/lastsecond.wav +0 -0
  99. package/packs/xonotic-announcer/sounds/leadgained.wav +0 -0
  100. package/packs/xonotic-announcer/sounds/narrowly.wav +0 -0
  101. package/packs/xonotic-announcer/sounds/prepareforbattle.wav +0 -0
  102. package/packs/xonotic-announcer/sounds/terminated.wav +0 -0
  103. package/packs/xonotic-announcer/sounds/yoda.wav +0 -0
  104. package/src/cli/browse.js +75 -0
  105. package/src/cli/completions.js +182 -0
  106. package/src/cli/config.js +32 -3
  107. package/src/cli/create.js +1 -1
  108. package/src/cli/doctor.js +134 -0
  109. package/src/cli/init.js +11 -8
  110. package/src/cli/play.js +22 -1
  111. package/src/cli/random-pack.js +44 -0
  112. package/src/cli/search.js +81 -0
  113. package/src/cli/select.js +36 -32
  114. package/src/cli/sounds.js +76 -0
  115. package/src/cli/theme.js +34 -1
  116. package/src/cli/uninstall.js +5 -0
  117. package/src/config.js +30 -0
  118. package/src/packs.js +1 -0
  119. package/src/player.js +17 -12
@@ -0,0 +1,75 @@
1
+ import { readConfig } from '../config.js';
2
+ import { listPacks } from '../packs.js';
3
+
4
+ function showHelp() {
5
+ console.log(`
6
+ Usage: pingthings browse [category]
7
+
8
+ Browse packs by category.
9
+
10
+ With no arguments, shows all categories with pack counts.
11
+ With a category, shows packs in that category.
12
+
13
+ Categories: military, arena, fantasy, sci-fi, fps, retro, ui, transport, spooky, racing
14
+
15
+ Examples:
16
+ pingthings browse List all categories
17
+ pingthings browse military Show military packs
18
+ pingthings browse sci-fi Show sci-fi packs
19
+ `);
20
+ }
21
+
22
+ export default function browse(args) {
23
+ if (args.includes('--help') || args.includes('-h')) {
24
+ showHelp();
25
+ return;
26
+ }
27
+
28
+ const config = readConfig();
29
+ const packs = listPacks();
30
+ const category = args[0];
31
+
32
+ if (!category) {
33
+ // Show all categories with counts
34
+ const categories = {};
35
+ for (const pack of packs) {
36
+ const cat = pack.category || 'other';
37
+ if (!categories[cat]) categories[cat] = [];
38
+ categories[cat].push(pack);
39
+ }
40
+
41
+ console.log('\nCategories:\n');
42
+ for (const [cat, catPacks] of Object.entries(categories).sort()) {
43
+ const totalSounds = catPacks.reduce((sum, p) => sum + p.soundCount, 0);
44
+ console.log(` ${cat.padEnd(14)} ${catPacks.length} pack${catPacks.length === 1 ? '' : 's'}, ${totalSounds} sounds`);
45
+ for (const p of catPacks) {
46
+ const active = p.name === config.activePack ? ' *' : ' ';
47
+ console.log(` ${active} └ ${p.name}`);
48
+ }
49
+ }
50
+ console.log('\n * = active pack');
51
+ console.log(' Run "pingthings browse <category>" to see details.\n');
52
+ return;
53
+ }
54
+
55
+ // Show packs in a specific category
56
+ const filtered = packs.filter(p => (p.category || 'other') === category);
57
+
58
+ if (filtered.length === 0) {
59
+ console.error(`No packs found in category: ${category}`);
60
+ const allCats = [...new Set(packs.map(p => p.category || 'other'))].sort();
61
+ console.error(`Available categories: ${allCats.join(', ')}`);
62
+ process.exit(1);
63
+ }
64
+
65
+ console.log(`\n${category} packs:\n`);
66
+ for (const pack of filtered) {
67
+ const active = pack.name === config.activePack ? ' *' : ' ';
68
+ console.log(`${active} ${pack.name} (${pack.soundCount} sounds)`);
69
+ if (pack.description) {
70
+ console.log(` ${pack.description}`);
71
+ }
72
+ console.log(` License: ${pack.license}`);
73
+ }
74
+ console.log('\n * = active pack\n');
75
+ }
@@ -0,0 +1,182 @@
1
+ function showHelp() {
2
+ console.log(`
3
+ Usage: pingthings completions <shell>
4
+
5
+ Generate shell completion script.
6
+
7
+ Arguments:
8
+ shell bash, zsh, or fish
9
+
10
+ Install:
11
+ pingthings completions bash >> ~/.bashrc
12
+ pingthings completions zsh >> ~/.zshrc
13
+ pingthings completions fish > ~/.config/fish/completions/pingthings.fish
14
+ `);
15
+ }
16
+
17
+ const COMMANDS = [
18
+ 'play', 'list', 'select', 'browse', 'search', 'sounds',
19
+ 'use', 'preview', 'test-events', 'theme', 'config',
20
+ 'init', 'create', 'install', 'uninstall', 'random-pack', 'doctor', 'completions',
21
+ ];
22
+
23
+ const THEMES = [
24
+ 'retro', 'sci-fi', 'arena', 'fantasy', 'ancient',
25
+ 'professional', '8bit', 'space', 'chaos', 'reset',
26
+ ];
27
+
28
+ const EVENTS = ['done', 'permission', 'complete', 'error', 'blocked'];
29
+
30
+ function bash() {
31
+ return `# pingthings bash completions
32
+ _pingthings() {
33
+ local cur prev commands
34
+ COMPREPLY=()
35
+ cur="\${COMP_WORDS[COMP_CWORD]}"
36
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
37
+ commands="${COMMANDS.join(' ')}"
38
+
39
+ case "\${prev}" in
40
+ pingthings)
41
+ COMPREPLY=( $(compgen -W "\${commands}" -- "\${cur}") )
42
+ return 0
43
+ ;;
44
+ use|preview|sounds|test-events|uninstall)
45
+ local packs=$(pingthings list 2>/dev/null | grep -oE '\\S+-\\S+' | head -20)
46
+ COMPREPLY=( $(compgen -W "\${packs}" -- "\${cur}") )
47
+ return 0
48
+ ;;
49
+ theme)
50
+ COMPREPLY=( $(compgen -W "${THEMES.join(' ')}" -- "\${cur}") )
51
+ return 0
52
+ ;;
53
+ --event|-e)
54
+ COMPREPLY=( $(compgen -W "${EVENTS.join(' ')}" -- "\${cur}") )
55
+ return 0
56
+ ;;
57
+ browse)
58
+ local cats="military arena fantasy sci-fi fps retro ui transport spooky racing"
59
+ COMPREPLY=( $(compgen -W "\${cats}" -- "\${cur}") )
60
+ return 0
61
+ ;;
62
+ completions)
63
+ COMPREPLY=( $(compgen -W "bash zsh fish" -- "\${cur}") )
64
+ return 0
65
+ ;;
66
+ esac
67
+
68
+ return 0
69
+ }
70
+ complete -F _pingthings pingthings`;
71
+ }
72
+
73
+ function zsh() {
74
+ return `# pingthings zsh completions
75
+ _pingthings() {
76
+ local -a commands themes events
77
+ commands=(
78
+ 'play:Play a sound from the active pack'
79
+ 'list:Show available sound packs'
80
+ 'select:Interactive pack selector'
81
+ 'browse:Browse packs by category'
82
+ 'search:Search packs and sounds'
83
+ 'sounds:List individual sounds in a pack'
84
+ 'use:Set the active sound pack'
85
+ 'preview:Preview a random sound from a pack'
86
+ 'test-events:Play all event sounds'
87
+ 'theme:Apply a sound theme'
88
+ 'config:Show or update configuration'
89
+ 'init:Set up Claude Code hooks'
90
+ 'create:Create a pack from audio files'
91
+ 'install:Install a pack from GitHub or local path'
92
+ 'uninstall:Remove a user-installed pack'
93
+ 'random-pack:Switch to a random pack'
94
+ 'doctor:Diagnose audio setup'
95
+ 'completions:Generate shell completions'
96
+ )
97
+ themes=(${THEMES.join(' ')})
98
+ events=(${EVENTS.join(' ')})
99
+
100
+ _arguments '1:command:->cmds' '*::arg:->args'
101
+
102
+ case "$state" in
103
+ cmds)
104
+ _describe -t commands 'pingthings command' commands
105
+ ;;
106
+ args)
107
+ case $words[1] in
108
+ use|preview|sounds|test-events|uninstall)
109
+ local packs=($(pingthings list 2>/dev/null | grep -oE '\\S+-\\S+' | head -20))
110
+ _describe -t packs 'pack' packs
111
+ ;;
112
+ theme)
113
+ _describe -t themes 'theme' themes
114
+ ;;
115
+ browse)
116
+ local cats=(military arena fantasy sci-fi fps retro ui transport spooky racing)
117
+ _describe -t categories 'category' cats
118
+ ;;
119
+ completions)
120
+ _describe -t shells 'shell' '(bash zsh fish)'
121
+ ;;
122
+ esac
123
+ ;;
124
+ esac
125
+ }
126
+ compdef _pingthings pingthings`;
127
+ }
128
+
129
+ function fish() {
130
+ const lines = [
131
+ '# pingthings fish completions',
132
+ 'complete -c pingthings -f',
133
+ ];
134
+
135
+ const cmdDescs = {
136
+ play: 'Play a sound', list: 'Show packs', select: 'Interactive picker',
137
+ browse: 'Browse by category', search: 'Search packs', sounds: 'List sounds',
138
+ use: 'Set active pack', preview: 'Preview a pack', 'test-events': 'Test event sounds',
139
+ theme: 'Apply a theme', config: 'Configuration', init: 'Set up hooks',
140
+ create: 'Create a pack', install: 'Install a pack', uninstall: 'Remove a pack',
141
+ 'random-pack': 'Random pack', doctor: 'Diagnose setup', completions: 'Shell completions',
142
+ };
143
+
144
+ for (const [cmd, desc] of Object.entries(cmdDescs)) {
145
+ lines.push(`complete -c pingthings -n '__fish_use_subcommand' -a '${cmd}' -d '${desc}'`);
146
+ }
147
+
148
+ for (const t of THEMES) {
149
+ lines.push(`complete -c pingthings -n '__fish_seen_subcommand_from theme' -a '${t}'`);
150
+ }
151
+
152
+ for (const e of EVENTS) {
153
+ lines.push(`complete -c pingthings -n '__fish_seen_subcommand_from play' -l 'event' -a '${e}'`);
154
+ }
155
+
156
+ return lines.join('\n');
157
+ }
158
+
159
+ export default function completions(args) {
160
+ const shell = args[0];
161
+
162
+ if (!shell || shell === '--help' || shell === '-h') {
163
+ showHelp();
164
+ if (!shell) process.exit(1);
165
+ return;
166
+ }
167
+
168
+ switch (shell) {
169
+ case 'bash':
170
+ console.log(bash());
171
+ break;
172
+ case 'zsh':
173
+ console.log(zsh());
174
+ break;
175
+ case 'fish':
176
+ console.log(fish());
177
+ break;
178
+ default:
179
+ console.error(`Unknown shell: ${shell}. Use bash, zsh, or fish.`);
180
+ process.exit(1);
181
+ }
182
+ }
package/src/cli/config.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { readConfig, writeConfig, VALID_MODES, VALID_EVENTS } from '../config.js';
2
2
 
3
- const VALID_KEYS = ['activePack', 'mode', 'specificSound', 'volume', 'eventPacks'];
3
+ const VALID_KEYS = ['activePack', 'mode', 'specificSound', 'volume', 'eventPacks', 'cooldown', 'quietHours'];
4
4
 
5
5
  function showHelp() {
6
6
  console.log(`
@@ -17,13 +17,16 @@ Keys:
17
17
  mode random, specific, or informational
18
18
  specificSound Sound name for specific mode
19
19
  volume Playback volume (0-100)
20
+ cooldown Avoid repeating the same sound (true/false)
21
+ quietHours Mute during hours, e.g. "22-7" (10pm-7am)
20
22
  eventPacks Per-event pack overrides (use "pingthings config eventPacks.<event> <pack>")
21
23
 
22
24
  Examples:
23
25
  pingthings config Show full config
24
- pingthings config volume Show current volume
25
26
  pingthings config volume 50 Set volume to 50%
26
- pingthings config mode informational Enable event-based sounds
27
+ pingthings config cooldown false Disable cooldown
28
+ pingthings config quietHours 22-7 Mute 10pm to 7am
29
+ pingthings config quietHours null Disable quiet hours
27
30
  pingthings config eventPacks.error openarena-announcer
28
31
  `);
29
32
  }
@@ -109,6 +112,32 @@ export default function config(args) {
109
112
  return;
110
113
  }
111
114
 
115
+ // Validate cooldown
116
+ if (key === 'cooldown') {
117
+ if (value !== 'true' && value !== 'false') {
118
+ console.error('Cooldown must be true or false.');
119
+ process.exit(1);
120
+ }
121
+ const cfg = readConfig();
122
+ cfg.cooldown = value === 'true';
123
+ writeConfig(cfg);
124
+ console.log(`cooldown set to: ${cfg.cooldown}`);
125
+ return;
126
+ }
127
+
128
+ // Validate quietHours
129
+ if (key === 'quietHours') {
130
+ if (value !== 'null' && !/^\d{1,2}-\d{1,2}$/.test(value)) {
131
+ console.error('Quiet hours must be in format "HH-HH" (e.g., "22-7") or "null" to disable.');
132
+ process.exit(1);
133
+ }
134
+ const cfg = readConfig();
135
+ cfg.quietHours = value === 'null' ? null : value;
136
+ writeConfig(cfg);
137
+ console.log(`quietHours set to: ${cfg.quietHours ?? '(disabled)'}`);
138
+ return;
139
+ }
140
+
112
141
  // Two args: set value
113
142
  const cfg = readConfig();
114
143
  cfg[key] = value === 'null' ? null : value;
package/src/cli/create.js CHANGED
@@ -24,7 +24,7 @@ Examples:
24
24
  `);
25
25
  }
26
26
 
27
- export default async function create(args) {
27
+ export default function create(args) {
28
28
  if (args.includes('--help') || args.includes('-h')) {
29
29
  showHelp();
30
30
  return;
@@ -0,0 +1,134 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { platform, homedir } from 'node:os';
5
+ import { readConfig, getConfigDir } from '../config.js';
6
+ import { listPacks } from '../packs.js';
7
+
8
+ function showHelp() {
9
+ console.log(`
10
+ Usage: pingthings doctor
11
+
12
+ Diagnose audio setup, check player availability, verify packs,
13
+ and validate Claude Code hook configuration.
14
+ `);
15
+ }
16
+
17
+ function check(label, fn) {
18
+ try {
19
+ const result = fn();
20
+ if (result === true) {
21
+ console.log(` OK ${label}`);
22
+ } else if (result === false) {
23
+ console.log(` !! ${label}`);
24
+ } else {
25
+ console.log(` OK ${label}: ${result}`);
26
+ }
27
+ return result;
28
+ } catch (err) {
29
+ console.log(` !! ${label}: ${err.message}`);
30
+ return false;
31
+ }
32
+ }
33
+
34
+ function commandExists(cmd) {
35
+ try {
36
+ execFileSync('which', [cmd], { stdio: 'pipe' });
37
+ return true;
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
42
+
43
+ export default function doctor(args) {
44
+ if (args.includes('--help') || args.includes('-h')) {
45
+ showHelp();
46
+ return;
47
+ }
48
+
49
+ console.log('\npingthings doctor\n');
50
+
51
+ // Platform
52
+ console.log('Platform:');
53
+ check('Operating system', () => {
54
+ const os = platform();
55
+ const names = { darwin: 'macOS', linux: 'Linux', win32: 'Windows' };
56
+ return names[os] || os;
57
+ });
58
+
59
+ // Audio player
60
+ console.log('\nAudio player:');
61
+ let hasPlayer = false;
62
+ if (platform() === 'darwin') {
63
+ hasPlayer = check('afplay available', () => commandExists('afplay'));
64
+ } else if (platform() === 'linux') {
65
+ const pa = check('paplay available (PulseAudio/PipeWire)', () => commandExists('paplay'));
66
+ const al = check('aplay available (ALSA)', () => commandExists('aplay'));
67
+ hasPlayer = pa || al;
68
+ } else if (platform() === 'win32') {
69
+ hasPlayer = check('PowerShell available', () => commandExists('powershell'));
70
+ }
71
+ if (!hasPlayer) {
72
+ console.log('\n No audio player found! Sounds will not play.');
73
+ if (platform() === 'linux') {
74
+ console.log(' Install: sudo apt install pulseaudio-utils (Ubuntu/Debian)');
75
+ console.log(' Or: sudo apt install alsa-utils');
76
+ }
77
+ }
78
+
79
+ // Config
80
+ console.log('\nConfiguration:');
81
+ const config = readConfig();
82
+ check('Config directory', () => getConfigDir());
83
+ check('Active pack', () => config.activePack);
84
+ check('Mode', () => config.mode);
85
+ check('Volume', () => `${config.volume}%`);
86
+ if (config.quietHours) {
87
+ check('Quiet hours', () => config.quietHours);
88
+ }
89
+
90
+ // Packs
91
+ console.log('\nPacks:');
92
+ const packs = listPacks();
93
+ check('Built-in packs', () => {
94
+ const builtIn = packs.filter(p => p.isBuiltIn);
95
+ return `${builtIn.length} packs, ${builtIn.reduce((s, p) => s + p.soundCount, 0)} sounds`;
96
+ });
97
+ check('User packs', () => {
98
+ const user = packs.filter(p => !p.isBuiltIn);
99
+ return user.length > 0 ? `${user.length} packs` : 'none';
100
+ });
101
+
102
+ // Active pack validation
103
+ const activePack = packs.find(p => p.name === config.activePack);
104
+ if (!activePack) {
105
+ check('Active pack exists', () => { throw new Error(`"${config.activePack}" not found`); });
106
+ } else {
107
+ check('Active pack valid', () => `${activePack.name} (${activePack.soundCount} sounds)`);
108
+ }
109
+
110
+ // Claude Code hooks
111
+ console.log('\nClaude Code integration:');
112
+ const settingsPath = join(homedir(), '.claude', 'settings.json');
113
+ if (existsSync(settingsPath)) {
114
+ try {
115
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
116
+ const hooks = settings.hooks || {};
117
+ const hasPingthings = JSON.stringify(hooks).includes('pingthings');
118
+ check('settings.json found', () => true);
119
+ check('pingthings hooks configured', () => hasPingthings);
120
+ if (hasPingthings) {
121
+ const hookTypes = Object.keys(hooks).filter(k =>
122
+ JSON.stringify(hooks[k]).includes('pingthings')
123
+ );
124
+ check('Hook events', () => hookTypes.join(', '));
125
+ }
126
+ } catch {
127
+ check('settings.json valid', () => false);
128
+ }
129
+ } else {
130
+ check('settings.json found', () => { throw new Error('Run "pingthings init" to set up'); });
131
+ }
132
+
133
+ console.log('');
134
+ }
package/src/cli/init.js CHANGED
@@ -113,13 +113,16 @@ export default async function init(args) {
113
113
  output: process.stdout,
114
114
  });
115
115
 
116
- rl.question('Choose (1 or 2): ', (answer) => {
117
- const choice = answer.trim();
118
- if (choice === '2') {
119
- applyHooks('informational');
120
- } else {
121
- applyHooks('basic');
122
- }
123
- rl.close();
116
+ await new Promise((resolve) => {
117
+ rl.question('Choose (1 or 2): ', (answer) => {
118
+ const choice = answer.trim();
119
+ if (choice === '2') {
120
+ applyHooks('informational');
121
+ } else {
122
+ applyHooks('basic');
123
+ }
124
+ rl.close();
125
+ resolve();
126
+ });
124
127
  });
125
128
  }
package/src/cli/play.js CHANGED
@@ -1,4 +1,4 @@
1
- import { readConfig, VALID_EVENTS } from '../config.js';
1
+ import { readConfig, VALID_EVENTS, getLastPlayed, setLastPlayed, isQuietHours } from '../config.js';
2
2
  import { getPackSounds, getEventSounds, pickRandom, resolvePack } from '../packs.js';
3
3
  import { playSound } from '../player.js';
4
4
  import { basename } from 'node:path';
@@ -133,5 +133,26 @@ export default function play(args) {
133
133
  process.exit(1);
134
134
  }
135
135
 
136
+ // Quiet hours check
137
+ if (isQuietHours(config)) {
138
+ return;
139
+ }
140
+
141
+ // Cooldown: avoid playing the same sound twice in a row
142
+ if (config.cooldown && !parsed.sound) {
143
+ const lastPlayed = getLastPlayed();
144
+ if (soundFile === lastPlayed) {
145
+ // Pick a different sound from the same pool
146
+ const pool = parsed.event
147
+ ? getEventSounds(config.eventPacks?.[parsed.event] || packName, parsed.event)
148
+ : getPackSounds(packName);
149
+ if (pool.length > 1) {
150
+ const alternatives = pool.filter(s => s !== lastPlayed);
151
+ soundFile = pickRandom(alternatives);
152
+ }
153
+ }
154
+ }
155
+
156
+ setLastPlayed(soundFile);
136
157
  playSound(soundFile, config.volume);
137
158
  }
@@ -0,0 +1,44 @@
1
+ import { readConfig, writeConfig } from '../config.js';
2
+ import { listPacks, getPackSounds, pickRandom } from '../packs.js';
3
+ import { playSound } from '../player.js';
4
+ import { basename } from 'node:path';
5
+
6
+ function showHelp() {
7
+ console.log(`
8
+ Usage: pingthings random-pack
9
+
10
+ Switch to a random pack for variety. Plays a preview sound from the new pack.
11
+ `);
12
+ }
13
+
14
+ export default function randomPack(args) {
15
+ if (args.includes('--help') || args.includes('-h')) {
16
+ showHelp();
17
+ return;
18
+ }
19
+
20
+ const config = readConfig();
21
+ const packs = listPacks();
22
+
23
+ if (packs.length <= 1) {
24
+ console.log('Only one pack available.');
25
+ return;
26
+ }
27
+
28
+ // Pick a different pack than the current one
29
+ const others = packs.filter(p => p.name !== config.activePack);
30
+ const chosen = others[Math.floor(Math.random() * others.length)];
31
+
32
+ config.activePack = chosen.name;
33
+ writeConfig(config);
34
+
35
+ const sounds = getPackSounds(chosen.name);
36
+ if (sounds.length > 0) {
37
+ const sample = pickRandom(sounds);
38
+ console.log(`Switched to: ${chosen.name} (${chosen.category})`);
39
+ console.log(`Preview: ${basename(sample)}`);
40
+ playSound(sample, config.volume);
41
+ } else {
42
+ console.log(`Switched to: ${chosen.name}`);
43
+ }
44
+ }
@@ -0,0 +1,81 @@
1
+ import { readConfig } from '../config.js';
2
+ import { listPacks, getPackSounds } from '../packs.js';
3
+ import { basename } from 'node:path';
4
+
5
+ function showHelp() {
6
+ console.log(`
7
+ Usage: pingthings search <term>
8
+
9
+ Search across all packs by name, description, category, or sound filename.
10
+
11
+ Arguments:
12
+ term Text to search for (case-insensitive)
13
+
14
+ Examples:
15
+ pingthings search sword Find packs/sounds with "sword"
16
+ pingthings search military Find military category packs
17
+ pingthings search explosion Find explosion sounds
18
+ pingthings search coin Find coin/pickup sounds
19
+ `);
20
+ }
21
+
22
+ export default function search(args) {
23
+ const term = args[0];
24
+
25
+ if (!term || term === '--help' || term === '-h') {
26
+ showHelp();
27
+ if (!term) process.exit(1);
28
+ return;
29
+ }
30
+
31
+ const query = term.toLowerCase();
32
+ const config = readConfig();
33
+ const packs = listPacks();
34
+ const results = { packs: [], sounds: [] };
35
+
36
+ for (const pack of packs) {
37
+ // Check pack-level matches
38
+ const packMatch =
39
+ pack.name.toLowerCase().includes(query) ||
40
+ pack.description.toLowerCase().includes(query) ||
41
+ (pack.category || '').toLowerCase().includes(query);
42
+
43
+ if (packMatch) {
44
+ results.packs.push(pack);
45
+ }
46
+
47
+ // Check individual sounds
48
+ const sounds = getPackSounds(pack.name);
49
+ for (const sound of sounds) {
50
+ const name = basename(sound).toLowerCase();
51
+ if (name.includes(query)) {
52
+ results.sounds.push({ pack: pack.name, sound: basename(sound), path: sound });
53
+ }
54
+ }
55
+ }
56
+
57
+ if (results.packs.length === 0 && results.sounds.length === 0) {
58
+ console.log(`No results for "${term}".`);
59
+ return;
60
+ }
61
+
62
+ if (results.packs.length > 0) {
63
+ console.log(`\nPacks matching "${term}":\n`);
64
+ for (const pack of results.packs) {
65
+ const active = pack.name === config.activePack ? ' *' : ' ';
66
+ console.log(`${active} ${pack.name} (${pack.category}, ${pack.soundCount} sounds)`);
67
+ if (pack.description) {
68
+ console.log(` ${pack.description}`);
69
+ }
70
+ }
71
+ }
72
+
73
+ if (results.sounds.length > 0) {
74
+ console.log(`\nSounds matching "${term}":\n`);
75
+ for (const s of results.sounds) {
76
+ console.log(` ${s.pack} / ${s.sound}`);
77
+ }
78
+ }
79
+
80
+ console.log('');
81
+ }