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
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.
Everything the SDKs do is plain HTTP on 127.0.0.1:4320, described in
one OpenAPI document.
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 alex.rivera@example.com"}'
# {"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, the installer, or the desktop app; GET /health
tells you what is running.
Contract and generators: chatpanel/chatpanel-sdk (Apache-2.0). Questions: FAQ.