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.
- package/assets/audio/README.md +74 -0
- package/assets/audio/eva_allied/TRANSCRIPTIONS.txt +131 -0
- package/assets/audio/eva_allied/ceva001.wav +0 -0
- package/assets/audio/eva_allied/ceva002.wav +0 -0
- package/assets/audio/eva_allied/ceva003.wav +0 -0
- package/assets/audio/eva_allied/ceva004.wav +0 -0
- package/assets/audio/eva_allied/ceva005.wav +0 -0
- package/assets/audio/eva_allied/ceva006.wav +0 -0
- package/assets/audio/eva_allied/ceva007.wav +0 -0
- package/assets/audio/eva_allied/ceva008.wav +0 -0
- package/assets/audio/eva_allied/ceva009.wav +0 -0
- package/assets/audio/eva_allied/ceva010.wav +0 -0
- package/assets/audio/eva_allied/ceva011.wav +0 -0
- package/assets/audio/eva_allied/ceva012.wav +0 -0
- package/assets/audio/eva_allied/ceva013.wav +0 -0
- package/assets/audio/eva_allied/ceva014.wav +0 -0
- package/assets/audio/eva_allied/ceva015.wav +0 -0
- package/assets/audio/eva_allied/ceva016.wav +0 -0
- package/assets/audio/eva_allied/ceva017.wav +0 -0
- package/assets/audio/eva_allied/ceva018.wav +0 -0
- package/assets/audio/eva_allied/ceva019.wav +0 -0
- package/assets/audio/eva_allied/ceva020.wav +0 -0
- package/assets/audio/eva_allied/ceva021.wav +0 -0
- package/assets/audio/eva_allied/ceva022.wav +0 -0
- package/assets/audio/eva_allied/ceva023.wav +0 -0
- package/assets/audio/eva_allied/ceva024.wav +0 -0
- package/assets/audio/eva_allied/ceva025.wav +0 -0
- package/assets/audio/eva_allied/ceva026.wav +0 -0
- package/assets/audio/eva_allied/ceva027.wav +0 -0
- package/assets/audio/eva_allied/ceva028.wav +0 -0
- package/assets/audio/eva_allied/ceva029.wav +0 -0
- package/assets/audio/eva_allied/ceva030.wav +0 -0
- package/assets/audio/eva_allied/ceva031.wav +0 -0
- package/assets/audio/eva_allied/ceva032.wav +0 -0
- package/assets/audio/eva_allied/ceva033.wav +0 -0
- package/assets/audio/eva_allied/ceva035.wav +0 -0
- package/assets/audio/eva_allied/ceva036.wav +0 -0
- package/assets/audio/eva_allied/ceva037.wav +0 -0
- package/assets/audio/eva_allied/ceva038.wav +0 -0
- package/assets/audio/eva_allied/ceva039.wav +0 -0
- package/assets/audio/eva_allied/ceva040.wav +0 -0
- package/assets/audio/eva_allied/ceva041.wav +0 -0
- package/assets/audio/eva_allied/ceva042.wav +0 -0
- package/assets/audio/eva_allied/ceva043.wav +0 -0
- package/assets/audio/eva_allied/ceva044.wav +0 -0
- package/assets/audio/eva_allied/ceva045.wav +0 -0
- package/assets/audio/eva_allied/ceva046.wav +0 -0
- package/assets/audio/eva_allied/ceva047.wav +0 -0
- package/assets/audio/eva_allied/ceva048.wav +0 -0
- package/assets/audio/eva_allied/ceva049.wav +0 -0
- package/assets/audio/eva_allied/ceva050.wav +0 -0
- package/assets/audio/eva_allied/ceva051.wav +0 -0
- package/assets/audio/eva_allied/ceva052.wav +0 -0
- package/assets/audio/eva_allied/ceva053.wav +0 -0
- package/assets/audio/eva_allied/ceva054.wav +0 -0
- package/assets/audio/eva_allied/ceva055.wav +0 -0
- package/assets/audio/eva_allied/ceva056.wav +0 -0
- package/assets/audio/eva_allied/ceva057.wav +0 -0
- package/assets/audio/eva_allied/ceva058.wav +0 -0
- package/assets/audio/eva_allied/ceva059.wav +0 -0
- package/assets/audio/eva_allied/ceva060.wav +0 -0
- package/assets/audio/eva_allied/ceva061.wav +0 -0
- package/assets/audio/eva_allied/ceva062.wav +0 -0
- package/assets/audio/eva_allied/ceva063.wav +0 -0
- package/assets/audio/eva_allied/ceva064.wav +0 -0
- package/assets/audio/eva_allied/ceva065.wav +0 -0
- package/assets/audio/eva_allied/ceva066.wav +0 -0
- package/assets/audio/eva_allied/ceva067.wav +0 -0
- package/assets/audio/eva_allied/ceva068.wav +0 -0
- package/assets/audio/eva_allied/ceva069.wav +0 -0
- package/assets/audio/eva_allied/ceva070.wav +0 -0
- package/assets/audio/eva_allied/ceva071.wav +0 -0
- package/assets/audio/eva_allied/ceva072.wav +0 -0
- package/assets/audio/eva_allied/ceva073.wav +0 -0
- package/assets/audio/eva_allied/ceva074.wav +0 -0
- package/assets/audio/eva_allied/ceva075.wav +0 -0
- package/assets/audio/eva_allied/ceva076.wav +0 -0
- package/assets/audio/eva_allied/ceva077.wav +0 -0
- package/assets/audio/eva_allied/ceva078.wav +0 -0
- package/assets/audio/eva_allied/ceva079.wav +0 -0
- package/assets/audio/eva_allied/ceva080.wav +0 -0
- package/assets/audio/eva_allied/ceva081.wav +0 -0
- package/assets/audio/eva_allied/ceva082.wav +0 -0
- package/assets/audio/eva_allied/ceva083.wav +0 -0
- package/assets/audio/eva_allied/ceva084.wav +0 -0
- package/assets/audio/eva_allied/ceva085.wav +0 -0
- package/assets/audio/eva_allied/ceva086.wav +0 -0
- package/assets/audio/eva_allied/ceva087.wav +0 -0
- package/assets/audio/eva_allied/ceva088.wav +0 -0
- package/assets/audio/eva_allied/ceva089.wav +0 -0
- package/assets/audio/eva_allied/ceva090.wav +0 -0
- package/assets/audio/eva_allied/ceva091.wav +0 -0
- package/assets/audio/eva_allied/ceva092.wav +0 -0
- package/assets/audio/eva_allied/ceva093.wav +0 -0
- package/assets/audio/eva_allied/ceva094.wav +0 -0
- package/assets/audio/eva_allied/ceva095.wav +0 -0
- package/assets/audio/eva_allied/ceva096.wav +0 -0
- package/assets/audio/eva_allied/ceva097.wav +0 -0
- package/assets/audio/eva_allied/ceva098.wav +0 -0
- package/assets/audio/eva_allied/ceva099.wav +0 -0
- package/assets/audio/eva_allied/ceva100.wav +0 -0
- package/assets/audio/eva_allied/ceva101.wav +0 -0
- package/assets/audio/eva_allied/ceva102.wav +0 -0
- package/assets/audio/eva_allied/ceva103.wav +0 -0
- package/assets/audio/eva_allied/ceva104.wav +0 -0
- package/assets/audio/eva_allied/ceva105.wav +0 -0
- package/assets/audio/eva_allied/ceva106.wav +0 -0
- package/assets/audio/eva_allied/ceva107.wav +0 -0
- package/assets/audio/eva_allied/ceva108.wav +0 -0
- package/assets/audio/eva_allied/ceva109.wav +0 -0
- package/assets/audio/eva_allied/ceva120.wav +0 -0
- package/assets/audio/eva_allied/ceva121.wav +0 -0
- package/assets/audio/eva_allied/ceva122.wav +0 -0
- package/assets/audio/eva_allied/cevau06.wav +0 -0
- package/assets/audio/eva_allied/cevau07.wav +0 -0
- package/assets/audio/eva_allied/cevau08.wav +0 -0
- package/assets/audio/eva_allied/cevau13.wav +0 -0
- package/assets/audio/eva_allied/cevau15.wav +0 -0
- package/assets/audio/eva_allied/cevau19.wav +0 -0
- package/assets/audio/eva_allied/cevau20.wav +0 -0
- package/assets/audio/eva_allied/cevau22.wav +0 -0
- package/assets/audio/eva_allied/cevau23.wav +0 -0
- package/assets/audio/eva_allied/cevau24.wav +0 -0
- package/assets/audio/eva_allied/cevau25.wav +0 -0
- package/assets/audio/eva_allied/cevau26.wav +0 -0
- package/assets/audio/eva_allied/cevau27.wav +0 -0
- package/assets/audio/eva_allied/cevau31.wav +0 -0
- package/assets/audio/eva_allied/cevau36.wav +0 -0
- package/assets/audio/eva_allied/cevau37.wav +0 -0
- package/assets/audio/eva_allied/cevau38.wav +0 -0
- package/assets/audio/eva_allied/transcriptions.json +130 -0
- package/assets/audio/eva_soviet/TRANSCRIPTIONS.txt +133 -0
- package/assets/audio/eva_soviet/csof001.wav +0 -0
- package/assets/audio/eva_soviet/csof002.wav +0 -0
- package/assets/audio/eva_soviet/csof003.wav +0 -0
- package/assets/audio/eva_soviet/csof004.wav +0 -0
- package/assets/audio/eva_soviet/csof005.wav +0 -0
- package/assets/audio/eva_soviet/csof006.wav +0 -0
- package/assets/audio/eva_soviet/csof007.wav +0 -0
- package/assets/audio/eva_soviet/csof008.wav +0 -0
- package/assets/audio/eva_soviet/csof009.wav +0 -0
- package/assets/audio/eva_soviet/csof010.wav +0 -0
- package/assets/audio/eva_soviet/csof011.wav +0 -0
- package/assets/audio/eva_soviet/csof012.wav +0 -0
- package/assets/audio/eva_soviet/csof013.wav +0 -0
- package/assets/audio/eva_soviet/csof014.wav +0 -0
- package/assets/audio/eva_soviet/csof015.wav +0 -0
- package/assets/audio/eva_soviet/csof016.wav +0 -0
- package/assets/audio/eva_soviet/csof017.wav +0 -0
- package/assets/audio/eva_soviet/csof018.wav +0 -0
- package/assets/audio/eva_soviet/csof019.wav +0 -0
- package/assets/audio/eva_soviet/csof020.wav +0 -0
- package/assets/audio/eva_soviet/csof021.wav +0 -0
- package/assets/audio/eva_soviet/csof022.wav +0 -0
- package/assets/audio/eva_soviet/csof023.wav +0 -0
- package/assets/audio/eva_soviet/csof024.wav +0 -0
- package/assets/audio/eva_soviet/csof025.wav +0 -0
- package/assets/audio/eva_soviet/csof026.wav +0 -0
- package/assets/audio/eva_soviet/csof027.wav +0 -0
- package/assets/audio/eva_soviet/csof028.wav +0 -0
- package/assets/audio/eva_soviet/csof029.wav +0 -0
- package/assets/audio/eva_soviet/csof030.wav +0 -0
- package/assets/audio/eva_soviet/csof031.wav +0 -0
- package/assets/audio/eva_soviet/csof032.wav +0 -0
- package/assets/audio/eva_soviet/csof033.wav +0 -0
- package/assets/audio/eva_soviet/csof035.wav +0 -0
- package/assets/audio/eva_soviet/csof036.wav +0 -0
- package/assets/audio/eva_soviet/csof037.wav +0 -0
- package/assets/audio/eva_soviet/csof038.wav +0 -0
- package/assets/audio/eva_soviet/csof039.wav +0 -0
- package/assets/audio/eva_soviet/csof040.wav +0 -0
- package/assets/audio/eva_soviet/csof041.wav +0 -0
- package/assets/audio/eva_soviet/csof042.wav +0 -0
- package/assets/audio/eva_soviet/csof043.wav +0 -0
- package/assets/audio/eva_soviet/csof044.wav +0 -0
- package/assets/audio/eva_soviet/csof045.wav +0 -0
- package/assets/audio/eva_soviet/csof046.wav +0 -0
- package/assets/audio/eva_soviet/csof047.wav +0 -0
- package/assets/audio/eva_soviet/csof048.wav +0 -0
- package/assets/audio/eva_soviet/csof049.wav +0 -0
- package/assets/audio/eva_soviet/csof050.wav +0 -0
- package/assets/audio/eva_soviet/csof051.wav +0 -0
- package/assets/audio/eva_soviet/csof052.wav +0 -0
- package/assets/audio/eva_soviet/csof053.wav +0 -0
- package/assets/audio/eva_soviet/csof054.wav +0 -0
- package/assets/audio/eva_soviet/csof055.wav +0 -0
- package/assets/audio/eva_soviet/csof056.wav +0 -0
- package/assets/audio/eva_soviet/csof057.wav +0 -0
- package/assets/audio/eva_soviet/csof058.wav +0 -0
- package/assets/audio/eva_soviet/csof059.wav +0 -0
- package/assets/audio/eva_soviet/csof060.wav +0 -0
- package/assets/audio/eva_soviet/csof061.wav +0 -0
- package/assets/audio/eva_soviet/csof062.wav +0 -0
- package/assets/audio/eva_soviet/csof063.wav +0 -0
- package/assets/audio/eva_soviet/csof064.wav +0 -0
- package/assets/audio/eva_soviet/csof065.wav +0 -0
- package/assets/audio/eva_soviet/csof066.wav +0 -0
- package/assets/audio/eva_soviet/csof067.wav +0 -0
- package/assets/audio/eva_soviet/csof068.wav +0 -0
- package/assets/audio/eva_soviet/csof069.wav +0 -0
- package/assets/audio/eva_soviet/csof070.wav +0 -0
- package/assets/audio/eva_soviet/csof071.wav +0 -0
- package/assets/audio/eva_soviet/csof072.wav +0 -0
- package/assets/audio/eva_soviet/csof073.wav +0 -0
- package/assets/audio/eva_soviet/csof074.wav +0 -0
- package/assets/audio/eva_soviet/csof075.wav +0 -0
- package/assets/audio/eva_soviet/csof076.wav +0 -0
- package/assets/audio/eva_soviet/csof077.wav +0 -0
- package/assets/audio/eva_soviet/csof078.wav +0 -0
- package/assets/audio/eva_soviet/csof079.wav +0 -0
- package/assets/audio/eva_soviet/csof080.wav +0 -0
- package/assets/audio/eva_soviet/csof081.wav +0 -0
- package/assets/audio/eva_soviet/csof082.wav +0 -0
- package/assets/audio/eva_soviet/csof083.wav +0 -0
- package/assets/audio/eva_soviet/csof084.wav +0 -0
- package/assets/audio/eva_soviet/csof085.wav +0 -0
- package/assets/audio/eva_soviet/csof086.wav +0 -0
- package/assets/audio/eva_soviet/csof087.wav +0 -0
- package/assets/audio/eva_soviet/csof088.wav +0 -0
- package/assets/audio/eva_soviet/csof089.wav +0 -0
- package/assets/audio/eva_soviet/csof090.wav +0 -0
- package/assets/audio/eva_soviet/csof091.wav +0 -0
- package/assets/audio/eva_soviet/csof092.wav +0 -0
- package/assets/audio/eva_soviet/csof093.wav +0 -0
- package/assets/audio/eva_soviet/csof094.wav +0 -0
- package/assets/audio/eva_soviet/csof095.wav +0 -0
- package/assets/audio/eva_soviet/csof096.wav +0 -0
- package/assets/audio/eva_soviet/csof097.wav +0 -0
- package/assets/audio/eva_soviet/csof098.wav +0 -0
- package/assets/audio/eva_soviet/csof099.wav +0 -0
- package/assets/audio/eva_soviet/csof100.wav +0 -0
- package/assets/audio/eva_soviet/csof101.wav +0 -0
- package/assets/audio/eva_soviet/csof102.wav +0 -0
- package/assets/audio/eva_soviet/csof103.wav +0 -0
- package/assets/audio/eva_soviet/csof104.wav +0 -0
- package/assets/audio/eva_soviet/csof105.wav +0 -0
- package/assets/audio/eva_soviet/csof106.wav +0 -0
- package/assets/audio/eva_soviet/csof107.wav +0 -0
- package/assets/audio/eva_soviet/csof108.wav +0 -0
- package/assets/audio/eva_soviet/csof109.wav +0 -0
- package/assets/audio/eva_soviet/csof120.wav +0 -0
- package/assets/audio/eva_soviet/csof121.wav +0 -0
- package/assets/audio/eva_soviet/csof122.wav +0 -0
- package/assets/audio/eva_soviet/csofu04.wav +0 -0
- package/assets/audio/eva_soviet/csofu06.wav +0 -0
- package/assets/audio/eva_soviet/csofu07.wav +0 -0
- package/assets/audio/eva_soviet/csofu08.wav +0 -0
- package/assets/audio/eva_soviet/csofu09.wav +0 -0
- package/assets/audio/eva_soviet/csofu10.wav +0 -0
- package/assets/audio/eva_soviet/csofu11.wav +0 -0
- package/assets/audio/eva_soviet/csofu15.wav +0 -0
- package/assets/audio/eva_soviet/csofu16.wav +0 -0
- package/assets/audio/eva_soviet/csofu18.wav +0 -0
- package/assets/audio/eva_soviet/csofu19.wav +0 -0
- package/assets/audio/eva_soviet/csofu20.wav +0 -0
- package/assets/audio/eva_soviet/csofu21.wav +0 -0
- package/assets/audio/eva_soviet/csofu25.wav +0 -0
- package/assets/audio/eva_soviet/csofu26.wav +0 -0
- package/assets/audio/eva_soviet/csofu27.wav +0 -0
- package/assets/audio/eva_soviet/csofu30.wav +0 -0
- package/assets/audio/eva_soviet/csofu31.wav +0 -0
- package/assets/audio/eva_soviet/csofu33.wav +0 -0
- package/assets/audio/eva_soviet/transcriptions.json +132 -0
- package/bin/install.js +249 -0
- package/package.json +33 -0
- package/src/index.ts +122 -0
- package/src/player.ts +230 -0
- package/src/sounds.ts +176 -0
- 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
|
+
}
|