Files
dotfiles/home/.pi/agent/extensions/calm/lib/preference.ts
T
Lap Tran 4c6e85c97c Switch firstmate to OpenCode 2 and refresh nix-darwin pins
Point fm at the isolated oc2 wrapper, upgrade Homebrew packages on rebuild, and declare ffmpeg, docker-desktop, puremac, utm, node, and tea. Update nixpkgs, home-manager, and nix-homebrew locks. Track Pi agent config, crew-dispatch, and the Gitea PR helper that home.nix already expected.
2026-09-18 13:58:49 -04:00

78 lines
2.6 KiB
TypeScript

// Pi Calm - persisted on/off preference.
//
// Copyright (c) 2026 Kun Chen. MIT License - see the LICENSE file in this directory.
//
// The preference lives in a plain local state file named "calm" directly under
// Pi's agent directory (~/.pi/agent by default, PI_CODING_AGENT_DIR when set).
// That directory is Pi runtime territory: this repository never tracks the
// state file and Home Manager never manages it. The file contains exactly
// "on\n" or "off\n"; anything else, including a missing or unreadable file,
// means off.
import { randomUUID } from "node:crypto";
import {
mkdirSync,
readFileSync,
renameSync,
rmSync,
writeFileSync,
} from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import * as PiCodingAgent from "@earendil-works/pi-coding-agent";
export const CALM_PREFERENCE_FILE_NAME = "calm";
/**
* Resolve Pi's agent directory through Pi's exported getAgentDir(), which
* honors PI_CODING_AGENT_DIR and tilde expansion. If a future Pi stops
* exporting it, fall back to the documented environment variable and default
* path instead of failing.
*/
export function calmAgentDir(): string {
if (typeof PiCodingAgent.getAgentDir === "function") return PiCodingAgent.getAgentDir();
const envDir = process.env.PI_CODING_AGENT_DIR?.trim();
if (envDir) return envDir;
return join(homedir(), ".pi", "agent");
}
export function calmPreferencePath(): string {
return join(calmAgentDir(), CALM_PREFERENCE_FILE_NAME);
}
/** Load the persisted preference. Calm is off by default and on any read error. */
export function loadCalmPreference(): boolean {
try {
return readFileSync(calmPreferencePath(), "utf8").trim() === "on";
} catch {
return false;
}
}
/**
* Persist the preference atomically (unique temp file plus rename) so a
* crashed write never leaves a truncated state file. A failure throws a clear
* error naming the path so /calm can surface it instead of silently applying
* a toggle that would not survive a restart.
*/
export function persistCalmPreference(active: boolean): void {
const path = calmPreferencePath();
try {
mkdirSync(dirname(path), { recursive: true });
const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
try {
writeFileSync(temporaryPath, active ? "on\n" : "off\n", {
encoding: "utf8",
flag: "wx",
mode: 0o600,
});
renameSync(temporaryPath, path);
} finally {
rmSync(temporaryPath, { force: true });
}
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
throw new Error(`Pi Calm could not persist its preference to ${path}: ${reason}`);
}
}