Skip to content

Commands

The CLI and the HTTP API are thin adapters over one nodum.service layer, sharing a single pydantic I/O schema. For identical data they emit byte-identical JSON — parity tests assert it — so you can prototype with the CLI and ship against the API without surprises.

CLI

Run a command with nodum <cmd> (installed) or uv run nodum <cmd> / make run -- <cmd> (from a checkout). Every command prints a single JSON object to stdout; human messages and errors go to stderr, so nodum … > out.json captures clean JSON.

Command Does
add KIND TEXT [--set k=v …] create a typed node
link FROM TO EDGE_KIND [--set k=v …] create a typed directed edge
get UUID a node plus its incident edges
search QUERY [--kind K] [--limit N] ranked full-text search
expand UUID [--depth N] [--edge-kind K …] seed → connected subgraph
edit-node UUID [--text …] [--set k=v …] merge + re-validate a node
edit-edge UUID [--set k=v …] merge an edge's payload
rm-node UUID delete a node (edges cascade)
rm-edge UUID delete one edge
schema print the metamodel contract
auth set-password set/replace the main password (prompt or piped stdin)
auth status report whether a password is configured (+ timestamp)
auth ensure-password set the password from NODUM_ADMIN_PASSWORD[_FILE] if unconfigured (entrypoint bootstrap)
init-db create the schema + seed the kind lookup tables
migrate upgrade a pre-typed (MVP) database
serve run the HTTP API (serves the SPA when NODUM_WEB_DIST is set)

The --set field syntax

--set key=value is repeatable, and each value is parsed as JSON, falling back to the raw string if that fails. So scalars and structured values both work:

nodum add Person "Ada Lovelace" --set born=1815                 # 1815 is an int
nodum add Reference "Menabrea/Lovelace, Sketch of the Analytical Engine (1843)" \
  --set year=1843 \
  --set 'authors=["Ada Lovelace","L. F. Menabrea"]' \           # a JSON list
  --set venue=Nature                                            # a bare string

TEXT (the first positional) sets the node's universal text; --set carries the kind's typed fields into data. edit-node merges: --text replaces the text, --set merges into data, and the result is re-validated against the kind.

Examples

nodum link <person-uuid> <reference-uuid> AuthorOf      # Person → Reference (signature-checked)
nodum get <uuid>                                        # node + every incident edge
nodum search "analytical engine" --kind Reference --limit 10
nodum expand <uuid> --depth 2 --edge-kind AuthorOf --edge-kind cites
nodum schema                                            # the whole metamodel contract

HTTP API

FastAPI. Start it with nodum serve (or the Docker image). Every data route is gated by authentication; each returns the same JSON envelope the CLI prints, via model_dump(mode="json") with no response_model, so keys are neither added, dropped, nor reordered.

Method & path Does
POST /nodes create a typed node ({"kind","text","data"})
GET /nodes/{uuid} a node plus its incident edges
PATCH /nodes/{uuid} merge {"text","data"} into a node, re-validate
DELETE /nodes/{uuid} delete a node; edges cascade (returns the count)
POST /edges create an edge ({"kind","from_uuid","to_uuid","data"})
PATCH /edges/{uuid} merge {"data"} into an edge (kind + endpoints fixed)
DELETE /edges/{uuid} delete one edge (returns the count)
GET /search?q=&kind=&limit= ranked full-text search (limit default 20)
GET /expand?seed=&depth=&edge_kind= seed → subgraph (depth default 1; edge_kind repeatable)
GET /schema the metamodel contract

Open (unauthenticated) routes: POST /auth/login, POST /auth/logout, GET /auth/session, GET /healthz, and — only when the SPA is mounted (NODUM_WEB_DIST set) — GET / and /assets.

Examples

# create a node
curl -s -X POST http://127.0.0.1:8600/nodes \
  -H 'content-type: application/json' \
  -H 'authorization: Bearer <token>' \
  -d '{"kind":"Note","text":"Spaced repetition improves long-term retention","data":{"role":"claim"}}'

# search, and expand a subgraph two hops out along one edge kind
curl -s 'http://127.0.0.1:8600/search?q=retention&kind=Note&limit=10' -H 'authorization: Bearer <token>'
curl -s 'http://127.0.0.1:8600/expand?seed=<uuid>&depth=2&edge_kind=cites' -H 'authorization: Bearer <token>'

# the contract — one call self-orients a client
curl -s http://127.0.0.1:8600/schema -H 'authorization: Bearer <token>'

Authentication

The data routes require a valid session (see Install & run for the model). Obtain a token by logging in with the main password, then send it as a Bearer header:

TOKEN=$(curl -s -X POST http://127.0.0.1:8600/auth/login \
  -H 'content-type: application/json' \
  -d '{"password":"change-me"}' | python -c 'import sys,json;print(json.load(sys.stdin)["token"])')

curl -s http://127.0.0.1:8600/schema -H "authorization: Bearer $TOKEN"

Browsers get the token as an HttpOnly cookie set by POST /auth/login and never handle it directly; the SPA calls GET /auth/session{configured, authenticated} to choose between the setup hint, the sign-in view, and the app.

Error contract

The service raises NodeNotFound / EdgeNotFound for missing rows and ValueError (including the metamodel ValidationError) for bad input. The surfaces map them consistently:

Condition CLI API
Missing node/edge stderr message, exit code 1 404 {"detail": …}
Invalid input (bad kind, missing field, bad endpoint) stderr message, exit code 1 422 {"detail": …}
Unauthenticated — (the CLI is trusted, talks to the DB directly) 401
No main password set yet 503 (locked; set one with nodum auth set-password)

The schema contract

nodum schema (CLI) and GET /schema (API) return the live metamodel — every node kind with its field schema, every edge kind with its from → to signature. It is the one call a client or an agent makes first to self-orient before any write, which is why every surface ships it. See Concepts for what the kinds and signatures mean.