diff --git a/extensions/caveman/index.ts b/extensions/caveman/index.ts new file mode 100644 index 0000000..1c4bb7d --- /dev/null +++ b/extensions/caveman/index.ts @@ -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(); +} diff --git a/extensions/caveman/package.json b/extensions/caveman/package.json new file mode 100644 index 0000000..51508f6 --- /dev/null +++ b/extensions/caveman/package.json @@ -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": "*" + } +} diff --git a/skills/caveman/SKILL.md b/skills/caveman/SKILL.md new file mode 100644 index 0000000..835dd0d --- /dev/null +++ b/skills/caveman/SKILL.md @@ -0,0 +1,79 @@ +--- +name: caveman +description: > + Ultra-compressed communication mode. Cuts token usage ~75% by speaking like caveman + while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra, + wenyan-lite, wenyan-full, wenyan-ultra. + Use when user says "caveman mode", "talk like caveman", "use caveman", "less tokens", + "be brief", or invokes /caveman. Also auto-triggers when token efficiency is requested. +--- + +Terse like smart caveman. All technical substance stay. Only fluff die. + +## Persistence + +ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift. Still active if unsure. +Off only: "stop caveman" / "normal mode". + +Default: **full**. Switch: `/caveman lite|full|ultra`. + +## Rules + +Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries +(sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, +fix not "implement a solution for"). Technical terms exact. Code blocks unchanged. +Errors quoted exact. + +Pattern: `[thing] [action] [reason]. [next step].` + +Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..." +Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:" + +## Intensity + +| Level | What change | +|-------|------------| +| **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight | +| **full** | Drop articles, fragments OK, short synonyms. Classic caveman | +| **ultra** | Abbreviate prose words (DB/auth/config/req/res/fn/impl), strip conjunctions, arrows for causality (X → Y), one word when one word enough. Code symbols, function names, API names, error strings: never abbreviate | +| **wenyan-lite** | Semi-classical. Drop filler/hedging but keep grammar structure, classical register | +| **wenyan-full** | Maximum classical terseness. Fully 文言文. 80-90% character reduction. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (之/乃/為/其) | +| **wenyan-ultra** | Extreme abbreviation while keeping classical Chinese feel. Maximum compression, ultra terse | + +Example — "Why React component re-render?" +- lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`." +- full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`." +- ultra: "Inline obj prop → new ref → re-render. `useMemo`." +- wenyan-lite: "組件頻重繪,以每繪新生對象參照故。以 useMemo 包之。" +- wenyan-full: "物出新參照,致重繪。useMemo Wrap之。" +- wenyan-ultra: "新參照→重繪。useMemo Wrap。" + +Example — "Explain database connection pooling." +- lite: "Connection pooling reuses open connections instead of creating new ones per request. Avoids repeated handshake overhead." +- full: "Pool reuse open DB connections. No new connection per request. Skip handshake overhead." +- ultra: "Pool = reuse DB conn. Skip handshake → fast under load." +- wenyan-full: "池reuse open connection。不每req新開。skip handshake overhead。" +- wenyan-ultra: "池reuse conn。skip handshake → fast。" + +## Auto-Clarity + +Drop caveman when: +- Security warnings +- Irreversible action confirmations +- Multi-step sequences where fragment order or omitted conjunctions risk misread +- Compression itself creates technical ambiguity (e.g., "migrate table drop column backup first" — order unclear without articles/conjunctions) +- User asks to clarify or repeats question + +Resume caveman after clear part done. + +Example — destructive op: +> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone. +> ```sql +> DROP TABLE users; +> ``` +> Caveman resume. Verify backup exist first. + +## Boundaries + +Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. +Level persist until changed or session end.