Skip to content

v0.2.0

Choose a tag to compare

@lonormaly lonormaly released this 28 Jul 13:11
· 10 commits to master since this release

Changed

  • Widget: the composer grows with the message. The message field was an <input>, which can hold exactly one line no matter how long the message is β€” anyone reporting a real problem typed into a slot that showed them ~40 characters of what they had written. It is now a <textarea class="in"> that starts at one line and animates its height as the content wraps, up to 5 rows, after which it scrolls (.in.tall). Height is animated (0.18s), and dropped under prefers-reduced-motion. Enter still sends and Shift/Ctrl/Cmd+Enter opens a new line; isComposing is honoured so Enter never sends mid-IME-composition in Japanese/Chinese/Korean input. The composer is align-items:flex-end so the send button stays on the last line rather than drifting to the middle of a paragraph. Sending resets the field to one line on the same transition.

    Themers: the selectors \.ft input, \.ft input:focus and \.ft input::placeholder are now \.ft .in, \.ft .in:focus and \.ft .in::placeholder. .cap input (the lead form) is unchanged. Anything overriding the composer by element name needs the class instead.

Added

  • Release process β€” the repo now cuts semver tags + GitHub Releases via node scripts/release.mjs <x.y.z> (promotes [Unreleased] β†’ a dated version section, bumps the package.json versions, tags vX.Y.Z, publishes the GitHub Release from the changelog section). deploy.sh production refuses to ship a commit that isn't a v* tag (break-glass: ALLOW_UNTAGGED_DEPLOY=1), so every production deploy is a real, changelog'd release. See RELEASING.md.
  • Widget: theme.launcherStyle: "pill" β€” an opt-in labelled launcher, and the animated states that go with it. The pill carries the avatar, a hairline separator and a word (theme.launcherLabel, ≀24 chars, defaults to the widget title). While the panel is open it collapses to the avatar alone and sheds its frame, so the launcher and the panel stop competing for the same corner; closing builds it back on the same transition. The leading edge lights up in primaryColor for the duration of the move (.kmoving) β€” it is pinned to the pill's own right edge, so the width transition carries it with no second animation to keep in sync. Unread lights the separator itself and breathes it (kattend, 3.2s) rather than adding a badge. Everything is gated on .kpill: with no theme, or any value other than the literal "pill", the launcher is byte-identical to today (asserted by a control run). Honours prefers-reduced-motion β€” the states survive, the motion does not.

Fixed

  • Widget: the notification ding was unreachable for anyone whose visitors interact with the page before the widget. Audio is gated on user interaction and the unlock was armed from a single listener on the widget's own host, so a visitor who had been using the page for ten minutes but had not yet touched the launcher got silence β€” and an operator running a custom launcher outside that host got silence permanently. The first pointerdown anywhere in the document now arms it (once, capture), in addition to the existing host listener.
  • Widget: unread no longer forgets itself on navigation. kunread was a class with a one-page lifetime, so an inbound message that arrived while the visitor was reading something else vanished the moment they refreshed or moved on. It is now remembered under krispy_unread_<tenant>, restored on boot when the panel is closed, and cleared by opening and nothing else β€” the same contract the class already had, made durable. localStorage failures are swallowed (Safari private mode, blocked cookies): the fallback is exactly today's behaviour.

