Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 27 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,52 +123,44 @@ algorithms, engine internals).

## Install

### Prerequisites

- **Node.js** 20+ (22 recommended)
- **PostgreSQL** 14 or newer (17 recommended) with the `pg_trgm` extension
- **Python 3** with `libcst` (optional — only for Python source analysis)

### Bootstrap (recommended)
Node.js 20 or newer, and nothing else. If the machine has no PostgreSQL, ContextZero creates one for
itself — in your user data directory, on a port nobody else uses, with a password it generates and
nobody has to type.

```bash
git clone https://github.com/Classevelabs/context-zero-engine.git context-zero-engine
git clone https://github.com/Classevelabs/context-zero-engine.git
cd context-zero-engine
npm run setup -- --install-mcp=claude
```

Windows:

```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\bootstrap.ps1 -Client claude
```
That installs dependencies, provisions the database, applies the schema, builds, writes the MCP
config for your client (`claude`, `codex`, `cursor`, or `all`), and checks the result. Restart the
client and the tools are there.

macOS / Linux:
Python source analysis also wants Python 3 with `libcst`; everything else works without it.

```bash
scripts/bootstrap.sh --client claude
```
### Using a PostgreSQL you already run

The bootstrap installs dependencies, creates `.env`, builds, runs database
migrations, runs diagnostics (`npm run doctor`), and optionally writes the
MCP config for your client (`claude`, `codex`, `cursor`, or `all`).

### Manual install
Set `DB_HOST`, `DB_USER` and `DB_PASSWORD` before running setup and ContextZero uses that database
and never provisions one. It needs PostgreSQL 14 or newer, the `pg_trgm` extension, and a UTF-8
database — source code is UTF-8, and a database created with a machine's code page rejects it:

```bash
npm ci

createdb scg_v2
createdb -E UTF8 -T template0 scg_v2
psql -d scg_v2 -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;"
```

cp .env.example .env # set DB_USER / DB_PASSWORD / SCG_ALLOWED_BASE_PATHS
### The database ContextZero provisions

npm run build
npm run db:migrate
npm run doctor # verifies node, database, python, env
```bash
npm run db:status # where it is, and whether it is running
npm run db:stop # stop it
npm run db:up # start it (the MCP client does this for you)
```

Full options, client config paths, and troubleshooting:
[docs/INSTALL.md](docs/INSTALL.md) and [docs/OPERATIONS.md](docs/OPERATIONS.md).
Its data, binaries and log live under `%LOCALAPPDATA%\ContextZero` on Windows,
`~/Library/Application Support/ContextZero` on macOS, and `~/.local/share/contextzero` on Linux.
Set `CONTEXTZERO_HOME` to put them somewhere else.

---

Expand All @@ -190,12 +182,13 @@ Claude Code CLI:
```bash
claude mcp add contextzero -s user \
-e CONTEXTZERO_ENV_FILE=/absolute/path/to/context-zero-engine/.env \
-- node /absolute/path/to/context-zero-engine/dist/mcp-bridge/index.js
-- node /absolute/path/to/context-zero-engine/scripts/mcp-start.mjs
```

Any MCP client that speaks stdio works: the server is
`node dist/mcp-bridge/index.js` with the `DB_*`/`SCG_*` environment (or a
single `CONTEXTZERO_ENV_FILE` pointing at your `.env`).
`node scripts/mcp-start.mjs` with the `DB_*`/`SCG_*` environment (or a single
`CONTEXTZERO_ENV_FILE` pointing at your `.env`). It starts the database when that
database is the one ContextZero provisioned, then becomes the bridge.

