Postgram

io.github.ivo-toby由 GitHub 帳號 ivo-toby 發布v0.1.3更新於 Oct 24, 2026

Self-hosted memory for AI agents with hybrid search, knowledge graphs, tasks, and document sync.

概覽

AI 產生的概覽

供 AI 代理使用的自架記憶伺服器,可儲存結構化知識、任務與文件,並提供混合檢索與知識圖譜。

可以做什麼
  • 儲存、更新與刪除帶標籤、可見度與 JSON 中繼資料的結構化知識物件(記憶、人物、專案、任務、互動、文件)
  • 以向量加關鍵字的混合排序、相似度門檻與可選的圖譜擴展來檢索已存知識
  • 以具型別的定向邊連結或解除連結實體,並走訪由此形成的知識圖譜
  • 建立、列出、更新與完成任務,支援 GTD 情境與到期日
  • 把本機 markdown 目錄同步為文件實體,並透過試跑或封存工具整理工作階段脈絡
適用情境
適合希望助理把長期記憶與工作階段脈絡保存在自己掌控的基礎設施、而非廠商雲端的場合。也適合需要把任務、筆記與同步的 markdown 文件放在同一個可查詢儲存區,並透過 MCP、REST 或命令列存取的團隊。
執行需求
透過 Docker 與 Docker Compose 在本機執行(僅命令列或本機開發需要 Node.js 22+)。需要啟用 pgvector 擴充功能的 PostgreSQL,透過 DATABASE_URL 或 POSTGRES_* 設定連線,並需要可連線的 OpenAI 或 Ollama 嵌入服務。管理員初始化需從容器日誌讀取一次性引導權杖、完成 MFA 註冊,並建立以 Authorization bearer 權杖傳送的 API 金鑰。選用金鑰:OPENAI_API_KEY、ANTHROPIC_API_KEY、OLLAMA_BASE_URL。
安裝前請注意
涉及敏感密鑰:ADMIN_MFA_SECRET_KEY 與 ADMIN_SETTINGS_ENCRYPTION_KEY 必須與資料庫備份分開保存,遺失後 TOTP 登入與已存供應商密鑰的解密都會失效。API 金鑰可按 read、write、delete、sync 範圍限制,應只授予所需權限。寫入、封存、整理與文件同步都會改動已存資料,管理員整理並啟用提升時會呼叫大型語言模型。不要把回送埠暴露到公網,遠端用戶端應經 HTTPS 並啟用 OAuth。

安裝

在 SourceWeft 中

  1. 開啟 儀表板中的 Postgram,將其新增到工作區。
  2. 為需要使用其工具的對話啟用該服務。

Desktop only,透過 STDIO。 STDIO 服務會啟動本機處理程序,因此需要 SourceWeft 桌面主機。

其他 MCP 客戶端

參照 儲存庫 中的啟動說明。

README

[Postgram]

Postgram

A self-hosted productivity and knowledge backend for humans and AI agents.

Website · Quick start · MCP guide · REST API · Demo

Postgram keeps the data you and your agents work from in one inspectable place: notes, documents, tasks, people, projects, interactions, decisions, and agent memory. Humans use the browser UI and CLI; agents use the same corpus over MCP, REST, or the CLI.

It is more than an agent-memory layer. Postgram preserves typed source objects, supports GTD-style task management and Markdown folder sync, combines full-text and vector retrieval with a knowledge graph, and separates short-lived agent working context from durable memory.

[Watch the Postgram demo video]
Watch the demo
[Postgram search interface showing ranked knowledge results]
Search across memories, documents, people, projects, and tasks

Why Postgram

  • One private corpus across tools. Give different agents and devices access to the same data without tying it to one editor or hosted memory provider.
  • Inspectable source data. Store typed entities instead of opaque chat summaries, then search, edit, link, archive, or delete them yourself.
  • Search before graph expansion. Hybrid retrieval finds relevant entities; edge summaries tell an agent when related graph context is worth following.
  • Working context is not durable memory. Session context has its own scope and lifecycle; grooming can archive it or distill selected context into durable memory.
  • Operator control. Choose where PostgreSQL runs, which embedding and extraction providers are allowed, who receives API keys, and what is kept.

Postgram is built for one person or a small trusted team running a local or single-VM deployment. It is not a hosted service or a multi-tenant SaaS platform. Knowledge extraction is optional, and the provided Docker Compose setup binds the raw API and UI ports to loopback by default.

Quick Start (Docker Compose)

You need Git, Docker, and Docker Compose. Node.js 22+ is needed only for local development or for installing the pgm CLI; gpg is needed only for encrypted CLI backups.

  1. Clone Postgram:

    bash
    git clone https://github.com/ivo-toby/postgram.gitcd postgram
  2. Choose an embedding path before the first start. For the local default, install and start Ollama on the Docker host, then pull Postgram's default embedding model:

    bash
    ollama pull bge-m3

    For hosted OpenAI embeddings instead, create a .env file containing a real key before starting Compose:

    dotenv
    OPENAI_API_KEY=<your-openai-key>
  3. Start the stack:

    bash
    docker compose up -d --build

    The first run creates persistent Docker volumes for PostgreSQL and installation secrets. No .env file is required for the default Compose path.

  4. Read the one-time bootstrap token:

    bash
    docker compose logs mcp-server \  | grep 'Bootstrap token:' \  | tail -n 1

    The plaintext appears only in the original first-start logs. Capture it before recreating the API container or discarding those logs.

  5. Open http://127.0.0.1:3000/admin, paste the token, create the first admin, enroll MFA, and follow the onboarding flow.

  6. Confirm the selected provider in the Admin Config tab. If you add or change staged settings, save, validate, and apply them, then restart mcp-server when Admin marks a restart as required:

    bash
    docker compose restart mcp-server

    When Ollama runs on the Docker host, its base URL is http://host.docker.internal:11434. Optional LLM relationship extraction is disabled by default and can use OpenAI, Anthropic, Ollama, or an OpenAI-compatible endpoint. Changing the embedding provider, model, or dimensions after the first start is migration work and is blocked from a simple config apply.

  7. Check health, then create an API key in the Admin Overview tab. For the smoke test below, allow read and write, the memory entity type, and personal visibility:

    bash
    curl -fsS http://127.0.0.1:3100/health

    The response should include "status":"ok" and "postgres":"connected".

  8. Install the CLI and verify an authenticated write and search. Enrichment is asynchronous, so wait for pgm queue to report no pending work before the search:

    bash
    npm install -g @ivotoby/postgram-cliexport PGM_API_URL=http://127.0.0.1:3100export PGM_API_KEY='<plaintext-api-key>'
    pgm store "Postgram quick start is working" \  --type memory \  --visibility personal \  --tags quickstartpgm queuepgm search "quick start"

If embeddings are unreachable, Postgram still starts and accepts writes, but enrichment and search will fail until the provider is available. See the full quick start and troubleshooting guide for the longer path.

For access from ChatGPT, Claude, or another remote MCP client, put Postgram behind HTTPS, enable OAuth, and follow the MCP integration guide. Do not publish the loopback development ports directly to the internet.

What It Does

