A prompt engineering workbench for local LLMs. Author, version, and test prompts through a browser UI. Integrate via WebSocket, HTTP, or Node.js library. Everything runs locally via Ollama.
- Node.js v22+
- Ollama running locally (default:
http://localhost:11434)
npm install
npm start # random port, no db (UI prompts on open)
npm start -- --port 3000 --db /path/to/ctxart.db # fixed port and db path
npm start -- --ollama http://192.168.1.5:11434 # custom Ollama URLThe server prints its port on startup:
Server running on port 54321
Webapp: http://localhost:54321
Open that URL in your browser. If no --db flag was passed, the UI will prompt
for a database path before you can use it — enter an absolute path and the file
will be created if it doesn't exist.
An objective is the named unit of work — what the LLM is supposed to
accomplish. Each objective has a slug (summarize-ticket,
classify-intent) that appears in all API and WS calls. It holds one or more
template versions.
A template defines how an objective executes: system prompt, seed chat (few-shot turns), and model selection. Versions are immutable once committed. You edit a draft, then commit it to create a new version.
- Create or select an objective
- Edit in the Editor tab — changes are saved as a draft (auto-persisted, 500ms debounce)
- Click Commit to lock the draft as a new version
- Committed versions are available for runs and benchmarks
Saved inputs (with optional ideal outputs) used to benchmark versions. Run all test cases for an objective across any version from the Tests tab.
| Tab | Purpose |
|---|---|
| Editor | Draft system prompt, seed chat, model selection. |
| Versions | List of committed versions and their run information. |
| Runs | Live and historical run output with token confidence heatmap and test case management. |
| Metrics | Aggregated analytics: mean logprob, latency, token cost per version. |
| Config | Objective-level settings: API info, tags. |
Connect to /socket. All LLM output flows through WebSockets.
Subscribe to channels:
{ "type": "sub", "channels": ["firehose"] }Channel options: firehose, objective:{slug}, tag:{tag}, run:{run_id}
Trigger a run (committed version):
{
"type": "run",
"objective": "summarize-ticket",
"input": "ticket content here",
"caller_id": "my-app"
}Optional fields: versionId (defaults to latest committed version),
testCaseId (uses a saved test case as input instead of input).
Events emitted:
Subscribe to run:{run_id} (from run.start) and unsubscribe after run.done
to wait on a single result without polling.
Draft runs use the objective's current draft instead of any committed version.
Their output is published only to objective:{slug}:draft and the
initiating socket's run:{run_id} — they never appear on firehose,
objective:{slug}, or tag:{tag} channels, and are never written to the
database.
Workflow:
- Create the objective and write its draft via the HTTP API (see Drafts below).
- Subscribe to the draft channel so you receive run events:
{ "type": "sub", "channels": ["objective:summarize-ticket:draft"] } - Trigger a draft run:
{ "type": "run", "objective": "summarize-ticket", "input": "test input", "draft": true } - Receive the same
run.start/run.token/run.doneevent sequence as a normal run, with"draft": trueon each event.
Multiple subscribers can monitor the same draft channel simultaneously — for example a CI script and the browser UI watching the same objective will both receive the output.
Once the draft is satisfactory, commit it to a version via the HTTP API
(POST /api/objectives/:slug/versions) and run against the version normally.
POST /api/objectives — create an objective
{
"name": "Summarize ticket", // required — human label
"slug": "summarize-ticket", // optional — auto-derived from name if omitted
"tags": ["production", "support"] // optional
}GET /api/objectives — list all objectives
GET /api/objectives/:slug — get one objective
PATCH /api/objectives/:slug — update tags or default version
{
"tags": ["production"], // optional
"defaultVersionId": "uuid" // optional
}Each objective has one draft — the in-progress edit before committing. The UI auto-saves the draft on every change.
GET /api/objectives/:slug/draft — get current draft
PATCH /api/objectives/:slug/draft — save draft (full replace)
{
"model": "qwen3:8b",
"systemBlocks": [{ "type": "text", "content": "You are a..." }],
"seedChat": [
{
"role": "user",
"blocks": [{ "type": "text", "content": "Example input" }]
},
{
"role": "assistant",
"blocks": [{ "type": "text", "content": "Example output" }]
}
],
"think": false,
"options": { "temperature": 0.7, "top_k": null, "top_p": null, "min_p": null }
}DELETE /api/objectives/:slug/draft — clear draft
Versions are created by committing a draft. They are immutable after creation.
GET /api/objectives/:slug/versions — list committed versions
POST /api/objectives/:slug/versions — commit a new version (body has the
same shape as the draft PATCH above)
GET /api/objectives/:slug/tests — list test cases
POST /api/objectives/:slug/tests — create a test case
{
"name": "Short ticket",
"inputBlocks": [{ "type": "text", "content": "Printer not working" }]
}PATCH /api/objectives/:slug/tests/:id — update a test case (same fields,
all optional)
DELETE /api/objectives/:slug/tests/:id — delete a test case
GET /api/objectives/:slug/runs — last 20 runs for an objective
GET /api/objectives/:slug/versions/:v/runs — last 20 runs for a specific
version number
GET /api/tags — list all tags in use across all objectives
GET /api/tags/:tag/objectives — list objectives with a given tag
GET /api/status — server readiness: { "db": true, "ollama": true }
POST /api/config/db — set database path at runtime (no db required to call
this)
{ "path": "/absolute/path/to/ctxart.db" }GET /api/ollama/models — list model names available in Ollama
All data is stored in /path/to/ctxart.db (SQLite, better-sqlite3). You can
select the path on load, or by passing --db /path/to/ctxart.db. Back it up by
copying the file.
The server compiles .ts files on-the-fly with esbuild — no build step needed.
Edit files in src/ui/app/ and reload the browser. The UI is built with
Lit web components.
To add a new component, create src/ui/app/components/ctx-my-thing.ts.