Menu Memory

indra CLI

Memory

indra mem: Markdown notes with typed links, graph-aware recall, and graph views.

Notes are plain Markdown files with [[wikilinks]] and typed relations. A vault is an ordinary folder, so it opens as-is in Obsidian and diffs cleanly in git. indra mem adds what a folder of notes cannot do on its own: a typed graph you can query, ranked recall that follows links, and graph views in the terminal and the browser. (indra m is a short alias.)

$ indra mem recall "django orm raw sql" -n 3
# Memory recall: django orm raw sql

## Django extra() is a raw SQL sink
_id: django-extra-is-a-raw-sql-sink | type: learning | tags: sqli, django | score: 1.00 | via: search_

QuerySet.extra(where=[...]) takes raw SQL. Any f-string into it is a SQLi sink.

links: -> instance-of SQL injection; -> mitigated-by Parameterized queries

## Parameterized queries
_id: parameterized-queries | type: pattern | score: 0.39 | via: Django extra() is a raw SQL sink -[mitigated-by]->_

Bind input as parameters. Identifiers cannot be bound; allowlist them.

The second result never mentions Django. It was recalled because the first one links to it, and the via: line says so.

Why not just Obsidian?

Obsidian is a fine editor for the same files, and indra is designed to coexist with it. What indra mem adds:

Obsidianindra mem
StorageMarkdown folderthe same Markdown folder
Linksuntyped [[links]]untyped and typed: mitigated-by:: [[x]] (Dataview syntax)
Graph queriesvisual onlytree, neighbors, path, --rel filters, JSON output
Retrievalkeyword searchBM25 search + spreading activation over the graph, with explanations
Use from scripts and AI agentsplugins, GUIa CLI with --json on every command
Safe for parallel writersnocross-process lock, atomic writes
FootprintElectron appone binary (memory and console), Linux / macOS / Windows
LicenceproprietaryMIT

Quick start

indra mem init ~/notes                    # or let the first `add` create the default vault
export INDRA_VAULT=~/notes

indra mem add "SQL injection" -t vuln-class --alias SQLi -b "Untrusted input reaches a SQL interpreter."
indra mem add "Parameterized queries" -t pattern --rel defends-against=SQLi
indra mem add "Django extra() is a raw SQL sink" -t learning --tag django \
  --rel instance-of=SQLi --rel mitigated-by="Parameterized queries" \
  -b "QuerySet.extra(where=[...]) takes raw SQL. Any f-string into it is a SQLi sink."

indra mem recall "django raw sql"         # what do I know about this?
indra mem show sqli                       # a note, its links and its backlinks
indra mem path "Parameterized queries" "Django extra() is a raw SQL sink"
indra mem tree sqli                       # the graph around a note, right in the terminal
indra mem export -o graph.html            # optional: an interactive view in a browser

Note format

---
title: Django extra() is a raw SQL sink
type: learning
tags: [sqli, django]
aliases: [extra() sink]
source: run-20261113-101500
created: 2026-11-13T10:15:00Z
updated: 2026-11-13T10:15:00Z
---

# Django extra() is a raw SQL sink

QuerySet.extra(where=[...]) takes raw SQL. See also [[orm-escape-hatches]].

## Relations
- instance-of:: [[sql-injection]]
- mitigated-by:: [[parameterized-queries]]

Every field is optional; a bare Markdown file is a valid note. What indra reads:

