agent-kb
v0.4.0
Published
Local-first, project-scoped knowledge base engine for AI agents: reviewable change sets, structured queries, relationship tracing, FTS search, and task-specific context packs. CLI + in-process API + optional MCP server.
Maintainers
Readme
Agent KB
A local-first, project-scoped knowledge base engine for AI agents. Agent KB lives in a hidden .agent-kb/ folder inside your project, stores entities and relationships as human-readable files (YAML/JSON), and exposes them via a reviewable change-set workflow: agents propose changes, humans (or CI) accept or reject them. The engine offers structured queries, relationship tracing, FTS search, and task-specific context packs — accessible over a CLI or an in-process API.
Requirements
- Node.js >= 20
Install / Quickstart
No install needed for one-off use — just run via npx:
npx agent-kb init
npx agent-kb context TASK-001For repeated use in a project, install locally:
npm install agent-kbTypical flow
# 1. Initialise a KB in the current project
npx agent-kb init
# 2. Check KB health and pending change sets
npx agent-kb status
# 3. Propose a change set from a JSON file (create/update/link ops)
npx agent-kb propose change-set.json
# 4. Review what is pending
npx agent-kb changesets
# 5. Accept or reject a change set by its ID
npx agent-kb accept CS-001
npx agent-kb reject CS-002 --reason "duplicate"
# 6. Query the KB
npx agent-kb get TASK-001
npx agent-kb find Task
npx agent-kb related TASK-001
npx agent-kb trace TASK-001
npx agent-kb why TASK-001 --in-context FEAT-001
npx agent-kb search "authentication flow"
# 7. Get a task-specific context pack
npx agent-kb context TASK-001
# 8. Scan the project for untracked files and validate a change set or entity
npx agent-kb scan
npx agent-kb validate CS-001
# 9. Docs and housekeeping
npx agent-kb docs generate
npx agent-kb verify
npx agent-kb reindexFull command reference: npx agent-kb --help or npx agent-kb <command> --help.
Schema
schema apply <file> [--dry-run] validates and installs a schema file (dry-run validates only, without writing). schema describe prints the active KB schema — every entity type with its fields, required flags, and relations, plus the available context recipes.
npx agent-kb schema describeMCP agents use kb_describe_schema for the same information. Call it first before any read or write to discover available entity types and field names.
Relation endpoints (union / wildcard)
A relation's from and to each accept one of three forms:
{ "kind": "relation", "name": "IMPLEMENTED_BY", "from": "Feature", "to": "CodeFile", "cardinality": "one_to_many" }
{ "kind": "relation", "name": "AFFECTS", "from": "ArchitectureDecision", "to": ["Feature", "UseCase"], "cardinality": "many_to_many" }
{ "kind": "relation", "name": "MENTIONS", "from": "KBChangeSet", "to": "*", "cardinality": "many_to_many" }- a single type name (
"Feature") — the endpoint must be exactly that type; - a union array (
["Feature", "UseCase"]) — the endpoint may be any of the listed types (non-empty, no duplicates); - the wildcard
"*"— the endpoint may be any defined entity type.
This replaces the workaround of defining one AFFECTS_<Type> relation per target type. A link is validated against the endpoint (expected one of Feature|UseCase, got Task); describe/data-model.md render the union readably. on_delete is a single { from, to } policy that applies uniformly to every member type — per-member-type delete policies are not supported. Cascade/block/detach follow the relation's links regardless of the endpoint's concrete type, and related/trace return neighbors of every member type. Using '*' with a cascade on_delete policy deletes every entity linked via that relation, regardless of type — treat it as a broad hammer and use it deliberately. Cardinality constraints count across all member types of a union: for a one_to_many relation with a union endpoint, the "one" limit is global, not per member type. For verify's orphan check, a '*' endpoint is opt-in linkage, not an obligation: it does not enroll entity types into orphan-flagging — a type is only expected to have links when an endpoint names it explicitly (single type or union member).
Extending types
An extend def adds new fields to an existing type without redefining it — no satellite entity + relation workaround. Apply it like any schema file (schema apply) or drop it in a numbered schema/NNN_*.json:
{
"kind": "extend",
"entity": "UseCase",
"fields": { "axis": { "type": "enum", "values": ["discovery", "activation", "retention"] } },
"search_fields": ["axis"]
}The added fields merge into the target and are thereafter indistinguishable from base fields — createable, queryable, shown by schema describe, and (when listed in search_fields) full-text searchable. Extensions are additive and optional-only: a field with required: true is rejected (existing entities have no value to backfill — use migrate for required-field evolution), and a field name that already exists on the target is a conflict. Extending an unknown entity is an error. To change identity (id_prefix) or a lifecycle, or to remove/retype a field, use migrate instead.
Migrations
Schema evolution beyond additive extend lives in numbered migration files under .agent-kb/migrations/*.json ({ "id": "...", "schema_ops": [ ... ] }), applied in id order by npx agent-kb migrate. Ops include add_field, remove_field, rename_field, change_field, add_enum_value, create_entity, rename_entity, create_relation, remove_relation, and copy_field. Destructive steps (dropping a field, an incompatible type change, narrowing an enum) require --allow-data-loss. Already-applied migrations are guarded by checksum; the post-migration schema is consolidated into schema/_migrated.json.
copy_field — cross-entity data copy via a relation
copy_field back-fills a field on one entity type from a linked entity of another type — e.g. retire a satellite UseCaseAxis after folding its trigger onto UseCase with extend, then copy the values across the HAS_AXIS relation:
{
"id": "0007-fold-axis-trigger",
"schema_ops": [
{ "kind": "add_field", "entity": "UseCase", "field": "trigger", "def": { "type": "text" } },
{
"kind": "copy_field",
"from_entity": "UseCaseAxis",
"from_field": "trigger",
"to_entity": "UseCase",
"to_field": "trigger",
"via_relation": "HAS_AXIS",
"direction": "out",
"overwrite": false
}
]
}Semantics — per to_entity instance, follow via_relation in direction (out is the default, as seen from the to_entity; in walks the link inbound) to its linked from_entity neighbour(s) and copy from_field → to_field:
- exactly one source → the value is copied when the source field is present and the target is empty (or
overwrite: true); - zero sources (or an absent source field) → skipped silently, target left as-is;
- more than one source → the migration fails loud pre-write, listing every offending target id (it cannot pick one);
overwritedefaults tofalse(a target that already has a value is kept);- a union
via_relationonly counts neighbours actually typed asfrom_entity; - each copied value is validated against the target field — including
ref/ list-of-ref targets, whose ids must exist and match the field's declared entity type (no dangling refs slip in).
Split renames and copies into separate migrations. A copy_field composed with a rename_field / rename_entity touching either of its entities in the same migration is rejected (MIGRATION_ERROR) — the copy reads pre-migration state while the rename rewrites it, which would silently clobber renamed values or copy nothing. Run the rename in one migration, then the copy in the next (or vice versa). The same advice applies to a change_field that retypes the copy's target field (e.g. re-pointing a ref): split it out.
Widening an existing relation's endpoint
To widen a relation's from/to (e.g. from a single type to a union, or to '*'), remove_relation then create_relation with the same name in one migration — the net effect is the relation redefined with the broader endpoint, applied atomically:
{
"id": "0008-widen-affects",
"schema_ops": [
{ "kind": "remove_relation", "name": "AFFECTS" },
{
"kind": "create_relation",
"def": {
"kind": "relation",
"name": "AFFECTS",
"from": "ArchitectureDecision",
"to": ["Feature", "UseCase"],
"cardinality": "many_to_many"
}
}
]
}Snapshot and export
snapshot create [--name <n>] writes a .tar.gz archive of the canonical files (entities, events, change sets, schema) to .agent-kb/snapshots/ — a portable point-in-time backup of the KB's source of truth (derived caches are rebuilt via reindex).
export --format json|graph exports the full KB in one of two projections:
npx agent-kb snapshot create --name before-migration
npx agent-kb export --format json # flat payload with all entities
npx agent-kb export --format graph # nodes + edges graph projectionAgent-skill onboarding
init --skill [global|project] (default project) installs the agent-kb Claude skill alongside KB initialisation. skill install [scope] installs or reinstalls the skill independently.
# Install the KB and the Claude skill in one step (project scope, default)
npx agent-kb init --skill
# Install to the global Claude skill directory instead
npx agent-kb init --skill global
# Reinstall the skill at any time
npx agent-kb skill install
npx agent-kb skill install globalProject scope (project, default) writes to <root>/.claude/skills/agent-kb/SKILL.md — visible only in that project.
Global scope writes to ~/.claude/skills/agent-kb/SKILL.md — available in every project.
Status
npx agent-kb status (or kb_status over MCP) returns:
| Field | Type | Meaning |
|---|---|---|
| entities | Record<string, number> | Entity count by type |
| pending_change_sets | number | Change sets awaiting accept / reject |
| index_healthy | boolean | FTS derived-index fingerprint matches canonical |
| vector_enabled | boolean | Semantic / vector search is available |
| vector_index_stale | boolean | Vector index may be out of date (cleared by reindex) |
vector_index_stale is set when a write's post-commit vector update fails (e.g. the embedding backend is unreachable) or when a startup-recovery rebuild could not re-embed. Run npx agent-kb reindex to rebuild the vector index and clear the flag. The flag is always false when vector_enabled is false.
Available commands: init, status, reindex, migrate, get <id>, find <type>, related <id>, trace <id>, why <id>, search <text>, context <id>, propose <file>, validate <idOrFile>, accept <id>, reject <id>, scan, verify, lint, docs generate, changesets, schema apply <file>, schema describe, snapshot create, export, semantic enable, semantic disable, skill install.
Embedding (in-process API)
Use the agent-kb/api entry point to call the engine directly from Node.js — no subprocess, no network:
import { openKb } from 'agent-kb/api';
const kb = openKb({ root: process.cwd() });
// Get a task-specific context pack
const res = kb.context({ id: 'TASK-001' });
if (res.ok) {
// res.data: ContextPack
console.log(res.data);
} else {
// res.error: KbErrorBody (code + message)
console.error(res.error);
}
// Always release the SQLite handle when done
kb.close();Every engine method returns an Envelope: { ok: true, data: T } on success or { ok: false, error: KbErrorBody } on failure — it never throws across the API boundary.
The openKb function accepts { root, clock?, lock?, embeddingProvider? }. If .agent-kb/ does not exist yet, the returned engine allows only init(); all other methods return a NOT_FOUND envelope until you call init() and re-open.
Transports
| Transport | How to use |
|-----------|-----------|
| CLI | npx agent-kb <command> — the agent-kb bin ships in dist/cli/index.js |
| In-process API | import { openKb } from 'agent-kb/api' — same engine, zero overhead |
| MCP server | npx agent-kb mcp launches the MCP server over stdio. Opt-in and config-gated: it requires mcp.enabled: true in .agent-kb/config.yml (off by default), otherwise it exits with a FEATURE_DISABLED error. Stdio is the only transport (no HTTP/SSE). createMcpServer remains internal — it is not re-exported from agent-kb/api; the CLI bundles it into dist/cli/index.js. Exposes 17 tools (kb_get_entity, kb_find_entities, kb_related, kb_trace, kb_search, kb_get_context_pack, kb_propose_change_set, kb_validate_change_set, kb_accept_change_set, kb_reject_change_set, kb_record_agent_run, kb_status, kb_reindex, kb_list_change_sets, kb_verify, kb_lint, kb_describe_schema). The server emits orienting instructions on connect (MCP_INSTRUCTIONS) so agents know to call kb_describe_schema first. Change-set and query inputs are typed Zod schemas with inline .describe() strings — agents receive field-level documentation directly from the JSON Schema. |
Configurable doc projections
docs generate writes one Markdown file per entity type. By default it generates the eight built-in projections (product.md, features.md, use-cases.md, …) plus data-model.md and traceability.md.
You can replace the eight built-in projections with your own set via docs.views in .agent-kb/config.yml. Each entry names a plain *.md basename (no subdirectories), the entity type it projects, and a heading title:
docs:
views:
- file: phase-2-tasks.md
entity: PhaseTask
title: Phase 2 Tasks
- file: epics.md
entity: Epic
title: Epicsdata-model.md (the schema projection) is always written alongside the custom views. When docs.views is absent the built-in eight are used unchanged. Note: traceability.md belongs to the built-in default set (it projects the stock Feature type) and is NOT emitted when docs.views is configured — custom views take full ownership of the view set. To override this behavior, set docs.traceability: true (always emit traceability.md, even with custom views) or docs.traceability: false (never emit, even with default views); omitting the flag keeps the default behavior. Validation rules: file must be a plain *.md basename (no path separators, no .., not absolute; the names data-model.md and traceability.md are reserved, case-insensitively); entity must be a valid identifier ([A-Za-z][A-Za-z0-9_]*); title must be non-empty; duplicate file names within the array are rejected at config load (case-insensitively). Unknown entity types are caught at generate time — all types are validated before anything is written. Changing the view set does not remove previously generated files; clear the docs directory (or delete old projections) manually when reshaping views.
context_recipes are likewise per-KB configurable in config.yml — see the context_recipes key in the config for per-task context packs.
Lint (policy rules)
verify checks BUILT-IN structural integrity (referential consistency, dangling links, staleness) and is non-configurable. lint is its policy counterpart: it evaluates PROJECT-SPECIFIC rules you define in lint.rules, and is empty by default (zero behavior change until you add rules).
Three rule types (V1), a discriminated union on rule:
| Rule | Fires when |
|---|---|
| required_relation | an entity of entity has fewer than min (default 1) links via relation in direction (out default, or in) |
| field_pattern | a present string value of field fails regex (absent / non-string values are skipped; an optional message overrides the finding text) |
| require_field | field is absent, '', or null (policy-level presence — 0 and false count as present) |
lint:
rules:
- rule: required_relation
entity: Feature
relation: IMPLEMENTED_BY
direction: in
min: 1
- rule: field_pattern
entity: Feature
field: key
regex: "^FEAT-[0-9]+$"
message: "Feature.key must look like FEAT-123"
- rule: require_field
entity: Feature
field: ownerRun npx agent-kb lint (or kb_lint over MCP). Like verify, it is a CI gate: any finding exits 1; a clean report exits 0 (there are no advisory rule types in V1 — every finding blocks). The report ({ findings, rules_evaluated }) is printed either way; use --json for a machine-readable envelope. Findings are ordered deterministically by rule order, then entity id. The regex of a field_pattern rule is validated to compile at config load, and entity / relation / field must be safe identifiers ([A-Za-z][A-Za-z0-9_]*); a rule referencing an unknown entity type, an unknown field name, or an unknown relation name yields a single per-rule finding rather than an error (a mistyped rule can never silently pass). Note: field_pattern applies to string-valued fields only (text, longtext, enum, date, ref, json); targeting a field of type int, number, bool, or list (which can never hold a string) is flagged with a per-rule finding rather than silently matching nothing.
Semantic search — standalone
Off by default. When enabled, semantic search runs fully in-process: the embedding model is a small (~34 MB) quantized model (Xenova/bge-small-en-v1.5, 384-dim) that runs locally via @huggingface/transformers — no external service, no API key. It is consent-gated: the model is downloaded only when you say yes.
Enabling it
At setup (interactive). On a terminal, init asks once:
Enable semantic search? Downloads an embedding model (~34 MB) once; fully local afterwards. [y/N]Plain Enter (or n) leaves it off. Answering y downloads and warms the model right then. Flags skip the prompt deterministically:
npx agent-kb init --semantic # enable now, download the model, no prompt
npx agent-kb init --no-semantic # leave it off, no promptNon-interactive runs (CI, scripts, agents — no TTY, or --json) never prompt: semantic stays off and a one-line hint points at semantic enable. (A failed download during init --semantic leaves the KB initialised and semantic off — nothing half-enabled — and tells you to retry with semantic enable.)
Later (any time).
npx agent-kb semantic enable # download the model + reindex existing entities
npx agent-kb semantic disable # turn it back offsemantic enable requires an existing KB. It downloads/warms the model, then reindexes so all existing entities are backfilled — semantic search works immediately afterward (no separate reindex needed). On any failure the config is rolled back and nothing is left half-enabled. semantic disable flips search.vector_enabled off but leaves the provider config and the vectors.sqlite data in place — re-enabling is cheap (no re-download, and reindex restores the index). A later plain semantic enable preserves a previously configured custom model (embedding_model/embedding_dim are reused when the provider is already transformers); pass --model explicitly to switch models. A non-384-dim custom model additionally needs embedding_dim set manually in config.yml.
One-time download, then offline
The model is cached at ~/.cache/agent-kb/models on first use. After that first download, semantic search is fully offline — no network access is ever required again.
Air-gapped / custom model
Point enable at a local model directory (or a different model id) with --model:
npx agent-kb semantic enable --model /abs/path/to/local-modelThe path is passed straight to the provider, so a pre-downloaded model works with no network at all. A custom model with a dimension other than 384 needs search.embedding_dim set manually in .agent-kb/config.yml.
Ollama is still available
The Ollama provider remains a config option for those who prefer a shared embedding server: set search.vector_enabled: true, search.embedding_provider: 'ollama', pull the model (ollama pull nomic-embed-text), then npx agent-kb reindex. The standalone transformers provider above is the zero-setup default that the consent flow configures.
Search modes
| Mode | What it matches | When to use |
|------|----------------|-------------|
| fts (default) | Porter-stemmed keyword tokens in search_fields | Exact / fuzzy keyword lookup |
| semantic | Meaning similarity via vector embeddings (cosine) | Paraphrase or concept queries — finds what FTS misses |
| hybrid | RRF fusion of FTS + vector rankings | Best of both — surface both keyword and meaning matches |
# keyword match (default)
npx agent-kb search "authentication"
# meaning match — finds "Verifying user credentials" even without the word "login"
npx agent-kb search "login" --mode semantic
# fused ranking (Reciprocal Rank Fusion)
npx agent-kb search "login" --mode hybridError surfaces
FEATURE_DISABLED—semanticmode returns this error whensearch.vector_enabledisfalseor no embedding provider is configured.hybriddegrades gracefully to FTS-only (no error).VECTOR_INDEX_FAILED— returned byacceptwhen the vector-index update fails in the post-commit write-path hook (e.g. the embedding backend errors or is unreachable). Canonical data and FTS are consistent; the vector index is stale untilreindexis run.
Project documents
For contributors and deeper understanding:
agent-kb-product-spec.md— external contracts (file formats, JSON ops, error codes, acceptance criteria)agent-kb-technical-design.md— internal engineering design (module decomposition, engine API, core types, storage internals, key flows)TICKETS.md— index of the 48 implementation tickets: build-order groups, execution order, critical path
License
Proprietary — all rights reserved. See LICENSE for the full terms. This software is not open source and no license is granted to use, copy, modify, or distribute it without the prior written permission of the copyright holder.