Postgram provides:

  • durable storage for typed entities: memory, person, project, task, interaction, document
  • hybrid keyword + vector search with asynchronous enrichment
  • knowledge graph with typed directional edges between entities
  • LLM-powered relationship extraction (OpenAI, Anthropic, or Ollama)
  • document sync from local markdown repos via manifest comparison
  • browser interfaces for knowledge work and guarded administration
  • UMAP and PCA projections of embedded entities
  • GTD-style capture, task organization, and Kanban views
  • scoped API-key authentication and visibility restrictions
  • a REST API for application and automation access
  • a Streamable HTTP MCP endpoint for agent-native tool access
  • a CLI (pgm) for humans and agents
  • a container-local admin CLI (pgm-admin)
  • Talon SQLite migration tooling
  • encrypted backup support
  • audit logging for mutating and privileged operations

How It Works

Postgram is a TypeScript Node.js application built around a service layer.

Main components:

  • PostgreSQL + pgvector for persistence and vector search
  • Hono for the HTTP server
  • MCP over Streamable HTTP for agent-facing tool access
  • CLI/admin CLIs built with Commander
  • background enrichment worker for chunking, embeddings, and LLM extraction

High-level flow:

  1. a client stores or updates an entity
  2. the entity is written immediately
  3. enrichment runs asynchronously: chunking, embedding, and optionally LLM extraction
  4. chunks and embeddings are produced in the background
  5. edges are created from extracted relationships (if extraction is enabled)
  6. search queries use hybrid keyword + vector retrieval and reciprocal-rank fusion, with optional graph expansion

Main Features

1. Typed Knowledge Storage

Store structured knowledge objects with:

  • type (memory, person, project, task, interaction, document)
  • content
  • tags
  • visibility (personal, work, shared)
  • status
  • arbitrary JSON metadata

Memory Roles

Postgram supports two roles for memory entities:

  • durable_memory: long-term memory future agents should trust, such as decisions, preferences, constraints, root causes, and completed-work summaries.
  • 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.

Use session context for "where were we in this thread?" Use durable memory for "what should future agents remember as true?"

CLI users can write session context with pgm memory session-context and search it with pgm search --memory-role session_context.

Operators can groom stale session context with pgm-admin memory groom. Use --client-id <client-id> for one client or --all-clients to batch over every session-context scope. --all-clients keeps each client scope separate; it is operational batching, not cross-client consolidation. --older-than <duration> defaults to 7d and accepts values like 30m, 4h, 7d, or 0d. --dry-run previews eligible memories without calling the LLM. Grooming has no default candidate cap; pass --limit <n> when you want to process a bounded batch. --mode archive --yes archives eligible working context directly. --mode promote --yes uses the configured extraction LLM to decide whether each session-context memory should be promoted; promoted memories are distilled into new durable_memory entities, the source context is archived, and provenance is recorded with metadata.promoted_to plus a promoted_to edge.

Authenticated users and agents can self-groom only their own client-scoped session context:

bash
pgm memory groom --dry-run --older-than 7dpgm memory groom --older-than 14d --topic postgram --tag session-context --yes

The normal CLI derives scope from PGM_API_KEY; it does not accept --client-id, --all-clients, or promotion mode. Archive requires --yes. Optional filters are --topic, --session-id, and repeatable --tag.

MCP clients can use the groom_session_context tool with the same self scope:

json
{  "mode": "dry_run",  "older_than": "7d",  "topic": "postgram",  "session_id": "optional-session-id",  "tags": ["session-context"]}

MCP mode is dry_run or archive; promotion remains admin-only.

For scheduled maintenance, run grooming from the host that has access to the Postgram container. This cron example assesses eligible session context for all client scopes every three days at 03:17 and appends JSON output to a log. The wrapper detects that cron does not provide a TTY and runs non-interactively:

cron
17 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

Use --mode archive --yes instead if you want to archive eligible working context without LLM-assisted promotion. Run the same command with --dry-run first to verify the eligible set.

Operators can also review durable memory quality without mutating the durable claim itself:

bash
./bin/pgm-admin memory groom-durable --dry-run --older-than 30d./bin/pgm-admin memory groom-durable --mode mark --yes --older-than 30d

Durable grooming selects active durable_memory rows, including legacy memory rows with no metadata.memory_role, and classifies them as keep, needs_grooming, archive, or superseded. Mark mode writes metadata.durable_grooming with the outcome, reason, review timestamp, and any LLM suggestions. It does not rewrite content, change status, archive rows, or merge duplicates.

To actually clean the marked rows, apply the grooming labels:

bash
./bin/pgm-admin memory apply-durable-grooming --dry-run./bin/pgm-admin memory apply-durable-grooming --yes

Apply mode defaults to auto: needs_grooming memories are rewritten from the stored suggestion or the configured extraction LLM, while archive and superseded memories are archived. Rewrites clear stale chunks and re-queue embedding enrichment. Use --mode rewrite or --mode archive, plus --status, --topic, --tag, --visibility, or --limit, to narrow the batch.

2. Async Enrichment

Entities with content are persisted first and enriched later. Each entity tracks enrichment_status: pending, completed, or failed. Failed entities are retried up to 3 times with a 5-minute backoff.

3. Hybrid Search

Search retrieves vector neighbors and chunks matching query lexemes, then deduplicates chunks and ranks them with reciprocal-rank fusion (RRF): 1 / (60 + vector_rank) + 1 / (60 + keyword_rank). A missing branch contributes zero. Keyword retrieval splits a query into whitespace-separated terms. Each term's simple lexemes are ANDed, while non-empty terms are ORed, preserving compound identifiers such as ABC-1234; a long query can therefore admit a chunk matching one complete term. For multi-term queries, terms whose English dictionary vector is empty (standalone English stopwords) are omitted; a single term is retained even when that vector is empty. Matching and ranking still use PostgreSQL's simple configuration. Broad searches select candidates through the HNSW index; small filtered sets use exact distance ranking, and HNSW falls back to exact ranking when its candidate scan cannot fill its target. Keyword parsing retains the first 24 distinct non-empty term queries after multi-term English stopword filtering, while embeddings use the full original query text.

Keyword candidate generation starts with a full conjunction probe over all retained terms, capped at 200 authorized GIN hits. If it fills the cap, it is the keyword pool and all other probes are skipped; a single retained term reuses this same probe. Otherwise, an authorized OR probe capped at 201 checks whether the complete union fits the 200-chunk pool. A union of at most 200 chunks is used in full without per-term scans; a 201-hit union falls back to independent per-term probes and the existing rarity/compound selection for the 200-chunk pool. Full-conjunction hits stay first in that fallback. Only the full AND query deduplicates lexemes; lexical OR scoring remains unchanged. When a capped probe has more authorized hits than its cap, its unordered membership may vary. ts_rank_cd and keyword ranks are therefore approximate within this bounded pool, just as vector ranks are bounded by their candidate retrieval.

If the full conjunction does not fill the pool and the OR probe saturates, a qualifying partial match such as five of six common terms can still fall outside the bounded probe membership. This is an accepted current limitation; evaluation may motivate later intersection probes.

