App SDK
The @neboai/app-sdk package provides everything your app's frontend needs to communicate with Nebo -- persistent storage, agent invocation, embedded chat, direct LLM calls, structured event surfaces, and WebSocket connections.
Installation
For React, Vue, Svelte, Solid, or any bundled framework:
npm install @neboai/app-sdk
import { nebo } from '@neboai/app-sdk';For HTMX or vanilla HTML, include the global script:
<script src="/sdk/nebo.global.js"></script>
<script>
// nebo is available as a global
const data = await nebo.storage.getItem('contacts');
</script>Storage
Scoped key-value store that persists across app restarts. Each app gets its own isolated namespace.
// Write a value
await nebo.storage.setItem('lastView', 'dashboard');
// Read a value
const view = await nebo.storage.getItem('lastView');
// Remove a value
await nebo.storage.removeItem('lastView');
// List all keys
const allKeys = await nebo.storage.keys();
// Clear everything
await nebo.storage.clear();| Method | Signature | Description |
|---|---|---|
setItem | (key: string, value: any) => Promise<void> | Store a value |
getItem | (key: string) => Promise<any | null> | Retrieve a value |
removeItem | (key: string) => Promise<void> | Delete a value |
keys | () => Promise<string[]> | List all keys |
clear | () => Promise<void> | Remove all keys |
Identity
Know who you are -- fetch the agent's name, persona, skills, and configured inputs.
const agent = await nebo.identity.get();
// => { id, name, displayName, description, persona, model, skills, inputValues }
// Clear cached identity (re-fetches on next get())
nebo.identity.invalidate();| Field | Type | Description |
|---|---|---|
id | string | Agent ID |
name | string | Agent name from AGENT.md |
displayName | string | Human-readable name (window title > first heading > name) |
description | string | One-line description |
persona | string | AGENT.md body (markdown after frontmatter) |
model | string | Configured model (e.g. "claude-sonnet-4-20250514") |
skills | string[] | Installed skill names |
inputValues | object | User-configured input values |
Agent Invocation
Invoke any agent in the user's workspace. By default, invocations target the app's own agent. Pass an agent option to invoke a different one.
// Synchronous (wait for full response)
const { text } = await nebo.agents.invoke('Analyze deal #42');
// With options
const result = await nebo.agents.invoke('Summarize report', {
agent: 'other-agent', // Override: invoke a different agent
data: { reportId: 42 } // Structured context passed to the agent
});
// Streaming
for await (const chunk of nebo.agents.stream('Summarize pipeline')) {
output.textContent += chunk.text;
}| Method | Returns | Description |
|---|---|---|
invoke | Promise<{ text, tools }> | Send a message and wait for the full response |
stream | AsyncGenerator<{ text, done }> | Stream a response incrementally |
Embedded Chat
Mount the full Nebo chat UI inside your app. The chat renders in an iframe with full feature parity -- streaming, slash commands, tool visualization, voice, @mentions, ask widgets, markdown, code blocks. Works in any framework.
// Mount chat into a DOM element
nebo.chat.mount(document.getElementById('chat'), {
placeholder: 'Ask about your ads...', // Custom placeholder
theme: 'auto', // 'auto' | 'light' | 'dark'
height: '400px', // CSS height
borderless: false, // No border/shadow
contextId: currentDoc.id, // Scope session per document
scope: 'read' // Tool scoping (see Apps Overview)
});
// Programmatic control
nebo.chat.send('Summarize today\'s performance');
nebo.chat.newThread();
// Listen for events from the chat
const unsub = nebo.chat.onMessage((msg) => {
if (msg.type === 'nebo:response-complete') {
updateDashboard(msg.text);
}
});
// Cleanup
nebo.chat.unmount();Chat Context
The embedded chat is an iframe -- it has no visibility into your app's state. Without context, when a user asks "tell me about this file," the agent has no idea what "this file" refers to. Use chat.setContext() to inject app state as invisible context into every agent request.
// Call setContext whenever the user's view changes
nebo.chat.setContext({
projectId: currentProject.id,
displayedDoc: { filename: 'contract.pdf', documentId: 'doc-123' },
attachedDocuments: selectedDocs.map(d => ({
filename: d.filename,
documentId: d.id,
})),
route: '/projects/abc/documents',
});
// Clear context when user navigates away
nebo.chat.setContext(null);| Field | Type | Description |
|---|---|---|
projectId | string | Current project the user is viewing |
displayedDoc | { filename, documentId } | Document currently visible in the viewer |
attachedDocuments | { filename, documentId }[] | Documents explicitly selected/attached |
route | string | Current page/route within the app |
[key: string] | unknown | Arbitrary app-specific data |
All fields are optional. Include only what's relevant to your app. The agent receives the context as App context: { ... } prepended to the request.
Document-Scoped Sessions (contextId)
By default, all chats share one session per agent. For apps that display different content (documents, projects, records), pass contextId to scope each conversation:
// Each document gets its own persistent chat history
nebo.chat.mount(chatContainer, {
contextId: document.id // session key: agent:{id}:app:{contextId}
});When the user switches documents, unmount and remount with the new contextId. Each context maintains its own conversation -- messages from one document don't leak into another.
"memory": { "context_isolated": true } to agent.json.Direct LLM Calls (Janus)
Call the LLM directly -- no agent persona, no memory, no tool use. Useful for one-off text processing, classification, or generation that doesn't need the full agent pipeline.
const summary = await nebo.janus.complete({
messages: [{ role: 'user', content: 'Summarize: ...' }],
model: 'claude-sonnet-4-20250514', // optional
temperature: 0.3, // optional
max_tokens: 1024, // optional
system: 'You are a concise editor.' // optional
});
// Streaming
for await (const chunk of nebo.janus.stream(options)) {
output.textContent += chunk;
}HTTP Proxy
Make CORS-free HTTP requests through Nebo's server. nebo.fetch() mirrors the native fetch() API.
// Relative URLs route to your sidecar
const resp = await nebo.fetch('/projects');
const projects = await resp.json();
// Absolute URLs route through Nebo's CORS-free proxy
const ext = await nebo.fetch('https://api.example.com/data');Surfaces API
Receives structured agent events without coupling to the Nebo chat UI. Use this when your app renders its own output and needs raw event data.
import { NeboSurfaces } from '@neboai/app-sdk';
const surfaces = new NeboSurfaces();
surfaces.connect();
surfaces.on('text_content', (e) => {
output.textContent += e.delta;
});
surfaces.on('state_snapshot', (e) => {
appState = e.snapshot;
rerender();
});
surfaces.on('state_delta', (e) => {
// RFC 6902 JSON Patch operations
applyPatch(appState, e.operations);
rerender();
});
// Send events back to the agent
surfaces.send('button_click', { buttonId: 'analyze' });| Event | Description |
|---|---|
run_started | Agent run began |
text_content | Incremental text delta from the agent |
tool_call_start | Agent invoked a tool |
state_snapshot | Full state replacement |
state_delta | Incremental state update (RFC 6902 JSON Patch) |
surface_create | New surface requested by the agent |
The SDK auto-maintains a shared state object updated from state_snapshot and state_delta events.
WebSocket
Direct WebSocket connection with auto-reconnect and exponential backoff (1s--30s).
import { NeboWebSocket } from '@neboai/app-sdk';
const ws = new NeboWebSocket();
// Connects to ws://{{base}}/ ws/app/{{appId}}You can also use the global SDK version:
const ws = new nebo.WebSocket('/apps/my-app/ws');
ws.addEventListener('message', (evt) => { ... });
ws.send('event-name', { key: 'value' });
ws.close();