Files
dotfiles/pi/files/agent/extensions/pi-done-notify.ts
T
2026-03-04 19:51:46 +00:00

101 lines
2.8 KiB
TypeScript

/**
* Pi Done Notify Extension
*
* Sends a notification when Pi finishes a prompt.
* If on SSH/remote, SSHs back to Linux PC to play sound + show notification.
*/
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
// ============ CONFIGURATION ============
const REMOTE_HOST = "linux-pc"; // SSH host for remote notifications
const REMOTE_COMMAND =
"paplay /usr/share/sounds/freedesktop/stereo/window-attention.oga & notify-send -i ~/.pi/agent/extensions/assets/pi-logo.svg 'Pi' 'Done. Ready for input.'";
const LOCAL_SOUND_MAC = "/System/Library/Sounds/Glass.aiff";
const LOCAL_SOUND_LINUX = "/usr/share/sounds/freedesktop/stereo/complete.oga";
// =======================================
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;
}
}
function notifyRemote(): void {
const { exec } = require("child_process");
exec(
`ssh -o ConnectTimeout=2 -o BatchMode=yes ${REMOTE_HOST} "${REMOTE_COMMAND}"`,
{ timeout: 5000 },
);
}
function notifyLocal(): void {
const { execFile, exec } = require("child_process");
if (process.platform === "darwin") {
execFile("afplay", [LOCAL_SOUND_MAC]);
} else if (process.platform === "linux") {
execFile(
"paplay",
["/usr/share/sounds/freedesktop/stereo/window-attention.oga"],
(err: any) => {
if (err) console.error("paplay error:", err);
},
);
exec(
"notify-send -i ~/.pi/agent/extensions/assets/pi-logo.svg 'Pi' 'Done. Ready for input.'",
(err: any) => {
if (err) console.error("notify-send error:", err);
},
);
}
}
function sendNotification(): void {
if (process.platform === "darwin" || isSSH()) {
notifyRemote();
}
// always notify locally too
notifyLocal();
}
const ACTION_NOTIFY_TOOLS = new Set(["question", "questionnaire"]);
export default function (pi: ExtensionAPI) {
pi.on("agent_end", async (_event, ctx) => {
if (!ctx.hasUI) return;
sendNotification();
});
// Notify when interactive tools are called so the user notices prompts immediately
pi.on("tool_call", async (event, ctx) => {
if (!ctx.hasUI) return;
if (!ACTION_NOTIFY_TOOLS.has(event.toolName)) return;
sendNotification();
});
// Simple test command
pi.registerCommand("notify", {
description: "Test notification",
handler: async (_args, ctx) => {
sendNotification();
ctx.ui.notify("Notification sent!", "info");
},
});
}