agentsclimarketplace

Bridle

Skill CleanSlice/skills/bridle

CleanSlice agent skills — architecture patterns, vertical slices, conventional commits for Claude Code and AI coding agents.

Install
npx -y skills add CleanSlice/skills --skill bridle

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Embed Bridle webchat into a website. Wire the SDK, mint embed JWTs server-side, theme the widget through Shadow DOM, and connect to a hub running your agent.

SKILL.md

12.2 KB, as published. Nobody here has run it

Bridle

Bridle is an embeddable webchat for AI agents — one script tag connects a browser to an agent process through a stateless WebSocket hub. There are three integration paths (drop-in script, npm, headless), one wire protocol, and one auth model with two flavors (public agent vs. JWT).

Source: https://github.com/CleanSlice/bridle · Docs: https://bridle.cleanslice.org · npm: @cleanslice/bridle


Architecture (always the same)

Browser  ─►  Bridle Hub (NestJS)  ─►  Agent Runtime
   │             │   /ws/client          │   /ws/agent
   │             │   JWT + agentId       │   apiKey + agentId
   │             └───────────────────────┘
   │       routes by agentId, no state stored
   ▼
SDK auto-mounts <bridle-chat> Custom Element with Shadow DOM

The hub is stateless — it just routes messages between matched (agentId) pairs. Sessions persist in the agent runtime, not the hub.


Quick Reference

NeedAnswer
Drop-in scripthttps://bridle.cleanslice.org/sdk/latest.js
Pinned version/sdk/v0.js (latest 0.x) or /sdk/v0.8.0.js (exact)
npm package@cleanslice/bridle (exports init, BridleClient, types)
Custom Element tag<bridle-chat>
Auth modesPublic (origin whitelist, no token) · JWT (browser sends JWT minted by your backend)
Theme tokensCSS vars on bridle-chat (e.g. --bridle-primary) — pierce Shadow DOM
Internal selectors.bridle__panel, .bridle__bubble, etc. — only reachable via customCss (v0.8.0+)

Integration path 1 — Drop-in <script>

The simplest option. One tag, no build step. Works on any HTML page (WordPress, Webflow, Shopify, plain HTML).

<script
  src="https://bridle.cleanslice.org/sdk/latest.js"
  data-api-url="https://your-hub.example.com"
  data-agent-id="agent-abc-123"
  data-token="<jwt>"
></script>

When this loads, the SDK registers a <bridle-chat> Custom Element and mounts a floating bubble in the bottom-right corner.

Available data-* attributes

AttributeDefaultPurpose
data-agent-idrequiredAgent identifier registered on the hub
data-tokenrequired unless public agentJWT minted by your backend
data-api-urlinferred from script's originHub origin
data-modefloatingfloating (FAB) or inline (mounted inside data-mount)
data-mount<body>CSS selector for inline mode
data-titleAgent ChatHeader text
data-placeholderType a message...Input placeholder
data-themedefaultdefault or cleanslice (teal palette)
data-color-modeautoauto / light / dark
data-custom-cssInline CSS injected into the shadow root (v0.8.0+)
data-stylesheetOne or more CSS file URLs (comma-separated) loaded inside the shadow root (v0.8.0+)
data-promptExtra context forwarded with every message at handshake

Integration path 2 — npm (bundler)

For Vite/Next/Nuxt/Webpack. You get tree-shaking, type defs, and full programmatic control.

npm i @cleanslice/bridle
import { init } from '@cleanslice/bridle'

const chat = init({
  apiUrl: 'https://your-hub.example.com',
  agentId: 'agent-abc-123',
  token: () => fetchJwt(),     // string OR async function for refresh
  mount: '#chat',              // CSS selector or HTMLElement
  mode: 'inline',
  title: 'Support',
  themeVars: { '--bridle-primary': '#0070f3' },
  customCss: `.bridle__panel { border-radius: 5px; }`,
  onReady: () => console.log('connected'),
  onMessage: (msg) => console.log(msg.text),
  onError: (err) => console.error(err),
})

