peon is done

This commit is contained in:
2026-03-10 15:49:04 +00:00
parent 68a7753500
commit c76a74ca6f
41 changed files with 916 additions and 8 deletions
+13
View File
@@ -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
}
}
+41
View File
@@ -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.
@@ -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",
+511
View File
@@ -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");
});
}
+3 -2
View File
@@ -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") {