MCP uses a trusted local stdio child-process boundary; it is not a remote
network authentication layer. The 43 read tools are listed by default. The 18
Expand Down
11 changes: 11 additions & 0 deletions db/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@ async function main(): Promise<void> {
try {
await acquireMigrationLock(lockClient, lockTimeoutMs)

// Said here rather than in the middle of a migration: a database on the machine's code page
// fails on the first schema text outside it, with an error that names a byte sequence.
const encoding = await pool.query<{ server_encoding: string }>("SHOW server_encoding")
const serverEncoding = encoding.rows[0]?.server_encoding
if (serverEncoding !== "UTF8") {
throw new Error(
`This database is ${serverEncoding ?? "of an unknown encoding"}; ContextZero needs UTF8, because source code is UTF-8. ` +
`Recreate it with: CREATE DATABASE <name> ENCODING 'UTF8' TEMPLATE template0;`,
)
}

// Ensure migrations tracking table exists
await pool.query(`
CREATE TABLE IF NOT EXISTS _migrations (
Expand Down
36 changes: 32 additions & 4 deletions docs/INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ ContextZero has two supported install paths:
- **Native local MCP**: best for Claude Desktop, Codex, Cursor, and local developer workflows.
- **Docker server**: best when you want a repeatable HTTP server with bundled PostgreSQL.

Both need Node.js 20 or newer. Neither needs you to install PostgreSQL: when the machine has none,
ContextZero provisions its own and generates the password itself.

For operating guidance after installation, see `docs/OPERATIONS.md`.

## Fastest Native Install
Expand Down Expand Up @@ -32,12 +35,35 @@ The bootstrap script:

- installs npm dependencies;
- installs Python `libcst` when Python is available;
- creates `.env` when missing;
- provisions a private PostgreSQL when no database is configured;
- creates `.env` when missing, with every secret generated;
- builds ContextZero;
- runs migrations unless disabled;
- runs `npm run doctor`;
- optionally installs MCP config with a backup of the existing client config.

## The Database

With no `DB_HOST`/`DB_USER`/`DB_PASSWORD` in the environment, setup provisions a PostgreSQL that
belongs to ContextZero: binaries fetched once into your user data directory, a port nobody else is
using, and a generated password written only into `.env`. The MCP launcher starts it when a client
opens, so nothing has to be running first.

```bash
npm run db:status # where it is, and whether it is running
npm run db:stop
npm run db:up
```

`CONTEXTZERO_HOME` moves that directory. To use a database you already run, set `DB_HOST`, `DB_USER`
and `DB_PASSWORD` before setup: PostgreSQL 14 or newer, with `pg_trgm`, and **UTF-8** — source code
is UTF-8, and `initdb` on a Windows locale produces a code page that cannot store it.

```bash
createdb -E UTF8 -T template0 scg_v2
psql -d scg_v2 -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;"
```

## Docker Install

Windows:
Expand Down Expand Up @@ -132,12 +158,14 @@ npm run doctor -- --fix

## PostgreSQL Notes

Native install requires PostgreSQL to be running locally or remotely.
These apply to a database you run yourself; the one ContextZero provisions is already set up this
way.

Minimum database setup:
Minimum database setup — UTF-8 from `template0`, because a database on the machine's code page
cannot store source code:

```bash
createdb scg_v2
createdb -E UTF8 -T template0 scg_v2
psql -d scg_v2 -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;"
```

Expand Down
2 changes: 1 addition & 1 deletion docs/OPERATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ python -m pip install libcst
Create the database:

```powershell
createdb scg_v2
createdb -E UTF8 -T template0 scg_v2
psql -d scg_v2 -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;"
```

Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
"db/migrations/",
"db/schema.sql",
"scripts/create-mcp-config.mjs",
"scripts/db.mjs",
"scripts/lib/local-postgres.mjs",
"scripts/mcp-start.mjs",
"scripts/doctor.mjs",
"scripts/mcp-install.mjs",
"scripts/setup.mjs",
Expand Down Expand Up @@ -39,6 +42,9 @@
"test:watch": "jest --watch",
"ingest": "ts-node src/ingestor/index.ts",
"db:migrate": "ts-node db/migrate.ts",
"db:up": "node scripts/db.mjs up",
"db:status": "node scripts/db.mjs status",
"db:stop": "node scripts/db.mjs stop",
"db:generate-schema": "node scripts/generate-schema.mjs",
"doctor": "node scripts/doctor.mjs",
"mcp:config": "node scripts/create-mcp-config.mjs",
Expand Down
3 changes: 2 additions & 1 deletion scripts/bootstrap.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ function Ensure-PostgresBestEffort {

Write-Step "Preparing PostgreSQL database when local tools allow it"
if (Get-Command createdb -ErrorAction SilentlyContinue) {
& createdb scg_v2 2>$null
# UTF-8 from template0: initdb on a Windows locale gives a code page that cannot store source.
& createdb -E UTF8 -T template0 scg_v2 2>$null
if ($LASTEXITCODE -ne 0) {
Write-Host "createdb scg_v2 skipped or already exists."
}
Expand Down
3 changes: 2 additions & 1 deletion scripts/bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,8 @@ ensure_postgres_best_effort() {

step "Preparing PostgreSQL database when local tools allow it"
if command -v createdb >/dev/null 2>&1; then
createdb scg_v2 2>/dev/null || echo "createdb scg_v2 skipped or already exists."
# UTF-8 from template0: initdb on a Windows locale gives a code page that cannot store source.
createdb -E UTF8 -T template0 scg_v2 2>/dev/null || echo "createdb scg_v2 skipped or already exists."
fi

psql -d scg_v2 -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;" 2>/dev/null || \
Expand Down
5 changes: 4 additions & 1 deletion scripts/create-mcp-config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ const repoRoot = path.resolve(scriptDir, "..")
const outDir = path.join(repoRoot, ".contextzero", "mcp")
const envPath = path.join(repoRoot, ".env")
const bridgePath = path.join(repoRoot, "dist", "mcp-bridge", "index.js")
// The client starts this, not the bridge directly: it makes sure the database is running first,
// then becomes the bridge, so nothing has to be started by hand before the client opens.
const launcherPath = path.join(repoRoot, "scripts", "mcp-start.mjs")

function parseEnvFile(filePath) {
if (!fs.existsSync(filePath)) return {}
Expand Down Expand Up @@ -110,7 +113,7 @@ const fileEnv = parseEnvFile(envPath)
const effectiveEnv = { ...fileEnv, ...process.env }
const server = {
command: process.execPath,
args: [bridgePath],
args: [launcherPath],
env: pickEnv(effectiveEnv),
}

Expand Down
47 changes: 47 additions & 0 deletions scripts/db.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env node
/**
* The private database, from the command line.
*
* npm run db:up start it (provisioning it the first time)
* npm run db:status where it is and whether it is running
* npm run db:stop stop it
*/
import { createRequire } from "node:module"
import path from "node:path"
import { fileURLToPath } from "node:url"
import { isListening, paths, provision, readState, start, stop } from "./lib/local-postgres.mjs"

const require = createRequire(import.meta.url)
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")
const command = process.argv[2] || "up"

try {
if (command === "up" || command === "provision") {
const pg = require(path.join(repoRoot, "node_modules", "pg"))
const state = await provision({ pg })
console.log(`Database ready on 127.0.0.1:${state.port}, database ${state.database}, user ${state.user}.`)
console.log(`Data: ${paths().dataDir}`)
} else if (command === "stop") {
console.log(stop() ? "Database stopped." : "No private database to stop.")
} else if (command === "status") {
const state = readState()
if (!state) {
console.log("No private database here. `npm run db:up` creates one.")
process.exit(0)
}
const running = await isListening(state.port)
console.log(`Private database on 127.0.0.1:${state.port}: ${running ? "running" : "stopped"}`)
console.log(`Data: ${paths().dataDir}`)
console.log(`Log: ${paths().logFile}`)
if (!running) process.exit(1)
} else if (command === "start") {
const state = await start()
console.log(`Database running on 127.0.0.1:${state.port}.`)
} else {
console.error(`Unknown command: ${command}. Use up, status, stop.`)
process.exit(2)
}
} catch (error) {
console.error(error instanceof Error ? error.message : String(error))
process.exit(1)
}
47 changes: 40 additions & 7 deletions scripts/doctor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import crypto from "crypto"
import net from "net"
import path from "path"
import { spawnSync } from "child_process"
import { createRequire } from "module"
import { fileURLToPath } from "url"
import { provision } from "./lib/local-postgres.mjs"

const scriptDir = path.dirname(fileURLToPath(import.meta.url))
const repoRoot = path.resolve(scriptDir, "..")
Expand Down Expand Up @@ -65,17 +67,34 @@ function escapeEnvValue(value) {
throw new Error("value contains both ' and \" and cannot be written to .env — set it by hand")
}

function createDefaultEnvFile() {
/** --fix means the install works afterwards, so it provisions the database setup would. */
async function resolveDatabaseForFix() {
if (process.env.DB_HOST && process.env.DB_USER && process.env.DB_PASSWORD) {
return {
host: process.env.DB_HOST,
port: process.env.DB_PORT || "5432",
name: process.env.DB_NAME || "scg_v2",
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
}
}
const require = createRequire(import.meta.url)
const state = await provision({ pg: require(path.join(repoRoot, "node_modules", "pg")) })
return { host: "127.0.0.1", port: String(state.port), name: state.database, user: state.user, password: state.password }
}

async function createDefaultEnvFile() {
const db = await resolveDatabaseForFix()
const allowedBasePath = process.env.SCG_ALLOWED_BASE_PATHS || path.dirname(repoRoot)
const apiKey = process.env.SCG_API_KEYS || crypto.randomBytes(32).toString("hex")
const adminApiKey = process.env.SCG_ADMIN_API_KEYS || crypto.randomBytes(32).toString("hex")
const lines = [
"# ContextZero local configuration generated by npm run doctor -- --fix",
`DB_HOST=${process.env.DB_HOST || "localhost"}`,
`DB_PORT=${process.env.DB_PORT || "5432"}`,
`DB_NAME=${process.env.DB_NAME || "scg_v2"}`,
`DB_USER=${process.env.DB_USER || "postgres"}`,
`DB_PASSWORD=${escapeEnvValue(process.env.DB_PASSWORD || "CHANGE_ME_BEFORE_RUNNING")}`,
`DB_HOST=${db.host}`,
`DB_PORT=${db.port}`,
`DB_NAME=${db.name}`,
`DB_USER=${db.user}`,
`DB_PASSWORD=${escapeEnvValue(db.password)}`,
`NODE_ENV=${process.env.NODE_ENV || "development"}`,
`LOG_LEVEL=${process.env.LOG_LEVEL || "info"}`,
`SCG_API_KEYS=${escapeEnvValue(apiKey)}`,
Expand All @@ -92,7 +111,7 @@ function createDefaultEnvFile() {

if (cliArgs.has("--fix") && !fs.existsSync(envPath)) {
try {
createDefaultEnvFile()
await createDefaultEnvFile()
} catch (error) {
console.error(`Could not write ${envPath}: ${error instanceof Error ? error.message : String(error)}`)
process.exit(1)
Expand Down Expand Up @@ -402,6 +421,20 @@ async function checkDatabase() {
fail("pg_trgm extension", "missing; run: CREATE EXTENSION IF NOT EXISTS pg_trgm;")
}

// A database created with the machine's code page — what initdb gives a Windows locale —
// rejects the schema's own text at migration 011, and every source file holding a character
// outside that page after it.
const encoding = await client.query("SHOW server_encoding")
if (encoding.rows[0].server_encoding === "UTF8") {
pass("Database encoding", "UTF8")
} else {
fail(
"Database encoding",
`${encoding.rows[0].server_encoding}; source code needs UTF8. Recreate it with: ` +
`CREATE DATABASE ${env.DB_NAME || "scg_v2"} ENCODING 'UTF8' TEMPLATE template0;`,
)
}

const migrations = fs.existsSync(migrationDir)
? fs.readdirSync(migrationDir).filter((file) => file.endsWith(".sql")).length
: 0
Expand Down
Loading
Loading