#me import

Get data into Memory Engine — one subcommand per source.

#Commands

There is no bare default: me import <file> does not parse — use me import memories <file>.


#me import memories

Import memory records from files or stdin. me memory import is an alias of this command.

me import memories [files...] [options]

See me memory import for the full option reference, format detection, skip semantics, and chunking behavior, and File Formats for the record schemas.


#me import claude / codex / opencode

Import agent sessions from each tool's native storage. The per-agent spellings (me claude import, me codex import, me opencode import) are aliases of these commands. OpenCode imports prefer the current SQLite database at ~/.local/share/opencode/opencode.db and fall back to the legacy JSON storage/ layout.

me import claude [options]
me import codex [options]
me import opencode [options]

See agent session imports for the shared option reference, tree layout, idempotency rules, content shape, and metadata schema.


#me import granola

Import meetings from Granola — one memory per meeting, holding the AI summary notes and (by default) the full transcript. Past meetings become searchable agent context ("what did we decide about X", "who was in the Y review").

me import granola [options]
Option Description
--tree-root <path> Tree root under which <document_id> leaves are placed. Default: ~/granola.
--since <iso> Only import meetings started at or after this ISO 8601 timestamp.
--until <iso> Only import meetings started at or before this ISO 8601 timestamp.
--no-transcript Import notes only, skipping the full meeting transcript (and its per-meeting API call).
--include-invalid Include notes Granola did not flag as a valid meeting (ad-hoc notes, calendar stubs).
--granola-dir <dir> Override the Granola application-support directory (default: the standard macOS path).
--dry-run Fetch and report what would be imported without writing.

#Authentication (no separate login)

The import reuses the Granola desktop app's existing session — there is no separate me-side Granola login. It reads Granola's locally-stored, safeStorage-encrypted WorkOS tokens (decrypting them via the macOS login keychain), refreshes the short-lived access token through Granola's API, then pulls your meetings. Requirements:

  • The Granola desktop app is installed and signed in on this machine.
  • macOS only for now (the credential read uses the login keychain). On other platforms the command exits with an actionable error.

If the token refresh fails (e.g. Granola has been signed out), open the Granola app to refresh its session and re-run.

#Tree layout

Each meeting is a named leaf (its Granola document_id) under the tree root:

<tree-root>/<document_id>

The default root is your personal home (~/granola), so meetings are private to you. Pass --tree-root /share/meetings (or similar) to import into a shared space instead.

#Content shape

Each memory's content is a self-contained Markdown document: a title heading, a metadata line (date, attendees), the AI summary notes, and — unless --no-transcript — the full transcript. Notes are sourced, in order of preference, from the meeting's own notes_markdown, then an AI summary panel (its structured content, else its HTML). Transcript segments are grouped into speaker turns labelled Me (your microphone) and Them (everyone else); Granola does not attribute remote speakers by name.

Meetings Granola flagged as not a valid meeting are skipped by default (--include-invalid keeps them), as are meetings with neither notes nor a transcript.

#Idempotency and re-runs

Idempotency is keyed on (tree, document_id) — each meeting is named by its Granola document id. The id is a timestamp-prefixed UUIDv7 (meeting start in the prefix, random tail), so meetings sort by date on the id. Re-imports reconcile in place via the server's content-aware upsert: an unchanged meeting is a no-op, a meeting whose notes/transcript changed (or an importer-version bump) is rewritten, and nothing is ever duplicated. Run it on a schedule to keep your meeting memory current.

#Metadata

Key Description
type Always "granola_meeting".
source_tool Always "granola".
source_document_id Granola document id (also the leaf name).
display_name Human label for the web tree ("Title — YYYY-MM-DD"); the leaf name stays the document id so re-imports stay idempotent.
source_workspace_id Granola workspace id (when present).
source_calendar_event_id Google Calendar event id (when the meeting has one).
attendees Calendar attendee emails (when present).
content_mode "with_transcript" or "notes_only".
has_notes / has_transcript Whether each section was captured.
transcript_segment_count Number of transcript segments.
valid_meeting Granola's valid-meeting flag (when set).
importer_version Version tag of the importer schema.

Temporal spans the meeting: calendar start→end when known, else the meeting's created time and last transcript segment.

#Example

me import granola --dry-run            # preview everything Granola has
me import granola                      # full import (notes + transcripts) into ~/granola
me import granola --no-transcript      # notes only (faster; fewer API calls)
me import granola --since 2026-01-01   # just this year's meetings

#me import git

Import a repo's git commit history as memories — one memory per commit, holding the commit message plus a capped changed-file list. Commit intent ("why did we do X") and touched paths become searchable agent context.