Added

  • Edge: kbase injection β€” TenantConfig.kbSources (KbSource[]: { id, name, text, updatedAt }, total text still hard-capped at 100K chars) is now assembled into the system prompt at chat time as a ## Knowledge block (runtime assembly, not compile-on-save), between the persona and the forms/guardrail contracts. Unset β†’ the prompt is byte-for-byte what it is today. detectPromptLeak is narrowed to the instruction portion (the same prompt without the knowledge block) so a bot quoting its own KB verbatim is no longer false-flagged as a prompt leak and forced into a handoff.
  • Edge: relearning from handoffs β€” off by default: it runs only for a tenant already using the knowledge base (β‰₯1 kbSource), so a tenant that never configured a KB incurs zero billable AI on handback and behaves exactly as today. When a handed-off session hands back to the AI (operator resolves it, or the silence alarm fires) and it had β‰₯1 operator message, the SessionDO makes one Workers-AI call (metered under ai) to extract at most one Qβ†’A the human answered that the bot couldn't, and appends it as a KbSuggestion ({ id, question, answer, createdAt }) under its own per-site KV key suggestions:<ns(t,s)> (cap 20 FIFO; normalized-question dedup against pending suggestions and approved kbSources). A separate key means the background write can never race a human config save. Failures are swallowed β€” relearning never breaks resolve. The DO now persists its tenantId+siteId write-once on the POST /context read (the handback fires from an alarm with no request in flight and the DO can't derive them from its own name). TenantConfig.kbVersion bumps on any kbSources write.
  • Edge: approval inbox routes (secret-authed, ?s=/siteId scoped like the other config routes) β€” GET /api/tenant/kb-suggestions?t=&s= (list pending), POST /api/tenant/kb-approve { tenantId, siteId?, id } (move into kbSources + bump kbVersion; 404 on unknown id), POST /api/tenant/kb-dismiss { tenantId, siteId?, id } (drop it). Suggestions are never projected to the public widget config. Human-in-the-loop by design: the bot never self-modifies its KB.
  • CLI: self-host parity for the approval inbox β€” krispy kb-suggestions (list pending), krispy kb-approve <id> (append to kbSources + bump kbVersion), krispy kb-dismiss <id> (drop it), over the secret-authed edge routes above. Each takes an optional --site <id> flag (β†’ ?s= / siteId) to scope a single site.
  • Multi-site (edge + widget foundation) β€” one account can run multiple sites, each with its own config blob (theme, persona, connectors, forms, popups, kbase) and liveness. A site is an optional ?s=<site> (query) / data-site (embed) / siteId (chat & lead body) that suffixes the tenant's KV namespace via ns(t, s). An absent or default site collapses to the exact legacy key β€” every existing tenant is untouched and nothing migrates (asserted by test). siteId is charset-guarded (/^[a-z0-9_-]{1,40}$/) at the trust boundary before it enters a :-delimited key; malformed β†’ 400. Config-consuming routes (/api/widget/config, /api/chat, /api/lead, /api/tenant/config GET+POST, /api/tenant/liveness) resolve it; conversations (session/thread/DO) and billing (usage/entitlement) stay keyed by tenantId alone β€” pooled quota per account, per-site conversations deferred. The cloud dashboard site-switcher + site table land next.

Added

  • Widget liveness β€” the widget's boot-time GET /api/widget/config now doubles as a heartbeat: the edge stamps a per-tenant last-seen record (timestamp + the embedding page's origin/url from Origin/Referer), throttled in-isolate to stay well under KV's write budget and never blocking the boot. New secret-authed GET /api/tenant/liveness?t=<tenant> returns { seen } so the dashboard can show "live on example.com β€” last seen 2m ago". Stores last-seen only for now (per-origin set + per-site keying are the multi-site upgrade path).