SyntaxMeaning
[[target]], [[target|label]], [[target#heading]]edge of type related
rel:: [[target]], - rel:: [[a]], [[b]]typed edges (Mitigated By normalises to mitigated-by)
[label](other-note.md)edge of type related
key: "[[target]]" in frontmatteredge of type key (e.g. parent: "[[x]]")
tags: in frontmatter, inline #tagstags (nested #a/b works with --tag a)
title:, else the first # heading, else the file nametitle
type: (or kind:)note type, used for filters and colours

Links inside code blocks and inline code are ignored. Hidden folders (.obsidian, .git, .trash, .indra) and symlinks are skipped.

Name resolution. [[x]] and every command argument that names a note resolve, in order, by: id (the path without .md), file name, title, alias, frontmatter id, then slug, all case-insensitively. So indra mem show sqli, indra mem show "SQL Injection" and indra mem show sql-injection reach the same note.

Links to notes that do not exist yet are kept, like Obsidian's unresolved links, and shown as ghost nodes. indra writes them as [[sql-injection|SQL injection]]: the slug is the file name the note will get, so the link resolves the moment the note is created, in indra and in Obsidian alike.

Commands

Every command accepts --vault DIR and --json.

CommandWhat it does
init [DIR]create a vault (a folder with a .indra/ marker)
whereprint the resolved vault path
add TITLE [-b TEXT|-] [-t TYPE] [--tag T] [--alias A] [-l NOTE] [-r REL=NOTE] [--source S] [--folder F] [-u]create a note; -u/--upsert merges into an existing note with that name (idempotently)
append NOTE TEXT|-add a paragraph above the Relations section
link FROM TO [-r REL]add a typed relation; a no-op if it already exists
unlink FROM TO [-r REL]remove relation lines
tag NOTE TAG... [--remove]edit frontmatter tags
show NOTEthe note, its links, backlinks and unresolved links
list [-t TYPE] [--tag T]list notes
search QUERY [-n N] [-t TYPE] [--tag T]ranked full-text search with snippets
recall QUERY [-n N] [-d DEPTH] [--max-chars N]graph-aware recall, formatted as context for an LLM
neighbors NOTE [-d DEPTH] [-r REL]the neighbourhood, either direction
path FROM TOthe shortest chain of relations between two notes
tree [NOTE] [-d DEPTH] [-r REL]draw the graph as a text tree in the terminal: around one note, or every connected group in the vault
tags, statstag counts; note, link, type, relation and hub counts
check [--strict]lint: unresolved links, ambiguous names (exit 1), orphans
export [-f html|json|dot|mermaid] [-o FILE] [--no-body]export the graph
rm NOTEdelete a note and report the links it leaves unresolved
import DIR [--from obsidian|notion|github-wiki|markdown] [--folder F] [--tag T] [--overwrite] [--dry-run]copy notes from an Obsidian vault, a Notion export, a GitHub wiki or any Markdown folder, converting links to wikilinks (see the guide)

Text can be read from stdin with -: git log -1 --format=%B | indra mem add "Release notes" -b -.

How recall works

  1. Search. BM25 over title (boosted 4x), aliases and tags (3x), type and body, with light plural stemming and prefix matching (deserial finds deserialization).
  2. Spread. Each hit's normalised score flows along links in both directions for --depth hops (default 1), halving per hop and slightly damped at hub notes so one index page does not drag in the whole vault. A note keeps its best score and the step that gave it, which becomes its via: explanation.
  3. Budget. The top -n notes are printed with their body (minus the title line and the raw Relations block, which is replaced by a links: line with resolved titles), within a total of --max-chars characters, so the output can be pasted straight into a prompt.

Vault location

--vault DIR, else $INDRA_VAULT, else the nearest parent of the working directory that contains a .indra/ folder (so a project can carry its own vault), else a per-user default:

OSDefault vault
Linux, BSD$XDG_DATA_HOME/indra/memory, or ~/.local/share/indra/memory
macOS~/Library/Application Support/indra/memory
Windows%LOCALAPPDATA%\indra\memory

Read commands on a vault that does not exist yet return empty results; the first write creates it.

Viewing the graph

Everything works from the terminal. indra mem tree draws the graph as text:

$ indra mem tree sqli
SQL injection  (sql-injection)  [vuln-class]
|-- <- defends-against  Parameterized queries
|   `-- <- mitigated-by  Django extra() is a raw SQL sink  (*)
|-- <- instance-of  Django extra() is a raw SQL sink
|   |-- -> mitigated-by  Parameterized queries  (*)
|   `-- <- instance-of  SH-SQLI-003 report export
`-- <- refines  False positive: sanitize_sql helper

-> is a link out, <- a backlink. Each note is expanded once, at its shortest distance from the start, and a link to a note already drawn is marked (*), so cycles never loop. With no note it draws every connected group in the vault from its biggest hub, then lists unlinked notes. Connectors are plain ASCII, so it renders the same in any console.

For a visual view, indra mem export -o graph.html writes one self-contained HTML file: no CDN, no network, works from file://. Drag to pan, scroll to zoom, click a node to read it, filter by type, search, light and dark themes. Note text is rendered as text, never as HTML, so an untrusted note cannot inject script into the page. -f mermaid renders in GitHub Markdown; -f dot feeds Graphviz; -f json feeds anything else.

Or open the vault folder in Obsidian. Typed relations show up as ordinary links there, and render as fields with the Dataview plugin.

Using it from AI agents

indra mem was built for agents that need memory across sessions:

The indra plugin's sh-memory skill is a complete example: it recalls past findings and known false positives before hunting, and records each run's results afterwards.

Design notes and limits