{
  "markdown": "<p align=\"center\">\n  <img src=\"assets/logo.png\" alt=\"Postgram\" width=\"260\" />\n</p>\n\n<h1 align=\"center\">Postgram</h1>\n\n<p align=\"center\">\n  <strong>A self-hosted productivity and knowledge backend for humans and AI agents.</strong>\n</p>\n\n<p align=\"center\">\n  <a href=\"https://postgram.dev\">Website</a> ·\n  <a href=\"https://postgram.dev/getting-started/quick-start/\">Quick start</a> ·\n  <a href=\"https://postgram.dev/guides/mcp-integration/\">MCP guide</a> ·\n  <a href=\"https://postgram.dev/reference/rest-api/\">REST API</a> ·\n  <a href=\"https://www.youtube.com/watch?v=xr7u11gtYgM\">Demo</a>\n</p>\n\nPostgram keeps the data you and your agents work from in one inspectable place:\nnotes, documents, tasks, people, projects, interactions, decisions, and agent\nmemory. Humans use the browser UI and CLI; agents use the same corpus over MCP,\nREST, or the CLI.\n\nIt is more than an agent-memory layer. Postgram preserves typed source objects,\nsupports GTD-style task management and Markdown folder sync, combines full-text\nand vector retrieval with a knowledge graph, and separates short-lived agent\nworking context from durable memory.\n\n<table>\n  <tr>\n    <td width=\"50%\">\n      <a href=\"https://www.youtube.com/watch?v=xr7u11gtYgM\">\n        <img src=\"https://img.youtube.com/vi/xr7u11gtYgM/maxresdefault.jpg\" alt=\"Watch the Postgram demo video\" />\n      </a>\n      <br />\n      <sub>Watch the demo</sub>\n    </td>\n    <td width=\"50%\">\n      <img src=\"assets/search.png\" alt=\"Postgram search interface showing ranked knowledge results\" />\n      <br />\n      <sub>Search across memories, documents, people, projects, and tasks</sub>\n    </td>\n  </tr>\n</table>\n\n## Why Postgram\n\n- **One private corpus across tools.** Give different agents and devices access\n  to the same data without tying it to one editor or hosted memory provider.\n- **Inspectable source data.** Store typed entities instead of opaque chat\n  summaries, then search, edit, link, archive, or delete them yourself.\n- **Search before graph expansion.** Hybrid retrieval finds relevant entities;\n  edge summaries tell an agent when related graph context is worth following.\n- **Working context is not durable memory.** Session context has its own scope\n  and lifecycle; grooming can archive it or distill selected context into\n  durable memory.\n- **Operator control.** Choose where PostgreSQL runs, which embedding and\n  extraction providers are allowed, who receives API keys, and what is kept.\n\nPostgram is built for one person or a small trusted team running a local or\nsingle-VM deployment. It is not a hosted service or a multi-tenant SaaS\nplatform. Knowledge extraction is optional, and the provided Docker Compose\nsetup binds the raw API and UI ports to loopback by default.\n\n## Quick Start (Docker Compose)\n\nYou need Git, Docker, and Docker Compose. Node.js 22+ is needed only for local\ndevelopment or for installing the `pgm` CLI; `gpg` is needed only for encrypted\nCLI backups.\n\n1. Clone Postgram:\n\n   ```bash\n   git clone https://github.com/ivo-toby/postgram.git\n   cd postgram\n   ```\n\n2. Choose an embedding path before the first start. For the local default,\n   install and start Ollama on the Docker host, then pull Postgram's default\n   embedding model:\n\n   ```bash\n   ollama pull bge-m3\n   ```\n\n   For hosted OpenAI embeddings instead, create a `.env` file containing a real\n   key before starting Compose:\n\n   ```dotenv\n   OPENAI_API_KEY=<your-openai-key>\n   ```\n\n3. Start the stack:\n\n   ```bash\n   docker compose up -d --build\n   ```\n\n   The first run creates persistent Docker volumes for PostgreSQL and\n   installation secrets. No `.env` file is required for the default Compose\n   path.\n\n4. Read the one-time bootstrap token:\n\n   ```bash\n   docker compose logs mcp-server \\\n     | grep 'Bootstrap token:' \\\n     | tail -n 1\n   ```\n\n   The plaintext appears only in the original first-start logs. Capture it\n   before recreating the API container or discarding those logs.\n\n5. Open [http://127.0.0.1:3000/admin](http://127.0.0.1:3000/admin), paste the\n   token, create the first admin, enroll MFA, and follow the onboarding flow.\n\n6. Confirm the selected provider in the Admin **Config** tab. If you add or\n   change staged settings, save, validate, and apply them, then restart\n   `mcp-server` when Admin marks a restart as required:\n\n   ```bash\n   docker compose restart mcp-server\n   ```\n\n   When Ollama runs on the Docker host, its base URL is\n   `http://host.docker.internal:11434`. Optional LLM relationship extraction is\n   disabled by default and can use OpenAI, Anthropic, Ollama, or an\n   OpenAI-compatible endpoint. Changing the embedding provider, model, or\n   dimensions after the first start is migration work and is blocked from a\n   simple config apply.\n\n7. Check health, then create an API key in the Admin **Overview** tab. For the\n   smoke test below, allow `read` and `write`, the `memory` entity type, and\n   `personal` visibility:\n\n   ```bash\n   curl -fsS http://127.0.0.1:3100/health\n   ```\n\n   The response should include `\"status\":\"ok\"` and\n   `\"postgres\":\"connected\"`.\n\n8. Install the CLI and verify an authenticated write and search. Enrichment is\n   asynchronous, so wait for `pgm queue` to report no pending work before the\n   search:\n\n   ```bash\n   npm install -g @ivotoby/postgram-cli\n   export PGM_API_URL=http://127.0.0.1:3100\n   export PGM_API_KEY='<plaintext-api-key>'\n\n   pgm store \"Postgram quick start is working\" \\\n     --type memory \\\n     --visibility personal \\\n     --tags quickstart\n   pgm queue\n   pgm search \"quick start\"\n   ```\n\nIf embeddings are unreachable, Postgram still starts and accepts writes, but\nenrichment and search will fail until the provider is available. See the\n[full quick start](https://postgram.dev/getting-started/quick-start/) and\n[troubleshooting guide](https://postgram.dev/operations/troubleshooting/) for\nthe longer path.\n\nFor access from ChatGPT, Claude, or another remote MCP client, put Postgram\nbehind HTTPS, enable OAuth, and follow the\n[MCP integration guide](https://postgram.dev/guides/mcp-integration/). Do not\npublish the loopback development ports directly to the internet.\n\n## What It Does\n\nPostgram provides:\n\n- durable storage for typed entities: `memory`, `person`, `project`, `task`,\n  `interaction`, `document`\n- hybrid BM25 + vector search with asynchronous enrichment\n- knowledge graph with typed directional edges between entities\n- LLM-powered relationship extraction (OpenAI, Anthropic, or Ollama)\n- document sync from local markdown repos via manifest comparison\n- browser interfaces for knowledge work and guarded administration\n- UMAP and PCA projections of embedded entities\n- GTD-style capture, task organization, and Kanban views\n- scoped API-key authentication and visibility restrictions\n- a REST API for application and automation access\n- a Streamable HTTP MCP endpoint for agent-native tool access\n- a CLI (`pgm`) for humans and agents\n- a container-local admin CLI (`pgm-admin`)\n- Talon SQLite migration tooling\n- encrypted backup support\n- audit logging for mutating and privileged operations\n\n## How It Works\n\nPostgram is a TypeScript Node.js application built around a service layer.\n\nMain components:\n\n- PostgreSQL + `pgvector` for persistence and vector search\n- Hono for the HTTP server\n- MCP over Streamable HTTP for agent-facing tool access\n- CLI/admin CLIs built with Commander\n- background enrichment worker for chunking, embeddings, and LLM extraction\n\nHigh-level flow:\n\n1. a client stores or updates an entity\n2. the entity is written immediately\n3. enrichment runs asynchronously: chunking, embedding, and optionally LLM extraction\n4. chunks and embeddings are produced in the background\n5. edges are created from extracted relationships (if extraction is enabled)\n6. search queries use hybrid BM25 + vector scoring, with optional graph expansion\n\n## Main Features\n\n### 1. Typed Knowledge Storage\n\nStore structured knowledge objects with:\n\n- `type` (memory, person, project, task, interaction, document)\n- `content`\n- `tags`\n- `visibility` (personal, work, shared)\n- `status`\n- arbitrary JSON metadata\n\n### Memory Roles\n\nPostgram supports two roles for `memory` entities:\n\n- `durable_memory`: long-term memory future agents should trust, such as decisions, preferences, constraints, root causes, and completed-work summaries.\n- `session_context`: working context for resuming recent conversations. Session context is scoped to the calling client, embedded for semantic recall, and skipped by graph extraction.\n\nUse session context for \"where were we in this thread?\" Use durable memory for \"what should future agents remember as true?\"\n\nCLI users can write session context with `pgm memory session-context` and search\nit with `pgm search --memory-role session_context`.\n\nOperators can groom stale session context with `pgm-admin memory groom`.\nUse `--client-id <client-id>` for one client or `--all-clients` to batch over\nevery session-context scope. `--all-clients` keeps each client scope separate;\nit is operational batching, not cross-client consolidation.\n`--older-than <duration>` defaults to `7d` and accepts values like `30m`,\n`4h`, `7d`, or `0d`. `--dry-run` previews eligible memories without calling\nthe LLM. Grooming has no default candidate cap; pass `--limit <n>` when you\nwant to process a bounded batch.\n`--mode archive --yes` archives eligible working context directly.\n`--mode promote --yes` uses the configured extraction LLM to decide whether\neach session-context memory should be promoted; promoted memories are distilled\ninto new `durable_memory` entities, the source context is archived, and\nprovenance is recorded with `metadata.promoted_to` plus a `promoted_to` edge.\n\nAuthenticated users and agents can self-groom only their own client-scoped\nsession context:\n\n```bash\npgm memory groom --dry-run --older-than 7d\npgm memory groom --older-than 14d --topic postgram --tag session-context --yes\n```\n\nThe normal CLI derives scope from `PGM_API_KEY`; it does not accept\n`--client-id`, `--all-clients`, or promotion mode. Archive requires `--yes`.\nOptional filters are `--topic`, `--session-id`, and repeatable `--tag`.\n\nMCP clients can use the `groom_session_context` tool with the same self scope:\n\n```json\n{\n  \"mode\": \"dry_run\",\n  \"older_than\": \"7d\",\n  \"topic\": \"postgram\",\n  \"session_id\": \"optional-session-id\",\n  \"tags\": [\"session-context\"]\n}\n```\n\nMCP `mode` is `dry_run` or `archive`; promotion remains admin-only.\n\nFor scheduled maintenance, run grooming from the host that has access to the\nPostgram container. This cron example assesses eligible session context for all\nclient scopes every three days at 03:17 and appends JSON output to a log. The\nwrapper detects that cron does not provide a TTY and runs non-interactively:\n\n```cron\n17 3 */3 * * cd /path/to/postgram && ./bin/pgm-admin --json memory groom --all-clients --older-than 7d --mode promote --yes >> /var/log/postgram-memory-groom.log 2>&1\n```\n\nUse `--mode archive --yes` instead if you want to archive eligible working\ncontext without LLM-assisted promotion. Run the same command with `--dry-run`\nfirst to verify the eligible set.\n\nOperators can also review durable memory quality without mutating the durable\nclaim itself:\n\n```bash\n./bin/pgm-admin memory groom-durable --dry-run --older-than 30d\n./bin/pgm-admin memory groom-durable --mode mark --yes --older-than 30d\n```\n\nDurable grooming selects active `durable_memory` rows, including legacy memory\nrows with no `metadata.memory_role`, and classifies them as `keep`,\n`needs_grooming`, `archive`, or `superseded`. Mark mode writes\n`metadata.durable_grooming` with the outcome, reason, review timestamp, and any\nLLM suggestions. It does not rewrite content, change status, archive rows, or\nmerge duplicates.\n\nTo actually clean the marked rows, apply the grooming labels:\n\n```bash\n./bin/pgm-admin memory apply-durable-grooming --dry-run\n./bin/pgm-admin memory apply-durable-grooming --yes\n```\n\nApply mode defaults to `auto`: `needs_grooming` memories are rewritten from the\nstored suggestion or the configured extraction LLM, while `archive` and\n`superseded` memories are archived. Rewrites clear stale chunks and re-queue\nembedding enrichment. Use `--mode rewrite` or `--mode archive`, plus\n`--status`, `--topic`, `--tag`, `--visibility`, or `--limit`, to narrow the\nbatch.\n\n### 2. Async Enrichment\n\nEntities with content are persisted first and enriched later. Each entity\ntracks `enrichment_status`: `pending`, `completed`, or `failed`. Failed\nentities are retried up to 3 times with a 5-minute backoff.\n\n### 3. Hybrid Search\n\nSearch blends vector cosine similarity (60%) with BM25 keyword ranking (40%)\ntransparently. Broad searches select candidates through the HNSW index; small\nfiltered sets use exact distance ranking, and HNSW falls back to exact ranking\nwhen its candidate scan cannot fill its target. Search requires a reachable\nembedding provider; if that provider is unavailable, writes still succeed but\nenrichment and search fail until it recovers. Results include:\n\n- ranked results with blended scores\n- similarity scores\n- recency-adjusted scores\n- matching chunk text\n- optional 1-hop graph neighbors (`expand_graph` parameter)\n\n<p align=\"center\">\n  <img src=\"assets/search.png\" alt=\"Hybrid search results in Postgram\" />\n</p>\n\n### 4. Knowledge Graph\n\nEntities can be connected by typed directional edges:\n\n- relation types: `involves`, `assigned_to`, `part_of`, `blocked_by`,\n  `mentioned_in`, `related_to`, or any custom type\n- edges have a confidence score (1.0 for manual, LLM-assigned for extracted)\n- graph traversal via `expand` with configurable depth (1-3 hops)\n- duplicate edge prevention via `UNIQUE(source_id, target_id, relation)`\n- edges are created manually via `link`/`unlink` or automatically by the\n  LLM extraction pipeline\n\n<p align=\"center\">\n  <img src=\"assets/graph.png\" alt=\"Postgram knowledge graph view\" />\n</p>\n\n### 5. LLM Extraction\n\nWhen enabled, the enrichment worker extracts relationships from entity content\nusing an LLM. Extracted entity names are matched against existing entities and\nedges are created automatically.\n\nSupported providers:\n\n| Provider  | Model default               | Env vars required                                     |\n| --------- | --------------------------- | ----------------------------------------------------- |\n| OpenAI    | `gpt-4o-mini`               | `OPENAI_API_KEY`                                      |\n| Anthropic | `claude-haiku-4-5-20251001` | `ANTHROPIC_API_KEY`                                   |\n| Ollama    | `llama3.2`                  | `OLLAMA_BASE_URL` (default: `http://localhost:11434`) |\n\nThese are configuration defaults, not model-quality recommendations. Graph\nextraction is a constrained structured-output task; validate the resulting\nedges on your own corpus before running a large backfill, especially with small\nlocal models.\n\n<p align=\"center\">\n  <img src=\"assets/embeddings.png\" alt=\"Postgram embedding projection view\" />\n</p>\n\n### 6. Document Sync\n\nSync local directories of markdown files into postgram:\n\n```bash\npgm sync ~/Documents/personal-notes\npgm sync ~/Documents/cf-notes --repo cf-notes --quiet\n```\n\nThe CLI walks the directory for `.md` files, computes SHA-256 hashes, and sends\na full manifest to the server. The server diffs against stored state and\ncreates, updates, or archives document entities. Supports `--dry-run` and cron\nscheduling.\n\n### 7. Access Control\n\nAPI keys can be restricted by:\n\n- scopes: `read`, `write`, `delete`, `sync`\n- allowed entity types\n- allowed visibility levels\n\n### 8. Task Management\n\nTasks are first-class entities with convenience operations for:\n\n- create (with GTD context and due dates)\n- list (filtered by status and context)\n- update\n- complete (with completion timestamp)\n\n### 9. Multiple Interfaces\n\nThe same service layer is exposed through:\n\n- REST API\n- Streamable HTTP MCP endpoint\n- `pgm` CLI\n- `pgm-admin` CLI (`./bin/pgm-admin`)\n- Browser extensions for [Chrome](./packages/browser-extension-chrome) and\n  [Firefox](./packages/browser-extension-firefox) — one-click web clipper\n  that captures the current page or text selection via the REST API.\n  Build with `npm run -w @ivotoby/postgram-browser-extension-chrome package`\n  (or the Firefox equivalent); install unpacked from the per-package\n  README.\n\n## Repository Layout\n\n```text\nsrc/\n  auth/            API key validation and auth middleware\n  cli/             CLI for humans/agents and admin CLI\n  db/              Pool and migrations\n  migrate-talon/   Talon import path\n  services/        Business logic (entities, search, edges, sync, extraction)\n  transport/       REST and MCP adapters\n  types/           Shared types\n  util/            Errors, audit, logging\n\nui/                User-facing web UI and Admin UI\ncli/               Published @ivotoby/postgram-cli package\ndocker/            Container entrypoint and secret bootstrap scripts\nbin/               Local operator wrappers\n\npackages/\n  browser-extension-chrome/   Chromium web clipper (MV3)\n  browser-extension-firefox/  Firefox web clipper (MV3)\n\ntests/\n  contract/        REST and MCP contract tests\n  integration/     Service and CLI integration tests\n  unit/            Pure logic tests\n```\n\n## Requirements\n\n- Docker and Docker Compose for the recommended deployment\n- Node.js 22+ for the `pgm` CLI or local development\n- a reachable OpenAI or Ollama embedding provider for enrichment and search\n\nOptional:\n\n- OpenAI API key (for OpenAI embeddings or extraction)\n- Anthropic API key (for LLM extraction)\n- Ollama (for local embeddings or LLM extraction)\n- `gpg` (for encrypted CLI backups)\n\n## Docker Setup Details\n\n### 1. Start Docker Compose\n\n```bash\ndocker compose up -d --build\n```\n\nThe default Compose path does not require manual `.env` edits. On first run it\ncreates a persistent `postgram_secrets` Docker volume containing:\n\n- the Postgres password used by the app container\n- `ADMIN_MFA_SECRET_KEY` for encrypted admin TOTP seeds\n- `ADMIN_SETTINGS_ENCRYPTION_KEY` for DB-backed provider secrets\n\nIf an existing Docker install already has `POSTGRES_PASSWORD` in `.env`, the\nfirst start after this change copies that legacy password into\n`postgram_secrets/postgres-password` instead of generating a different database\npassword. Keep the old `.env` value in place for that first upgraded start.\n\nThe API binds to `127.0.0.1:3100` and the UI binds to `127.0.0.1:3000` by\ndefault. Use `POSTGRAM_API_PORT=<port>` or `UI_PORT=<port>` as shell overrides\nwhen running more than one local stack.\n\nTo use an existing Postgres cluster with Compose, set `POSTGRES_HOST`,\n`POSTGRES_PORT`, `POSTGRES_DB`, and `POSTGRES_USER` on `mcp-server` in a Compose\noverride and remove the `postgres` dependency, as in the operator examples. If\nthat external cluster requires password auth, set `POSTGRES_PASSWORD` in `.env`;\nif it uses passwordless local auth, leave `POSTGRES_PASSWORD=` blank. You can\nalso bypass the split settings entirely by setting `DATABASE_URL`.\n\nFor embeddings, Compose preserves the OpenAI default when `OPENAI_API_KEY` is\npresent. If no OpenAI key and no explicit `EMBEDDING_PROVIDER` are supplied, the\ncontainer entrypoint chooses local Ollama embeddings so a clean stack can boot\nbefore provider secrets are configured.\n\n### 2. Complete first admin setup\n\nOn first start, the API container prints a clear one-time bootstrap banner with\nthe token:\n\n```text\nPostgram first admin setup\nBootstrap token: ...\nOpen http://127.0.0.1:3000/admin and paste this token.\n```\n\nIf the console has scrolled, read the same one-time bootstrap token from the\ntrusted local operator channel:\n\n```bash\ndocker compose logs mcp-server | grep 'Bootstrap token:' | tail -n 1\n```\n\nThen open `http://127.0.0.1:3000/admin`, create the first admin user, and\ncomplete MFA enrollment. The bootstrap token is stored hash-only in Postgres,\nexpires after 24 hours, and is invalidated after the first admin is created.\nIf you changed the Postgres target, copy the latest bootstrap-token log line;\nolder lines may belong to a previous database and will be rejected.\n\nAfter active MFA login, the Admin dashboard opens a guided onboarding flow until\nit is completed or deliberately skipped. The guide explains the setup path in\nplain operator language:\n\n- what bootstrap, admin login, and MFA confirmation protect\n- how provider settings, embedding dimensions, extraction models, and\n  write-only provider secrets fit together\n- when to validate and apply saved provider configuration\n- why backup/restore is staged before switch-over\n- how maintenance dry-runs, re-extraction, re-embedding, and edge pruning work\n\nOnboarding progress is stored server-side in Postgres. Refreshing the browser,\nclosing the tab, logging out and back in, or restarting the Docker containers\nresumes at the latest saved step as long as the existing `pgdata` volume is\npreserved. The Onboarding tab remains available from the dashboard after skip\nor completion.\n\nFor local Docker testing, preserve the database volume:\n\n```bash\ndocker compose up -d --build\ndocker compose restart mcp-server postgram-ui\n```\n\nDo not use `docker compose down -v` when testing onboarding resume behavior.\nThat command removes named volumes, including the `pgdata` Postgres volume, and\nwill reset the server-side onboarding state along with the database.\n\n### 3. Check health\n\n```bash\ncurl http://127.0.0.1:3100/health\n```\n\nExpected:\n\n- `status: \"ok\"`\n- `postgres: \"connected\"`\n\n### 4. Configure providers and create API keys\n\nUse the Admin dashboard in the browser for the supported happy path:\n\n- Onboarding tab: resume, skip, or complete the Docker-first setup guide.\n- Config tab: save provider settings and write-only provider secrets.\n- Overview tab: create Postgram API keys, inspect health, queue, stats,\n  config/model/job status, and audit rows.\n- Maintenance tab: run safe dry-run previews and poll job status before any\n  destructive apply.\n- Backup tab: download a gzipped v2 archive containing a data-only PostgreSQL\n  custom dump plus redacted runtime configuration. Restore is intentionally\n  staged: the server rejects legacy v1/full-schema archives, accepts only\n  approved Postgram table-data entries from `pg_restore --list`, creates the\n  trusted schema from bundled migrations, and restores the accepted data into\n  a new database name. Health checks run before operator-approved switch-over.\n  If the restored database misbehaves, roll back by restoring the previous\n  `POSTGRES_DB` or `DATABASE_URL` setting and restarting\n  `mcp-server`/`postgram-ui`; the old database is left untouched for this\n  emergency path.\n\nNormal Docker setup and maintenance should not require `pgm-admin` after\nstartup/bootstrap. The `pgm-admin` CLI remains documented below for emergency\nrecovery, embedding migrations, raw SQL inspection, and advanced operator\njobs.\n\n### Docker Secret Backup And Failure Behavior\n\nBack up the `postgram_secrets` Docker volume separately from database backups.\nDatabase backups contain encrypted provider secrets and encrypted TOTP factors;\nthey do not contain the installation keys needed to decrypt them.\n\nLosing or replacing `ADMIN_MFA_SECRET_KEY` prevents existing TOTP factors from\nbeing verified. Losing or replacing `ADMIN_SETTINGS_ENCRYPTION_KEY` prevents\nstored provider secrets from being decrypted. With the wrong settings key,\nprovider config reads remain redacted, provider apply/runtime secret use fails\nclosed, and operators must restore the original key or re-save provider\nsecrets after a deliberate rotation/recovery procedure.\n\nFor Docker Compose, missing secret files are generated only on an empty\n`postgram_secrets` volume. Invalid persisted secret files fail container\nstartup before the server binds. Optional env overrides still work, but keep\nthose values outside database backups and browser storage.\n\n## Environment Variables\n\n### Server\n\n| Variable                      | Required    | Default | Description                                                                                                                    |\n| ----------------------------- | ----------- | ------- | ------------------------------------------------------------------------------------------------------------------------------ |\n| `DATABASE_URL`                | non-Compose | Docker secret file + Postgres env | Full Postgres connection string. Compose constructs it from the generated Postgres password secret when unset.                 |\n| `POSTGRES_HOST`               | no          | `postgres` | Compose Postgres host used when `DATABASE_URL` is unset. Override to `host.docker.internal` or another hostname for an existing cluster. |\n| `POSTGRES_PORT`               | no          | `5432`  | Compose Postgres port used when `DATABASE_URL` is unset.                                                                        |\n| `POSTGRES_DB`                 | no          | `postgram` | Compose Postgres database used when `DATABASE_URL` is unset.                                                                    |\n| `POSTGRES_USER`               | no          | `postgram` | Compose Postgres user used when `DATABASE_URL` is unset.                                                                        |\n| `POSTGRES_PASSWORD`           | no          | Docker secret file | Compose Postgres password used when `DATABASE_URL` is unset. For external hosts, an explicit blank value builds a passwordless URL. |\n| `ADMIN_MFA_SECRET_KEY`        | admin setup | Docker secret file | Stable 32+ character secret used to encrypt admin TOTP seeds. Compose generates and persists it in `postgram_secrets` when unset. |\n| `OPENAI_API_KEY`              | conditional |         | Required when `EMBEDDING_PROVIDER=openai` OR (`EXTRACTION_ENABLED=true` AND `EXTRACTION_PROVIDER=openai`). Optional otherwise. |\n| `ADMIN_SETTINGS_ENCRYPTION_KEY` | when saving admin-managed secrets | Docker secret file | 32-byte base64url installation key used to encrypt DB-backed provider secrets. Compose generates and persists it in `postgram_secrets` when unset. Keep it outside database backups. |\n| `PORT`                        | no          | `3100`  | HTTP/MCP server port                                                                                                           |\n| `POSTGRAM_API_PORT`           | no          | `3100`  | Docker Compose host port for the API/backend. The container listen port stays `3100`.                                          |\n| `UI_PORT`                     | no          | `3000`  | Docker Compose host port for the UI.                                                                                           |\n| `OAUTH_ENABLED`               | no          | `false` | Enable OAuth authorization-code, PKCE, and Dynamic Client Registration routes for native remote MCP connectors.                 |\n| `PUBLIC_BASE_URL`             | conditional |         | Public HTTPS origin for OAuth metadata and callback URLs. Required when `OAUTH_ENABLED=true`. Example: `https://postgram.example.com`. |\n| `LOG_LEVEL`                   | no          | `info`  | pino log level                                                                                                                 |\n| `ENRICHMENT_POLL_INTERVAL_MS` | no          | `1000`  | Enrichment worker poll interval                                                                                                |\n\n### Embeddings\n\n| Variable               | Required             | Default                         | Description                                                                                                                     |\n| ---------------------- | -------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |\n| `EMBEDDING_PROVIDER`   | no                   | `openai` (Compose auto-selects) | `openai` or `ollama`. Compose keeps OpenAI when `OPENAI_API_KEY` is present, otherwise chooses Ollama unless explicitly set. |\n| `EMBEDDING_MODEL`      | no                   | per-provider                    | Defaults: `text-embedding-3-small` (openai, 1536 dims), `bge-m3` (ollama, 1024 dims)                                            |\n| `EMBEDDING_DIMENSIONS` | no                   | per-provider                    | Must match the active `embedding_models` row. Run `./bin/pgm-admin embeddings migrate --target-dimensions <N> --yes` to change. |\n| `EMBEDDING_BASE_URL`   | when provider=ollama | falls back to `OLLAMA_BASE_URL` | Embedding host. Independent from LLM-extraction host so embeddings and inference can target different machines.                 |\n| `EMBEDDING_API_KEY`    | no                   |                                 | Optional bearer token for `EMBEDDING_BASE_URL`.                                                                                 |\n| `EMBEDDING_TIMEOUT_MS` | no                   | `15000`                         | Hard timeout for a single embedding provider call. Bounds how long one stalled call can delay a request. |\n| `QUERY_EMBEDDING_CACHE_SIZE` | no             | `512`                           | In-process query embeddings held in front of the Postgres-backed cache. |\n| `QUERY_EMBEDDING_CACHE_SECRET` | no           |                                 | Keys the query digest with an HMAC. Without it the digest is an unkeyed sha256, which a reader of the database can dictionary-test to confirm whether a guessed query was run. Set it if you treat query text as more sensitive than entity content; it must live outside the database to mean anything. Changing it invalidates existing cache rows. |\n| `QUERY_EMBEDDING_CACHE_RETENTION_DAYS` | no   | `30`                            | Age at which persisted query embeddings are pruned. The hourly prune also retains only the 2,000 newest entries per client. |\n\nWhen Postgram runs in Docker and Ollama runs directly on the Docker host, use `http://host.docker.internal:11434` for `EMBEDDING_BASE_URL`; `localhost` inside the container points at the Postgram container, not the host machine.\n\nSee [`specs/002-local-embeddings/quickstart.md`](specs/002-local-embeddings/quickstart.md) for a walkthrough of fresh-install-on-Ollama and migrating from OpenAI.\n\n### LLM Extraction\n\n| Variable                                       | Required                        | Default                      | Description                                                                                                                                                                                                                                                                                                  |\n| ---------------------------------------------- | ------------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `EXTRACTION_ENABLED`                           | no                              | `false`                      | Enable LLM relationship extraction                                                                                                                                                                                                                                                                           |\n| `EXTRACTION_MEMORY_MODE`                       | no                              | `embed_only`                 | Controls graph extraction for `type=memory`: `embed_only` keeps all memories searchable through embeddings without graph/entity extraction; `extract_durable` extracts only `durable_memory`; `extract_all` extracts both durable and session-context memories.                                                |\n| `EXTRACTION_PROVIDER`                          | no                              | `openai`                     | LLM provider: `openai`, `anthropic`, `ollama`, or `openai-compatible`                                                                                                                                                                                                                                        |\n| `EXTRACTION_MODEL`                             | no                              | per-provider                 | Model name (defaults: `gpt-4o-mini` for OpenAI, `claude-haiku-4-5-20251001` for Anthropic, `llama3.2` for Ollama, `gpt-4o-mini` for OpenAI-compatible)                                                                                                                                                       |\n| `EXTRACTION_BASE_URL`                          | when provider=openai-compatible |                              | Base URL for OpenAI-compatible chat-completions APIs, including any `/v1` path. Postgram appends `/chat/completions`. Example: `http://host.docker.internal:8000/v1`.                                                                                                                                        |\n| `EXTRACTION_API_KEY`                           | no                              |                              | Optional bearer token for `EXTRACTION_BASE_URL`.                                                                                                                                                                                                                                                             |\n| `EXTRACTION_AUTO_CREATE_ENTITIES`              | no                              | `false`                      | When true, extraction creates stub entities for referenced targets that don't yet exist (e.g. a person named in a document gets a `person` entity automatically). Tagged `auto-created`; metadata records the originating document.                                                                          |\n| `EXTRACTION_AUTO_CREATE_TYPES`                 | no                              | `person,project,interaction` | Comma-separated list of entity types eligible for auto-creation. `document`, `task`, `memory` are intentionally excluded from the default to keep those user-authored.                                                                                                                                       |\n| `EXTRACTION_AUTO_CREATE_MIN_CONFIDENCE`        | no                              | `0.7`                        | Minimum per-extraction confidence (0–1) required to auto-create an entity. Raise to cut noise, lower for a denser graph.                                                                                                                                                                                     |\n| `ANTHROPIC_API_KEY`                            | when provider=anthropic         |                              | Anthropic API key                                                                                                                                                                                                                                                                                            |\n| `OLLAMA_BASE_URL`                              | no                              | `http://localhost:11434`     | Ollama server URL                                                                                                                                                                                                                                                                                            |\n| `EXTRACTION_REASONING_EFFORT`                  | no                              | unset                        | `minimal` \\| `low` \\| `medium` \\| `high`. Forwarded as `reasoning_effort` to OpenAI and Ollama for reasoning models (o-series, gpt-5, gpt-oss). When set, overrides the implicit `minimal` that `EXTRACTION_DISABLE_THINKING=true` sends to OpenAI.                                                          |\n| `LLM_REQUEST_TIMEOUT_MS`                       | no                              | `120000`                     | Hard cap per LLM call in milliseconds. Bump this when running slow local models (e.g. `gpt-oss:120b-cloud`).                                                                                                                                                                                                 |\n| `EXTRACTION_SEMANTIC_NEIGHBORS_ENABLED`        | no                              | `false`                      | Enable semantic neighbor linking (see below).                                                                                                                                                                                                                                                                |\n| `EXTRACTION_SEMANTIC_NEIGHBORS_MAX`            | no                              | `10`                         | Maximum number of neighbor edges to create per entity.                                                                                                                                                                                                                                                       |\n| `EXTRACTION_SEMANTIC_NEIGHBORS_MIN_SIMILARITY` | no                              | `0.65`                       | Minimum cosine similarity (0–1) for an entity to qualify as a neighbor. Raise to reduce noise; lower if you're finding too few neighbors. The right value depends on your embedding model's similarity distribution — use `./bin/pgm-admin link-neighbors --all --dry-run` to inspect actual scores before tuning. |\n\n**Semantic neighbor linking**: the LLM extraction pass only finds entities that\nare explicitly named in the source content. It misses entities that are\nthematically related but not cited by name — a weekly kickoff meeting about the\nsame initiative, a wiki page covering the same strategy, a decision memo about\nthe same project. When `EXTRACTION_SEMANTIC_NEIGHBORS_ENABLED=true`, a second\npass runs after LLM extraction that queries the knowledge store for entities\nwhose stored chunk embeddings are cosine-similar to the source entity's own\nembeddings, and links them with `related_to`. No extra LLM or embedding API\ncalls are needed — the source entity's chunks are already stored by the\nenrichment step that runs before extraction. Edges created by this pass carry\n`source = 'semantic-neighbor'` so they are distinguishable from LLM-extracted\nedges. Entities already linked by the LLM pass are excluded to avoid a weaker\n`related_to` edge shadowing a stronger-typed edge for the same pair.\n\n**Backfilling and maintaining neighbor edges**: the `./bin/pgm-admin link-neighbors`\ncommand runs the semantic neighbor pass directly — no LLM calls, no extraction\nqueue, just cosine similarity over stored chunks. Use it to backfill an\nexisting graph or as a recurring maintenance job after new entities are added.\n\n```bash\n# Backfill all enriched entities (safe to re-run — edges are upserted).\n./bin/pgm-admin link-neighbors --all\n\n# Only documents:\n./bin/pgm-admin link-neighbors --type document\n\n# Single entity:\n./bin/pgm-admin link-neighbors --id <uuid>\n\n# Preview what would be linked and at what similarity — no edges created:\n./bin/pgm-admin link-neighbors --id <uuid> --dry-run\n./bin/pgm-admin link-neighbors --all --dry-run\n\n# Tune the similarity threshold or edge cap:\n./bin/pgm-admin link-neighbors --all --min-similarity 0.75 --max-neighbors 5\n\n# Process in bounded batches (oldest-first):\n./bin/pgm-admin link-neighbors --all --limit 500\n```\n\nUse `--dry-run` to inspect actual cosine similarity scores before committing edges — especially useful when tuning `--min-similarity` for a new embedding model. The output shows each entity and its candidate neighbors with their raw similarity scores.\n\nIf you also want to re-run LLM extraction at the same time (e.g. after enabling\n`EXTRACTION_SEMANTIC_NEIGHBORS_ENABLED=true`), use `reextract` instead — the\nworker runs both the LLM pass and the neighbor pass together:\n\n```bash\n./bin/pgm-admin reextract --all\n```\n\nNote: `--clean-edges` on `reextract` only removes edges with\n`source='llm-extraction'` — it does not touch `semantic-neighbor` edges. For a\nfull clean slate:\n\n```sql\nDELETE FROM edges WHERE source = 'semantic-neighbor';\n```\n\n**Scheduling as a recurring maintenance job**: because `link-neighbors` is\ncheap (no LLM calls) and idempotent (edges are upserted, not duplicated), it\nworks well as a weekly cron job that keeps the neighbor graph fresh as new\nentities are added. Example cron entry running every Sunday at 02:00:\n\n```cron\n0 2 * * 0 cd /path/to/postgram && ./bin/pgm-admin link-neighbors --all\n```\n\nOr with Docker Compose:\n\n```bash\n./bin/pgm-admin link-neighbors --all\n```\n\n**Auto-created entities**: when `EXTRACTION_AUTO_CREATE_ENTITIES=true`,\nentities that didn't exist before a document mentioned them are inserted\nwith `content` = the extracted name, `tags` including `auto-created`, and\n`metadata.auto_created_by = 'llm-extraction'` plus\n`metadata.source_entity_id` pointing at the document that caused the\ncreation. They enter the normal embedding queue so they become\nsearchable, **but they are deliberately excluded from the extraction\nqueue** — their only content is a bare name, so asking the LLM \"what\ndoes Alice relate to?\" with no context would just free-associate new\nstubs in a loop. To review or clean them up:\n\n```bash\npgm list --tags auto-created --type person\n# or wholesale prune:\ndocker compose exec postgres psql -U postgram -d postgram -c \\\n  \"DELETE FROM entities WHERE 'auto-created' = ANY(tags);\"\n```\n\n### CLI\n\n| Variable      | Required | Description                |\n| ------------- | -------- | -------------------------- |\n| `PGM_API_URL` | yes      | Server URL                 |\n| `PGM_API_KEY` | yes      | API key for authentication |\n\n### Admin CLI\n\n| Variable       | Required | Description                               |\n| -------------- | -------- | ----------------------------------------- |\n| `DATABASE_URL` | yes      | Direct DB connection for admin operations |\n\n### Backup\n\n| Variable                             | Required               | Description               |\n| ------------------------------------ | ---------------------- | ------------------------- |\n| `DATABASE_URL` or `PGM_DATABASE_URL` | yes                    | Database connection       |\n| `PGM_BACKUP_PASSPHRASE`              | when using `--encrypt` | GPG encryption passphrase |\n\n## Running The Server\n\n### Pre-built Docker image\n\nPull from GitHub Container Registry:\n\n```bash\ndocker pull ghcr.io/ivo-toby/postgram:latest\n```\n\nImages are multi-arch (`linux/amd64`, `linux/arm64`). Tags available:\n\n- `latest` — most recent build of `main`\n- `main` — same as `latest`, explicit branch name\n- `sha-<short>` — pinned to a specific commit\n\nThe `docker-compose.yml` in this repo builds locally by default; to use the\npre-built image instead, replace `build: .` with `image: ghcr.io/ivo-toby/postgram:latest`\nfor the `mcp-server` service.\n\n### Local development\n\n```bash\nnpm run dev\n```\n\nProduction-style local run:\n\n```bash\nnpm run build\nnpm start\n```\n\nThe server exposes:\n\n- REST API at `http://127.0.0.1:3100/api`\n- MCP endpoint at `http://127.0.0.1:3100/mcp`\n- Health endpoint at `http://127.0.0.1:3100/health`\n\n## Authentication\n\nCreate an API key from the Admin dashboard at `http://127.0.0.1:3000/admin`.\nThe plaintext key is displayed once in the browser and cannot be recovered\nafter dismissal or reload.\n\nExport it for CLI use:\n\n```bash\nexport PGM_API_URL=http://127.0.0.1:3100\nexport PGM_API_KEY='<plaintext-key>'\n```\n\n## REST API Overview\n\n### Entity endpoints\n\n- `POST /api/entities` — store entity\n- `GET /api/entities/:id` — recall entity\n- `PATCH /api/entities/:id` — update entity\n- `DELETE /api/entities/:id` — soft-delete entity\n- `GET /api/entities` — list entities\n\n### Search\n\n- `POST /api/search` — hybrid BM25+vector search (supports `expand_graph` and `include_content`)\n\nREST search keeps full entity content by default for backwards compatibility.\nPass `include_content: false` to return matched chunks without hydrating or\nserializing full result and graph-neighbor content. Other REST routes continue\nto return their existing full JSON responses. Compact JSON and TOON remain\ntransport-layer conveniences for MCP and the CLI.\n\n### Tasks\n\n- `POST /api/tasks` — create task\n- `GET /api/tasks` — list tasks\n- `PATCH /api/tasks/:id` — update task\n- `POST /api/tasks/:id/complete` — complete task\n\n### Document sync\n\n- `POST /api/sync/diff` — diff local manifest against server; returns paths to upload and delete\n- `POST /api/sync/upload` — upload a batch of file contents\n- `POST /api/sync/finalize` — archive orphans and restore stale matches\n- `POST /api/sync` — single-shot push (retained for MCP and small syncs)\n- `GET /api/sync/status/:repo` — get sync status\n\n`pgm sync` uses the three-phase protocol (`diff` → batched `upload` → `finalize`)\nso large repos don't send a single oversized payload. Each upload batch is\ncapped at ~50 files or ~4 MB, whichever comes first.\n\n### Knowledge graph\n\n- `POST /api/edges` — create edge\n- `DELETE /api/edges/:id` — delete edge\n- `GET /api/entities/:id/edges` — list edges for entity\n- `GET /api/entities/:id/graph` — expand graph neighborhood\n\n### Queue / enrichment\n\n- `GET /api/queue` — enrichment + extraction queue status.\n  Pass `?include_failures=true` (optionally `&failure_limit=N`, default 20,\n  max 100) to also receive the most recent failed entities with their\n  error messages, e.g.:\n\n  ```json\n  {\n    \"embedding\": {\n      \"pending\": 0,\n      \"completed\": 120,\n      \"failed\": 0,\n      \"retry_eligible\": 0,\n      \"oldest_pending_secs\": null\n    },\n    \"extraction\": { \"pending\": 2, \"completed\": 98, \"failed\": 3 },\n    \"failures\": [\n      {\n        \"id\": \"…\",\n        \"type\": \"document\",\n        \"kind\": \"extraction\",\n        \"error\": \"llm context exceeded\",\n        \"path\": \"notes/long.md\",\n        \"updatedAt\": \"2026-04-22T10:12:33Z\"\n      }\n    ]\n  }\n  ```\n\nAll `/api/*` routes require `Authorization: Bearer <api-key>`.\n\n## MCP Overview\n\nMCP is served over Streamable HTTP at:\n\n```text\nhttp://127.0.0.1:3100/mcp\n```\n\nExposed tools:\n\n- `store`, `recall`, `search`, `update`, `delete`, `queue`\n- `store_session_context`, `groom_session_context`\n- `task_create`, `task_list`, `task_update`, `task_complete`\n- `sync_push`, `sync_status`\n- `link`, `unlink`, `expand`\n\nThe MCP tool behavior is intentionally aligned with the REST surface, but\ntoken-heavy outputs default to compact agent-friendly responses:\n\n- write acknowledgements (`store`, `store_session_context`, `update`, task\n  writes, `link`) return compact ids/status/version instead of echoing full\n  metadata and timestamps\n- `search`, `task_list`, and `expand` return compact rows/graph payloads by\n  default; compact search contains the matched chunk rather than full result or\n  neighbor content and may include `edges.count` and `edges.relations` as cheap\n  traversal affordances\n- pass `full_response: true` to get the full REST-shaped payload, including\n  complete entity content\n- pass `toon: true` on list-like tools (`search`, `task_list`, `expand`) to\n  receive compact TOON text from the MCP layer\n\nSearch is the discovery step: inspect compact IDs, scores, and matched chunks,\nthen call `recall` only for the selected entities whose complete content is\nneeded. Compact `edges` summaries contain counts and relation labels only. They\ndo not include neighbor content. Use `expand_graph` or `expand` when the user\nneeds causes, provenance, decisions, dependencies, blockers, ownership,\ninvolvement, discussion participants, connected context, or graph-based\ndisambiguation. Avoid expansion for direct facts already present in the matched\nchunk.\n\nThe underlying API remains JSON; compacting and TOON happen only in MCP/CLI\nhandlers.\n\n### Native Remote Connectors (ChatGPT and Claude)\n\nLocal MCP clients can connect with a static bearer API key. ChatGPT accounts\nwith custom-connector/developer-mode access and Claude's Connectors UI can\nconnect to a public Postgram endpoint through OAuth, without storing a static\nAPI-key header in the client settings:\n\n```bash\nOAUTH_ENABLED=true\nPUBLIC_BASE_URL=https://postgram.example.com\n```\n\nAdd `${PUBLIC_BASE_URL}/mcp` as the connector URL in ChatGPT or Claude. The\nclient discovers `/.well-known/oauth-protected-resource/mcp`, registers through\n`/oauth/register`, opens `/oauth/authorize`, and receives OAuth tokens from\n`/oauth/token`. The endpoint must be reachable over public HTTPS.\n\nThe authorize page asks for an existing Postgram API key once. Tokens issued\nfrom that approval inherit the API key's scopes, `client_id`, allowed entity\ntypes, and allowed visibility. If the source API key is revoked, OAuth access\nand refresh tokens derived from it stop working. Existing `Authorization:\nBearer <api-key>` clients and `/mcp?apiKey=...` keep working unchanged.\n\n## CLI (`pgm`)\n\n### Install from npm\n\n```bash\nnpm install -g @ivotoby/postgram-cli\n```\n\nThen configure once:\n\n```bash\nexport PGM_API_URL=http://<postgram-host>:3100\nexport PGM_API_KEY=<your-api-key>\n# or persist them in ~/.pgmrc as JSON: { \"api_url\": \"...\", \"api_key\": \"...\" }\n```\n\n### Run from source (for development)\n\nFrom the repo root, invoke the TypeScript entrypoint directly — no build\nstep needed, and it picks up local changes immediately:\n\n```bash\nnpx tsx cli/src/pgm.ts <command>\n# e.g.\nnpx tsx cli/src/pgm.ts sync ~/Documents/personal-notes --repo personal-notes\n```\n\n### Entity commands\n\n```bash\npgm store \"decided to use pgvector\" --type memory --tags decisions\npgm search \"database decisions\"\npgm search \"database decisions\" --type memory          # filter by entity type\npgm search \"who worked on embeddings\" --expand-graph   # include graph neighbours\npgm search \"database decisions\" --json                 # matched chunks for agents\npgm search \"database decisions\" --json --full-response # complete legacy search response\npgm search \"database decisions\" --limit 5 --toon       # compact discovery output\npgm recall <selected-entity-id>                         # complete selected content\npgm list --json                                        # compact JSON rows\npgm list --json --full-response                        # full API-shaped rows\npgm list --toon                                        # compact TOON rows\npgm expand <id> --json                                 # compact graph JSON\npgm expand <id> --toon                                 # compact TOON graph\npgm recall <id>\npgm list --type memory\npgm update <id> --content \"updated text\" --version 1\npgm delete <id>\n```\n\n### Task commands\n\n```bash\npgm task add \"set up monitoring\" --context @focus-work --status next\npgm task list --status next\npgm task update <id> --status waiting --version 1\npgm task complete <id> --version 2\n```\n\n### Document sync\n\n```bash\npgm sync ~/Documents/personal-notes\npgm sync ~/Documents/cf-notes --repo cf-notes --dry-run\npgm sync ~/Documents/personal-notes --quiet  # for cron\n```\n\n### Knowledge graph\n\n```bash\npgm link <source-id> <target-id> --relation involves\npgm expand <entity-id> --depth 2\npgm unlink <edge-id>\n```\n\n### Backup\n\n```bash\npgm backup --encrypt --output /tmp/postgram-backups/\n```\n\n## Admin CLI (`pgm-admin`)\n\nThe supported Docker happy path uses the browser Admin dashboard for bootstrap,\nprovider configuration, API-key creation, status inspection, and safe\nmaintenance dry-runs. `pgm-admin` remains available for emergency recovery,\nembedding migrations, raw SQL inspection, and advanced operator jobs.\n\nThe easy CLI path uses the `bin/pgm-admin` wrapper shipped in the repo. It runs\n`pgm-admin` via `docker exec` when the container is up, and falls back to\n`docker compose run --rm` when it isn't (useful for first-boot migrations\nor when the startup dimension gate is refusing to boot):\n\n```bash\n./bin/pgm-admin <command> [args...]\n```\n\nFor cron or other non-interactive automation, call Docker with `-T` so it does\nnot try to allocate a TTY:\n\n```bash\ndocker compose exec -T mcp-server /app/docker-entrypoint.sh pgm-admin <command>\n```\n\nExamples:\n\n```bash\n./bin/pgm-admin key create --name local --scopes read,write,delete --visibility personal,work,shared\n./bin/pgm-admin stats\n./bin/pgm-admin embeddings migrate --target-dimensions 1024 --dry-run\ndocker compose stop mcp-server\n./bin/pgm-admin embeddings migrate --target-dimensions 1024 --yes\ndocker compose up -d mcp-server\n```\n\nFor an embedding provider, model, or dimension change made in Admin, save,\nvalidate, and apply the target settings before running that migration sequence.\nThe wrapper refuses `--yes` while `mcp-server` is running so the live enrichment\nworker cannot process the re-embedding queue with its previous in-memory\nprovider. The dry-run is safe while the service is running.\n\nShell alias for daily use (add to `~/.bashrc` or `~/.zshrc` on your docker\nhost):\n\n```bash\nalias pgm-admin='/path/to/postgram/bin/pgm-admin'\n# then just: pgm-admin stats\n```\n\nOverride with env if your service/container names differ:\n\n```bash\nPGM_SERVICE=mcp-server PGM_CONTAINER=my-postgram-mcp-server-1 ./bin/pgm-admin stats\n```\n\nDirect equivalent without the wrapper (for reference):\n\n```bash\ndocker compose exec -T mcp-server /app/docker-entrypoint.sh pgm-admin <command>\n# or, when the container is down:\ndocker compose run --rm mcp-server pgm-admin <command>\n```\n\nThe entrypoint is required for commands executed in an already-running\ncontainer. It reconstructs Docker-managed values such as `DATABASE_URL`, which\nare exported for the server process but are not present in a plain\n`docker compose exec` environment.\n\nMain commands:\n\n- `key create`, `key list`, `key revoke`\n- `audit` — query audit logs\n- `model list`, `model set-active`\n- `reembed --all` — mark entities for re-embedding (optionally\n  `--type <type>`; pair with `--model <id>` to switch the active embedding\n  model in the same transaction)\n- `reextract --all` — reset `extraction_status = 'pending'` and clear any\n  stored `extraction_error` so the worker retries extraction (e.g. after\n  switching to a better LLM). Key flags:\n  - `--type <type>` — scope to a specific entity type\n  - `--only-failed` — only re-queue entities whose extraction previously failed\n  - `--no-edges-only` — only re-queue entities that have **no** LLM-extracted\n    edges; useful for targeted maintenance without re-processing entities that\n    already linked correctly (combine with `--type document` to catch large\n    documents that silently produced no edges)\n  - `--clean-edges` — delete existing `source='llm-extraction'` edges for the\n    in-scope entities before re-queuing, giving a clean-slate redo rather than\n    appending alongside old edges\n  - `--limit <n>` — cap how many entities are queued (oldest-first)\n\n  User-created edges (`source != 'llm-extraction'`) are never touched.\n\n- `improve-graph` — queue entities for re-extraction with an optional per-run\n  model/provider override stored on the row. The worker uses the override\n  instead of the env-configured default, then clears it on success. Existing\n  edges are kept by default (no wipe) — overlapping edges have their confidence\n  overwritten by the new run. Key flags:\n  - `--all`, `--type <type>`, `--id <uuid>` — scope what to queue\n  - `--model <name>` — e.g. `claude-sonnet-4-6`; stored per-row\n  - `--provider <name>` — `openai | anthropic | ollama | openai-compatible`; stored per-row\n  - `--no-edges-only` — only queue entities with no LLM-extracted edges\n  - `--clean-edges` — wipe existing LLM edges before queueing\n  - `--limit <n>` — cap the queue size\n\n  Typical maintenance run targeting gaps without paying for the full graph:\n\n  ```bash\n  ./bin/pgm-admin improve-graph --type document --no-edges-only --provider ollama --model <model>\n  ```\n\n- `prune-edges --below <threshold>` — delete edges with `confidence` below\n  the threshold. Scoped to `source='llm-extraction'` by default; pass\n  `--source any` to include all, or `--source <name>` for a specific one.\n  Supports `--relation <name>` and `--dry-run` for a safe preview.\n- `validate-edges` — run an LLM-as-judge quality pass. For each\n  `source='llm-extraction'` edge (configurable via `--source`), asks the\n  configured extraction LLM whether the relationship is supported by the\n  source content; removes edges it judges invalid or below\n  `--min-confidence` (default `0.4`). Tracks `last_validated_at` in edge\n  metadata and skips edges validated within `--skip-validated-days`\n  (default `7`) — run as a maintenance cron without redoing work. Flags:\n  `--limit <n>` (default 100), `--force`, `--dry-run`. Requires\n  `EXTRACTION_ENABLED=true` and the usual `EXTRACTION_PROVIDER` /\n  `EXTRACTION_MODEL` env vars; costs ≈ one LLM call per edge.\n- `sql \"<statement>\"` — execute a raw SQL statement against the database.\n  Accepts a positional argument or reads from stdin for multi-line queries.\n  SELECT results are printed tab-separated (or as JSON with `--json`); DML\n  commands print the affected row count.\n\n  ```bash\n  ./bin/pgm-admin sql \"SELECT id, type, extraction_status FROM entities LIMIT 5\"\n  ./bin/pgm-admin sql --json \"SELECT COUNT(*) FROM edges WHERE source = 'llm-extraction'\"\n\n  # pipe multi-line SQL from a file\n  cat fix.sql | ./bin/pgm-admin sql\n  ```\n\n- `stats` — entity counts, chunk count, DB size\n- `embeddings migrate` — switch embedding dimensions (see [`specs/002-local-embeddings/quickstart.md`](specs/002-local-embeddings/quickstart.md))\n\n## Graph Maintenance\n\nThe knowledge graph builds up over time as LLM extraction links entities\ntogether. Occasionally edges go missing (e.g. after a provider change, a\n`max_tokens` limit being hit, or a model outage) or need refreshing. The admin\nCLI has tools to handle this without re-processing the entire graph.\n\n### Finding gaps\n\nEntities that completed extraction but produced no edges are the primary signal\nof a silent failure:\n\n```bash\n./bin/pgm-admin sql \"\n  SELECT id, char_length(content) AS chars, created_at\n  FROM entities\n  WHERE type = 'document'\n    AND extraction_status = 'completed'\n    AND NOT EXISTS (\n      SELECT 1 FROM edges WHERE source_id = id AND source = 'llm-extraction'\n    )\n  ORDER BY chars DESC\n  LIMIT 20\n\"\n```\n\n### Targeted re-extraction (no wipe)\n\nRe-queue only the entities with no edges. Existing edges on other entities are\nuntouched:\n\n```bash\n# Using the default extraction model\n./bin/pgm-admin reextract --type document --no-edges-only\n\n# Using a local Ollama model (zero API cost)\n./bin/pgm-admin improve-graph --type document --no-edges-only --provider ollama --model <model>\n```\n\n### Full re-extraction pass\n\nWhen you want to redo everything (e.g. after switching to a better model):\n\n```bash\n# Wipe and redo — gives a clean slate\n./bin/pgm-admin reextract --all --clean-edges\n\n# Or scope to documents only\n./bin/pgm-admin reextract --type document --clean-edges\n```\n\n### Confidence pruning\n\nRemove low-confidence edges left behind by older or weaker models:\n\n```bash\n./bin/pgm-admin prune-edges --below 0.5 --dry-run   # preview\n./bin/pgm-admin prune-edges --below 0.5             # apply\n```\n\n### Edge validation\n\nRun an LLM-as-judge pass to remove edges not supported by the source content:\n\n```bash\n./bin/pgm-admin validate-edges --dry-run --limit 200\n./bin/pgm-admin validate-edges --limit 200\n```\n\n### Monitoring queue progress\n\n```bash\npgm queue                              # via pgm CLI\n./bin/pgm-admin sql \"SELECT extraction_status, COUNT(*) FROM entities GROUP BY 1\"\n```\n\n## Talon Migration\n\n```bash\ndocker compose cp /path/to/talon.sqlite mcp-server:/tmp/talon.sqlite\n\ndocker compose exec -T mcp-server \\\n  node dist/migrate-talon/index.js /tmp/t",
  "bytes": 60000,
  "sha": "75275025d6ccb7fccb1d36a2a8822d13047d1e6a822af67050f88a6a7cf9bd57",
  "repo_slug": "ivo-toby/postgram",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_ivo_toby_postgram_6475589f/readme"
}