pingthings 0.8.0 → 1.0.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/LICENSE +39 -1
- package/README.md +40 -1
- package/bin/pingthings.js +6 -0
- package/package.json +1 -1
- package/src/cli/completions.js +9 -2
- package/src/cli/demo.js +36 -0
- package/src/cli/doctor.js +2 -1
- package/src/cli/play.js +8 -4
- package/src/cli/setup.js +173 -0
- package/src/cli/stats.js +98 -0
- package/src/cli/theme.js +44 -0
- package/src/notify.js +9 -4
- package/src/player.js +2 -1
package/LICENSE
CHANGED
|
@@ -27,4 +27,42 @@ warzone2100-command: Warzone 2100
|
|
|
27
27
|
0ad-civilizations: 0 A.D.
|
|
28
28
|
Copyright Wildfire Games — CC-BY-SA 3.0
|
|
29
29
|
Distributed as part of this aggregate work alongside GPL v2 code.
|
|
30
|
-
|
|
30
|
+
|
|
31
|
+
xonotic-announcer: Xonotic
|
|
32
|
+
Copyright Xonotic Project — GPL v2+
|
|
33
|
+
|
|
34
|
+
fighting-announcer: Fighting Game Announcer
|
|
35
|
+
Copyright Alba MacKenna — CC-BY 4.0
|
|
36
|
+
|
|
37
|
+
retro-8bit: 8-bit Sound Effects
|
|
38
|
+
Copyright Juhani Junkala / SubspaceAudio — CC0
|
|
39
|
+
|
|
40
|
+
retro-weapons: 8-bit Weapons (from same collection)
|
|
41
|
+
Copyright Juhani Junkala / SubspaceAudio — CC0
|
|
42
|
+
|
|
43
|
+
retro-movement: 8-bit Movement (from same collection)
|
|
44
|
+
Copyright Juhani Junkala / SubspaceAudio — CC0
|
|
45
|
+
|
|
46
|
+
kenney-interface: Interface Sounds
|
|
47
|
+
Copyright Kenney (kenney.nl) — CC0
|
|
48
|
+
|
|
49
|
+
kenney-scifi: Sci-fi Sounds
|
|
50
|
+
Copyright Kenney (kenney.nl) — CC0
|
|
51
|
+
|
|
52
|
+
kenney-voiceover: Voiceover Pack
|
|
53
|
+
Copyright Kenney (kenney.nl) — CC0
|
|
54
|
+
|
|
55
|
+
kenney-fighter: Voiceover Pack: Fighter
|
|
56
|
+
Copyright Kenney (kenney.nl) — CC0
|
|
57
|
+
|
|
58
|
+
kenney-digital: Digital Audio
|
|
59
|
+
Copyright Kenney (kenney.nl) — CC0
|
|
60
|
+
|
|
61
|
+
kenney-rpg: RPG Audio
|
|
62
|
+
Copyright Kenney (kenney.nl) — CC0
|
|
63
|
+
|
|
64
|
+
kenney-impacts: Impact Sounds
|
|
65
|
+
Copyright Kenney (kenney.nl) — CC0
|
|
66
|
+
|
|
67
|
+
droid-announcer: DROID Video Game Announcer
|
|
68
|
+
Copyright VoiceBosch — CC-BY-SA 4.0
|
package/README.md
CHANGED
|
@@ -140,7 +140,12 @@ For different sounds based on what Claude is doing, set up multiple hooks:
|
|
|
140
140
|
| `pingthings random-pack` | Switch to a random pack |
|
|
141
141
|
| `pingthings install <source>` | Install a pack from GitHub or local path |
|
|
142
142
|
| `pingthings uninstall <pack>` | Remove a user-installed pack |
|
|
143
|
+
| `pingthings demo` | Play one sound from every pack — showroom tour |
|
|
144
|
+
| `pingthings stats` | Show usage statistics |
|
|
145
|
+
| `pingthings setup <ide>` | Configure hooks for any IDE (cursor, copilot, codex, etc.) |
|
|
143
146
|
| `pingthings doctor` | Diagnose audio setup and configuration |
|
|
147
|
+
| `pingthings update` | Check for new versions on npm |
|
|
148
|
+
| `pingthings cesp [pack\|--all]` | Generate CESP-compatible manifests |
|
|
144
149
|
| `pingthings completions <shell>` | Generate shell completions (bash/zsh/fish) |
|
|
145
150
|
|
|
146
151
|
## Configuration
|
|
@@ -153,7 +158,10 @@ Config lives at `~/.config/pingthings/config.json`:
|
|
|
153
158
|
"mode": "random",
|
|
154
159
|
"specificSound": null,
|
|
155
160
|
"volume": 100,
|
|
156
|
-
"eventPacks": {}
|
|
161
|
+
"eventPacks": {},
|
|
162
|
+
"cooldown": true,
|
|
163
|
+
"quietHours": null,
|
|
164
|
+
"notifications": false
|
|
157
165
|
}
|
|
158
166
|
```
|
|
159
167
|
|
|
@@ -162,6 +170,9 @@ Config lives at `~/.config/pingthings/config.json`:
|
|
|
162
170
|
- **specificSound** — sound name to always play when mode is `"specific"`
|
|
163
171
|
- **volume** — playback volume, 0-100 (default: 100)
|
|
164
172
|
- **eventPacks** — per-event pack overrides (e.g. `{"error": "freedoom-arsenal"}`)
|
|
173
|
+
- **cooldown** — avoid repeating the same sound twice in a row (default: true)
|
|
174
|
+
- **quietHours** — mute during hours, e.g. `"22-7"` for 10pm-7am (default: null)
|
|
175
|
+
- **notifications** — show desktop notifications alongside sound (default: false)
|
|
165
176
|
|
|
166
177
|
Set values via CLI:
|
|
167
178
|
|
|
@@ -240,6 +251,30 @@ Arena FPS announcer voice lines from **Xonotic** — 15 sounds including "awesom
|
|
|
240
251
|
### fighting-announcer
|
|
241
252
|
Fighting game announcer voice lines — 20 sounds including "Fight!", "Victory!", "K.O!", "Game Over!", "Ready?", "You Win!". License: CC-BY 4.0.
|
|
242
253
|
|
|
254
|
+
### kenney-voiceover
|
|
255
|
+
Human voice notifications by **Kenney** — 19 sounds including "mission completed", "objective achieved", "game over", "congratulations". License: CC0.
|
|
256
|
+
|
|
257
|
+
### droid-announcer
|
|
258
|
+
Robotic AI voice lines — 15 sounds including "objective complete", "action required", "instruction unclear", "mission complete". Perfect for AI coding tools. License: CC-BY-SA 4.0.
|
|
259
|
+
|
|
260
|
+
### kenney-digital
|
|
261
|
+
Digital and space notification tones by **Kenney** — 18 sounds including power-ups, phasers, zaps, and bleeps. License: CC0.
|
|
262
|
+
|
|
263
|
+
### kenney-rpg
|
|
264
|
+
Fantasy RPG foley sounds by **Kenney** — 18 sounds including metal latches, book flips, sword draws, door creaks. License: CC0.
|
|
265
|
+
|
|
266
|
+
### kenney-impacts
|
|
267
|
+
Material impact sounds by **Kenney** — 18 sounds including metal plates, wood, glass, bells, and mining. License: CC0.
|
|
268
|
+
|
|
269
|
+
### kenney-fighter
|
|
270
|
+
Female fighting game announcer by **Kenney** — 18 sounds including "flawless victory!", "combo breaker!", "prepare yourself!". License: CC0.
|
|
271
|
+
|
|
272
|
+
### retro-weapons
|
|
273
|
+
8-bit weapons, explosions, and death screams — 18 sounds from the SubspaceAudio collection. License: CC0.
|
|
274
|
+
|
|
275
|
+
### retro-movement
|
|
276
|
+
8-bit portals, doors, jumps, and bleeps — 18 sounds from the SubspaceAudio collection. License: CC0.
|
|
277
|
+
|
|
243
278
|
## Custom packs
|
|
244
279
|
|
|
245
280
|
Place packs in `~/.config/pingthings/packs/<pack-name>/`:
|
|
@@ -303,6 +338,10 @@ pingthings theme reset # back to defaults
|
|
|
303
338
|
| `professional` | Clean and minimal — Kenney UI sounds for everything |
|
|
304
339
|
| `8bit` | Pure retro — 8-bit chiptune for everything |
|
|
305
340
|
| `space` | Space station — Warzone 2100 + Kenney sci-fi |
|
|
341
|
+
| `developer` | AI assistant vibes — droid announcer + human voiceover |
|
|
342
|
+
| `arcade` | Full 8-bit arcade experience |
|
|
343
|
+
| `tabletop` | Tavern sounds — RPG foley + material impacts |
|
|
344
|
+
| `tournament` | Fighting game tournament — multiple announcers |
|
|
306
345
|
| `chaos` | Different pack for every event — maximum variety |
|
|
307
346
|
|
|
308
347
|
## Tools
|
package/bin/pingthings.js
CHANGED
|
@@ -23,6 +23,9 @@ const commands = {
|
|
|
23
23
|
install: () => import('../src/cli/install.js'),
|
|
24
24
|
uninstall: () => import('../src/cli/uninstall.js'),
|
|
25
25
|
init: () => import('../src/cli/init.js'),
|
|
26
|
+
setup: () => import('../src/cli/setup.js'),
|
|
27
|
+
demo: () => import('../src/cli/demo.js'),
|
|
28
|
+
stats: () => import('../src/cli/stats.js'),
|
|
26
29
|
create: () => import('../src/cli/create.js'),
|
|
27
30
|
theme: () => import('../src/cli/theme.js'),
|
|
28
31
|
'test-events': () => import('../src/cli/test-events.js'),
|
|
@@ -51,7 +54,10 @@ Commands:
|
|
|
51
54
|
test-events [pack] Play all event sounds to hear what each one sounds like
|
|
52
55
|
theme [name] Apply a sound theme (maps events across packs)
|
|
53
56
|
config [key] [val] Show or update configuration
|
|
57
|
+
demo Play one sound from every pack — showroom tour
|
|
58
|
+
stats Show usage statistics
|
|
54
59
|
init Set up Claude Code hooks automatically
|
|
60
|
+
setup <ide> Configure hooks for any IDE (cursor, copilot, codex, etc.)
|
|
55
61
|
random-pack Switch to a random pack
|
|
56
62
|
create <dir> Create a new pack from a folder of audio files
|
|
57
63
|
install <source> Install a pack from GitHub or URL
|
package/package.json
CHANGED
package/src/cli/completions.js
CHANGED
|
@@ -17,12 +17,14 @@ Install:
|
|
|
17
17
|
const COMMANDS = [
|
|
18
18
|
'play', 'list', 'select', 'browse', 'search', 'sounds',
|
|
19
19
|
'use', 'preview', 'test-events', 'theme', 'config',
|
|
20
|
-
'init', '
|
|
20
|
+
'init', 'setup', 'demo', 'stats', 'create', 'install', 'uninstall',
|
|
21
|
+
'random-pack', 'doctor', 'update', 'cesp', 'completions',
|
|
21
22
|
];
|
|
22
23
|
|
|
23
24
|
const THEMES = [
|
|
24
25
|
'retro', 'sci-fi', 'arena', 'fantasy', 'ancient',
|
|
25
|
-
'professional', '8bit', 'space', '
|
|
26
|
+
'professional', '8bit', 'space', 'developer', 'arcade',
|
|
27
|
+
'tabletop', 'tournament', 'chaos', 'reset',
|
|
26
28
|
];
|
|
27
29
|
|
|
28
30
|
const EVENTS = ['done', 'permission', 'complete', 'error', 'blocked'];
|
|
@@ -92,6 +94,11 @@ _pingthings() {
|
|
|
92
94
|
'uninstall:Remove a user-installed pack'
|
|
93
95
|
'random-pack:Switch to a random pack'
|
|
94
96
|
'doctor:Diagnose audio setup'
|
|
97
|
+
'setup:Configure hooks for any IDE'
|
|
98
|
+
'demo:Play one sound from every pack'
|
|
99
|
+
'stats:Show usage statistics'
|
|
100
|
+
'update:Check for new versions'
|
|
101
|
+
'cesp:Generate CESP manifests'
|
|
95
102
|
'completions:Generate shell completions'
|
|
96
103
|
)
|
|
97
104
|
themes=(${THEMES.join(' ')})
|
package/src/cli/demo.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { readConfig } from '../config.js';
|
|
2
|
+
import { listPacks, getPackSounds, pickRandom } from '../packs.js';
|
|
3
|
+
import { playSoundSync } from '../player.js';
|
|
4
|
+
import { basename } from 'node:path';
|
|
5
|
+
|
|
6
|
+
function showHelp() {
|
|
7
|
+
console.log(`
|
|
8
|
+
Usage: pingthings demo
|
|
9
|
+
|
|
10
|
+
Play one sound from every pack — a showroom tour of all your notification sounds.
|
|
11
|
+
`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function demo(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
|
+
console.log(`\n PINGTHINGS DEMO — ${packs.length} packs\n`);
|
|
24
|
+
|
|
25
|
+
for (const pack of packs) {
|
|
26
|
+
const sounds = getPackSounds(pack.name);
|
|
27
|
+
if (sounds.length === 0) continue;
|
|
28
|
+
|
|
29
|
+
const sound = pickRandom(sounds);
|
|
30
|
+
const active = pack.name === config.activePack ? ' *' : ' ';
|
|
31
|
+
console.log(`${active} ${pack.name.padEnd(22)} ${basename(sound)}`);
|
|
32
|
+
playSoundSync(sound, config.volume);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log(`\n * = active pack\n`);
|
|
36
|
+
}
|
package/src/cli/doctor.js
CHANGED
|
@@ -33,7 +33,8 @@ function check(label, fn) {
|
|
|
33
33
|
|
|
34
34
|
function commandExists(cmd) {
|
|
35
35
|
try {
|
|
36
|
-
|
|
36
|
+
const checker = platform() === 'win32' ? 'where' : 'which';
|
|
37
|
+
execFileSync(checker, [cmd], { stdio: 'pipe' });
|
|
37
38
|
return true;
|
|
38
39
|
} catch {
|
|
39
40
|
return false;
|
package/src/cli/play.js
CHANGED
|
@@ -2,10 +2,11 @@ import { readConfig, VALID_EVENTS, getLastPlayed, setLastPlayed, isQuietHours }
|
|
|
2
2
|
import { getPackSounds, getEventSounds, pickRandom, resolvePack } from '../packs.js';
|
|
3
3
|
import { playSound } from '../player.js';
|
|
4
4
|
import { sendNotification } from '../notify.js';
|
|
5
|
+
import { recordPlay } from './stats.js';
|
|
5
6
|
import { basename } from 'node:path';
|
|
6
7
|
|
|
7
8
|
function parseArgs(args) {
|
|
8
|
-
const result = { sound: null, event: null, notify: false };
|
|
9
|
+
const result = { sound: null, event: null, notify: false, silent: false };
|
|
9
10
|
|
|
10
11
|
for (let i = 0; i < args.length; i++) {
|
|
11
12
|
if ((args[i] === '--event' || args[i] === '-e') && args[i + 1]) {
|
|
@@ -13,6 +14,8 @@ function parseArgs(args) {
|
|
|
13
14
|
i++;
|
|
14
15
|
} else if (args[i] === '--notify' || args[i] === '-n') {
|
|
15
16
|
result.notify = true;
|
|
17
|
+
} else if (args[i] === '--silent' || args[i] === '-s') {
|
|
18
|
+
result.silent = true;
|
|
16
19
|
} else if (args[i] === '--help' || args[i] === '-h') {
|
|
17
20
|
showHelp();
|
|
18
21
|
process.exit(0);
|
|
@@ -136,8 +139,9 @@ export default function play(args) {
|
|
|
136
139
|
process.exit(1);
|
|
137
140
|
}
|
|
138
141
|
|
|
139
|
-
// Quiet hours
|
|
140
|
-
if (isQuietHours(config)) {
|
|
142
|
+
// Quiet hours or silent mode — skip playback
|
|
143
|
+
if (isQuietHours(config) || parsed.silent) {
|
|
144
|
+
recordPlay(packName, parsed.event);
|
|
141
145
|
return;
|
|
142
146
|
}
|
|
143
147
|
|
|
@@ -145,7 +149,6 @@ export default function play(args) {
|
|
|
145
149
|
if (config.cooldown && !parsed.sound) {
|
|
146
150
|
const lastPlayed = getLastPlayed();
|
|
147
151
|
if (soundFile === lastPlayed) {
|
|
148
|
-
// Pick a different sound from the same pool
|
|
149
152
|
const pool = parsed.event
|
|
150
153
|
? getEventSounds(config.eventPacks?.[parsed.event] || packName, parsed.event)
|
|
151
154
|
: getPackSounds(packName);
|
|
@@ -158,6 +161,7 @@ export default function play(args) {
|
|
|
158
161
|
|
|
159
162
|
setLastPlayed(soundFile);
|
|
160
163
|
playSound(soundFile, config.volume);
|
|
164
|
+
recordPlay(packName, parsed.event);
|
|
161
165
|
|
|
162
166
|
// Desktop notification
|
|
163
167
|
if (parsed.notify || config.notifications) {
|
package/src/cli/setup.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { createInterface } from 'node:readline';
|
|
5
|
+
|
|
6
|
+
const IDES = {
|
|
7
|
+
'claude-code': {
|
|
8
|
+
name: 'Claude Code',
|
|
9
|
+
configPath: () => join(homedir(), '.claude', 'settings.json'),
|
|
10
|
+
generate: (mode) => ({
|
|
11
|
+
hooks: mode === 'informational' ? {
|
|
12
|
+
Notification: [{ matcher: '', hooks: [{ type: 'command', command: 'pingthings play --event permission' }] }],
|
|
13
|
+
Stop: [{ matcher: '', hooks: [{ type: 'command', command: 'pingthings play --event complete' }] }],
|
|
14
|
+
PostToolUseFailure: [{ matcher: '', hooks: [{ type: 'command', command: 'pingthings play --event error' }] }],
|
|
15
|
+
StopFailure: [{ matcher: '', hooks: [{ type: 'command', command: 'pingthings play --event blocked' }] }],
|
|
16
|
+
} : {
|
|
17
|
+
Notification: [{ matcher: '', hooks: [{ type: 'command', command: 'pingthings play' }] }],
|
|
18
|
+
},
|
|
19
|
+
}),
|
|
20
|
+
},
|
|
21
|
+
cursor: {
|
|
22
|
+
name: 'Cursor',
|
|
23
|
+
configPath: () => join(homedir(), '.cursor', 'hooks.json'),
|
|
24
|
+
generate: (mode) => ({
|
|
25
|
+
version: 1,
|
|
26
|
+
hooks: mode === 'informational' ? {
|
|
27
|
+
stop: [{ command: 'pingthings play --event complete' }],
|
|
28
|
+
afterFileEdit: [{ command: 'pingthings play --event done' }],
|
|
29
|
+
} : {
|
|
30
|
+
stop: [{ command: 'pingthings play' }],
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
},
|
|
34
|
+
copilot: {
|
|
35
|
+
name: 'GitHub Copilot',
|
|
36
|
+
configPath: () => join('.github', 'hooks', 'pingthings.json'),
|
|
37
|
+
generate: (mode) => ({
|
|
38
|
+
version: 1,
|
|
39
|
+
hooks: mode === 'informational' ? {
|
|
40
|
+
sessionEnd: [{ type: 'command', bash: 'pingthings play --event complete' }],
|
|
41
|
+
postToolUse: [{ type: 'command', bash: 'pingthings play --event done' }],
|
|
42
|
+
errorOccurred: [{ type: 'command', bash: 'pingthings play --event error' }],
|
|
43
|
+
} : {
|
|
44
|
+
sessionEnd: [{ type: 'command', bash: 'pingthings play' }],
|
|
45
|
+
},
|
|
46
|
+
}),
|
|
47
|
+
},
|
|
48
|
+
codex: {
|
|
49
|
+
name: 'OpenAI Codex',
|
|
50
|
+
configPath: () => join(homedir(), '.codex', 'config.toml'),
|
|
51
|
+
generate: () => null, // TOML, handle separately
|
|
52
|
+
toml: 'notify = ["pingthings", "play", "--event", "complete"]\n',
|
|
53
|
+
},
|
|
54
|
+
windsurf: {
|
|
55
|
+
name: 'Windsurf',
|
|
56
|
+
configPath: () => join('.windsurf', 'hooks.json'),
|
|
57
|
+
generate: (mode) => ({
|
|
58
|
+
hooks: mode === 'informational' ? {
|
|
59
|
+
post_cascade_response: [{ command: 'pingthings play --event complete' }],
|
|
60
|
+
post_write_code: [{ command: 'pingthings play --event done' }],
|
|
61
|
+
} : {
|
|
62
|
+
post_cascade_response: [{ command: 'pingthings play' }],
|
|
63
|
+
},
|
|
64
|
+
}),
|
|
65
|
+
},
|
|
66
|
+
gemini: {
|
|
67
|
+
name: 'Gemini CLI',
|
|
68
|
+
configPath: () => join(homedir(), '.gemini', 'settings.json'),
|
|
69
|
+
generate: (mode) => ({
|
|
70
|
+
hooks: mode === 'informational' ? {
|
|
71
|
+
SessionEnd: [{ matcher: '*', hooks: [{ type: 'command', command: 'pingthings play --event complete' }] }],
|
|
72
|
+
AfterTool: [{ matcher: '*', hooks: [{ type: 'command', command: 'pingthings play --event done' }] }],
|
|
73
|
+
Notification: [{ matcher: 'error', hooks: [{ type: 'command', command: 'pingthings play --event error' }] }],
|
|
74
|
+
} : {
|
|
75
|
+
SessionEnd: [{ matcher: '*', hooks: [{ type: 'command', command: 'pingthings play' }] }],
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
function showHelp() {
|
|
82
|
+
console.log(`
|
|
83
|
+
Usage: pingthings setup <ide>
|
|
84
|
+
|
|
85
|
+
Configure pingthings hooks for your IDE/coding tool.
|
|
86
|
+
|
|
87
|
+
Supported IDEs:
|
|
88
|
+
claude-code Claude Code CLI
|
|
89
|
+
cursor Cursor AI editor
|
|
90
|
+
copilot GitHub Copilot CLI
|
|
91
|
+
codex OpenAI Codex CLI
|
|
92
|
+
windsurf Windsurf (Cascade)
|
|
93
|
+
gemini Gemini CLI
|
|
94
|
+
|
|
95
|
+
Options:
|
|
96
|
+
--basic Random sounds (no event mapping)
|
|
97
|
+
--informational Event-based sounds (recommended)
|
|
98
|
+
|
|
99
|
+
Examples:
|
|
100
|
+
pingthings setup cursor
|
|
101
|
+
pingthings setup copilot --informational
|
|
102
|
+
pingthings setup codex
|
|
103
|
+
`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function writeJsonConfig(path, config) {
|
|
107
|
+
const dir = join(path, '..');
|
|
108
|
+
mkdirSync(dir, { recursive: true });
|
|
109
|
+
|
|
110
|
+
let existing = {};
|
|
111
|
+
if (existsSync(path)) {
|
|
112
|
+
try {
|
|
113
|
+
existing = JSON.parse(readFileSync(path, 'utf8'));
|
|
114
|
+
} catch {}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const merged = { ...existing, ...config };
|
|
118
|
+
if (existing.hooks && config.hooks) {
|
|
119
|
+
merged.hooks = { ...existing.hooks, ...config.hooks };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const tmpPath = path + '.tmp';
|
|
123
|
+
writeFileSync(tmpPath, JSON.stringify(merged, null, 2) + '\n', 'utf8');
|
|
124
|
+
renameSync(tmpPath, path);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export default async function setup(args) {
|
|
128
|
+
const ideName = args[0];
|
|
129
|
+
|
|
130
|
+
if (!ideName || ideName === '--help' || ideName === '-h') {
|
|
131
|
+
showHelp();
|
|
132
|
+
if (!ideName) process.exit(1);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const ide = IDES[ideName];
|
|
137
|
+
if (!ide) {
|
|
138
|
+
console.error(`Unknown IDE: ${ideName}`);
|
|
139
|
+
console.error(`Supported: ${Object.keys(IDES).join(', ')}`);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let mode = 'informational';
|
|
144
|
+
if (args.includes('--basic')) mode = 'basic';
|
|
145
|
+
|
|
146
|
+
const configPath = ide.configPath();
|
|
147
|
+
|
|
148
|
+
console.log(`\nSetting up pingthings for ${ide.name}...\n`);
|
|
149
|
+
|
|
150
|
+
if (ideName === 'codex') {
|
|
151
|
+
// Codex uses TOML
|
|
152
|
+
mkdirSync(join(configPath, '..'), { recursive: true });
|
|
153
|
+
if (existsSync(configPath)) {
|
|
154
|
+
const existing = readFileSync(configPath, 'utf8');
|
|
155
|
+
if (existing.includes('pingthings')) {
|
|
156
|
+
console.log('pingthings is already configured in Codex.');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
writeFileSync(configPath, existing + '\n' + ide.toml, 'utf8');
|
|
160
|
+
} else {
|
|
161
|
+
writeFileSync(configPath, ide.toml, 'utf8');
|
|
162
|
+
}
|
|
163
|
+
console.log(`Written to: ${configPath}`);
|
|
164
|
+
console.log('Codex will run "pingthings play --event complete" on agent-turn-complete.');
|
|
165
|
+
} else {
|
|
166
|
+
const config = ide.generate(mode);
|
|
167
|
+
writeJsonConfig(configPath, config);
|
|
168
|
+
console.log(`Written to: ${configPath}`);
|
|
169
|
+
console.log(`Mode: ${mode}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
console.log('\nRestart your IDE for hooks to take effect.');
|
|
173
|
+
}
|
package/src/cli/stats.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { readConfig, getConfigDir } from '../config.js';
|
|
4
|
+
import { listPacks } from '../packs.js';
|
|
5
|
+
|
|
6
|
+
function showHelp() {
|
|
7
|
+
console.log(`
|
|
8
|
+
Usage: pingthings stats
|
|
9
|
+
|
|
10
|
+
Show usage statistics — total sounds, packs, play count, and more.
|
|
11
|
+
`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getStatsPath() {
|
|
15
|
+
return join(getConfigDir(), 'stats.json');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function recordPlay(packName, event) {
|
|
19
|
+
const statsPath = getStatsPath();
|
|
20
|
+
let stats = { totalPlays: 0, packPlays: {}, eventPlays: {}, dailyPlays: {} };
|
|
21
|
+
|
|
22
|
+
if (existsSync(statsPath)) {
|
|
23
|
+
try {
|
|
24
|
+
stats = JSON.parse(readFileSync(statsPath, 'utf8'));
|
|
25
|
+
} catch {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
stats.totalPlays = (stats.totalPlays || 0) + 1;
|
|
29
|
+
|
|
30
|
+
if (!stats.packPlays) stats.packPlays = {};
|
|
31
|
+
stats.packPlays[packName] = (stats.packPlays[packName] || 0) + 1;
|
|
32
|
+
|
|
33
|
+
if (event) {
|
|
34
|
+
if (!stats.eventPlays) stats.eventPlays = {};
|
|
35
|
+
stats.eventPlays[event] = (stats.eventPlays[event] || 0) + 1;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const today = new Date().toISOString().split('T')[0];
|
|
39
|
+
if (!stats.dailyPlays) stats.dailyPlays = {};
|
|
40
|
+
stats.dailyPlays[today] = (stats.dailyPlays[today] || 0) + 1;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const tmpPath = statsPath + '.tmp';
|
|
44
|
+
writeFileSync(tmpPath, JSON.stringify(stats, null, 2) + '\n', 'utf8');
|
|
45
|
+
renameSync(tmpPath, statsPath);
|
|
46
|
+
} catch {}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default function stats(args) {
|
|
50
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
51
|
+
showHelp();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const packs = listPacks();
|
|
56
|
+
const totalSounds = packs.reduce((s, p) => s + p.soundCount, 0);
|
|
57
|
+
const statsPath = getStatsPath();
|
|
58
|
+
|
|
59
|
+
console.log('\n PINGTHINGS STATS\n');
|
|
60
|
+
console.log(` Packs installed: ${packs.length}`);
|
|
61
|
+
console.log(` Total sounds: ${totalSounds}`);
|
|
62
|
+
console.log(` Categories: ${new Set(packs.map(p => p.category)).size}`);
|
|
63
|
+
|
|
64
|
+
if (!existsSync(statsPath)) {
|
|
65
|
+
console.log('\n No play history yet. Start using pingthings!\n');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let data;
|
|
70
|
+
try {
|
|
71
|
+
data = JSON.parse(readFileSync(statsPath, 'utf8'));
|
|
72
|
+
} catch {
|
|
73
|
+
console.log('\n No play history yet.\n');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(`\n Total plays: ${data.totalPlays || 0}`);
|
|
78
|
+
|
|
79
|
+
const today = new Date().toISOString().split('T')[0];
|
|
80
|
+
console.log(` Plays today: ${data.dailyPlays?.[today] || 0}`);
|
|
81
|
+
|
|
82
|
+
if (data.packPlays && Object.keys(data.packPlays).length > 0) {
|
|
83
|
+
const sorted = Object.entries(data.packPlays).sort((a, b) => b[1] - a[1]);
|
|
84
|
+
console.log(`\n Most played packs:`);
|
|
85
|
+
for (const [pack, count] of sorted.slice(0, 5)) {
|
|
86
|
+
console.log(` ${pack.padEnd(24)} ${count} plays`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (data.eventPlays && Object.keys(data.eventPlays).length > 0) {
|
|
91
|
+
console.log(`\n Events:`);
|
|
92
|
+
for (const [event, count] of Object.entries(data.eventPlays)) {
|
|
93
|
+
console.log(` ${event.padEnd(16)} ${count}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log('');
|
|
98
|
+
}
|
package/src/cli/theme.js
CHANGED
|
@@ -90,6 +90,50 @@ const THEMES = {
|
|
|
90
90
|
blocked: 'kenney-scifi',
|
|
91
91
|
},
|
|
92
92
|
},
|
|
93
|
+
'developer': {
|
|
94
|
+
description: 'AI assistant vibes — droid announcer + human voiceover',
|
|
95
|
+
activePack: 'droid-announcer',
|
|
96
|
+
eventPacks: {
|
|
97
|
+
done: 'droid-announcer',
|
|
98
|
+
permission: 'droid-announcer',
|
|
99
|
+
complete: 'kenney-voiceover',
|
|
100
|
+
error: 'droid-announcer',
|
|
101
|
+
blocked: 'kenney-voiceover',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
'arcade': {
|
|
105
|
+
description: 'Full arcade experience — 8-bit everything',
|
|
106
|
+
activePack: 'retro-8bit',
|
|
107
|
+
eventPacks: {
|
|
108
|
+
done: 'retro-8bit',
|
|
109
|
+
permission: 'retro-movement',
|
|
110
|
+
complete: 'retro-8bit',
|
|
111
|
+
error: 'retro-weapons',
|
|
112
|
+
blocked: 'retro-weapons',
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
'tabletop': {
|
|
116
|
+
description: 'Tavern sounds — RPG foley meets material impacts',
|
|
117
|
+
activePack: 'kenney-rpg',
|
|
118
|
+
eventPacks: {
|
|
119
|
+
done: 'kenney-rpg',
|
|
120
|
+
permission: 'kenney-rpg',
|
|
121
|
+
complete: 'kenney-impacts',
|
|
122
|
+
error: 'kenney-impacts',
|
|
123
|
+
blocked: 'kenney-impacts',
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
'tournament': {
|
|
127
|
+
description: 'Fighting game tournament — multiple announcers',
|
|
128
|
+
activePack: 'kenney-fighter',
|
|
129
|
+
eventPacks: {
|
|
130
|
+
done: 'fighting-announcer',
|
|
131
|
+
permission: 'kenney-fighter',
|
|
132
|
+
complete: 'kenney-fighter',
|
|
133
|
+
error: 'fighting-announcer',
|
|
134
|
+
blocked: 'xonotic-announcer',
|
|
135
|
+
},
|
|
136
|
+
},
|
|
93
137
|
'chaos': {
|
|
94
138
|
description: 'Random pack for every event — maximum variety',
|
|
95
139
|
activePack: '7kaa-soldiers',
|
package/src/notify.js
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { platform } from 'node:os';
|
|
3
3
|
|
|
4
|
+
function sanitize(str) {
|
|
5
|
+
return str.replace(/[\\"]/g, '').replace(/[^\x20-\x7E]/g, '');
|
|
6
|
+
}
|
|
7
|
+
|
|
4
8
|
export function sendNotification(title, message) {
|
|
5
9
|
const os = platform();
|
|
10
|
+
const safeTitle = sanitize(title);
|
|
11
|
+
const safeMessage = sanitize(message);
|
|
6
12
|
|
|
7
13
|
if (os === 'darwin') {
|
|
8
14
|
spawn('osascript', [
|
|
9
15
|
'-e',
|
|
10
|
-
`display notification "${
|
|
11
|
-
], { detached: true, stdio: 'ignore' }).unref();
|
|
16
|
+
`display notification "${safeMessage}" with title "${safeTitle}"`,
|
|
17
|
+
], { detached: true, stdio: 'ignore' }).on('error', () => {}).unref();
|
|
12
18
|
} else if (os === 'linux') {
|
|
13
|
-
spawn('notify-send', [
|
|
19
|
+
spawn('notify-send', [safeTitle, safeMessage], {
|
|
14
20
|
detached: true,
|
|
15
21
|
stdio: 'ignore',
|
|
16
22
|
}).on('error', () => {}).unref();
|
|
17
23
|
}
|
|
18
|
-
// Windows: no native CLI notification without dependencies, skip for now
|
|
19
24
|
}
|
package/src/player.js
CHANGED
|
@@ -4,7 +4,8 @@ import { platform } from 'node:os';
|
|
|
4
4
|
|
|
5
5
|
function commandExists(cmd) {
|
|
6
6
|
try {
|
|
7
|
-
|
|
7
|
+
const checker = platform() === 'win32' ? 'where' : 'which';
|
|
8
|
+
execFileSync(checker, [cmd], { stdio: 'pipe' });
|
|
8
9
|
return true;
|
|
9
10
|
} catch {
|
|
10
11
|
return false;
|