UNPKG

@phren/cli

Version:

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

110 lines (109 loc) • 5.55 kB
import { mcpResponse, resolveStoreForProject } from "./types.js"; import { z } from "zod"; import * as fs from "fs"; import * as path from "path"; import { runtimeDir } from "../shared.js"; import { recordFeedback, flushEntryScores, } from "../shared/governance.js"; import { upsertCanonical } from "../shared/content.js"; import { isValidProjectName } from "../utils.js"; import { storeAwareProjectPath } from "../store-routing.js"; /** truths.md path via the same store-aware routing upsertCanonical writes with. */ function truthsFilePath(phrenPath, project) { return storeAwareProjectPath(phrenPath, project, "truths.md") ?? path.join(phrenPath, project, "truths.md"); } export function register(server, ctx) { const { withWriteQueue, updateFileInIndex } = ctx; server.registerTool("pin_memory", { title: "◆ phren · pin memory", description: "Write a truth — a high-confidence, always-inject entry in truths.md that never decays.", inputSchema: z.object({ project: z.string().describe("Project name."), memory: z.string().describe("Truth text."), }), }, async ({ project: projectInput, memory }) => { let phrenPath; let project; try { const resolved = resolveStoreForProject(ctx, projectInput); phrenPath = resolved.phrenPath; project = resolved.project; } catch (err) { return mcpResponse({ ok: false, error: err instanceof Error ? err.message : String(err) }); } if (!isValidProjectName(project)) return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` }); return withWriteQueue(async () => { const result = upsertCanonical(phrenPath, project, memory); if (!result.ok) return mcpResponse({ ok: false, error: result.error }); updateFileInIndex(truthsFilePath(phrenPath, project)); return mcpResponse({ ok: true, message: result.data, data: { project, memory } }); }); }); server.registerTool("get_truths", { title: "◆ phren · truths", description: "Read all pinned truths for a project. Truths are high-confidence entries in truths.md that never decay.", inputSchema: z.object({ project: z.string().describe("Project name."), }), }, async ({ project: projectInput }) => { let phrenPath; let project; try { const resolved = resolveStoreForProject(ctx, projectInput, "read"); phrenPath = resolved.phrenPath; project = resolved.project; } catch (err) { return mcpResponse({ ok: false, error: err instanceof Error ? err.message : String(err) }); } if (!isValidProjectName(project)) return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` }); const truthsPath = truthsFilePath(phrenPath, project); if (!fs.existsSync(truthsPath)) { return mcpResponse({ ok: true, message: `No truths pinned for "${project}" yet.`, data: { project, truths: [], count: 0 } }); } const content = fs.readFileSync(truthsPath, "utf8"); const truths = content.split("\n") .filter((line) => line.startsWith("- ")) .map((line) => line.slice(2).trim()); return mcpResponse({ ok: true, message: truths.length > 0 ? `${truths.length} truth(s) pinned for "${project}".` : `No truths pinned for "${project}" yet.`, data: { project, truths, count: truths.length }, }); }); server.registerTool("memory_feedback", { title: "◆ phren · feedback", description: "Record feedback on whether an injected memory was helpful or noisy/regressive. " + "The key is printed in <phren-context> snippet headers as `fb:<project>/<file>:<digest>` — pass that token (with or without the `fb:` prefix). " + "Feedback adjusts that memory's ranking weight in future injections.", inputSchema: z.object({ key: z.string().describe("Feedback key from an injected snippet header, e.g. `fb:myproj/FINDINGS.md:a1b2c3d4e5f6` or `myproj/FINDINGS.md:a1b2c3d4e5f6`."), feedback: z.enum(["helpful", "reprompt", "regression"]).describe("Feedback type."), }), }, async ({ key, feedback }) => { const phrenPath = ctx.phrenPath; return withWriteQueue(async () => { recordFeedback(phrenPath, key, feedback); flushEntryScores(phrenPath); const feedbackWeights = { helpful: 1.0, reprompt: -0.5, regression: -1.0, }; const weight = feedbackWeights[feedback] ?? 0; // Write feedback audit to a dedicated file — NOT to scores.jsonl, which uses a // different schema ({key, delta, at}) and would crash readScoreJournal if polluted. const auditFile = path.join(runtimeDir(phrenPath), "feedback-audit.jsonl"); fs.mkdirSync(path.dirname(auditFile), { recursive: true }); const entry = { key, feedback, weight, timestamp: new Date().toISOString() }; fs.appendFileSync(auditFile, JSON.stringify(entry) + "\n"); return mcpResponse({ ok: true, message: `Recorded feedback ${feedback} for ${key} (weight: ${weight})`, data: { key, feedback, weight } }); }); }); }