me import git [repo] [options]
Argument Required Description
repo no Path inside the repo to import. Default: the current directory.
Option Description
--branch <rev> Branch, tag, or rev to walk. Default: HEAD.
--since <date> Only commits at/after this date (any format git accepts).
--until <date> Only commits at/before this date.
--max-count <n> Import at most this many recent commits.
--full Walk the full history (skip the incremental high-water lookup).
--no-merges Drop all merge commits.
--no-file-list Omit the changed-file list from commit memories.
--tree <path> Full project tree to place git_history under (no slug appended — me import git is single-repo). Default: the target repo's .me tree (resolved from the repo path, so it works from any cwd; its .me server/space pins apply too, whitelist-gated), else <tree_root>/<slug> (your global tree_root override, default the private ~/projects).
--dry-run Parse and report what would be imported without writing.
-v, --verbose Per-commit progress output.

#Tree layout

Each commit is a named leaf (the commit <sha>) under the project's git_history node:

<tree>/git_history/<sha>

<tree> is the full project node — --tree, else the repo's .me tree, else <tree_root>/<project_slug> (the private ~/projects unless your global config overrides tree_root). The default project slug is derived exactly as for agent session imports (git remote repo name, else repo root directory name), so a project's commit history sits next to its agent_sessions node — e.g. a commit lands at ~/projects/memory_engine/git_history/<sha> and is addressable by that path (or at /share/projects/memory_engine/git_history/<sha> when the project's .me tree pins the shared layout).

#Content shape

Each memory's content is the commit subject, the body (truncated past 64 KiB), and a Files: block listing up to 50 changed paths with (+added -deleted) line counts ((binary) for binary files). --no-file-list omits the block.

Merge commits with no message body (Merge branch 'x' boilerplate) are skipped by default; merges that carry a body — GitHub PR merge commits put the PR title there — are imported. --no-merges drops all merges.

#Idempotency and incremental re-runs

Idempotency is keyed on (tree, sha) — each commit is named by its sha. The id is a timestamp-prefixed UUIDv7 (commit date in the prefix, random tail), so commits sort by date on the id. Re-imports are server-side no-ops: an already-imported commit is skipped, never duplicated.

Re-runs are also incremental: the newest already-imported commit is looked up server-side, and when it is an ancestor of the target rev only <sha>..<rev> is walked. After a force-push (or when importing a different branch) the walk falls back to the full log — still safe, because the (tree, sha) key dedupes the overlap. Explicit bounds (--since, --until, --max-count, --full) always walk exactly what they say.

#Metadata

Key Description
type Always "git_commit".
sha Full 40-hex commit sha.
source_git_repo Git remote URL (when the repo has one).
source_project_slug ltree-safe project label (same as the tree subnode).
author_name / author_email Commit author.
author_date / commit_date ISO 8601 author and committer dates.
files_changed / insertions / deletions Change stats (binary files excluded from line counts).
is_merge true on merge commits (absent otherwise).
importer_version Version tag of the importer schema.
$prev Path of the first-parent commit (absent on the root commit).

$prev is the reserved thread-link key that lets the web UI step through history with Previous / Next. Commits link along the first-parent chain, stepping through dropped (boilerplate) merges to the nearest imported ancestor. Unlike the session importers, git sets no $thread — a repo's history is a DAG, not one linear thread — so there is no "Entire thread" grouping; $next is derived from $prev.

Temporal is a point-in-time at the commit date.

#Example

Backfill this repo's history, then keep it current with cheap re-runs:

me import git --dry-run -v   # preview
me import git                # full backfill (first run)
me import git                # later: walks only commits since the last import

#me import docs

Import a directory's markdown docs as memories — one memory per file, with the directory layout mirrored as the tree path. By default it imports the files on disk under the directory you point at, including generated or gitignored docs. Pass --git-aware when you want repo-managed behavior: git discovery, gitignore filtering, git last-modified dates, and repo-root safety.

me import docs [dir] [options]
Argument Required Description
dir no Directory to import — the import root; tree paths derive relative to it. With --git-aware, this must be inside a git repo and must be the repo toplevel unless --allow-subdir-root is passed (scope with --include instead). Default: the current directory.
Option Description
--tree <path> Full project tree to place docs under (no slug appended). Default: the repo/project .me tree, else <tree_root>/<slug> (your global tree_root override, default the private ~/projects). An explicit --tree controls only the destination; it does not imply git-aware discovery.
--include <globs...> Glob patterns to import, replacing the default set (**/*.md, **/*.markdown, **/*.mdx).
--exclude <globs...> Glob patterns to drop from the include set.
--temporal-key <key> Frontmatter key parsed as the memory's temporal start (e.g. date for blogs/ADRs). In git-aware mode, falls back to git last-modified.
--no-temporal No temporal and no date-seeded ids (also skips the git-aware git log pass).
--git-aware Use repo-managed discovery: require a git repo, list files through git, respect gitignore by default, apply git last-modified dates, and enable the repo-root guard.
--include-ignored With --git-aware, include ignored/generated files too. Without --git-aware, this is invalid because plain mode already walks files on disk.
--prune Delete previously-imported docs under the docs root that are absent from this walk. Full-corpus runs only — see Pruning.
--allow-subdir-root Allow a git-mode import root below the repo toplevel — see Tree layout. Prefer --include scoping from the toplevel, which narrows without re-rooting.
--dry-run Discover and report what would be imported without writing (with --prune: lists what would be deleted).
-v, --verbose Per-file progress output (prints each tree / name).

#Discovery Modes

Plain mode is the default and is intentionally literal: it walks the filesystem under dir, skips hidden files/dirs and node_modules, and imports matching markdown files even when they are generated or gitignored. It does not detect git, use git dates, or apply the subdir-root guard.

--git-aware is explicit. It requires dir to be inside a git work tree and discovers files with git ls-files: tracked plus untracked-but-not-ignored, so a doc you just wrote imports while ignored build output stays out. It also uses git last-modified dates unless --no-temporal is passed. Add --include-ignored when you want repo context and git dates for tracked files, but also want ignored/generated markdown included; ignored and untracked files simply have no git date.

--include/--exclude are globs applied client-side in both modes, so they mean exactly the same thing with and without git. The output names the mode a run used.

#Tree layout

Each file is a named leaf under the project's docs node, mirroring its directory path relative to dir:

<tree>/docs/<sub>/<dirs>/<file-name>

Directory segments are normalized to ltree labels and the leaf name is a filename-derived slug keeping its (lowercased) extension, unique within its tree — e.g. docs/guides/Getting Started.md in repo acme lands at ~/projects/acme/docs/docs/guides/getting-started.md. Files at the import root sit directly on the docs node.

In plain mode, the fallback project slug comes from the import directory basename. With --git-aware, the fallback project slug comes from the git remote/repo root, so docs, git history, and agent sessions naturally sit beside each other. An explicit --tree or .me tree bypasses slug-derived placement entirely.

Because slots derive from the import root while a git-aware fallback identity or .me tree stays repo-level, runs rooted at different directories of the same repo would mint parallel corpora under one docs root — and a cross-root --prune would delete the other root's slots. So with --git-aware, an import root below the repo toplevel is refused unless --allow-subdir-root is passed; scope with --include from the toplevel instead, and if you do opt into a subfolder root, always use the same one. Plain mode needs no guard: it means exactly "the files under this directory".

#Frontmatter is document data, never engine fields

Unlike me import memories — where frontmatter is the memory record — this importer never reads engine fields (id, name, tree, temporal) from frontmatter: real-world docs own that vocabulary (Docusaurus id:, Hugo slug:), and the engine fields derive from the file path so idempotency keys stay stable. The parsed frontmatter object is preserved verbatim under meta.doc, and it is stripped from the content. Two exceptions use frontmatter as data: a string title: wins over the first-H1 heuristic for meta.title, and --temporal-key designates one key as the temporal source. A file whose frontmatter block is invalid YAML (or a non-object) imports with its content verbatim and no meta.doc — a broken header never fails the file.

#Content shape

The content is the markdown body with frontmatter stripped. .mdx files additionally drop top-level import/export statement lines (module wiring noise) — lines inside fenced code blocks are kept, since usage examples legitimately contain imports, and JSX component tags stay (the same tolerable noise as raw HTML in .md). Content past 64 KiB is truncated with a marker and flagged meta.truncated. Files that are empty after stripping are skipped — there is nothing to embed.

#Temporal and ordering

The temporal is a point-in-time answering "when is this content from": the --temporal-key frontmatter value when given and parseable, else the file's git last-modified date (author date of the newest commit touching the path, computed in one streamed git log pass — never per-file spawns). There is deliberately no filesystem-mtime fallback: mtime churns on clone/copy/checkout, which would make a byte-identical re-import rewrite every row. Per-file degradation, not per-run: an uncommitted file simply gets no git date. In a shallow clone every path's history collapses to the shallow boundary, so git dates are dropped with a warning (--temporal-key still applies).

A dated doc seeds a date-prefixed UUIDv7 id, so docs sort by recency in default (id) order. On re-import the existing row — and its id — is kept, so id order reflects recency at first ingest while temporal tracks live recency.

#Idempotency and re-runs

Idempotency is keyed on (tree, name). Docs are submitted with onConflict: "replace" and deterministic meta, so re-runs reconcile in place: an unchanged file is a no-op, an edited file updates its row, and an importer-version bump re-renders everything. The walk is sorted, so name disambiguation (-2, -3 suffixes on collisions) is stable across runs. A file deleted (or renamed) in the source leaves its old row behind — nothing is pruned unless you ask.

#Pruning (deletions and renames)

--prune reconciles deletions in one atomic server-side pass: every row under the docs root that carries the importer's stamp (meta.source: "docs" — rows this importer didn't write are never pruned, so a manual note under the same tree survives reconciliation; note the import phase itself still replaces any row whose (tree, name) slot collides with a walked file, whatever its meta) and whose (tree, name) slot this walk didn't produce is deleted in a single set-based statement — complete at any corpus size, with no enumerate/delete race. --dry-run --prune runs the same predicate as a read and returns the exact would-delete list. The everyday reason to run it is renames: a renamed file gets a fresh (tree, name) slot, and without pruning the old slot lingers as a stale duplicate.

Prune means "everything under the docs root not in this walk". A narrowed invocation — a subdirectory argument, or --include/--exclude filters — makes the walk a slice, and pruning against a slice deletes the rest of the corpus. Only combine --prune with a full-corpus run (same dir, same mode, same filters as the original import), and preview with --dry-run --prune when unsure. Plain-mode prune is root-sensitive by design; use --git-aware when you want repo-root safety. Prune either reconciles completely or refuses cleanly with nothing deleted (exit 1): on an empty manual walk (a wrong cwd must not read as "delete everything"), and when the walked slot list is too large for a single delete-orphans request (roughly 20k docs — the keep-list cannot be split across requests).

#Metadata

Key Description
title Frontmatter title:, else first # H1, else the filename — human-readable, emoji/punctuation intact.
source Always "docs".
repo_path The file's path relative to the import root (posix separators).
doc The parsed frontmatter object, verbatim (absent when the file has none).
truncated true when the content hit the 64 KiB cap (absent otherwise).
importer_version Version tag of the importer schema.

#Example

me import docs ./markdown --tree /share/pg/18
me import docs . --git-aware --tree /share/acme
me import docs ./build/docs --git-aware --include-ignored --allow-subdir-root --tree /share/acme
me import docs --prune --dry-run        # preview a full-corpus reconcile
me import docs --git-aware --prune      # repo-root-safe re-sync
me memory tree ~/projects/acme/docs --levels 3   # browse the result

#me import ci

Run the project's configured imports — git history, then docs — from the repo toplevel with CI-appropriate defaults. This is the one command the scaffolded GitHub workflow calls (see me project ci); the orchestration lives in the CLI so behavior ships with me releases and the committed workflow file stays frozen.

me import ci [--dry-run] [-v]
Option Description
--dry-run Report what each phase would import/prune without writing. Run it locally to preview exactly what CI will do.
-v, --verbose Per-item progress output.

Must run inside a git repository; everything anchors at the repo toplevel regardless of the invocation directory. Phases:

  1. Git historyme import git on HEAD: incremental via the server-side high-water lookup, so the first CI run is automatically a full-history backfill and every later run imports only new commits.
  2. Docsme import docs --git-aware from the repo toplevel with the default markdown globs and prune on: the CI walk is the authoritative full corpus, so deleted/renamed docs are removed. A repo with no matching docs skips cleanly.

Phases are fail-fast and loud: any failure exits non-zero, so the workflow goes red instead of rotting silently.

The optional import: block in .me/config.yaml shapes the run — phase toggles and docs scoping (project config → the import block):

import:
  git: true                 # default true
  docs: true                # default true
  docs_include: ["docs/**"] # optional; default = all markdown in the repo
  docs_exclude: []          # optional

In CI the identity is the ME_API_KEY bearer — a service account with a write grant at the project tree. Targeting (server/space/tree) resolves from the committed .me/config.yaml exactly as it does for the underlying importers. --json/--yaml are not supported (two phases can't render one structured document) — run the phases separately for structured output.

me import ci --dry-run     # locally: preview what the next CI run does
me import ci               # what the workflow runs on push to the default branch

#me import slab

Import a Slab knowledge-base export — markdown posts laid out in topic folders — as one memory per post. Slab's topic hierarchy becomes the tree path, so the whole wiki is browsable (me memory tree) and searchable (hybrid BM25 + semantic) in one space.

The source is a Slab export: nested folders of .md files (one per post, mirroring Slab's topic hierarchy), with no per-file frontmatter. Unlike me import memories (which needs frontmatter and otherwise flattens everything into share), this command derives the tree, name, title, and temporal from the filesystem layout itself.

The source may be either an already-unzipped directory or the raw .zip export — a zip is detected by its contents (not just the .zip extension), extracted into a temporary directory (only .md entries; auto-removed when the import finishes), and imported exactly like a directory. If the zip wraps everything in a single top-level folder, that wrapper is stripped so topics still map directly under the tree root.

me import slab <source> [options]
Argument Required Description
source yes Path to the Slab export — a directory, or a .zip file.
Option Description
--tree-root <path> Tree root under which the topic hierarchy is placed. Default: /share/slab.
--uncategorized-node <name> Bucket label for topic-less posts at the export root (an ltree label, [a-z0-9_]+). Default: uncategorized.
--no-temporal Do not derive a memory temporal from a leading date in the filename.
--dry-run Parse and report what would be imported without writing.
-v, --verbose Per-file progress output (prints each tree / name).

#Tree layout

Each post is a named leaf under its slugified topic path:

<tree-root>/<topic>/<subtopic>/.../<post-name>

Every directory segment is normalized to an ltree label (lowercase, non-alphanumeric runs collapsed to _, e.g. Customer Success/SOPs & Playbookscustomer_success/sops_playbooks), and the leaf name is a filename-derived slug that keeps its .md extension, unique within its tree. Posts sitting loose at the export root (no topic folder) go under <tree-root>/<uncategorized-node>. A post is then addressable by path, e.g. me memory get "/share/slab/customer_success/sops_playbooks/cloud-faq.md".

#Content shape

Each memory's content is the post's full markdown body, trimmed of leading/trailing whitespace (image links and slab.com URLs preserved as-is). The title is the post's first # H1 heading, or the filename when there is none; it is stored in meta.title (the human-readable form, with punctuation and emoji intact that the ltree slug drops).

Images are not rehosted. Image links in a post are kept exactly as they appear in the export — they still point at Slab-hosted (or otherwise external) URLs. The import copies no image bytes, so an embedded image renders only while its original URL is reachable; if the source becomes unavailable or its links expire, those images will break. The surrounding markdown text is unaffected.

#Idempotency

Idempotency is keyed on (tree, name). Posts are submitted with onConflict: "replace" and a deterministic meta.importer_version, so a re-import is a no-op when nothing changed, and bumping the importer version re-renders every post in place. The walk is sorted, so name disambiguation (-2, -3 suffixes on any slug collision within a folder) is stable across runs. A post whose filename starts with a date seeds a date-prefixed UUIDv7 id so it sorts chronologically by id; undated posts get a server-generated id.

#Metadata

Key Description
title Post title (first H1, else filename) — human-readable, emoji/punctuation intact.
source Always "slab".
slab_topic_path Original (un-slugified) topic folder path, e.g. Customer Success/SOPs & Playbooks.
original_filename The post's source .md filename.
importer_version Version tag of the importer schema.

Temporal is a point-in-time parsed from a leading YYYY-MM-DD, YYYY.MM.DD, or YYYYMMDD date in the filename (e.g. weekly updates), or absent when the filename carries no valid date. --no-temporal disables this.

#Example

me import slab ./export.zip --dry-run -v   # preview straight from the zip
me import slab ./export.zip                 # extract + import under /share/slab
me import slab ./data                       # or from an already-unzipped dir
me memory tree /share/slab --levels 2       # browse the reconstructed topics

Everything lands under one tree root, so the import is reversible — me memory deltree /share/slab removes it cleanly.


#me import git-hook (removed)

me import git-hook has been removed. The local post-commit hook imported whatever HEAD was — feature-branch and rebased commits landed in the shared tree keyed by (tree, sha) forever — it ran per-clone with the committing human's credentials, and it failed silently. Imports now run from CI on push to the default branch instead: run me project ci to set that up.

An already-installed hook keeps firing until its managed block is deleted. me project ci detects the block and removes it once CI credentials are in place; or delete the # >>> memory-engine block from .git/hooks/post-commit by hand.

If you genuinely want local-commit capture (into a private tree), the primitive remains — add this line to your own post-commit hook, knowing it imports unmerged and rebased commits:

me import git >/dev/null 2>&1 &