chat.sendMessage('Hi!')
chat.open()
chat.close()
chat.destroy()

init() returns { element, open, close, sendMessage, destroy }. The token can be a string OR a function returning string | Promise<string> so you can refresh expired JWTs without remounting.


Integration path 3 — Headless (no UI)

When you want to bring your own UI but use the hub for transport.

import { BridleClient } from '@cleanslice/bridle'

const client = new BridleClient({
  apiUrl: 'https://your-hub.example.com',
  agentId: 'agent-abc-123',
  token: 'eyJhbG...',
})

client.on('message', (m) => render(m.text))
client.on('stream', (m) => render(m.text))      // partial text as it streams
client.on('stream_end', (m) => finalize(m.text))
client.on('error', (err) => handleAuthError(err))

await client.connect()
client.send('hello')

Authentication

Pick public for marketing sites, docs, demos. Pick JWT when the user is signed into your product and you want per-user routing/quota/identity.

Public agent (origin whitelist, no token)

Configure the agent on the hub to accept connections from specific origins. The browser sends no token; the hub accepts the WebSocket based on the request's Origin header.

Use this when:

  • The site is public and there's no per-user state to scope by
  • All visitors get the same agent treatment

The SDK call simply omits data-token / token. If the origin isn't whitelisted, the connection is rejected and the SDK shows an inline "Origin … isn't whitelisted" banner.

JWT minted server-side

The integrator's backend mints a short-lived JWT and hands it to the browser. The Ranch hub provides this endpoint at POST /auth/embed/token:

