feat: add first-class memory management

Expose gateway memory APIs for status, init, files, search, and sync.
Align pi-memory-md with project-scoped, local-first memory behavior.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Harivansh Rathi 2026-03-08 12:47:38 -07:00
parent df702d95a3
commit 2886855706
4 changed files with 1437 additions and 74 deletions

View file

@ -6,15 +6,21 @@ import { Text } from "@mariozechner/pi-tui";
import { Type } from "@sinclair/typebox";
import type { MemoryFrontmatter, MemoryMdSettings } from "./memory-md.js";
import {
createDefaultFiles,
ensureDirectoryStructure,
getCurrentDate,
getMemoryDir,
getProjectRepoPath,
gitExec,
listMemoryFiles,
readMemoryFile,
resolveMemoryPath,
syncRepository,
writeMemoryFile,
} from "./memory-md.js";
type MemorySettingsGetter = () => MemoryMdSettings;
function renderWithExpandHint(
text: string,
theme: Theme,
@ -34,7 +40,7 @@ function renderWithExpandHint(
export function registerMemorySync(
pi: ExtensionAPI,
settings: MemoryMdSettings,
getSettings: MemorySettingsGetter,
isRepoInitialized: { value: boolean },
): void {
pi.registerTool({
@ -52,26 +58,73 @@ export function registerMemorySync(
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const { action } = params as { action: "pull" | "push" | "status" };
const localPath = settings.localPath!;
const settings = getSettings();
const localPath = settings.localPath;
const memoryDir = getMemoryDir(settings, ctx);
const projectRepoPath = getProjectRepoPath(settings, ctx);
const coreUserDir = path.join(memoryDir, "core", "user");
const configured = Boolean(settings.repoUrl);
const initialized = fs.existsSync(coreUserDir);
const repoReady = Boolean(
localPath && fs.existsSync(path.join(localPath, ".git")),
);
if (action === "status") {
const initialized =
isRepoInitialized.value && fs.existsSync(coreUserDir);
if (!initialized) {
return {
content: [
{
type: "text",
text: "Memory repository not initialized. Use memory_init to set up.",
text: "Memory not initialized. Use memory_init to set up.",
},
],
details: { initialized: false },
details: { initialized: false, configured, dirty: null },
};
}
const result = await gitExec(pi, localPath, "status", "--porcelain");
if (!configured) {
return {
content: [
{
type: "text",
text: "Memory repository is not configured. Local memory is available only on this machine.",
},
],
details: { initialized: true, configured: false, dirty: null },
};
}
if (!repoReady || !localPath) {
return {
content: [
{
type: "text",
text: "Memory repository is configured but not initialized locally.",
},
],
details: { initialized: true, configured: true, dirty: null },
};
}
const result = await gitExec(
pi,
localPath,
"status",
"--porcelain",
"--",
projectRepoPath,
);
if (!result.success) {
return {
content: [
{
type: "text",
text: "Unable to inspect memory repository status.",
},
],
details: { initialized: true, configured: true, dirty: null },
};
}
const dirty = result.stdout.trim().length > 0;
return {
@ -88,36 +141,87 @@ export function registerMemorySync(
}
if (action === "pull") {
if (!configured) {
return {
content: [
{
type: "text",
text: "Memory repository is not configured. Nothing to pull.",
},
],
details: { success: false, configured: false },
};
}
const result = await syncRepository(pi, settings, isRepoInitialized);
return {
content: [{ type: "text", text: result.message }],
details: { success: result.success },
details: { success: result.success, configured: true },
};
}
if (action === "push") {
if (!configured || !localPath) {
return {
content: [
{
type: "text",
text: "Memory repository is not configured. Nothing to push.",
},
],
details: { success: false, configured: false },
};
}
if (!repoReady) {
return {
content: [
{
type: "text",
text: "Memory repository is configured but not initialized locally.",
},
],
details: { success: false, configured: true },
};
}
const statusResult = await gitExec(
pi,
localPath,
"status",
"--porcelain",
"--",
projectRepoPath,
);
if (!statusResult.success) {
return {
content: [
{
type: "text",
text: "Unable to inspect memory repository before push.",
},
],
details: { success: false, configured: true },
};
}
const hasChanges = statusResult.stdout.trim().length > 0;
if (hasChanges) {
await gitExec(pi, localPath, "add", ".");
await gitExec(pi, localPath, "add", "-A", "--", projectRepoPath);
const timestamp = new Date()
.toISOString()
.replace(/[:.]/g, "-")
.slice(0, 19);
const commitMessage = `Update memory - ${timestamp}`;
const commitMessage = `Update memory for ${path.basename(ctx.cwd)} - ${timestamp}`;
const commitResult = await gitExec(
pi,
localPath,
"commit",
"-m",
commitMessage,
"--only",
"--",
projectRepoPath,
);
if (!commitResult.success) {
@ -189,7 +293,7 @@ export function registerMemorySync(
export function registerMemoryRead(
pi: ExtensionAPI,
settings: MemoryMdSettings,
getSettings: MemorySettingsGetter,
): void {
pi.registerTool({
name: "memory_read",
@ -204,8 +308,8 @@ export function registerMemoryRead(
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const { path: relPath } = params as { path: string };
const memoryDir = getMemoryDir(settings, ctx);
const fullPath = path.join(memoryDir, relPath);
const settings = getSettings();
const fullPath = resolveMemoryPath(settings, ctx, relPath);
const memory = readMemoryFile(fullPath);
if (!memory) {
@ -271,7 +375,7 @@ export function registerMemoryRead(
export function registerMemoryWrite(
pi: ExtensionAPI,
settings: MemoryMdSettings,
getSettings: MemorySettingsGetter,
): void {
pi.registerTool({
name: "memory_write",
@ -300,17 +404,24 @@ export function registerMemoryWrite(
tags?: string[];
};
const memoryDir = getMemoryDir(settings, ctx);
const fullPath = path.join(memoryDir, relPath);
const settings = getSettings();
const fullPath = resolveMemoryPath(settings, ctx, relPath);
const existing = readMemoryFile(fullPath);
const existingFrontmatter = existing?.frontmatter || { description };
const existingFrontmatter = existing?.frontmatter;
const frontmatter: MemoryFrontmatter = {
...existingFrontmatter,
description,
created: existingFrontmatter?.created ?? getCurrentDate(),
updated: getCurrentDate(),
...(tags && { tags }),
...(existingFrontmatter?.limit !== undefined
? { limit: existingFrontmatter.limit }
: {}),
...(tags !== undefined
? { tags }
: existingFrontmatter?.tags
? { tags: existingFrontmatter.tags }
: {}),
};
writeMemoryFile(fullPath, content, frontmatter);
@ -367,7 +478,7 @@ export function registerMemoryWrite(
export function registerMemoryList(
pi: ExtensionAPI,
settings: MemoryMdSettings,
getSettings: MemorySettingsGetter,
): void {
pi.registerTool({
name: "memory_list",
@ -381,8 +492,11 @@ export function registerMemoryList(
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const { directory } = params as { directory?: string };
const settings = getSettings();
const memoryDir = getMemoryDir(settings, ctx);
const searchDir = directory ? path.join(memoryDir, directory) : memoryDir;
const searchDir = directory
? resolveMemoryPath(settings, ctx, directory)
: memoryDir;
const files = listMemoryFiles(searchDir);
const relPaths = files.map((f) => path.relative(memoryDir, f));
@ -432,7 +546,7 @@ export function registerMemoryList(
export function registerMemorySearch(
pi: ExtensionAPI,
settings: MemoryMdSettings,
getSettings: MemorySettingsGetter,
): void {
pi.registerTool({
name: "memory_search",
@ -457,6 +571,7 @@ export function registerMemorySearch(
query: string;
searchIn: "content" | "tags" | "description";
};
const settings = getSettings();
const memoryDir = getMemoryDir(settings, ctx);
const files = listMemoryFiles(memoryDir);
const results: Array<{ path: string; match: string }> = [];
@ -544,7 +659,7 @@ export function registerMemorySearch(
export function registerMemoryInit(
pi: ExtensionAPI,
settings: MemoryMdSettings,
getSettings: MemorySettingsGetter,
isRepoInitialized: { value: boolean },
): void {
pi.registerTool({
@ -558,10 +673,19 @@ export function registerMemoryInit(
),
}) as any,
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const { force = false } = params as { force?: boolean };
const settings = getSettings();
const memoryDir = getMemoryDir(settings, ctx);
const alreadyInitialized = fs.existsSync(
path.join(memoryDir, "core", "user"),
);
const repoReady = Boolean(
settings.localPath &&
fs.existsSync(path.join(settings.localPath, ".git")),
);
if (isRepoInitialized.value && !force) {
if (alreadyInitialized && (!settings.repoUrl || repoReady) && !force) {
return {
content: [
{
@ -573,18 +697,35 @@ export function registerMemoryInit(
};
}
const result = await syncRepository(pi, settings, isRepoInitialized);
if (settings.repoUrl) {
const result = await syncRepository(pi, settings, isRepoInitialized);
if (!result.success) {
return {
content: [
{
type: "text",
text: `Initialization failed: ${result.message}`,
},
],
details: { success: false },
};
}
}
ensureDirectoryStructure(memoryDir);
createDefaultFiles(memoryDir);
isRepoInitialized.value = true;
return {
content: [
{
type: "text",
text: result.success
? `Memory repository initialized:\n${result.message}\n\nCreated directory structure:\n${["core/user", "core/project", "reference"].map((d) => ` - ${d}`).join("\n")}`
: `Initialization failed: ${result.message}`,
text: settings.repoUrl
? `Memory repository initialized.\n\nCreated directory structure:\n${["core/user", "core/project", "reference"].map((d) => ` - ${d}`).join("\n")}`
: `Local memory initialized.\n\nCreated directory structure:\n${["core/user", "core/project", "reference"].map((d) => ` - ${d}`).join("\n")}`,
},
],
details: { success: result.success },
details: { success: true },
};
},
@ -628,7 +769,7 @@ export function registerMemoryInit(
export function registerMemoryCheck(
pi: ExtensionAPI,
settings: MemoryMdSettings,
getSettings: MemorySettingsGetter,
): void {
pi.registerTool({
name: "memory_check",
@ -637,6 +778,7 @@ export function registerMemoryCheck(
parameters: Type.Object({}) as any,
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
const settings = getSettings();
const memoryDir = getMemoryDir(settings, ctx);
if (!fs.existsSync(memoryDir)) {
@ -719,14 +861,14 @@ export function registerMemoryCheck(
export function registerAllTools(
pi: ExtensionAPI,
settings: MemoryMdSettings,
getSettings: MemorySettingsGetter,
isRepoInitialized: { value: boolean },
): void {
registerMemorySync(pi, settings, isRepoInitialized);
registerMemoryRead(pi, settings);
registerMemoryWrite(pi, settings);
registerMemoryList(pi, settings);
registerMemorySearch(pi, settings);
registerMemoryInit(pi, settings, isRepoInitialized);
registerMemoryCheck(pi, settings);
registerMemorySync(pi, getSettings, isRepoInitialized);
registerMemoryRead(pi, getSettings);
registerMemoryWrite(pi, getSettings);
registerMemoryList(pi, getSettings);
registerMemorySearch(pi, getSettings);
registerMemoryInit(pi, getSettings, isRepoInitialized);
registerMemoryCheck(pi, getSettings);
}