ra2-eva-cursor-hooks 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.
Files changed (269) hide show
  1. package/assets/audio/README.md +74 -0
  2. package/assets/audio/eva_allied/TRANSCRIPTIONS.txt +131 -0
  3. package/assets/audio/eva_allied/ceva001.wav +0 -0
  4. package/assets/audio/eva_allied/ceva002.wav +0 -0
  5. package/assets/audio/eva_allied/ceva003.wav +0 -0
  6. package/assets/audio/eva_allied/ceva004.wav +0 -0
  7. package/assets/audio/eva_allied/ceva005.wav +0 -0
  8. package/assets/audio/eva_allied/ceva006.wav +0 -0
  9. package/assets/audio/eva_allied/ceva007.wav +0 -0
  10. package/assets/audio/eva_allied/ceva008.wav +0 -0
  11. package/assets/audio/eva_allied/ceva009.wav +0 -0
  12. package/assets/audio/eva_allied/ceva010.wav +0 -0
  13. package/assets/audio/eva_allied/ceva011.wav +0 -0
  14. package/assets/audio/eva_allied/ceva012.wav +0 -0
  15. package/assets/audio/eva_allied/ceva013.wav +0 -0
  16. package/assets/audio/eva_allied/ceva014.wav +0 -0
  17. package/assets/audio/eva_allied/ceva015.wav +0 -0
  18. package/assets/audio/eva_allied/ceva016.wav +0 -0
  19. package/assets/audio/eva_allied/ceva017.wav +0 -0
  20. package/assets/audio/eva_allied/ceva018.wav +0 -0
  21. package/assets/audio/eva_allied/ceva019.wav +0 -0
  22. package/assets/audio/eva_allied/ceva020.wav +0 -0
  23. package/assets/audio/eva_allied/ceva021.wav +0 -0
  24. package/assets/audio/eva_allied/ceva022.wav +0 -0
  25. package/assets/audio/eva_allied/ceva023.wav +0 -0
  26. package/assets/audio/eva_allied/ceva024.wav +0 -0
  27. package/assets/audio/eva_allied/ceva025.wav +0 -0
  28. package/assets/audio/eva_allied/ceva026.wav +0 -0
  29. package/assets/audio/eva_allied/ceva027.wav +0 -0
  30. package/assets/audio/eva_allied/ceva028.wav +0 -0
  31. package/assets/audio/eva_allied/ceva029.wav +0 -0
  32. package/assets/audio/eva_allied/ceva030.wav +0 -0
  33. package/assets/audio/eva_allied/ceva031.wav +0 -0
  34. package/assets/audio/eva_allied/ceva032.wav +0 -0
  35. package/assets/audio/eva_allied/ceva033.wav +0 -0
  36. package/assets/audio/eva_allied/ceva035.wav +0 -0
  37. package/assets/audio/eva_allied/ceva036.wav +0 -0
  38. package/assets/audio/eva_allied/ceva037.wav +0 -0
  39. package/assets/audio/eva_allied/ceva038.wav +0 -0
  40. package/assets/audio/eva_allied/ceva039.wav +0 -0
  41. package/assets/audio/eva_allied/ceva040.wav +0 -0
  42. package/assets/audio/eva_allied/ceva041.wav +0 -0
  43. package/assets/audio/eva_allied/ceva042.wav +0 -0
  44. package/assets/audio/eva_allied/ceva043.wav +0 -0
  45. package/assets/audio/eva_allied/ceva044.wav +0 -0
  46. package/assets/audio/eva_allied/ceva045.wav +0 -0
  47. package/assets/audio/eva_allied/ceva046.wav +0 -0
  48. package/assets/audio/eva_allied/ceva047.wav +0 -0
  49. package/assets/audio/eva_allied/ceva048.wav +0 -0
  50. package/assets/audio/eva_allied/ceva049.wav +0 -0
  51. package/assets/audio/eva_allied/ceva050.wav +0 -0
  52. package/assets/audio/eva_allied/ceva051.wav +0 -0
  53. package/assets/audio/eva_allied/ceva052.wav +0 -0
  54. package/assets/audio/eva_allied/ceva053.wav +0 -0
  55. package/assets/audio/eva_allied/ceva054.wav +0 -0
  56. package/assets/audio/eva_allied/ceva055.wav +0 -0
  57. package/assets/audio/eva_allied/ceva056.wav +0 -0
  58. package/assets/audio/eva_allied/ceva057.wav +0 -0
  59. package/assets/audio/eva_allied/ceva058.wav +0 -0
  60. package/assets/audio/eva_allied/ceva059.wav +0 -0
  61. package/assets/audio/eva_allied/ceva060.wav +0 -0
  62. package/assets/audio/eva_allied/ceva061.wav +0 -0
  63. package/assets/audio/eva_allied/ceva062.wav +0 -0
  64. package/assets/audio/eva_allied/ceva063.wav +0 -0
  65. package/assets/audio/eva_allied/ceva064.wav +0 -0
  66. package/assets/audio/eva_allied/ceva065.wav +0 -0
  67. package/assets/audio/eva_allied/ceva066.wav +0 -0
  68. package/assets/audio/eva_allied/ceva067.wav +0 -0
  69. package/assets/audio/eva_allied/ceva068.wav +0 -0
  70. package/assets/audio/eva_allied/ceva069.wav +0 -0
  71. package/assets/audio/eva_allied/ceva070.wav +0 -0
  72. package/assets/audio/eva_allied/ceva071.wav +0 -0
  73. package/assets/audio/eva_allied/ceva072.wav +0 -0
  74. package/assets/audio/eva_allied/ceva073.wav +0 -0
  75. package/assets/audio/eva_allied/ceva074.wav +0 -0
  76. package/assets/audio/eva_allied/ceva075.wav +0 -0
  77. package/assets/audio/eva_allied/ceva076.wav +0 -0
  78. package/assets/audio/eva_allied/ceva077.wav +0 -0
  79. package/assets/audio/eva_allied/ceva078.wav +0 -0
  80. package/assets/audio/eva_allied/ceva079.wav +0 -0
  81. package/assets/audio/eva_allied/ceva080.wav +0 -0
  82. package/assets/audio/eva_allied/ceva081.wav +0 -0
  83. package/assets/audio/eva_allied/ceva082.wav +0 -0
  84. package/assets/audio/eva_allied/ceva083.wav +0 -0
  85. package/assets/audio/eva_allied/ceva084.wav +0 -0
  86. package/assets/audio/eva_allied/ceva085.wav +0 -0
  87. package/assets/audio/eva_allied/ceva086.wav +0 -0
  88. package/assets/audio/eva_allied/ceva087.wav +0 -0
  89. package/assets/audio/eva_allied/ceva088.wav +0 -0
  90. package/assets/audio/eva_allied/ceva089.wav +0 -0
  91. package/assets/audio/eva_allied/ceva090.wav +0 -0
  92. package/assets/audio/eva_allied/ceva091.wav +0 -0
  93. package/assets/audio/eva_allied/ceva092.wav +0 -0
  94. package/assets/audio/eva_allied/ceva093.wav +0 -0
  95. package/assets/audio/eva_allied/ceva094.wav +0 -0
  96. package/assets/audio/eva_allied/ceva095.wav +0 -0
  97. package/assets/audio/eva_allied/ceva096.wav +0 -0
  98. package/assets/audio/eva_allied/ceva097.wav +0 -0
  99. package/assets/audio/eva_allied/ceva098.wav +0 -0
  100. package/assets/audio/eva_allied/ceva099.wav +0 -0
  101. package/assets/audio/eva_allied/ceva100.wav +0 -0
  102. package/assets/audio/eva_allied/ceva101.wav +0 -0
  103. package/assets/audio/eva_allied/ceva102.wav +0 -0
  104. package/assets/audio/eva_allied/ceva103.wav +0 -0
  105. package/assets/audio/eva_allied/ceva104.wav +0 -0
  106. package/assets/audio/eva_allied/ceva105.wav +0 -0
  107. package/assets/audio/eva_allied/ceva106.wav +0 -0
  108. package/assets/audio/eva_allied/ceva107.wav +0 -0
  109. package/assets/audio/eva_allied/ceva108.wav +0 -0
  110. package/assets/audio/eva_allied/ceva109.wav +0 -0
  111. package/assets/audio/eva_allied/ceva120.wav +0 -0
  112. package/assets/audio/eva_allied/ceva121.wav +0 -0
  113. package/assets/audio/eva_allied/ceva122.wav +0 -0
  114. package/assets/audio/eva_allied/cevau06.wav +0 -0
  115. package/assets/audio/eva_allied/cevau07.wav +0 -0
  116. package/assets/audio/eva_allied/cevau08.wav +0 -0
  117. package/assets/audio/eva_allied/cevau13.wav +0 -0
  118. package/assets/audio/eva_allied/cevau15.wav +0 -0
  119. package/assets/audio/eva_allied/cevau19.wav +0 -0
  120. package/assets/audio/eva_allied/cevau20.wav +0 -0
  121. package/assets/audio/eva_allied/cevau22.wav +0 -0
  122. package/assets/audio/eva_allied/cevau23.wav +0 -0
  123. package/assets/audio/eva_allied/cevau24.wav +0 -0
  124. package/assets/audio/eva_allied/cevau25.wav +0 -0
  125. package/assets/audio/eva_allied/cevau26.wav +0 -0
  126. package/assets/audio/eva_allied/cevau27.wav +0 -0
  127. package/assets/audio/eva_allied/cevau31.wav +0 -0
  128. package/assets/audio/eva_allied/cevau36.wav +0 -0
  129. package/assets/audio/eva_allied/cevau37.wav +0 -0
  130. package/assets/audio/eva_allied/cevau38.wav +0 -0
  131. package/assets/audio/eva_allied/transcriptions.json +130 -0
  132. package/assets/audio/eva_soviet/TRANSCRIPTIONS.txt +133 -0
  133. package/assets/audio/eva_soviet/csof001.wav +0 -0
  134. package/assets/audio/eva_soviet/csof002.wav +0 -0
  135. package/assets/audio/eva_soviet/csof003.wav +0 -0
  136. package/assets/audio/eva_soviet/csof004.wav +0 -0
  137. package/assets/audio/eva_soviet/csof005.wav +0 -0
  138. package/assets/audio/eva_soviet/csof006.wav +0 -0
  139. package/assets/audio/eva_soviet/csof007.wav +0 -0
  140. package/assets/audio/eva_soviet/csof008.wav +0 -0
  141. package/assets/audio/eva_soviet/csof009.wav +0 -0
  142. package/assets/audio/eva_soviet/csof010.wav +0 -0
  143. package/assets/audio/eva_soviet/csof011.wav +0 -0
  144. package/assets/audio/eva_soviet/csof012.wav +0 -0
  145. package/assets/audio/eva_soviet/csof013.wav +0 -0
  146. package/assets/audio/eva_soviet/csof014.wav +0 -0
  147. package/assets/audio/eva_soviet/csof015.wav +0 -0
  148. package/assets/audio/eva_soviet/csof016.wav +0 -0
  149. package/assets/audio/eva_soviet/csof017.wav +0 -0
  150. package/assets/audio/eva_soviet/csof018.wav +0 -0
  151. package/assets/audio/eva_soviet/csof019.wav +0 -0
  152. package/assets/audio/eva_soviet/csof020.wav +0 -0
  153. package/assets/audio/eva_soviet/csof021.wav +0 -0
  154. package/assets/audio/eva_soviet/csof022.wav +0 -0
  155. package/assets/audio/eva_soviet/csof023.wav +0 -0
  156. package/assets/audio/eva_soviet/csof024.wav +0 -0
  157. package/assets/audio/eva_soviet/csof025.wav +0 -0
  158. package/assets/audio/eva_soviet/csof026.wav +0 -0
  159. package/assets/audio/eva_soviet/csof027.wav +0 -0
  160. package/assets/audio/eva_soviet/csof028.wav +0 -0
  161. package/assets/audio/eva_soviet/csof029.wav +0 -0
  162. package/assets/audio/eva_soviet/csof030.wav +0 -0
  163. package/assets/audio/eva_soviet/csof031.wav +0 -0
  164. package/assets/audio/eva_soviet/csof032.wav +0 -0
  165. package/assets/audio/eva_soviet/csof033.wav +0 -0
  166. package/assets/audio/eva_soviet/csof035.wav +0 -0
  167. package/assets/audio/eva_soviet/csof036.wav +0 -0
  168. package/assets/audio/eva_soviet/csof037.wav +0 -0
  169. package/assets/audio/eva_soviet/csof038.wav +0 -0
  170. package/assets/audio/eva_soviet/csof039.wav +0 -0
  171. package/assets/audio/eva_soviet/csof040.wav +0 -0
  172. package/assets/audio/eva_soviet/csof041.wav +0 -0
  173. package/assets/audio/eva_soviet/csof042.wav +0 -0
  174. package/assets/audio/eva_soviet/csof043.wav +0 -0
  175. package/assets/audio/eva_soviet/csof044.wav +0 -0
  176. package/assets/audio/eva_soviet/csof045.wav +0 -0
  177. package/assets/audio/eva_soviet/csof046.wav +0 -0
  178. package/assets/audio/eva_soviet/csof047.wav +0 -0
  179. package/assets/audio/eva_soviet/csof048.wav +0 -0
  180. package/assets/audio/eva_soviet/csof049.wav +0 -0
  181. package/assets/audio/eva_soviet/csof050.wav +0 -0
  182. package/assets/audio/eva_soviet/csof051.wav +0 -0
  183. package/assets/audio/eva_soviet/csof052.wav +0 -0
  184. package/assets/audio/eva_soviet/csof053.wav +0 -0
  185. package/assets/audio/eva_soviet/csof054.wav +0 -0
  186. package/assets/audio/eva_soviet/csof055.wav +0 -0
  187. package/assets/audio/eva_soviet/csof056.wav +0 -0
  188. package/assets/audio/eva_soviet/csof057.wav +0 -0
  189. package/assets/audio/eva_soviet/csof058.wav +0 -0
  190. package/assets/audio/eva_soviet/csof059.wav +0 -0
  191. package/assets/audio/eva_soviet/csof060.wav +0 -0
  192. package/assets/audio/eva_soviet/csof061.wav +0 -0
  193. package/assets/audio/eva_soviet/csof062.wav +0 -0
  194. package/assets/audio/eva_soviet/csof063.wav +0 -0
  195. package/assets/audio/eva_soviet/csof064.wav +0 -0
  196. package/assets/audio/eva_soviet/csof065.wav +0 -0
  197. package/assets/audio/eva_soviet/csof066.wav +0 -0
  198. package/assets/audio/eva_soviet/csof067.wav +0 -0
  199. package/assets/audio/eva_soviet/csof068.wav +0 -0
  200. package/assets/audio/eva_soviet/csof069.wav +0 -0
  201. package/assets/audio/eva_soviet/csof070.wav +0 -0
  202. package/assets/audio/eva_soviet/csof071.wav +0 -0
  203. package/assets/audio/eva_soviet/csof072.wav +0 -0
  204. package/assets/audio/eva_soviet/csof073.wav +0 -0
  205. package/assets/audio/eva_soviet/csof074.wav +0 -0
  206. package/assets/audio/eva_soviet/csof075.wav +0 -0
  207. package/assets/audio/eva_soviet/csof076.wav +0 -0
  208. package/assets/audio/eva_soviet/csof077.wav +0 -0
  209. package/assets/audio/eva_soviet/csof078.wav +0 -0
  210. package/assets/audio/eva_soviet/csof079.wav +0 -0
  211. package/assets/audio/eva_soviet/csof080.wav +0 -0
  212. package/assets/audio/eva_soviet/csof081.wav +0 -0
  213. package/assets/audio/eva_soviet/csof082.wav +0 -0
  214. package/assets/audio/eva_soviet/csof083.wav +0 -0
  215. package/assets/audio/eva_soviet/csof084.wav +0 -0
  216. package/assets/audio/eva_soviet/csof085.wav +0 -0
  217. package/assets/audio/eva_soviet/csof086.wav +0 -0
  218. package/assets/audio/eva_soviet/csof087.wav +0 -0
  219. package/assets/audio/eva_soviet/csof088.wav +0 -0
  220. package/assets/audio/eva_soviet/csof089.wav +0 -0
  221. package/assets/audio/eva_soviet/csof090.wav +0 -0
  222. package/assets/audio/eva_soviet/csof091.wav +0 -0
  223. package/assets/audio/eva_soviet/csof092.wav +0 -0
  224. package/assets/audio/eva_soviet/csof093.wav +0 -0
  225. package/assets/audio/eva_soviet/csof094.wav +0 -0
  226. package/assets/audio/eva_soviet/csof095.wav +0 -0
  227. package/assets/audio/eva_soviet/csof096.wav +0 -0
  228. package/assets/audio/eva_soviet/csof097.wav +0 -0
  229. package/assets/audio/eva_soviet/csof098.wav +0 -0
  230. package/assets/audio/eva_soviet/csof099.wav +0 -0
  231. package/assets/audio/eva_soviet/csof100.wav +0 -0
  232. package/assets/audio/eva_soviet/csof101.wav +0 -0
  233. package/assets/audio/eva_soviet/csof102.wav +0 -0
  234. package/assets/audio/eva_soviet/csof103.wav +0 -0
  235. package/assets/audio/eva_soviet/csof104.wav +0 -0
  236. package/assets/audio/eva_soviet/csof105.wav +0 -0
  237. package/assets/audio/eva_soviet/csof106.wav +0 -0
  238. package/assets/audio/eva_soviet/csof107.wav +0 -0
  239. package/assets/audio/eva_soviet/csof108.wav +0 -0
  240. package/assets/audio/eva_soviet/csof109.wav +0 -0
  241. package/assets/audio/eva_soviet/csof120.wav +0 -0
  242. package/assets/audio/eva_soviet/csof121.wav +0 -0
  243. package/assets/audio/eva_soviet/csof122.wav +0 -0
  244. package/assets/audio/eva_soviet/csofu04.wav +0 -0
  245. package/assets/audio/eva_soviet/csofu06.wav +0 -0
  246. package/assets/audio/eva_soviet/csofu07.wav +0 -0
  247. package/assets/audio/eva_soviet/csofu08.wav +0 -0
  248. package/assets/audio/eva_soviet/csofu09.wav +0 -0
  249. package/assets/audio/eva_soviet/csofu10.wav +0 -0
  250. package/assets/audio/eva_soviet/csofu11.wav +0 -0
  251. package/assets/audio/eva_soviet/csofu15.wav +0 -0
  252. package/assets/audio/eva_soviet/csofu16.wav +0 -0
  253. package/assets/audio/eva_soviet/csofu18.wav +0 -0
  254. package/assets/audio/eva_soviet/csofu19.wav +0 -0
  255. package/assets/audio/eva_soviet/csofu20.wav +0 -0
  256. package/assets/audio/eva_soviet/csofu21.wav +0 -0
  257. package/assets/audio/eva_soviet/csofu25.wav +0 -0
  258. package/assets/audio/eva_soviet/csofu26.wav +0 -0
  259. package/assets/audio/eva_soviet/csofu27.wav +0 -0
  260. package/assets/audio/eva_soviet/csofu30.wav +0 -0
  261. package/assets/audio/eva_soviet/csofu31.wav +0 -0
  262. package/assets/audio/eva_soviet/csofu33.wav +0 -0
  263. package/assets/audio/eva_soviet/transcriptions.json +132 -0
  264. package/bin/install.js +249 -0
  265. package/package.json +33 -0
  266. package/src/index.ts +122 -0
  267. package/src/player.ts +230 -0
  268. package/src/sounds.ts +176 -0
  269. package/src/types.ts +241 -0
