Skip to content

http api

bower serve --port 8787

bowerbird ships a thin HTTP wrapper around the same primitives the CLI uses. it's intentionally narrow.

endpoints

GET /health

liveness probe. ~400 µs response.

curl http://localhost:8787/health
# { "status": "ok", "memories": 1247, "uptime_seconds": 8214 }

POST /query

hybrid search.

{
  "query": "how do we handle authentication",
  "limit": 5,
  "deep": false,
  "fast": false,
  "no_cache": false
}

response:

{
  "results": [
    {
      "id": "jwt-token-expiry-a3f1",
      "score": 0.87,
      "content": "We use JWT tokens with 24-hour expiry...",
      "path": "architecture/auth/jwt-token-expiry-a3f1.md",
      "maturity": "validated",
      "relations": ["oauth-callback-flow-7c2e"]
    }
  ],
  "tier_used": 3,
  "latency_ms": 412,
  "cache_hit": false
}

POST /curate

store a memory.

{ "content": "we moved off lambda to fargate because cold-start was blown" }

response includes the IDs of the structured pieces extracted by the LLM (if a key is configured) or the single note ID otherwise.

GET /status

system stats: memory count, db size, cache hit rates, last dream run, index health.

POST /dream

run a dream pass synchronously. returns the candidate list and what was applied.

{
  "mode": "apply",
  "confidence_threshold": 0.85
}

POST /consolidate

cosine-similarity dedup pass without the full dream pipeline. useful for catching obvious duplicates without paying for LLM analysis.

POST /prune

remove memories that have decayed below a threshold importance score. respects pinned core tier — those never prune.

{ "max_age_days": 365, "min_importance": 0.05 }

auth

the daemon binds to 127.0.0.1 by default and has no auth. if you bind to 0.0.0.0, put it behind a reverse proxy that handles auth. there is no plan to bake auth into the daemon itself.

as a python client

import httpx

c = httpx.Client(base_url="http://localhost:8787")

c.post("/curate", json={"content": "the team prefers ruff over flake8"})

r = c.post("/query", json={"query": "what linter do we use", "limit": 3})
for hit in r.json()["results"]:
    print(hit["score"], hit["content"][:80])