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.
Files changed (142) hide show
  1. package/bin/pingthings.js +7 -0
  2. package/package.json +1 -1
  3. package/packs/0ad-civilizations/openpeon.json +172 -0
  4. package/packs/7kaa-soldiers/openpeon.json +217 -0
  5. package/packs/droid-announcer/openpeon.json +107 -0
  6. package/packs/fighting-announcer/openpeon.json +132 -0
  7. package/packs/freedoom-arsenal/openpeon.json +117 -0
  8. package/packs/kenney-digital/manifest.json +58 -0
  9. package/packs/kenney-digital/openpeon.json +122 -0
  10. package/packs/kenney-digital/sounds/high-up.ogg +0 -0
  11. package/packs/kenney-digital/sounds/low-down.ogg +0 -0
  12. package/packs/kenney-digital/sounds/pep-1.ogg +0 -0
  13. package/packs/kenney-digital/sounds/pep-2.ogg +0 -0
  14. package/packs/kenney-digital/sounds/phase-jump-1.ogg +0 -0
  15. package/packs/kenney-digital/sounds/phase-jump-2.ogg +0 -0
  16. package/packs/kenney-digital/sounds/phaser-down.ogg +0 -0
  17. package/packs/kenney-digital/sounds/phaser-up-1.ogg +0 -0
  18. package/packs/kenney-digital/sounds/phaser-up-2.ogg +0 -0
  19. package/packs/kenney-digital/sounds/power-up-1.ogg +0 -0
  20. package/packs/kenney-digital/sounds/power-up-2.ogg +0 -0
  21. package/packs/kenney-digital/sounds/power-up-3.ogg +0 -0
  22. package/packs/kenney-digital/sounds/space-trash.ogg +0 -0
  23. package/packs/kenney-digital/sounds/three-tone.ogg +0 -0
  24. package/packs/kenney-digital/sounds/tone.ogg +0 -0
  25. package/packs/kenney-digital/sounds/two-tone.ogg +0 -0
  26. package/packs/kenney-digital/sounds/zap-1.ogg +0 -0
  27. package/packs/kenney-digital/sounds/zap-2.ogg +0 -0
  28. package/packs/kenney-fighter/manifest.json +58 -0
  29. package/packs/kenney-fighter/openpeon.json +122 -0
  30. package/packs/kenney-fighter/sounds/begin.ogg +0 -0
  31. package/packs/kenney-fighter/sounds/combo-breaker.ogg +0 -0
  32. package/packs/kenney-fighter/sounds/combo.ogg +0 -0
  33. package/packs/kenney-fighter/sounds/fight.ogg +0 -0
  34. package/packs/kenney-fighter/sounds/final-round.ogg +0 -0
  35. package/packs/kenney-fighter/sounds/flawless-victory.ogg +0 -0
  36. package/packs/kenney-fighter/sounds/game-over.ogg +0 -0
  37. package/packs/kenney-fighter/sounds/loser.ogg +0 -0
  38. package/packs/kenney-fighter/sounds/multi-kill.ogg +0 -0
  39. package/packs/kenney-fighter/sounds/prepare-yourself.ogg +0 -0
  40. package/packs/kenney-fighter/sounds/ready.ogg +0 -0
  41. package/packs/kenney-fighter/sounds/round-1.ogg +0 -0
  42. package/packs/kenney-fighter/sounds/sudden-death.ogg +0 -0
  43. package/packs/kenney-fighter/sounds/tie-breaker.ogg +0 -0
  44. package/packs/kenney-fighter/sounds/time.ogg +0 -0
  45. package/packs/kenney-fighter/sounds/winner.ogg +0 -0
  46. package/packs/kenney-fighter/sounds/you-lose.ogg +0 -0
  47. package/packs/kenney-fighter/sounds/you-win.ogg +0 -0
  48. package/packs/kenney-impacts/manifest.json +57 -0
  49. package/packs/kenney-impacts/openpeon.json +116 -0
  50. package/packs/kenney-impacts/sounds/bell-heavy-2.ogg +0 -0
  51. package/packs/kenney-impacts/sounds/bell-heavy.ogg +0 -0
  52. package/packs/kenney-impacts/sounds/glass-heavy.ogg +0 -0
  53. package/packs/kenney-impacts/sounds/glass-light.ogg +0 -0
  54. package/packs/kenney-impacts/sounds/glass-medium.ogg +0 -0
  55. package/packs/kenney-impacts/sounds/mining-1.ogg +0 -0
  56. package/packs/kenney-impacts/sounds/mining-2.ogg +0 -0
  57. package/packs/kenney-impacts/sounds/plate-heavy-2.ogg +0 -0
  58. package/packs/kenney-impacts/sounds/plate-heavy.ogg +0 -0
  59. package/packs/kenney-impacts/sounds/plate-light-1.ogg +0 -0
  60. package/packs/kenney-impacts/sounds/plate-light-2.ogg +0 -0
  61. package/packs/kenney-impacts/sounds/plate-medium.ogg +0 -0
  62. package/packs/kenney-impacts/sounds/soft-heavy.ogg +0 -0
  63. package/packs/kenney-impacts/sounds/soft-medium.ogg +0 -0
  64. package/packs/kenney-impacts/sounds/tin-medium.ogg +0 -0
  65. package/packs/kenney-impacts/sounds/wood-heavy.ogg +0 -0
  66. package/packs/kenney-impacts/sounds/wood-light.ogg +0 -0
  67. package/packs/kenney-impacts/sounds/wood-medium.ogg +0 -0
  68. package/packs/kenney-interface/openpeon.json +122 -0
  69. package/packs/kenney-rpg/manifest.json +57 -0
  70. package/packs/kenney-rpg/openpeon.json +117 -0
  71. package/packs/kenney-rpg/sounds/belt-handle-2.ogg +0 -0
  72. package/packs/kenney-rpg/sounds/belt-handle.ogg +0 -0
  73. package/packs/kenney-rpg/sounds/book-close.ogg +0 -0
  74. package/packs/kenney-rpg/sounds/book-flip.ogg +0 -0
  75. package/packs/kenney-rpg/sounds/book-open.ogg +0 -0
  76. package/packs/kenney-rpg/sounds/book-place.ogg +0 -0
  77. package/packs/kenney-rpg/sounds/cloth-belt.ogg +0 -0
  78. package/packs/kenney-rpg/sounds/creak.ogg +0 -0
  79. package/packs/kenney-rpg/sounds/door-close.ogg +0 -0
  80. package/packs/kenney-rpg/sounds/door-open.ogg +0 -0
  81. package/packs/kenney-rpg/sounds/draw-knife.ogg +0 -0
  82. package/packs/kenney-rpg/sounds/leather-drop.ogg +0 -0
  83. package/packs/kenney-rpg/sounds/leather-handle.ogg +0 -0
  84. package/packs/kenney-rpg/sounds/metal-click.ogg +0 -0
  85. package/packs/kenney-rpg/sounds/metal-latch.ogg +0 -0
  86. package/packs/kenney-rpg/sounds/metal-pot-2.ogg +0 -0
  87. package/packs/kenney-rpg/sounds/metal-pot.ogg +0 -0
  88. package/packs/kenney-rpg/sounds/sword-draw.ogg +0 -0
  89. package/packs/kenney-scifi/openpeon.json +122 -0
  90. package/packs/kenney-voiceover/openpeon.json +127 -0
  91. package/packs/openarena-announcer/openpeon.json +107 -0
  92. package/packs/retro-8bit/openpeon.json +122 -0
  93. package/packs/retro-movement/manifest.json +58 -0
  94. package/packs/retro-movement/openpeon.json +122 -0
  95. package/packs/retro-movement/sounds/blip-1.ogg +0 -0
  96. package/packs/retro-movement/sounds/blip-2.ogg +0 -0
  97. package/packs/retro-movement/sounds/door-open-1.ogg +0 -0
  98. package/packs/retro-movement/sounds/door-open-2.ogg +0 -0
  99. package/packs/retro-movement/sounds/door-open-3.ogg +0 -0
  100. package/packs/retro-movement/sounds/jump-1.ogg +0 -0
  101. package/packs/retro-movement/sounds/jump-2.ogg +0 -0
  102. package/packs/retro-movement/sounds/jump-3.ogg +0 -0
  103. package/packs/retro-movement/sounds/menu-select-1.ogg +0 -0
  104. package/packs/retro-movement/sounds/menu-select-2.ogg +0 -0
  105. package/packs/retro-movement/sounds/pause-in-2.ogg +0 -0
  106. package/packs/retro-movement/sounds/pause-in-3.ogg +0 -0
  107. package/packs/retro-movement/sounds/pause-in.ogg +0 -0
  108. package/packs/retro-movement/sounds/pause-out.ogg +0 -0
  109. package/packs/retro-movement/sounds/portal-1.ogg +0 -0
  110. package/packs/retro-movement/sounds/portal-2.ogg +0 -0
  111. package/packs/retro-movement/sounds/portal-3.ogg +0 -0
  112. package/packs/retro-movement/sounds/portal-4.ogg +0 -0
  113. package/packs/retro-weapons/manifest.json +57 -0
  114. package/packs/retro-weapons/openpeon.json +117 -0
  115. package/packs/retro-weapons/sounds/death-android-2.ogg +0 -0
  116. package/packs/retro-weapons/sounds/death-android.ogg +0 -0
  117. package/packs/retro-weapons/sounds/death-human-1.ogg +0 -0
  118. package/packs/retro-weapons/sounds/death-human-2.ogg +0 -0
  119. package/packs/retro-weapons/sounds/death-robot.ogg +0 -0
  120. package/packs/retro-weapons/sounds/explosion-1.ogg +0 -0
  121. package/packs/retro-weapons/sounds/explosion-2.ogg +0 -0
  122. package/packs/retro-weapons/sounds/explosion-long.ogg +0 -0
  123. package/packs/retro-weapons/sounds/explosion-soft.ogg +0 -0
  124. package/packs/retro-weapons/sounds/impact-1.ogg +0 -0
  125. package/packs/retro-weapons/sounds/impact-2.ogg +0 -0
  126. package/packs/retro-weapons/sounds/impact-3.ogg +0 -0
  127. package/packs/retro-weapons/sounds/interact-1.ogg +0 -0
  128. package/packs/retro-weapons/sounds/interact-2.ogg +0 -0
  129. package/packs/retro-weapons/sounds/interact-3.ogg +0 -0
  130. package/packs/retro-weapons/sounds/shot-1.ogg +0 -0
  131. package/packs/retro-weapons/sounds/shot-2.ogg +0 -0
  132. package/packs/retro-weapons/sounds/shot-3.ogg +0 -0
  133. package/packs/warzone2100-command/openpeon.json +137 -0
  134. package/packs/wesnoth-combat/openpeon.json +122 -0
  135. package/packs/xonotic-announcer/openpeon.json +107 -0
  136. package/src/cli/cesp.js +101 -0
  137. package/src/cli/demo.js +36 -0
  138. package/src/cli/play.js +8 -4
  139. package/src/cli/setup.js +173 -0
  140. package/src/cli/stats.js +96 -0
  141. package/src/cli/theme.js +44 -0
  142. 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 check
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) {
@@ -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
+ }
@@ -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 (!existsSync(manifestPath)) return null;
18
- try {
19
- const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
20
- if (!manifest.name) {
21
- console.error(`Warning: manifest.json in ${packDir} is missing "name" field`);
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) {