package/src/player.ts ADDED
@@ -0,0 +1,230 @@
1
+ /// <reference types="bun-types" />
2
+ /**
3
+ * Red Alert 2 EVA Audio Player
4
+ * Plays WAV files using macOS afplay command
5
+ * Includes audio queue to prevent overlapping sounds
6
+ */
7
+
8
+ import { spawnSync } from "bun";
9
+ import { existsSync, unlinkSync, writeFileSync } from "fs";
10
+ import { dirname, resolve } from "path";
11
+ import { SOUND_MAPPINGS, getStopSoundKey } from "./sounds";
12
+ import type { Faction, HookInput, PostToolUseInput, StopInput } from "./types";
13
+
14
+ // Resolve assets directory relative to this file
15
+ const SCRIPT_DIR = dirname(Bun.main);
16
+ const ASSETS_DIR = resolve(SCRIPT_DIR, "assets");
17
+
18
+ // Lock file for audio queue
19
+ const LOCK_FILE = "/tmp/ra2-eva-audio.lock";
20
+ const MAX_WAIT_MS = 10000; // Max 10 seconds to wait for lock
21
+ const POLL_INTERVAL_MS = 50; // Check every 50ms
22
+
23
+ /**
24
+ * Acquire the audio lock (blocks until available or timeout)
25
+ */
26
+ async function acquireLock(): Promise<boolean> {
27
+ const startTime = Date.now();
28
+
29
+ while (existsSync(LOCK_FILE)) {
30
+ // Check if lock is stale (older than 5 seconds = stuck process)
31
+ try {
32
+ const stat = Bun.file(LOCK_FILE);
33
+ const lockTime = parseInt(await stat.text(), 10);
34
+ if (Date.now() - lockTime > 5000) {
35
+ // Stale lock, remove it
36
+ try {
37
+ unlinkSync(LOCK_FILE);
38
+ } catch {}
39
+ break;
40
+ }
41
+ } catch {}
42
+
43
+ // Timeout check
44
+ if (Date.now() - startTime > MAX_WAIT_MS) {
45
+ console.error("[EVA] Timeout waiting for audio lock");
46
+ return false;
47
+ }
48
+
49
+ await Bun.sleep(POLL_INTERVAL_MS);
50
+ }
51
+
52
+ // Create lock with current timestamp
53
+ try {
54
+ writeFileSync(LOCK_FILE, Date.now().toString());
55
+ return true;
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Release the audio lock
63
+ */
64
+ function releaseLock(): void {
65
+ try {
66
+ unlinkSync(LOCK_FILE);
67
+ } catch {}
68
+ }
69
+
70
+ /**
71
+ * Get the current faction based on hour
72
+ * Odd hours = Allied, Even hours = Soviet
73
+ */
74
+ export function getFaction(): Faction {
75
+ const hour = new Date().getHours();
76
+ return hour % 2 === 1 ? "allied" : "soviet";
77
+ }
78
+
79
+ /**
80
+ * Get a random element from an array
81
+ */
82
+ function randomChoice<T>(arr: T[]): T {
83
+ return arr[Math.floor(Math.random() * arr.length)];
84
+ }
85
+
86
+ /**
87
+ * Get the sound file path for a hook event
88
+ */
89
+ export function getSoundPath(
90
+ hookType: string,
91
+ faction: Faction
92
+ ): string | null {
93
+ const mapping = SOUND_MAPPINGS[hookType];
94
+ if (!mapping) {
95
+ console.error(`[EVA] No sound mapping for hook: ${hookType}`);
96
+ return null;
97
+ }
98
+
99
+ const sounds = mapping[faction];
100
+ if (!sounds || sounds.length === 0) {
101
+ return null;
102
+ }
103
+
104
+ const soundFile = randomChoice(sounds);
105
+ const factionDir = faction === "allied" ? "eva_allied" : "eva_soviet";
106
+
107
+ return resolve(ASSETS_DIR, factionDir, soundFile);
108
+ }
109
+
110
+ /**
111
+ * Determine the sound key for lookup
112
+ * Handles special cases like stop with different statuses
113
+ * Returns null to skip playing sound for this hook
114
+ */
115
+ export function getSoundKey(input: HookInput): string | null {
116
+ const hookName = input.hook_event_name;
117
+
118
+ // Handle stop hook with status-based sounds
119
+ if (hookName === "stop") {
120
+ const stopInput = input as StopInput;
121
+ return getStopSoundKey(stopInput.status);
122
+ }
123
+
124
+ // Handle preToolUse - skip for tools with dedicated hooks
125
+ if (hookName === "preToolUse") {
126
+ const toolInput = input as PostToolUseInput;
127
+ if (toolInput.tool_name === "Shell") {
128
+ return null; // Skip - beforeShellExecution handles shell
129
+ }
130
+ if (toolInput.tool_name === "Read") {
131
+ return null; // Skip - beforeReadFile handles read
132
+ }
133
+ if (
134
+ toolInput.tool_name === "Write" ||
135
+ toolInput.tool_name === "StrReplace"
136
+ ) {
137
+ return null; // Skip - afterFileEdit handles write/edit
138
+ }
139
+ }
140
+
141
+ // Handle postToolUse - skip for tools with dedicated hooks
142
+ if (hookName === "postToolUse") {
143
+ const toolInput = input as PostToolUseInput;
144
+ // Skip Shell - afterShellExecution handles it
145
+ if (toolInput.tool_name === "Shell") {
146
+ return null;
147
+ }
148
+ // Skip Read - no sound needed after reading
149
+ if (toolInput.tool_name === "Read") {
150
+ return null;
151
+ }
152
+ // Skip Write/StrReplace - afterFileEdit handles it
153
+ if (
154
+ toolInput.tool_name === "Write" ||
155
+ toolInput.tool_name === "StrReplace"
156
+ ) {
157
+ return null;
158
+ }
159
+ // Delete tool plays "Unit lost" - something was destroyed!
160
+ if (toolInput.tool_name === "Delete") {
161
+ return "postToolUseFailure"; // Maps to "Unit lost"
162
+ }
163
+ }
164
+
165
+ // Handle postToolUseFailure - skip Read failures (often just "file doesn't exist" checks)
166
+ if (hookName === "postToolUseFailure") {
167
+ const toolInput = input as PostToolUseInput;
168
+ if (toolInput.tool_name === "Read") {
169
+ return null; // Skip - file not existing is expected when checking before create
170
+ }
171
+ }
172
+
173
+ return hookName;
174
+ }
175
+
176
+ /**
177
+ * Play a WAV file with queue support
178
+ * Waits for previous audio to finish before playing
179
+ */
180
+ export async function playSound(filePath: string): Promise<void> {
181
+ try {
182
+ // Check if file exists
183
+ const file = Bun.file(filePath);
184
+ if (!(await file.exists())) {
185
+ console.error(`[EVA] Sound file not found: ${filePath}`);
186
+ return;
187
+ }
188
+
189
+ // Acquire lock (wait for previous audio to finish)
190
+ const gotLock = await acquireLock();
191
+ if (!gotLock) {
192
+ console.error(
193
+ `[EVA] Could not acquire audio lock, skipping: ${filePath}`
194
+ );
195
+ return;
196
+ }
197
+
198
+ console.error(`[EVA] Playing: ${filePath}`);
199
+
200
+ try {
201
+ // Play audio SYNCHRONOUSLY (wait for it to finish)
202
+ spawnSync({
203
+ cmd: ["afplay", filePath],
204
+ stdout: "ignore",
205
+ stderr: "ignore",
206
+ });
207
+ } finally {
208
+ // Always release lock
209
+ releaseLock();
210
+ }
211
+ } catch (error) {
212
+ console.error(`[EVA] Error playing sound: ${error}`);
213
+ releaseLock(); // Ensure lock is released on error
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Play the appropriate EVA sound for a hook event
219
+ */
220
+ export async function playHookSound(input: HookInput): Promise<void> {
221
+ const faction = getFaction();
222
+ const soundKey = getSoundKey(input);
223
+ const soundPath = getSoundPath(soundKey, faction);
224
+
225
+ console.error(`[EVA] Faction: ${faction}, Sound key: ${soundKey}`);
226
+
227
+ if (soundPath) {
228
+ await playSound(soundPath);
229
+ }
230
+ }
package/src/sounds.ts ADDED
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Red Alert 2 EVA Sound Mappings
3
+ * Maps hook events to EVA voice lines for both Allied and Soviet factions
4
+ */
5
+
6
+ import type { SoundMapping } from "./types";
7
+
8
+ /**
9
+ * Sound mappings for each hook event
10
+ * Allied files: ceva###.wav
11
+ * Soviet files: csof###.wav (same numbers, different prefix)
12
+ *
13
+ * Hook event names use camelCase as per Cursor docs
14
+ */
15
+ export const SOUND_MAPPINGS: Record<string, SoundMapping> = {
16
+ // ============================================================
17
+ // Session Lifecycle
18
+ // ============================================================
19
+
20
+ sessionStart: {
21
+ // "Establishing battlefield control. Stand by."
22
+ allied: ["ceva016.wav"],
23
+ soviet: ["csof016.wav"],
24
+ },
25
+
26
+ sessionEnd: {
27
+ // "Battle control terminated."
28
+ allied: ["ceva015.wav"],
29
+ soviet: ["csof015.wav"],
30
+ },
31
+
32
+ // ============================================================
33
+ // Tool Operations
34
+ // ============================================================
35
+
36
+ preToolUse: {
37
+ // "Building."
38
+ allied: ["ceva052.wav"],
39
+ soviet: ["csof052.wav"],
40
+ },
41
+
42
+ postToolUse: {
43
+ // "Unit ready."
44
+ allied: ["ceva062.wav"],
45
+ soviet: ["csof062.wav"],
46
+ },
47
+
48
+ postToolUseFailure: {
49
+ // "Unit lost."
50
+ allied: ["ceva064.wav"],
51
+ soviet: ["csof064.wav"],
52
+ },
53
+
54
+ // ============================================================
55
+ // Shell Commands
56
+ // ============================================================
57
+
58
+ beforeShellExecution: {
59
+ // "Building."
60
+ allied: ["ceva052.wav"],
61
+ soviet: ["csof052.wav"],
62
+ },
63
+
64
+ afterShellExecution: {
65
+ // "Unit ready."
66
+ allied: ["ceva062.wav"],
67
+ soviet: ["csof062.wav"],
68
+ },
69
+
70
+ // ============================================================
71
+ // File Operations
72
+ // ============================================================
73
+
74
+ beforeReadFile: {
75
+ // "Training." (training the LLM!)
76
+ allied: ["ceva066.wav"],
77
+ soviet: ["csof066.wav"],
78
+ },
79
+
80
+ afterFileEdit: {
81
+ // "Unit promoted." (file improved!)
82
+ allied: ["ceva079.wav"],
83
+ soviet: ["csof079.wav"],
84
+ },
85
+
86
+ // ============================================================
87
+ // MCP Tools (Special Technology!)
88
+ // ============================================================
89
+
90
+ beforeMCPExecution: {
91
+ // "Upgrade in progress."
92
+ allied: ["ceva084.wav"],
93
+ soviet: ["csof084.wav"],
94
+ },
95
+
96
+ afterMCPExecution: {
97
+ // "New technology acquired."
98
+ allied: ["ceva074.wav"],
99
+ soviet: ["csof074.wav"],
100
+ },
101
+
102
+ // ============================================================
103
+ // Prompts
104
+ // ============================================================
105
+
106
+ beforeSubmitPrompt: {
107
+ // "New mission objective received." / "New construction options."
108
+ allied: ["ceva083.wav", "ceva049.wav"],
109
+ soviet: ["csof083.wav", "csof049.wav"],
110
+ },
111
+
112
+ // ============================================================
113
+ // Subagents (Reinforcements!)
114
+ // ============================================================
115
+
116
+ subagentStart: {
117
+ // Random: "Reinforcements have arrived." / "Reinforcements ready."
118
+ allied: ["ceva038.wav", "ceva121.wav"],
119
+ soviet: ["csof038.wav", "csof121.wav"],
120
+ },
121
+
122
+ subagentStop: {
123
+ // Random: "Primary/Secondary/Tertiary objective achieved."
124
+ allied: ["ceva017.wav", "ceva018.wav", "ceva019.wav"],
125
+ soviet: ["csof017.wav", "csof018.wav", "csof019.wav"],
126
+ },
127
+
128
+ // ============================================================
129
+ // Agent Stop (Status-based)
130
+ // ============================================================
131
+
132
+ "stop:completed": {
133
+ // "Construction complete." / "Primary objective achieved."
134
+ allied: ["ceva048.wav", "ceva017.wav"],
135
+ soviet: ["csof048.wav", "csof017.wav"],
136
+ },
137
+
138
+ "stop:aborted": {
139
+ // "Cancelled."
140
+ allied: ["ceva051.wav"],
141
+ soviet: ["csof051.wav"],
142
+ },
143
+
144
+ "stop:error": {
145
+ // "Cannot deploy here."
146
+ allied: ["ceva063.wav"],
147
+ soviet: ["csof063.wav"],
148
+ },
149
+
150
+ // ============================================================
151
+ // Agent Thoughts
152
+ // ============================================================
153
+
154
+ afterAgentThought: {
155
+ // "Incoming transmission." (agent finished thinking)
156
+ allied: ["ceva040.wav"],
157
+ soviet: ["csof040.wav"],
158
+ },
159
+
160
+ // ============================================================
161
+ // Context Management
162
+ // ============================================================
163
+
164
+ preCompact: {
165
+ // Random: "Low power." / "Base defenses offline." / "Building offline."
166
+ allied: ["ceva053.wav"],
167
+ soviet: ["csof053.wav"],
168
+ },
169
+ };
170
+
171
+ /**
172
+ * Get the sound key for a stop event based on status
173
+ */
174
+ export function getStopSoundKey(status: string): string {
175
+ return `stop:${status}`;
176
+ }
package/src/types.ts ADDED
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Red Alert 2 EVA Cursor Hooks - TypeScript Types
3
+ * Based on Cursor's hook specification
4
+ */
5
+
6
+ // ============================================================
7
+ // Common Input Fields (all hooks receive these)
8
+ // ============================================================
9
+
10
+ export interface CommonHookInput {
11
+ conversation_id: string;
12
+ generation_id: string;
13
+ model: string;
14
+ hook_event_name: string;
15
+ cursor_version: string;
16
+ workspace_roots: string[];
17
+ user_email: string | null;
18
+ transcript_path: string | null;
19
+ }
20
+
21
+ // ============================================================
22
+ // Hook-Specific Input Types
23
+ // ============================================================
24
+
25
+ export interface SessionStartInput extends CommonHookInput {
26
+ hook_event_name: "sessionStart";
27
+ session_id: string;
28
+ is_background_agent: boolean;
29
+ composer_mode?: "agent" | "ask" | "edit";
30
+ }
31
+
32
+ export interface SessionEndInput extends CommonHookInput {
33
+ hook_event_name: "sessionEnd";
34
+ session_id: string;
35
+ reason: "completed" | "aborted" | "error" | "window_close" | "user_close";
36
+ duration_ms: number;
37
+ is_background_agent: boolean;
38
+ final_status: string;
39
+ error_message?: string;
40
+ }
41
+
42
+ export interface PreToolUseInput extends CommonHookInput {
43
+ hook_event_name: "preToolUse";
44
+ tool_name: string;
45
+ tool_input: Record<string, unknown>;
46
+ tool_use_id: string;
47
+ cwd: string;
48
+ agent_message?: string;
49
+ }
50
+
51
+ export interface PostToolUseInput extends CommonHookInput {
52
+ hook_event_name: "postToolUse";
53
+ tool_name: string;
54
+ tool_input: Record<string, unknown>;
55
+ tool_output: string;
56
+ tool_use_id: string;
57
+ cwd: string;
58
+ duration: number;
59
+ }
60
+
61
+ export interface PostToolUseFailureInput extends CommonHookInput {
62
+ hook_event_name: "postToolUseFailure";
63
+ tool_name: string;
64
+ tool_input: Record<string, unknown>;
65
+ tool_use_id: string;
66
+ cwd: string;
67
+ error_message: string;
68
+ failure_type: "timeout" | "error" | "permission_denied";
69
+ duration: number;
70
+ is_interrupt: boolean;
71
+ }
72
+
73
+ export interface BeforeShellExecutionInput extends CommonHookInput {
74
+ hook_event_name: "beforeShellExecution";
75
+ command: string;
76
+ cwd: string;
77
+ timeout: number;
78
+ }
79
+
80
+ export interface AfterShellExecutionInput extends CommonHookInput {
81
+ hook_event_name: "afterShellExecution";
82
+ command: string;
83
+ output: string;
84
+ duration: number;
85
+ }
86
+
87
+ export interface BeforeReadFileInput extends CommonHookInput {
88
+ hook_event_name: "beforeReadFile";
89
+ file_path: string;
90
+ content: string;
91
+ attachments?: Array<{ type: "file" | "rule"; filePath: string }>;
92
+ }
93
+
94
+ export interface AfterFileEditInput extends CommonHookInput {
95
+ hook_event_name: "afterFileEdit";
96
+ file_path: string;
97
+ edits: Array<{ old_string: string; new_string: string }>;
98
+ }
99
+
100
+ export interface BeforeMCPExecutionInput extends CommonHookInput {
101
+ hook_event_name: "beforeMCPExecution";
102
+ tool_name: string;
103
+ tool_input: string;
104
+ url?: string;
105
+ command?: string;
106
+ }
107
+
108
+ export interface AfterMCPExecutionInput extends CommonHookInput {
109
+ hook_event_name: "afterMCPExecution";
110
+ tool_name: string;
111
+ tool_input: string;
112
+ result_json: string;
113
+ duration: number;
114
+ }
115
+
116
+ export interface BeforeSubmitPromptInput extends CommonHookInput {
117
+ hook_event_name: "beforeSubmitPrompt";
118
+ prompt: string;
119
+ attachments?: Array<{ type: "file" | "rule"; filePath: string }>;
120
+ }
121
+
122
+ export interface SubagentStartInput extends CommonHookInput {
123
+ hook_event_name: "subagentStart";
124
+ subagent_type: string;
125
+ prompt: string;
126
+ }
127
+
128
+ export interface SubagentStopInput extends CommonHookInput {
129
+ hook_event_name: "subagentStop";
130
+ subagent_type: string;
131
+ status: "completed" | "error";
132
+ result: string;
133
+ duration: number;
134
+ agent_transcript_path?: string | null;
135
+ }
136
+
137
+ export interface StopInput extends CommonHookInput {
138
+ hook_event_name: "stop";
139
+ status: "completed" | "aborted" | "error";
140
+ loop_count: number;
141
+ }
142
+
143
+ export interface PreCompactInput extends CommonHookInput {
144
+ hook_event_name: "preCompact";
145
+ trigger: "auto" | "manual";
146
+ context_usage_percent: number;
147
+ context_tokens: number;
148
+ context_window_size: number;
149
+ message_count: number;
150
+ messages_to_compact: number;
151
+ is_first_compaction: boolean;
152
+ }
153
+
154
+ // Union of all hook inputs
155
+ export type HookInput =
156
+ | SessionStartInput
157
+ | SessionEndInput
158
+ | PreToolUseInput
159
+ | PostToolUseInput
160
+ | PostToolUseFailureInput
161
+ | BeforeShellExecutionInput
162
+ | AfterShellExecutionInput
163
+ | BeforeReadFileInput
164
+ | AfterFileEditInput
165
+ | BeforeMCPExecutionInput
166
+ | AfterMCPExecutionInput
167
+ | BeforeSubmitPromptInput
168
+ | SubagentStartInput
169
+ | SubagentStopInput
170
+ | StopInput
171
+ | PreCompactInput;
172
+
173
+ // ============================================================
174
+ // Hook Output Types
175
+ // ============================================================
176
+
177
+ export interface SessionStartOutput {
178
+ env?: Record<string, string>;
179
+ additional_context?: string;
180
+ continue?: boolean;
181
+ user_message?: string;
182
+ }
183
+
184
+ export interface PreToolUseOutput {
185
+ decision?: "allow" | "deny";
186
+ reason?: string;
187
+ updated_input?: Record<string, unknown>;
188
+ }
189
+
190
+ export interface PostToolUseOutput {
191
+ updated_mcp_tool_output?: Record<string, unknown>;
192
+ }
193
+
194
+ export interface BeforeShellExecutionOutput {
195
+ permission?: "allow" | "deny" | "ask";
196
+ user_message?: string;
197
+ agent_message?: string;
198
+ }
199
+
200
+ export interface BeforeReadFileOutput {
201
+ permission?: "allow" | "deny";
202
+ user_message?: string;
203
+ }
204
+
205
+ export interface BeforeSubmitPromptOutput {
206
+ continue?: boolean;
207
+ user_message?: string;
208
+ }
209
+
210
+ export interface SubagentStartOutput {
211
+ decision?: "allow" | "deny";
212
+ reason?: string;
213
+ }
214
+
215
+ export interface SubagentStopOutput {
216
+ followup_message?: string;
217
+ }
218
+
219
+ export interface StopOutput {
220
+ followup_message?: string;
221
+ }
222
+
223
+ export interface PreCompactOutput {
224
+ user_message?: string;
225
+ }
226
+
227
+ // Empty output for observation-only hooks
228
+ export interface EmptyOutput {}
229
+
230
+ // ============================================================
231
+ // Sound Types
232
+ // ============================================================
233
+
234
+ export type Faction = "allied" | "soviet";
235
+
236
+ export type HookEventName = HookInput["hook_event_name"];
237
+
238
+ export interface SoundMapping {
239
+ allied: string[];
240
+ soviet: string[];
241
+ }