Agent-readable docs index: /docs/llms.txt. Full docs in one file: /docs/llms-full.txt. Download /docs/docs.zip to grep all markdown files locally.

Import and export

Every import turns records into events. Events go through the same extraction as anything you remember, so the newest statement replaces the one it contradicts. Nothing is written until you drop --dry-run.
kaibo import --dry-run memories.json # report only, no key needed, nothing sent kaibo import memories.json --from mem0 # append the events kaibo import memories.json --from mem0 # again: nothing new, same keys
The source is guessed from the file name and content when you leave out --from. Pass - to read stdin. A directory is read for Markdown files.

Safe to repeat

Each event gets an idempotency key made from the source, the record's own id and a digest of the event. Running the same import twice stores nothing the second time. If the source edited a record since your last import, that record has a new digest, so it arrives as a newer event instead of failing with idempotency_mismatch.
Events are appended oldest first when every record has a time, because newer statements replace older ones.

What each source reads

SourceInputImportedSkipped
mem0get_all / POST /v3/memories/ response ({results: [...]}), a bare array, JSON Lines, or a structured memory export objectone event per memory, with user_id, agent_id, categories, metadata; time is updated_at, else created_atmemories with replaced_by, memories past expiration_date, empty ones. Graph relations are ignored. A structured export becomes one event per field
zepwhat Zep's list calls return: thread messages, graph edges (facts), episodes. A bundle {threads, edges, episodes}, a list page {items}, an array or JSON Linesuser and assistant messages, valid facts (time is valid_at), text and json episodessystem and tool messages, facts with invalid_at or expired_at, message episodes (the thread has them), nodes and summaries
lettaan Agent File (.af), including the older form that is JSON encoded as a stringeach memory block as one event, and each agent's user and assistant messagessystem prompts, tool messages and tool-call-only replies, tools and their source code, secrets and environment variables, data sources. Archival passages are not in the format yet
jsonlone record per line: {text}, {content}, {memory}, {message} or {body} with optional id and created_at; a plain string line; an event as the API takes it; or a line from kaibo exportthe textlines without text, lines that are not JSON, memory lines from an export (kaibo rebuilds those)
markdowna file, or a directory of themone event per heading section, text starts with the heading path; date from front matter becomes the timesections without text. Long sections are cut at paragraph breaks into parts
Pass --include-invalid to keep records the source marks as expired, replaced or invalidated. They are statements that stopped being true, so the default leaves them out.

How these were checked

Mem0 and Zep do not offer one export file, so the readers follow the response shapes in their current API docs (Mem0 v3 memories, Zep v4 list calls). The Letta reader was run against real .af files from the letta-ai/agent-file repository, including one that is double encoded. The Mem0 and Zep test fixtures are built from the documented fields, not captured from an account. If your export does not parse, the dry run tells you which record and why.

Dry run report

with-history.af format : letta agent file (.af) read : 8 would create : 4 skipped : 4 skipped #3: role "system" is not conversation 2026-02-02 We moved the release target from July 13 to July 17.
--json prints the full report: counts, up to 25 skipped records with the reason, warnings, and the first events as they would be stored.

From the API

curl -X POST "$KAIBO_URL/v1/imports?dryRun=true" \ -H "Authorization: Bearer $KAIBO_KEY" -H "Content-Type: application/json" \ -d "$(jq -n --rawfile c memories.json '{source:"mem0",content:$c}')"
POST /v1/imports takes the file text in content and needs a key that can write, for a dry run too. One request holds about 1 MB and 2,000 events; above that it returns payload_too_large and the message says so. kaibo import has no such cap: it parses locally and sends 100 events at a time.
const dry = await kaibo.imports.create({ source: "mem0", content }, { dryRun: true }); const done = await kaibo.imports.create({ source: "mem0", content }); await kaibo.jobs.wait(done.appended!.jobId!);
dry = kaibo.imports.create("mem0", content, dry_run=True)

Export

kaibo export > backup.jsonl # every stream, events and memories kaibo export --stream chat --no-memories # one stream, events only kaibo import backup.jsonl --from jsonl --stream copy # into another project
POST /v1/exports streams JSON Lines: a header, then event and memory records, then an end record with counts. A file without an end line was cut off, and the SDKs raise an error for it. Past 50,000 records the server stops and sets truncated: true; the CLI exits 1 and tells you to export one stream at a time. Each key may export 10 times an hour. It needs the read scope.
Event lines are what GET /v1/events returns, so importing them with jsonl keeps the original idempotency keys. Memory lines are derived data and are skipped on import.