Skip to main content

Listener

The local listener is an HTTP + WebSocket server that the standalone CLI binds on 127.0.0.1:53219. It exposes three things to localhost: a health endpoint, a per-hook webhook fire endpoint, and a WebSocket upgrade that lets the web app drive the standalone CLI as a remote target. Standalone-only — there is no listener in the web CLI runtime. Relevant when you want to pair the web tab to a running CLI, receive webhooks from TradingView (or anything else), or keep accounts streaming on a dedicated host while you switch browsers.


HTTP surface

Method + pathAuthPurpose
GET /healthnoneLiveness probe. Returns { ok: true, pid, port }.
POST /hook/<id>X-Hook-Token: <32-hex secret>Fire a registered hook. See Hooks & webhooks.
WS upgrade /wsorigin allowlist + double-trustBidirectional channel for the web listener target.
anything else404.

GET /health:

$ curl http://127.0.0.1:53219/health
{"ok":true,"pid":42101,"port":53219}

WebSocket handshake

When the web app's CLI tab targets listener, it opens ws://127.0.0.1:53219/ws and runs through this sequence:

  1. Origin allowlist. The Sec-WebSocket-Origin header is checked against the configured allowlist before the socket is accepted.
  2. hello frame. Client must send { t: 'hello', clientId, label? } as its first frame. clientId is a UUID the web app generates and persists per browser-tab namespace; max 64 chars. label is an optional human string for the trust prompt.
  3. Trust check. The CLI looks up clientId in ~/.tealstreet/trusted-clients.json. Known clients pass through silently. Unknown clients pause the WS handshake and trigger a REPL prompt asking the operator to approve or deny.
  4. Accept or reject. On approval the CLI replies { t: 'welcome', listenerId, user: { email } }. On rejection it replies { t: 'rejected' } then closes the socket with code 4001.
  5. Topic subscribe. Welcomed clients receive snapshots for the topics they care about: accounts, tasks, hooks, hook-output.

Once a client is welcomed, all further interaction happens via the wire frames below.


Wire frames

Every frame is JSON with a discriminator field t. Listed in handshake-then- runtime order.

tDirectionPayloadPurpose
helloclient → CLI{ clientId, label? }First frame after socket open. Identifies the web client.
welcomeCLI → client{ listenerId, user: { email } }Acceptance. listenerId is the CLI's persistent UUID — the web app pins it to detect "different CLI" on the next connect. Email is display-only.
rejectedCLI → client(empty body)Trust denied. Always followed by close code 4001.
cmd:execclient → CLI{ id, command, account?, symbol? }Execute command as if typed at the REPL prompt. Optional account / symbol scope the run.
cmd:outputCLI → client{ id, kind, text }kind ∈ print | success | warn | error | infoStreamed output. Same PrintKind values as the REPL renders (see Output). Webhook fires also stream on the hook-output topic.
cmd:doneCLI → client{ id, success, error? }Run finished. Stray late cmd:output frames from ; / & chains may still arrive after this — match by id.
mutate:accountclient → CLI{ id, op: 'upsert' | 'delete', account?, name?, updatedAt? }LWW merge of an account from the web side into the CLI's accounts list.
mutate:hookclient → CLI{ id, op: 'create' | 'rotate' | 'revoke', name?, command?, account?, hookId? }Manage the hook registry from the web. See Hooks & webhooks.
mutate:ackCLI → client{ id, data? }Mutation succeeded.
mutate:errCLI → client{ id, error }Mutation failed.
state:accountsCLI → topic{ accounts: [...] }Full snapshot. Pushed on subscribe + after each write (debounced).
state:tasksCLI → topic{ tasks: [...] }Snapshot of tasks.json.
state:hooksCLI → topic{ hooks: [...] }Snapshot of hooks.json with secret scrubbed. Web never sees raw secrets over the wire.

Auth model

Two opaque identifiers cooperate to gate the WS.

IdentifierLives whereUsed for
clientIdWeb app (browser-tab namespace UUID; max 64)Sent in hello. Looked up in trusted-clients.json for double-consent.
listenerId~/.tealstreet/listener.json (generated on first run)Sent in welcome. Web pins it on first approval and surfaces a warning if a later welcome returns a different one.

Layers on top:

  • Double-consent. First connection from a new clientId always triggers a REPL prompt. The operator must approve before the socket completes. See Trusted clients for the persistence shape and revocation commands.
  • Origin allowlist. Enforced before the handshake even starts.
  • JWT email. The welcome.user.email is pulled from the standalone CLI's stored OAuth token. Display only — there is no incoming JWT and the email is not used as an auth credential.

Kill switches

All three default to true. They live under listener.* in config.json and are read fresh on every call — no memoization, no restart needed.

FlagWhat it gatesEffect when false
listener.enabledWhether the loopback server binds at all.Server never starts. POST /hook/<id> returns 503. WS upgrade refused.
listener.accountSyncstate:accounts broadcasts + mutate:account accept.Accounts topic stops emitting; mutate:account returns mutate:err.
listener.remoteExeccmd:exec frames and POST /hook/<id> fires.cmd:exec returns an error frame; webhook fires return 423.

To set them, edit ~/.tealstreet/config.json directly:

{
"listener": {
"enabled": true,
"accountSync": true,
"remoteExec": false
}
}

Files on disk

All under ~/.tealstreet/. Override the directory with the TEALSTREET_CONFIG_DIR env var if you need a non-default location.

FileContentsSensitive?
listener.json{ listenerId, createdAt }no
trusted-clients.json{ clients: [{ clientId, label, origin, approvedAt }] }no
hooks.json{ hooks: [{ id, name, secret, command, account?, createdAt }] }yes — contains shared secrets

See Config files for the full per-file map.


Port and reconnect

  • Default port 53219, hardcoded.
  • 75 s client-hint timer. If nothing connects to the listener within 75 s of CLI startup, the REPL prints a hint reminding you to refresh the web tab. The web app caps its reconnect backoff at 60 s, so 75 s is enough headroom to assume the web side has given up.
  • Topics: accounts, tasks, hooks, hook-output. A client opts in per-topic after welcome.

For the end-to-end web ↔ standalone setup, see Pairing. For the webhook HTTP variant of cmd:exec, see Hooks & webhooks.