Skip to content

Install memhtml and initialize a store

One package carries the whole system, and installing it puts two binaries on your PATH: memhtml and memhtml-mcp. Node 24 or newer.

At the end of this page you will have both binaries and a scaffolded, git-initialized store.

Terminal window
npm i -g memhtml # or: pnpm add -g memhtml, bun add -g memhtml
memhtml manifest | head -5
{
"apiVersion": "1",
"type": "cli.manifest",
"data": {

memhtml manifest is the liveness check, and it answers on a machine with no repository, no database, and no credentials. An envelope back means the install is good. npx memhtml manifest answers the same way without installing anything, which is the cheapest way to read the whole command surface before deciding.

There is no @memhtml/* package to install. Every workspace package is private, and the one assembled memhtml — bundling the libraries and the binary-bearing apps, with the docs site left out — is the only thing published. See Packages and dependency direction for why the layering stays internal.

Contributors, and anyone who wants the binary from a specific commit, build it instead. mise is the command surface for the repository: it installs the toolchain itself, meaning node 24, pnpm, lefthook, and the scanners, from mise.toml. The committed mise.lock pins each one by checksum and provenance, so your clone resolves the same binaries CI does.

Terminal window
git clone https://github.com/memhtml/memhtml.git
cd memhtml
mise install # node, pnpm, lefthook, scanners, from mise.lock
mise run install # dependencies from the lockfile, plus the git hooks
mise run build # tsc -b across the project-reference graph

mise run install depends on mise run tools:verify, which fails when mise.toml’s [tools] pnpm pin and package.json’s packageManager disagree. Both declare the same pnpm and neither can be derived from the other, which is why the check runs on every install.

When mise run install refuses because a dependency is too young to install, that is minimumReleaseAge: 4320 working: the newest release of a package stays uninstallable for its first 72 hours. Wait, or take the previous release.

mise run build emits an executable with a node shebang at apps/cli/dist/bin.js. Reach it three equivalent ways:

Terminal window
mkdir -p ~/.local/bin
ln -sf "$PWD/apps/cli/dist/bin.js" ~/.local/bin/memhtml
  • mise run cli <args> runs the same file from inside the repository. The task is declared raw, so mise prefixes nothing onto its output lines and the one-envelope-per-command contract survives.
  • node /path/to/memhtml/apps/cli/dist/bin.js <args> from anywhere.

Every @memhtml/* package’s exports resolve only to ./dist, so after editing any package’s src/ run mise run build again before the binary reflects the edit.

To build the published artifact rather than the workspace binary, mise run package:assemble writes it to dist-package/, and mise run package:smoke installs that tarball into a temporary directory and drives every command and MCP tool through it.

memhtml init commits the scaffold it writes, and git commit refuses to run without an author. On a machine with a ~/.gitconfig this is already true and you can skip ahead. On a fresh container or a CI runner it is not, and the first command fails with git commit failed (exit 128) — git’s own explanation goes to stderr, so check there when an ERR_GIT envelope surprises you.

Terminal window
git config --global user.name "You"
git config --global user.email "you@example.com"

An unattended agent should prefer the environment, which needs no repository and writes no config:

Terminal window
export GIT_AUTHOR_NAME="memhtml" GIT_AUTHOR_EMAIL="memhtml@localhost"
export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME" GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"

Nothing is lost if you find out the hard way. memhtml init is convergent: it asks the repository what is already true and supplies only what is missing, so setting an identity and running it again carries the staged scaffold to a commit.

MEMHTML_ROOT is where the memory repository lives. It defaults to ~/memhtml, and the binary expands a leading ~ itself, because the value arrives from a shell profile, an MCP client config, and a cron line, and only the shell expands tildes.

Terminal window
export MEMHTML_ROOT=~/memhtml
memhtml init
{
"apiVersion": "1",
"type": "repo.init",
"data": {
"root": "/home/you/memhtml",
"created": true,
"headSha": "1d12ed327db623a2fdcfbec91f78e41fd8d9c6c4",
"wrote": [
"projects/.gitkeep",
"areas/.gitkeep",
"resources/.gitkeep",
"archive/.gitkeep",
"areas/arcs/.gitkeep",
"resources/people/.gitkeep",
"areas/inbox/.gitkeep",
".memhtml/state/.gitkeep",
".memhtml/sleep/.gitkeep",
".gitignore",
".gitattributes",
"README.html"
]
}
}

created: true means this call created the repository. On a re-run it is false, wrote is empty, and headSha is the existing HEAD: memhtml init is convergent, asking the repository what is already true and supplying only what is missing (packages/store/src/layout.ts:183). It reaches the same end state from an empty directory, from a fully scaffolded repository, and from one a killed run left half finished, so re-running it is always safe.

The top level is fixed at four PARA buckets: projects/, areas/, resources/, and archive/. A workspace is a directory under projects/, and there is no workspaces table. Store layout and path algebra explains the rest of the tree.

memhtml init applies the migrations, so .memhtml/index.db exists and holds nothing. Project the tree into it:

Terminal window
memhtml index rebuild --embed

--embed fills vectors from Bedrock. With no AWS credentials yet, pass --no-embed: the rebuild becomes instant, retrieval runs on its lexical floor, and you fill the vectors later by running memhtml index embed, which embeds every chunk that has no vector and rebuilds nothing. Both states are honest ones to be in:

Terminal window
export MEMHTML_EMBED=off # an explicit opt-out, distinct from a missing credential
memhtml index rebuild --no-embed

A missing credential degrades one search at call time, while MEMHTML_EMBED=off degrades every search. Only the second is a decision you made, and the manifest reports the two as different states.

Terminal window
memhtml status
{
"apiVersion": "1",
"type": "status.health",
"data": {
"root": "/home/you/memhtml",
"headSha": "4e232759bfad745b0445ecd83cc9883c30a0c426",
"dirty": false,
"dirtyPaths": [],
"countsByType": {},
"archivedCount": 0,
"edges": 0,
"derivedEdges": 0,
"chunks": 0,
"embeddings": 0,
"traces": 0,
"indexFresh": true,
"indexHeadSha": "4e232759bfad745b0445ecd83cc9883c30a0c426",
"embedModel": "cohere.embed-v4:0@1024",
"embedderUp": false,
"hasState": true,
"lastSleep": null
}
}

indexFresh: true means the index describes the current HEAD. embedderUp is read off the stored watermark rather than by probing Bedrock, so it reads false on a store with zero vectors and stays false until an embedding pass writes some.

memhtml doctor is the other confirmation. On a fresh store it answers healthy: true with every finding list empty. Audit and publish the corpus covers what each finding means when the lists are not empty.

Two databases sit under .memhtml/, and they differ in what you can recover:

  • index.db is gitignored and rebuildable from the tree. Losing it costs a rebuild.
  • state.db is gitignored and holds the one set of facts the tree cannot reproduce: access counts, reinforcement counts, and the outcome EWMA. Its durability comes from the committed sidecar .memhtml/state/access.jsonl.

Both are plain SQLite, opened through node’s built-in node:sqlite. The only database dependency is node itself, with no driver flags to keep in step, so sqlite3 or any GUI browser opens both files directly and a stuck index stays inspectable without this binary.

Next: write your first memory.