From 6f4e0c8b68b5ab0c5bc7cc38343a7c74f5f6ebb6 Mon Sep 17 00:00:00 2001 From: rain Date: Fri, 21 Aug 2026 17:29:47 -0400 Subject: [PATCH 1/5] zsh: auto-update omp before launch --- dot_zshrc.tmpl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dot_zshrc.tmpl b/dot_zshrc.tmpl index 9758eaa..b53821c 100644 --- a/dot_zshrc.tmpl +++ b/dot_zshrc.tmpl @@ -115,6 +115,9 @@ alias ll='eza -l --color=always --group-directories-first --icons' alias la='eza -a --color=always --group-directories-first --icons' alias lt='eza -aT --color=always --group-directories-first --icons' +# Oh My Pi — self-update before launch +alias omp='omp update && omp' + {{ if eq .os_family "arch" -}} # Pacman / system (Arch-base only) alias update='sudo pacman -Syu' From 0c363443bbcd818c9b3bbc63c90d3d99105abe34 Mon Sep 17 00:00:00 2001 From: rain Date: Fri, 21 Aug 2026 17:29:47 -0400 Subject: [PATCH 2/5] llama-swap: require LLAMA_SWAP_API_KEY auth --- dot_pi/agent/extensions/llama-swap.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dot_pi/agent/extensions/llama-swap.ts b/dot_pi/agent/extensions/llama-swap.ts index f2a620a..924aa9c 100644 --- a/dot_pi/agent/extensions/llama-swap.ts +++ b/dot_pi/agent/extensions/llama-swap.ts @@ -357,7 +357,7 @@ export default async function (pi: ExtensionAPI) { "byte.llama", "byte.llama (byte.local GPU)", HOST.startsWith("byte") ? "http://localhost:9292/v1" : "http://byte.local:9292/v1", - "none", + "LLAMA_SWAP_API_KEY", ); // miche.llama — miche.local GPU (localhost when running on miche itself) @@ -366,6 +366,6 @@ export default async function (pi: ExtensionAPI) { "miche.llama", "miche.llama (miche.local GPU)", HOST.startsWith("miche") ? "http://localhost:9292/v1" : "http://miche.local:9292/v1", - "none", + "LLAMA_SWAP_API_KEY", ); } From fd4eb2a059948ea02412520e0768ca2baabdf0af Mon Sep 17 00:00:00 2001 From: rain Date: Fri, 21 Aug 2026 17:29:47 -0400 Subject: [PATCH 3/5] pi: add powerline-footer and themes-bundle, fold runtime settings --- dot_pi/agent/settings.json.tmpl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dot_pi/agent/settings.json.tmpl b/dot_pi/agent/settings.json.tmpl index 8435ef3..e655e30 100644 --- a/dot_pi/agent/settings.json.tmpl +++ b/dot_pi/agent/settings.json.tmpl @@ -19,7 +19,9 @@ "npm:@danielmeneses/pi-llama-swap", "npm:pi-llama-cpp-stats", "https://github.com/firecrawl/pi-firecrawl", - "git:github.com/davebcn87/pi-autoresearch" + "git:github.com/davebcn87/pi-autoresearch", + "npm:pi-powerline-footer", + "npm:@firstpick/pi-themes-bundle" ], "enableInstallTelemetry": false, "subagents": { @@ -53,4 +55,4 @@ } }, "hideThinkingBlock": true -} \ No newline at end of file +} From 6c3a47f3504131db94dfec99cd5fc474ae6e0633 Mon Sep 17 00:00:00 2001 From: rain Date: Fri, 21 Aug 2026 17:29:47 -0400 Subject: [PATCH 4/5] pi: manage caveman-native extension via chezmoi template --- .../agent/extensions/caveman-native.js.tmpl | 789 ++++++++++++++++++ 1 file changed, 789 insertions(+) create mode 100644 dot_pi/agent/extensions/caveman-native.js.tmpl diff --git a/dot_pi/agent/extensions/caveman-native.js.tmpl b/dot_pi/agent/extensions/caveman-native.js.tmpl new file mode 100644 index 0000000..dbf7d1a --- /dev/null +++ b/dot_pi/agent/extensions/caveman-native.js.tmpl @@ -0,0 +1,789 @@ +// caveman:native-pi — GENERATED by `caveman enable pi`. +process.env.CAVEMAN_PI_HOOK_CMD ??= "[\"{{ .chezmoi.homeDir }}/.npm-global/bin/caveman\"]"; +// ../pi-extension/src/index.ts +import { readFileSync as readFileSync2 } from "node:fs"; +import { homedir as homedir3 } from "node:os"; +import { join as join4 } from "node:path"; +import { Type } from "typebox"; + +// ../pi-extension/src/lifecycle.ts +import { execFile } from "node:child_process"; +import { createHash } from "node:crypto"; +import { homedir } from "node:os"; +import { join as join2 } from "node:path"; + +// ../pi-extension/src/portable-command.ts +import { existsSync, readFileSync, statSync } from "node:fs"; +import { dirname, extname, isAbsolute, join, resolve } from "node:path"; +function parseWindowsNodeShim(source) { + for (const line of source.split(/\r?\n/)) { + if (!/(?:\bnode(?:\.exe)?\b|_prog)/i.test(line) || !/%\*/.test(line)) continue; + const match = line.match(/"%(?:dp0%|~dp0)\\([^"\r\n]+\.(?:cjs|mjs|js))"\s+%\*/i) ?? line.match(/"([A-Za-z]:[\\/][^"\r\n]+\.(?:cjs|mjs|js))"\s+%\*/i); + if (match) return match[1]; + } + return null; +} +function envValue(env, name) { + const key = Object.keys(env).find((candidate) => candidate.toLowerCase() === name.toLowerCase()); + return key === void 0 ? void 0 : env[key]; +} +function resolveWindowsCommand(command, env) { + const pathExt = envValue(env, "PATHEXT") ?? ".COM;.EXE;.BAT;.CMD"; + const names = extname(command) ? [command] : pathExt.split(";").map((extension) => `${command}${extension.startsWith(".") ? extension : `.${extension}`}`); + if (isAbsolute(command) || /[\\/]/.test(command)) { + for (const name of names) if (existsSync(name)) return name; + return existsSync(command) ? command : void 0; + } + for (const directory of (envValue(env, "PATH") ?? "").split(";")) { + if (!directory) continue; + for (const name of names) { + const candidate = join(directory, name); + if (existsSync(candidate)) return candidate; + } + } + return void 0; +} +function portableInvocation(command, args, platform = process.platform, env = process.env) { + if (platform !== "win32") return { command, args: [...args] }; + const executable = resolveWindowsCommand(command, env) ?? command; + if (!/\.(?:cmd|bat)$/i.test(executable)) return { command: executable, args: [...args] }; + const stat = statSync(executable); + if (!stat.isFile() || stat.size > 256 * 1024) { + throw new Error(`cannot safely launch Windows command shim: ${executable}`); + } + const shimScript = parseWindowsNodeShim(readFileSync(executable, "utf8")); + if (!shimScript) { + throw new Error(`cannot safely launch non-Node Windows command shim: ${executable}; install a native .exe`); + } + const script = /^[A-Za-z]:[\\/]/.test(shimScript) ? shimScript : resolve(dirname(executable), ...shimScript.split(/[\\/]+/)); + if (!statSync(script).isFile()) { + throw new Error(`Windows command shim target is missing: ${script}`); + } + return { command: process.execPath, args: [script, ...args] }; +} + +// ../pi-extension/src/protocol.ts +var MAX_CONTEXT_BYTES = 64 * 1024; +var MAX_MESSAGE_BYTES = 4 * 1024; +var MAX_OUTPUT_REPLACEMENT_BYTES = 2 * 1024 * 1024; +var MAX_RECOVERY_REF_BYTES = 1024; +var MAX_DECISION_ID_BYTES = 256; +var MAX_TOOL_OUTPUT_BYTES = 2 * 1024 * 1024; +function fits(value, max) { + return typeof value === "string" && Buffer.byteLength(value, "utf8") <= max; +} +function sanitizeHookResponse(raw) { + if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {}; + const value = raw; + const out = {}; + if (fits(value.context, MAX_CONTEXT_BYTES)) out.context = value.context; + if (fits(value.message, MAX_MESSAGE_BYTES)) out.message = value.message; + if (fits(value.output_replacement, MAX_OUTPUT_REPLACEMENT_BYTES)) out.output_replacement = value.output_replacement; + if (fits(value.recovery_ref, MAX_RECOVERY_REF_BYTES)) out.recovery_ref = value.recovery_ref; + if (fits(value.decision_id, MAX_DECISION_ID_BYTES)) out.decision_id = value.decision_id; + if (typeof value.action === "string") out.action = value.action; + const hso = value.hookSpecificOutput; + if (hso && typeof hso === "object" && !Array.isArray(hso)) { + const nested = hso; + const cleaned = {}; + if (typeof nested.hookEventName === "string") cleaned.hookEventName = nested.hookEventName; + if (fits(nested.additionalContext, MAX_CONTEXT_BYTES)) cleaned.additionalContext = nested.additionalContext; + if (fits(nested.updatedToolOutput, MAX_OUTPUT_REPLACEMENT_BYTES)) cleaned.updatedToolOutput = nested.updatedToolOutput; + out.hookSpecificOutput = cleaned; + } + return out; +} +function additionalContextOf(response) { + const context = response?.hookSpecificOutput?.additionalContext ?? response?.context; + return context ? context : void 0; +} +function outputReplacementOf(response) { + const replacement = response?.hookSpecificOutput?.updatedToolOutput ?? response?.output_replacement; + return replacement ? replacement : void 0; +} +var ROUTES_BY_API = { + "anthropic-messages": "/w/pi", + "openai-completions": "/w/pi/openai/v1", + "openai-responses": "/w/pi/openai/v1", + "google-generative-ai": "/w/pi/v1beta" +}; +function routeForApi(gateway, api) { + const path = api ? ROUTES_BY_API[api] : void 0; + return path ? joinUrl(gateway, path) : void 0; +} +function joinUrl(base, path) { + return `${base.replace(/\/+$/, "")}${path}`; +} +function isLoopbackUrl(url) { + try { + const host = new URL(url).hostname; + return host === "127.0.0.1" || host === "::1" || host === "[::1]" || host === "localhost"; + } catch { + return false; + } +} + +// ../pi-extension/src/lifecycle.ts +var HOOK_TIMEOUT_MS = 2e3; +var SESSION_START_TIMEOUT_MS = 6e3; +var HOOK_MAX_BUFFER = 2 * 1024 * 1024; +function resolveHookInvocations(env = process.env) { + const localBin = join2(env.CAVEMAN_HOME || join2(homedir(), ".caveman"), "bin", "caveman"); + const candidates = [ + { command: "caveman", args: [] }, + { command: "cave", args: [] }, + { command: localBin, args: [] } + ]; + const stamped = env.CAVEMAN_PI_HOOK_CMD; + if (stamped) { + try { + const parsed = JSON.parse(stamped); + if (Array.isArray(parsed) && parsed.length > 0 && parsed.every((part) => typeof part === "string" && part.length > 0)) { + candidates.unshift({ command: parsed[0], args: parsed.slice(1) }); + } + } catch { + } + } + return candidates; +} +var TRY_NEXT = "__caveman_try_next__"; +var HookBridge = class { + invocations; + constructor(invocations = resolveHookInvocations()) { + this.invocations = invocations; + } + // call runs one native-hook event. Fail-open by contract: any spawn error, + // timeout, or unparseable output resolves to undefined. + async call(event, payload) { + const input = JSON.stringify({ event_name: event, surface: "cli", ...payload }); + for (const invocation of this.invocations) { + const raw = await this.invoke(invocation, event, input); + if (raw === TRY_NEXT) continue; + if (raw === void 0 || !raw.trim()) return void 0; + try { + return sanitizeHookResponse(JSON.parse(raw)); + } catch { + return void 0; + } + } + return void 0; + } + invoke(invocation, event, input) { + return new Promise((resolve2) => { + let portable; + try { + portable = portableInvocation(invocation.command, [...invocation.args, "native-hook", "pi", event]); + } catch { + resolve2(TRY_NEXT); + return; + } + const child = execFile( + portable.command, + portable.args, + { timeout: event === "SessionStart" ? SESSION_START_TIMEOUT_MS : HOOK_TIMEOUT_MS, maxBuffer: HOOK_MAX_BUFFER, encoding: "utf8" }, + (error, stdout) => { + const timedOut = !!error && error.killed === true; + if (error && stdout === "") return resolve2(timedOut ? void 0 : TRY_NEXT); + resolve2(stdout); + } + ); + child.stdin?.on("error", () => { + }); + child.stdin?.end(input); + }); + } +}; +function promptDigest(prompt) { + return { + bytes: Buffer.byteLength(prompt, "utf8"), + sha256: `sha256:${createHash("sha256").update(prompt, "utf8").digest("hex")}` + }; +} +function taskType(text) { + const lower = text.toLowerCase(); + const has = (...terms) => terms.some((term) => lower.includes(term)); + if (has("migration", "migrate", "schema change", "backfill", "rollback")) return "migration"; + if (has("bug", "fix", "broken", "regression", "crash", "error", "incorrect")) return "bugfix"; + if (has("investigate", "diagnose", "root cause", "why does", "trace")) return "investigation"; + if (has("refactor", "restructure", "reorganize", "cleanup")) return "refactor"; + if (has("review", "audit", "critique", "assess")) return "review"; + if (has("verify", "verification", "prove", "validate", "check that")) return "verification"; + if (has("build", "implement", "add", "create", "ship", "feature")) return "feature"; + return "general"; +} +var TASK_STOPWORDS = /* @__PURE__ */ new Set(["about", "after", "agent", "before", "build", "change", "code", "create", "from", "have", "help", "implement", "into", "make", "please", "project", "repository", "should", "spec", "task", "that", "then", "there", "these", "they", "this", "through", "user", "want", "what", "when", "where", "which", "with", "would", "your"]); +function taskTerms(text) { + const out = []; + const seen = /* @__PURE__ */ new Set(); + for (const raw of text.match(/[A-Za-z][A-Za-z0-9_./-]{2,63}/g) ?? []) { + const term = raw.toLowerCase().replace(/^[-./]+|[-./]+$/g, ""); + if (!term || term.includes("..") || TASK_STOPWORDS.has(term) || seen.has(term)) continue; + if (/^(?:sk|pk|rk|ghp|github_pat|xox[baprs]|akia)[-_]/i.test(term) || /^[a-z0-9_-]{40,}$/i.test(term)) continue; + seen.add(term); + out.push(term); + if (out.length === 12) break; + } + return out; +} +function taskContinuation(text) { + const prompt = text.trim().toLowerCase(); + if (!prompt || prompt.length > 160 || prompt.split(/\s+/).length > 14) return false; + return /^(?:please\s+)?(?:continue|go ahead|keep going|proceed|do (?:it|that)|fix (?:it|that)|retry|try again|explain (?:it|that)|what do you mean|yes|yep|yeah|why\??|how\??)[.!?\s]*$/.test(prompt); +} + +// ../pi-extension/src/provider.ts +var ProviderRouter = class { + pi; + notify; + gateway; + gateOpen = false; + overridden = /* @__PURE__ */ new Set(); + applying = false; + warnedModels = /* @__PURE__ */ new Set(); + constructor(pi, notify) { + this.pi = pi; + this.notify = notify; + } + // openGate is called once per session after the recovery gate held. Refuses + // non-loopback gateways: managed routing needs auth proof v1 does not carry. + async openGate(gateway, ctx) { + if (!isLoopbackUrl(gateway)) { + this.notify("Caveman: direct mode, no compression this session (gateway is not loopback)", "warning"); + return; + } + this.gateway = gateway; + this.gateOpen = true; + await this.apply(ctx.model, ctx); + } + closeGate() { + this.gateOpen = false; + this.clearOverrides(); + } + routing() { + return this.gateOpen && this.overridden.size > 0; + } + // apply routes one model's provider, or restores direct mode when the model + // has no verified route. Called from the gate and from model_select; the + // applying flag swallows the model_select echo of our own setModel call. + async apply(model, ctx) { + if (!this.gateOpen || this.applying || !this.gateway) return; + if (!model) return; + const route = routeForApi(this.gateway, model.api); + let oauth = true; + try { + oauth = ctx.modelRegistry.isUsingOAuth(model); + } catch { + } + if (!route || oauth) { + this.clearOverrides(); + const key = `${model.provider}/${model.id}`; + if (!this.warnedModels.has(key)) { + this.warnedModels.add(key); + const reason = oauth ? "OAuth/subscription credentials are not routed" : `unsupported API "${model.api}"`; + this.notify(`Caveman: pass-through for ${key} (${reason}); no compression`, "warning"); + } + return; + } + this.applying = true; + try { + for (const provider of this.overridden) { + if (provider !== model.provider) { + this.pi.unregisterProvider(provider); + this.overridden.delete(provider); + } + } + this.pi.registerProvider(model.provider, { baseUrl: route }); + this.overridden.add(model.provider); + const refreshed = ctx.modelRegistry.find(model.provider, model.id); + if (!refreshed || !await this.pi.setModel(refreshed)) { + this.clearOverrides(); + this.notify("Caveman: direct mode, no compression this session (model re-resolution failed)", "warning"); + return; + } + } catch { + this.clearOverrides(); + this.notify("Caveman: direct mode, no compression this session (provider override failed)", "warning"); + } finally { + this.applying = false; + } + } + clearOverrides() { + for (const provider of this.overridden) { + try { + this.pi.unregisterProvider(provider); + } catch { + } + } + this.overridden.clear(); + } +}; + +// ../pi-extension/src/recovery.ts +import { execFile as execFile2, spawn } from "node:child_process"; +import { existsSync as existsSync2 } from "node:fs"; +import { homedir as homedir2 } from "node:os"; +import { delimiter, join as join3 } from "node:path"; +var PROBE_TIMEOUT_MS = 2e3; +var CALL_TIMEOUT_MS = 3e4; +var INIT_TIMEOUT_MS = 2e3; +function resolveMcpBinary(env = process.env) { + const explicit = env.CAVEMAN_MCP_BIN?.trim(); + if (explicit) return explicit; + const name = process.platform === "win32" ? "caveman-mcp.exe" : "caveman-mcp"; + for (const dir of (env.PATH ?? "").split(delimiter)) { + if (dir && existsSync2(join3(dir, name))) return join3(dir, name); + } + const local = join3(env.CAVEMAN_HOME || join3(homedir2(), ".caveman"), "bin", name); + return existsSync2(local) ? local : void 0; +} +var RecoveryClient = class { + binary; + child; + initialized = false; + probed; + ensuring; + nextId = 1; + pending = /* @__PURE__ */ new Map(); + buffer = ""; + disposed = false; + constructor(binary = resolveMcpBinary()) { + this.binary = binary; + } + // ensure probes the binary once and brings up an initialized child. Returns + // false (never throws) when recovery cannot hold — the caller passes through. + async ensure() { + if (this.disposed || !this.binary) return false; + if (this.child && this.initialized) return true; + this.ensuring ??= this.bringUp().finally(() => { + this.ensuring = void 0; + }); + return this.ensuring; + } + async bringUp() { + if (this.probed === void 0) this.probed = await this.probe(); + if (!this.probed || this.disposed) return false; + if (this.child && this.initialized) return true; + return this.start(); + } + ready() { + return Boolean(this.child && this.initialized && !this.disposed); + } + async retrieve(handle, query, signal) { + if (!await this.ensure()) { + return { text: JSON.stringify({ error: "cave_recovery_unavailable", message: "caveman-mcp is not available this session" }), isError: true }; + } + try { + const result = await this.call("tools/call", { + name: "caveman_retrieve", + arguments: { recovery_handle: handle, ...query ? { query } : {} } + }, signal); + const value = result; + const text = (value.content ?? []).map((block) => block.type === "text" && typeof block.text === "string" ? block.text : "").join(""); + return { text, isError: Boolean(value.isError) }; + } catch (error) { + return { text: JSON.stringify({ error: "cave_recovery_transport", message: error.message }), isError: true }; + } + } + dispose() { + this.disposed = true; + const child = this.child; + this.child = void 0; + this.initialized = false; + for (const [, entry] of this.pending) { + clearTimeout(entry.timer); + entry.reject(new Error("recovery client disposed")); + } + this.pending.clear(); + this.stop(child); + void this.ensuring?.then(() => { + const late = this.child; + this.child = void 0; + this.stop(late); + }); + } + // resolveMcpBinary appends .exe on win32, but CAVEMAN_MCP_BIN is an operator + // override that can name anything — including a .cmd shim, which execFile + // and spawn both refuse with EFTYPE. Route through the same resolver the hook + // bridge uses so the override is as spawnable as the default. Fail-closed: + // callers treat a throw as "recovery cannot hold" and pass through. + invocation(args) { + try { + return portableInvocation(this.binary, args); + } catch { + return void 0; + } + } + probe() { + return new Promise((resolve2) => { + const portable = this.invocation(["version", "--json"]); + if (!portable) return resolve2(false); + execFile2(portable.command, portable.args, { timeout: PROBE_TIMEOUT_MS, encoding: "utf8" }, (error, stdout) => { + if (error) return resolve2(false); + try { + const parsed = JSON.parse(stdout); + resolve2(Array.isArray(parsed?.capabilities) && parsed.capabilities.includes("mcp_recovery")); + } catch { + resolve2(false); + } + }); + }); + } + async start() { + let child; + try { + const portable = this.invocation([]); + if (!portable) return false; + child = spawn(portable.command, portable.args, { stdio: ["pipe", "pipe", "ignore"] }); + } catch { + return false; + } + this.child = child; + this.initialized = false; + this.buffer = ""; + child.on("error", () => this.onChildGone(child)); + child.on("exit", () => this.onChildGone(child)); + child.stdin?.on("error", () => this.onChildGone(child)); + child.stdout?.setEncoding("utf8"); + child.stdout?.on("data", (chunk) => this.onData(chunk)); + try { + await this.call("initialize", { + protocolVersion: "2024-11-05", + capabilities: {}, + clientInfo: { name: "caveman-pi-extension", version: "0.1.0" } + }, void 0, INIT_TIMEOUT_MS); + this.notify("notifications/initialized"); + if (this.disposed) { + this.onChildGone(child); + this.stop(child); + return false; + } + this.initialized = true; + return true; + } catch { + this.onChildGone(child); + this.stop(child); + return false; + } + } + stop(child) { + if (!child) return; + try { + child.stdin?.end(); + } catch { + } + const term = setTimeout(() => { + try { + child.kill("SIGTERM"); + } catch { + } + }, 500); + const kill = setTimeout(() => { + try { + child.kill("SIGKILL"); + } catch { + } + }, 2e3); + child.once("exit", () => { + clearTimeout(term); + clearTimeout(kill); + }); + } + onChildGone(child) { + if (this.child !== child) return; + this.child = void 0; + this.initialized = false; + for (const [, entry] of this.pending) { + clearTimeout(entry.timer); + entry.reject(new Error("caveman-mcp exited")); + } + this.pending.clear(); + } + onData(chunk) { + this.buffer += chunk; + let newline; + while ((newline = this.buffer.indexOf("\n")) !== -1) { + const line = this.buffer.slice(0, newline).trim(); + this.buffer = this.buffer.slice(newline + 1); + if (!line) continue; + let message; + try { + message = JSON.parse(line); + } catch { + continue; + } + if (typeof message.id !== "number") continue; + const entry = this.pending.get(message.id); + if (!entry) continue; + this.pending.delete(message.id); + clearTimeout(entry.timer); + if (message.error) entry.reject(new Error(message.error.message || "caveman-mcp error")); + else entry.resolve(message.result); + } + } + call(method, params, signal, timeoutMs = CALL_TIMEOUT_MS) { + const child = this.child; + if (!child?.stdin?.writable) return Promise.reject(new Error("caveman-mcp not running")); + if (signal?.aborted) return Promise.reject(new Error("retrieve cancelled")); + const id = this.nextId++; + return new Promise((resolve2, reject) => { + const timer = setTimeout(() => { + this.pending.delete(id); + reject(new Error(`caveman-mcp ${method} timed out`)); + }, timeoutMs); + timer.unref?.(); + const onAbort = () => { + this.pending.delete(id); + clearTimeout(timer); + reject(new Error("retrieve cancelled")); + }; + signal?.addEventListener("abort", onAbort, { once: true }); + this.pending.set(id, { + resolve: (value) => { + signal?.removeEventListener("abort", onAbort); + resolve2(value); + }, + reject: (error) => { + signal?.removeEventListener("abort", onAbort); + reject(error); + }, + timer + }); + child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n", (error) => { + if (error) { + this.pending.delete(id); + clearTimeout(timer); + signal?.removeEventListener("abort", onAbort); + reject(error); + } + }); + }); + } + notify(method) { + try { + this.child?.stdin?.write(JSON.stringify({ jsonrpc: "2.0", method }) + "\n"); + } catch { + } + } +}; + +// ../pi-extension/src/tool-output.ts +async function shrinkToolResult(bridge, sessionId, event) { + const text = event.content.map((block) => block.type === "text" && typeof block.text === "string" ? block.text : "").join(""); + if (!text) return void 0; + if (Buffer.byteLength(text, "utf8") > MAX_TOOL_OUTPUT_BYTES) return void 0; + const response = await bridge.call(event.isError ? "PostToolUseFailure" : "PostToolUse", { + session_id: sessionId, + tool_name: event.toolName, + tool_input: event.input, + tool_output: text + }); + const replacement = outputReplacementOf(response); + if (!replacement) return void 0; + return { content: [{ type: "text", text: replacement }, ...event.content.filter((block) => block.type !== "text")] }; +} + +// ../pi-extension/src/index.ts +var HEALTH_TIMEOUT_MS = 750; +var RECOVERY_TOOL = "caveman_retrieve"; +function cavemanHome() { + return process.env.CAVEMAN_HOME || join4(homedir3(), ".caveman"); +} +function gatewayUrl() { + const env = process.env.CAVE_GATEWAY_URL?.trim(); + if (env) return env; + try { + const config = JSON.parse(readFileSync2(join4(homedir3(), ".caveman-cloud", "config.json"), "utf8")); + if (typeof config.gatewayUrl === "string" && config.gatewayUrl) return config.gatewayUrl; + } catch { + } + return "http://127.0.0.1:8787"; +} +function runStateRecoveryViaMcp(gateway) { + let port; + try { + const url = new URL(gateway); + port = url.port || (url.protocol === "https:" ? "443" : "80"); + } catch { + return void 0; + } + try { + const state = JSON.parse(readFileSync2(join4(cavemanHome(), "run", `${port}.json`), "utf8")); + if (state?.schema !== "caveman.proxy.run.v1") return void 0; + if (typeof state.pid !== "number" || typeof state.instance_token !== "string") return void 0; + if (state.owner !== "wrap" && state.owner !== "start") return void 0; + try { + process.kill(state.pid, 0); + } catch { + return void 0; + } + return Boolean(state.recovery_via_mcp); + } catch { + return void 0; + } +} +async function proxyAliveOnce(gateway) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS); + try { + const response = await fetch(`${gateway.replace(/\/+$/, "")}/health/live`, { signal: controller.signal }); + await response.body?.cancel(); + return response.ok; + } catch { + return false; + } finally { + clearTimeout(timer); + } +} +async function proxyAlive(gateway) { + for (let attempt = 0; attempt < 3; attempt++) { + if (await proxyAliveOnce(gateway)) return true; + await new Promise((resolve2) => setTimeout(resolve2, 300)); + } + return false; +} +function index_default(pi) { + const bridge = new HookBridge(); + const recovery = new RecoveryClient(); + let router; + let sessionId = "default"; + let coreContext; + let pendingContext = []; + let pendingBytes = 0; + let gateDone = false; + const notify = (ctx, message, kind) => { + if (ctx?.hasUI) ctx.ui.notify(message, kind); + else process.stderr.write(`${message} +`); + }; + const debug = (message) => { + if (process.env.CAVEMAN_PI_DEBUG === "1") process.stderr.write(`[caveman-pi] ${message} +`); + }; + const guardedBase = (fn, label = fn.name || "handler") => (async (...args) => { + try { + debug(`enter ${label}`); + const result = await fn(...args); + debug(`exit ${label}`); + return result; + } catch (error) { + process.stderr.write(`caveman pi extension: ${error.message} +`); + return void 0; + } + }); + const GUARD = (label) => (fn) => guardedBase(fn, label); + const GUARD_session_start = GUARD("session_start"); + const GUARD_model_select = GUARD("model_select"); + const GUARD_before_agent_start = GUARD("before_agent_start"); + const GUARD_turn_start = GUARD("turn_start"); + const GUARD_turn_end = GUARD("turn_end"); + const GUARD_tool_call = GUARD("tool_call"); + const GUARD_tool_result = GUARD("tool_result"); + const GUARD_session_before_compact = GUARD("session_before_compact"); + const GUARD_session_compact = GUARD("session_compact"); + const GUARD_session_shutdown = GUARD("session_shutdown"); + pi.registerTool({ + name: RECOVERY_TOOL, + label: "Retrieve compressed context", + description: "Recover exact original content from a Caveman recovery handle.", + parameters: Type.Object({ + recovery_handle: Type.String({ description: "Exact ccr_ handle returned by Caveman or copied from a <> marker." }), + query: Type.Optional(Type.String({ description: "One broad description covering every detail needed from this handle." })) + }), + async execute(_toolCallId, params, signal) { + const result = await recovery.retrieve(params.recovery_handle, params.query, signal); + if (result.isError) throw new Error(result.text || "caveman_retrieve failed"); + return { content: [{ type: "text", text: result.text }], details: { recovery_handle: params.recovery_handle } }; + } + }); + pi.on("session_start", GUARD_session_start(async (_event, ctx) => { + router ??= new ProviderRouter(pi, (message, kind) => notify(ctx, message, kind)); + gateDone = false; + coreContext = void 0; + pendingContext = []; + pendingBytes = 0; + try { + sessionId = ctx.sessionManager.getSessionId() || "default"; + } catch { + sessionId = "default"; + } + const start = await bridge.call("SessionStart", { session_id: sessionId }); + coreContext = additionalContextOf(start); + if (!start) { + gateDone = true; + notify(ctx, "Caveman: direct mode, no compression this session (caveman native runtime unreachable)", "warning"); + return; + } + const gateway = gatewayUrl(); + const alive = await proxyAlive(gateway); + const published = runStateRecoveryViaMcp(gateway); + const recoveryReady = alive ? await recovery.ensure() : false; + gateDone = true; + if (!alive || published === void 0) { + notify(ctx, "Caveman: direct mode, no compression this session (local proxy not running)", "warning"); + return; + } + if (!published || !recoveryReady) { + notify(ctx, "Caveman: direct mode, no compression this session (recovery not available \u2014 run `caveman doctor pi`)", "warning"); + return; + } + await router.openGate(gateway, ctx); + })); + pi.on("model_select", GUARD_model_select(async (event, ctx) => { + if (gateDone) await router?.apply(event.model, ctx); + })); + pi.on("before_agent_start", GUARD_before_agent_start(async (event, ctx) => { + const response = await bridge.call("UserPromptSubmit", { + session_id: sessionId, + model: ctx.model?.id, + provider: ctx.model?.provider, + prompt: promptDigest(event.prompt), + task_type: taskType(event.prompt), + task_terms: taskTerms(event.prompt), + task_continuation: taskContinuation(event.prompt) + }); + const dynamic = [additionalContextOf(response), ...pendingContext].filter(Boolean); + pendingContext = []; + pendingBytes = 0; + if (!coreContext && dynamic.length === 0) return void 0; + const parts = [event.systemPrompt, coreContext, ...dynamic].filter(Boolean); + return { systemPrompt: parts.join("\n\n") }; + })); + pi.on("turn_start", GUARD_turn_start(() => { + void bridge.call("ModelBefore", { session_id: sessionId }); + })); + pi.on("turn_end", GUARD_turn_end(async () => { + await bridge.call("ModelAfter", { session_id: sessionId }); + void bridge.call("Stop", { session_id: sessionId }); + })); + pi.on("tool_call", GUARD_tool_call(async (event) => { + const response = await bridge.call("PreToolUse", { + session_id: sessionId, + tool_name: event.toolName, + tool_input: event.input + }); + const context = additionalContextOf(response); + const bytes = context ? Buffer.byteLength(context, "utf8") : 0; + if (context && pendingBytes + bytes <= MAX_CONTEXT_BYTES) { + pendingContext.push(context); + pendingBytes += bytes; + } + })); + pi.on("tool_result", GUARD_tool_result((event) => event.toolName === RECOVERY_TOOL ? void 0 : shrinkToolResult(bridge, sessionId, event))); + pi.on("session_before_compact", GUARD_session_before_compact(() => { + void bridge.call("PreCompact", { session_id: sessionId }); + })); + pi.on("session_compact", GUARD_session_compact(async () => { + const response = await bridge.call("PostCompact", { session_id: sessionId }); + const context = additionalContextOf(response); + if (context) coreContext = context; + })); + pi.on("session_shutdown", GUARD_session_shutdown(async () => { + await bridge.call("SessionEnd", { session_id: sessionId }); + router?.closeGate(); + recovery.dispose(); + })); +} +export { + index_default as default +}; From bacafdf715918429c4760ca0c6295d9df8eaba03 Mon Sep 17 00:00:00 2001 From: rain Date: Fri, 21 Aug 2026 17:29:47 -0400 Subject: [PATCH 5/5] fold omp runtime config and remaining pi extensions into source --- dot_omp/private_agent/mcp.json | 13 +- dot_omp/private_agent/private_config.yml | 8 +- dot_pi/agent/extensions/herdr-agent-state.ts | 394 +++++++++++++++++++ dot_pi/agent/extensions/private_notify.ts | 37 ++ 4 files changed, 446 insertions(+), 6 deletions(-) create mode 100644 dot_pi/agent/extensions/herdr-agent-state.ts create mode 100644 dot_pi/agent/extensions/private_notify.ts diff --git a/dot_omp/private_agent/mcp.json b/dot_omp/private_agent/mcp.json index 5525ba0..c92581f 100644 --- a/dot_omp/private_agent/mcp.json +++ b/dot_omp/private_agent/mcp.json @@ -3,11 +3,20 @@ "mcpServers": { "firecrawl": { "command": "npx", - "args": ["-y", "firecrawl-mcp"], + "args": [ + "-y", + "firecrawl-mcp" + ], "env": { "FIRECRAWL_API_KEY": "123", "FIRECRAWL_API_URL": "http://bazzite.local:3002" } + }, + "godot-mcp-pro": { + "command": "node", + "args": [ + "/home/melon/Desktop/godot-mcp-pro-v1.16.0/server/build/index.js" + ] } } -} +} \ No newline at end of file diff --git a/dot_omp/private_agent/private_config.yml b/dot_omp/private_agent/private_config.yml index e12d5fe..98c3579 100644 --- a/dot_omp/private_agent/private_config.yml +++ b/dot_omp/private_agent/private_config.yml @@ -29,18 +29,18 @@ symbolPreset: nerd theme: dark: dark-gruvbox light: light -setupVersion: 1 +setupVersion: 2 modelRoles: - task: llama-swap.byte/qwen36-27b-711 + task: llama-swap.miche/godoter-27b plan: zai-coding/glm-5.2:xhigh slow: deepseek/deepseek-v4-pro:high vision: llama-swap.byte/qwen36-35b-heretic-apex tiny: llama-swap.byte/gemma4-26b-hauhau - default: deepseek/deepseek-v4-flash:high smol: llama-swap.byte/kat-coder-v2.5-dev commit: llama-swap.byte/gemma4-26b-hauhau designer: zai-coding/glm-5.2 advisor: zai-coding/glm-5.2 + default: openrouter/stealth/ox-alpha:max retry: fallbackChains: default: @@ -73,5 +73,5 @@ tui: textSizing: false defaultThinkingLevel: auto personality: pragmatic -hideThinkingBlock: false +hideThinkingBlock: true readLineNumbers: true \ No newline at end of file diff --git a/dot_pi/agent/extensions/herdr-agent-state.ts b/dot_pi/agent/extensions/herdr-agent-state.ts new file mode 100644 index 0000000..9048911 --- /dev/null +++ b/dot_pi/agent/extensions/herdr-agent-state.ts @@ -0,0 +1,394 @@ +// installed by herdr +// managed by herdr; reinstalling or updating the integration overwrites this file. +// add custom hooks/plugins beside this file instead of editing it. +// HERDR_INTEGRATION_ID=pi +// HERDR_INTEGRATION_VERSION=4 +// @ts-nocheck + +import { createConnection } from "node:net"; + +const HERDR_ENV = process.env.HERDR_ENV; +const socketPath = process.env.HERDR_SOCKET_PATH; +const paneId = process.env.HERDR_PANE_ID; +const source = "herdr:pi"; + +function enabled() { + return HERDR_ENV === "1" && !!socketPath && !!paneId; +} + +function sendRequestAttempt(request: unknown, timeoutMs: number): Promise { + if (!enabled()) { + return Promise.resolve(true); + } + + return new Promise((resolve) => { + let done = false; + let timeout: ReturnType | undefined; + const finish = (delivered: boolean) => { + if (done) return; + done = true; + if (timeout) { + clearTimeout(timeout); + } + socket.destroy(); + resolve(delivered); + }; + + const socket = createConnection(socketPath!); + socket.on("error", () => finish(false)); + socket.on("connect", () => socket.write(`${JSON.stringify(request)}\n`)); + socket.on("data", () => finish(true)); + socket.on("end", () => finish(false)); + timeout = setTimeout(() => finish(false), timeoutMs); + timeout.unref?.(); + }); +} + +async function sendRequest(request: unknown): Promise { + if (await sendRequestAttempt(request, 500)) { + return; + } + await sendRequestAttempt(request, 1500); +} + +type AgentState = "working" | "blocked" | "idle"; + +type QueuedState = { + state: AgentState; + message?: string; + seq: number; +}; + +const idleDebounceMs = parseDurationEnv("HERDR_PI_IDLE_DEBOUNCE_MS", 250); +const retryGraceMs = parseDurationEnv("HERDR_PI_RETRY_GRACE_MS", 2500); +const retryableErrorPattern = + /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i; +let reportSeq = Date.now() * 1000; +let currentAgentSessionId: string | undefined; +let currentAgentSessionPath: string | undefined; + +function nextReportSeq(): number { + reportSeq += 1; + return reportSeq; +} + +function parseDurationEnv(name: string, fallback: number): number { + const raw = process.env[name]; + if (!raw) { + return fallback; + } + const parsed = Number.parseInt(raw, 10); + if (!Number.isFinite(parsed) || parsed < 0) { + return fallback; + } + return parsed; +} + +function updateSessionRef(ctx: any): void { + try { + const file = ctx?.sessionManager?.getSessionFile?.(); + currentAgentSessionPath = + typeof file === "string" && file.startsWith("/") ? file : undefined; + } catch { + currentAgentSessionPath = undefined; + } + + try { + const id = ctx?.sessionManager?.getSessionId?.(); + currentAgentSessionId = typeof id === "string" && id.length > 0 ? id : undefined; + } catch { + currentAgentSessionId = undefined; + } +} + +function withSessionRef(params: Record): Record { + if (currentAgentSessionPath) { + return { ...params, agent_session_path: currentAgentSessionPath }; + } + if (currentAgentSessionId) { + return { ...params, agent_session_id: currentAgentSessionId }; + } + return params; +} + +function currentSessionRef(): Record | undefined { + if (currentAgentSessionPath) { + return { agent_session_path: currentAgentSessionPath }; + } + if (currentAgentSessionId) { + return { agent_session_id: currentAgentSessionId }; + } + return undefined; +} + +function reportSession(): Promise { + const sessionRef = currentSessionRef(); + if (!sessionRef) { + return Promise.resolve(); + } + + return sendRequest({ + id: `${source}:session:${Date.now()}:${Math.random().toString(36).slice(2)}`, + method: "pane.report_agent_session", + params: { + pane_id: paneId, + source, + agent: "pi", + seq: nextReportSeq(), + ...sessionRef, + }, + }); +} + +function sendState(state: AgentState, message?: string, seq = nextReportSeq()): Promise { + return sendRequest({ + id: `${source}:${Date.now()}:${Math.random().toString(36).slice(2)}`, + method: "pane.report_agent", + params: withSessionRef({ + pane_id: paneId, + source, + agent: "pi", + state, + message, + seq, + }), + }); +} + +function releaseAgent(): Promise { + return sendRequest({ + id: `${source}:release:${Date.now()}:${Math.random().toString(36).slice(2)}`, + method: "pane.release_agent", + params: { + pane_id: paneId, + source, + agent: "pi", + seq: nextReportSeq(), + }, + }); +} + +function shouldReleaseOnSessionShutdown(event: any): boolean { + // Pi tears down and rebinds extension runtimes for internal lifecycle actions + // such as /reload, /new, /resume, and /fork. Those do not mean the pane's + // agent process has exited, and releasing hook authority there can suppress + // legitimate reports from the replacement runtime. Only a user/process quit + // should release Herdr's full-lifecycle authority. + const reason = event?.reason; + return reason === "quit"; +} + +let sendInFlight = false; +let queuedState: QueuedState | undefined; + +function queueState(state: AgentState, message?: string): void { + queuedState = { state, message, seq: nextReportSeq() }; + if (!sendInFlight) { + void drainStateQueue(); + } +} + +async function drainStateQueue(): Promise { + if (sendInFlight) { + return; + } + + sendInFlight = true; + try { + while (queuedState) { + const next = queuedState; + queuedState = undefined; + await sendState(next.state, next.message, next.seq); + } + } finally { + sendInFlight = false; + if (queuedState) { + void drainStateQueue(); + } + } +} + +function lastAssistantMessage(messages: unknown[]): any | undefined { + for (let i = messages.length - 1; i >= 0; i -= 1) { + const message = messages[i] as any; + if (message?.role === "assistant") { + return message; + } + } + return undefined; +} + +function retryableErrorMessage(event: any): string | undefined { + const messages = Array.isArray(event?.messages) ? event.messages : []; + const assistant = lastAssistantMessage(messages); + if (assistant?.stopReason !== "error") { + return undefined; + } + + const errorMessage = String(assistant.errorMessage ?? ""); + if (!retryableErrorPattern.test(errorMessage)) { + return undefined; + } + return errorMessage || "retryable provider error"; +} + +export default function (pi) { + if (!enabled()) { + return; + } + + let agentActive = false; + let retryHoldActive = false; + let failureBlocked = false; + let failureMessage: string | undefined; + let blockedCount = 0; + let blockedMessage: string | undefined; + let lastState: AgentState | undefined; + let lastMessage: string | undefined; + let idleTimer: ReturnType | undefined; + let retryTimer: ReturnType | undefined; + let rootSession = false; + + function clearTimer(timer: ReturnType | undefined) { + if (timer) { + clearTimeout(timer); + } + } + + function clearPendingTimers() { + clearTimer(idleTimer); + clearTimer(retryTimer); + idleTimer = undefined; + retryTimer = undefined; + } + + function clearFailureState() { + retryHoldActive = false; + failureBlocked = false; + failureMessage = undefined; + } + + function desiredState() { + if (blockedCount > 0) { + return { state: "blocked" as const, message: blockedMessage }; + } + if (failureBlocked) { + return { state: "blocked" as const, message: failureMessage }; + } + if (agentActive || retryHoldActive) { + return { state: "working" as const, message: undefined }; + } + return { state: "idle" as const, message: undefined }; + } + + function publishState(force = false) { + const next = desiredState(); + if (!force && next.state === lastState && next.message === lastMessage) { + return; + } + lastState = next.state; + lastMessage = next.message; + queueState(next.state, next.message); + } + + function scheduleIdle() { + clearPendingTimers(); + clearFailureState(); + idleTimer = setTimeout(() => { + idleTimer = undefined; + publishState(); + }, idleDebounceMs); + idleTimer.unref?.(); + } + + function holdForRetry(message: string) { + clearPendingTimers(); + retryHoldActive = true; + failureBlocked = false; + failureMessage = message; + publishState(); + + retryTimer = setTimeout(() => { + retryTimer = undefined; + retryHoldActive = false; + failureBlocked = true; + publishState(); + }, retryGraceMs); + retryTimer.unref?.(); + } + + pi.events.on("herdr:blocked", (data) => { + if (!rootSession) { + return; + } + if (!data?.active) { + blockedCount = Math.max(0, blockedCount - 1); + if (blockedCount === 0) { + blockedMessage = undefined; + } + publishState(); + return; + } + + clearPendingTimers(); + blockedCount += 1; + blockedMessage = data.label; + publishState(); + }); + + pi.on("session_start", (_event, ctx) => { + if (ctx?.hasUI !== true) { + return; + } + rootSession = true; + updateSessionRef(ctx); + void reportSession(); + // A reload can replace this extension mid-run without emitting another agent_start. + agentActive = ctx?.isIdle?.() === false; + publishState(true); + }); + + pi.on("agent_start", (_event, ctx) => { + if (!rootSession) { + return; + } + updateSessionRef(ctx); + void reportSession(); + clearPendingTimers(); + clearFailureState(); + agentActive = true; + publishState(); + }); + + pi.on("agent_end", (event) => { + if (!rootSession) { + return; + } + if (!agentActive) { + // Pi can emit duplicate/late end events while auto-retry is already + // holding the pane in Working. Do not let an unqualified duplicate end + // cancel the retry hold and publish a false Idle. + return; + } + + agentActive = false; + + const retryableMessage = retryableErrorMessage(event); + if (retryableMessage) { + holdForRetry(retryableMessage); + return; + } + + scheduleIdle(); + }); + + pi.on("session_shutdown", async (event) => { + if (!rootSession) { + return; + } + clearPendingTimers(); + if (shouldReleaseOnSessionShutdown(event)) { + await releaseAgent(); + } + }); +} diff --git a/dot_pi/agent/extensions/private_notify.ts b/dot_pi/agent/extensions/private_notify.ts new file mode 100644 index 0000000..e21a67a --- /dev/null +++ b/dot_pi/agent/extensions/private_notify.ts @@ -0,0 +1,37 @@ +/** + * Pi Notify Extension (with audible bell) + * + * Fires a terminal bell + native desktop notification when Pi finishes. + * - Bell (\x07): tmux flags the window in the status bar + audible ding + * - OSC 777: Ghostty, iTerm2, WezTerm desktop notification popup + * - OSC 99: Kitty desktop notification popup + * + * Based on the official example from earendil-works/pi, with bell added. + */ +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; + +function notifyOSC777(title: string, body: string): void { + process.stdout.write(`\x1b]777;notify;${title};${body}\x07`); +} + +function notifyOSC99(title: string, body: string): void { + process.stdout.write(`\x1b]99;i=1:d=0;${title}\x1b\\`); + process.stdout.write(`\x1b]99;i=1:p=body;${body}\x1b\\`); +} + +function notify(title: string, body: string): void { + // Audible bell — tmux catches this, flags the window, terminal dings + process.stdout.write("\x07"); + // Desktop notification popup (OSC 777 for Ghostty, OSC 99 for Kitty) + if (process.env.KITTY_WINDOW_ID) { + notifyOSC99(title, body); + } else { + notifyOSC777(title, body); + } +} + +export default function (pi: ExtensionAPI) { + pi.on("agent_end", async () => { + notify("Pi", "Task complete — ready for input"); + }); +}