A structured, queryable knowledge base focused on AI agents — concepts, frameworks, research, tools, workflows, people, papers. Stored as a graph, exposed over REST + MCP, with a Notion-shaped wiki UI and scriptable widgets.
The name: a trellis is a lattice structure that supports climbing plants. Same idea here — a scaffold on which knowledge grows and connects.
| Home | Domain article (long-form, scrollable, sticky TOC) |
|---|---|
![]() |
![]() |
| Tools article | Single node (editor view) |
|---|---|
![]() |
![]() |
| Reading list (curated, cross-domain) | Reading list editor |
|---|---|
![]() |
![]() |
| Graph | All pages |
|---|---|
![]() |
![]() |
| Widgets index | Vega-Lite chart widget |
|---|---|
![]() |
![]() |
- Graph backend — PostgreSQL with
pgvector(placeholder for future embeddings),tsvectorfull-text search, recursive-CTE graph traversal. No ORM. - Workspaces — full isolation between workspaces; every node, edge, widget, comment, and version is scoped. No cross-workspace queries.
- REST API — every resource mounted under
/api/v1/workspaces/:ws/…. - MCP server — Model Context Protocol over SSE at
/mcp, so any MCP-aware agent (Claude Code, Claude Desktop, etc.) can read and write the graph. - Script sandbox —
POST /runexecutes JS in avmsandbox with a workspace-scopedkbSDK injected, plusfetchfor third-party APIs. Python supported via subprocess. - Widgets — persistent, renderable outputs from agent scripts. Renderer + data are independent:
vega-lite(charts),table,markdown(with templates),graph(arbitrary node/edge graphs), andhtml(sandboxed iframe escape hatch). - Reading UI — Notion-shaped React SPA with two browse modes:
- Domain articles — opening a domain shows every node concatenated as sections with a sticky TOC + scroll-spy + TOC fuzzy search. Sections below the fold lazy-render (placeholder until 500px from viewport), so a 200-node article still paints fast. Mermaid diagrams render on demand. In-article wikilinks scroll smoothly; cross-article wikilinks navigate.
- Reading lists — curated, ordered selections that may span any
domain. Each item has an optional editorial note that appears above the
section body. Manage from the UI or from
kb_list_*MCP tools, so an agent can build a "learning path" through the graph and refresh it later. Editing per node (CodeMirror with[[-autocomplete), threaded comments, version history with side-by-side diff, force-directed graph view.
- Versioning — every save snapshots into
node_versions. Diff and restore from the UI.
You need Docker with Compose.
git clone https://github.com/kaiohenrique/Trellis.git
cd Trellis
cp .env.example .env
docker compose up -d # start Postgres
docker run --rm --network host \
--env-file .env \
ghcr.io/kaiohenrique/trellis:latest \
npm run seed -w server # seed the AI Agents workspace
docker run -d --name trellis --network host \
--env-file .env \
ghcr.io/kaiohenrique/trellis:latestOpen http://localhost:3000.
You need Node 20+ and Docker.
git clone https://github.com/kaiohenrique/Trellis.git
cd Trellis
cp .env.example .env
docker compose up -d # only postgres
npm install
npm run migrate # create schema + ai-agents workspace
npm run seed # load 20 seed nodes + 5 widgets
npm run dev # api on :3000, vite on :5173Open http://localhost:5173.
You need Node 20+ and a reachable PostgreSQL 14+ with the vector and pg_trgm extensions.
git clone https://github.com/kaiohenrique/Trellis.git
cd Trellis
cp .env.example .env
# Point KB_DATABASE_URL at your own Postgres
psql "$KB_DATABASE_URL" -c "CREATE EXTENSION IF NOT EXISTS vector; CREATE EXTENSION IF NOT EXISTS pg_trgm;"
npm install
npm run migrate
npm run seed
npm run devEvery push to main publishes a multi-arch image to ghcr.io:
docker pull ghcr.io/kaiohenrique/trellis:latestTags available:
| Tag | When |
|---|---|
latest |
Tip of main |
main |
Same as latest |
sha-<short> |
Every build |
vX.Y.Z / vX.Y |
Git tags matching v* |
Architectures: linux/amd64, linux/arm64.
The image needs a reachable Postgres. Example with the bundled docker-compose.yml:
docker compose up -d # postgres only
docker run --rm \
-e KB_DATABASE_URL=postgres://kb:kb@host.docker.internal:5432/kb \
ghcr.io/kaiohenrique/trellis:latest \
npm run migrate -w server # one-shot migrate
docker run -d --name trellis -p 3000:3000 \
-e KB_DATABASE_URL=postgres://kb:kb@host.docker.internal:5432/kb \
ghcr.io/kaiohenrique/trellis:latesthttp://localhost:3000 serves both the API and the built React app (in NODE_ENV=production, Express serves client/dist at /).
All config via environment variables. See .env.example.
| Variable | Default | Notes |
|---|---|---|
KB_DATABASE_URL |
— | Required. postgres://user:pass@host:port/db |
KB_PORT |
3000 |
HTTP port the server listens on |
KB_SCRIPT_TIMEOUT |
10000 |
JS/Python sandbox timeout in ms |
KB_MCP_ENABLED |
true |
Set false to disable /mcp |
KB_AUTH_TOKEN |
— | If set, all API + MCP routes require Authorization: Bearer <token> |
NODE_ENV |
development |
Set to production so Express serves client/dist |
Base path: /api/v1. Every response follows { data: T, error?: string }.
| Method | Path | Body |
|---|---|---|
GET |
/workspaces |
— |
POST |
/workspaces |
{ id, name, description? } |
GET |
/workspaces/:ws |
— |
PUT |
/workspaces/:ws |
{ name?, description? } |
DELETE |
/workspaces/:ws |
Cascades to all data in that workspace |
All paths below are mounted under /api/v1/workspaces/:ws/.
| Method | Path | Notes |
|---|---|---|
GET |
/nodes |
Filters: domain, tags (csv), q, limit, offset |
GET |
/nodes/:id |
Includes { edges: { outgoing, incoming } } |
POST |
/nodes |
{ id, title, domain, body?, tags?, metadata?, changed_by?, change_summary? } |
PUT |
/nodes/:id |
Same; auto-snapshots a new version |
DELETE |
/nodes/:id |
Cascades to edges/versions/comments |
GET |
/nodes/autocomplete?q=… |
Prefix match, returns { id, title, domain }[] |
GET |
/nodes/:id/versions |
Newest first, no body |
GET |
/nodes/:id/versions/:v |
Full snapshot |
POST |
/nodes/:id/versions/:v/restore |
Restore to past version |
GET |
/nodes/:id/comments |
Tree of threaded comments |
POST |
/nodes/:id/comments |
{ author?, body, parent_id? } |
PUT |
/comments/:id |
{ body } |
DELETE |
/comments/:id |
— |
GET |
/edges?from=&to=&relation= |
All filters optional |
POST |
/edges |
{ from, to, relation, weight?, metadata? } |
DELETE |
/edges |
{ from, to, relation } |
POST |
/query |
Structured graph query (see below) |
POST |
/run |
{ lang: "js" | "python", code } |
GET |
/graph |
Full export |
GET |
/graph/domain/:domain |
Domain subgraph |
GET |
/widgets[?renderer=&q=] |
List |
GET |
/widgets/:id |
Full spec |
POST |
/widgets |
Create/replace |
PUT |
/widgets/:id |
Replace |
POST |
/widgets/:id/run |
Re-execute the widget's source script |
GET |
/reading-lists |
List reading lists with item counts |
POST |
/reading-lists |
Create — { id, title, description? } |
GET |
/reading-lists/:id |
Full list with ordered items |
PUT |
/reading-lists/:id |
Update title/description |
DELETE |
/reading-lists/:id |
Cascades to items |
POST |
/reading-lists/:id/items |
Add item — { node_id, position?, note? } |
DELETE |
/reading-lists/:id/items/:nodeId |
Remove item |
PUT |
/reading-lists/:id/order |
Reorder — { order: [node_id, …] } |
DELETE |
/widgets/:id |
— |
{
"domain": "architectures",
"tags": ["multi-agent"],
"relation": { "from": "react-pattern", "type": "extends" },
"text": "planning",
"limit": 25,
"depth": 2
}All fields optional, AND-combined. depth > 0 triggers graph expansion from matched seed nodes.
Submit JS to POST /api/v1/workspaces/:ws/run and the sandbox runs it in vm.runInNewContext with a workspace-scoped kb SDK injected. fetch, URL, Headers, AbortController are available. Default timeout 10s (KB_SCRIPT_TIMEOUT).
curl -s localhost:3000/api/v1/workspaces/ai-agents/run \
-H 'content-type: application/json' \
-d '{
"lang": "js",
"code": "const tools = await kb.list({ domain: \"tools\" }); kb.log(\"found\", tools.length); result = tools.map(t => t.id);"
}' | jqA widget is a named, persistent renderable output. data is the raw payload; renderer + renderer_options describe how to display it. Re-running the source script updates data and last_run_at without touching the spec.
Chart from a third-party API (GitHub PRs):
const res = await fetch('https://api.github.com/repos/langchain-ai/langchain/pulls?state=open&per_page=100');
const prs = await res.json();
const byUser = {};
for (const pr of prs) byUser[pr.user.login] = (byUser[pr.user.login] || 0) + 1;
const rows = Object.entries(byUser).map(([user, count]) => ({ user, count }));
await kb.widget('langchain-prs-by-user', 'LangChain open PRs by contributor', {
renderer: 'vega-lite',
renderer_options: {
spec: {
mark: 'bar',
encoding: {
x: { field: 'count', type: 'quantitative' },
y: { field: 'user', type: 'nominal', sort: '-x' }
}
}
},
data: rows,
source_url: 'https://api.github.com/repos/langchain-ai/langchain/pulls',
description: 'Number of open PRs per contributor'
});
result = rows.length + ' contributors';Templated markdown card:
const res = await fetch('https://api.open-meteo.com/v1/forecast?latitude=48.85&longitude=2.35¤t_weather=true');
const weather = await res.json();
await kb.widget('paris-weather', 'Paris weather', {
renderer: 'markdown',
renderer_options: {
template: '## {{location}}\n**{{temp}}°C** · {{wind}} km/h\n\n*Updated {{updated}}*'
},
data: {
location: 'Paris, France',
temp: weather.current_weather.temperature,
wind: weather.current_weather.windspeed,
updated: new Date().toISOString(),
}
});Refresh from outside: POST /api/v1/workspaces/:ws/widgets/:id/run re-executes the stored source_script. Useful for cron-style refresh.
There are three install paths, in increasing order of "Claude Code does it automatically".
The server exposes an MCP endpoint at /mcp (SSE transport). Register it once and Claude Code (or
any MCP-aware client) discovers the tools.
claude mcp add --transport sse trellis http://localhost:3000/mcpOr via .mcp.json checked into your project root:
{
"mcpServers": {
"trellis": {
"type": "sse",
"url": "http://localhost:3000/mcp"
}
}
}This gives Claude Code access to the tools but it'll only use them when you ask. No automatic memory behavior.
Trellis ships with a Claude Code plugin that wires up:
- The MCP server registration (you don't need step 1)
- A
trellis-memoryskill that auto-activates on agent-related conversations - Slash commands:
/kb-search,/kb-remember,/kb-link,/kb-domains,/kb-context - A Stop hook that nudges the model to checkpoint generalizable knowledge before ending the turn
Install:
# Inside a Claude Code session
/plugin marketplace add kaiohenrique/Trellis
/plugin install trellis-kb@trellisAfter install, any conversation about AI agents will:
- Search the KB before answering
- Reference existing nodes with
[[wikilinks]] - Save new generalizable learnings as nodes with typed edges
- Skip ephemeral content (debugging, project-specific stuff)
The plugin lives in .claude-plugin/ — open it to read the skill instructions
and slash commands. Configure via env vars:
| Env var | Default | What it does |
|---|---|---|
TRELLIS_URL |
http://localhost:3000/mcp |
Where the plugin's MCP client connects |
TRELLIS_WORKSPACE |
ai-agents |
Default workspace for skill + commands |
If you want different KB behavior per project (e.g. different default workspace, or different
trigger rules), drop a project-level CLAUDE.md and a project-level .mcp.json in the project
root. They override the plugin defaults in that project only.
| Tool | What it does |
|---|---|
kb_workspace_list / kb_workspace_get / kb_workspace_create |
Workspace registry |
kb_domain_list / kb_domain_get / kb_domain_save |
Domain entity registry |
kb_get, kb_list, kb_search, kb_query |
Read nodes |
kb_save, kb_link |
Write nodes + edges |
kb_run |
Execute a JS/Python script in the workspace sandbox |
kb_neighbors, kb_graph |
Graph traversal |
kb_widget_list / kb_widget_get / kb_widget_save / kb_widget_refresh_data |
Widgets |
kb_list_index / kb_list_get / kb_list_save |
Reading list registry |
kb_list_add_item / kb_list_remove_item / kb_list_reorder |
Reading list items |
Every data tool requires workspace_id — there is no "current workspace" session state on the
server. The plugin's skill and slash commands inject the default from TRELLIS_WORKSPACE.
If KB_AUTH_TOKEN is set on the server, supply it in the MCP client config:
{
"mcpServers": {
"trellis": {
"type": "sse",
"url": "http://localhost:3000/mcp",
"headers": { "Authorization": "Bearer YOUR_TOKEN" }
}
}
}┌──────────────────┐ ┌─────────────────────────────────────┐
│ Claude Code │ ── MCP / SSE ──► │ Express │
│ (or any MCP │ │ ├── /api/v1/workspaces/:ws/… │
│ client) │ │ │ • nodes / edges / versions │
└──────────────────┘ │ │ • comments / widgets │
│ │ • query / run / graph │
┌──────────────────┐ HTTP/REST │ ├── /api/v1/workspaces │
│ React SPA │ ───────────────► │ └── /mcp (SSE) │
│ (Vite, on :5173 │ │ │
│ in dev) │ │ core/ │
└──────────────────┘ │ ├── graph.ts ── only file that │
│ ├── query.ts touches db/ │
│ ├── sandbox.ts │
│ └── wiki.ts │
│ sdk/kb.ts ── injected into scripts│
└──────────┬──────────────────────────┘
│
pg + recursive CTEs
▼
┌─────────────────────────────────────┐
│ PostgreSQL │
│ workspaces │
│ nodes (workspace_id, id) PK │
│ edges (FK to nodes, scoped) │
│ node_versions / comments │
│ widgets │
│ pgvector(1536) placeholder │
│ tsvector + GIN index │
└─────────────────────────────────────┘
trellis/
├── shared/ # @kb/shared — TypeScript types only
├── server/
│ ├── core/ # graph, query, sandbox, wiki helpers
│ ├── db/ # pg client, migrate, schema.sql
│ ├── api/ # Express routes + MCP server
│ ├── sdk/kb.ts # SDK injected into agent scripts
│ ├── seed/seed.ts # 20 nodes + 21 edges + 5 widgets across all renderers
│ └── tests/ # vitest
└── client/
└── src/
├── pages/ # Home, NodePage, HistoryPage, DiffPage, GraphView, Manage, Widgets
├── components/ # MarkdownRenderer, WikiEditor, GraphCanvas, Sidebar, …
├── hooks/ # React Query hooks
└── context/ # WorkspaceContext
make install # npm install
make db-up # docker compose up -d postgres
make migrate # apply schema
make seed # load seed data
make dev # run server + client concurrently
make build # build everything
make test # vitestTests:
npm test -w server # unit tests only (no DB needed)
KB_DATABASE_URL=… npm test -w server # also runs integration testsTwo GitHub Actions workflows in .github/workflows:
ci.yml— typechecks server + client, runsvitest, builds the client. Runs on every push and PR tomain.docker.yml— builds and pushes a multi-arch image toghcr.io/<owner>/trellison every push tomainand onv*tags. Uses the built-inGITHUB_TOKEN, includes build provenance attestation for supply-chain integrity.
Cutting a release:
git tag v0.2.0
git push origin v0.2.0
# triggers docker.yml — image published as ghcr.io/kaiohenrique/trellis:0.2.0MIT.









