Session prompt: implement Local API PR 1 (/v1 daemon API)
Paste everything below the line into a new Claude Code session opened in a local
clone of dfrostar/neuralmind. The prompt is self-contained.
The spec documents it refers to were added to main by
https://github.com/dfrostar/neuralmind/pull/552. A follow-up PR from the branch
claude/clever-sagan-m9475i revised them with a mem0 gap analysis.
If docs/specs/LOCAL-API-SPEC.md on your main has no §4.1 (“What else to
borrow from mem0”), that revision hasn’t merged yet. Read the revised docs from
the branch instead:
git fetch origin claude/clever-sagan-m9475i && git show origin/claude/clever-sagan-m9475i:docs/specs/LOCAL-API-SPEC.md
(and the same for LOCAL-API-PR1-SCOPE.md).
You are implementing PR 1 of the NeuralMind Local API: a versioned /v1
HTTP API on the existing local daemon, plus a stdlib Python client. Work in this
repository (dfrostar/neuralmind) and follow its CLAUDE.md.
1. Background (already decided; don’t re-open)
The product
NeuralMind is a local-first code-intelligence layer for AI coding agents. It has four parts:
- a semantic index with progressive context disclosure (L0–L3);
- a Hebbian “synapse” store that learns which files go together;
- a decision memory (
DecisionStore,.neuralmind/memory.db); - MCP tools and Claude Code hooks.
It is MIT core, plus source-available modules under neuralmind/tier2/.
The decision
The question was “should NeuralMind have an API like mem0, that is free?”. The
answer: copy mem0’s developer experience (one documented endpoint,
client.add() / client.search(), a server you run yourself), but don’t host
anything. The reasons:
- Privacy promise. The README promises that NeuralMind “transmits no
repository content off your machine” (
README.md:67). - Roadmap. Hosted SaaS is an explicit non-goal (
ROADMAP.md:212). - Cost. The free tier costs nothing to serve today; a hosted free tier would cost money per repository and mean holding customers’ source code.
- Latency. Hooks run on every tool call, so a network hop doesn’t belong there.
- Pricing. NeuralMind sells seats ($29/user/mo Team, in
commercial-terms.json), not operations.
The API is therefore self-hosted only: loopback by default, with a team “server mode” in a later PR.
What exists today
| Piece | Where | State |
|---|---|---|
| The daemon | neuralmind/daemon.py |
Stdlib ThreadingHTTPServer, default 127.0.0.1:8787. Bearer token in ~/.neuralmind/daemon.json (mode 0600). |
ProjectRegistry |
daemon.py:145-215 |
Warm NeuralMind instance per project, with a per-project RLock |
JobManager |
daemon.py:254-298 |
Background builds |
dispatch() |
daemon.py:338-403 |
Transport-agnostic. Unversioned routes /health /status /jobs /jobs/{id} /stats /savings /query /search /build /validate /shutdown. The project is a raw path in the request body. |
_Handler |
daemon.py:496-546 |
GET/POST only, no body cap, accepts ?token=, and --host accepts any address |
| Daemon client and CLI | neuralmind/daemon_client.py |
The CLI prefers the daemon for query/stats only (cli.py:734-747) |
| Decision store | neuralmind/memory/store.py DecisionStore |
Writes: record() (:338; swallows INSERT errors), update() (:454), update_status() (:435), invalidate() (:487; silent no-op for an unknown id), restore() (:509; raises KeyError), delete() (:540). Reads: get() (:552), query(text, limit, status) (:668; only status=None means all statuses), list_all(status) (:847), find_by_files(files, include_invalidated) (:576; exact string match on project-relative paths). |
| Store concurrency | — | A new SQLite connection per call, WAL, timeout=30, autocommit. Thread- and process-safe without Python locks, so memory routes need no project RLock. Index routes do need it (they touch mind.embedder and turbovec’s persistent connection). |
| MCP memory wrappers | neuralmind/memory/mcp_tools.py |
_compact_row at :176. tool_record_decision defaults commit_sha="", whereas the CLI defaults to HEAD; v1 follows the CLI. |
| MCP search | mcp_server.tool_search (:153-169) |
Normalises hits to {id, label, file_type, source_file, score}, which is the v1 shape |
| MCP mind cache | mcp_server.get_mind |
Has its own _mind_cache. Don’t call tool_* functions from the daemon; call core and store methods on ctx.registry.get(...). |
| Pydantic | pyproject.toml:97 |
pydantic>=2.4 is a base dependency, used for v1 request and response models (server side only) |
| Free-standing JSON | pyproject.toml hatch include |
JSON files need explicit wheel includes, so generate OpenAPI from code at runtime and commit only a docs snapshot |
| Perf plan | docs/specs/PERFORMANCE-FUTURE-PROOFING-PLAN.md Phase 2 (WP 2.2, 2.4) |
Also changes the daemon: auto-spawn, idle TTL, a port move off 8787, a hook route, and load()/maintain(). Those are not yours. Leave the port at 8787 and add no auto-spawn. If any of it has already landed on main, adapt to it; put any hook route under /_internal/ and keep it out of OpenAPI. |
2. Your task
Implement exactly what docs/specs/LOCAL-API-PR1-SCOPE.md describes. The
design reference is docs/specs/LOCAL-API-SPEC.md; read §5 (conventions, routes,
shapes), §5.4 (locking) and §6.1 (loopback security) closely. In summary:
- New package
neuralmind/api_v1/containing:routing.py: a declarativeRoutetable, path templates, and a 405 withAllow;errors.py: the{"error": {code, message, status, request_id, details}}envelope;models.py: pydantic v2 models,extra="ignore";projects.py:ProjectDirectory. Ids arep_plus 12 hex characters ofsha256(realpath). It has a reserved-path denylist re-implemented in core (never importneuralmind.tier2) and persists to<daemon home>/projects.jsonwith mode 0600, written atomically;shapes.py: shared normalisers, moved out ofmcp_server.tool_searchandmemory/mcp_tools._compact_rowwithout changing their behaviour;handlers/{meta,projects,index,memories,jobs}.py;openapi.py: OpenAPI 3.1 built from the route table plusmodel_json_schema().python -m neuralmind.api_v1.openapi --write docs/api/openapi-v1.jsonwrites the committed snapshot.
- Routes:
- Meta:
GET /v1,GET /v1/health(no auth; returns no paths or pid),GET /v1/status,GET /v1/openapi.json(no auth),POST /v1/admin/shutdown. - Projects:
GET|POST /v1/projects,GET /v1/projects/{id}. - Index, under
/v1/projects/{id}/:build(202 +Location: /v1/jobs/{jid}, orwait=truefor 200),query(passesquery_typeandcontext_budgetthrough; mapsTokenBudgettotokens.{total,l0,l1,l2,l3}),search,wakeup,stats,savings,validate. - Memories, under
/v1/projects/{id}/memories: create (201 +Location), list (status=ALLmaps toNone,file=repeatable, cursor paging),POST …/search(limit at most 25,view=full|compact), get,PATCH,POST …/{mid}/invalidate,POST …/{mid}/restore,DELETE(204). - Three mem0-derived additions (spec §4.1 G3–G5; details in spec §5.3):
possible_duplicateson the create response: advisory, up to 3 ACTIVE matches by title that share a file.scoreon each search result.- The
tag,type,author,created_after,created_beforeandmin_confidencefilters on list and search, combined with AND.
- Jobs:
GET /v1/jobs,GET /v1/jobs/{jid}, with ISO-8601 UTC times.
- Meta:
- Memory rules:
commit_shadefaults togit rev-parse HEADin a git repository, otherwise"".files_affectedis normalised to project-relative POSIX paths. A path outside the project returns 422.decision_typeandstatusare validated strictly (422).- A duplicate client
idreturns 409. - Read back after create; if the record is missing, return 500.
- Check existence before invalidate and restore (404).
- Duplicate hints. After the write, call
query(title, limit=5, status="ACTIVE", with_scores=True), drop the new record, keep only matches that share a file (when the new record has files), and return at most 3. Never block or merge. - Search filters. Ask the store for
min(limit × 4, 100)results, filter, then trim tolimit.min_confidencemaps to the store’s existingmin_scoreparameter, which filters on confidence.
daemon.py:- Add
do_PATCHanddo_DELETE. - Cap the body at
NEURALMIND_API_MAX_BODY_BYTES(default 1 MiB; 413), for all routes. - Check the Host header (
127.0.0.1,localhostor[::1], else 403host_not_allowed), for all routes. /v1/*goes todispatch_v1with header-only bearer auth. Every other path goes to the untouched legacydispatch().- Send the response headers
NeuralMind-API-Version: 1andX-Request-Id(echo or generate). create_server()andmain()refuse non-loopback hosts (exit code 2 with a clear message).DaemonContextgainsprojectsandmode="loopback".- Leave
ProjectRegistry._keyasabspath; read its comment.
- Add
cli.py:daemon startvalidates--hostup front. No new subcommands.neuralmind/client.py: a stdliburllibclient. It provides:NeuralMindClient(base_url=None, token=None), falling back toNEURALMIND_API_URL/NEURALMIND_API_TOKENand then the discovery file;.project(path_or_id), returning aProjectClientwith.build/.query/.search/.wakeup/.stats/.savingsand.memories.add/list/search/get/update/invalidate/restore/delete;.jobs.get/wait;- frozen dataclasses with
.raw; NeuralMindAPIErrorand its subclasses.
Importing it must not pull in
neuralmind.core, onnxruntime, chromadb, turbovec or pydantic.- Tests: these new files, following the
FakeMindpattern intests/test_daemon.py, with a realDecisionStoreintmp_pathfor memories:test_api_v1_routing.pytest_api_v1_projects.pytest_api_v1_index.pytest_api_v1_memories.py: includes duplicate hints, each filter, and score orderingtest_memory_store_scores.py:with_scores=Truekeeps the default order, and the default return type is unchangedtest_api_v1_openapi.py: snapshot equality and route ↔ path paritytest_client.py: includes a subprocess check that the import is lightweighttest_daemon_bind_guard.pytest_api_v1_license_boundary.py: an AST check for notier2imports
Do not modify existing tests.
- Docs, in the same PR. This follows
CLAUDE.md’s “Shipping a feature” checklist; the list is in scope doc §4:- Release notes:
docs/releases/RELEASE_NOTES_v<next minor>.md, with the version taken from.release-please-manifest.jsonor an open release PR. Include a per-agent table: MCP agents see no change; scripts, CI and non-MCP agents gain/v1. Call out the loopback-only bind. - README: banner, history trail, an “NeuralMind API (self-hosted)” section, and the release-notes row.
docs/index.html(banner and meta) anddocs/about.html(a “What’s New” section).docs/wiki/CLI-Reference.md: the daemon section, plus env varsNEURALMIND_API_URL,NEURALMIND_API_TOKENandNEURALMIND_API_MAX_BODY_BYTES.- A new
docs/wiki/HTTP-API.mdwith the mem0 → NeuralMind cheat sheet, linked fromHome.mdandAPI-Reference.md. - A new
docs/use-cases/local-http-api.md. docs/comparisons/vs-mem0-zep.md: the Distribution row and the “no server” bullet.pyproject.tomlkeywords:local-rest-api,openapi,agent-memory-api.docs/sitemap.xmlanddocs/llms.txt.
- Release notes:
3. Rules
Never:
- edit
CHANGELOG.md, or bump versions inpyproject.toml/.release-please-manifest.json(release-please owns them); - import
neuralmind.tier2from core code (license boundary); - add runtime dependencies;
- touch hooks,
core.pyor the stores, beyond the two normaliser moves and one backward-compatible store change:DecisionStore.query(…, with_scores=False), which returns(record, -bm25)pairs whenTrue; - start PR M (spec §4.2): no schema changes, history table, embeddings,
review_by, oragent/session_idfields. That is a separate PR.
Don’t fix the unrelated bugs still open in spec §12 (compose restart loop, the always-on.md port). Mention them in the PR body as follow-ups. Items 1, 2, 3, 5 and 7 are already fixed. Build on them: the store normalises status filters, so the handler maps its ValueError to 422.
Claims discipline (CI-enforced by tests/test_docs_claims.py and tests/test_site_claims.py):
- No latency or throughput numbers; describe the mechanism instead (“keeps the index warm between calls”).
- No absolute privacy phrases (“never leaves”, “no data leaves”, “fully air-gapped”). The safe phrasing is “sends no telemetry and transmits no repository content off your machine.”
- Document only shipped behaviour. Server mode, Docker and
/activityare later PRs.
Docs CLI guard. tests/test_docs_cli_paths.py checks that every neuralmind <cmd> <sub> written in the docs exists in the parser. Don’t document commands that don’t exist.
Scope. If the change grows well past ~1,800 lines (excluding docs and the snapshot), stop and cut PATCH/restore/DELETE into a follow-up PR 1b rather than widening.
4. Suggested order
- Recon. Read
CLAUDE.md, both spec docs,daemon.py,daemon_client.py,tests/test_daemon.py,memory/store.pyandmemory/mcp_tools.py. Rungit log --oneline origin/main -- neuralmind/daemon.pyto see whether the perf Phase 2 work has landed. Then run the baseline:pip install -e ".[dev]"andpytest tests/test_daemon.py tests/test_memory.py tests/test_mcp_server.py -q. - Write
api_v1/errors.py,routing.pyandmodels.py, then the routing tests. - Integrate with the daemon (handler, body cap, Host check, bind guard), then the bind-guard tests.
- Write
projects.pyand its handlers, then the tests. - Write the index handlers, then the tests.
- Write the memory handlers, then the tests.
- Write the jobs and meta handlers and
openapi.py, generate the snapshot, then the tests. - Do the
shapes.pyrefactor and re-run the MCP tests. - Write
client.py, then its tests, including the import-weight test. - Write the docs.
- Validate:
pytest tests/ -qruff check .black --check .python scripts/check_commercial_terms.py- Manual end-to-end against this repository, with both the Python client and
curl: start the daemon; register.; build withwait; query and search; add a memory, search for it, invalidate it, and list withstatus=ALL; shut down. Keep a trimmed transcript.
- Open a draft PR from a new branch, e.g.
feat/local-api-v1off an up-to-datemain.- Title:
feat(daemon): versioned /v1 local API with project registry, decision-memory endpoints and Python client.mainis squash-merged, so the title becomes the release-please commit. - Fill in
.github/PULL_REQUEST_TEMPLATE.md, paste the end-to-end transcript, and list the follow-ups from spec §12.
- Title:
5. When you finish, report
- what shipped, route by route;
- any deviation from the scope document, and why;
- test and lint results;
- the PR link;
- anything in the spec that turned out to be wrong when it met the code. If the spec revision has merged (see the note above), fix the spec in your PR; otherwise list the corrections in your report.