Added

  • Edge: new WidgetTheme knobs β€” glowColor, tagline, sparkle, direction, popupText, and timing (WidgetTiming: launcherDelayMs/sparkleAfterMs/popupDelayMs/popupCooldownHrs/autoOpenMs) β€” projected through the public GET /api/widget/config whitelist. All default unset/off: a tenant that configures nothing gets today's neutral widget unchanged.
  • Edge: hard write caps on POST /api/tenant/config (trust boundary; invalid configs never reach KV) β€” theme.avatar ≀48KB + scheme check (buttr | https:// | data:image/(png|webp|jpeg);base64,), connector CTA urls https-only, free-text theme.tagline/theme.popupText ≀500 chars, kbSources total text ≀100K chars. Size overruns β†’ 413; malformed values β†’ 400.
  • Edge: TenantConfig gains persona (PersonaSpec: toneOfVoice + styleRules[]), script (ConversationScript: opening[] + starters[]), and popups (PopupSpec[]: timer/section-proximity teaser engine; theme.popupText is sugar for one timer popup). All default unset β€” the bot speaks/opens exactly as today until configured.
  • Edge: the Connector union grows CTA fields (cta opt-out, label, caption, showAfterMs) and new types (phone, facebook, tiktok, link, each with a server-built url/tel:/wa.me href). cta: false keeps an otherwise-CTA connector delivery-only.
  • Edge: GET /api/widget/config projection now also serves ctas (CTA-capable connectors minus cta:false opt-outs, with server-built hrefs + default labels), script (opening ≀5 / starters ≀4), and popups (or the popupText sugar). persona is structurally excluded β€” it is instruction text folded into the system prompt only.
  • Edge: buildSystemPrompt folds persona into the prompt (## Voice + ## Style rules) between the tenant instructions and the guardrails, so it sits inside the detectPromptLeak scope (a bot reciting its own tone verbatim counts as a leak).
  • Edge: additional POST /api/tenant/config write caps β€” popups ≀8 entries, popup text ≀500 chars, popup selectors (near selector / cancelOnClick) ≀200 chars, script.opening ≀5, script.starters ≀4, and persona+script combined free text ≀8K chars (all 413).
  • Edge: GET /api/widget/config now sends Cache-Control: public, max-age=60 β€” the boot config grew to ~10–30KB with data-URI avatars and was refetched uncached on every page load; 60s keeps edits near-live.
  • Widget: the new theme knobs come alive β€” primaryColor now drives the visitor bubble, send button, and input focus ring (was a dead CSS var); launcherColor fills a badge circle behind the launcher mascot; glowColor adds an opt-in launcher glow (no glow layer at all when unset); sparkle adds a 10s idle shadow-swell + conic-ring loop after sparkleAfterMs; tagline replaces the header sub-line; direction: "rtl" flips the panel (bubble corners, input dir, mirrored send icon); timing.launcherDelayMs > 0 hides the launcher then plays a one-time entrance pop (skipped on revisit via sessionStorage); timing.autoOpenMs (opt-in; default 0 = never) auto-opens a closed panel after an inbound reply. All animations respect prefers-reduced-motion.
  • Widget: proactive timer popup β€” theme.popupText shows a dismissible teaser card above the launcher after timing.popupDelayMs (default 8s) with a per-tenant timing.popupCooldownHrs cooldown (default 24h) in localStorage; suppressed while the panel is open; clicking it opens the chat. Unset = nothing ever shows.
  • Widget: avatars accept data:image/… URIs in addition to "buttr" and https URLs (shared isRenderableAvatar() gate).
  • Edge/widget: FormSpec.successText β€” the line the submitted lead-form card collapses to in the transcript (widget default: "Thanks β€” we'll be in touch.").
  • Edge: lead emails set Resend reply_to to the lead's captured email (the form's email-typed field) β€” the tenant hits Reply and talks to the lead; omitted when no email was captured.
  • Edge: Telegram quiet ops β€” routine mirrors post silently (disable_notification), and a handoff @mentions the tenant's operators (via text_mention entities, no public username needed) so notifications fire only when a human is needed. New TenantConfig.operators (auto-learned from topic replies, capped at 10, never exposed to the public widget config).
  • Tilt: KRISPY-CORE banner resource in its own capitalized label group β€” names the dev dashboard (Tilt has no native project title).
  • Chat suite: lead capture + connectors β€” edge fan-out and a data-driven widget lead form.
  • Chat suite: widget theming β€” :host --k-* CSS vars, boot-fetch of tenant config, avatar/greeting/position, CSS-boundary sanitizers.
  • Chat suite: keyboard-aware floating widget card on mobile (visualViewport pin, safe-area, 16px inputs).
  • Chat suite: message notifications β€” WebAudio ding, launcher pulse, unread dot, visitor mute (theme.sound).
  • Chat suite: safe minimal markdown in bot/operator bubbles (bold/italic/code/links, XSS-safe; visitor text stays literal).
  • Chat suite: security guardrails β€” always-appended SECURITY_INSTRUCTION in the system prompt (refuses prompt/architecture/secret disclosure, injection resistance); survives custom tenant prompt overrides.
  • Docs site: Fumadocs documentation site under apps/docs (quickstart, concepts, security, guides, reference for edge routes / tenant config / CLI / markers).
  • Governance: CHANGELOG.md + documentation-sync rule (AGENTS.md Β§7).
  • CI: build gate β€” CI now builds apps/docs (present-guarded) after typecheck/test.
  • API contract: api-collection/openapi.yaml (OpenAPI 3.1) covering the edge Worker's full HTTP surface; new Bruno requests for /api/chat, /api/contact, /api/lead, /api/widget/config, /api/usage. AGENTS.md Β§7 now requires OpenAPI + Bruno on any route change.
  • Deploy: Tilt deploy:* manual resources + ./deploy.sh <edge|docs|widget> <preview|production> (preflight β†’ build β†’ wrangler deploy β†’ smoke) with scripts/cf-deploy-preflight.mjs + scripts/cf-deploy-smoke.mjs; named preview/production wrangler envs for the edge Worker. Cloudflare creds sourced from Infisical-fed .env.local, never GitHub Actions.
  • CLI: krispy logo <file> β€” removes a logo's background locally (no cloud, no API key) via corner chroma-key and prints a paste-ready data:image/png;base64,… avatar URI for theme.avatar (self-host parity for the cloud dashboard's AI bg-removal). Handles png/jpg/webp/svg, downscales to 144px; leaves already-transparent or busy-background images untouched. Depends on sharp (lazy-loaded so the CLI core stays dep-free at import).
  • Release: @krispyai/cli is now publishable (0.1.0, public) via npm Trusted Publishing (.github/workflows/publish.yml, OIDC, no npm token); other packages stay private. Founder setup + first-publish bootstrap documented in AGENTS.md Β§Β§10–11.
  • Domains: krispyai.com β€” prod hostnames edge.krispyai.com / docs.krispyai.com / widget.krispyai.com as deploy.sh smoke defaults, OpenAPI prod server, and embed-snippet examples. Custom domains attached in the CF dashboard after the first deploy (NS transfer pending); no hostname is hardcoded in runtime source.