// On YOUR backend (Node example, no dotenv):
const res = await fetch(`${RANCH_API_URL}/auth/embed/token`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${RANCH_API_KEY}`,   // server-side key, never to browser
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    sub: user.id,                                // becomes JWT.sub → clientId on hub
    email: user.email,
    expiresIn: '7d',                             // <n>(s|m|h|d). Default 15m is short for dev.
  }),
})
const { data } = await res.json()                // ranch wraps as { success, data }
return data.token                                // string + expiresAt

The JWT carries sub (becomes clientId for message routing) and email. Pass roles: ['ADMIN'] to mint an admin token (clientId = 'admin').

Never put RANCH_API_KEY in the browser. The whole point of the mint endpoint is to keep it server-side. Expose only the minted JWT.


Theming

The Custom Element uses Shadow DOM, so host-page CSS doesn't reach internal classes. Two levers:

CSS variables (cross the shadow boundary)

VariablePurpose
--bridle-primaryBrand color (FAB, send button, user bubbles)
--bridle-primary-fgForeground on primary
--bridle-bg / --bridle-fgPanel background / body text
--bridle-bubble-bgAssistant bubble background
--bridle-user-bg / --bridle-user-fgUser bubble
--bridle-borderBorders, dividers
--bridle-radiusPanel + bubble corner radius
--bridle-shadowPanel/FAB shadow
--bridle-zz-index of the floating panel
--bridle-fontFont family

Three ways to set them:

/* Plain CSS (host page) */
bridle-chat { --bridle-primary: #ec4899; --bridle-radius: 8px; }
// init() option
init({ ..., themeVars: { '--bridle-primary': '#ec4899' } })

Pre-built palettes: theme: 'default' | 'cleanslice' (cleanslice = teal/cyan).

customCss (inject inside Shadow DOM, v0.8.0+)

For everything CSS variables can't express — overriding .bridle__panel's border, the .bridle__bubble font size, etc.:

init({
  ...,
  customCss: `
    .bridle__panel {
      border-radius: 5px;
      box-shadow: 0 2px 6px #00000029;
      border: 1px solid #C5D5FF;
    }
    .bridle__bubble { font-size: 13px; }
  `,
  stylesheets: ['/css/bridle-overrides.css'],  // or comma-list via data-stylesheet
})

Cascade order: your overrides are appended after the component's own <style>, so equal-specificity rules win without !important.

Internal classes (part of the public surface)

ClassWhat it is
.bridle__panelThe chat panel
.bridle__headerHeader row with title and close button
.bridle__messagesScrollable message list
.bridle__bubbleA single message bubble
.bridle__bubble--mdAssistant bubble with rendered Markdown
.bridle__msg--user / .bridle__msg--assistantBubble wrapper per role
.bridle__inputFooter composer
.bridle__fabFloating action button
.bridle__typingThree-dot typing indicator
.bridle__banner--errorConnection-error banner

These class names are part of the SDK's public surface — they won't be renamed without a major-version bump.


Message Parts (wire protocol)

All messages carry parts: BridlePart[] — text, image, file:

{ type: 'text',  text: 'Hello' }
{ type: 'image', base64: '...', mediaType: 'image/jpeg' }
{ type: 'file',  url: 'https://...', name: 'doc.pdf', mimeType: 'application/pdf' }

The text field is always present as a shorthand. Legacy clients sending { text, images } are auto-converted by the hub.

Streaming uses stream events (accumulated text, not deltas — easier for clients) and a final stream_end event.


Recipe: embed bridle into a customer site

Paste-ready prompt the agent can adapt:

  1. Decide auth mode. Public site / docs / marketing → public agent (no token). Signed-in product → JWT.
  2. For public: ask the operator to whitelist the customer's origin on the agent in Ranch admin UI.
  3. For JWT: add a server-side endpoint that calls POST {RANCH_API_URL}/auth/embed/token with the API key as a Bearer header. Return only { token, expiresAt } to the browser.
  4. Mount the SDK. For static HTML, drop the <script> tag. For SPA, call init() in a single mount-on-load lifecycle hook (onMounted, useEffect, etc.). Always pair with destroy() on unmount.
  5. Match brand. Start with themeVars for color/radius/font. Drop to customCss only when you need to restyle internal classes.
  6. Verify. Open the page; the FAB should appear in the corner (floating) or inline in the mount. The "●" status dot in the header should be green within ~1 second.

Troubleshooting

SymptomCauseFix
Failed to start bridle: fetch failedHub isn't reachable from the SDK's backend (token mint) — usually means the hub process is down or RANCH_API_URL is wrongStart the hub, fix the URL
Banner: Origin … isn't whitelisted for this agentPublic agent's allowed-origin list doesn't include this siteAdd the origin in the admin UI
Banner: MISSING_TOKENNon-public agent and no token was passedPass data-token / token
Banner: INVALID_TOKENJWT signature failed (wrong JWT_SECRET) or expiredRe-mint a fresh token; verify JWT_SECRET matches between mint backend and hub
Banner: MISSING_AGENT_IDdata-agent-id / agentId missingSet it
Chat connects, you send a message, nothing happensNo agent runtime is online for this agentIdStart the agent process / paddock; check /api/agent/<agentId>/health for agentConnected: true
Host-page CSS doesn't reach the panelShadow DOM isolation by designUse themeVars for variables, customCss / stylesheets for everything else (v0.8.0+)
FAB never appears (drop-in)Script 404 (/sdk/latest.js), or no data-agent-id (script skips auto-mount)Check Network tab; verify data-agent-id is present

Don't

  • Don't put RANCH_API_KEY / hub JWT_SECRET in the browser. Both are server-side only.
  • Don't rely on host-page CSS reaching .bridle__* classes — use customCss instead.
  • Don't init() more than once per mount. Use destroy() first if you need to recreate.
  • Don't hardcode data-api-url if the SDK is served from the same origin as the hub — it's inferred from the script src.
  • Don't mount in floating mode AND inline mode simultaneously for the same agent unless you intentionally want two independent sessions (each gets its own clientId).

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.