vielhuber / memhelper
Markdown-first memory layer for LLM agents — one call, transparent indexing and auto-extraction.
Requires
- php: ^8.3
- ext-json: *
- ext-pdo: *
- symfony/yaml: ^8.1.2
- vielhuber/aihelper: ^4.7.3
- vielhuber/dbhelper: ^2.5.5
- vielhuber/simplemcp: ^1.1.4
Requires (Dev)
- phpunit/phpunit: ^13
Suggests
- ext-pdo_sqlite: Required for the internal FTS5 search index
- ext-zip: Required to index .docx and .xlsx attachments
- poppler-utils: Provides pdftotext for indexing PDFs
README
🧠 memhelper 🧠
Markdown-first memory layer for LLM agents. Exposes a grab(query) MCP tool that returns the curated facts most relevant to a natural-language question. A separate supervisor worker handles all writes — refreshing the search index across every configured input source, distilling new sources via an LLM, and periodically compacting duplicates and obsolete entries. Memory entries are plain .md files with a tiny YAML frontmatter — readable, editable, git-versionable. Cross-references between entries are written as [[slug]] wiki-links and followed one hop at retrieval time, so a single query surfaces related neighbours automatically. Ranking fuses full-text relevance (bm25 over description, body and tags) with the entry's degree in the link graph and its recency, into a single positive score — higher is better. Entries carrying a valid_until date are hidden once it passes and dropped on the next worker tick.
installation
composer require vielhuber/memhelper
ai: provider: openai model: gpt-5 api_key: sk-... cli_ssh_host: cli_ssh_user: cli_ssh_port: cli_ssh_key: output: /path/to/memory max_source_bytes: 20000 existing_memory_limit: 200 injection_max_bytes: 12000 input_files: - /path/to/external/docs - /path/to/external/notes input_dbs: - driver: sqlite path: /path/to/database.db include_tables: [chats_messages] # optional where: # optional chats_messages: role = 'user' AND status = 'completed' compaction_pause_when: SELECT COUNT(*) FROM jobs WHERE status = 'running' # optional - key: project-chats driver: sqlite path: /path/to/database.db query: | SELECT messages.id, projects.path AS scope, messages.role, messages.content FROM messages INNER JOIN projects ON projects.id = messages.project_id primary_key: id scope_column: scope content_columns: [role, content] - driver: mysql host: 127.0.0.1 port: 3306 user: root password: database: memhelper exclude_tables: [analytics_events] # optional - driver: postgres host: 127.0.0.1 port: 5432 user: root password: database: database
usage
library
use vielhuber\memhelper\memhelper; $memory = new memhelper( configPath: '/path/to/config.yaml', logPath: '/var/log/memory.log' ); $facts = $memory->grab( query: 'how is the user\'s dog named?', limit: 10 ); // → [['slug' => 'pet-roger', 'tags' => ['pet', 'dog'], 'description' => '...', // 'body' => '...', 'sources' => ['dbrow:…'], 'score' => 1.0824, 'via' => null], ...] $projectFacts = $memory->grab( scope: '/var/www/project', query: 'database migration', limit: 10, ); $systemPrompt = $memory->inject( scope: '/var/www/project', context: 'Implement the next database migration.' );
scope is an exact technical namespace, independent of semantic memory tags. Distillation, links, retrieval and compaction never cross scope boundaries. inject() performs no AI call; it reads the already curated FTS index, follows scoped links and returns a size-limited Markdown block. Without a scope, grab() continues to use only the global memory store.
memory entries
--- name: pet-roger description: der Hund des Users metadata: tags: - pet - dog sources: - dbrow:charly:chats_messages:42 created: '2026-08-06T09:12:44+02:00' updated: '2026-08-06T09:12:44+02:00' valid_until: '2027-03-31' --- Der Hund des Users heißt Roger. Roger gehört zu [[household-budget]].
created / updated are stamped by the worker; valid_until is optional and may be set by hand or by the LLM when a fact has a known end date. Everything else is free to edit — the worker picks changes up on the next tick.
worker
[program:memhelper-worker] command=php /app/vendor/vielhuber/memhelper/bin/memhelper-worker /path/to/memory.yaml /var/log/memory.log autostart=true autorestart=true
tests
./vendor/bin/phpunit
mcp
{
"mcpServers": {
"memory": {
"command": "php",
"args": ["./vendor/bin/mcp-server.php"],
"env": {
"MEMHELPER_CONFIG": "/path/to/memory.yaml",
"MEMHELPER_LOG": "/var/log/memory.log"
}
}
}
}