diff --git a/openpeon/files/config.json b/openpeon/files/config.json new file mode 100644 index 0000000..238af64 --- /dev/null +++ b/openpeon/files/config.json @@ -0,0 +1,13 @@ +{ + "activePack": "solid_snake", + "volume": 0.5, + "muted": false, + "enabledCategories": { + "session.start": true, + "task.acknowledge": true, + "task.complete": true, + "task.error": true, + "input.required": true, + "resource.limit": true + } +} \ No newline at end of file diff --git a/openpeon/files/packs/solid_snake/LICENSE b/openpeon/files/packs/solid_snake/LICENSE new file mode 100644 index 0000000..4b4e7dc --- /dev/null +++ b/openpeon/files/packs/solid_snake/LICENSE @@ -0,0 +1,41 @@ +Creative Commons Attribution-NonCommercial 4.0 International (CC BY-NC 4.0) + +This work is licensed under the Creative Commons Attribution-NonCommercial 4.0 International License. + +To view a copy of this license, visit: +https://creativecommons.org/licenses/by-nc/4.0/ + +-------------------------------------------------------------------------------- + +ATTRIBUTION + +Solid Snake Sound Pack +Created by: will +GitHub: https://github.com/will/openpeon-solid-snake + +Source Audio: Metal Gear Solid series voice clips +Original Voice Actor: David Hayter +Copyright Holder: Konami Digital Entertainment + +These audio clips are used under fair use for personal notification purposes only. +This sound pack is not endorsed by or affiliated with Konami. + +-------------------------------------------------------------------------------- + +LICENSE SUMMARY + +You are free to: + - Share: copy and redistribute the material in any medium or format + - Adapt: remix, transform, and build upon the material + +Under the following terms: + - Attribution: You must give appropriate credit, provide a link to the license, + and indicate if changes were made. + - NonCommercial: You may not use the material for commercial purposes. + - No additional restrictions: You may not apply legal terms or technological + measures that legally restrict others from doing anything the license permits. + +-------------------------------------------------------------------------------- + +The full legal text of the license is available at: +https://creativecommons.org/licenses/by-nc/4.0/legalcode diff --git a/openpeon/files/packs/solid_snake/README.md b/openpeon/files/packs/solid_snake/README.md new file mode 100644 index 0000000..a3e2721 --- /dev/null +++ b/openpeon/files/packs/solid_snake/README.md @@ -0,0 +1,79 @@ +# Solid Snake Sound Pack + +**Tactical espionage audio for your coding sessions.** + +A [peon-ping](https://github.com/PeonPing/peon-ping) sound pack featuring the legendary Solid Snake from the Metal Gear Solid series. Get iconic voice lines when your AI coding agent needs attention. + +## Installation + +### Quick Install + +```bash +# Install peon-ping if you haven't already +curl -fsSL https://raw.githubusercontent.com/PeonPing/peon-ping/main/install.sh | bash + +# Clone this pack +git clone https://github.com/will/openpeon-solid-snake.git +cd openpeon-solid-snake + +# Copy to your peon-ping packs directory +cp -r . ~/.claude/hooks/peon-ping/packs/solid_snake/ + +# Activate the pack +peon packs use solid_snake +``` + +### Via Registry (Coming Soon) + +Once this pack is registered in the [OpenPeon registry](https://github.com/PeonPing/registry), you'll be able to install it directly: + +```bash +peon packs install solid_snake +peon packs use solid_snake +``` + +## What You'll Hear + +| Event | Example Quotes | +|---|---| +| **Session starts** | *"Kept you waiting, huh?"*, *"This is Snake"*, *"It's Snake"* | +| **Task acknowledged** | *"Roger that"*, *"Sounds like a plan"*, *"Got it"* | +| **Task complete** | *"Negative, finished"*, *"Watch your friendly fire"*, *"OK"* | +| **Task error** | *"What the hell?"*, *"Damn!"*, *[Snake scream]* | +| **Input required** | *"We've got a job to do"*, *"I need your help"*, *"What am I supposed to do?"* | +| **Resource limit** | *"No"*, *"I can't move"*, *"Running out of time"* | +| **User spam** | *"Give me a break"*, *"You'll pay for that"* | + +All 32 voice clips have been volume-normalized for consistent playback. + +## Pack Details + +- **CESP Version**: 1.0 +- **Total Sounds**: 32 MP3 files +- **Language**: English +- **License**: CC-BY-NC-4.0 (for personal/educational use) +- **Audio Sources**: Various Metal Gear Solid games + +## Audio Quality + +All sounds have been: +- Cleaned and trimmed for quick playback +- Volume normalized using hybrid loudness+peak normalization (-16 LUFS target) +- Encoded at 192 kbps MP3 for quality and small file size +- SHA256 hashed for integrity verification + +## Contributing + +Found a great Snake quote that's missing? Have suggestions for better sounds? Open an issue or submit a PR! + +## Credits + +Voice clips are property of Konami and are used under fair use for personal notification purposes. Original voice actor: David Hayter (MGS 1-4, Peace Walker, Ground Zeroes). + +Pack created by [@will](https://github.com/will) for the [OpenPeon](https://openpeon.com) sound pack ecosystem. + +## Links + +- [peon-ping](https://github.com/PeonPing/peon-ping) - Main CLI tool +- [OpenPeon](https://openpeon.com) - CESP spec and pack browser +- [Create your own pack](https://openpeon.com/create) - Pack creation guide diff --git a/openpeon/files/packs/solid_snake/openpeon.json b/openpeon/files/packs/solid_snake/openpeon.json new file mode 100644 index 0000000..50b6b6c --- /dev/null +++ b/openpeon/files/packs/solid_snake/openpeon.json @@ -0,0 +1,208 @@ +{ + "cesp_version": "1.0", + "name": "solid_snake", + "display_name": "Solid Snake", + "version": "1.0.0", + "description": "Tactical espionage audio - featuring the legendary Solid Snake from the Metal Gear Solid series", + "author": { + "name": "will", + "github": "wsturgiss" + }, + "license": "CC-BY-NC-4.0", + "language": "en", + "categories": { + "session.start": { + "sounds": [ + { + "file": "sounds/kept_you_waiting_mgs2.mp3", + "label": "Kept you waiting, huh?", + "sha256": "8f252b6401a60d9679bd7f90005f590f286c47a3b8397cdc4ed8a2bf4ec7484c" + }, + { + "file": "sounds/snake_thisissnake.mp3", + "label": "This is Snake", + "sha256": "05ac36cb15fb92051c435746fa9f0cf0b8eb3cdd891f3298daece2b1539f1d99" + }, + { + "file": "sounds/snake_itssnake.mp3", + "label": "It's Snake", + "sha256": "ae6b47684afd88a9c23c6b92cfbddafa614fd0613dc36ddab489afbd9789e356" + } + ] + }, + "task.acknowledge": { + "sounds": [ + { + "file": "sounds/snake_roger_that.mp3", + "label": "Roger that", + "sha256": "289020f79fd09ac0e83290de54043bef591405264cc5e3bf20cd345c81f3934e" + }, + { + "file": "sounds/snake_sounds_like_a_plan.mp3", + "label": "Sounds like a plan", + "sha256": "f707a8844ea5e821f708267a6e3e702056baf3f818c9b72858daef24329cbd82" + }, + { + "file": "sounds/snake_hmm.mp3", + "label": "Hmm", + "sha256": "6b2af2cc56973ff0ad5a7d6e417dd0f2a54ec5b8720dbd5d48c33acdb9982112" + }, + { + "file": "sounds/snake_gotit.mp3", + "label": "Got it", + "sha256": "d019ef1ae555e2bacbf7429b21d5046f0a726d6d5baa3418d468569e19594b9a" + }, + { + "file": "sounds/snake_yes.mp3", + "label": "Yes", + "sha256": "748c4784451890636ffd130a00d89c4c17c79e03b0a03cc2495123e80fbd5856" + } + ] + }, + "task.complete": { + "sounds": [ + { + "file": "sounds/kept_you_waiting_mgs2.mp3", + "label": "Kept you waiting, huh?", + "sha256": "8f252b6401a60d9679bd7f90005f590f286c47a3b8397cdc4ed8a2bf4ec7484c" + }, + { + "file": "sounds/snake_negfinished.mp3", + "label": "Negative, finished", + "sha256": "8350fcdbf50d2d7c5ef1b63609ce453cd4900442e57e410118869a2763ecd6a3" + }, + { + "file": "sounds/snake_friendly_fire_trimmed.mp3", + "label": "Watch your friendly fire", + "sha256": "ba396e445ef2d950b443280110d075997920941136c933518f0ebb9fa7741e2d" + }, + { + "file": "sounds/snake_ok.mp3", + "label": "OK", + "sha256": "4eada7b92f83b6a91abac4419e4c50139714e71cf56a27a21951a0b258f94007" + }, + { + "file": "sounds/snake_thatsok.mp3", + "label": "That's OK", + "sha256": "2481194aeb6cd9b1edb15eb72e851894e327d624a38634f311b6d47f8e3cc5ef" + }, + { + "file": "sounds/snake_great.mp3", + "label": "Great", + "sha256": "3bc7a004e107978de90f7a2f548456a5e020b7be81ad3d95adf5d47b8c3cf5b3" + } + ] + }, + "task.error": { + "sounds": [ + { + "file": "sounds/snake_scream.mp3", + "label": "Snake scream", + "sha256": "763a97f888c43ad12f0608f8c519872e57ef3a76b89bd770ea4d1bf049c339d2" + }, + { + "file": "sounds/snake_what_the_hell.mp3", + "label": "What the hell?", + "sha256": "61f5150aebf09acbda8b737c71156ebf3bc57a79afa8b8d6c19e296981391f81" + }, + { + "file": "sounds/snake_damn.mp3", + "label": "Damn", + "sha256": "5254731d1d80dc3613006c8cf1185350e6c4bcf83adb567118866309fae90603" + }, + { + "file": "sounds/snake_wth.mp3", + "label": "What the hell", + "sha256": "bb36264cf9f76341a768dcf8d3d8b546f5709b110e02f3f69f955e9725839881" + } + ] + }, + "input.required": { + "sounds": [ + { + "file": "sounds/snake_job_to_do_trimmed.mp3", + "label": "We've got a job to do", + "sha256": "a38cd5a86d18a2e72a8a692996984406cae9c717ce33180758d7c4200d851b32" + }, + { + "file": "sounds/snake_whereareu_clean.mp3", + "label": "Where are you?", + "sha256": "b7d45a536f5468ee8cf42b291c0fd42968ea15429124a456889daa732eb6a5fd" + }, + { + "file": "sounds/snake_supposedtodo.mp3", + "label": "What am I supposed to do?", + "sha256": "46e3ff90dc018f55d2cd344308135b5d97541c9238543a16dbdb25ab799e0db0" + }, + { + "file": "sounds/snake_favor.mp3", + "label": "I need a favor", + "sha256": "57a97aaeb6684a2ee541fd4f0217cc8da10cec129167b24f4a3b98e7c5718828" + }, + { + "file": "sounds/snake_needhelp_clean.mp3", + "label": "I need your help", + "sha256": "f9b7ea978f78f8b91bdfe5b5fd5226f304dfb74c7fe72f9d96137f0cd6f204b7" + }, + { + "file": "sounds/snake_ulistening.mp3", + "label": "Are you listening?", + "sha256": "0b88cdaef78215b48b0889995df253426faecf013940bcffd46c6b70f242e7f6" + }, + { + "file": "sounds/snake_what.mp3", + "label": "What do you mean?", + "sha256": "6f11527a68b63ad20857136f164f62452e015974db060f809559779804f68ae4" + } + ] + }, + "resource.limit": { + "sounds": [ + { + "file": "sounds/snake_no.mp3", + "label": "No", + "sha256": "d6feddf707e6c637dab61941edb0e6beb998dff4e94f27f540c0e5415b5cc2e6" + }, + { + "file": "sounds/snake_noway.mp3", + "label": "No way", + "sha256": "019aa8bb1a743c4d6ed0baeb9942b618168930c9fd6d2da8150a29bc6e6428b2" + }, + { + "file": "sounds/snake_cantmove.mp3", + "label": "I can't move", + "sha256": "962d8624c33fa9e014be96b2096f4f22bc330a677a7b3fbdf6a0d0f118e8a000" + }, + { + "file": "sounds/snake_outtatime.mp3", + "label": "Running out of time", + "sha256": "205e091b160eac359719e0795bee563043101b61de3b8c5d40fef2d7e18dbd0a" + } + ] + }, + "user.spam": { + "sounds": [ + { + "file": "sounds/snake_gimmebreak.mp3", + "label": "Give me a break", + "sha256": "377dd8818f9e3cd9b7bf90bb3e47e3b2e4fc5750a2fcc90866551defd22968d2" + }, + { + "file": "sounds/snake_changedcol.mp3", + "label": "You changed the colonel", + "sha256": "608b10064f6bfd0737519f1626d6551be5e2c1197ec176e2f6e772b4880ecad6" + }, + { + "file": "sounds/snake_utoldme.mp3", + "label": "You told me", + "sha256": "e80ec43dd4def334b2c1ea282e230d8ea363b8bab2c11fe2e0650dfe3e2c3ec2" + }, + { + "file": "sounds/snake_youllpay_clean.mp3", + "label": "You'll pay for that", + "sha256": "5276062c773c6a63fdd9e79c0312b258ae2973c12cb2a80b0bec491f78889b70" + } + ] + } + } +} diff --git a/openpeon/files/packs/solid_snake/registry-entry.json b/openpeon/files/packs/solid_snake/registry-entry.json new file mode 100644 index 0000000..999f8b4 --- /dev/null +++ b/openpeon/files/packs/solid_snake/registry-entry.json @@ -0,0 +1,51 @@ +{ + "name": "solid_snake", + "display_name": "Solid Snake", + "version": "1.0.0", + "description": "Tactical espionage audio - featuring the legendary Solid Snake from the Metal Gear Solid series", + "author": { + "name": "will", + "github": "wsturgiss" + }, + "trust_tier": "community", + "categories": [ + "session.start", + "task.acknowledge", + "task.complete", + "task.error", + "input.required", + "resource.limit", + "user.spam" + ], + "language": "en", + "license": "CC-BY-NC-4.0", + "sound_count": 32, + "total_size_bytes": 1067760, + "source_repo": "wsturgiss/openpeon-solid-snake", + "source_ref": "v1.0.0", + "source_path": "", + "manifest_sha256": "252aaea9da0105be637776938a226d77096a56533d3a2b6fa07d5c6c9686ef7d", + "tags": [ + "gaming", + "metal-gear-solid", + "stealth", + "military", + "tactical" + ], + "preview_sounds": [ + { + "file": "sounds/kept_you_waiting_mgs2.mp3", + "label": "Kept you waiting, huh?" + }, + { + "file": "sounds/snake_roger_that.mp3", + "label": "Roger that" + }, + { + "file": "sounds/snake_what_the_hell.mp3", + "label": "What the hell?" + } + ], + "added": "2026-02-13", + "updated": "2026-02-13" +} diff --git a/openpeon/files/packs/solid_snake/sounds/kept_you_waiting_mgs2.mp3 b/openpeon/files/packs/solid_snake/sounds/kept_you_waiting_mgs2.mp3 new file mode 100644 index 0000000..d56e4f1 Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/kept_you_waiting_mgs2.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_cantmove.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_cantmove.mp3 new file mode 100644 index 0000000..5735fd6 Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_cantmove.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_changedcol.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_changedcol.mp3 new file mode 100644 index 0000000..474b228 Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_changedcol.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_damn.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_damn.mp3 new file mode 100644 index 0000000..e68c53a Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_damn.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_favor.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_favor.mp3 new file mode 100644 index 0000000..c36a8b8 Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_favor.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_friendly_fire_trimmed.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_friendly_fire_trimmed.mp3 new file mode 100644 index 0000000..96e1147 Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_friendly_fire_trimmed.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_gimmebreak.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_gimmebreak.mp3 new file mode 100644 index 0000000..f468229 Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_gimmebreak.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_gotit.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_gotit.mp3 new file mode 100644 index 0000000..e97dd64 Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_gotit.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_great.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_great.mp3 new file mode 100644 index 0000000..f262532 Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_great.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_hmm.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_hmm.mp3 new file mode 100644 index 0000000..16ed945 Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_hmm.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_itssnake.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_itssnake.mp3 new file mode 100644 index 0000000..e57a68a Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_itssnake.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_job_to_do_trimmed.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_job_to_do_trimmed.mp3 new file mode 100644 index 0000000..a086bac Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_job_to_do_trimmed.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_needhelp_clean.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_needhelp_clean.mp3 new file mode 100644 index 0000000..45824dd Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_needhelp_clean.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_negfinished.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_negfinished.mp3 new file mode 100644 index 0000000..fa2295c Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_negfinished.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_no.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_no.mp3 new file mode 100644 index 0000000..5e36fca Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_no.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_noway.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_noway.mp3 new file mode 100644 index 0000000..e57c9e3 Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_noway.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_ok.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_ok.mp3 new file mode 100644 index 0000000..da4633b Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_ok.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_outtatime.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_outtatime.mp3 new file mode 100644 index 0000000..1586b13 Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_outtatime.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_roger_that.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_roger_that.mp3 new file mode 100644 index 0000000..45ba8da Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_roger_that.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_scream.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_scream.mp3 new file mode 100644 index 0000000..a1e14fb Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_scream.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_sounds_like_a_plan.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_sounds_like_a_plan.mp3 new file mode 100644 index 0000000..f00e938 Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_sounds_like_a_plan.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_supposedtodo.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_supposedtodo.mp3 new file mode 100644 index 0000000..15dbc3a Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_supposedtodo.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_thatsok.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_thatsok.mp3 new file mode 100644 index 0000000..93d46a2 Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_thatsok.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_thisissnake.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_thisissnake.mp3 new file mode 100644 index 0000000..0f6aee5 Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_thisissnake.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_ulistening.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_ulistening.mp3 new file mode 100644 index 0000000..a71bffd Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_ulistening.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_utoldme.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_utoldme.mp3 new file mode 100644 index 0000000..91ca5df Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_utoldme.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_what.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_what.mp3 new file mode 100644 index 0000000..8b58894 Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_what.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_what_the_hell.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_what_the_hell.mp3 new file mode 100644 index 0000000..def681a Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_what_the_hell.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_whereareu_clean.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_whereareu_clean.mp3 new file mode 100644 index 0000000..bc8dc01 Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_whereareu_clean.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_wth.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_wth.mp3 new file mode 100644 index 0000000..89a51a6 Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_wth.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_yes.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_yes.mp3 new file mode 100644 index 0000000..81565d2 Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_yes.mp3 differ diff --git a/openpeon/files/packs/solid_snake/sounds/snake_youllpay_clean.mp3 b/openpeon/files/packs/solid_snake/sounds/snake_youllpay_clean.mp3 new file mode 100644 index 0000000..76e2897 Binary files /dev/null and b/openpeon/files/packs/solid_snake/sounds/snake_youllpay_clean.mp3 differ diff --git a/pi/files/agent/extensions/confirm-destructive.ts b/pi/files/agent/extensions/confirm-destructive.ts index caadf13..b61a3f8 100644 --- a/pi/files/agent/extensions/confirm-destructive.ts +++ b/pi/files/agent/extensions/confirm-destructive.ts @@ -6,14 +6,14 @@ */ import type { ExtensionAPI, SessionBeforeSwitchEvent, SessionMessageEntry } from "@mariozechner/pi-coding-agent"; -import { sendNotification } from "./notify.js"; export default function (pi: ExtensionAPI) { pi.on("session_before_switch", async (event: SessionBeforeSwitchEvent, ctx) => { - if (!ctx.hasUI) return; - if (event.reason === "new") { - sendNotification("Clear session confirmation"); + // Emit event for sound extensions (before hasUI check) + pi.events.emit("peon:input_required", { source: "confirm-destructive", action: "clear-session" }); + if (!ctx.hasUI) return; + const confirmed = await ctx.ui.confirm( "Clear session?", "This will delete all messages in the current session.", @@ -33,7 +33,10 @@ export default function (pi: ExtensionAPI) { ); if (hasUnsavedWork) { - sendNotification("Switch session confirmation"); + // Emit event for sound extensions (before hasUI check) + pi.events.emit("peon:input_required", { source: "confirm-destructive", action: "switch-session" }); + if (!ctx.hasUI) return; + const confirmed = await ctx.ui.confirm( "Switch session?", "You have messages in the current session. Switch anyway?", @@ -47,9 +50,10 @@ export default function (pi: ExtensionAPI) { }); pi.on("session_before_fork", async (event, ctx) => { + // Emit event for sound extensions (before hasUI check) + pi.events.emit("peon:input_required", { source: "confirm-destructive", action: "fork-session" }); if (!ctx.hasUI) return; - sendNotification("Fork session confirmation"); const choice = await ctx.ui.select(`Fork from entry ${event.entryId.slice(0, 8)}?`, [ "Yes, create fork", "No, stay in current session", diff --git a/pi/files/agent/extensions/notify.ts b/pi/files/agent/extensions/notify.ts.disabled similarity index 100% rename from pi/files/agent/extensions/notify.ts rename to pi/files/agent/extensions/notify.ts.disabled diff --git a/pi/files/agent/extensions/peon.ts b/pi/files/agent/extensions/peon.ts new file mode 100644 index 0000000..410c8a7 --- /dev/null +++ b/pi/files/agent/extensions/peon.ts @@ -0,0 +1,511 @@ +/** + * Peon Extension - CESP (Coding Event Sound Pack) Integration for Pi + * + * Announces Pi events with customizable sound packs following the CESP standard. + * Default pack: Warcraft Orc Peon + * + * Usage: + * /peon status - Show current pack and settings + * /peon list - List installed packs + * /peon set - Switch to a different pack + * /peon volume <0-100> - Set master volume + * /peon mute - Toggle global mute + * /peon test - Test a sound category + * + * Categories: session.start, task.acknowledge, task.complete, task.error, + * input.required, resource.limit + */ + +import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import { exec, execFile } from "node:child_process"; +import { promisify } from "node:util"; + +const execAsync = promisify(exec); + +// ============ CONFIGURATION ============ +const PACKS_DIR = path.join(process.env.HOME || "~", ".config/openpeon/packs"); +const CONFIG_FILE = path.join(process.env.HOME || "~", ".config/openpeon/config.json"); +const DEFAULT_PACK = "peon"; +const DEBOUNCE_MS = 500; +const REMOTE_HOST = "linux-pc"; // SSH host for remote notifications +// ======================================= + +// ============ SSH DETECTION ============ +function isSSH(): boolean { + if ( + process.env.SSH_CONNECTION || + process.env.SSH_CLIENT || + process.env.SSH_TTY + ) { + return true; + } + // Check for sshd-session process (works in tmux/zellij) + try { + const { execSync } = require("child_process"); + const result = execSync("pgrep -u $USER -f sshd-session 2>/dev/null", { + encoding: "utf-8", + timeout: 1000, + }); + return result.trim().length > 0; + } catch { + return false; + } +} +// ======================================= + +// CESP Core Categories +const CORE_CATEGORIES = [ + "session.start", + "task.acknowledge", + "task.complete", + "task.error", + "input.required", + "resource.limit", +] as const; + +type Category = typeof CORE_CATEGORIES[number]; + +interface Sound { + file: string; + label: string; + sha256?: string; +} + +interface CategoryConfig { + sounds: Sound[]; +} + +interface OpenPeonManifest { + cesp_version: string; + name: string; + display_name: string; + version: string; + categories: Record; + category_aliases?: Record; +} + +interface PeonConfig { + activePack: string; + volume: number; // 0.0 to 1.0 + muted: boolean; + enabledCategories: Record; +} + +// Default config +const DEFAULT_CONFIG: PeonConfig = { + activePack: DEFAULT_PACK, + volume: 0.7, + muted: false, + enabledCategories: { + "session.start": true, + "task.acknowledge": true, + "task.complete": true, + "task.error": true, + "input.required": true, + "resource.limit": true, + }, +}; + +// State +let config: PeonConfig = { ...DEFAULT_CONFIG }; +let lastPlayed: number = 0; + +// ============ CONFIG PERSISTENCE ============ +function loadConfig(): PeonConfig { + try { + if (fs.existsSync(CONFIG_FILE)) { + const content = fs.readFileSync(CONFIG_FILE, "utf-8"); + const saved = JSON.parse(content); + return { ...DEFAULT_CONFIG, ...saved }; + } + } catch { + // Fall back to defaults + } + return { ...DEFAULT_CONFIG }; +} + +function saveConfig(): void { + try { + const dir = path.dirname(CONFIG_FILE); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2)); + } catch (err) { + console.error("[peon] Failed to save config:", err); + } +} +let lastSoundPerCategory: Map = new Map(); +let manifestCache: Map = new Map(); + +// ============ AUDIO PLAYBACK ============ + +function playSoundLocal(soundPath: string, volume: number): void { + if (volume <= 0) return; + + const platform = process.platform; + + if (platform === "darwin") { + // macOS + const vol = volume.toFixed(2); + exec(`nohup afplay -v ${vol} "${soundPath}" >/dev/null 2>&1 &`); + } else if (platform === "linux") { + // Linux - try PipeWire first, then fall back + execFile("pw-play", ["--volume=" + volume, soundPath], (err) => { + if (err) { + // Fallback to paplay (PulseAudio) + const paVol = Math.round(volume * 65536); + execFile("paplay", ["--volume=" + paVol, soundPath], (err2) => { + if (err2) { + // Final fallback to aplay (ALSA) + execFile("aplay", [soundPath]); + } + }); + } + }); + } +} + +function playSoundRemote(soundPath: string, volume: number): void { + // Play sound on remote host via SSH + const vol = Math.round(volume * 100); + exec( + `ssh -o ConnectTimeout=2 -o BatchMode=yes ${REMOTE_HOST} "pw-play --volume=${vol} '${soundPath}'" 2>/dev/null || true`, + { timeout: 5000 } + ); +} + +function playSound(soundPath: string, volume: number): void { + // Always play locally + playSoundLocal(soundPath, volume); + + // If on SSH, also play remotely + if (isSSH()) { + playSoundRemote(soundPath, volume); + } +} + +function sendNotification(title: string, message: string): void { + if (process.platform === "linux") { + exec( + `notify-send -i ~/.pi/agent/extensions/assets/pi-logo.svg '${title}' '${message}' 2>/dev/null || true`, + { timeout: 5000 } + ); + } +} + +// ============ PACK MANAGEMENT ============ + +function getPackPath(packName: string): string { + return path.join(PACKS_DIR, packName); +} + +function loadManifest(packName: string): OpenPeonManifest | null { + if (manifestCache.has(packName)) { + return manifestCache.get(packName)!; + } + + const manifestPath = path.join(getPackPath(packName), "openpeon.json"); + if (!fs.existsSync(manifestPath)) { + return null; + } + + try { + const content = fs.readFileSync(manifestPath, "utf-8"); + const manifest = JSON.parse(content) as OpenPeonManifest; + manifestCache.set(packName, manifest); + return manifest; + } catch { + return null; + } +} + +function getInstalledPacks(): string[] { + if (!fs.existsSync(PACKS_DIR)) { + return []; + } + + return fs.readdirSync(PACKS_DIR).filter((name) => { + const packPath = path.join(PACKS_DIR, name); + const manifestPath = path.join(packPath, "openpeon.json"); + return fs.statSync(packPath).isDirectory() && fs.existsSync(manifestPath); + }); +} + +function resolveCategory(manifest: OpenPeonManifest, category: Category): CategoryConfig | null { + // Direct lookup + if (manifest.categories[category]) { + return manifest.categories[category]; + } + + // Check aliases + if (manifest.category_aliases) { + const aliased = manifest.category_aliases[category]; + if (aliased && manifest.categories[aliased]) { + return manifest.categories[aliased]; + } + } + + return null; +} + +function pickSound(categoryConfig: CategoryConfig, category: Category): Sound | null { + const sounds = categoryConfig.sounds; + if (sounds.length === 0) return null; + + // No-repeat: exclude last played sound if there are alternatives + const lastSound = lastSoundPerCategory.get(category); + let candidates = sounds; + if (lastSound && sounds.length > 1) { + candidates = sounds.filter((s) => s.file !== lastSound); + } + + // Random selection + const sound = candidates[Math.floor(Math.random() * candidates.length)]; + lastSoundPerCategory.set(category, sound.file); + return sound; +} + +// ============ SOUND PLAYBACK ============ + +function play(category: Category): void { + if (config.muted) return; + if (!config.enabledCategories[category]) return; + + // Global debounce check - never play two sounds at once + const now = Date.now(); + if (now - lastPlayed < DEBOUNCE_MS) { + return; + } + lastPlayed = now; + + // Load manifest + const manifest = loadManifest(config.activePack); + if (!manifest) { + console.error(`[peon] Pack not found: ${config.activePack}`); + return; + } + + // Resolve category + const categoryConfig = resolveCategory(manifest, category); + if (!categoryConfig) { + // Silently skip if pack doesn't have this category + return; + } + + // Pick and play sound + const sound = pickSound(categoryConfig, category); + if (!sound) return; + + const soundPath = path.join(getPackPath(config.activePack), sound.file); + if (!fs.existsSync(soundPath)) { + // Try with sounds/ prefix if no slash in path + if (!sound.file.includes("/")) { + const altPath = path.join(getPackPath(config.activePack), "sounds", sound.file); + if (fs.existsSync(altPath)) { + playSound(altPath, config.volume); + return; + } + } + console.error(`[peon] Sound file not found: ${soundPath}`); + return; + } + + playSound(soundPath, config.volume); + + // Send desktop notification for important events + const notificationMessages: Record = { + "session.start": null, // Too noisy on startup + "task.acknowledge": null, // Too noisy + "task.complete": { title: "Pi", message: "Task complete" }, + "task.error": { title: "Pi", message: "Task failed" }, + "input.required": { title: "Pi", message: "Input required" }, + "resource.limit": { title: "Pi", message: "Rate limited" }, + }; + + const notification = notificationMessages[category]; + if (notification) { + sendNotification(notification.title, notification.message); + } +} + +// ============ COMMANDS ============ + +function registerCommands(pi: ExtensionAPI) { + pi.registerCommand("peon", { + description: "Manage sound packs and settings", + handler: async (args: string, ctx: ExtensionContext) => { + const parts = args.trim().split(/\s+/); + const cmd = parts[0] || "status"; + + switch (cmd) { + case "status": { + const manifest = loadManifest(config.activePack); + const packDisplay = manifest?.display_name || config.activePack; + const lines = [ + `Active pack: ${packDisplay}`, + `Volume: ${Math.round(config.volume * 100)}%`, + `Muted: ${config.muted}`, + "Enabled categories:", + ]; + for (const cat of CORE_CATEGORIES) { + const enabled = config.enabledCategories[cat] ? "✓" : "✗"; + lines.push(` ${enabled} ${cat}`); + } + ctx.ui.notify(lines.join("\n"), "info"); + break; + } + + case "list": { + const packs = getInstalledPacks(); + if (packs.length === 0) { + ctx.ui.notify("No packs installed. Add packs to ~/.config/openpeon/packs/", "warning"); + } else { + const lines = ["Installed packs:"]; + for (const pack of packs) { + const m = loadManifest(pack); + const marker = pack === config.activePack ? "→ " : " "; + lines.push(`${marker}${m?.display_name || pack} (${pack})`); + } + ctx.ui.notify(lines.join("\n"), "info"); + } + break; + } + + case "set": { + const packName = parts[1]; + if (!packName) { + ctx.ui.notify("Usage: /peon set ", "error"); + return; + } + const packs = getInstalledPacks(); + if (!packs.includes(packName)) { + ctx.ui.notify(`Pack '${packName}' not found. Run /peon list to see installed packs.`, "error"); + return; + } + config.activePack = packName; + manifestCache.delete(packName); // Clear cache + saveConfig(); + ctx.ui.notify(`Switched to pack: ${packName}`, "info"); + break; + } + + case "volume": { + const volStr = parts[1]; + if (!volStr) { + ctx.ui.notify(`Current volume: ${Math.round(config.volume * 100)}%`, "info"); + return; + } + const vol = parseInt(volStr, 10); + if (isNaN(vol) || vol < 0 || vol > 100) { + ctx.ui.notify("Volume must be 0-100", "error"); + return; + } + config.volume = vol / 100; + saveConfig(); + ctx.ui.notify(`Volume set to ${vol}%`, "info"); + break; + } + + case "mute": { + config.muted = !config.muted; + saveConfig(); + ctx.ui.notify(config.muted ? "Muted" : "Unmuted", "info"); + break; + } + + case "toggle": { + const cat = parts[1] as Category; + if (!CORE_CATEGORIES.includes(cat)) { + ctx.ui.notify(`Unknown category: ${cat}\nAvailable: ${CORE_CATEGORIES.join(", ")}`, "error"); + return; + } + config.enabledCategories[cat] = !config.enabledCategories[cat]; + saveConfig(); + ctx.ui.notify(`${cat}: ${config.enabledCategories[cat] ? "enabled" : "disabled"}`, "info"); + break; + } + + case "test": { + const cat = parts[1] as Category; + if (!CORE_CATEGORIES.includes(cat)) { + ctx.ui.notify(`Unknown category: ${cat}\nAvailable: ${CORE_CATEGORIES.join(", ")}`, "error"); + return; + } + ctx.ui.notify(`Testing ${cat}...`, "info"); + play(cat); + break; + } + + default: { + ctx.ui.notify( + "Usage: /peon [status|list|set |volume <0-100>|mute|toggle |test ]", + "info" + ); + } + } + }, + }); +} + +// ============ EVENT WIRING ============ + +const INTERACTIVE_TOOLS = new Set(["question", "questionnaire"]); + +export default function(pi: ExtensionAPI) { + registerCommands(pi); + + // Session start + pi.on("session_start", async (_event, ctx) => { + // Load persisted config + config = loadConfig(); + if (!ctx.hasUI) return; + play("session.start"); + }); + + // Task acknowledge - when agent starts working + pi.on("agent_start", async (_event, ctx) => { + if (!ctx.hasUI) return; + play("task.acknowledge"); + }); + + // Task complete - when agent finishes + pi.on("agent_end", async (_event, ctx) => { + if (!ctx.hasUI) return; + play("task.complete"); + }); + + // Task error - when a tool errors + pi.on("tool_result", async (event, ctx) => { + if (!ctx.hasUI) return; + if (event.isError) { + play("task.error"); + } + }); + + // Input required - when question/questionnaire is called + pi.on("tool_call", async (event, ctx) => { + if (!ctx.hasUI) return; + if (INTERACTIVE_TOOLS.has(event.toolName)) { + play("input.required"); + } + }); + + // Resource limit - detect rate limiting in tool results + pi.on("tool_result", async (event, ctx) => { + if (!ctx.hasUI) return; + // Check for rate limit indicators in error messages + const firstContent = event.content?.[0]; + const content = firstContent?.type === "text" ? firstContent.text : ""; + if (event.isError && /rate.limit|quota|too.many.requests|429/i.test(content)) { + play("resource.limit"); + } + }); + + // Listen for input_required events from other extensions (permission-gate, confirm-destructive, etc.) + pi.events.on("peon:input_required", (_data) => { + play("input.required"); + }); +} diff --git a/pi/files/agent/extensions/permission-gate.ts b/pi/files/agent/extensions/permission-gate.ts index b77d5d9..44850e5 100644 --- a/pi/files/agent/extensions/permission-gate.ts +++ b/pi/files/agent/extensions/permission-gate.ts @@ -6,7 +6,6 @@ */ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; -import { sendNotification } from "./notify.js"; export default function (pi: ExtensionAPI) { const dangerousPatterns = [/\brm\s+(-rf?|--recursive)/i, /\bsudo\b/i, /\b(chmod|chown)\b.*777/i]; @@ -18,12 +17,14 @@ export default function (pi: ExtensionAPI) { const isDangerous = dangerousPatterns.some((p) => p.test(command)); if (isDangerous) { + // Emit event for sound extensions (play sound regardless of UI mode) + pi.events.emit("peon:input_required", { source: "permission-gate", action: "dangerous-command", command }); + if (!ctx.hasUI) { // In non-interactive mode, block by default return { block: true, reason: "Dangerous command blocked (no UI for confirmation)" }; } - sendNotification("Destructive command pending"); const choice = await ctx.ui.select(`⚠️ Dangerous command:\n\n ${command}\n\nAllow?`, ["Yes", "No"]); if (choice !== "Yes") {