UNPKG

@phren/cli

Version:

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

499 lines (498 loc) • 22.4 kB
/** * Shared config schema — the single source of truth every surface renders from. * * The CLI (`phren config`), the Web UI settings tab, and the VS Code settings * webview all import these descriptors so their labels, help text, option lists, * defaults, and ranges can never drift apart. * * Each field is a {@link ConfigFieldDescriptor}; fields are grouped into the * eight {@link CONFIG_DOMAINS}. Nothing here renders UI — descriptors are pure * data so every surface can present them in its own idiom. */ import { DEFAULT_POLICY, DEFAULT_WORKFLOW_POLICY, DEFAULT_INDEX_POLICY, } from "../governance/policy.js"; // ── Reusable option lists ───────────────────────────────────────────────────── const PROACTIVITY_OPTIONS = [ { value: "high", label: "High", blurb: "The agent auto-captures eagerly without waiting for an explicit ask.", recommended: true, }, { value: "medium", label: "Medium", blurb: "The agent only auto-captures when your message has an explicit signal (e.g. \"remember this\").", }, { value: "low", label: "Low", blurb: "The agent never auto-captures — you drive every save yourself.", }, ]; // ── Domain definitions ──────────────────────────────────────────────────────── export const CONFIG_DOMAINS = [ { id: "proactivity", label: "Proactivity", icon: "pulse", summary: "How eagerly the agent acts on its own to capture memory.", scope: "global+project", fields: [ { key: "proactivity.base", domain: "proactivity", label: "Base proactivity", summary: "Master auto-capture level for both findings and tasks.", help: "Sets the default eagerness for everything the agent captures without being asked. " + "Findings and tasks each inherit this unless you override them below.", control: "enum", options: PROACTIVITY_OPTIONS, default: "high", scope: "global+project", impact: "Higher = more memories saved automatically; lower = you stay fully in control.", risk: "safe", }, { key: "proactivity.findings", domain: "proactivity", label: "Findings proactivity", summary: "Auto-capture level for findings specifically.", help: "Overrides the base level just for findings (insights, decisions, pitfalls). " + "Leave unset to inherit the base level.", control: "enum", options: PROACTIVITY_OPTIONS, default: "high", scope: "global+project", impact: "Controls how often the agent writes to FINDINGS.md on its own.", risk: "safe", }, { key: "proactivity.tasks", domain: "proactivity", label: "Tasks proactivity", summary: "Auto-capture level for tasks specifically.", help: "Overrides the base level just for tasks. Leave unset to inherit the base level.", control: "enum", options: PROACTIVITY_OPTIONS, default: "high", scope: "global+project", impact: "Controls how often the agent adds items to tasks.md on its own.", risk: "safe", }, ], }, { id: "taskMode", label: "Task mode", icon: "checklist", summary: "How much authority the agent has over your task list.", scope: "global+project", fields: [ { key: "taskMode", domain: "taskMode", label: "Task mode", summary: "What the agent is allowed to do with tasks.", help: "Governs whether the agent may create, complete, and reorder tasks for you, " + "or whether task management stays entirely in your hands.", control: "enum", options: [ { value: "auto", label: "Auto", blurb: "The agent freely creates, updates, and completes tasks as work progresses.", recommended: true, }, { value: "suggest", label: "Suggest", blurb: "The agent proposes task changes but waits for you to confirm.", }, { value: "manual", label: "Manual", blurb: "Only you create and change tasks; the agent never touches them.", }, { value: "off", label: "Off", blurb: "Task tracking is disabled entirely.", }, ], default: DEFAULT_WORKFLOW_POLICY.taskMode, scope: "global+project", impact: "`off` hides the task system completely; `auto` lets the agent run it.", risk: "caution", }, ], }, { id: "findingSensitivity", label: "Finding sensitivity", icon: "lightbulb", summary: "How selective the agent is about what counts as worth remembering.", scope: "global+project", fields: [ { key: "findingSensitivity", domain: "findingSensitivity", label: "Finding sensitivity", summary: "The bar for what the agent saves as a finding.", help: "Sets both the per-session cap on auto-captured findings and the instruction the " + "agent follows about what is worth keeping.", control: "enum", options: [ { value: "minimal", label: "Minimal", blurb: "Only save findings when the user explicitly asks you to remember something.", }, { value: "conservative", label: "Conservative", blurb: "Save decisions and pitfalls only — skip patterns and observations.", }, { value: "balanced", label: "Balanced", blurb: "Save non-obvious patterns, decisions, pitfalls, and bugs worth remembering next session.", recommended: true, }, { value: "aggressive", label: "Aggressive", blurb: "Save everything worth remembering — err on the side of capturing.", }, ], default: DEFAULT_WORKFLOW_POLICY.findingSensitivity, scope: "global+project", impact: "Higher = more findings per session (cap 0/3/10/20), larger context injection, more noise.", risk: "safe", }, ], }, { id: "retention", label: "Retention", icon: "archive", summary: "How long memory lives and how its confidence decays over time.", scope: "global+project", fields: [ { key: "retention.ttlDays", domain: "retention", label: "TTL (days)", summary: "Age at which a memory becomes eligible for pruning review.", help: "Findings older than this are considered for cleanup during consolidation. " + "It does not delete anything by itself.", control: "number", range: { min: 7, max: 3650, step: 1 }, default: DEFAULT_POLICY.ttlDays, scope: "global+project", impact: "Lower = memory turns over faster; higher = old findings linger longer.", risk: "safe", }, { key: "retention.retentionDays", domain: "retention", label: "Retention (days)", summary: "Hard age cutoff — findings past this are pruned.", help: "The maximum age a finding can reach before `phren maintain prune` removes it. " + "Shrinking this can permanently drop old memory.", control: "number", range: { min: 30, max: 3650, step: 1 }, default: DEFAULT_POLICY.retentionDays, scope: "global+project", impact: "Lowering this deletes findings older than the new cutoff on the next prune.", risk: "caution", }, { key: "retention.autoAcceptThreshold", domain: "retention", label: "Auto-accept threshold", summary: "Confidence score above which a finding skips the review queue.", help: "Findings scoring at or above this go straight in; lower-scoring ones land in the " + "review queue for you to approve.", control: "number", range: { min: 0, max: 1, step: 0.05 }, default: DEFAULT_POLICY.autoAcceptThreshold, scope: "global+project", impact: "Higher = more findings routed to manual review; lower = more auto-accepted.", risk: "safe", }, { key: "retention.minInjectConfidence", domain: "retention", label: "Min inject confidence", summary: "Lowest confidence a finding may have to be injected into context.", help: "Findings scoring below this are kept on disk but not surfaced into the agent's " + "context automatically.", control: "number", range: { min: 0, max: 1, step: 0.05 }, default: DEFAULT_POLICY.minInjectConfidence, scope: "global+project", impact: "Higher = only strong memories get injected; lower = weaker ones surface too.", risk: "safe", }, { key: "retention.decay.d30", domain: "retention", label: "Decay @ 30 days", summary: "Confidence multiplier applied to a finding 30 days old.", help: "Part of the decay curve — how much a 30-day-old finding's confidence is scaled.", control: "number", range: { min: 0, max: 1, step: 0.05 }, default: DEFAULT_POLICY.decay.d30, scope: "global+project", impact: "Lower values make recent memory fade faster.", risk: "safe", }, { key: "retention.decay.d60", domain: "retention", label: "Decay @ 60 days", summary: "Confidence multiplier applied to a finding 60 days old.", help: "Part of the decay curve — how much a 60-day-old finding's confidence is scaled.", control: "number", range: { min: 0, max: 1, step: 0.05 }, default: DEFAULT_POLICY.decay.d60, scope: "global+project", impact: "Lower values make mid-age memory fade faster.", risk: "safe", }, { key: "retention.decay.d90", domain: "retention", label: "Decay @ 90 days", summary: "Confidence multiplier applied to a finding 90 days old.", help: "Part of the decay curve — how much a 90-day-old finding's confidence is scaled.", control: "number", range: { min: 0, max: 1, step: 0.05 }, default: DEFAULT_POLICY.decay.d90, scope: "global+project", impact: "Lower values make older memory fade faster.", risk: "safe", }, { key: "retention.decay.d120", domain: "retention", label: "Decay @ 120 days", summary: "Confidence multiplier applied to a finding 120 days old.", help: "Part of the decay curve — how much a 120-day-old finding's confidence is scaled.", control: "number", range: { min: 0, max: 1, step: 0.05 }, default: DEFAULT_POLICY.decay.d120, scope: "global+project", impact: "Lower values make stale memory fade faster.", risk: "safe", }, ], }, { id: "workflow", label: "Workflow", icon: "settings-gear", summary: "Review gating and which memory sections are treated as risky.", scope: "global+project", fields: [ { key: "workflow.lowConfidenceThreshold", domain: "workflow", label: "Low-confidence threshold", summary: "Score below which a finding is flagged as low-confidence.", help: "Findings scoring under this are treated as low-confidence and may be routed to the " + "review queue rather than accepted silently.", control: "number", range: { min: 0, max: 1, step: 0.05 }, default: DEFAULT_WORKFLOW_POLICY.lowConfidenceThreshold, scope: "global+project", impact: "Higher = more findings flagged for review; lower = fewer flagged.", risk: "safe", }, { key: "workflow.riskySections", domain: "workflow", label: "Risky sections", summary: "Review-queue sections that require explicit attention.", help: "Findings landing in these sections are surfaced for review instead of being " + "treated as settled. Choose any of Review, Stale, Conflicts.", control: "string-list", options: [ { value: "Review", label: "Review", blurb: "Items explicitly queued for your review." }, { value: "Stale", label: "Stale", blurb: "Findings that have aged past their TTL." }, { value: "Conflicts", label: "Conflicts", blurb: "Findings that contradict another finding." }, ], default: DEFAULT_WORKFLOW_POLICY.riskySections, scope: "global+project", impact: "More sections = more findings held back for explicit attention.", risk: "safe", }, ], }, { id: "index", label: "Index", icon: "search", summary: "Which files the search indexer reads.", scope: "global-only", fields: [ { key: "index.includeGlobs", domain: "index", label: "Include globs", summary: "Glob patterns the indexer reads.", help: "Files matching any of these globs are indexed for search. Removing a pattern hides " + "those files from search until the next reindex.", control: "string-list", default: DEFAULT_INDEX_POLICY.includeGlobs, scope: "global-only", impact: "Narrowing this shrinks what search and context injection can find.", risk: "caution", }, { key: "index.excludeGlobs", domain: "index", label: "Exclude globs", summary: "Glob patterns the indexer skips.", help: "Files matching any of these globs are never indexed, even if they match an include " + "pattern. Excludes win over includes.", control: "string-list", default: DEFAULT_INDEX_POLICY.excludeGlobs, scope: "global-only", impact: "Adding a pattern removes those files from search on the next reindex.", risk: "caution", }, { key: "index.includeHidden", domain: "index", label: "Index hidden files", summary: "Whether dotfiles and dot-directories are indexed.", help: "When on, files and folders beginning with `.` are eligible for indexing.", control: "boolean", default: DEFAULT_INDEX_POLICY.includeHidden, scope: "global-only", impact: "Turning this on can pull in `.config` and similar files.", risk: "caution", }, ], }, { id: "topic", label: "Topics", icon: "symbol-namespace", summary: "Per-project topic classification for routing findings.", scope: "project-only", fields: [ { key: "topic.domain", domain: "topic", label: "Project domain", summary: "The high-level domain this project belongs to.", help: "A coarse classification (e.g. backend, frontend, data) used to tune how findings " + "are organised for this project.", control: "string-list", default: null, scope: "project-only", impact: "Affects how new findings are routed into topic files.", risk: "safe", }, { key: "topic.topics", domain: "topic", label: "Topics", summary: "Named topics findings are bucketed into for this project.", help: "Each topic has a slug, a label, an optional description, and keywords used to " + "match findings to it.", control: "object", default: [], scope: "project-only", impact: "Editing topics changes how this project's findings are grouped.", risk: "safe", }, ], }, { id: "access", label: "Access control", icon: "shield", summary: "Role-based permissions over who can read and write memory.", scope: "global+project", fields: [ { key: "access.admins", domain: "access", label: "Admins", summary: "Actors allowed every action, including config changes.", help: "Actor names (matched against PHREN_ACTOR) with full permissions. " + "When all three role lists are empty, phren runs in open mode.", control: "string-list", default: [], scope: "global+project", impact: "Admins can change config and manage findings, tasks, and access itself.", risk: "caution", }, { key: "access.contributors", domain: "access", label: "Contributors", summary: "Actors allowed to add and edit findings and tasks.", help: "Actor names with read-write access to findings and tasks but not config.", control: "string-list", default: [], scope: "global+project", impact: "Contributors can write memory but cannot change configuration.", risk: "caution", }, { key: "access.readers", domain: "access", label: "Readers", summary: "Actors allowed read-only access.", help: "Actor names that may search and read but never mutate memory.", control: "string-list", default: [], scope: "global+project", impact: "Readers can search and view but cannot add or edit anything.", risk: "caution", }, ], }, ]; // ── Lookups ─────────────────────────────────────────────────────────────────── const DOMAIN_BY_ID = new Map(CONFIG_DOMAINS.map((d) => [d.id, d])); const FIELD_BY_KEY = new Map(CONFIG_DOMAINS.flatMap((d) => d.fields.map((f) => [f.key, f]))); /** Look up a domain descriptor by id. */ export function getConfigDomain(id) { return DOMAIN_BY_ID.get(id); } /** Look up a field descriptor by its dotted key. */ export function getConfigField(key) { return FIELD_BY_KEY.get(key); } /** Every field across every domain, in domain order. */ export function allConfigFields() { return CONFIG_DOMAINS.flatMap((d) => d.fields); } /** * Aliases mapping the historical hyphenated `phren config` subcommands to the * canonical domain ids. Help text and routing are generated from this so the * CLI surface can never drift from the schema. */ export const CONFIG_DOMAIN_ALIASES = { proactivity: "proactivity", "task-mode": "taskMode", taskmode: "taskMode", "finding-sensitivity": "findingSensitivity", findingsensitivity: "findingSensitivity", policy: "retention", retention: "retention", workflow: "workflow", index: "index", topic: "topic", topics: "topic", access: "access", }; /** Resolve a user-supplied domain/subcommand token to a canonical domain id. */ export function resolveConfigDomainAlias(token) { return CONFIG_DOMAIN_ALIASES[token.trim().toLowerCase()]; }