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

@ -0,0 +1,945 @@
import {
type Dirent,
existsSync,
mkdirSync,
readdirSync,
readFileSync,
renameSync,
statSync,
writeFileSync,
} from "node:fs";
import { createHash } from "node:crypto";
import { homedir } from "node:os";
import { basename, dirname, join, relative, resolve, sep } from "node:path";
import { execCommand } from "../exec.js";
import type { SettingsManager } from "../settings-manager.js";
import { parseFrontmatter } from "../../utils/frontmatter.js";
import { HttpError } from "./internal-types.js";
export interface MemoryFrontmatter {
description: string;
limit?: number;
tags?: string[];
created?: string;
updated?: string;
}
export interface MemoryMdSettings {
enabled?: boolean;
repoUrl?: string;
localPath?: string;
autoSync?: {
onSessionStart?: boolean;
};
injection?: "system-prompt" | "message-append";
systemPrompt?: {
maxTokens?: number;
includeProjects?: string[];
};
}
export interface MemoryStatus {
enabled: boolean;
cwd: string;
project: string;
directory: string;
localPath: string;
repoUrl: string | null;
repoConfigured: boolean;
repositoryReady: boolean;
initialized: boolean;
dirty: boolean | null;
fileCount: number;
}
export interface MemoryFileSummary {
path: string;
description: string | null;
tags: string[];
created?: string;
updated?: string;
valid: boolean;
}
export interface MemoryFileRecord {
path: string;
frontmatter: MemoryFrontmatter;
content: string;
}
export interface MemorySearchResult {
path: string;
match: string;
description: string;
tags: string[];
}
export interface MemorySyncResult {
success: boolean;
message: string;
configured: boolean;
initialized: boolean;
dirty: boolean | null;
updated?: boolean;
committed?: boolean;
}
export type MemorySearchScope = "content" | "tags" | "description";
export type MemorySyncAction = "pull" | "push" | "status";
const DEFAULT_LOCAL_PATH = join(homedir(), ".pi", "memory-md");
const DEFAULT_MEMORY_SETTINGS: MemoryMdSettings = {
enabled: true,
repoUrl: "",
localPath: DEFAULT_LOCAL_PATH,
autoSync: { onSessionStart: true },
injection: "message-append",
systemPrompt: {
maxTokens: 10000,
includeProjects: ["current"],
},
};
function getCurrentDate(): string {
return new Date().toISOString().split("T")[0] ?? "";
}
function getLegacyProjectDirName(cwd: string): string {
return basename(cwd);
}
function getProjectDirName(cwd: string): string {
const projectName = getLegacyProjectDirName(cwd);
const hash = createHash("sha256")
.update(resolve(cwd))
.digest("hex")
.slice(0, 12);
return `${projectName}-${hash}`;
}
function getMemoryDirCandidates(
settings: MemoryMdSettings,
cwd: string,
): {
preferred: string;
legacy: string;
} {
const basePath = settings.localPath ?? DEFAULT_LOCAL_PATH;
return {
preferred: join(basePath, getProjectDirName(cwd)),
legacy: join(basePath, getLegacyProjectDirName(cwd)),
};
}
function migrateLegacyMemoryDir(preferred: string, legacy: string): string {
try {
renameSync(legacy, preferred);
return preferred;
} catch {
return legacy;
}
}
function normalizePathSeparators(value: string): string {
return value.replaceAll("\\", "/");
}
function expandPath(value: string): string {
if (!value.startsWith("~")) {
return value;
}
return join(homedir(), value.slice(1));
}
function asRecord(value: unknown): Record<string, unknown> | null {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
return null;
}
return value as Record<string, unknown>;
}
function readScopedMemorySettings(
settings: Record<string, unknown>,
): MemoryMdSettings {
const scoped = asRecord(settings["pi-memory-md"]);
return scoped ? (scoped as MemoryMdSettings) : {};
}
function getMemorySettings(settingsManager: SettingsManager): MemoryMdSettings {
const globalSettings = readScopedMemorySettings(
settingsManager.getGlobalSettings() as Record<string, unknown>,
);
const projectSettings = readScopedMemorySettings(
settingsManager.getProjectSettings() as Record<string, unknown>,
);
const merged: MemoryMdSettings = {
...DEFAULT_MEMORY_SETTINGS,
...globalSettings,
...projectSettings,
autoSync: {
...DEFAULT_MEMORY_SETTINGS.autoSync,
...globalSettings.autoSync,
...projectSettings.autoSync,
},
systemPrompt: {
...DEFAULT_MEMORY_SETTINGS.systemPrompt,
...globalSettings.systemPrompt,
...projectSettings.systemPrompt,
},
};
if (merged.localPath) {
merged.localPath = expandPath(merged.localPath);
}
return merged;
}
function getMemoryDir(settings: MemoryMdSettings, cwd: string): string {
const { preferred, legacy } = getMemoryDirCandidates(settings, cwd);
if (existsSync(preferred)) {
return preferred;
}
if (existsSync(legacy)) {
return migrateLegacyMemoryDir(preferred, legacy);
}
return preferred;
}
function getProjectRepoPath(settings: MemoryMdSettings, cwd: string): string {
const localPath = settings.localPath ?? DEFAULT_LOCAL_PATH;
return normalizePathSeparators(relative(localPath, getMemoryDir(settings, cwd)));
}
function validateFrontmatter(frontmatter: Record<string, unknown>): {
valid: boolean;
error?: string;
} {
if (
typeof frontmatter.description !== "string" ||
frontmatter.description.trim().length === 0
) {
return {
valid: false,
error: "Frontmatter must contain a non-empty description",
};
}
if (
frontmatter.limit !== undefined &&
(typeof frontmatter.limit !== "number" || frontmatter.limit <= 0)
) {
return {
valid: false,
error: "Frontmatter limit must be a positive number",
};
}
if (frontmatter.tags !== undefined) {
if (!Array.isArray(frontmatter.tags)) {
return {
valid: false,
error: "Frontmatter tags must be an array of strings",
};
}
if (
frontmatter.tags.some((tag) => typeof tag !== "string" || tag.length === 0)
) {
return {
valid: false,
error: "Frontmatter tags must contain only non-empty strings",
};
}
}
return { valid: true };
}
function normalizeFrontmatter(frontmatter: Record<string, unknown>): MemoryFrontmatter {
return {
description: frontmatter.description as string,
...(typeof frontmatter.limit === "number"
? { limit: frontmatter.limit }
: {}),
...(Array.isArray(frontmatter.tags)
? {
tags: frontmatter.tags.filter(
(tag): tag is string => typeof tag === "string",
),
}
: {}),
...(typeof frontmatter.created === "string"
? { created: frontmatter.created }
: {}),
...(typeof frontmatter.updated === "string"
? { updated: frontmatter.updated }
: {}),
};
}
function readMemoryFile(filePath: string): MemoryFileRecord | null {
try {
const content = readFileSync(filePath, "utf8");
const parsed = parseFrontmatter<Record<string, unknown>>(content);
const validation = validateFrontmatter(parsed.frontmatter);
if (!validation.valid) {
return null;
}
return {
path: filePath,
frontmatter: normalizeFrontmatter(parsed.frontmatter),
content: parsed.body,
};
} catch {
return null;
}
}
function escapeYamlString(value: string): string {
return JSON.stringify(value);
}
function serializeFrontmatter(frontmatter: MemoryFrontmatter): string {
const lines = ["---", `description: ${escapeYamlString(frontmatter.description)}`];
if (typeof frontmatter.limit === "number") {
lines.push(`limit: ${frontmatter.limit}`);
}
if (frontmatter.tags && frontmatter.tags.length > 0) {
lines.push("tags:");
for (const tag of frontmatter.tags) {
lines.push(` - ${escapeYamlString(tag)}`);
}
}
if (frontmatter.created) {
lines.push(`created: ${escapeYamlString(frontmatter.created)}`);
}
if (frontmatter.updated) {
lines.push(`updated: ${escapeYamlString(frontmatter.updated)}`);
}
lines.push("---");
return lines.join("\n");
}
function writeMemoryFile(
filePath: string,
content: string,
frontmatter: MemoryFrontmatter,
): void {
mkdirSync(dirname(filePath), { recursive: true });
const normalizedContent = content.replace(/\r\n/g, "\n");
const body = normalizedContent.endsWith("\n")
? normalizedContent
: `${normalizedContent}\n`;
const output = `${serializeFrontmatter(frontmatter)}\n\n${body}`;
writeFileSync(filePath, output, "utf8");
}
function listMemoryFiles(memoryDir: string): string[] {
if (!existsSync(memoryDir)) {
return [];
}
const files: string[] = [];
const stack = [memoryDir];
while (stack.length > 0) {
const currentDir = stack.pop();
if (!currentDir) {
continue;
}
let entries: Dirent[];
try {
entries = readdirSync(currentDir, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
const fullPath = join(currentDir, entry.name);
if (entry.isDirectory()) {
stack.push(fullPath);
continue;
}
if (entry.isFile() && entry.name.endsWith(".md")) {
files.push(fullPath);
}
}
}
return files.sort((left, right) => left.localeCompare(right));
}
function ensureDirectoryStructure(memoryDir: string): void {
const directories = [
join(memoryDir, "core", "user"),
join(memoryDir, "core", "project"),
join(memoryDir, "reference"),
];
for (const directory of directories) {
mkdirSync(directory, { recursive: true });
}
}
function ensureDefaultFiles(memoryDir: string): void {
const identityPath = join(memoryDir, "core", "user", "identity.md");
if (!existsSync(identityPath)) {
writeMemoryFile(
identityPath,
"# User Identity\n\nCustomize this file with your information.",
{
description: "User identity and background",
tags: ["user", "identity"],
created: getCurrentDate(),
},
);
}
const preferencesPath = join(memoryDir, "core", "user", "prefer.md");
if (!existsSync(preferencesPath)) {
writeMemoryFile(
preferencesPath,
"# User Preferences\n\n## Communication Style\n- Be concise\n- Show code examples\n\n## Code Style\n- 2 space indentation\n- Prefer const over var\n- Functional programming preferred",
{
description: "User habits and code style preferences",
tags: ["user", "preferences"],
created: getCurrentDate(),
},
);
}
}
function resolveWithinMemoryDir(memoryDir: string, relativePath: string): string {
const trimmed = relativePath.trim();
if (trimmed.length === 0) {
throw new HttpError(400, "Memory path is required");
}
const resolvedPath = resolve(memoryDir, trimmed);
const resolvedRoot = resolve(memoryDir);
if (
resolvedPath !== resolvedRoot &&
!resolvedPath.startsWith(`${resolvedRoot}${sep}`)
) {
throw new HttpError(400, `Memory path escapes root: ${relativePath}`);
}
return resolvedPath;
}
function summarizeMemoryFile(
memoryDir: string,
filePath: string,
): MemoryFileSummary {
const relativePath = normalizePathSeparators(relative(memoryDir, filePath));
const memoryFile = readMemoryFile(filePath);
if (!memoryFile) {
return {
path: relativePath,
description: null,
tags: [],
valid: false,
};
}
return {
path: relativePath,
description: memoryFile.frontmatter.description,
tags: memoryFile.frontmatter.tags ?? [],
created: memoryFile.frontmatter.created,
updated: memoryFile.frontmatter.updated,
valid: true,
};
}
function readMemoryFileOrThrow(
memoryDir: string,
relativePath: string,
): MemoryFileRecord {
const fullPath = resolveWithinMemoryDir(memoryDir, relativePath);
if (!existsSync(fullPath)) {
throw new HttpError(404, `Memory file not found: ${relativePath}`);
}
const memoryFile = readMemoryFile(fullPath);
if (!memoryFile) {
throw new HttpError(422, `Invalid memory file: ${relativePath}`);
}
return {
...memoryFile,
path: normalizePathSeparators(relative(memoryDir, fullPath)),
};
}
async function runGit(
cwd: string,
...args: string[]
): Promise<{ success: boolean; stdout: string; stderr: string }> {
const result = await execCommand("git", args, cwd, { timeout: 30_000 });
return {
success: result.code === 0 && !result.killed,
stdout: result.stdout,
stderr: result.stderr,
};
}
function getRepoName(settings: MemoryMdSettings): string {
if (!settings.repoUrl) {
return "memory-md";
}
const match = settings.repoUrl.match(/\/([^/]+?)(\.git)?$/);
return match?.[1] ?? "memory-md";
}
async function getRepositoryDirtyState(
localPath: string,
projectPath?: string,
): Promise<boolean | null> {
if (!existsSync(join(localPath, ".git"))) {
return null;
}
const args = projectPath
? ["status", "--porcelain", "--", projectPath]
: ["status", "--porcelain"];
const result = await runGit(localPath, ...args);
if (!result.success) {
return null;
}
return result.stdout.trim().length > 0;
}
async function syncRepository(
settings: MemoryMdSettings,
): Promise<{ success: boolean; message: string; updated?: boolean }> {
const localPath = settings.localPath ?? DEFAULT_LOCAL_PATH;
const repoUrl = settings.repoUrl?.trim() ?? "";
if (!repoUrl) {
return {
success: false,
message: "Memory repo URL is not configured",
};
}
if (existsSync(localPath)) {
if (!existsSync(join(localPath, ".git"))) {
let existingEntries: string[];
try {
existingEntries = readdirSync(localPath);
} catch {
return {
success: false,
message: `Path exists but is not a directory: ${localPath}`,
};
}
if (existingEntries.length === 0) {
const cloneIntoEmptyDir = await runGit(localPath, "clone", repoUrl, ".");
if (!cloneIntoEmptyDir.success) {
return {
success: false,
message:
cloneIntoEmptyDir.stderr.trim() ||
"Clone failed. Check repo URL and auth.",
};
}
return {
success: true,
message: `Cloned ${getRepoName(settings)} successfully`,
updated: true,
};
}
return {
success: false,
message: `Directory exists but is not a git repo: ${localPath}`,
};
}
const pullResult = await runGit(localPath, "pull", "--rebase", "--autostash");
if (!pullResult.success) {
return {
success: false,
message:
pullResult.stderr.trim() || "Pull failed. Check repository state.",
};
}
const updated =
pullResult.stdout.includes("Updating") ||
pullResult.stdout.includes("Fast-forward");
return {
success: true,
message: updated
? `Pulled latest changes from ${getRepoName(settings)}`
: `${getRepoName(settings)} is already up to date`,
updated,
};
}
mkdirSync(dirname(localPath), { recursive: true });
const cloneResult = await runGit(
dirname(localPath),
"clone",
repoUrl,
basename(localPath),
);
if (!cloneResult.success) {
return {
success: false,
message: cloneResult.stderr.trim() || "Clone failed. Check repo URL and auth.",
};
}
return {
success: true,
message: `Cloned ${getRepoName(settings)} successfully`,
updated: true,
};
}
export async function getMemoryStatus(
settingsManager: SettingsManager,
cwd: string,
): Promise<MemoryStatus> {
const settings = getMemorySettings(settingsManager);
const localPath = settings.localPath ?? DEFAULT_LOCAL_PATH;
const memoryDir = getMemoryDir(settings, cwd);
const initialized = existsSync(join(memoryDir, "core", "user"));
const fileCount = listMemoryFiles(memoryDir).length;
const dirty = await getRepositoryDirtyState(
localPath,
getProjectRepoPath(settings, cwd),
);
return {
enabled: settings.enabled ?? true,
cwd,
project: basename(cwd),
directory: memoryDir,
localPath,
repoUrl: settings.repoUrl?.trim() || null,
repoConfigured: Boolean(settings.repoUrl?.trim()),
repositoryReady: existsSync(join(localPath, ".git")),
initialized,
dirty,
fileCount,
};
}
export async function initializeMemory(
settingsManager: SettingsManager,
cwd: string,
options: { force?: boolean } = {},
): Promise<{
ok: true;
created: boolean;
message: string;
memory: MemoryStatus;
}> {
const settings = getMemorySettings(settingsManager);
const localPath = settings.localPath ?? DEFAULT_LOCAL_PATH;
const memoryDir = getMemoryDir(settings, cwd);
const initialized = existsSync(join(memoryDir, "core", "user"));
if (settings.repoUrl?.trim()) {
const repositoryReady = existsSync(join(localPath, ".git"));
if (!initialized || options.force || !repositoryReady) {
const syncResult = await syncRepository(settings);
if (!syncResult.success) {
throw new HttpError(409, syncResult.message);
}
}
} else {
mkdirSync(localPath, { recursive: true });
}
ensureDirectoryStructure(memoryDir);
ensureDefaultFiles(memoryDir);
return {
ok: true,
created: !initialized,
message:
settings.repoUrl?.trim()
? initialized
? "Memory repository refreshed and project memory verified"
: "Memory repository initialized for this project"
: initialized
? "Local memory verified"
: "Local memory initialized",
memory: await getMemoryStatus(settingsManager, cwd),
};
}
export async function listProjectMemoryFiles(
settingsManager: SettingsManager,
cwd: string,
directory?: string,
): Promise<{
directory: string;
files: MemoryFileSummary[];
}> {
const settings = getMemorySettings(settingsManager);
const memoryDir = getMemoryDir(settings, cwd);
const searchDir = directory
? resolveWithinMemoryDir(memoryDir, directory)
: memoryDir;
const files = listMemoryFiles(searchDir).map((filePath) =>
summarizeMemoryFile(memoryDir, filePath),
);
return {
directory: directory?.trim() || "",
files,
};
}
export async function readProjectMemoryFile(
settingsManager: SettingsManager,
cwd: string,
relativePath: string,
): Promise<{ file: MemoryFileRecord }> {
const settings = getMemorySettings(settingsManager);
const memoryDir = getMemoryDir(settings, cwd);
return {
file: readMemoryFileOrThrow(memoryDir, relativePath),
};
}
export async function writeProjectMemoryFile(
settingsManager: SettingsManager,
cwd: string,
params: {
path: string;
content: string;
description: string;
tags?: string[];
},
): Promise<{ ok: true; file: MemoryFileRecord }> {
const relativePath = params.path.trim();
if (!relativePath.endsWith(".md")) {
throw new HttpError(400, "Memory files must use the .md extension");
}
if (params.description.trim().length === 0) {
throw new HttpError(400, "Memory description is required");
}
const settings = getMemorySettings(settingsManager);
const memoryDir = getMemoryDir(settings, cwd);
const fullPath = resolveWithinMemoryDir(memoryDir, relativePath);
if (existsSync(fullPath) && statSync(fullPath).isDirectory()) {
throw new HttpError(400, `Memory path points to a directory: ${relativePath}`);
}
const existing = existsSync(fullPath) ? readMemoryFile(fullPath) : null;
const hasTagsInput = params.tags !== undefined;
const tags = (params.tags ?? []).map((tag) => tag.trim()).filter(Boolean);
const frontmatter: MemoryFrontmatter = {
description: params.description.trim(),
created: existing?.frontmatter.created ?? getCurrentDate(),
updated: getCurrentDate(),
...(existing?.frontmatter.limit !== undefined
? { limit: existing.frontmatter.limit }
: {}),
...(hasTagsInput
? { tags }
: existing?.frontmatter.tags
? { tags: existing.frontmatter.tags }
: {}),
};
writeMemoryFile(fullPath, params.content, frontmatter);
return {
ok: true,
file: {
path: normalizePathSeparators(relative(memoryDir, fullPath)),
frontmatter,
content: params.content.replace(/\r\n/g, "\n"),
},
};
}
export async function searchProjectMemory(
settingsManager: SettingsManager,
cwd: string,
query: string,
searchIn: MemorySearchScope,
): Promise<{ results: MemorySearchResult[] }> {
const normalizedQuery = query.trim().toLowerCase();
if (normalizedQuery.length === 0) {
throw new HttpError(400, "Memory search query is required");
}
const settings = getMemorySettings(settingsManager);
const memoryDir = getMemoryDir(settings, cwd);
const results: MemorySearchResult[] = [];
for (const filePath of listMemoryFiles(memoryDir)) {
const memoryFile = readMemoryFile(filePath);
if (!memoryFile) {
continue;
}
const relativePath = normalizePathSeparators(relative(memoryDir, filePath));
if (searchIn === "content") {
if (!memoryFile.content.toLowerCase().includes(normalizedQuery)) {
continue;
}
const match =
memoryFile.content
.split("\n")
.find((line) => line.toLowerCase().includes(normalizedQuery)) ??
memoryFile.content.slice(0, 120);
results.push({
path: relativePath,
match,
description: memoryFile.frontmatter.description,
tags: memoryFile.frontmatter.tags ?? [],
});
continue;
}
if (searchIn === "tags") {
const tags = memoryFile.frontmatter.tags ?? [];
if (!tags.some((tag) => tag.toLowerCase().includes(normalizedQuery))) {
continue;
}
results.push({
path: relativePath,
match: `Tags: ${tags.join(", ")}`,
description: memoryFile.frontmatter.description,
tags,
});
continue;
}
if (
memoryFile.frontmatter.description.toLowerCase().includes(normalizedQuery)
) {
results.push({
path: relativePath,
match: memoryFile.frontmatter.description,
description: memoryFile.frontmatter.description,
tags: memoryFile.frontmatter.tags ?? [],
});
}
}
return { results };
}
export async function syncProjectMemory(
settingsManager: SettingsManager,
cwd: string,
action: MemorySyncAction,
): Promise<MemorySyncResult> {
const settings = getMemorySettings(settingsManager);
const localPath = settings.localPath ?? DEFAULT_LOCAL_PATH;
const projectPath = getProjectRepoPath(settings, cwd);
const configured = Boolean(settings.repoUrl?.trim());
const repositoryReady = existsSync(join(localPath, ".git"));
const initialized = existsSync(join(getMemoryDir(settings, cwd), "core", "user"));
if (action === "status") {
const dirty = await getRepositoryDirtyState(localPath, projectPath);
return {
success: repositoryReady,
message: repositoryReady
? dirty
? "Memory repository has uncommitted changes"
: "Memory repository is clean"
: configured
? "Memory repository is not initialized"
: "Memory repository is not configured",
configured,
initialized,
dirty,
};
}
if (!configured) {
return {
success: false,
message: "Memory repo URL is not configured",
configured,
initialized,
dirty: null,
};
}
if (action === "pull") {
const result = await syncRepository(settings);
return {
success: result.success,
message: result.message,
configured,
initialized,
dirty: await getRepositoryDirtyState(localPath, projectPath),
updated: result.updated,
};
}
if (!repositoryReady) {
const cloned = await syncRepository(settings);
if (!cloned.success) {
return {
success: false,
message: cloned.message,
configured,
initialized,
dirty: null,
};
}
}
const statusResult = await runGit(
localPath,
"status",
"--porcelain",
"--",
projectPath,
);
if (!statusResult.success) {
return {
success: false,
message: statusResult.stderr.trim() || "Failed to inspect memory repository",
configured,
initialized,
dirty: null,
};
}
const hasChanges = statusResult.stdout.trim().length > 0;
if (hasChanges) {
const addResult = await runGit(localPath, "add", "-A", "--", projectPath);
if (!addResult.success) {
return {
success: false,
message: addResult.stderr.trim() || "Failed to stage memory changes",
configured,
initialized,
dirty: true,
};
}
const timestamp = new Date()
.toISOString()
.replace(/[:.]/g, "-")
.slice(0, 19);
const commitResult = await runGit(
localPath,
"commit",
"-m",
`Update memory for ${basename(cwd)} - ${timestamp}`,
"--only",
"--",
projectPath,
);
if (!commitResult.success) {
return {
success: false,
message:
commitResult.stderr.trim() || "Failed to commit memory changes",
configured,
initialized,
dirty: true,
};
}
}
const pushResult = await runGit(localPath, "push");
return {
success: pushResult.success,
message: pushResult.success
? hasChanges
? "Committed and pushed memory changes"
: "No memory changes to push"
: pushResult.stderr.trim() || "Failed to push memory changes",
configured,
initialized,
dirty: await getRepositoryDirtyState(localPath, projectPath),
committed: hasChanges,
};
}

View file

@ -29,6 +29,15 @@ import type {
HistoryPart,
ModelInfo,
} from "./types.js";
import {
getMemoryStatus,
initializeMemory,
listProjectMemoryFiles,
readProjectMemoryFile,
searchProjectMemory,
syncProjectMemory,
writeProjectMemoryFile,
} from "./memory.js";
import type { Settings } from "../settings-manager.js";
import {
createVercelStreamListener,
@ -624,6 +633,111 @@ export class GatewayRuntime {
return;
}
if (method === "GET" && path === "/memory/status") {
const memory = await getMemoryStatus(
this.primarySession.settingsManager,
this.primarySession.sessionManager.getCwd(),
);
this.writeJson(response, 200, { memory });
return;
}
if (method === "POST" && path === "/memory/init") {
const body = await this.readJsonBody(request);
const result = await initializeMemory(
this.primarySession.settingsManager,
this.primarySession.sessionManager.getCwd(),
{ force: body.force === true },
);
this.writeJson(response, 200, result);
return;
}
if (method === "GET" && path === "/memory/files") {
const directory = url.searchParams.get("directory") ?? undefined;
const result = await listProjectMemoryFiles(
this.primarySession.settingsManager,
this.primarySession.sessionManager.getCwd(),
directory,
);
this.writeJson(response, 200, result);
return;
}
if (method === "GET" && path === "/memory/file") {
const filePath = url.searchParams.get("path");
if (!filePath) {
this.writeJson(response, 400, { error: "Missing memory file path" });
return;
}
const result = await readProjectMemoryFile(
this.primarySession.settingsManager,
this.primarySession.sessionManager.getCwd(),
filePath,
);
this.writeJson(response, 200, result);
return;
}
if (method === "POST" && path === "/memory/file") {
const body = await this.readJsonBody(request);
const filePath = typeof body.path === "string" ? body.path : "";
const content = typeof body.content === "string" ? body.content : "";
const description =
typeof body.description === "string" ? body.description : "";
const tags = Array.isArray(body.tags)
? body.tags.filter((tag): tag is string => typeof tag === "string")
: undefined;
const result = await writeProjectMemoryFile(
this.primarySession.settingsManager,
this.primarySession.sessionManager.getCwd(),
{
path: filePath,
content,
description,
tags,
},
);
this.writeJson(response, 200, result);
return;
}
if (method === "POST" && path === "/memory/search") {
const body = await this.readJsonBody(request);
const query = typeof body.query === "string" ? body.query : "";
const searchIn =
body.searchIn === "content" ||
body.searchIn === "tags" ||
body.searchIn === "description"
? body.searchIn
: "content";
const result = await searchProjectMemory(
this.primarySession.settingsManager,
this.primarySession.sessionManager.getCwd(),
query,
searchIn,
);
this.writeJson(response, 200, result);
return;
}
if (method === "POST" && path === "/memory/sync") {
const body = await this.readJsonBody(request);
const action =
body.action === "pull" ||
body.action === "push" ||
body.action === "status"
? body.action
: "status";
const result = await syncProjectMemory(
this.primarySession.settingsManager,
this.primarySession.sessionManager.getCwd(),
action,
);
this.writeJson(response, 200, result);
return;
}
const sessionMatch = path.match(
/^\/sessions\/([^/]+)(?:\/(events|messages|abort|reset|chat|history|model|reload))?$/,
);

View file

@ -1,4 +1,5 @@
import fs from "node:fs";
import { createHash } from "node:crypto";
import os from "node:os";
import path from "node:path";
import type {
@ -61,7 +62,7 @@ export type ParsedFrontmatter = GrayMatterFile<string>["data"];
const DEFAULT_LOCAL_PATH = path.join(os.homedir(), ".pi", "memory-md");
export function getCurrentDate(): string {
return new Date().toISOString().split("T")[0];
return new Date().toISOString().split("T")[0] ?? "";
}
function expandPath(p: string): string {
@ -71,12 +72,73 @@ function expandPath(p: string): string {
return p;
}
function getLegacyProjectDirName(cwd: string): string {
return path.basename(cwd);
}
function getProjectDirName(cwd: string): string {
const projectName = getLegacyProjectDirName(cwd);
const hash = createHash("sha256")
.update(path.resolve(cwd))
.digest("hex")
.slice(0, 12);
return `${projectName}-${hash}`;
}
function migrateLegacyMemoryDir(
preferredDir: string,
legacyDir: string,
): string {
try {
fs.renameSync(legacyDir, preferredDir);
return preferredDir;
} catch (error) {
console.warn("Failed to migrate legacy memory dir:", error);
return legacyDir;
}
}
export function getMemoryDir(
settings: MemoryMdSettings,
ctx: ExtensionContext,
): string {
const basePath = settings.localPath || DEFAULT_LOCAL_PATH;
return path.join(basePath, path.basename(ctx.cwd));
const preferredDir = path.join(basePath, getProjectDirName(ctx.cwd));
if (fs.existsSync(preferredDir)) {
return preferredDir;
}
const legacyDir = path.join(basePath, getLegacyProjectDirName(ctx.cwd));
if (fs.existsSync(legacyDir)) {
return migrateLegacyMemoryDir(preferredDir, legacyDir);
}
return preferredDir;
}
export function getProjectRepoPath(
settings: MemoryMdSettings,
ctx: ExtensionContext,
): string {
const basePath = settings.localPath || DEFAULT_LOCAL_PATH;
return path.relative(basePath, getMemoryDir(settings, ctx)).split(path.sep).join("/");
}
export function resolveMemoryPath(
settings: MemoryMdSettings,
ctx: ExtensionContext,
relativePath: string,
): string {
const memoryDir = getMemoryDir(settings, ctx);
const resolvedPath = path.resolve(memoryDir, relativePath.trim());
const resolvedRoot = path.resolve(memoryDir);
if (
resolvedPath !== resolvedRoot &&
!resolvedPath.startsWith(`${resolvedRoot}${path.sep}`)
) {
throw new Error(`Memory path escapes root: ${relativePath}`);
}
return resolvedPath;
}
function getRepoName(settings: MemoryMdSettings): string {
@ -85,7 +147,26 @@ function getRepoName(settings: MemoryMdSettings): string {
return match ? match[1] : "memory-md";
}
function loadSettings(): MemoryMdSettings {
function loadScopedSettings(settingsPath: string): MemoryMdSettings {
if (!fs.existsSync(settingsPath)) {
return {};
}
try {
const content = fs.readFileSync(settingsPath, "utf-8");
const parsed = JSON.parse(content);
const scoped = parsed["pi-memory-md"];
if (!scoped || typeof scoped !== "object" || Array.isArray(scoped)) {
return {};
}
return scoped as MemoryMdSettings;
} catch (error) {
console.warn("Failed to load memory settings:", error);
return {};
}
}
function loadSettings(cwd?: string): MemoryMdSettings {
const DEFAULT_SETTINGS: MemoryMdSettings = {
enabled: true,
repoUrl: "",
@ -104,27 +185,34 @@ function loadSettings(): MemoryMdSettings {
"agent",
"settings.json",
);
if (!fs.existsSync(globalSettings)) {
return DEFAULT_SETTINGS;
const projectSettings = cwd
? path.join(cwd, ".pi", "settings.json")
: undefined;
const globalLoaded = loadScopedSettings(globalSettings);
const projectLoaded = projectSettings
? loadScopedSettings(projectSettings)
: {};
const loadedSettings = {
...DEFAULT_SETTINGS,
...globalLoaded,
...projectLoaded,
autoSync: {
...DEFAULT_SETTINGS.autoSync,
...globalLoaded.autoSync,
...projectLoaded.autoSync,
},
systemPrompt: {
...DEFAULT_SETTINGS.systemPrompt,
...globalLoaded.systemPrompt,
...projectLoaded.systemPrompt,
},
};
if (loadedSettings.localPath) {
loadedSettings.localPath = expandPath(loadedSettings.localPath);
}
try {
const content = fs.readFileSync(globalSettings, "utf-8");
const parsed = JSON.parse(content);
const loadedSettings = {
...DEFAULT_SETTINGS,
...(parsed["pi-memory-md"] as MemoryMdSettings),
};
if (loadedSettings.localPath) {
loadedSettings.localPath = expandPath(loadedSettings.localPath);
}
return loadedSettings;
} catch (error) {
console.warn("Failed to load memory settings:", error);
return DEFAULT_SETTINGS;
}
return loadedSettings;
}
/**
@ -165,6 +253,33 @@ export async function syncRepository(
if (fs.existsSync(localPath)) {
const gitDir = path.join(localPath, ".git");
if (!fs.existsSync(gitDir)) {
let existingEntries: string[];
try {
existingEntries = fs.readdirSync(localPath);
} catch {
return {
success: false,
message: `Path exists but is not a directory: ${localPath}`,
};
}
if (existingEntries.length === 0) {
const cloneIntoEmptyDir = await gitExec(pi, localPath, "clone", repoUrl, ".");
if (cloneIntoEmptyDir.success) {
isRepoInitialized.value = true;
const repoName = getRepoName(settings);
return {
success: true,
message: `Cloned [${repoName}] successfully`,
updated: true,
};
}
return {
success: false,
message: "Clone failed - check repo URL and auth",
};
}
return {
success: false,
message: `Directory exists but is not a git repo: ${localPath}`,
@ -322,7 +437,7 @@ export function writeMemoryFile(
* Build memory context for agent prompt.
*/
function ensureDirectoryStructure(memoryDir: string): void {
export function ensureDirectoryStructure(memoryDir: string): void {
const dirs = [
path.join(memoryDir, "core", "user"),
path.join(memoryDir, "core", "project"),
@ -334,7 +449,7 @@ function ensureDirectoryStructure(memoryDir: string): void {
}
}
function createDefaultFiles(memoryDir: string): void {
export function createDefaultFiles(memoryDir: string): void {
const identityFile = path.join(memoryDir, "core", "user", "identity.md");
if (!fs.existsSync(identityFile)) {
writeMemoryFile(
@ -434,7 +549,7 @@ export default function memoryMdExtension(pi: ExtensionAPI) {
let memoryInjected = false;
pi.on("session_start", async (_event, ctx) => {
settings = loadSettings();
settings = loadSettings(ctx.cwd);
if (!settings.enabled) {
return;
@ -451,7 +566,11 @@ export default function memoryMdExtension(pi: ExtensionAPI) {
return;
}
if (settings.autoSync?.onSessionStart && settings.localPath) {
if (
settings.autoSync?.onSessionStart &&
settings.localPath &&
settings.repoUrl
) {
syncPromise = syncRepository(pi, settings, repoInitialized).then(
(syncResult) => {
if (settings.repoUrl) {
@ -509,14 +628,20 @@ export default function memoryMdExtension(pi: ExtensionAPI) {
return undefined;
});
registerAllTools(pi, settings, repoInitialized);
registerAllTools(pi, () => settings, repoInitialized);
pi.registerCommand("memory-status", {
description: "Show memory repository status",
handler: async (_args, ctx) => {
settings = loadSettings(ctx.cwd);
const projectName = path.basename(ctx.cwd);
const memoryDir = getMemoryDir(settings, ctx);
const projectRepoPath = getProjectRepoPath(settings, ctx);
const coreUserDir = path.join(memoryDir, "core", "user");
const repoConfigured = Boolean(settings.repoUrl);
const repoReady = Boolean(
settings.localPath && fs.existsSync(path.join(settings.localPath, ".git")),
);
if (!fs.existsSync(coreUserDir)) {
ctx.ui.notify(
@ -526,12 +651,37 @@ export default function memoryMdExtension(pi: ExtensionAPI) {
return;
}
if (!repoConfigured) {
ctx.ui.notify(
`Memory: ${projectName} | Local only | Path: ${memoryDir}`,
"info",
);
return;
}
if (!repoReady || !settings.localPath) {
ctx.ui.notify(
`Memory: ${projectName} | Repo not initialized | Path: ${memoryDir}`,
"warning",
);
return;
}
const result = await gitExec(
pi,
settings.localPath!,
settings.localPath,
"status",
"--porcelain",
"--",
projectRepoPath,
);
if (!result.success) {
ctx.ui.notify(
`Memory: ${projectName} | Repo status unavailable | Path: ${memoryDir}`,
"warning",
);
return;
}
const isDirty = result.stdout.trim().length > 0;
ctx.ui.notify(
@ -544,26 +694,36 @@ export default function memoryMdExtension(pi: ExtensionAPI) {
pi.registerCommand("memory-init", {
description: "Initialize memory repository",
handler: async (_args, ctx) => {
settings = loadSettings(ctx.cwd);
const memoryDir = getMemoryDir(settings, ctx);
const alreadyInitialized = fs.existsSync(
path.join(memoryDir, "core", "user"),
);
const result = await syncRepository(pi, settings, repoInitialized);
if (!result.success) {
ctx.ui.notify(`Initialization failed: ${result.message}`, "error");
return;
if (settings.repoUrl) {
const result = await syncRepository(pi, settings, repoInitialized);
if (!result.success) {
ctx.ui.notify(`Initialization failed: ${result.message}`, "error");
return;
}
}
ensureDirectoryStructure(memoryDir);
createDefaultFiles(memoryDir);
repoInitialized.value = true;
if (alreadyInitialized) {
ctx.ui.notify(`Memory already exists: ${result.message}`, "info");
ctx.ui.notify(
settings.repoUrl
? "Memory already exists and repository is ready"
: "Local memory already exists",
"info",
);
} else {
ctx.ui.notify(
`Memory initialized: ${result.message}\n\nCreated:\n - core/user\n - core/project\n - reference`,
settings.repoUrl
? "Memory initialized and repository is ready\n\nCreated:\n - core/user\n - core/project\n - reference"
: "Local memory initialized\n\nCreated:\n - core/user\n - core/project\n - reference",
"info",
);
}
@ -573,6 +733,7 @@ export default function memoryMdExtension(pi: ExtensionAPI) {
pi.registerCommand("memory-refresh", {
description: "Refresh memory context from files",
handler: async (_args, ctx) => {
settings = loadSettings(ctx.cwd);
const memoryContext = buildMemoryContext(settings, ctx);
if (!memoryContext) {
@ -610,6 +771,7 @@ export default function memoryMdExtension(pi: ExtensionAPI) {
pi.registerCommand("memory-check", {
description: "Check memory folder structure",
handler: async (_args, ctx) => {
settings = loadSettings(ctx.cwd);
const memoryDir = getMemoryDir(settings, ctx);
if (!fs.existsSync(memoryDir)) {

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);
}