UNPKG

@phren/cli

Version:

Knowledge layer for AI agents - CLI, MCP server, and data layer

64 lines (63 loc) • 3.57 kB
import * as fs from "node:fs"; import * as path from "node:path"; import { atomicWriteText } from "../phren-paths.js"; import { withFileLock } from "../governance/locks.js"; import { permissionDeniedError } from "../governance/rbac.js"; import { registeredStoreIdentity, registerStoreIdentity } from "../store-registry.js"; const file = (base) => path.join(base, ".config", "task-format.json"); /** The oldest writers that keep task metadata when they rewrite a task. */ export const taskFormatCurrentWriters = "Phren CLI, MCP and Hook 0.3.30, iOS build 177, Android 1.0.5"; export const taskFormatMigrationHint = `Task metadata is off for this store. Once every app and tool that writes it is current (${taskFormatCurrentWriters} or later), an owner can turn it on with phren task format enable --all-writers-compatible.`; /** An explicit store-owner acknowledgement, not inferred from one serving Hook. * It cannot fence an old binary that ignores this file: coordinated adoption of * every writer remains a prerequisite, and is stated at the activation boundary. */ export function taskFormatStatus(base) { try { const value = JSON.parse(fs.readFileSync(file(base), "utf8")); if (value?.version === 1 && value.allWritersCompatible === true && typeof value.storeId === "string" && value.storeId === registeredStoreIdentity(base) && typeof value.acknowledgedAt === "string" && Number.isFinite(Date.parse(value.acknowledgedAt))) { return { enabled: true, version: 1, acknowledgedAt: value.acknowledgedAt }; } } catch { /* Missing, malformed or unsupported activation remains disabled. */ } return { enabled: false, version: 1 }; } /** Wire status describes admission, never proof that old filesystem/Git writers * have been upgraded. Such writers do not consult our activation file. */ export function taskWriterSafety(base) { const status = taskFormatStatus(base); return { version: 1, metadataVersion: 1, activation: status.enabled ? "owner-acknowledged" : "disabled", requiresCoordinatedAdoption: true, legacyWritersFenced: false, ...(status.acknowledgedAt ? { acknowledgedAt: status.acknowledgedAt } : {}), }; } export function enableTaskFormat(base, allWritersCompatible) { const denied = permissionDeniedError(base, "manage_config"); if (denied) throw new Error(denied); if (allWritersCompatible !== true) throw new Error(taskFormatMigrationHint); // Enabling is the owner's explicit act on this store, so a legacy store // without a portable identity gets one here instead of needing a second command. writeTaskFormat(base, registerStoreIdentity(base)); } /** A store phren has just created has no old writers, so metadata starts on. */ export function initializeTaskFormat(base) { writeTaskFormat(base, registerStoreIdentity(base)); } function writeTaskFormat(base, storeId) { fs.mkdirSync(path.dirname(file(base)), { recursive: true }); withFileLock(file(base), () => { if (taskFormatStatus(base).enabled) return; // A future-format or malformed file is evidence, not permission to replace it. if (fs.existsSync(file(base))) throw new Error("Existing task-format.json is unsupported or belongs to another store. Review it before enabling metadata."); atomicWriteText(file(base), JSON.stringify({ version: 1, storeId, allWritersCompatible: true, acknowledgedAt: new Date().toISOString() }, null, 2) + "\n"); }); }