202 lines
6.6 KiB
TypeScript
202 lines
6.6 KiB
TypeScript
/**
|
|
* pi-caveman — Compressed-output mode for Pi coding agent.
|
|
*
|
|
* Registers a `/caveman` slash command that toggles caveman-speak
|
|
* compression levels and persists state across session restarts.
|
|
*
|
|
* Usage:
|
|
* /caveman — toggle between off and full
|
|
* /caveman lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra|off|status
|
|
*/
|
|
|
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
import * as fs from "node:fs";
|
|
import * as path from "node:path";
|
|
|
|
// ── Types ────────────────────────────────────────────────────────────────
|
|
|
|
type CavemanLevel = "lite" | "full" | "ultra" | "wenyan-lite" | "wenyan-full" | "wenyan-ultra";
|
|
|
|
interface CavemanState {
|
|
active: boolean;
|
|
level: CavemanLevel;
|
|
}
|
|
|
|
// ── Constants ────────────────────────────────────────────────────────────
|
|
|
|
const DEFAULT_LEVEL: CavemanLevel = "full";
|
|
|
|
const STATE_DIR = path.join(
|
|
process.env.XDG_STATE_HOME ?? path.join(process.env.HOME!, ".local", "state"),
|
|
"pi-caveman",
|
|
);
|
|
|
|
const STATE_FILE = path.join(STATE_DIR, "caveman-state.json");
|
|
|
|
const CAVEMAN_SKILL_PATH = path.join(
|
|
process.env.HOME!,
|
|
"ai-assets",
|
|
"skills",
|
|
"caveman",
|
|
"SKILL.md",
|
|
);
|
|
|
|
// ── State helpers ────────────────────────────────────────────────────────
|
|
|
|
function readState(): CavemanState {
|
|
try {
|
|
const raw = fs.readFileSync(STATE_FILE, "utf-8");
|
|
const parsed = JSON.parse(raw);
|
|
return {
|
|
active: typeof parsed.active === "boolean" ? parsed.active : false,
|
|
level: isValidLevel(parsed.level) ? parsed.level : DEFAULT_LEVEL,
|
|
};
|
|
} catch {
|
|
return { active: false, level: DEFAULT_LEVEL };
|
|
}
|
|
}
|
|
|
|
function writeState(state: CavemanState): void {
|
|
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), "utf-8");
|
|
}
|
|
|
|
function isValidLevel(v: unknown): v is CavemanLevel {
|
|
return (
|
|
typeof v === "string" &&
|
|
["lite", "full", "ultra", "wenyan-lite", "wenyan-full", "wenyan-ultra"].includes(v)
|
|
);
|
|
}
|
|
|
|
// ── Skill content cache ──────────────────────────────────────────────────
|
|
|
|
let cachedSkillContent: string | null = null;
|
|
|
|
function getSkillContent(): string {
|
|
if (cachedSkillContent !== null) return cachedSkillContent;
|
|
try {
|
|
cachedSkillContent = fs.readFileSync(CAVEMAN_SKILL_PATH, "utf-8");
|
|
return cachedSkillContent;
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
// ── Extension ────────────────────────────────────────────────────────────
|
|
|
|
export default function (pi: ExtensionAPI): void {
|
|
// ── State ──
|
|
let state = readState();
|
|
|
|
// ── Footer badge ──
|
|
function updateFooter(ctx: { ui: { setStatus: (k: string, v: string) => void } }): void {
|
|
ctx.ui.setStatus("caveman", state.active ? `🦴 caveman:${state.level}` : "");
|
|
}
|
|
|
|
// ── /caveman command ────────────────────────────────────────────────
|
|
|
|
pi.registerCommand("caveman", {
|
|
description: "Toggle caveman compressed-output mode (lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra|off|status)",
|
|
handler: async (args, ctx) => {
|
|
const arg = args?.trim().toLowerCase() ?? "";
|
|
|
|
switch (arg) {
|
|
case "":
|
|
// Toggle: off → full, anything → off
|
|
if (state.active) {
|
|
state.active = false;
|
|
ctx.ui.notify("🦴 Caveman: off", "info");
|
|
} else {
|
|
state.active = true;
|
|
state.level = DEFAULT_LEVEL;
|
|
ctx.ui.notify(`🦴 Caveman: ${state.level}`, "info");
|
|
}
|
|
break;
|
|
|
|
case "off":
|
|
case "disable":
|
|
state.active = false;
|
|
ctx.ui.notify("🦴 Caveman: off", "info");
|
|
break;
|
|
|
|
case "status":
|
|
ctx.ui.notify(
|
|
state.active ? `🦴 Caveman: ${state.level} (active)` : "🦴 Caveman: off",
|
|
"info",
|
|
);
|
|
return; // Don't update footer (already correct)
|
|
|
|
case "lite":
|
|
case "full":
|
|
case "ultra":
|
|
case "wenyan-lite":
|
|
case "wenyan-full":
|
|
case "wenyan-ultra":
|
|
state.active = true;
|
|
state.level = arg as CavemanLevel;
|
|
ctx.ui.notify(`🦴 Caveman: ${state.level}`, "info");
|
|
break;
|
|
|
|
default:
|
|
ctx.ui.notify(
|
|
`🦴 Unknown level: "${arg}". Use: lite, full, ultra, wenyan-lite, wenyan-full, wenyan-ultra, off, status`,
|
|
"error",
|
|
);
|
|
return;
|
|
}
|
|
|
|
writeState(state);
|
|
updateFooter(ctx);
|
|
|
|
// Inject skill content as a user message when activating
|
|
if (state.active) {
|
|
const skill = getSkillContent();
|
|
if (skill) {
|
|
pi.sendUserMessage(
|
|
`/skill:caveman`,
|
|
{ deliverAs: "nextTurn" },
|
|
);
|
|
}
|
|
}
|
|
},
|
|
});
|
|
|
|
// ── Startup ─────────────────────────────────────────────────────────
|
|
|
|
pi.on("session_start", async (_event, ctx) => {
|
|
// Re-read state (may have changed externally)
|
|
state = readState();
|
|
updateFooter(ctx);
|
|
|
|
// If active, inject skill into agent context
|
|
if (state.active) {
|
|
const skill = getSkillContent();
|
|
if (skill) {
|
|
pi.appendEntry("caveman-active", { level: state.level });
|
|
}
|
|
}
|
|
});
|
|
|
|
// ── Inject caveman rules when active ────────────────────────────────
|
|
|
|
pi.on("before_agent_start", async (_event) => {
|
|
// Re-read state in case it changed
|
|
const current = readState();
|
|
if (!current.active) return;
|
|
|
|
const skill = getSkillContent();
|
|
if (!skill) return;
|
|
|
|
return {
|
|
systemPrompt: `${_event.systemPrompt}\n\n---\nCRITICAL OUTPUT MODE: You are in CAVEMAN MODE at level "${current.level}". Follow these rules exactly:\n\n${skill}`,
|
|
};
|
|
});
|
|
|
|
// ── Cleanup on shutdown ─────────────────────────────────────────────
|
|
|
|
pi.on("session_shutdown", async () => {
|
|
writeState(state);
|
|
});
|
|
|
|
}
|