The official client SDK for the Diosc AI Assistant Kit — a typed, namespaced API for embedding and driving the Diosc chat assistant on your web app.
Zero runtime dependencies. Framework-agnostic (React, Vue, Angular, vanilla).
A few things shape the entire API:
- The kit owns the chat UI. When you embed Diosc, the assistant-kit web components are loaded from DioscHub at runtime and render the whole chat experience. You don't build a chat UI; you configure one and wire it to your app.
- Two display layouts, one widget. The same
<diosc-chat>renders either as a FAB (floating bubble, default) or embed (inline panel). There is no headless mode — the SDK never asks you to render messages, sessions, or approvals yourself. - Two planes of integration:
- Control — drive the widget (connect, send, open/close).
- Extend — give the AI host capabilities (tools, mentions, page context, custom approval UI, navigation).
- Identity is BYOA (Bring Your Own Auth). Diosc never sees your credentials. Your app authenticates the user and binds that identity to the connection server-to-server. There is no auth-header / token API in this SDK by design.
This package gives you two entry points:
| Entry point | Use when |
|---|---|
createDiosc() |
The supported, fully-typed instance API. Use this. |
loadDiosc() |
Lower-level script injector + raw command queue. Use only if you need the gtag-style global directly. |
npm install @dioschub/clientimport { createDiosc } from '@dioschub/client';
const diosc = createDiosc({
apiKey: 'ak_xxx', // public embed key (like a Maps key)
backendUrl: 'https://hub.example.com',
});
await diosc.ready; // optional — resolves when the kit loads
diosc.on('stream:chunk', (c) => console.log(c));
diosc.send('Hello!');Then render the widget anywhere in your page:
<diosc-chat></diosc-chat>That's the whole happy path. Everything below is opt-in.
The kit auto-mounts <diosc-chat>. You can configure it via createDiosc(...)
(recommended) or with attributes:
<!-- FAB (default) -->
<diosc-chat></diosc-chat>
<!-- Inline embed panel -->
<diosc-chat mode="embed"></diosc-chat>
<!-- Fully attribute-driven (no JS) -->
<diosc-chat
api-key="ak_xxx"
backend-url="https://hub.example.com"
bind-endpoint="/api/diosc/bind">
</diosc-chat>| Attribute | Maps to |
|---|---|
api-key |
apiKey |
backend-url |
backendUrl |
assistant-id |
assistantId (auto-detected if omitted) |
bind-endpoint |
bindEndpoint (see Identity) |
mode |
"embed" for inline; omit for FAB |
createDiosc(options) returns a DioscInstance with a small core plus three
namespaces: ui, extend, identity.
options is your DioscConfig plus two loader controls:
| Option | Type | Default | Notes |
|---|---|---|---|
apiKey |
string |
— | Public embed key. |
backendUrl |
string |
— | DioscHub URL. |
assistantId |
string |
auto | Resolved from config fetch if omitted. |
bindEndpoint |
string |
— | Your server endpoint for BYOA bind. |
autoConnect |
boolean |
true |
Connect on init. |
verbose |
boolean |
false |
Verbose logging. |
reconnectAttempts / reconnectDelay / reconnectDelayMax / connectionTimeout |
number |
— | Transport tuning. |
scriptUrl |
string |
derived | Custom kit script URL. |
autoLoad |
boolean |
true |
Set false if the loader <script> is already on the page. |
await diosc.ready; // Promise<void> — kit script loaded
await diosc.connect(); // open the connection (no-op if autoConnect)
diosc.disconnect();
await diosc.send('Summarize my order', { pageContext: { orderId } });
diosc.cancelStream(); // drain the in-flight response
const off = diosc.on('stream:chunk', (chunk) => render(chunk));
off(); // unsubscribe
diosc.onAny((event, payload) => log(event, payload));
await diosc.fetchAssistantConfig(); // re-fetch public assistant configAttachments caveat:
send(text, { attachments })accepts anattachmentsfield for forward-compatibility, but it is not wired end-to-end yet — today it is a no-op (file sends go through the composer UI). See Known gaps.
diosc.ui.open();
diosc.ui.close();
diosc.ui.toggle();
diosc.ui.setPosition('bottom-left'); // 'bottom-left' | 'bottom-right'
// Change listeners (fire immediately with the current value, then on change):
const offOpen = diosc.ui.onOpenChange((isOpen) => syncMyButton(isOpen));
const offPos = diosc.ui.onPositionChange((pos) => layout(pos));This is the high-value plane. Everything here is optional.
diosc.extend.tool('navigate', async (params) => {
router.push(params.path);
return { navigatedTo: params.path };
});The host filter is authoritative — return exactly what the popover should
show. Selecting an item serializes to the wire format @[Name](kind:id), where
id is what the LLM sees.
const people = [{ id: 'u_1', name: 'Ada', kind: 'user' }];
diosc.extend.mentions((needle) =>
people.filter((p) => p.name.toLowerCase().includes(needle.toLowerCase())),
);
diosc.extend.mentions(null); // clearThe adapter gives the AI a fresh page snapshot each turn plus a set of intents it may invoke. Each intent can declare a client-local approval gate that the AI cannot bypass.
diosc.extend.browser({
read: async () => ({ url: location.href, title: document.title, data: getState() }),
intents: [
{
name: 'apply_coupon',
description: 'Apply a discount coupon to the cart',
schema: { type: 'object', properties: { code: { type: 'string' } }, required: ['code'] },
handler: async ({ code }) => {
await cart.applyCoupon(code);
return { success: true, data: { code } };
},
// Optional: gate behind a local approval dialog before running
approval: {
severity: 'medium',
summary: ({ code }) => `Apply coupon ${code}?`,
},
},
],
});The kit ships the consensus (approval) dialog and always owns the decision. What you can replace is the dialog's detail body, per tool, so a gated call reads in your domain's terms instead of a generic schema form:
const off = diosc.extend.consensusView(/acme-helpdesk_/, {
mount(el, ctx) {
// `el` is a light-DOM anchor — your stylesheets apply. Portal into it from
// your own component tree to keep context/providers.
render(<TicketBody ctx={ctx} />, el);
},
update(ctx) { /* operator edited a field, or switched request */ },
unmount() { /* teardown */ },
});ctx.fields is the kit's computed diff (render from it, don't re-derive), and
ctx.setField(key, value) reports an edit, which becomes modifiedArgs on
submit. The descriptor has no approve/reject — see below.
There is intentionally no
diosc.approve()/reject(), and no host-rendered approval surface at all. Approval decisions resolve only from the built-in consensus dialog. This preserves the human-in-the-loop (Responsibility-First) guarantee.
diosc.extend.observeNavigation((notify) => {
const stop = router.afterEach((route) => notify({ path: route.path }));
return stop; // return a cleanup fn (optional)
});Diosc never receives your credentials. Your app authenticates the user and binds that identity to the live connection server-to-server.
Flow:
- You set
bindEndpoint(a route on your server). - When the connection needs identity, the kit
POSTs{ wsId }to yourbindEndpointwithcredentials: 'include'(so your auth cookie rides along). - Your endpoint authenticates the user and forwards the identity to DioscHub's
POST /auth/bind. The bound identity (and feature flags) flow back to the kit.
const diosc = createDiosc({
apiKey: 'ak_xxx',
backendUrl: 'https://hub.example.com',
bindEndpoint: '/api/diosc/bind',
});
// If your auth lives in JS (e.g. a Bearer token) rather than a cookie,
// supply extra headers for the bind request:
diosc.identity.setBindHeaders(() => ({ Authorization: `Bearer ${getToken()}` }));
// After the user signs in (e.g. anonymous → authenticated), rebind in place:
diosc.identity.reauth();A minimal bindEndpoint on your server:
// POST /api/diosc/bind body: { wsId: string }
app.post('/api/diosc/bind', requireAuth, async (req, res) => {
const { wsId } = req.body;
// Forward the authenticated identity to DioscHub.
await fetch(`${HUB_URL}/auth/bind`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
wsId,
identity: { userId: req.user.id, username: req.user.name, role: req.user.role },
}),
});
res.sendStatus(204);
});See the DioscHub docs for the authoritative bind protocol, including feature flags and REST-token rotation.
Subscribe with diosc.on(event, handler) (typed) or diosc.onAny(handler).
| Event | Payload (shape) | Meaning |
|---|---|---|
stream:start |
{ … } |
Assistant began responding |
stream:chunk |
{ … } |
Streamed content delta |
stream:end |
{ … } |
Response complete |
tool:started |
{ toolCallId, toolName, toolParameters } |
A tool call started |
tool:completed |
{ toolCallId, toolName, durationMs, resultSummary } |
Tool call finished |
tool:failed |
{ toolCallId, toolName, errorMessage } |
Tool call failed |
approval:request |
{ toolCalls, … } |
A human decision is required |
content_blocked |
{ reason, message } |
Output guardrail tripped |
session:started / joined / loaded / restored |
{ session, … } |
Session lifecycle |
session:renamed / pinned |
{ sessionId, … } |
Session list change |
auth:refreshed / auth:failed |
{ … } |
Identity bind lifecycle |
files:updated |
{ fileId?, files? } |
File set changed |
browser:read_page |
{ … } |
Kit requested a page snapshot |
ProtocolEventName is the full typed union; on() also accepts any string for
forward-compat.
Short-lived AI triggers (one-shot, non-chat calls like generate a description from the selected items) are out of scope for this SDK. Today they belong on your backend. If introduced later, they will ship as UI components, not as a bare client method. See the DioscHub docs.
import { useEffect, useRef } from 'react';
import { createDiosc, type DioscInstance } from '@dioschub/client';
export function Assistant() {
const ref = useRef<DioscInstance>();
useEffect(() => {
const diosc = createDiosc({
apiKey: import.meta.env.VITE_DIOSC_KEY,
backendUrl: import.meta.env.VITE_DIOSC_URL,
bindEndpoint: '/api/diosc/bind',
});
ref.current = diosc;
diosc.extend.tool('navigate', async ({ path }) => {
window.history.pushState({}, '', path);
return { navigatedTo: path };
});
const off = diosc.on('approval:request', () => toast('Action needs approval'));
return () => off();
}, []);
return <diosc-chat />;
}<template><diosc-chat /></template>
<script setup lang="ts">
import { onMounted } from 'vue';
import { createDiosc } from '@dioschub/client';
onMounted(() => {
const diosc = createDiosc({
apiKey: import.meta.env.VITE_DIOSC_KEY,
backendUrl: import.meta.env.VITE_DIOSC_URL,
});
diosc.extend.observeNavigation((notify) =>
router.afterEach((r) => notify({ path: r.path })),
);
});
</script>import { Injectable } from '@angular/core';
import { createDiosc, type DioscInstance } from '@dioschub/client';
@Injectable({ providedIn: 'root' })
export class DioscService {
readonly diosc: DioscInstance = createDiosc({
apiKey: environment.dioscKey,
backendUrl: environment.dioscUrl,
bindEndpoint: '/api/diosc/bind',
});
constructor() {
this.diosc.identity.setBindHeaders(() => ({
Authorization: `Bearer ${this.auth.token}`,
}));
}
}<script type="module">
import { createDiosc } from 'https://esm.sh/@dioschub/client';
const diosc = createDiosc({ apiKey: 'ak_xxx', backendUrl: 'https://hub.example.com' });
diosc.ui.setPosition('bottom-left');
</script>
<diosc-chat></diosc-chat>If you need the gtag-style global directly (or are migrating older code),
loadDiosc() injects the script and returns the raw command function and a
ready promise. The raw doorway is also reachable via instance.raw.
import { loadDiosc } from '@dioschub/client';
const { diosc, ready } = loadDiosc({ backendUrl: 'https://hub.example.com', apiKey: 'ak_xxx' });
diosc('config', { autoConnect: true }); // commands buffered until the script loads
await ready;createDiosc() is built on top of loadDiosc() — prefer the instance API; reach
for the raw doorway only as an escape hatch.
These omissions are deliberate design decisions, not gaps:
- No headless mode. The kit renders the chat UI (FAB/embed). You never build message lists, session pickers, or the approval dialog yourself.
- No host session API.
loadSession/startNewSession/renameSession/ etc. are driven by the kit's own session-history panel, not host code. - No
approve/rejectglobal, and no host-rendered approval surface. Decisions resolve only from the kit's own consensus dialog. You may restyle its body viaextend.consensusView— that carries no verdict channel — Responsibility-First. - No auth-header / token API. Identity is bound server-to-server via
bindEndpoint. The credential-blind principle means Diosc never sees tokens.
See the DioscHub docs for the full rationale.
| Gap | Status |
|---|---|
send(text, { attachments }) |
attachments is accepted but a no-op (engine invoke doesn't forward it yet). |
DioscInstance, CreateDioscOptions, DioscUi, DioscExtend, DioscIdentity,
SendOptions, WidgetPosition, DioscConfig, BoundIdentity, BoundRole,
NavigationData, NavigationObserverCallback, BrowserToolHandler,
BrowserAdapter, PageSnapshot, IntentDefinition, IntentApproval,
IntentResult, JsonSchema, MentionItem, MentionQuery, ApprovalRequest,
ApprovalToolCall, ApprovalActions, ApprovalValidationError,
ApprovalHandler, SessionEventName, ServerEventName, ProtocolEventName,
EventHandler, WildcardEventHandler, Unsubscribe, DioscCommand,
DioscFunction, DioscEngineHandle, LoadDioscOptions, LoadDioscResult.
MIT