bernard
Repo-committed, project-scoped AI memory for coding agents — zero API keys, zero embeddings, zero external services.
🇹🇷 Türkçe README
bernard is your codebase's team memory. It stores the decisions you made, the architecture notes, the bugs you fixed, and the preferences you settled on — as plain text (JSONL) inside the repo. Then you, or an AI agent, can pull that context back in seconds before starting new work. The knowledge lives where the code lives — in git — so it's committed, versioned, reviewed, and shared with the whole team.
It connects to AI coding agents (Claude Code, Cursor, and anything that speaks MCP) as an MCP server, so the agent searches past decisions and records new ones on its own.
// .bernard/records.jsonl — committed, one JSON object per line
{"id":"ber_a1b2c3","type":"decision","title":"Switched to Postgres","content":"MySQL → Postgres for JSONB and better indexing.","tags":["db"],"created":"2026-06-27T10:00:00Z","invalid":false,"superseded_by":null,"source":"manual"}
Why bernard?
A memory.md file rots: it's unstructured, unsearchable, and nobody trims it. Cloud "agent memory" products want an API key, ship your code context to a third party, and vanish when the subscription lapses.
bernard takes the opposite stance:
- It lives in your repo. Clone the repo, get the memory. No service to run, nothing to provision.
- Zero API keys, zero embeddings, zero network calls. Everything is local files + a pure algorithm.
- It's structured and queryable. Typed records, tags, synonyms, recency weighting, and supersession — not a flat dump.
- It's MCP-native. Your agent uses it automatically; you rarely touch the CLI.
- It's private by construction. Data never leaves the repo, so it's GDPR/KVKK-friendly by default.
Smart without an API key
How is "no API key" still smart? Intelligence comes from two places, neither of which needs the network:
- The agent itself. An agent like Claude Code is already capable. bernard's job isn't to understand — it's to put the right past records in front of the agent. bernard retrieves, the agent reasons.
- BM25 retrieval. A classic, robust, dependency-free full-text ranking algorithm, plus synonym expansion (e.g.
login→auth), recency weighting (newer decisions rank higher), and record invalidation (superseded decisions drop out). All local, all instant.
The bridge between the two is bernard. No keys, no subscription, no privacy leak.
Quick start (with Claude Code)
# In your project, set up the memory store once:
npx -y bernard-mcp init
Add bernard as an MCP server in your project's .mcp.json:
{
"mcpServers": {
"bernard": {
"command": "npx",
"args": ["-y", "bernard-mcp", "mcp"]
}
}
}
That's it. Your agent can now call bernard_search before starting a task and bernard_add to record decisions. Commit .bernard/ and your teammates inherit the same memory.
Installation
Run on demand with npx (no install):
npx -y bernard-mcp <command>
Or install globally to get a bernard binary:
npm i -g bernard-mcp
bernard --help
Or add it as a dev dependency:
npm i -D bernard-mcp
Requires Node.js ≥ 20.
CLI usage
Every command has an English name and a Turkish alias (e.g. add / ekle). Output language follows your locale — see Internationalization.
Initialize
bernard init
Creates (never overwrites existing files):
.bernard/records.jsonl— the records (JSONL, committed).bernard/categories.json— types + synonyms (committed).bernard/config.json— project settings (committed)- adds
.bernard/.cache/to.gitignore - adds
.bernard/records.jsonl merge=unionto.gitattributes
Add a record
bernard add --type decision --title "Switched to Postgres" \
--content "MySQL → Postgres for JSONB and better indexing." \
--tags db,architecture
Run bernard add with no flags in a terminal and it asks interactively.
Search
bernard search authentication flow
bernard search "token refresh" --type bug
bernard search migration --tag db
Results print with score, type, title, date, tags, and a content snippet.
List
bernard list
bernard list --type decision
bernard list --tag auth
Invalidate (supersede)
bernard invalidate ber_abc123
bernard invalidate ber_abc123 --by ber_def456
Marks a record invalid without deleting it (invalid: true). --by links the record that replaces it, preserving the decision history. Search drops invalid records by default.
To record a new decision that directly supersedes an old one:
bernard add --type decision --title "Switched to Param" \
--content "Better fees than the previous provider." --tags payment \
--supersedes ber_abc123
Distill
bernard distill
bernard distill --apply
Surfaces possible conflicts (multiple records with the same type + tags), supersession chains, and suggestions. For each conflict it proposes keep the newest, invalidate the rest and prints ready-to-run bernard invalidate … --by … commands. --apply (TTY) applies them interactively. It never deletes anything — it only marks and suggests.
Stats
bernard stats
A summary dashboard: total records, distribution by type, top 5 tags, invalid count, oldest/newest dates, and source (manual/agent) breakdown.
Tags (synonym management)
bernard tags
bernard tags --add "auth=login,session"
bernard tags --remove "auth"
Lists/edits the types and synonym groups in categories.json. Synonyms power query expansion at search time, so the team's vocabulary becomes the team's recall.
Suggest (draft from commits)
bernard suggest --n 3
bernard suggest --add
Drafts records from recent commits: type guessed from the title (feat→decision, fix→bug, refactor→architecture, docs/chore→note) and tags from changed directories. This is only a starting point — the real understanding is the agent's. --add (TTY) saves drafts after you confirm.
Install hook (post-commit reminder)
bernard install-hook
bernard install-hook --remove
Installs a marked, idempotent .git/hooks/post-commit reminder to run bernard suggest after each commit. It never overwrites a non-bernard hook (it prints a snippet to add by hand). --remove removes only bernard's hook.
MCP integration (the main path)
bernard's real power is connecting to an AI agent. As an MCP server it exposes six tools that an agent uses automatically — searching prior decisions before it starts, recording new ones, and invalidating stale ones.
Add to your project's .mcp.json:
{
"mcpServers": {
"bernard": {
"command": "npx",
"args": ["-y", "bernard-mcp", "mcp"]
}
}
}
If you installed globally, use the binary directly:
{
"mcpServers": {
"bernard": {
"command": "bernard",
"args": ["mcp"]
}
}
}
The server exposes:
| Tool | What it does |
|---|---|
bernard_search |
Search past decisions, architecture notes, fixed bugs, and preferences. The key tool — the agent calls it before starting work. |
bernard_add |
Add a new record (source: "agent"); validates the schema before writing. |
bernard_list |
List records, optionally filtered by type/tag. |
bernard_invalidate |
Invalidate a record (id, optional by). Marks, never deletes. |
bernard_update |
Update fields of an existing record; only provided fields change. |
bernard_suggest |
Fetch recent commits + diffs so the agent can extract records worth keeping. |
Works with any MCP client (Claude Code, Cursor, and others) — the config block is the same, just placed wherever that client reads MCP servers.
Data model
Records live in .bernard/records.jsonl, one JSON object per line:
{
"id": "ber_a1b2c3d4",
"type": "decision",
"title": "Switched to Postgres",
"content": "MySQL → Postgres for JSONB and better indexing.",
"tags": ["db", "architecture"],
"created": "2026-06-27T10:00:00.000Z",
"invalid": false,
"superseded_by": null,
"source": "manual"
}
Record types:
| type | Meaning |
|---|---|
decision |
A decision that was made |
architecture |
An architecture note / structural fact |
bug |
A fixed bug and its fix |
preference |
A style / approach preference |
note |
A free-form note |
JSONL is line-oriented: appends are cheap, git diffs stay clean (one record per line), it's machine-friendly, and still human-readable via git log -p.
Invalidation lifecycle
bernard never deletes knowledge; it invalidates stale decisions and preserves history. A new decision is added (optionally with --supersedes) linking the old one; the agent can do the same via bernard_add + bernard_invalidate. Over time, bernard distill surfaces conflicts and gives you ready commands to keep the newest and invalidate the rest. Invalid records stay in the file but drop out of search; the superseded_by chain answers "why did this change?".
Team sharing (merge=union)
init adds a .gitattributes line:
.bernard/records.jsonl merge=union
Because each record is an independent line in an append-mostly file, git unions both sides when two branches add different records — so teammates writing to memory at the same time don't hit needless merge conflicts.
Architecture: what's committed, what's local
your-project/
├── .git/
├── .gitignore # .bernard/.cache/ is added here
├── .gitattributes # .bernard/records.jsonl merge=union
├── .mcp.json # agent integration (committed)
└── .bernard/
├── records.jsonl # COMMITTED — shared memory
├── categories.json # COMMITTED — types + synonyms
├── config.json # COMMITTED — project settings
└── .cache/ # LOCAL — gitignored
└── index.json # BM25 index (derived, auto-rebuilt)
Committed: records.jsonl, categories.json, config.json. Memory is code — it should be shared, reviewed, and versioned with git history.
Local: .bernard/.cache/. The BM25 index.json is fully derivable from records.jsonl (rebuilt automatically when the records change, or when the locale changes). Committing derived data would only create meaningless conflicts.
Internationalization
bernard ships bilingual: English (default) and Turkish. All user-facing output is localized; command names stay English with Turkish aliases, and stable identifiers (record types, field names) stay canonical.
Locale precedence (highest first):
--locale <en|tr>flag (e.g.bernard --locale tr stats)BERNARD_LOCALEenvironment variablelocalein.bernard/config.json(set atinit, committed → team-wide)en(default)
Retrieval is locale-aware too: Turkish gets a conservative stemmer (so tokenları matches token); English uses lowercase tokenization (BM25 + synonyms carry the recall).
Configuration
.bernard/config.json:
{
"project": "my-app",
"locale": "en",
"retrieval": {
"max_results": 8,
"recency_weight": 0.3,
"half_life_days": 30,
"type_weights": {
"decision": 1.2,
"architecture": 1.2,
"bug": 1.0,
"preference": 1.0,
"note": 0.9
}
}
}
recency_weight— how much newer records are boosted.half_life_days— how fast the recency boost decays.type_weights— per-type ranking multipliers.
Roadmap
- Optional local embeddings (Ollama) — keep BM25 as the keyless default; add a fully local, keyless semantic layer for those who want it.
- Automatic extraction from commits/diffs — propose decision/architecture records straight from git history.
- Team conflict resolution — a shared flow for the conflicts
distillfinds: flag, discuss, invalidate. distillautomation — periodic distillation and warnings via pre-commit hook or CI.
Contributing
Contributions are welcome — see CONTRIBUTING.md. The project is intentionally dependency-light (only the MCP SDK + zod for the server) and ships a zero-dependency smoke test:
npm install
npm test
Please also read the Code of Conduct and the Security Policy.
License
MIT © Yener Yiğit Çelik
Not generated — crafted.