vielhuber/memhelper

Markdown-first memory layer for LLM agents — one call, transparent indexing and auto-extraction.

Maintainers

Package info

github.com/vielhuber/memhelper

pkg:composer/vielhuber/memhelper

Transparency log

Statistics

Installs: 63

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

1.3.2 2026-08-17 13:46 UTC

README

build status GitHub Tag Code Style License Last Commit PHP Version Support Packagist Downloads

🧠 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"
            }
        }
    }
}