Changed

  • Widget: handoff-without-form now renders a built-in default contact FormSpec through the one lead-form renderer (posts /api/lead); the legacy static .cap markup + showCapture() path is gone. /api/contact stays as an edge shim for already-deployed widgets.
  • Edge hardening per security audit: self KV-config merge, fetch timeouts, Telegram mirror best-effort, WS backoff cap, lead rate-limit, DO internal auth.

Fixed

  • Widget: a tenant avatar now reaches the floating launcher badge too β€” applyTheme set only the header avatar's src, so the launcher always kept the default mascot no matter what the tenant configured.
  • Widget: the lead form is no longer a sticky band pinned between the log and the composer β€” it renders inside the message log as a bubble-style card that scrolls with the conversation (scrollIntoView on insert) and collapses in place to a compact record after submit.
  • CI: gitleaks runs via the free CLI image (the GitHub action needs a paid org license β€” was red on every PR, blocking dependabot).
  • README: real Buttr hero image (was a leftover builders-stack architecture diagram).
  • Dev servers run on fixed ports β€” edge (wrangler) + widget; portless is an alias only.
  • Docs deploy: apps/docs now fully static-exports (output: 'export') and ./deploy.sh docs uploads out/ to CF Pages directly β€” replaces the broken @cloudflare/next-on-pages path (which required runtime = 'edge' on /api/search + /docs/[[...slug]] and then failed to edge-bundle lib/source.ts). Search switched to the build-time Orama static index (staticGET + search.type: 'static'); smoke now also checks /api/search for the docs site.