pingthings 0.7.0 → 0.9.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/bin/pingthings.js +7 -0
- package/package.json +1 -1
- package/packs/0ad-civilizations/openpeon.json +172 -0
- package/packs/7kaa-soldiers/openpeon.json +217 -0
- package/packs/droid-announcer/openpeon.json +107 -0
- package/packs/fighting-announcer/openpeon.json +132 -0
- package/packs/freedoom-arsenal/openpeon.json +117 -0
- package/packs/kenney-digital/manifest.json +58 -0
- package/packs/kenney-digital/openpeon.json +122 -0
- package/packs/kenney-digital/sounds/high-up.ogg +0 -0
- package/packs/kenney-digital/sounds/low-down.ogg +0 -0
- package/packs/kenney-digital/sounds/pep-1.ogg +0 -0
- package/packs/kenney-digital/sounds/pep-2.ogg +0 -0
- package/packs/kenney-digital/sounds/phase-jump-1.ogg +0 -0
- package/packs/kenney-digital/sounds/phase-jump-2.ogg +0 -0
- package/packs/kenney-digital/sounds/phaser-down.ogg +0 -0
- package/packs/kenney-digital/sounds/phaser-up-1.ogg +0 -0
- package/packs/kenney-digital/sounds/phaser-up-2.ogg +0 -0
- package/packs/kenney-digital/sounds/power-up-1.ogg +0 -0
- package/packs/kenney-digital/sounds/power-up-2.ogg +0 -0
- package/packs/kenney-digital/sounds/power-up-3.ogg +0 -0
- package/packs/kenney-digital/sounds/space-trash.ogg +0 -0
- package/packs/kenney-digital/sounds/three-tone.ogg +0 -0
- package/packs/kenney-digital/sounds/tone.ogg +0 -0
- package/packs/kenney-digital/sounds/two-tone.ogg +0 -0
- package/packs/kenney-digital/sounds/zap-1.ogg +0 -0
- package/packs/kenney-digital/sounds/zap-2.ogg +0 -0
- package/packs/kenney-fighter/manifest.json +58 -0
- package/packs/kenney-fighter/openpeon.json +122 -0
- package/packs/kenney-fighter/sounds/begin.ogg +0 -0
- package/packs/kenney-fighter/sounds/combo-breaker.ogg +0 -0
- package/packs/kenney-fighter/sounds/combo.ogg +0 -0
- package/packs/kenney-fighter/sounds/fight.ogg +0 -0
- package/packs/kenney-fighter/sounds/final-round.ogg +0 -0
- package/packs/kenney-fighter/sounds/flawless-victory.ogg +0 -0
- package/packs/kenney-fighter/sounds/game-over.ogg +0 -0
- package/packs/kenney-fighter/sounds/loser.ogg +0 -0
- package/packs/kenney-fighter/sounds/multi-kill.ogg +0 -0
- package/packs/kenney-fighter/sounds/prepare-yourself.ogg +0 -0
- package/packs/kenney-fighter/sounds/ready.ogg +0 -0
- package/packs/kenney-fighter/sounds/round-1.ogg +0 -0
- package/packs/kenney-fighter/sounds/sudden-death.ogg +0 -0
- package/packs/kenney-fighter/sounds/tie-breaker.ogg +0 -0
- package/packs/kenney-fighter/sounds/time.ogg +0 -0
- package/packs/kenney-fighter/sounds/winner.ogg +0 -0
- package/packs/kenney-fighter/sounds/you-lose.ogg +0 -0
- package/packs/kenney-fighter/sounds/you-win.ogg +0 -0
- package/packs/kenney-impacts/manifest.json +57 -0
- package/packs/kenney-impacts/openpeon.json +116 -0
- package/packs/kenney-impacts/sounds/bell-heavy-2.ogg +0 -0
- package/packs/kenney-impacts/sounds/bell-heavy.ogg +0 -0
- package/packs/kenney-impacts/sounds/glass-heavy.ogg +0 -0
- package/packs/kenney-impacts/sounds/glass-light.ogg +0 -0
- package/packs/kenney-impacts/sounds/glass-medium.ogg +0 -0
- package/packs/kenney-impacts/sounds/mining-1.ogg +0 -0
- package/packs/kenney-impacts/sounds/mining-2.ogg +0 -0
- package/packs/kenney-impacts/sounds/plate-heavy-2.ogg +0 -0
- package/packs/kenney-impacts/sounds/plate-heavy.ogg +0 -0
- package/packs/kenney-impacts/sounds/plate-light-1.ogg +0 -0
- package/packs/kenney-impacts/sounds/plate-light-2.ogg +0 -0
- package/packs/kenney-impacts/sounds/plate-medium.ogg +0 -0
- package/packs/kenney-impacts/sounds/soft-heavy.ogg +0 -0
- package/packs/kenney-impacts/sounds/soft-medium.ogg +0 -0
- package/packs/kenney-impacts/sounds/tin-medium.ogg +0 -0
- package/packs/kenney-impacts/sounds/wood-heavy.ogg +0 -0
- package/packs/kenney-impacts/sounds/wood-light.ogg +0 -0
- package/packs/kenney-impacts/sounds/wood-medium.ogg +0 -0
- package/packs/kenney-interface/openpeon.json +122 -0
- package/packs/kenney-rpg/manifest.json +57 -0
- package/packs/kenney-rpg/openpeon.json +117 -0
- package/packs/kenney-rpg/sounds/belt-handle-2.ogg +0 -0
- package/packs/kenney-rpg/sounds/belt-handle.ogg +0 -0
- package/packs/kenney-rpg/sounds/book-close.ogg +0 -0
- package/packs/kenney-rpg/sounds/book-flip.ogg +0 -0
- package/packs/kenney-rpg/sounds/book-open.ogg +0 -0
- package/packs/kenney-rpg/sounds/book-place.ogg +0 -0
- package/packs/kenney-rpg/sounds/cloth-belt.ogg +0 -0
- package/packs/kenney-rpg/sounds/creak.ogg +0 -0
- package/packs/kenney-rpg/sounds/door-close.ogg +0 -0
- package/packs/kenney-rpg/sounds/door-open.ogg +0 -0
- package/packs/kenney-rpg/sounds/draw-knife.ogg +0 -0
- package/packs/kenney-rpg/sounds/leather-drop.ogg +0 -0
- package/packs/kenney-rpg/sounds/leather-handle.ogg +0 -0
- package/packs/kenney-rpg/sounds/metal-click.ogg +0 -0
- package/packs/kenney-rpg/sounds/metal-latch.ogg +0 -0
- package/packs/kenney-rpg/sounds/metal-pot-2.ogg +0 -0
- package/packs/kenney-rpg/sounds/metal-pot.ogg +0 -0
- package/packs/kenney-rpg/sounds/sword-draw.ogg +0 -0
- package/packs/kenney-scifi/openpeon.json +122 -0
- package/packs/kenney-voiceover/openpeon.json +127 -0
- package/packs/openarena-announcer/openpeon.json +107 -0
- package/packs/retro-8bit/openpeon.json +122 -0
- package/packs/retro-movement/manifest.json +58 -0
- package/packs/retro-movement/openpeon.json +122 -0
- package/packs/retro-movement/sounds/blip-1.ogg +0 -0
- package/packs/retro-movement/sounds/blip-2.ogg +0 -0
- package/packs/retro-movement/sounds/door-open-1.ogg +0 -0
- package/packs/retro-movement/sounds/door-open-2.ogg +0 -0
- package/packs/retro-movement/sounds/door-open-3.ogg +0 -0
- package/packs/retro-movement/sounds/jump-1.ogg +0 -0
- package/packs/retro-movement/sounds/jump-2.ogg +0 -0
- package/packs/retro-movement/sounds/jump-3.ogg +0 -0
- package/packs/retro-movement/sounds/menu-select-1.ogg +0 -0
- package/packs/retro-movement/sounds/menu-select-2.ogg +0 -0
- package/packs/retro-movement/sounds/pause-in-2.ogg +0 -0
- package/packs/retro-movement/sounds/pause-in-3.ogg +0 -0
- package/packs/retro-movement/sounds/pause-in.ogg +0 -0
- package/packs/retro-movement/sounds/pause-out.ogg +0 -0
- package/packs/retro-movement/sounds/portal-1.ogg +0 -0
- package/packs/retro-movement/sounds/portal-2.ogg +0 -0
- package/packs/retro-movement/sounds/portal-3.ogg +0 -0
- package/packs/retro-movement/sounds/portal-4.ogg +0 -0
- package/packs/retro-weapons/manifest.json +57 -0
- package/packs/retro-weapons/openpeon.json +117 -0
- package/packs/retro-weapons/sounds/death-android-2.ogg +0 -0
- package/packs/retro-weapons/sounds/death-android.ogg +0 -0
- package/packs/retro-weapons/sounds/death-human-1.ogg +0 -0
- package/packs/retro-weapons/sounds/death-human-2.ogg +0 -0
- package/packs/retro-weapons/sounds/death-robot.ogg +0 -0
- package/packs/retro-weapons/sounds/explosion-1.ogg +0 -0
- package/packs/retro-weapons/sounds/explosion-2.ogg +0 -0
- package/packs/retro-weapons/sounds/explosion-long.ogg +0 -0
- package/packs/retro-weapons/sounds/explosion-soft.ogg +0 -0
- package/packs/retro-weapons/sounds/impact-1.ogg +0 -0
- package/packs/retro-weapons/sounds/impact-2.ogg +0 -0
- package/packs/retro-weapons/sounds/impact-3.ogg +0 -0
- package/packs/retro-weapons/sounds/interact-1.ogg +0 -0
- package/packs/retro-weapons/sounds/interact-2.ogg +0 -0
- package/packs/retro-weapons/sounds/interact-3.ogg +0 -0
- package/packs/retro-weapons/sounds/shot-1.ogg +0 -0
- package/packs/retro-weapons/sounds/shot-2.ogg +0 -0
- package/packs/retro-weapons/sounds/shot-3.ogg +0 -0
- package/packs/warzone2100-command/openpeon.json +137 -0
- package/packs/wesnoth-combat/openpeon.json +122 -0
- package/packs/xonotic-announcer/openpeon.json +107 -0
- package/src/cli/cesp.js +101 -0
- package/src/cli/demo.js +36 -0
- package/src/cli/play.js +8 -4
- package/src/cli/setup.js +173 -0
- package/src/cli/stats.js +96 -0
- package/src/cli/theme.js +44 -0
- package/src/packs.js +66 -9
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,96 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } 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
|
+
writeFileSync(statsPath, JSON.stringify(stats, null, 2) + '\n', 'utf8');
|
|
44
|
+
} catch {}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default function stats(args) {
|
|
48
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
49
|
+
showHelp();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const packs = listPacks();
|
|
54
|
+
const totalSounds = packs.reduce((s, p) => s + p.soundCount, 0);
|
|
55
|
+
const statsPath = getStatsPath();
|
|
56
|
+
|
|
57
|
+
console.log('\n PINGTHINGS STATS\n');
|
|
58
|
+
console.log(` Packs installed: ${packs.length}`);
|
|
59
|
+
console.log(` Total sounds: ${totalSounds}`);
|
|
60
|
+
console.log(` Categories: ${new Set(packs.map(p => p.category)).size}`);
|
|
61
|
+
|
|
62
|
+
if (!existsSync(statsPath)) {
|
|
63
|
+
console.log('\n No play history yet. Start using pingthings!\n');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let data;
|
|
68
|
+
try {
|
|
69
|
+
data = JSON.parse(readFileSync(statsPath, 'utf8'));
|
|
70
|
+
} catch {
|
|
71
|
+
console.log('\n No play history yet.\n');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log(`\n Total plays: ${data.totalPlays || 0}`);
|
|
76
|
+
|
|
77
|
+
const today = new Date().toISOString().split('T')[0];
|
|
78
|
+
console.log(` Plays today: ${data.dailyPlays?.[today] || 0}`);
|
|
79
|
+
|
|
80
|
+
if (data.packPlays && Object.keys(data.packPlays).length > 0) {
|
|
81
|
+
const sorted = Object.entries(data.packPlays).sort((a, b) => b[1] - a[1]);
|
|
82
|
+
console.log(`\n Most played packs:`);
|
|
83
|
+
for (const [pack, count] of sorted.slice(0, 5)) {
|
|
84
|
+
console.log(` ${pack.padEnd(24)} ${count} plays`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (data.eventPlays && Object.keys(data.eventPlays).length > 0) {
|
|
89
|
+
console.log(`\n Events:`);
|
|
90
|
+
for (const [event, count] of Object.entries(data.eventPlays)) {
|
|
91
|
+
console.log(` ${event.padEnd(16)} ${count}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log('');
|
|
96
|
+
}
|
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/packs.js
CHANGED
|
@@ -12,19 +12,76 @@ function getUserPacksDir() {
|
|
|
12
12
|
return join(getConfigDir(), 'packs');
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
// CESP event → pingthings event mapping
|
|
16
|
+
const CESP_EVENT_MAP = {
|
|
17
|
+
'session.start': 'permission',
|
|
18
|
+
'task.acknowledge': 'done',
|
|
19
|
+
'task.complete': 'complete',
|
|
20
|
+
'task.error': 'error',
|
|
21
|
+
'input.required': 'permission',
|
|
22
|
+
'resource.limit': 'blocked',
|
|
23
|
+
'user.spam': 'blocked',
|
|
24
|
+
'session.end': 'complete',
|
|
25
|
+
'task.progress': 'done',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function convertCespManifest(cesp) {
|
|
29
|
+
const events = {};
|
|
30
|
+
for (const [cespEvent, sounds] of Object.entries(cesp.categories || {})) {
|
|
31
|
+
const pingEvent = CESP_EVENT_MAP[cespEvent];
|
|
32
|
+
if (!pingEvent) continue;
|
|
33
|
+
const files = (sounds.sounds || []).map(s => s.file);
|
|
34
|
+
if (!events[pingEvent]) events[pingEvent] = [];
|
|
35
|
+
events[pingEvent].push(...files);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const allSounds = [];
|
|
39
|
+
for (const sounds of Object.values(cesp.categories || {})) {
|
|
40
|
+
for (const s of sounds.sounds || []) {
|
|
41
|
+
if (!allSounds.includes(s.file)) allSounds.push(s.file);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
name: cesp.name,
|
|
47
|
+
description: cesp.display_name || cesp.description || '',
|
|
48
|
+
version: cesp.version || '1.0.0',
|
|
49
|
+
license: cesp.license || 'Unknown',
|
|
50
|
+
credits: cesp.author?.name || '',
|
|
51
|
+
category: cesp.tags?.[0] || 'other',
|
|
52
|
+
sounds: allSounds,
|
|
53
|
+
events,
|
|
54
|
+
_cesp: true,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
15
58
|
function readManifest(packDir) {
|
|
59
|
+
// Prefer manifest.json (our native format) — richer metadata
|
|
16
60
|
const manifestPath = join(packDir, 'manifest.json');
|
|
17
|
-
if (
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
61
|
+
if (existsSync(manifestPath)) {
|
|
62
|
+
try {
|
|
63
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
64
|
+
if (!manifest.name) {
|
|
65
|
+
console.error(`Warning: manifest.json in ${packDir} is missing "name" field`);
|
|
66
|
+
}
|
|
67
|
+
return manifest;
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error(`Warning: Failed to parse ${manifestPath}: ${err.message}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Fall back to CESP format (openpeon.json) for community packs
|
|
74
|
+
const cespPath = join(packDir, 'openpeon.json');
|
|
75
|
+
if (existsSync(cespPath)) {
|
|
76
|
+
try {
|
|
77
|
+
const cesp = JSON.parse(readFileSync(cespPath, 'utf8'));
|
|
78
|
+
return convertCespManifest(cesp);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.error(`Warning: Failed to parse ${cespPath}: ${err.message}`);
|
|
22
81
|
}
|
|
23
|
-
return manifest;
|
|
24
|
-
} catch (err) {
|
|
25
|
-
console.error(`Warning: Failed to parse ${manifestPath}: ${err.message}`);
|
|
26
|
-
return null;
|
|
27
82
|
}
|
|
83
|
+
|
|
84
|
+
return null;
|
|
28
85
|
}
|
|
29
86
|
|
|
30
87
|
function discoverSounds(packDir) {
|