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.5
- vielhuber/aihelper: ^5.0.0
- vielhuber/dbhelper: ^2.6.0
- vielhuber/simplemcp: ^1.1.6
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
Provides
None
Conflicts
None
Replaces
None
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_session_home: /persistent/harness/sessions/memory cli_auth_home: /persistent/harness/profiles/codex 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
cli_session_home and cli_auth_home are only relevant when ai.provider is a CLI harness such as Codex,
Claude Code or OpenCode. The session home isolates Memhelper's native CLI history and configuration. The auth home
points at the persistent login profile that may be shared with other sessions. API providers ignore both options.
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"
}
}
}
}