@phren/cli
Version:
Knowledge layer for AI agents - CLI, MCP server, and data layer
220 lines (219 loc) • 10.2 kB
JavaScript
import * as fs from "fs";
import { bootstrapPhrenDotEnv } from "./phren-dotenv.js";
import { debugLog, findPhrenPath } from "./phren-paths.js";
import { governanceInstallPreferencesFile, readInstallPreferences } from "./init/preferences.js";
import { errorMessage } from "./utils.js";
import { getWorkflowPolicy } from "./governance/policy.js";
export const PROACTIVITY_LEVELS = ["high", "medium", "low"];
const DEFAULT_PROACTIVITY_LEVEL = "high";
const EXPLICIT_FINDING_SIGNAL_PATTERN = /\b(add finding|worth remembering)\b/i;
const EXPLICIT_FINDING_TAG_PATTERN = /\[(pitfall|decision|pattern|bug|workaround|context)\]/i;
const EXPLICIT_TASK_SIGNAL_PATTERN = /\b(?:add(?:\s+(?:this|that|it))?\s+(?:to\s+(?:the\s+)?)?(?:task|todo(?:\s+list)?|task(?:\s+list)?)|add\s+(?:a\s+)?task|put(?:\s+(?:this|that|it))?\s+(?:in|on)\s+(?:the\s+)?(?:task|todo(?:\s+list)?|task(?:\s+list)?))\b/i;
export function parseProactivityLevel(raw) {
if (!raw)
return undefined;
const normalized = raw.trim().toLowerCase();
return PROACTIVITY_LEVELS.includes(normalized)
? normalized
: undefined;
}
function resolveProactivityPhrenPath(explicitPhrenPath) {
return explicitPhrenPath ?? findPhrenPath();
}
/** Read per-user preferences from ~/.phren/.users/<actor>/preferences.json. Actor from PHREN_ACTOR env var. */
function readUserPreferences(explicitPhrenPath) {
const phrenPath = resolveProactivityPhrenPath(explicitPhrenPath);
if (!phrenPath)
return {};
const actor = (process.env.PHREN_ACTOR || "").trim();
if (!actor || !/^[a-zA-Z0-9_@.-]{1,128}$/.test(actor))
return {};
// Sanitize actor name to safe path component (no path traversal)
const safeActor = actor.replace(/[^a-zA-Z0-9_@.-]/g, "_");
const prefsFile = `${phrenPath}/.users/${safeActor}/preferences.json`;
if (!fs.existsSync(prefsFile))
return {};
try {
const parsed = JSON.parse(fs.readFileSync(prefsFile, "utf8"));
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
return {};
return {
proactivity: parseProactivityLevel(parsed.proactivity),
proactivityFindings: parseProactivityLevel(parsed.proactivityFindings),
proactivityTask: parseProactivityLevel(parsed.proactivityTask),
findingSensitivity: ["minimal", "conservative", "balanced", "aggressive"].includes(String(parsed.findingSensitivity))
? parsed.findingSensitivity
: undefined,
};
}
catch (err) {
debugLog(`readUserPreferences: failed to parse ${prefsFile}: ${errorMessage(err)}`);
return {};
}
}
function readGovernanceProactivityPreferences(explicitPhrenPath) {
const phrenPath = resolveProactivityPhrenPath(explicitPhrenPath);
if (!phrenPath)
return {};
const filePath = governanceInstallPreferencesFile(phrenPath);
if (!fs.existsSync(filePath))
return {};
try {
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
return {
proactivity: parseProactivityLevel(parsed?.proactivity),
proactivityFindings: parseProactivityLevel(parsed?.proactivityFindings),
proactivityTask: parseProactivityLevel(parsed?.proactivityTask),
};
}
catch (err) {
debugLog(`readGovernanceProactivityPreferences: failed to parse ${filePath}: ${errorMessage(err)}`);
}
return {};
}
function getConfiguredProactivityDefault(explicitPhrenPath) {
// Resolution chain: user prefs (highest) → governance prefs → install prefs → default
const userPrefs = readUserPreferences(explicitPhrenPath);
if (userPrefs.proactivity)
return userPrefs.proactivity;
const governancePreference = readGovernanceProactivityPreferences(explicitPhrenPath).proactivity;
if (governancePreference)
return governancePreference;
const phrenPath = resolveProactivityPhrenPath(explicitPhrenPath);
if (phrenPath) {
const runtimePreference = parseProactivityLevel(readInstallPreferences(phrenPath).proactivity);
if (runtimePreference)
return runtimePreference;
}
return DEFAULT_PROACTIVITY_LEVEL;
}
/** Map findingSensitivity from workflow-policy.json to a ProactivityLevel. */
function sensitivityToProactivity(sensitivity) {
switch (sensitivity) {
case "minimal": return "low";
case "conservative": return "medium";
case "balanced": return "high";
case "aggressive": return "high";
default: return undefined;
}
}
function getWorkflowPolicySensitivityLevel(explicitPhrenPath) {
const phrenPath = resolveProactivityPhrenPath(explicitPhrenPath);
if (!phrenPath)
return undefined;
try {
const policy = getWorkflowPolicy(phrenPath);
return sensitivityToProactivity(policy.findingSensitivity);
}
catch {
return undefined;
}
}
function getConfiguredProactivityLevelForFindingsDefault(explicitPhrenPath) {
// User prefs take priority over governance prefs
const userPrefs = readUserPreferences(explicitPhrenPath);
if (userPrefs.proactivityFindings)
return userPrefs.proactivityFindings;
if (userPrefs.proactivity)
return userPrefs.proactivity;
const prefs = readGovernanceProactivityPreferences(explicitPhrenPath);
return prefs.proactivityFindings
?? prefs.proactivity
?? getWorkflowPolicySensitivityLevel(explicitPhrenPath)
?? getConfiguredProactivityDefault(explicitPhrenPath);
}
function getConfiguredProactivityLevelForTaskDefault(explicitPhrenPath) {
// User prefs take priority over governance prefs
const userPrefs = readUserPreferences(explicitPhrenPath);
if (userPrefs.proactivityTask)
return userPrefs.proactivityTask;
if (userPrefs.proactivity)
return userPrefs.proactivity;
const prefs = readGovernanceProactivityPreferences(explicitPhrenPath);
return prefs.proactivityTask
?? prefs.proactivity
?? getWorkflowPolicySensitivityLevel(explicitPhrenPath)
?? getConfiguredProactivityDefault(explicitPhrenPath);
}
function resolveProactivityLevel(raw, fallback) {
return parseProactivityLevel(raw) ?? fallback;
}
export function getProactivityLevel(explicitPhrenPath) {
bootstrapPhrenDotEnv();
return resolveProactivityLevel(process.env.PHREN_PROACTIVITY, getConfiguredProactivityDefault(explicitPhrenPath));
}
export function getProactivityLevelForFindings(explicitPhrenPath) {
bootstrapPhrenDotEnv();
const findingsPreference = parseProactivityLevel(process.env.PHREN_PROACTIVITY_FINDINGS);
if (findingsPreference)
return findingsPreference;
return resolveProactivityLevel(process.env.PHREN_PROACTIVITY, getConfiguredProactivityLevelForFindingsDefault(explicitPhrenPath));
}
export function getProactivityLevelForTask(explicitPhrenPath) {
bootstrapPhrenDotEnv();
const taskPreference = parseProactivityLevel(process.env.PHREN_PROACTIVITY_TASKS);
if (taskPreference)
return taskPreference;
return resolveProactivityLevel(process.env.PHREN_PROACTIVITY, getConfiguredProactivityLevelForTaskDefault(explicitPhrenPath));
}
export function hasExplicitFindingSignal(...texts) {
return texts.some((text) => {
if (!text)
return false;
return EXPLICIT_FINDING_SIGNAL_PATTERN.test(text) || EXPLICIT_FINDING_TAG_PATTERN.test(text);
});
}
export function hasExplicitTaskSignal(...texts) {
return texts.some((text) => {
if (!text)
return false;
return EXPLICIT_TASK_SIGNAL_PATTERN.test(text);
});
}
export function shouldAutoCaptureFindingsForLevel(level, ...texts) {
if (level === "high")
return true;
if (level === "low")
return false;
return hasExplicitFindingSignal(...texts);
}
export function shouldAutoCaptureTaskForLevel(level, ...texts) {
if (level === "high")
return true;
if (level === "low")
return false;
return hasExplicitTaskSignal(...texts);
}
// ── Intent-aware auto task signals ──────────────────────────────────────────
const EXECUTION_INTENT_PATTERN = /\b(?:yes\s+do\s+it|do\s+it|go\s+ahead|work\s+on\s+(?:these|this|that|it)|let(?:'|')s\s+(?:build|ship|implement|start|do|go)|approve(?:d)?|ship\s+it|merge\s+it|commit|proceed|execute|deploy|push\s+it|make\s+it\s+happen|get\s+(?:it|this|that)\s+done)\b/i;
const DISCOVERY_INTENT_PATTERN = /\b(?:brainstorm|explore|what\s+if|ideas?\b|review\s+findings|think\s+about|consider|hypothetical|might\s+we|could\s+we\s+try|what\s+are\s+(?:the|some)\s+(?:options|alternatives|possibilities)|pros?\s+(?:and|&)\s+cons?|compare|evaluate|assess|weigh)\b/i;
export function hasExecutionIntent(...texts) {
return texts.some((text) => {
if (!text)
return false;
return EXECUTION_INTENT_PATTERN.test(text);
});
}
export function hasDiscoveryIntent(...texts) {
return texts.some((text) => {
if (!text)
return false;
return DISCOVERY_INTENT_PATTERN.test(text);
});
}
const SUPPRESS_TASK_PATTERN = /\b(?:don(?:'|\u2019)t\s+(?:create|add|track|make)\s+(?:a\s+)?task|no\s+task|skip\s+(?:the\s+)?task|don(?:'|\u2019)t\s+add\s+(?:that|this|those)\s+to\s+task|do\s+not\s+add|not\s+a\s+task|just\s+(?:a\s+)?question|just\s+asking|hypothetically|just\s+curious|ignore\s+(?:this|that))\b/i;
export function hasSuppressTaskIntent(...texts) {
return texts.some((text) => {
if (!text)
return false;
return SUPPRESS_TASK_PATTERN.test(text);
});
}
const CODE_CHANGE_PATTERN = /\b(?:git\s+(?:diff|status|add|commit|log)|npm\s+(?:run|test|build)|(?:edit|fix|change|update|modify)\s+(?:the\s+)?(?:file|code|function|method|class)|diff|patch|pull\s+request|pr\s+#?\d+|changed\s+file|modified\s+file|deleted\s+file|added\s+file|renamed\s+file)\b/i;
export function hasCodeChangeContext(...texts) {
return texts.some((text) => {
if (!text)
return false;
return CODE_CHANGE_PATTERN.test(text);
});
}