@kelu/sdk is the JavaScript client for Kelu. It streams answers with citations, runs search and records feedback. It works in browsers, React Native and Node.js 18 or later, and ships ESM and CommonJS builds with types.
@kelu/sdk is not on npm yet, so npm install @kelu/sdk fails today. Until it is published, embed the website widget or call the REST API directly.

Example

import { Kelu } from "@kelu/sdk";

const kelu = new Kelu({
  knowledgeBaseId: "YOUR_KNOWLEDGE_BASE_ID",
  clientKey: "kl_pk_...",
});

const { answer, citations, threadId, messageId } = await kelu.chat(
  "How do I authenticate?",
  { onToken: (t) => render(t) },
);

await kelu.chat("And with SSO?", { threadId });     // follow-up, same conversation
await kelu.feedback({ messageId, rating: "up" });   // rate the first answer
On a server, pass clientId and clientSecret instead of clientKey. Create either kind of key under the knowledge base’s Integrations → Client Keys.

Options

new Kelu(options)
OptionTypeDefaultDescription
knowledgeBaseIdstringrequiredThe knowledge base every request goes to
clientKeystringnonePublic key (kl_pk_…), for browsers and React Native
clientId, clientSecretstringnoneSecret key pair (kl_ci_…, kl_cs_…), for servers only
baseUrlstringhttps://app.kelu.devAPI base URL. The WebSocket URL follows it
transport"auto" | "ws" | "sse""auto"How answers stream. See Transport
widgetIdstringnoneA widget integration id. Answers use that widget’s configured voice, and each question must pass that widget’s Enabled switch and Allowed origins
getCaptchaToken() => Promise<string>noneReturns a reCAPTCHA v3 token, sent on every request. Return "" to send none
A clientSecret in a browser bundle is visible to every visitor. The client logs a warning when it finds one in a browser. Use clientKey there.

Methods

MethodReturnsDescription
chat(query, options?)Promise<ChatResult>Ask a question and stream the answer. Resolves when the answer is complete
search(query, options?)Promise<SearchResponse>The best-matching passages. No answer is generated
feedback({ messageId, rating, comment? })Promise<void>Rate an answer "up" or "down". A second rating replaces the first
clearFeedback(messageId)Promise<void>Remove the rating on an answer

chat() options

OptionTypeDescription
threadIdstringContinue an earlier conversation
groupIdsstring[]Answer only from these source groups. Can narrow the key’s allowed groups, never widen them
user{ email?, id? }Who is asking. The conversation is attributed to id, or to email when there is no id
signalAbortSignalCancel the request
onToken(token: string) => voidCalled for each piece of the answer as it streams
onCitations(citations: Citation[]) => voidCalled once, at the end, with the cited sources
onAnswer(answer: string) => voidCalled at the end when Kelu renumbered the citation markers. Replace the streamed text with this full answer
getCaptchaToken() => Promise<string>Overrides the client option for this call

search() options

OptionTypeDescription
limitnumberResults to return. Default 10, max 100
groupIdsstring[]Search only these source groups

Types

ChatResult
FieldTypeDescription
answerstringThe full answer, with final citation numbers
citationsCitation[]The sources the answer cited
threadIdstringPass back as threadId to continue the conversation
messageIdstringPass to feedback(). Empty when the workspace keeps no chat data
Citation
FieldTypeDescription
titlestringDocument title
urlstringDocument URL
snippetstring?The cited passage
indexnumber?The [n] marker the answer uses for this source. Label with this, not the position in the array
SearchResult (returned as { results: SearchResult[] })
FieldTypeDescription
contentstringThe matching passage
url, titlestringThe document it comes from
scorenumberRelevance score. 0 when no reranking ran
heading_pathstring?Section breadcrumb, for example Authentication › OAuth

Transport

ModeBehaviour
"auto" (default)Uses a WebSocket. If it fails to connect within 5 seconds, this question and every later one on the client go over SSE
"ws"WebSocket only. A connection failure throws
"sse"SSE only
Runtimes without a global WebSocket (Node.js before 22) always use SSE. If the server goes silent for 60 seconds after accepting a question, chat() fails with timeout. The question is not resent, because it may already have been answered and counted.

React Native

"auto" streams over React Native’s WebSocket. On the SSE path, React Native’s fetch does not stream: onToken and onCitations do not fire, but the resolved ChatResult is complete. To stream over SSE, import a polyfill before any SDK call:
npm install react-native-fetch-api web-streams-polyfill
// index.js
import "web-streams-polyfill/polyfill";
import "react-native-fetch-api/polyfill";

Errors

Every failure throws KeluError.
import { KeluError } from "@kelu/sdk";

try {
  await kelu.chat("...");
} catch (err) {
  if (err instanceof KeluError && err.retryable) showRetryButton();
}
PropertyTypeDescription
messagestringA sentence you can show the reader
codestringOne of the codes below. Empty for a network failure
statusnumberHTTP status. 0 for a network failure. 200 when the answer failed after it started streaming
retryablebooleanWhether asking again could succeed
requestIdstring?Quote it when you contact support
retryAfterSecondsnumber?How long to wait, when rate limited
CodeRetryableMeaning
bad_requestnoEmpty question, question over 4,000 characters, invalid body, or unknown thread
unauthorizednoThe key is missing, invalid or revoked, or the page’s origin is not on the key’s allowlist (status 403)
forbiddennoThe key belongs to another knowledge base, or a groupIds entry is not allowed for this key
captcha_required, captcha_failednoThis deployment requires reCAPTCHA for public keys, and no valid token was sent
quota_exceedednoThe workspace has used its monthly question allowance
not_foundnofeedback() named a message that does not exist
rate_limitedyesThe key’s rate limit was hit
unavailableyesThe model provider is temporarily unavailable
timeoutyesThe answer took too long
misconfigurednoThe model provider rejected its credentials. An admin has to fix it
content_filteredyesThe model withheld part of the answer under a safety filter
truncatedyesThe answer was cut off
cancelledyesThe request was cancelled before the answer was ready
internalyesAny other server error
connection_closedyesThe connection dropped before the answer arrived
abortedyesYou cancelled through signal
An internal knowledge base has no public surface. Requests to one fail with status 403 and an empty code.