peon is done
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -6,14 +6,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ExtensionAPI, SessionBeforeSwitchEvent, SessionMessageEntry } from "@mariozechner/pi-coding-agent";
|
import type { ExtensionAPI, SessionBeforeSwitchEvent, SessionMessageEntry } from "@mariozechner/pi-coding-agent";
|
||||||
import { sendNotification } from "./notify.js";
|
|
||||||
|
|
||||||
export default function (pi: ExtensionAPI) {
|
export default function (pi: ExtensionAPI) {
|
||||||
pi.on("session_before_switch", async (event: SessionBeforeSwitchEvent, ctx) => {
|
pi.on("session_before_switch", async (event: SessionBeforeSwitchEvent, ctx) => {
|
||||||
|
if (event.reason === "new") {
|
||||||
|
// Emit event for sound extensions (before hasUI check)
|
||||||
|
pi.events.emit("peon:input_required", { source: "confirm-destructive", action: "clear-session" });
|
||||||
if (!ctx.hasUI) return;
|
if (!ctx.hasUI) return;
|
||||||
|
|
||||||
if (event.reason === "new") {
|
|
||||||
sendNotification("Clear session confirmation");
|
|
||||||
const confirmed = await ctx.ui.confirm(
|
const confirmed = await ctx.ui.confirm(
|
||||||
"Clear session?",
|
"Clear session?",
|
||||||
"This will delete all messages in the current session.",
|
"This will delete all messages in the current session.",
|
||||||
@@ -33,7 +33,10 @@ export default function (pi: ExtensionAPI) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (hasUnsavedWork) {
|
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(
|
const confirmed = await ctx.ui.confirm(
|
||||||
"Switch session?",
|
"Switch session?",
|
||||||
"You have messages in the current session. Switch anyway?",
|
"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) => {
|
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;
|
if (!ctx.hasUI) return;
|
||||||
|
|
||||||
sendNotification("Fork session confirmation");
|
|
||||||
const choice = await ctx.ui.select(`Fork from entry ${event.entryId.slice(0, 8)}?`, [
|
const choice = await ctx.ui.select(`Fork from entry ${event.entryId.slice(0, 8)}?`, [
|
||||||
"Yes, create fork",
|
"Yes, create fork",
|
||||||
"No, stay in current session",
|
"No, stay in current session",
|
||||||
|
|||||||
@@ -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 <pack> - Switch to a different pack
|
||||||
|
* /peon volume <0-100> - Set master volume
|
||||||
|
* /peon mute - Toggle global mute
|
||||||
|
* /peon test <category> - 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<string, CategoryConfig>;
|
||||||
|
category_aliases?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PeonConfig {
|
||||||
|
activePack: string;
|
||||||
|
volume: number; // 0.0 to 1.0
|
||||||
|
muted: boolean;
|
||||||
|
enabledCategories: Record<Category, boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<Category, string> = new Map();
|
||||||
|
let manifestCache: Map<string, OpenPeonManifest> = 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<Category, { title: string; message: string } | null> = {
|
||||||
|
"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 <pack-name>", "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 <pack>|volume <0-100>|mute|toggle <category>|test <category>]",
|
||||||
|
"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");
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||||
import { sendNotification } from "./notify.js";
|
|
||||||
|
|
||||||
export default function (pi: ExtensionAPI) {
|
export default function (pi: ExtensionAPI) {
|
||||||
const dangerousPatterns = [/\brm\s+(-rf?|--recursive)/i, /\bsudo\b/i, /\b(chmod|chown)\b.*777/i];
|
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));
|
const isDangerous = dangerousPatterns.some((p) => p.test(command));
|
||||||
|
|
||||||
if (isDangerous) {
|
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) {
|
if (!ctx.hasUI) {
|
||||||
// In non-interactive mode, block by default
|
// In non-interactive mode, block by default
|
||||||
return { block: true, reason: "Dangerous command blocked (no UI for confirmation)" };
|
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"]);
|
const choice = await ctx.ui.select(`⚠️ Dangerous command:\n\n ${command}\n\nAllow?`, ["Yes", "No"]);
|
||||||
|
|
||||||
if (choice !== "Yes") {
|
if (choice !== "Yes") {
|
||||||
|
|||||||
Reference in New Issue
Block a user