Add caveman compressed-output extension and skill
This commit is contained in:
226
extensions/caveman/index.ts
Normal file
226
extensions/caveman/index.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* 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();
|
||||
|
||||
function applyMode(): void {
|
||||
writeState(state);
|
||||
if (state.active) {
|
||||
pi.setStatus("caveman", `🦴 caveman:${state.level}`);
|
||||
// Inject caveman instructions into the system prompt each turn
|
||||
pi.on("before_agent_start", async (_event) => {
|
||||
if (!state.active) return;
|
||||
const skill = getSkillContent();
|
||||
if (!skill) return;
|
||||
return {
|
||||
systemPrompt: `${_event.systemPrompt}\n\n---\nCRITICAL OUTPUT MODE: You are in CAVEMAN MODE. Follow these rules:\n\n${skill}`,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// Clear any previous status
|
||||
// (constructor-style state means we can't easily unregister)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Footer badge (always shown) ──
|
||||
function updateFooter(): void {
|
||||
if (state.active) {
|
||||
pi.setStatus("caveman", `🦴 caveman:${state.level}`);
|
||||
} else {
|
||||
pi.setStatus("caveman", "");
|
||||
}
|
||||
}
|
||||
|
||||
// ── /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();
|
||||
|
||||
// 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 () => {
|
||||
// Re-read state (may have changed externally)
|
||||
state = readState();
|
||||
updateFooter();
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
// ── Apply initial mode ──────────────────────────────────────────────
|
||||
applyMode();
|
||||
}
|
||||
16
extensions/caveman/package.json
Normal file
16
extensions/caveman/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "pi-caveman",
|
||||
"version": "1.0.0",
|
||||
"description": "Caveman compressed-output mode for Pi coding agent — toggle with /caveman",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"main": "./index.ts",
|
||||
"pi": {
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
]
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@mariozechner/pi-coding-agent": "*"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user