/docs / apps / sdk

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();
MethodSignatureDescription
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();
FieldTypeDescription
idstringAgent ID
namestringAgent name from AGENT.md
displayNamestringHuman-readable name (window title > first heading > name)
descriptionstringOne-line description
personastringAGENT.md body (markdown after frontmatter)
modelstringConfigured model (e.g. "claude-sonnet-4-20250514")
skillsstring[]Installed skill names
inputValuesobjectUser-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;
}
MethodReturnsDescription
invokePromise<{ text, tools }>Send a message and wait for the full response
streamAsyncGenerator<{ 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);
FieldTypeDescription
projectIdstringCurrent project the user is viewing
displayedDoc{ filename, documentId }Document currently visible in the viewer
attachedDocuments{ filename, documentId }[]Documents explicitly selected/attached
routestringCurrent page/route within the app
[key: string]unknownArbitrary 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 isolation: By default, all contexts share the agent's memory pool. To isolate memories per context (so Client A's facts never appear in Client B's chat), add "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' });
EventDescription
run_startedAgent run began
text_contentIncremental text delta from the agent
tool_call_startAgent invoked a tool
state_snapshotFull state replacement
state_deltaIncremental state update (RFC 6902 JSON Patch)
surface_createNew 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();