ChatPanel from the terminal and from your own code — the CLI, the SDKs, and the local gateway's API they all use.
Every ChatPanel client — the browser extension, the desktop app, the terminal CLI, and anything you
build with the SDK — is a client of the same thing: the gateway, a small server on
your machine that only answers on 127.0.0.1:4320. It replaces names, emails, numbers and
secrets with placeholders before a request leaves the machine and restores them in the reply; routes
each turn to the model or coding agent you pick; and holds what every client shares — your searchable
history (chats, notes, meetings), durable memory, settings, and the team and project boards.
| Piece | What it is | Get it |
|---|---|---|
| Extension | The side panel in Chrome, Edge and Firefox. Works alone with an in-browser model or your own key. | Install |
| Gateway | The local privacy proxy. Brings redaction, routing, dictation, and the shared history to every client. Bundles the bridge, which runs coding agents (Codex, Claude Code, OpenCode…) and MCP servers. | below |
| Desktop | A native app that bundles the gateway and bridge, so there is one thing to install. | Install |
| CLI | chatpanel — the same chats, models, tools, memory and teams in a terminal, and as commands you can script. | below |
| SDK | Typed clients for TypeScript, Python, Go, Rust, Java, Kotlin, C#, Ruby, PHP, Swift and Dart — your application as one more client. | below |
The CLI and the SDKs need it running. Pick one of these (the desktop app counts):
# the standalone binary (macOS / Linux) — also installs the bridge
curl -fsSL https://dl.chatpanel.net/install.sh | bash
# or, with Node 22+
npm i -g @chatpanel/gateway --ignore-scripts
chatpanel-gateway
Check it: curl -s http://127.0.0.1:4320/health answers with its version. The extension
detects it automatically; nothing else to configure. Details and which build to pick:
FAQ.
chatpanelnpm i -g @chatpanel/cli
chatpanel # the chat screen (also `cpcli`)
chatpanel -c # continue the last chat · -r picks one
chatpanel ask "what did we decide about the rollback?"
The screen is the side panel in your terminal: settled messages scroll into your terminal's own
history, the reply streams with what the turn is doing (which tool it called, what a coding agent
is reading), and every answer ends with what it cost. Type / for the menu —
/model, /search, /memory, /skills,
/teams, /redact… and every skill, recipe, team and agent you saved
in the extension or the desktop is a /command here too, because they live on
the gateway. !git status runs a shell command and carries its output into your next
question. Ctrl+P picks a model, Ctrl+O opens a past chat, Esc
stops a reply.
chatpanel ask "<question>" [--model <id>] [--chat <id> | --continue] [--no-tools] [--redaction off] [--json]
chatpanel models [--all] # what can answer, agents first
chatpanel chats [filter] # your chats; continue one with --chat <id>
chatpanel show <record id> # a chat, note or meeting
chatpanel search "<query>" [--type note] [--since 7d]
chatpanel memory [remember "<fact>" | forget <id>]
chatpanel skills · teams · runs [<id> | stop <id>]
chatpanel sync [--push] [--pull]
chatpanel status
chatpanel config set model codex # this terminal's defaults
chatpanel redact "<text>" # what the model would receive
ask reads stdin as context when piped, streams the answer, and prints the chat id so you can continue it. --json gives a script the answer, the model, usage, timing and the tool steps in one object — never scrape prose.
# review a diff with a coding agent, through the redacting proxy
git diff | chatpanel ask "review this for bugs" --model claude
# a script: one JSON object on stdout
chatpanel ask "list the action items from yesterday's standup" --json | jq -r .answer
# search everything you've captured — chats, notes, meetings — then read one
chatpanel search "rollback" --since 30d
chatpanel show chat:mu5bogfxq6ouzk
# what would the model actually receive?
chatpanel redact "Call Jordan Blake at 555-0100 about invoice 4471"
# → Call [[PERSON_1]] at [[PHONE_1]] about invoice [[NUMBER_1]]
Start chatpanel inside a repository and it asks, once, whether coding agents may work
there. Say yes and a Codex or Claude Code turn from that chat reads and changes files in that
folder. /cwd shows or changes it; chatpanel config trust [dir] from a
script; ask --cwd <dir> for a one-off. Your home directory and / are never a
project.
Everything lives under ~/.chatpanel. CHATPANEL_GATEWAY_URL points the CLI at
another gateway. It authenticates to the gateway as you, with the token in
~/.chatpanel/gateway-token (see tokens).
One OpenAPI contract for the gateway, generated into every language, in the chatpanel-sdk repository. TypeScript and Python are first-class — zero dependencies, streaming, pairing, and a runtime that enforces the rules below. Go, Rust, Java, Kotlin, C#, Ruby, PHP, Swift and Dart are generated whole from the same contract.
| Language | Install | Import |
|---|---|---|
| TypeScript / JavaScript | npm i @chatpanel/sdk | import { ChatPanel } from '@chatpanel/sdk' |
| Python 3.11+ | pip install chatpanel | from chatpanel import ChatPanel |
| Go 1.23+ | go get github.com/chatpanel/chatpanel-sdk/generated/go | chatpanel "github.com/chatpanel/chatpanel-sdk/generated/go" |
| Rust, Java, Kotlin, C#, Ruby, PHP, Swift, Dart | generated/<lang> in the repo, each with its own README | |
import { ChatPanel } from '@chatpanel/sdk';
const cp = await ChatPanel.fromEnvironment(); // finds ~/.chatpanel/gateway-token
for await (const text of cp.chat.text({
model: 'claude', // any id from cp.models.list(); 'claude/opus' picks the agent's model
messages: [{ role: 'user', content: 'Summarise the rollback decision for the team.' }],
})) {
process.stdout.write(text); // names/emails were redacted on the way out, restored here
}
from chatpanel import ChatPanel
cp = ChatPanel.from_environment() # finds ~/.chatpanel/gateway-token
for text in cp.chat.text({
"model": "claude",
"messages": [{"role": "user", "content": "Summarise the rollback decision for the team."}],
}):
print(text, end="", flush=True)
client := chatpanel.NewAPIClient(chatpanel.NewConfiguration()) // http://127.0.0.1:4320
// Coding agents need the gateway token; a cloud/API model does not.
home, _ := os.UserHomeDir()
tok, _ := os.ReadFile(filepath.Join(home, ".chatpanel", "gateway-token"))
ctx := context.WithValue(context.Background(), chatpanel.ContextAccessToken, strings.TrimSpace(string(tok)))
prompt := "Summarise the rollback decision for the team."
content := chatpanel.StringAsChatMessageContent(&prompt)
req := chatpanel.ChatCompletionRequest{
Model: "claude",
Messages: []chatpanel.ChatMessage{{Role: "user", Content: *chatpanel.NewNullableChatMessageContent(&content)}},
}
out, _, err := client.ChatAPI.ChatCompletions(ctx).ChatCompletionRequest(req).Execute()
if err != nil { log.Fatal(err) }
fmt.Println(*out.Choices[0].Message.GetContent().String)
// Several phrasings, rank-fused; briefs (compactions of many records) come first
const { results } = await cp.history.smartSearch({
question: 'what did we decide about the rollback?',
queries: ['rollback decision', 'roll back friday'],
limit: 5,
});
const record = await cp.history.get({ id: results[0].id, maxChars: 4000 });
// Durable facts about the user, as a prompt block you can drop into your own model call
const { block } = await cp.memory.recall({ text: 'how does the user like answers written' });
// Writes need the token (fromEnvironment found it); attribution is recorded
await cp.memory.remember({ text: 'Prefers short answers with a code sample', source: { via: 'my-app' } });
// What the model would receive if this text were sent now — never the real values back
const { text, count } = await cp.redaction.preview({ text: 'Call Jordan Blake at 555-0100' });
// text === 'Call [[PERSON_1]] at [[PHONE_1]]', count === 2
// Follow a team run live
for await (const ev of cp.teams.runEvents(runId, { after: -1 })) console.log(ev.data.type);
hits = cp.history.smart_search({"question": "what did we decide about the rollback?", "queries": ["rollback decision"], "limit": 5})
record = cp.history.get({"id": hits["results"][0]["id"], "maxChars": 4000})
recalled = cp.memory.recall({"text": "how does the user like answers written"})
cp.memory.remember({"text": "Prefers short answers with a code sample", "source": {"via": "my-app"}})
preview = cp.redaction.preview({"text": "Call Jordan Blake at 555-0100"})
# preview["text"] == "Call [[PERSON_1]] at [[PHONE_1]]"
for frame in cp.teams.run_events(run_id, {"after": -1}):
print(frame.data["type"])
A browser cannot read the token file, so it pairs: the user runs chatpanel-gateway pair
(or opens the desktop's pairing screen) and types the six-digit code into your page.
const cp = new ChatPanel(); // http://127.0.0.1:4320; localhost origins are allowed
const { token } = await cp.pair(codeTheUserTyped); // single use, five minutes
// keep `token` somewhere only this user can read; next time: new ChatPanel({ token })
toString, not in a log line./health once and refuses a call your gateway is too old for — "your gateway is 0.9.4; history.records needs 0.10.0" — instead of an obscure failure.GatewayUnreachableError, GatewayTooOldError, ForbiddenError, NotFoundError, InvalidRequestError, all with the gateway's own message, its status and type.
The SDK never redacts by itself — redaction is the gateway's job and happens between the gateway and
the model. Your code sees the user's real text on both sides, like any local client;
redaction.preview shows what the model would get.
Not every question needs a language model. A typed decision — which team, how urgent,
is this spam, is this a complaint — is answered by a small classifier in one pass, in tens of
milliseconds, on your machine, with a probability per option. Ranking — which of these
twenty passages actually answers the query — is a cross-encoder, the same way. The gateway runs both
as loopback-only containers you start under Settings › Runtime (pick the model there, by your
machine's memory, or paste any Hugging Face id of the right kind), and serves them at two routes every
SDK carries: POST /v1/decide and POST /v1/rerank. Nothing leaves the machine.
The shape is small on purpose. A decision is a state (the text) and one or more
questions, each a choice (pick one of the options), a score
(place it on an ordered rubric) or a noul (yes / no). The answer carries the
value, its p, the whole distribution, and calibrated — whether
p may be read as a probability at all. A zero-shot classifier's is not: treat it as an
ordering and threshold with care.
import { ChatPanel } from '@chatpanel/sdk';
const cp = await ChatPanel.fromEnvironment();
// Is the decision model up? (the Runtime's container, or a server you configured)
const caps = await cp.capabilities.list();
const decide = caps.capabilities.find((c) => c.id === 'decide'); // { models, calibrated, stats… } or undefined
const r = await cp.capabilities.decide({
state: 'Third time my order arrived broken. I want a refund now.',
questions: {
department: { type: 'choice', instructions: 'Which team should handle this?',
options: [{ value: 'billing', describe: 'payments, invoices, refunds' },
{ value: 'technical', describe: 'bugs, outages' },
{ value: 'sales', describe: 'pricing, upgrades' }] },
urgency: { type: 'score', instructions: 'How urgent is this?', options: ['not urgent', 'somewhat urgent', 'very urgent'] },
refund: { type: 'noul', instructions: 'Does the writer ask for a refund?' },
},
});
r.answers.department.value // 'billing' r.answers.department.p // 0.88
r.answers.urgency.value // 1.7 (on the 0–2 rubric)
r.answers.refund.value // true
r.calibrated // false — an ordering, not a probability
from chatpanel import ChatPanel
cp = ChatPanel.from_environment()
r = cp.capabilities.decide({"state": text, "questions": {"spam": {"type": "noul", "instructions": "Is this unsolicited promotion?"}}})
if r["answers"]["spam"]["value"]: ...
choice with a
describe per option, so the classifier reads your criteria, not just the label.
Run it on every ticket, e-mail or chat turn before a language model ever sees it.score over ['poor', 'fair', 'good', 'excellent']. The answer is a
position on the rubric plus the distribution, so a 1.7 means "between fair and good".nouls in one call, each a
yes/no with its probability, checked passively on every message; the language model is never
in the loop, so there is nothing to prompt-inject.choice
over your categories, or one noul per policy line. Tens of milliseconds, on-device,
no content leaves.choice, or rank twenty candidates with rerank and take the top three.const ranked = await cp.capabilities.rerank({
query: 'What is deep learning?',
documents: passages, // strings, up to 1000
top_n: 5,
});
ranked.results // [{ index, relevance_score }] best first — indexes into `documents`
ranked.model // 'BAAI/bge-reranker-v2-m3'
# the same, from a shell
curl -s http://127.0.0.1:4320/v1/decide -H 'content-type: application/json' -d '{
"state": "Claim your free cruise now, click here",
"questions": { "spam": { "type": "noul", "instructions": "Is this unsolicited promotion?" } } }'
curl -s http://127.0.0.1:4320/v1/rerank -H 'content-type: application/json' \
-d '{"query":"deep learning","documents":["Lisbon is mild in spring.","Deep learning is a family of neural-network methods."]}'
Budgets are refused, not missed. Pass budgetMs and a provider that cannot
make it from its own latency record answers 503 over_budget before running — so a
check on the hot path never waits. No provider configured is 404 no_provider; a container
that is down is 503 provider_unavailable; an answer that does not fit the contract is a
502 here, never a bad answer in your code. Every provider off the device is a line in
chatpanel-gateway --audit.
Bring your own: any server that speaks TypeSafe's /v1/systemone (Jev, OpenDecision, Laya)
or a Text Embeddings Inference /rerank can stand behind the same two routes — point
capabilities.decide / capabilities.rerank in the gateway's config at it, or add
it under Models and Probe it. Your code does not change.
Everything the SDKs do is plain HTTP on 127.0.0.1:4320, described in
one OpenAPI document
— browse it, route by route with auth level, since-version and shapes, in the
API reference.
The chat endpoint is OpenAI-compatible, so any tool that takes a base URL can use the gateway as its
"provider" and get redaction and routing for free.
# is it up, and which version?
curl -s http://127.0.0.1:4320/health
# what can answer (coding agents first; `available` says whether the CLI is installed)
curl -s http://127.0.0.1:4320/v1/models | jq '.data[] | {id, provider_type, available}'
# a turn — OpenAI shape; add "stream": true for server-sent events.
# A coding agent (codex, claude, …) needs the token; a cloud/API model does not.
curl -s http://127.0.0.1:4320/v1/chat/completions -H 'content-type: application/json' \
-H "Authorization: Bearer $(cat ~/.chatpanel/gateway-token)" \
-d '{"model":"codex","messages":[{"role":"user","content":"Reply with the single word: pong"}]}'
# search your history
curl -s http://127.0.0.1:4320/v1/history/search -H 'content-type: application/json' \
-d '{"query":"rollback","limit":5,"since":"7d"}'
# what the model would receive
curl -s http://127.0.0.1:4320/redact -H 'content-type: application/json' -d '{"text":"mail [email protected]"}'
# {"text":"mail [[EMAIL_1]]","count":1,...}
| Group | Routes | Auth |
|---|---|---|
| gateway | GET /health · POST /whoami · POST /pair · POST /pair/code · GET /audit | open · open · open · token · token |
| models / chat | GET /v1/models · POST /v1/chat/completions | open · open for API models, token for coding agents (401 otherwise) |
| redaction | POST /redact | open |
| history | search · smart-search · related · status · list · get · records · stream (SSE) · ingest · PUT records | reads open · writes token |
| memory | list · recall · remember · forget · sync | reads open · writes token |
| prefs | GET/POST/DELETE /v1/prefs · /v1/prefs/events (SSE) | open |
| teams · projects · agents · engines · skills | runs, boards, events (SSE), scorecards, engine cards, skills | open |
Errors are { "error": { "message", "type" } } with the HTTP status; type is a
stable word (not_found, invalid_request, unavailable, …). Admin
and destructive routes (/config, /logs, clearing history or memory) exist for
the extension and the CLI and are deliberately not part of the SDK contract.
Reads on the /v1 data plane are open to any process on your machine — that is the
product; the gateway only listens on loopback and rejects web origins it does not know. Writes that
change what every client sees (remember, forget, ingest) and anything admin-shaped need the
per-install token the gateway writes to ~/.chatpanel/gateway-token
(mode 0600). A program running as you may read it — the CLI does, and
ChatPanel.fromEnvironment() does. Send it as Authorization: Bearer ….
Asking a coding agent (codex, claude, opencode…) needs it too: an agent turn
spawns a process on your machine, so the gateway only takes it from a caller you trust — an
anonymous request gets 401 agent_lane_token_required. Cloud and local API models are
a proxy hop and stay open.
A client that cannot read that file — a web page, a container — pairs: you run
chatpanel-gateway pair, get a six-digit code (single use, five minutes, five attempts),
and the client exchanges it with POST /pair. POST /whoami tells any client
what the gateway makes of it.
curl -s -X POST http://127.0.0.1:4320/whoami
# {"ok":true,"trust":"local","paired":false,"version":"0.11.1"}
curl -s -X POST http://127.0.0.1:4320/whoami -H "Authorization: Bearer $(cat ~/.chatpanel/gateway-token)"
# {"ok":true,"trust":"token","paired":true,"version":"0.11.1"}
Keep a paired token in the platform keychain or a 0600 file — never in a URL, a log, or source control.
The gateway ships continuously; clients update at their own pace. The API is therefore
additive only: a new route, a new optional field, never a rename or a removal. An
older extension, CLI or SDK keeps working against a newer gateway. The other direction is handled by
the SDKs' version gate — each route knows the gateway version that introduced it and a call your
gateway predates is refused with the version you have and the one you need. Update with
npm i -g @chatpanel/gateway --ignore-scripts, the installer, or the desktop app; GET /health
tells you what is running.
Contract and generators: chatpanel/chatpanel-sdk (Apache-2.0). Questions: FAQ.