Metadata-Version: 2.4
Name: opendiff
Version: 0.2.0
Summary: Append-only, multi-tenant raw memory in Postgres, exposed over HTTP and MCP.
Project-URL: Homepage, https://github.com/carwaan/opendiff
Project-URL: Repository, https://github.com/carwaan/opendiff
Project-URL: Issues, https://github.com/carwaan/opendiff/issues
Author-email: akankshadeswal <akanksha.deswal01@gmail.com>
Keywords: agents,mcp,memory,multi-tenant,postgres
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Database
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.12
Requires-Dist: boto3>=1.34
Requires-Dist: fastapi>=0.115
Requires-Dist: httpx>=0.27
Requires-Dist: jinja2>=3.1
Requires-Dist: mcp<2,>=1.2
Requires-Dist: psycopg-pool>=3.2
Requires-Dist: psycopg[binary]>=3.2
Requires-Dist: pydantic>=2.8
Requires-Dist: pyjwt[crypto]>=2.9
Requires-Dist: pyyaml>=6.0
Requires-Dist: typer>=0.12
Requires-Dist: uvicorn>=0.30
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8.3; extra == 'dev'
Requires-Dist: testcontainers[postgres]>=4.8; extra == 'dev'
Description-Content-Type: text/markdown

# opendiff

Append-only, multi-tenant **raw memory** in Postgres — every meeting, message, and
config a team's tools produce, stored byte-verbatim, versioned, and content-addressed —
exposed read-only over HTTP and MCP so skills and agents can reason over it.

The argument for building it: [BRIEFING.md](https://github.com/carwaan/opendiff/blob/main/BRIEFING.md).
The canonical design: [spec.md](https://github.com/carwaan/opendiff/blob/main/spec.md).
Backing research: [research/](https://github.com/carwaan/opendiff/tree/main/research).

## Quickstart

```sh
docker compose up -d                 # postgres:16 on :5433
uv venv && uv pip install -e ".[dev]"

opendiff migrate                     # tables, triggers, RLS, grants
opendiff tenant create acme          # → tenant id
opendiff token create acme --name me # → odf_… bearer token (shown once)

opendiff serve                       # http://127.0.0.1:8000
```

Ingest your Granola workspace:

```sh
export GRANOLA_API_KEY=grn_…
curl -sX POST localhost:8000/v1/sources \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"kind":"granola","name":"prod","credential_ref":"env://GRANOLA_API_KEY"}'
curl -sX POST localhost:8000/v1/sources/<source-id>/sync \
  -H "Authorization: Bearer $TOKEN"
```

Read it in Claude Code:

```sh
claude mcp add opendiff -e OPENDIFF_TOKEN=$TOKEN -- \
  uv run --project /path/to/opendiff opendiff mcp
```

Eight read-only tools: `ls · cat · stat · history · find · read_many · unrouted ·
sources`. Start with `ls('')`, search with `find(query=…)`.

`ls`, `find` and `unrouted` return `{items, next_cursor}`; a non-null `next_cursor`
means the result is incomplete, so pass it back for the next page. `limit` outside
1–500 is an error rather than being clamped — a listing that quietly stopped short
reads exactly like a complete one.

Plus `publish_artifact` / `artifacts`, which write **derived** memory back — a
versioned document rendered on the workspace home page. These two are a temporary
stand-in: the intended producer is a skill running on a schedule over raw memory,
and when that exists it calls the same endpoint.

## Shape of the thing

```
Granola ─┐
Slack ───┤→ INGESTION APIs → store/ → Postgres raw memory ─┬→ HTTP read + sync (§8)
future ──┘   + CustomerResolver   append-only · RLS         └→ MCP, stdio      (§9)
                                  content-addressed             ↓
                                                     CachedHttpReader (§10)
                                                     local cache · pinned reads
                                                                ↓
                                        agent reasons ─→ publish_artifact ─┐
                                                                           ↓
                              Postgres DERIVED memory ── versioned, human-reviewed
                              ACCOUNT.md · breach alerts · product signals
```

- **Append-only, enforced twice** — triggers *and* role grants; the app role cannot
  UPDATE or DELETE content even in a raw psql session.
- **Multi-tenant from migration 0001** — Postgres RLS with `force row level
  security`; an unset tenant returns zero rows, never all rows.
- **One read implementation** — HTTP and MCP are transports over `store/read.py`;
  a parity test asserts byte-identical JSON.
- **Connectors call the public API** — no private ingest path; the watermark
  advances only on a clean, complete drain.

## Tests

```sh
pytest                    # unit + integration + e2e (testcontainers postgres)
pytest -m live            # §14.3 — the real Granola API; needs GRANOLA_API_KEY
```

The e2e suite runs against the real 82-meeting corpus in `granola-transcripts/`
(private, never committed) and asserts exact byte counts, dedup no-ops, and
twin-tenant isolation.
