Initial: pi-skill — 68 skills, 43 extensions, 11 themes for Pi

This commit is contained in:
Kunthawat Greethong
2026-05-25 16:38:02 +07:00
commit 69f7d8bdda
1689 changed files with 342427 additions and 0 deletions

501
extensions/sounds.ts Normal file
View File

@@ -0,0 +1,501 @@
// ABOUTME: Soundcn Extension — Browser-based sound viewer with Pi lifecycle hook notifications.
// ABOUTME: /sounds command opens browser UI to browse, preview, and assign sounds from soundcn.xyz to Pi events.
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
import { Text } from "@mariozechner/pi-tui";
import { Type } from "@sinclair/typebox";
import { readFileSync, existsSync } from "node:fs";
import { join, dirname } from "node:path";
import { execSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { createServer, type Server, type IncomingMessage, type ServerResponse } from "node:http";
import { outputLine } from "./lib/output-box.ts";
import { applyExtensionDefaults } from "./lib/themeMap.ts";
import { generateSoundsViewerHTML, type CatalogItem } from "./lib/sounds-viewer-html.ts";
import {
loadConfig, saveConfig, getActiveAssignmentCount, getAssignedSoundNames,
type SoundsConfig, type HookName, ALL_HOOKS, HOOK_DISPLAY_NAMES,
} from "./lib/sounds-config.ts";
import {
playInstalledSound, installSound, uninstallSound, isSoundInstalled,
cleanupAllPlayback,
} from "./lib/sounds-player.ts";
import { registerActiveViewer, clearActiveViewer, notifyViewerOpen } from "./lib/viewer-session.ts";
// ── Types ────────────────────────────────────────────────────────────
interface SoundsViewerResult {
action: "applied" | "cancelled";
assignments?: Record<string, string>;
volume?: number;
enabled?: boolean;
}
// ── Catalog Fetching ─────────────────────────────────────────────────
let cachedCatalog: CatalogItem[] | null = null;
async function fetchCatalog(): Promise<CatalogItem[]> {
if (cachedCatalog) return cachedCatalog;
const resp = await fetch(
"https://raw.githubusercontent.com/ruizrica/soundcn/main/registry.json",
);
if (!resp.ok) throw new Error(`Failed to fetch catalog: ${resp.status}`);
const data = await resp.json();
const items: CatalogItem[] = (data.items || [])
.filter((item: any) => item.type === "registry:block")
.map((item: any) => ({
name: item.name,
title: item.title || item.name,
description: item.description || "",
categories: item.categories || [],
author: item.author,
meta: item.meta,
}));
cachedCatalog = items;
return items;
}
// ── HTTP Server ──────────────────────────────────────────────────────
function startSoundsServer(
catalog: CatalogItem[],
config: SoundsConfig,
): Promise<{ port: number; server: Server; waitForResult: () => Promise<SoundsViewerResult> }> {
return new Promise((resolveSetup) => {
let resolveResult: (result: SoundsViewerResult) => void;
const resultPromise = new Promise<SoundsViewerResult>((res) => {
resolveResult = res;
});
let lastHeartbeat = Date.now();
const heartbeatCheck = setInterval(() => {
if (Date.now() - lastHeartbeat > 15_000) {
clearInterval(heartbeatCheck);
resolveResult!({ action: "cancelled" });
}
}, 5_000);
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
if (req.method === "OPTIONS") {
res.writeHead(204);
res.end();
return;
}
const url = new URL(req.url || "/", "http://localhost");
// Serve main HTML page
if (req.method === "GET" && url.pathname === "/") {
const port = (server.address() as any)?.port || 0;
res.setHeader("Cache-Control", "no-store");
const html = generateSoundsViewerHTML({ catalog, config, port });
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(html);
return;
}
// Serve logo
if (req.method === "GET" && url.pathname === "/logo.png") {
try {
const logoPath = join(dirname(fileURLToPath(import.meta.url)), "assets", "agent-logo.png");
const logoData = readFileSync(logoPath);
res.writeHead(200, { "Content-Type": "image/png", "Cache-Control": "public, max-age=3600" });
res.end(logoData);
} catch {
res.writeHead(404);
res.end();
}
return;
}
// Heartbeat keep-alive
if (req.method === "POST" && url.pathname === "/heartbeat") {
lastHeartbeat = Date.now();
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true }));
return;
}
// CORS proxy: fetch sound data from soundcn.xyz server-side
if (req.method === "GET" && url.pathname.startsWith("/api/sound/")) {
const name = decodeURIComponent(url.pathname.slice("/api/sound/".length));
if (!name || name.includes("/") || name.includes("..")) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Invalid sound name" }));
return;
}
(async () => {
try {
const upstream = await fetch(`https://soundcn.xyz/r/${encodeURIComponent(name)}.json`);
if (!upstream.ok) {
res.writeHead(upstream.status, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: `Upstream returned ${upstream.status}` }));
return;
}
const body = await upstream.text();
res.writeHead(200, {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=3600",
});
res.end(body);
} catch (err: any) {
res.writeHead(502, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: err?.message || "Proxy fetch failed" }));
}
})();
return;
}
// Handle result submission (apply/cancel)
if (req.method === "POST" && url.pathname === "/result") {
let body = "";
req.on("data", (chunk) => { body += chunk; });
req.on("end", () => {
try {
const data = JSON.parse(body);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true }));
resolveResult!({
action: data.action || "cancelled",
assignments: data.assignments,
volume: data.volume,
enabled: data.enabled,
});
} catch {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Invalid JSON" }));
}
});
return;
}
// Install sound (save base64 data to cache)
if (req.method === "POST" && url.pathname === "/install") {
let body = "";
req.on("data", (chunk) => { body += chunk; });
req.on("end", () => {
try {
const data = JSON.parse(body);
if (data.name && data.dataUri) {
installSound(data.name, data.dataUri);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true }));
} else {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Missing name or dataUri" }));
}
} catch (err: any) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: err?.message || "Install failed" }));
}
});
return;
}
// Uninstall sound (remove from cache)
if (req.method === "POST" && url.pathname === "/uninstall") {
let body = "";
req.on("data", (chunk) => { body += chunk; });
req.on("end", () => {
try {
const data = JSON.parse(body);
if (data.name) {
uninstallSound(data.name);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true }));
} else {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Missing name" }));
}
} catch (err: any) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: err?.message || "Uninstall failed" }));
}
});
return;
}
res.writeHead(404);
res.end("Not found");
});
server.listen(0, "127.0.0.1", () => {
const addr = server.address() as any;
resolveSetup({
port: addr.port,
server,
waitForResult: () => resultPromise.finally(() => clearInterval(heartbeatCheck)),
});
});
});
}
function openBrowser(url: string): void {
try {
execSync(`open "${url}"`, { stdio: "ignore" });
} catch {
try {
execSync(`xdg-open "${url}"`, { stdio: "ignore" });
} catch {
try {
execSync(`start "${url}"`, { stdio: "ignore" });
} catch {}
}
}
}
// ── Extension ────────────────────────────────────────────────────────
export default function (pi: ExtensionAPI) {
let activeServer: Server | null = null;
let activeSession: any | null = null;
let currentConfig: SoundsConfig = loadConfig();
function cleanupServer() {
const server = activeServer;
activeServer = null;
if (server) {
try { server.close(); } catch {}
}
if (activeSession) {
clearActiveViewer(activeSession);
activeSession = null;
}
}
function updateStatus(ctx: ExtensionContext) {
if (!ctx.hasUI) return;
const count = getActiveAssignmentCount(currentConfig);
if (!currentConfig.enabled) {
ctx.ui.setStatus("sounds", "🔇 Sounds OFF");
} else if (count > 0) {
ctx.ui.setStatus("sounds", `🔊 ${count} hook${count !== 1 ? "s" : ""}`);
} else {
ctx.ui.setStatus("sounds", "🔊 Sounds");
}
}
// ── Core viewer logic ────────────────────────────────────────────
async function runSoundsViewer(ctx: ExtensionContext): Promise<SoundsViewerResult> {
cleanupServer();
// Fetch catalog
ctx.ui.notify("Loading sound catalog from soundcn.xyz...", "info");
let catalog: CatalogItem[];
try {
catalog = await fetchCatalog();
} catch (err: any) {
ctx.ui.notify(`Failed to fetch catalog: ${err.message}`, "error");
return { action: "cancelled" };
}
ctx.ui.notify(`Loaded ${catalog.length} sounds. Opening browser...`, "info");
// Start server
const { port, server, waitForResult } = await startSoundsServer(catalog, currentConfig);
activeServer = server;
const url = `http://127.0.0.1:${port}`;
activeSession = {
kind: "sounds" as const,
title: "Sound Browser",
url,
server,
onClose: () => { activeServer = null; activeSession = null; },
};
registerActiveViewer(activeSession);
openBrowser(url);
notifyViewerOpen(ctx, activeSession);
try {
const result = await waitForResult();
// Apply config if user clicked "Apply"
if (result.action === "applied" && result.assignments) {
currentConfig = {
assignments: result.assignments as Partial<Record<HookName, string>>,
volume: typeof result.volume === "number" ? result.volume : currentConfig.volume,
enabled: typeof result.enabled === "boolean" ? result.enabled : currentConfig.enabled,
};
saveConfig(currentConfig);
updateStatus(ctx);
}
return result;
} finally {
cleanupServer();
}
}
// ── /sounds command ──────────────────────────────────────────────
pi.registerCommand("sounds", {
description: "Open the sound browser, or use: /sounds toggle | /sounds status",
handler: async (args, ctx) => {
if (!ctx.hasUI) {
ctx.ui.notify("/sounds requires interactive mode", "error");
return;
}
const arg = args.trim().toLowerCase();
// /sounds toggle
if (arg === "toggle") {
currentConfig = { ...currentConfig, enabled: !currentConfig.enabled };
saveConfig(currentConfig);
updateStatus(ctx);
ctx.ui.notify(
currentConfig.enabled ? "🔊 Sounds enabled" : "🔇 Sounds disabled",
"info",
);
return;
}
// /sounds status
if (arg === "status") {
const count = getActiveAssignmentCount(currentConfig);
const lines: string[] = [
`Sounds: ${currentConfig.enabled ? "Enabled" : "Disabled"}`,
`Volume: ${Math.round(currentConfig.volume * 100)}%`,
`Hooks: ${count}/${ALL_HOOKS.length} assigned`,
];
for (const hook of ALL_HOOKS) {
const sound = currentConfig.assignments[hook];
const label = HOOK_DISPLAY_NAMES[hook];
lines.push(` ${label}: ${sound || "(none)"}`);
}
ctx.ui.notify(lines.join("\n"), "info");
return;
}
// /sounds — open browser
const result = await runSoundsViewer(ctx);
if (result.action === "applied") {
const count = getActiveAssignmentCount(currentConfig);
ctx.ui.notify(`✓ Sound config applied — ${count} hook${count !== 1 ? "s" : ""} assigned`, "info");
}
},
});
// ── show_sounds tool ─────────────────────────────────────────────
pi.registerTool({
name: "show_sounds",
label: "Show Sounds",
description:
"Open the sound browser to let the user browse, preview, and assign sounds from soundcn.xyz to Pi lifecycle hooks like task completion, agent start, tool calls, etc.",
parameters: Type.Object({}),
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
if (!ctx.hasUI) {
return {
content: [{ type: "text" as const, text: "Sound browser requires interactive mode." }],
};
}
const result = await runSoundsViewer(ctx);
if (result.action === "applied") {
const count = getActiveAssignmentCount(currentConfig);
const assigned = Object.entries(currentConfig.assignments)
.map(([hook, sound]) => ` ${HOOK_DISPLAY_NAMES[hook as HookName]}: ${sound}`)
.join("\n");
return {
content: [{
type: "text" as const,
text: `Sound config applied. ${count} hook${count !== 1 ? "s" : ""} assigned:\n${assigned || " (none)"}`,
}],
details: { action: "applied", config: currentConfig },
};
}
return {
content: [{ type: "text" as const, text: "Sound browser closed without applying changes." }],
details: { action: "cancelled" },
};
},
renderCall(_args, theme) {
const text = theme.fg("toolTitle", theme.bold("show_sounds ")) +
theme.fg("dim", "Opening sound browser...");
return new Text(outputLine(theme, "accent", text), 0, 0);
},
renderResult(result, _options, theme) {
const details = result.details as any;
if (!details) {
const text = result.content[0];
return new Text(text?.type === "text" ? text.text : "", 0, 0);
}
if (details.action === "applied") {
const count = getActiveAssignmentCount(details.config || {});
return new Text(outputLine(theme, "success", `Sound config applied — ${count} hooks`), 0, 0);
}
return new Text(outputLine(theme, "warning", "Sound browser closed"), 0, 0);
},
});
// ── Lifecycle Hook Sound Playback ────────────────────────────────
function playHookSound(hookName: HookName): void {
if (!currentConfig.enabled) return;
const soundName = currentConfig.assignments[hookName];
if (!soundName) return;
if (!isSoundInstalled(soundName)) return;
// Fire and forget — don't block the hook
playInstalledSound(soundName, currentConfig.volume).catch(() => {});
}
pi.on("agent_end", async () => {
playHookSound("agent_end");
});
pi.on("agent_start", async () => {
playHookSound("agent_start");
});
pi.on("tool_execution_start", async () => {
playHookSound("tool_execution_start");
});
pi.on("tool_execution_end", async () => {
playHookSound("tool_execution_end");
});
pi.on("turn_start", async () => {
playHookSound("turn_start");
});
pi.on("turn_end", async () => {
playHookSound("turn_end");
});
pi.on("session_compact", async () => {
playHookSound("session_compact");
});
// ── Session Lifecycle ────────────────────────────────────────────
pi.on("session_start", async (_event, ctx) => {
applyExtensionDefaults(import.meta.url, ctx);
currentConfig = loadConfig();
updateStatus(ctx);
// Play session start sound if assigned
playHookSound("session_start");
});
pi.on("session_shutdown", async () => {
cleanupServer();
cleanupAllPlayback();
});
}