Persistent structured memory layer for LLM agents. Mnemonio gives your agent a file-based memory system that survives across sessions -- markdown files with YAML frontmatter, a manifest file, semantic search, automatic extraction from conversations, and periodic consolidation.
It ships as an MCP server, a CLI, and a TypeScript library. LLM operations use a callback you provide, so it works with any provider.
The fastest way to get started. Add mnemonio to your MCP client settings and your AI coding assistant gets persistent memory automatically.
{
"mcpServers": {
"mnemonio": {
"command": "npx",
"args": ["-p", "@memel06/mnemonio", "mnemonio-mcp"],
"env": {
"MNEMONIO_DIR": "./.mnemonio"
}
}
}
}For LLM-dependent tools (search, extract, distill), create a .env file in your
project root (and add it to .gitignore):
MNEMONIO_API_KEY=your-api-key
MNEMONIO_BASE_URL=https://your-llm-provider.com/v1
MNEMONIO_MODEL=your-modelMnemonio loads .env automatically.
You can also pass these as env vars in the MCP config if you prefer:
{
"mcpServers": {
"mnemonio": {
"command": "npx",
"args": ["-p", "@memel06/mnemonio", "mnemonio-mcp"],
"env": {
"MNEMONIO_DIR": "./.mnemonio",
"MNEMONIO_BASE_URL": "https://your-llm-provider.com/v1",
"MNEMONIO_MODEL": "your-model"
}
}
}
}The server auto-detects the provider from the base URL:
| Base URL contains | Provider | Token param | Auth | Endpoint |
|---|---|---|---|---|
openai.com |
OpenAI (modern) | max_completion_tokens |
Bearer |
/chat/completions |
anthropic.com |
Anthropic | max_tokens |
x-api-key |
/v1/messages |
openrouter.ai |
OpenRouter | max_tokens |
Bearer |
/chat/completions |
| anything else | Generic (OpenAI-compatible) | max_tokens |
Bearer |
/chat/completions |
Override auto-detection with MNEMONIO_PROVIDER (in .env or MCP config):
MNEMONIO_PROVIDER=anthropic
MNEMONIO_API_KEY=sk-ant-...
MNEMONIO_BASE_URL=https://api.anthropic.com
MNEMONIO_MODEL=claude-sonnet-4-6-20250514Valid provider values: openai, openai-classic, anthropic, openrouter.
| Tool | LLM needed | Team aware | Description |
|---|---|---|---|
memory_list |
No | Yes | List all memories, optionally filtered by type or tags |
memory_read |
No | Yes | Read full content of a memory file |
memory_save |
No | No | Save a new memory with frontmatter + manifest entry |
memory_delete |
No | No | Delete a memory file and remove it from the manifest |
memory_search |
Yes | Yes | Semantic search across all memories, with optional tag pre-filter |
memory_extract |
Yes | No | Auto-extract durable facts from a conversation |
memory_distill |
Yes | No | Consolidate: merge duplicates, prune stale, tighten |
memory_stats |
No | Yes | File count, size, type breakdown |
Team memory is a shared, read-only directory that gives every developer's agent the same baseline context -- coding standards, onboarding notes, project conventions. Commit it to your repo so the whole team shares it.
Set MNEMONIO_TEAM_DIR in your MCP config:
{
"mcpServers": {
"mnemonio": {
"command": "npx",
"args": ["-p", "@memel06/mnemonio", "mnemonio-mcp"],
"env": {
"MNEMONIO_DIR": "./.mnemonio",
"MNEMONIO_TEAM_DIR": "./team-memory"
}
}
}
}Team memories automatically appear in memory_list, memory_read,
memory_search, and memory_stats. Write operations (memory_save,
memory_extract, memory_distill) only touch the private directory.
Use --team-dir with any read command:
mnemonio list .mnemonio --team-dir ./team-memory
mnemonio search "coding standards" .mnemonio --team-dir ./team-memoryconst store = createMnemonioStore({
memoryDir: './.mnemonio',
teamDir: './team-memory',
});
// Combined prompt includes both private and team memories
const prompt = await store.buildCombinedPrompt();
// Path traversal protection for team writes
const safePath = await store.validateTeamWritePath('notes.md');Path traversal attacks (symlinks, ../ segments, null bytes) are blocked by
validateTeamWritePath. Use isTeamPath to check if a path falls within the
team directory.
npm install -g @memel06/mnemonioThe CLI reads MNEMONIO_API_KEY from your environment for LLM-dependent
commands (search, distill). It auto-detects the provider from
MNEMONIO_BASE_URL (or you can set MNEMONIO_PROVIDER explicitly) and works
with OpenAI, Anthropic, OpenRouter, and any OpenAI-compatible endpoint.
mnemonio init [dir] Create memory directory with MANIFEST.md
mnemonio scan [dir] [--team-dir <d>] [--json] Display all memory file headers
mnemonio list [dir] [--type <t>] [--team-dir <d>] [--json] List memories with descriptions + age
mnemonio search <query> [dir] [--team-dir <d>] [--json] Find relevant memories (LLM required)
mnemonio distill [dir] [--force] [--json] Run consolidation pass (LLM required)
mnemonio stats [dir] [--team-dir <d>] [--json] File count, size, type breakdown
mnemonio prune [dir] [--max-age <days>] [--dry-run] Remove stale/empty files
All commands default to the current directory if [dir] is omitted. Use --team-dir
to include a shared team memory directory in listings, search, and stats.
npm install @memel06/mnemonioimport { createMnemonioStore } from '@memel06/mnemonio';
const store = createMnemonioStore({
memoryDir: './.mnemonio',
});
// Create the memory directory and MANIFEST.md if they don't exist
await store.ensureDir();
// Inject memory context into your system prompt
const memoryPrompt = await store.buildPrompt();
const systemPrompt = `You are a helpful assistant.\n\n${memoryPrompt}`;
// List all memory files
const headers = await store.scan();
console.log(store.formatManifest(headers));
// Get stats
const stats = await store.stats();
console.log(`${stats.totalFiles} files, ${stats.totalBytes} bytes`);The quickest way is to use the built-in resolveLlm helper, which reads
MNEMONIO_API_KEY, MNEMONIO_BASE_URL, MNEMONIO_MODEL, and
MNEMONIO_PROVIDER from your environment and auto-detects the provider:
import { createMnemonioStore, resolveLlm } from '@memel06/mnemonio';
const store = createMnemonioStore({
memoryDir: './.mnemonio',
llm: resolveLlm(),
});Or wire up your own callback for full control:
import { createMnemonioStore, type LlmCallback } from '@memel06/mnemonio';
const llm: LlmCallback = async ({ system, messages, maxTokens }) => {
const response = await yourClient.chat({
model: 'your-model',
max_tokens: maxTokens,
system,
messages,
});
return response.text;
};
const store = createMnemonioStore({
memoryDir: './.mnemonio',
llm,
});
// Semantic search
const results = await store.findRelevant('database testing approach');
// Extract memories from a conversation
await store.extract({
messages: [
{ role: 'user', content: "Don't mock the database in integration tests." },
{ role: 'assistant', content: 'Understood, I will use the real database.' },
],
});
// Consolidate (merge duplicates, prune stale entries)
await store.distill({ force: true });interface MnemonioConfig {
/** Path to the memory directory */
memoryDir: string;
/** Optional path to a shared team memory directory */
teamDir?: string;
/** LLM callback -- required for search, extract, distill */
llm?: LlmCallback;
/** Name of the entrypoint file (default: "MANIFEST.md") */
entrypointName?: string;
/** Max lines to include from MANIFEST.md in prompts (default: 200) */
maxEntrypointLines?: number;
/** Max bytes to include from MANIFEST.md in prompts (default: 25000) */
maxEntrypointBytes?: number;
/** Optional structured logger */
logger?: (msg: string, level: 'debug' | 'info' | 'warn' | 'error') => void;
}The LlmCallback type accepts any provider. You wire it up once:
type LlmCallback = (params: {
system: string;
messages: ReadonlyArray<{ role: 'user' | 'assistant'; content: string }>;
maxTokens: number;
}) => Promise<string>;| Method | Returns | Description |
|---|---|---|
ensureDir() |
Promise<void> |
Create memory dir and MANIFEST.md if missing |
scan(signal?) |
Promise<MemoryHeader[]> |
List all memory files with frontmatter metadata |
readEntrypoint() |
Promise<EntrypointTruncation> |
Read MANIFEST.md with truncation info |
buildPrompt() |
Promise<string> |
Build memory context string for system prompts |
buildCombinedPrompt() |
Promise<string> |
Build prompt including team memory |
stats(signal?) |
Promise<MnemonioStats> |
File count, total size, type breakdown, age range |
formatManifest(headers) |
string |
Format headers as a human-readable manifest |
| Method | Returns | Description |
|---|---|---|
findRelevant(query, opts?) |
Promise<RelevantMemory[]> |
Semantic search across memories |
extract(config) |
Promise<ExtractResult> |
Extract memories from a conversation |
distill(config?) |
Promise<DistillResult> |
Consolidate: merge duplicates, prune stale, tighten |
| Method | Returns | Description |
|---|---|---|
readLastDistilledAt() |
Promise<number> |
Timestamp of last distillation |
tryAcquireLock() |
Promise<number | null> |
Acquire distillation lock (null if held) |
rollbackLock(priorMtime) |
Promise<void> |
Restore lock mtime on failure |
| Method | Returns | Description |
|---|---|---|
validateTeamWritePath(filePath) |
Promise<string> |
Resolve and validate path within team dir |
isTeamPath(filePath) |
boolean |
Check if path is inside team directory |
Each memory is a markdown file with YAML frontmatter:
---
name: testing-approach
description: Team prefers integration tests with real DB over mocks
type: directive
tags: [testing, database]
---
Always use the real database for integration tests.
> reason: A prior incident where mocked tests passed but production broke on a
> schema change.
> scope: Use test containers or a dedicated test database. Never mock
> the DB layer in integration suites.The MANIFEST.md file in the memory directory serves as the entrypoint. It
contains short one-line pointers to individual memory files. Mnemonio
auto-truncates it when injecting into prompts to stay within token budgets.
| Type | Purpose |
|---|---|
identity |
Information about the user -- role, goals, preferences, expertise |
directive |
Corrections and confirmed approaches -- behavioral guidance |
context |
Ongoing work, decisions, timelines, incidents |
bookmark |
Pointers to external systems, docs, dashboards, resources |
| Field | Required | Description |
|---|---|---|
name |
No | Human-readable slug for the memory |
description |
No | One-line summary shown in listings and manifests |
type |
No | identity, directive, context, or bookmark |
tags |
No | Freeform labels for cross-cutting concerns |
expires |
No | ISO date after which this memory is automatically excluded from all operations |
MIT