threshold is a cosine similarity floor from 0 to 1. A candidate passes when its vector similarity meets the floor or its chunk matches at least half of the retained distinct keyword terms, rounded up. A single-term query, including a compound identifier such as ABC-1234, requires its one retained term. Partial keyword candidates remain eligible when they meet the vector floor or this coverage rule. The returned score is the relative RRF ordering value, not a probability or percentage; similarity remains the raw cosine value. An optional recency multiplier uses a true 30-day half-life. A candidate ranked first in both branches scores about 0.03279 before recency. Equal final scores prefer the lower keyword rank, then vector rank, then stable identifiers. Search requires a reachable embedding provider; if that provider is unavailable, writes still succeed but enrichment and search fail until it recovers. Results include:

  • ranked results with relative RRF scores
  • cosine similarity values
  • recency-adjusted RRF scores
  • matching chunk text
  • optional 1-hop graph neighbors (expand_graph parameter)

Migration 017 adds an expression GIN index on to_tsvector('simple', chunks.content). The normal transactional CREATE INDEX keeps reads available while migration runs, but writes to chunks wait until the migration commits. It does not rewrite the table heap. The migration runner cannot use CREATE INDEX CONCURRENTLY, so schedule startup accordingly.

[Hybrid search results in Postgram]

4. Knowledge Graph

Entities can be connected by typed directional edges:

  • relation types: involves, assigned_to, part_of, blocked_by, mentioned_in, related_to, or any custom type
  • edges have a confidence score (1.0 for manual, LLM-assigned for extracted)
  • graph traversal via expand with configurable depth (1-3 hops)
  • duplicate edge prevention via UNIQUE(source_id, target_id, relation)
  • edges are created manually via link/unlink or automatically by the LLM extraction pipeline

[Postgram knowledge graph view]

5. LLM Extraction

When enabled, the enrichment worker extracts relationships from entity content using an LLM. Extracted entity names are matched against existing entities and edges are created automatically.

Supported providers:

ProviderModel defaultEnv vars required
OpenAIgpt-4o-miniOPENAI_API_KEY
Anthropicclaude-haiku-4-5-20251001ANTHROPIC_API_KEY
Ollamallama3.2OLLAMA_BASE_URL (default: http://localhost:11434)

These are configuration defaults, not model-quality recommendations. Graph extraction is a constrained structured-output task; validate the resulting edges on your own corpus before running a large backfill, especially with small local models.

[Postgram embedding projection view]

6. Document Sync

Sync local directories of markdown files into postgram:

bash
pgm sync ~/Documents/personal-notespgm sync ~/Documents/cf-notes --repo cf-notes --quiet

The CLI walks the directory for .md files, computes SHA-256 hashes, and sends a full manifest to the server. The server diffs against stored state and creates, updates, or archives document entities. Supports --dry-run and cron scheduling.

7. Access Control

API keys can be restricted by:

  • scopes: read, write, delete, sync
  • allowed entity types
  • allowed visibility levels

8. Task Management

Tasks are first-class entities with convenience operations for:

  • create (with GTD context and due dates)
  • list (filtered by status and context)
  • update
  • complete (with completion timestamp)

9. Multiple Interfaces

The same service layer is exposed through:

  • REST API
  • Streamable HTTP MCP endpoint
  • pgm CLI
  • pgm-admin CLI (./bin/pgm-admin)
  • Browser extensions for Chrome and Firefox — one-click web clipper that captures the current page or text selection via the REST API. Build with npm run -w @ivotoby/postgram-browser-extension-chrome package (or the Firefox equivalent); install unpacked from the per-package README.

Repository Layout

text
src/  auth/            API key validation and auth middleware  cli/             CLI for humans/agents and admin CLI  db/              Pool and migrations  migrate-talon/   Talon import path  services/        Business logic (entities, search, edges, sync, extraction)  transport/       REST and MCP adapters  types/           Shared types  util/            Errors, audit, logging
ui/                User-facing web UI and Admin UIcli/               Published @ivotoby/postgram-cli packagedocker/            Container entrypoint and secret bootstrap scriptsbin/               Local operator wrappers
packages/  browser-extension-chrome/   Chromium web clipper (MV3)  browser-extension-firefox/  Firefox web clipper (MV3)
tests/  contract/        REST and MCP contract tests  integration/     Service and CLI integration tests  unit/            Pure logic tests

Requirements

  • Docker and Docker Compose for the recommended deployment
  • Node.js 22+ for the pgm CLI or local development
  • a reachable OpenAI or Ollama embedding provider for enrichment and search

Optional:

  • OpenAI API key (for OpenAI embeddings or extraction)
  • Anthropic API key (for LLM extraction)
  • Ollama (for local embeddings or LLM extraction)
  • gpg (for encrypted CLI backups)

Docker Setup Details

1. Start Docker Compose

bash
docker compose up -d --build

The default Compose path does not require manual .env edits. On first run it creates a persistent postgram_secrets Docker volume containing:

  • the Postgres password used by the app container
  • ADMIN_MFA_SECRET_KEY for encrypted admin TOTP seeds
  • ADMIN_SETTINGS_ENCRYPTION_KEY for DB-backed provider secrets

If an existing Docker install already has POSTGRES_PASSWORD in .env, the first start after this change copies that legacy password into postgram_secrets/postgres-password instead of generating a different database password. Keep the old .env value in place for that first upgraded start.

The API binds to 127.0.0.1:3100 and the UI binds to 127.0.0.1:3000 by default. Use POSTGRAM_API_PORT=<port> or UI_PORT=<port> as shell overrides when running more than one local stack.

To use an existing Postgres cluster with Compose, set POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DB, and POSTGRES_USER on mcp-server in a Compose override and remove the postgres dependency, as in the operator examples. If that external cluster requires password auth, set POSTGRES_PASSWORD in .env; if it uses passwordless local auth, leave POSTGRES_PASSWORD= blank. You can also bypass the split settings entirely by setting DATABASE_URL.

For embeddings, Compose preserves the OpenAI default when OPENAI_API_KEY is present. If no OpenAI key and no explicit EMBEDDING_PROVIDER are supplied, the container entrypoint chooses local Ollama embeddings so a clean stack can boot before provider secrets are configured.

2. Complete first admin setup

On first start, the API container prints a clear one-time bootstrap banner with the token:

text
Postgram first admin setupBootstrap token: ...Open http://127.0.0.1:3000/admin and paste this token.

If the console has scrolled, read the same one-time bootstrap token from the trusted local operator channel:

bash
docker compose logs mcp-server | grep 'Bootstrap token:' | tail -n 1

Then open http://127.0.0.1:3000/admin, create the first admin user, and complete MFA enrollment. The bootstrap token is stored hash-only in Postgres, expires after 24 hours, and is invalidated after the first admin is created. If you changed the Postgres target, copy the latest bootstrap-token log line; older lines may belong to a previous database and will be rejected.

After active MFA login, the Admin dashboard opens a guided onboarding flow until it is completed or deliberately skipped. The guide explains the setup path in plain operator language:

  • what bootstrap, admin login, and MFA confirmation protect
  • how provider settings, embedding dimensions, extraction models, and write-only provider secrets fit together
  • when to validate and apply saved provider configuration
  • why backup/restore is staged before switch-over
  • how maintenance dry-runs, re-extraction, re-embedding, and edge pruning work

Onboarding progress is stored server-side in Postgres. Refreshing the browser, closing the tab, logging out and back in, or restarting the Docker containers resumes at the latest saved step as long as the existing pgdata volume is preserved. The Onboarding tab remains available from the dashboard after skip or completion.

For local Docker testing, preserve the database volume:

bash
docker compose up -d --builddocker compose restart mcp-server postgram-ui

Do not use docker compose down -v when testing onboarding resume behavior. That command removes named volumes, including the pgdata Postgres volume, and will reset the server-side onboarding state along with the database.

3. Check health

bash
curl http://127.0.0.1:3100/health

Expected:

  • status: "ok"
  • postgres: "connected"

4. Configure providers and create API keys

Use the Admin dashboard in the browser for the supported happy path:

  • Onboarding tab: resume, skip, or complete the Docker-first setup guide.
  • Config tab: save provider settings and write-only provider secrets.
  • Overview tab: create Postgram API keys, inspect health, queue, stats, config/model/job status, and audit rows.
  • Maintenance tab: run safe dry-run previews and poll job status before any destructive apply.
  • Backup tab: download a gzipped v2 archive containing a data-only PostgreSQL custom dump plus redacted runtime configuration. Restore is intentionally staged: the server rejects legacy v1/full-schema archives, accepts only approved Postgram table-data entries from pg_restore --list, creates the trusted schema from bundled migrations, and restores the accepted data into a new database name. Health checks run before operator-approved switch-over. If the restored database misbehaves, roll back by restoring the previous POSTGRES_DB or DATABASE_URL setting and restarting mcp-server/postgram-ui; the old database is left untouched for this emergency path.

Normal Docker setup and maintenance should not require pgm-admin after startup/bootstrap. The pgm-admin CLI remains documented below for emergency recovery, embedding migrations, raw SQL inspection, and advanced operator jobs.

Docker Secret Backup And Failure Behavior

Back up the postgram_secrets Docker volume separately from database backups. Database backups contain encrypted provider secrets and encrypted TOTP factors; they do not contain the installation keys needed to decrypt them.

Losing or replacing ADMIN_MFA_SECRET_KEY prevents existing TOTP factors from being verified. Losing or replacing ADMIN_SETTINGS_ENCRYPTION_KEY prevents stored provider secrets from being decrypted. With the wrong settings key, provider config reads remain redacted, provider apply/runtime secret use fails closed, and operators must restore the original key or re-save provider secrets after a deliberate rotation/recovery procedure.

For Docker Compose, missing secret files are generated only on an empty postgram_secrets volume. Invalid persisted secret files fail container startup before the server binds. Optional env overrides still work, but keep those values outside database backups and browser storage.

Environment Variables

Server

VariableRequiredDefaultDescription
DATABASE_URLnon-ComposeDocker secret file + Postgres envFull Postgres connection string. Compose constructs it from the generated Postgres password secret when unset.
POSTGRES_HOSTnopostgresCompose Postgres host used when DATABASE_URL is unset. Override to host.docker.internal or another hostname for an existing cluster.
POSTGRES_PORTno5432Compose Postgres port used when DATABASE_URL is unset.
POSTGRES_DBnopostgramCompose Postgres database used when DATABASE_URL is unset.
POSTGRES_USERnopostgramCompose Postgres user used when DATABASE_URL is unset.
POSTGRES_PASSWORDnoDocker secret fileCompose Postgres password used when DATABASE_URL is unset. For external hosts, an explicit blank value builds a passwordless URL.
ADMIN_MFA_SECRET_KEYadmin setupDocker secret fileStable 32+ character secret used to encrypt admin TOTP seeds. Compose generates and persists it in postgram_secrets when unset.
OPENAI_API_KEYconditionalRequired when EMBEDDING_PROVIDER=openai OR (EXTRACTION_ENABLED=true AND EXTRACTION_PROVIDER=openai). Optional otherwise.
ADMIN_SETTINGS_ENCRYPTION_KEYwhen saving admin-managed secretsDocker secret file32-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.
PORTno3100HTTP/MCP server port
POSTGRAM_API_PORTno3100Docker Compose host port for the API/backend. The container listen port stays 3100.
UI_PORTno3000Docker Compose host port for the UI.
OAUTH_ENABLEDnofalseEnable OAuth authorization-code, PKCE, and Dynamic Client Registration routes for native remote MCP connectors.
PUBLIC_BASE_URLconditionalPublic HTTPS origin for OAuth metadata and callback URLs. Required when OAUTH_ENABLED=true. Example: https://postgram.example.com.
LOG_LEVELnoinfopino log level
ENRICHMENT_POLL_INTERVAL_MSno1000Enrichment worker poll interval

Embeddings

VariableRequiredDefaultDescription
EMBEDDING_PROVIDERnoopenai (Compose auto-selects)openai, ollama, or openai-compatible. Compose keeps OpenAI when OPENAI_API_KEY is present, otherwise chooses Ollama unless explicitly set. openai-compatible calls any OpenAI-style /embeddings API, such as Mistral, and never sends a dimensions field.
EMBEDDING_MODELwhen provider=openai-compatibleper-providerDefaults: text-embedding-3-small (openai, 1536 dims), bge-m3 (ollama, 1024 dims). No default for openai-compatible.
EMBEDDING_DIMENSIONSwhen provider=openai-compatibleper-providerMust match the active embedding_models row. Run ./bin/pgm-admin embeddings migrate --target-dimensions <N> --yes to change.
EMBEDDING_BASE_URLwhen provider=ollama or openai-compatibleollama: falls back to OLLAMA_BASE_URLEmbedding host. For openai-compatible, include any /v1 path; Postgram appends /embeddings (Mistral: https://api.mistral.ai/v1). For Ollama it is independent from the LLM-extraction host so embeddings and inference can target different machines.
EMBEDDING_API_KEYnoOptional bearer token for EMBEDDING_BASE_URL.
EMBEDDING_TIMEOUT_MSno15000Hard timeout for a single embedding provider call. Bounds how long one stalled call can delay a request.
QUERY_EMBEDDING_CACHE_SIZEno512In-process query embeddings held in front of the Postgres-backed cache.
QUERY_EMBEDDING_CACHE_SECRETnoKeys 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.
QUERY_EMBEDDING_CACHE_RETENTION_DAYSno30Age at which persisted query embeddings are pruned. The hourly prune also retains only the 2,000 newest entries per client.
SEARCH_DEFAULT_THRESHOLDno0.35Cosine-similarity floor for searches that don't pass threshold. The right value depends on the embedding model: text-embedding-3-small puts most unrelated text below 0.35, while mistral-embed scores unrelated text around 0.7, where 0.35 filters nothing. Retune it, and EXTRACTION_MATCH_MIN_SIMILARITY, when you switch models.

When 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.

See specs/002-local-embeddings/quickstart.md for a walkthrough of fresh-install-on-Ollama and migrating from OpenAI.

LLM Extraction

VariableRequiredDefaultDescription
EXTRACTION_ENABLEDnofalseEnable LLM relationship extraction
EXTRACTION_MEMORY_MODEnoembed_onlyControls 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.
EXTRACTION_PROVIDERnoopenaiLLM provider: openai, anthropic, ollama, or openai-compatible
EXTRACTION_MODELnoper-providerModel name (defaults: gpt-4o-mini for OpenAI, claude-haiku-4-5-20251001 for Anthropic, llama3.2 for Ollama, gpt-4o-mini for OpenAI-compatible)
EXTRACTION_BASE_URLwhen provider=openai-compatibleBase URL for OpenAI-compatible chat-completions APIs, including any /v1 path. Postgram appends /chat/completions. Example: http://host.docker.internal:8000/v1.
EXTRACTION_API_KEYnoOptional bearer token for EXTRACTION_BASE_URL.
EXTRACTION_AUTO_CREATE_ENTITIESnofalseWhen 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.
EXTRACTION_AUTO_CREATE_TYPESnoperson,project,interactionComma-separated list of entity types eligible for auto-creation. document, task, memory are intentionally excluded from the default to keep those user-authored.
EXTRACTION_AUTO_CREATE_MIN_CONFIDENCEno0.7Minimum per-extraction confidence (0–1) required to auto-create an entity. Raise to cut noise, lower for a denser graph.
ANTHROPIC_API_KEYwhen provider=anthropicAnthropic API key
OLLAMA_BASE_URLnohttp://localhost:11434Ollama server URL
EXTRACTION_REASONING_EFFORTnounsetminimal | low | medium | high. Forwarded as reasoning_effort to OpenAI and Ollama for reasoning models (o-series, gpt-5, gpt-oss), and to openai-compatible endpoints for any model when set explicitly. When set, overrides the implicit minimal that EXTRACTION_DISABLE_THINKING=true sends to OpenAI. Mistral accepts only low and high from this list.
LLM_REQUEST_TIMEOUT_MSno120000Hard cap per LLM call in milliseconds. Bump this when running slow local models (e.g. gpt-oss:120b-cloud).
EXTRACTION_SEMANTIC_NEIGHBORS_ENABLEDnofalseEnable semantic neighbor linking (see below).
EXTRACTION_SEMANTIC_NEIGHBORS_MAXno10Maximum number of neighbor edges to create per entity.
EXTRACTION_MATCH_MIN_SIMILARITYno0.5Minimum cosine similarity (0–1) for the semantic fallback that matches an extracted target name to an existing entity. Like the other similarity floors, its useful range depends on the embedding model.
EXTRACTION_SEMANTIC_NEIGHBORS_MIN_SIMILARITYno0.65Minimum 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.

Semantic neighbor linking: the LLM extraction pass only finds entities that are explicitly named in the source content. It misses entities that are thematically related but not cited by name — a weekly kickoff meeting about the same initiative, a wiki page covering the same strategy, a decision memo about the same project. When EXTRACTION_SEMANTIC_NEIGHBORS_ENABLED=true, a second pass runs after LLM extraction that queries the knowledge store for entities whose stored chunk embeddings are cosine-similar to the source entity's own embeddings, and links them with related_to. No extra LLM or embedding API calls are needed — the source entity's chunks are already stored by the enrichment step that runs before extraction. Edges created by this pass carry source = 'semantic-neighbor' so they are distinguishable from LLM-extracted edges. Entities already linked by the LLM pass are excluded to avoid a weaker related_to edge shadowing a stronger-typed edge for the same pair.

Backfilling and maintaining neighbor edges: the ./bin/pgm-admin link-neighbors command runs the semantic neighbor pass directly — no LLM calls, no extraction queue, just cosine similarity over stored chunks. Use it to backfill an existing graph or as a recurring maintenance job after new entities are added.

bash
# Backfill all enriched entities (safe to re-run — edges are upserted)../bin/pgm-admin link-neighbors --all
# Only documents:./bin/pgm-admin link-neighbors --type document
# Single entity:./bin/pgm-admin link-neighbors --id <uuid>
# Preview what would be linked and at what similarity — no edges created:./bin/pgm-admin link-neighbors --id <uuid> --dry-run./bin/pgm-admin link-neighbors --all --dry-run
# Tune the similarity threshold or edge cap:./bin/pgm-admin link-neighbors --all --min-similarity 0.75 --max-neighbors 5
# Process in bounded batches (oldest-first):./bin/pgm-admin link-neighbors --all --limit 500

Use --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.

If you also want to re-run LLM extraction at the same time (e.g. after enabling EXTRACTION_SEMANTIC_NEIGHBORS_ENABLED=true), use reextract instead — the worker runs both the LLM pass and the neighbor pass together:

bash
./bin/pgm-admin reextract --all

Note: --clean-edges on reextract only removes edges with source='llm-extraction' — it does not touch semantic-neighbor edges. For a full clean slate:

sql
DELETE FROM edges WHERE source = 'semantic-neighbor';

Scheduling as a recurring maintenance job: because link-neighbors is cheap (no LLM calls) and idempotent (edges are upserted, not duplicated), it works well as a weekly cron job that keeps the neighbor graph fresh as new entities are added. Example cron entry running every Sunday at 02:00:

cron
0 2 * * 0 cd /path/to/postgram && ./bin/pgm-admin link-neighbors --all

Or with Docker Compose:

bash
./bin/pgm-admin link-neighbors --all

Auto-created entities: when EXTRACTION_AUTO_CREATE_ENTITIES=true, entities that didn't exist before a document mentioned them are inserted with content = the extracted name, tags including auto-created, and metadata.auto_created_by = 'llm-extraction' plus metadata.source_entity_id pointing at the document that caused the creation. They enter the normal embedding queue so they become searchable, but they are deliberately excluded from the extraction queue — their only content is a bare name, so asking the LLM "what does Alice relate to?" with no context would just free-associate new stubs in a loop. To review or clean them up:

bash
pgm list --tags auto-created --type person# or wholesale prune:docker compose exec postgres psql -U postgram -d postgram -c \  "DELETE FROM entities WHERE 'auto-created' = ANY(tags);"

CLI

VariableRequiredDescription
PGM_API_URLyesServer URL
PGM_API_KEYyesAPI key for authentication

Admin CLI

VariableRequiredDescription
DATABASE_URLyesDirect DB connection for admin operations

Backup

VariableRequiredDescription
DATABASE_URL or PGM_DATABASE_URLyesDatabase connection
PGM_BACKUP_PASSPHRASEwhen using --encryptGPG encryption passphrase

Running The Server

Pre-built Docker image

Pull from GitHub Container Registry:

bash
docker pull ghcr.io/ivo-toby/postgram:latest

Images are multi-arch (linux/amd64, linux/arm64). Tags available:

  • latest — most recent build of main
  • main — same as latest, explicit branch name
  • sha-<short> — pinned to a specific commit

The docker-compose.yml in this repo builds locally by default; to use the pre-built image instead, replace build: . with image: ghcr.io/ivo-toby/postgram:latest for the mcp-server service.

Local development

bash
npm run dev

Production-style local run:

bash
npm run buildnpm start

The server exposes:

  • REST API at http://127.0.0.1:3100/api
  • MCP endpoint at http://127.0.0.1:3100/mcp
  • Health endpoint at http://127.0.0.1:3100/health

Authentication

Create an API key from the Admin dashboard at http://127.0.0.1:3000/admin. The plaintext key is displayed once in the browser and cannot be recovered after dismissal or reload.

Export it for CLI use:

bash
export PGM_API_URL=http://127.0.0.1:3100export PGM_API_KEY='<plaintext-key>'

REST API Overview

Entity endpoints

  • POST /api/entities — store entity
  • GET /api/entities/:id — recall entity
  • PATCH /api/entities/:id — update entity
  • DELETE /api/entities/:id — soft-delete entity
  • GET /api/entities — list entities

Search

  • POST /api/search — hybrid keyword+vector search with RRF scores (supports expand_graph and include_content)

REST search keeps full entity content by default for backwards compatibility. Pass include_content: false to return matched chunks without hydrating or serializing full result and graph-neighbor content. Other REST routes continue to return their existing full JSON responses. Compact JSON and TOON remain transport-layer conveniences for MCP and the CLI.

Tasks

  • POST /api/tasks — create task
  • GET /api/tasks — list tasks
  • PATCH /api/tasks/:id — update task
  • POST /api/tasks/:id/complete — complete task

Document sync

  • POST /api/sync/diff — diff local manifest against server; returns paths to upload and delete
  • POST /api/sync/upload — upload a batch of file contents
  • POST /api/sync/finalize — archive orphans and restore stale matches
  • POST /api/sync — single-shot push (retained for MCP and small syncs)
  • GET /api/sync/status/:repo — get sync status

pgm sync uses the three-phase protocol (diff → batched upload → finalize) so large repos don't send a single oversized payload. Each upload batch is capped at ~50 files or ~4 MB, whichever comes first.

Knowledge graph

  • POST /api/edges — create edge
  • DELETE /api/edges/:id — delete edge
  • GET /api/entities/:id/edges — list edges for entity
  • GET /api/entities/:id/graph — expand graph neighborhood

Queue / enrichment

  • GET /api/queue — enrichment + extraction queue status. Pass ?include_failures=true (optionally &failure_limit=N, default 20, max 100) to also receive the most recent failed entities with their error messages, e.g.:

    json
    {  "embedding": {    "pending": 0,    "completed": 120,    "failed": 0,    "retry_eligible": 0,    "oldest_pending_secs": null  },  "extraction": { "pending": 2, "completed": 98, "failed": 3 },  "failures": [    {      "id": "…",      "type": "document",      "kind": "extraction",      "error": "llm context exceeded",      "path": "notes/long.md",      "updatedAt": "2026-04-22T10:12:33Z"    }  ]}

All /api/* routes require Authorization: Bearer <api-key>.

MCP Overview

MCP is served over Streamable HTTP at:

text
http://127.0.0.1:3100/mcp

Exposed tools:

  • store, recall, search, update, delete, queue
  • store_session_context, groom_session_context
  • task_create, task_list, task_update, task_complete
  • sync_push, sync_status
  • link, unlink, expand

The MCP tool behavior is intentionally aligned with the REST surface, but token-heavy outputs default to compact agent-friendly responses:

  • write acknowledgements (store, store_session_context, update, task writes, link) return compact ids/status/version instead of echoing full metadata and timestamps
  • search, task_list, and expand return compact rows/graph payloads by default; compact search contains the matched chunk rather than full result or neighbor content and may include edges.count and edges.relations as cheap traversal affordances
  • pass full_response: true to get the full REST-shaped payload, including complete entity content
  • pass toon: true on list-like tools (search, task_list, expand) to receive compact TOON text from the MCP layer

Search is the discovery step: inspect compact IDs, relative RRF scores, and matched chunks, then call recall only for the selected entities whose complete content is needed. Search scores are numeric ordering values, not percentages; cosine similarity is available in the full response when a caller needs the vector signal. Compact edges summaries contain counts and relation labels only. They do not include neighbor content. Use expand_graph or expand when the user needs causes, provenance, decisions, dependencies, blockers, ownership, involvement, discussion participants, connected context, or graph-based disambiguation. Avoid expansion for direct facts already present in the matched chunk.

The underlying API remains JSON; compacting and TOON happen only in MCP/CLI handlers.

Native Remote Connectors (ChatGPT and Claude)

Local MCP clients can connect with a static bearer API key. ChatGPT accounts with custom-connector/developer-mode access and Claude's Connectors UI can connect to a public Postgram endpoint through OAuth, without storing a static API-key header in the client settings:

bash
OAUTH_ENABLED=truePUBLIC_BASE_URL=https://postgram.example.com

Add ${PUBLIC_BASE_URL}/mcp as the connector URL in ChatGPT or Claude. The client discovers /.well-known/oauth-protected-resource/mcp, registers through /oauth/register, opens /oauth/authorize, and receives OAuth tokens from /oauth/token. The endpoint must be reachable over public HTTPS.

The authorize page asks for an existing Postgram API key once. Tokens issued from that approval inherit the API key's scopes, client_id, allowed entity types, and allowed visibility. If the source API key is revoked, OAuth access and refresh tokens derived from it stop working. Existing Authorization: Bearer <api-key> clients and /mcp?apiKey=... keep working unchanged.

CLI (pgm)

Install from npm

bash
npm install -g @ivotoby/postgram-cli

Then configure once:

bash
export PGM_API_URL=http://<postgram-host>:3100export PGM_API_KEY=<your-api-key># or persist them in ~/.pgmrc as JSON: { "api_url": "...", "api_key": "..." }

Run from source (for development)

From the repo root, invoke the TypeScript entrypoint directly — no build step needed, and it picks up local changes immediately:

bash
npx tsx cli/src/pgm.ts <command># e.g.npx tsx cli/src/pgm.ts sync ~/Documents/personal-notes --repo personal-notes

Entity commands

bash
pgm store "decided to use pgvector" --type memory --tags decisionspgm search "database decisions"pgm search "database decisions" --type memory          # filter by entity typepgm search "who worked on embeddings" --expand-graph   # include graph neighbourspgm search "database decisions" --json                 # matched chunks for agentspgm search "database decisions" --json --full-response # complete legacy search responsepgm search "database decisions" --limit 5 --toon       # compact discovery outputpgm recall <selected-entity-id>                         # complete selected contentpgm list --json                                        # compact JSON rowspgm list --json --full-response                        # full API-shaped rowspgm list --toon                                        # compact TOON rowspgm expand <id> --json                                 # compact graph JSONpgm expand <id> --toon                                 # compact TOON graphpgm recall <id>pgm list --type memorypgm update <id> --content "updated text" --version 1pgm delete <id>

Task commands

bash
pgm task add "set up monitoring" --context @focus-work --status nextpgm task list --status nextpgm task update <id> --status waiting --version 1pgm task complete <id> --version 2

Document sync

bash
pgm sync ~/Documents/personal-notespgm sync ~/Documents/cf-notes --repo cf-notes --dry-runpgm sync ~/Documents/personal-notes --quiet  # for cron

Knowledge graph

bash
pgm link <source-id> <target-id> --relation involvespgm expand <entity-id> --depth 2pgm unlink <edge-id>

Backup

bash
pgm backup --encrypt --output /tmp/postgram-backups/

Admin CLI (pgm-admin)

The supported Docker happy path uses the browser Admin dashboard for bootstrap, provider configuration, API-key creation, status inspection, and safe maintenance dry-runs. pgm-admin remains available for emergency recovery, embedding migrations, raw SQL inspection, and advanced operator jobs.

The easy CLI path uses the bin/pgm-admin wrapper shipped in the repo. It runs pgm-admin via docker exec when the container is up, and falls back to docker compose run --rm when it isn't (useful for first-boot migrations or when the startup dimension gate is refusing to boot):

bash
./bin/pgm-admin <command> [args...]

For cron or other non-interactive automation, call Docker with -T so it does not try to allocate a TTY:

bash
docker compose exec -T mcp-server /app/docker-entrypoint.sh pgm-admin <command>

Examples:

bash
./bin/pgm-admin key create --name local --scopes read,write,delete --visibility personal,work,shared./bin/pgm-admin stats./bin/pgm-admin embeddings migrate --target-dimensions 1024 --dry-rundocker compose stop mcp-server./bin/pgm-admin embeddings migrate --target-dimensions 1024 --yesdocker compose up -d mcp-server

For an embedding provider, model, or dimension change made in Admin, save, validate, and apply the target settings before running that migration sequence. The wrapper refuses --yes while mcp-server is running so the live enrichment worker cannot process the re-embedding queue with its previous in-memory provider. The dry-run is safe while the service is running.

Shell alias for daily use (add to ~/.bashrc or ~/.zshrc on your docker host):

bash
alias pgm-admin='/path/to/postgram/bin/pgm-admin'# then just: pgm-admin stats

Override with env if your service/container names differ:

bash
PGM_SERVICE=mcp-server PGM_CONTAINER=my-postgram-mcp-server-1 ./bin/pgm-admin stats

Direct equivalent without the wrapper (for reference):

bash
docker compose exec -T mcp-server /app/docker-entrypoint.sh pgm-admin <command># or, when the container is down:docker compose run --rm mcp-server pgm-admin <command>

The entrypoint is required for commands executed in an already-running container. It reconstructs Docker-managed values such as DATABASE_URL, which are exported for the server process but are not present in a plain docker compose exec environment.

Main commands:

  • key create, key list, key revoke

  • audit — query audit logs

  • model list, model set-active

  • reembed --all — mark entities for re-embedding (optionally --type <type>; pair with --model <id> to switch the active embedding model in the same transaction)

  • reextract --all — reset extraction_status = 'pending' and clear any stored extraction_error so the worker retries extraction (e.g. after switching to a better LLM). Key flags:

    • --type <type> — scope to a specific entity type
    • --only-failed — only re-queue entities whose extraction previously failed
    • --no-edges-only — only re-queue entities that have no LLM-extracted edges; useful for targeted maintenance without re-processing entities that already linked correctly (combine with --type document to catch large documents that silently produced no edges)
    • --clean-edges — delete existing source='llm-extraction' edges for the in-scope entities before re-queuing, giving a clean-slate redo rather than appending alongside old edges
    • --limit <n> — cap how many entities are queued (oldest-first)

    User-created edges (source != 'llm-extraction') are never touched.

  • improve-graph — queue entities for re-extraction with an optional per-run model/provider override stored on the row. The worker uses the override instead of the env-configured default, then clears it on success. Existing edges are kept by default (no wipe) — overlapping edges have their confidence overwritten by the new run. Key flags:

    • --all, --type <type>, --id <uuid> — scope what to queue
    • --model <name> — e.g. claude-sonnet-4-6; stored per-row
    • --provider <name> — openai | anthropic | ollama | openai-compatible; stored per-row
    • --no-edges-only — only queue entities with no LLM-extracted edges
    • --clean-edges — wipe existing LLM edges before queueing
    • --limit <n> — cap the queue size

    Typical maintenance run targeting gaps without paying for the full graph:

    bash
    ./bin/pgm-admin improve-graph --type document --no-edges-only --provider ollama --model <model>
  • prune-edges --below <threshold> — delete edges with confidence below the threshold. Scoped to source='llm-extraction' by default; pass --source any to include all, or --source <name> for a specific one. Supports --relation <name> and --dry-run for a safe preview.

  • validate-edges — run an LLM-as-judge quality pass. For each source='llm-extraction' edge (configurable via --source), asks the configured extraction LLM whether the relationship is supported by the source content; removes edges it judges invalid or below --min-confidence (default 0.4). Tracks last_validated_at in edge metadata and skips edges validated within --skip-validated-days (default 7) — run as a maintenance cron without redoing work. Flags: --limit <n> (default 100), --force, --dry-run. Requires EXTRACTION_ENABLED=true and the usual EXTRACTION_PROVIDER / EXTRACTION_MODEL env vars; costs ≈ one LLM call per edge.

  • sql "<statement>" — execute a raw SQL statement against the database. Accepts a positional argument or reads from stdin for multi-line queries. SELECT results are printed tab-separated (or as JSON with --json); DML commands print the affected row count.

    bash
    ./bin/pgm-admin sql "SELECT id, type, extraction_status FROM entities LIMIT 5"./bin/pgm-admin sql --json "SELECT COUNT(*) FROM edges WHERE source = 'llm-extraction'"
    # pipe multi-line SQL from a filecat fix.sql | ./bin/pgm-admin sql
  • stats — entity counts, chunk count, DB size

  • embeddings migrate — switch embedding dimensions (see specs/002-local-embeddings/quickstart.md)

Graph Maintenance

The knowledge graph builds up over time as LLM extraction links entities together. Occasionally edges go missing (e.g. after a provider change, a max_tokens limit being hit, or a model outage) or need refreshing. The admin CLI has tools to handle this without re-processing the entire graph.

Finding gaps

Entities that completed extraction but produced no edges are the primary signal of a silent failure:

bash
./bin/pgm-admin sql "  SELECT id, char_length(content) AS chars, created_at  FROM entities  WHERE type = 'document'    AND extraction_status = 'completed'    AND NOT EXISTS (      SELECT 1 FROM edges WHERE source_id = id AND source = 'llm-extraction'    )  ORDER BY chars DESC  LIMIT 20"

Targeted re-extraction (no wipe)

Re-queue only the entities with no edges. Existing edges on other entities are untouched:

bash
# Using the default extraction model./bin/pgm-admin reextract --type document --no-edges-only
# Using a local Ollama model (zero API cost)./bin/pgm-admin improve-graph --type document --no-edges-only --provider ollama --model <model>

Full re-extraction pass

When you want to redo everything (e.g. after switching to a better model):

bash
# Wipe and redo — gives a clean slate./bin/pgm-admin reextract --all --clean-edges
# Or scope to documents only./bin/pgm-admin reextract --type document --clean-edges

Confidence pruning

Remove low-confidence edges left behind by older or weaker models:

bash
./bin/pgm-admin prune-edges --below 0.5 --dry-run   # preview./bin/pgm-admin prune-edges --below 0.5             # apply

Edge validation

Run an LLM-as-judge pass to remove edges not supported by the source content:

bash
./bin/pgm-admin validate-edges --dry-run --limit 200./bin/pgm-admin validate-edges --limit 200

Monitoring queue progress

bash
pgm queue                              # via pgm CLI./bin/pgm-admin sql "SELECT extraction_status, COUNT(*) FROM entities GROUP BY 1"

Talon Migration

bash
docker compose cp /path/to/talon.sqlite mcp-server:/tmp/talon.sqlite
docker compose exec -T mcp-server \  node dist/migrate-talon/index.js /tmp/talon.sqlite \  --api-base-url http://127.0.0.1:3100 \  --api-key "$PGM_API_KEY"

Useful flags: --dry-run, --thread <id>, --batch-size <n>, --skip-embeddings

Testing

bash
npm test            # all testsnpm run lint        # eslintnpm run build       # typechecknpm run test:coveragenpm run benchmark:search -- --assert  # 5k-entity/6k-chunk latency gatenpm run benchmark:quality -- --assert # synthetic search relevance gate

The search benchmark reports p50/p95 latency for six profiles — cold_unique_queries (every query pays a provider round trip), memory_cache_hit, database_cache_hit (in-process cache empty, so only the persisted cache can serve it), long_keyword_query (the bounded chunk GIN keyword branch), pasted_query (300 distinct input terms with 24 retained), and underfilled_hnsw_fallback (forced HNSW over a narrow tagged fixture followed by exact fallback) — along with the number of embedding provider calls each profile made and an EXPLAIN (ANALYZE, BUFFERS) summary of the hybrid SQL. It stubs the embedding provider with a fixed delay, so it measures SQL time and cache hit rate; it says nothing about real provider latency.

Capture full plans for the actual long-keyword fixture and the HNSW strict iterative-scan settings with:

bash
npm run benchmark:search -- --explain-output /tmp/search-plans.json

The quality benchmark reports nDCG@5, MRR, and recall@10 by category against the public synthetic corpus. To run private graded labels, set SEARCH_QUALITY_LABELS to a local JSONL file, pass --private to use the ignored default path benchmarks/fixtures/private-search-quality.labels.jsonl, and configure an explicit DATABASE_URL. This mode reads the configured database and label file only; it does not seed, reset, or mutate the database. For scoped memories, pass --client-id <client> or set SEARCH_QUALITY_CLIENT_ID to the actual client identity; private mode enforces read-only database access and verifies the configured embedding provider, model, and dimensions against the active identity. The benchmark defaults to the exact strategy for public and private labels. Public baseline and --assert gate runs therefore measure relevance independently of ANN approximation. Pass --strategy auto or --strategy hnsw to inspect approximate retrieval. Keep private labels outside version control.

Targeted suites:

bash
npx vitest run tests/unit/npx vitest run tests/integration/npx vitest run tests/contract/

Current Status

Postgram is actively developed by one maintainer and used daily in a personal deployment. Entity storage, task management, Markdown sync, hybrid search, knowledge-graph traversal, memory lifecycles, OAuth, the user-facing web UI, and the guarded Admin UI are implemented. The project is deliberately optimized for personal and small-team self-hosting rather than multi-tenant scale.

Notes And Limitations

  • Postgram is optimized for personal/small-team scale
  • Non-Compose deployments default to OpenAI embeddings; Compose selects OpenAI when a key is present and otherwise selects local Ollama. Search requires the selected provider to be reachable.
  • LLM extraction is optional and disabled by default
  • Backup encryption requires gpg

Claude Code skill

A portable Claude Code skill for using pgm from your own agent lives in skill/postgram/SKILL.md. Copy the skill/postgram/ directory into your own project's .claude/skills/ (or your user-level ~/.claude/skills/) and the agent will know when to invoke pgm store, pgm search, pgm link, etc. It assumes the CLI is on PATH and PGM_API_URL + PGM_API_KEY are set. The skill file is deliberately not under .claude/ in this repo so you can decide where to put it.

Optimising your global CLAUDE.md

To get the most out of Postgram across sessions, add Postgram-aware guidance to your global ~/.claude/CLAUDE.md. A ready-to-use template is provided at templates/CLAUDE.md — it covers when to search (with type filters), how to inspect compact edges.count/edges.relations, when to use expand_graph, when to store, when to link, and general principles. Copy the relevant sections into your own CLAUDE.md and Claude will proactively use the MCP tools to persist and recall knowledge without being asked. Its default retrieval flow is search for compact matched chunks, then recall only the selected entities that require complete content.

For coding agents that should avoid broad knowledge-work behavior, use templates/AGENTS.coding.md or templates/CLAUDE.coding.md. It narrows Postgram usage to session-context memory and durable development memory only.

Releases & CI

The CLI package publishes to npm as @ivotoby/postgram-cli on every merge to main, driven by semantic-release v25 and conventional commits scoped to cli (e.g. feat(cli): ...). Non-CLI-scoped commits don't bump the CLI version. Workflow: .github/workflows/release-cli.yml.

Publishing uses an npm Automation token stored as the NPM_TOKEN repository secret. The --provenance flag is passed at publish time so every release gets a Sigstore-signed provenance attestation regardless.

First-time setup:

  1. On npmjs.com: Avatar → Access Tokens → Generate New Token → Automation
  2. GitHub repo: Settings → Secrets and variables → Actions → New repository secret → name NPM_TOKEN, value: the token from step 1
  3. Subsequent publishes happen automatically from the workflow.

The server's Docker image publishes to ghcr.io/ivo-toby/postgram on every merge to main and on semver tag pushes (multi-arch amd64 + arm64). Workflow: .github/workflows/docker.yml. Uses the built-in GITHUB_TOKEN; no extra secret required, but repo packages:write permission must be enabled.

Official MCP Registry

Postgram is published as io.github.ivo-toby/postgram in the official MCP Registry. The registry metadata in server.json describes the public GHCR image and its Streamable HTTP endpoint.

Registry releases are intentionally manual. After changing server.json, wait for the docker-publish workflow on main to finish, then run the publish-mcp-registry workflow. It verifies the official publisher download, pins the current multi-architecture main image by digest, validates the metadata, authenticates with GitHub OIDC, and publishes it. Increase the top-level version in server.json before publishing a metadata update; the registry treats each published version as immutable.

Licensing

Postgram uses a deliberate multi-license structure:

The AGPL permits commercial use. Its network copyleft requires operators of a modified Postgram service to offer the corresponding source to users who interact with that modified service over a network. See LICENSING.md for the exact path boundaries and practical examples.

Contributions are welcome under the process in CONTRIBUTING.md. Contributors must accept the Postgram Contributor License Agreement before a contribution can be merged.

Related Docs

來源:README.md,提交 269111c

工具

0
SourceWeft 尚未在沙箱中啟動過該軟體包,因此其工具未知。

版本歷史

1
  1. v0.1.3最新Sep 16, 2026

評分

已滿足適用檢查項的 50%

該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。

  • 近 30 天內可連通25 分

    請保持端點能夠回應 MCP 握手;SourceWeft 每 30 天探測一次。

  • 已宣告認證方式10 分

    請在註冊中心條目中宣告憑證請求標頭,或保持端點能夠回應探測,以便判定其認證方式。

  • 已列出工具15 分

    請讓伺服器在無憑證時回應 tools/list,或發布可在沙箱中無憑證啟動的軟體包。

  • 描述必需15 分
  • 圖示10 分
  • 文件15 分
  • 授權條款10 分