diff --git a/dist/index.cjs b/dist/index.cjs
index 758bf0e..db7f08d 100644
--- a/dist/index.cjs
+++ b/dist/index.cjs
@@ -1,3 +1,3 @@
-"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});let i=null,l=null;function h(){if(i)return i;if(window.location.protocol==="neboapp:")return i=window.location.hostname,i;const s=window.location.pathname.match(/^\/apps\/([^/]+)\/ui/);if(s)return i=s[1],i;const e=document.querySelector('meta[name="nebo-app-id"]');if(e)return i=e.getAttribute("content")||"",i;throw new Error('[nebo-sdk] Cannot detect app ID. In production, serve from /apps/{id}/ui/. In dev, add to index.html.')}function u(){if(l)return l;if(window.location.protocol==="neboapp:")return l="http://localhost:27895",l;const s=document.querySelector('meta[name="nebo-base-url"]');return s?(l=s.getAttribute("content")||"",l):(l=window.location.origin,l)}function O(s){i=s}function $(s){l=s}async function E(s,e){const t=h(),n=u();if(s.startsWith("http://")||s.startsWith("https://")){const f=`${n}/api/v1/apps/${t}/http/proxy`,r={};e!=null&&e.headers&&new Headers(e.headers).forEach((C,A)=>{r[A]=C});const p={url:s,method:(e==null?void 0:e.method)||"GET",headers:r,body:e!=null&&e.body?String(e.body):void 0},d=await(await fetch(f,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(p)})).json();return new Response(d.body,{status:d.status,headers:d.headers})}const a=s.startsWith("/")?s:`/${s}`,o=`${n}/api/v1/apps/${t}/api${a}`;return fetch(o,e)}class v{constructor(e){this.ws=null,this.reconnectTimer=null,this.reconnectDelay=1e3,this.maxReconnectDelay=3e4,this._closed=!1,this.onopen=null,this.onmessage=null,this.onerror=null,this.onclose=null;const t=h(),a=u().replace(/^http/,"ws");this.url=`${a}/ws/app/${t}`,this.connect()}connect(){this._closed||(this.ws=new WebSocket(this.url),this.ws.onopen=e=>{var t;this.reconnectDelay=1e3,(t=this.onopen)==null||t.call(this,e)},this.ws.onmessage=e=>{var t;return(t=this.onmessage)==null?void 0:t.call(this,e)},this.ws.onerror=e=>{var t;return(t=this.onerror)==null?void 0:t.call(this,e)},this.ws.onclose=e=>{var t;(t=this.onclose)==null||t.call(this,e),this._closed||this.scheduleReconnect()})}scheduleReconnect(){this.reconnectTimer||this._closed||(this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.reconnectDelay=Math.min(this.reconnectDelay*2,this.maxReconnectDelay),this.connect()},this.reconnectDelay))}get readyState(){var e;return((e=this.ws)==null?void 0:e.readyState)??WebSocket.CLOSED}send(e){var t;(t=this.ws)==null||t.send(e)}close(e,t){var n;this._closed=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),(n=this.ws)==null||n.close(e,t)}}function b(s){const e=h(),t=u();return s?`${t}/api/v1/apps/${e}/storage/${encodeURIComponent(s)}`:`${t}/api/v1/apps/${e}/storage`}const j={async getItem(s){const e=await fetch(b(s));if(e.status===404)return null;const t=await e.json();try{return JSON.parse(t.value)}catch{return t.value}},async setItem(s,e){const t=typeof e=="string"?e:JSON.stringify(e);await fetch(b(s),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({value:t})})},async removeItem(s){await fetch(b(s),{method:"DELETE"})},async clear(){const s=await this.keys();await Promise.all(s.map(e=>this.removeItem(e)))},async keys(){return((await(await fetch(b())).json()).items||[]).map(t=>t[0])}};function T(s){const e=h();return`${u()}/api/v1/apps/${e}/agents/${s}`}const k={async invoke(s,e){return(await fetch(T("invoke"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:s,agent:e==null?void 0:e.agent,data:e==null?void 0:e.data})})).json()},async*stream(s,e){const t=await fetch(T("stream"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:s,agent:e==null?void 0:e.agent,data:e==null?void 0:e.data})});if(!t.body)throw new Error("No response body for streaming");const n=t.body.getReader(),a=new TextDecoder;let o="";for(;;){const{done:f,value:r}=await n.read();if(f)break;o+=a.decode(r,{stream:!0});const p=o.split(`
-`);o=p.pop()||"";for(const y of p)if(y.startsWith("data: ")){const d=y.slice(6);if(d==="[DONE]")return;try{yield JSON.parse(d)}catch{yield{text:d,done:!1}}}}}};function I(s){const e=h();return`${u()}/api/v1/apps/${e}/janus/${s}`}const D={async complete(s){const t=await(await fetch(I("complete"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)})).json();return t.text||t.content||""},async*stream(s){const e=await fetch(I("stream"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!e.body)throw new Error("No response body for streaming");const t=e.body.getReader(),n=new TextDecoder;let a="";for(;;){const{done:o,value:f}=await t.read();if(o)break;a+=n.decode(f,{stream:!0});const r=a.split(`
-`);a=r.pop()||"";for(const p of r)if(p.startsWith("data: ")){const y=p.slice(6);if(y==="[DONE]")return;try{yield JSON.parse(y).text}catch{yield y}}}}};class x{constructor(){this.ws=null,this.listeners=new Map,this._connected=!1,this._a2uiHandler=null,this.state={}}get connected(){return this._connected}connect(){this.ws||(this.ws=new v,this.ws.onopen=()=>{this._connected=!0},this.ws.onmessage=e=>{var t,n;try{const a=JSON.parse(e.data);if(a.type==="a2ui_message"&&((t=a.data)!=null&&t.message)){(n=this._a2uiHandler)==null||n.call(this,a.data.message);return}this.handleEvent(a)}catch{}},this.ws.onclose=()=>{this._connected=!1},this.ws.onerror=()=>{this._connected=!1})}disconnect(){this.ws&&(this.ws.close(),this.ws=null),this._connected=!1}on(e,t){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t),()=>{var n;(n=this.listeners.get(e))==null||n.delete(t)}}off(e,t){var n;(n=this.listeners.get(e))==null||n.delete(t)}send(e,t){!this.ws||this.ws.readyState!==WebSocket.OPEN||this.ws.send(JSON.stringify({type:"action",name:e,...t}))}requestState(){this.send("request_state")}_rawSend(e){this.ws&&this.ws.readyState===WebSocket.OPEN&&this.ws.send(e)}handleEvent(e){e.type==="state_snapshot"?this.state={...e.snapshot}:e.type==="state_delta"&&this.applyDelta(e.delta);const t=this.listeners.get(e.type);if(t)for(const a of t)try{a(e)}catch{}const n=this.listeners.get("*");if(n)for(const a of n)try{a(e)}catch{}}applyDelta(e){for(const t of e){const n=t.path.split("/").filter(Boolean);if(n.length===0)continue;const a=this.resolveParent(n);if(!a)continue;const o=n[n.length-1];switch(t.op){case"add":case"replace":a[o]=t.value;break;case"remove":delete a[o];break}}}resolveParent(e){let t=this.state;for(let n=0;n{g=g.filter(e=>e!==s)}},setContext(s){var e;(e=c==null?void 0:c.contentWindow)==null||e.postMessage({type:"nebo:set-context",context:s},"*")},newThread(){var s;(s=c==null?void 0:c.contentWindow)==null||s.postMessage({type:"nebo:new-thread"},"*")}};class P{constructor(){this.fetch=E,this.WebSocket=v,this.storage=j,this.agents=k,this.janus=D,this.surfaces=_,this.a2ui=N,this.identity=W,this.chat=J,_._a2uiHandler=e=>N._handleMessage(e),N._setSendFn(e=>_._rawSend(e))}configure(e){e.appId&&O(e.appId),e.baseUrl&&$(e.baseUrl)}}const F=new P;exports.NeboA2UI=U;exports.NeboSDK=P;exports.NeboSurfaces=x;exports.NeboWebSocket=v;exports.a2ui=N;exports.agents=k;exports.chat=J;exports.getAppId=h;exports.getBaseUrl=u;exports.identity=W;exports.janus=D;exports.nebo=F;exports.neboFetch=E;exports.setAppId=O;exports.setBaseUrl=$;exports.storage=j;exports.surfaces=_;
+"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});let i=null,d=null;function l(){if(i)return i;if(window.location.protocol==="neboapp:")return i=window.location.hostname,i;const s=window.location.pathname.match(/^\/apps\/([^/]+)\/ui/);if(s)return i=s[1],i;const e=document.querySelector('meta[name="nebo-app-id"]');if(e)return i=e.getAttribute("content")||"",i;throw new Error('[nebo-sdk] Cannot detect app ID. In production, serve from /apps/{id}/ui/. In dev, add to index.html.')}function u(){if(d)return d;if(window.location.protocol==="neboapp:")return d="http://localhost:27895",d;const s=document.querySelector('meta[name="nebo-base-url"]');return s?(d=s.getAttribute("content")||"",d):(d=window.location.origin,d)}function E(s){i=s}function k(s){d=s}async function j(s,e){const t=l(),n=u();if(s.startsWith("http://")||s.startsWith("https://")){const f=`${n}/api/v1/apps/${t}/http/proxy`,r={};e!=null&&e.headers&&new Headers(e.headers).forEach((F,M)=>{r[M]=F});const p={url:s,method:(e==null?void 0:e.method)||"GET",headers:r,body:e!=null&&e.body?String(e.body):void 0},h=await(await fetch(f,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(p)})).json();return new Response(h.body,{status:h.status,headers:h.headers})}const a=s.startsWith("/")?s:`/${s}`,o=`${n}/api/v1/apps/${t}/api${a}`;return fetch(o,e)}class I{constructor(e){this.ws=null,this.reconnectTimer=null,this.reconnectDelay=1e3,this.maxReconnectDelay=3e4,this._closed=!1,this.onopen=null,this.onmessage=null,this.onerror=null,this.onclose=null;const t=l(),a=u().replace(/^http/,"ws");this.url=`${a}/ws/app/${t}`,this.connect()}connect(){this._closed||(this.ws=new WebSocket(this.url),this.ws.onopen=e=>{var t;this.reconnectDelay=1e3,(t=this.onopen)==null||t.call(this,e)},this.ws.onmessage=e=>{var t;return(t=this.onmessage)==null?void 0:t.call(this,e)},this.ws.onerror=e=>{var t;return(t=this.onerror)==null?void 0:t.call(this,e)},this.ws.onclose=e=>{var t;(t=this.onclose)==null||t.call(this,e),this._closed||this.scheduleReconnect()})}scheduleReconnect(){this.reconnectTimer||this._closed||(this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.reconnectDelay=Math.min(this.reconnectDelay*2,this.maxReconnectDelay),this.connect()},this.reconnectDelay))}get readyState(){var e;return((e=this.ws)==null?void 0:e.readyState)??WebSocket.CLOSED}send(e){var t;(t=this.ws)==null||t.send(e)}close(e,t){var n;this._closed=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),(n=this.ws)==null||n.close(e,t)}}const T=new Set;let O=null;function R(){O||(O=new I,O.onmessage=s=>{let e;try{e=JSON.parse(s.data)}catch{return}e.type!=="app_data_changed"||!e.data||e.data.appId&&e.data.appId!==l()||T.forEach(t=>t(e.data))})}function B(s){if(typeof s!="string")return s;let e;try{e=JSON.parse(s)}catch{return s}if(typeof e!="string")return e;try{return JSON.parse(e)}catch{return e}}function b(s){const e=l(),t=u();return s?`${t}/api/v1/apps/${e}/storage/${encodeURIComponent(s)}`:`${t}/api/v1/apps/${e}/storage`}const x={async getItem(s){const e=await fetch(b(s));if(e.status===404)return null;const t=await e.json();return B(t.value)},async setItem(s,e){const t=typeof e=="string"?e:JSON.stringify(e);await fetch(b(s),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({value:t})})},async removeItem(s){await fetch(b(s),{method:"DELETE"})},async clear(){const s=await this.keys();await Promise.all(s.map(e=>this.removeItem(e)))},async keys(){return((await(await fetch(b())).json()).items||[]).map(t=>t[0])},onChange(s){return T.add(s),R(),()=>{T.delete(s)}}};function v(s){const e=l();return`${u()}/api/v1/apps/${e}/agents/${s}`}const D={async invoke(s,e){return(await fetch(v("invoke"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:s,agent:e==null?void 0:e.agent,data:e==null?void 0:e.data})})).json()},async*stream(s,e){const t=await fetch(v("stream"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:s,agent:e==null?void 0:e.agent,data:e==null?void 0:e.data})});if(!t.body)throw new Error("No response body for streaming");const n=t.body.getReader(),a=new TextDecoder;let o="";for(;;){const{done:f,value:r}=await n.read();if(f)break;o+=a.decode(r,{stream:!0});const p=o.split(`
+`);o=p.pop()||"";for(const y of p)if(y.startsWith("data: ")){const h=y.slice(6);if(h==="[DONE]")return;try{yield JSON.parse(h)}catch{yield{text:h,done:!1}}}}}};function $(s){const e=l();return`${u()}/api/v1/apps/${e}/janus/${s}`}const U={async complete(s){const t=await(await fetch($("complete"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)})).json();return t.text||t.content||""},async*stream(s){const e=await fetch($("stream"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!e.body)throw new Error("No response body for streaming");const t=e.body.getReader(),n=new TextDecoder;let a="";for(;;){const{done:o,value:f}=await t.read();if(o)break;a+=n.decode(f,{stream:!0});const r=a.split(`
+`);a=r.pop()||"";for(const p of r)if(p.startsWith("data: ")){const y=p.slice(6);if(y==="[DONE]")return;try{yield JSON.parse(y).text}catch{yield y}}}}};class J{constructor(){this.ws=null,this.listeners=new Map,this._connected=!1,this._a2uiHandler=null,this.state={}}get connected(){return this._connected}connect(){this.ws||(this.ws=new I,this.ws.onopen=()=>{this._connected=!0},this.ws.onmessage=e=>{var t,n;try{const a=JSON.parse(e.data);if(a.type==="a2ui_message"&&((t=a.data)!=null&&t.message)){(n=this._a2uiHandler)==null||n.call(this,a.data.message);return}this.handleEvent(a)}catch{}},this.ws.onclose=()=>{this._connected=!1},this.ws.onerror=()=>{this._connected=!1})}disconnect(){this.ws&&(this.ws.close(),this.ws=null),this._connected=!1}on(e,t){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t),()=>{var n;(n=this.listeners.get(e))==null||n.delete(t)}}off(e,t){var n;(n=this.listeners.get(e))==null||n.delete(t)}send(e,t){!this.ws||this.ws.readyState!==WebSocket.OPEN||this.ws.send(JSON.stringify({type:"action",name:e,...t}))}requestState(){this.send("request_state")}_rawSend(e){this.ws&&this.ws.readyState===WebSocket.OPEN&&this.ws.send(e)}handleEvent(e){e.type==="state_snapshot"?this.state={...e.snapshot}:e.type==="state_delta"&&this.applyDelta(e.delta);const t=this.listeners.get(e.type);if(t)for(const a of t)try{a(e)}catch{}const n=this.listeners.get("*");if(n)for(const a of n)try{a(e)}catch{}}applyDelta(e){for(const t of e){const n=t.path.split("/").filter(Boolean);if(n.length===0)continue;const a=this.resolveParent(n);if(!a)continue;const o=n[n.length-1];switch(t.op){case"add":case"replace":a[o]=t.value;break;case"remove":delete a[o];break}}}resolveParent(e){let t=this.state;for(let n=0;n{m=m.filter(e=>e!==s)}},setContext(s){var e;(e=c==null?void 0:c.contentWindow)==null||e.postMessage({type:"nebo:set-context",context:s},"*")},newThread(){var s;(s=c==null?void 0:c.contentWindow)==null||s.postMessage({type:"nebo:new-thread"},"*")}};class A{constructor(){this.fetch=j,this.WebSocket=I,this.storage=x,this.agents=D,this.janus=U,this.surfaces=_,this.a2ui=N,this.identity=C,this.chat=P,_._a2uiHandler=e=>N._handleMessage(e),N._setSendFn(e=>_._rawSend(e))}configure(e){e.appId&&E(e.appId),e.baseUrl&&k(e.baseUrl)}}const L=new A;exports.NeboA2UI=W;exports.NeboSDK=A;exports.NeboSurfaces=J;exports.NeboWebSocket=I;exports.a2ui=N;exports.agents=D;exports.chat=P;exports.getAppId=l;exports.getBaseUrl=u;exports.identity=C;exports.janus=U;exports.nebo=L;exports.neboFetch=j;exports.setAppId=E;exports.setBaseUrl=k;exports.storage=x;exports.surfaces=_;
diff --git a/dist/index.d.ts b/dist/index.d.ts
index 27a297b..4312636 100644
--- a/dist/index.d.ts
+++ b/dist/index.d.ts
@@ -19,6 +19,7 @@ export { NeboWebSocket } from './websocket';
export { NeboSurfaces, surfaces } from './surfaces';
export { NeboA2UI, a2ui } from './a2ui';
export { storage } from './storage';
+export type { StorageChange } from './storage';
export { agents } from './agents';
export { janus } from './janus';
export { neboFetch } from './fetch';
diff --git a/dist/index.js b/dist/index.js
index d40c210..3877c85 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -1,5 +1,5 @@
-let i = null, l = null;
-function p() {
+let i = null, d = null;
+function l() {
if (i) return i;
if (window.location.protocol === "neboapp:")
return i = window.location.hostname, i;
@@ -14,47 +14,47 @@ function p() {
);
}
function y() {
- if (l) return l;
+ if (d) return d;
if (window.location.protocol === "neboapp:")
- return l = "http://localhost:27895", l;
+ return d = "http://localhost:27895", d;
const s = document.querySelector('meta[name="nebo-base-url"]');
- return s ? (l = s.getAttribute("content") || "", l) : (l = window.location.origin, l);
+ return s ? (d = s.getAttribute("content") || "", d) : (d = window.location.origin, d);
}
-function E(s) {
+function k(s) {
i = s;
}
-function x(s) {
- l = s;
+function D(s) {
+ d = s;
}
-async function k(s, e) {
- const t = p(), n = y();
+async function j(s, e) {
+ const t = l(), n = y();
if (s.startsWith("http://") || s.startsWith("https://")) {
- const h = `${n}/api/v1/apps/${t}/http/proxy`, r = {};
- e != null && e.headers && new Headers(e.headers).forEach((N, I) => {
- r[I] = N;
+ const u = `${n}/api/v1/apps/${t}/http/proxy`, r = {};
+ e != null && e.headers && new Headers(e.headers).forEach((E, x) => {
+ r[x] = E;
});
- const u = {
+ const f = {
url: s,
method: (e == null ? void 0 : e.method) || "GET",
headers: r,
body: e != null && e.body ? String(e.body) : void 0
- }, d = await (await fetch(h, {
+ }, h = await (await fetch(u, {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify(u)
+ body: JSON.stringify(f)
})).json();
- return new Response(d.body, {
- status: d.status,
- headers: d.headers
+ return new Response(h.body, {
+ status: h.status,
+ headers: h.headers
});
}
const a = s.startsWith("/") ? s : `/${s}`, o = `${n}/api/v1/apps/${t}/api${a}`;
return fetch(o, e);
}
-class O {
+class $ {
constructor(e) {
this.ws = null, this.reconnectTimer = null, this.reconnectDelay = 1e3, this.maxReconnectDelay = 3e4, this._closed = !1, this.onopen = null, this.onmessage = null, this.onerror = null, this.onclose = null;
- const t = p(), a = y().replace(/^http/, "ws");
+ const t = l(), a = y().replace(/^http/, "ws");
this.url = `${a}/ws/app/${t}`, this.connect();
}
connect() {
@@ -90,20 +90,44 @@ class O {
this._closed = !0, this.reconnectTimer && (clearTimeout(this.reconnectTimer), this.reconnectTimer = null), (n = this.ws) == null || n.close(e, t);
}
}
+const T = /* @__PURE__ */ new Set();
+let _ = null;
+function J() {
+ _ || (_ = new $(), _.onmessage = (s) => {
+ let e;
+ try {
+ e = JSON.parse(s.data);
+ } catch {
+ return;
+ }
+ e.type !== "app_data_changed" || !e.data || e.data.appId && e.data.appId !== l() || T.forEach((t) => t(e.data));
+ });
+}
+function W(s) {
+ if (typeof s != "string") return s;
+ let e;
+ try {
+ e = JSON.parse(s);
+ } catch {
+ return s;
+ }
+ if (typeof e != "string") return e;
+ try {
+ return JSON.parse(e);
+ } catch {
+ return e;
+ }
+}
function b(s) {
- const e = p(), t = y();
+ const e = l(), t = y();
return s ? `${t}/api/v1/apps/${e}/storage/${encodeURIComponent(s)}` : `${t}/api/v1/apps/${e}/storage`;
}
-const D = {
+const C = {
async getItem(s) {
const e = await fetch(b(s));
if (e.status === 404) return null;
const t = await e.json();
- try {
- return JSON.parse(t.value);
- } catch {
- return t.value;
- }
+ return W(t.value);
},
async setItem(s, e) {
const t = typeof e == "string" ? e : JSON.stringify(e);
@@ -122,15 +146,24 @@ const D = {
},
async keys() {
return ((await (await fetch(b())).json()).items || []).map((t) => t[0]);
+ },
+ /**
+ * Call `handler` after every change to the store: the employee's writes
+ * and other open views'. Returns a function that stops listening.
+ */
+ onChange(s) {
+ return T.add(s), J(), () => {
+ T.delete(s);
+ };
}
};
-function T(s) {
- const e = p();
+function v(s) {
+ const e = l();
return `${y()}/api/v1/apps/${e}/agents/${s}`;
}
-const j = {
+const U = {
async invoke(s, e) {
- return (await fetch(T("invoke"), {
+ return (await fetch(v("invoke"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
@@ -141,7 +174,7 @@ const j = {
})).json();
},
async *stream(s, e) {
- const t = await fetch(T("stream"), {
+ const t = await fetch(v("stream"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
@@ -155,32 +188,32 @@ const j = {
const n = t.body.getReader(), a = new TextDecoder();
let o = "";
for (; ; ) {
- const { done: h, value: r } = await n.read();
- if (h) break;
+ const { done: u, value: r } = await n.read();
+ if (u) break;
o += a.decode(r, { stream: !0 });
- const u = o.split(`
+ const f = o.split(`
`);
- o = u.pop() || "";
- for (const f of u)
- if (f.startsWith("data: ")) {
- const d = f.slice(6);
- if (d === "[DONE]") return;
+ o = f.pop() || "";
+ for (const p of f)
+ if (p.startsWith("data: ")) {
+ const h = p.slice(6);
+ if (h === "[DONE]") return;
try {
- yield JSON.parse(d);
+ yield JSON.parse(h);
} catch {
- yield { text: d, done: !1 };
+ yield { text: h, done: !1 };
}
}
}
}
};
-function $(s) {
- const e = p();
+function I(s) {
+ const e = l();
return `${y()}/api/v1/apps/${e}/janus/${s}`;
}
-const W = {
+const P = {
async complete(s) {
- const t = await (await fetch($("complete"), {
+ const t = await (await fetch(I("complete"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(s)
@@ -188,7 +221,7 @@ const W = {
return t.text || t.content || "";
},
async *stream(s) {
- const e = await fetch($("stream"), {
+ const e = await fetch(I("stream"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(s)
@@ -198,26 +231,26 @@ const W = {
const t = e.body.getReader(), n = new TextDecoder();
let a = "";
for (; ; ) {
- const { done: o, value: h } = await t.read();
+ const { done: o, value: u } = await t.read();
if (o) break;
- a += n.decode(h, { stream: !0 });
+ a += n.decode(u, { stream: !0 });
const r = a.split(`
`);
a = r.pop() || "";
- for (const u of r)
- if (u.startsWith("data: ")) {
- const f = u.slice(6);
- if (f === "[DONE]") return;
+ for (const f of r)
+ if (f.startsWith("data: ")) {
+ const p = f.slice(6);
+ if (p === "[DONE]") return;
try {
- yield JSON.parse(f).text;
+ yield JSON.parse(p).text;
} catch {
- yield f;
+ yield p;
}
}
}
}
};
-class J {
+class F {
constructor() {
this.ws = null, this.listeners = /* @__PURE__ */ new Map(), this._connected = !1, this._a2uiHandler = null, this.state = {};
}
@@ -227,7 +260,7 @@ class J {
}
/** Connect to the app's surface WebSocket */
connect() {
- this.ws || (this.ws = new O(), this.ws.onopen = () => {
+ this.ws || (this.ws = new $(), this.ws.onopen = () => {
this._connected = !0;
}, this.ws.onmessage = (e) => {
var t, n;
@@ -319,8 +352,8 @@ class J {
return t;
}
}
-const _ = new J();
-class U {
+const O = new F();
+class M {
constructor() {
this.processor = null, this.sendFn = null;
}
@@ -387,12 +420,12 @@ class U {
}));
}
}
-const v = new U();
+const N = new M();
let w = null;
-const P = {
+const R = {
async get() {
if (w) return w;
- const s = p(), e = y(), t = await fetch(`${e}/api/v1/apps/${s}/identity`);
+ const s = l(), e = y(), t = await fetch(`${e}/api/v1/apps/${s}/identity`);
if (!t.ok)
throw new Error(`[nebo-sdk] identity fetch failed: ${t.status}`);
return w = await t.json(), w;
@@ -402,20 +435,20 @@ const P = {
}
};
let c = null, S = null, g = [], m = null;
-function C(s) {
+function A(s) {
if (!(!s.data || typeof s.data.type != "string") && s.data.type.startsWith("nebo:")) {
s.data.type === "nebo:resize" && c && s.data.height && (c.style.height = `${s.data.height}px`);
for (const e of g)
e(s.data);
}
}
-const M = {
+const H = {
mount(s, e) {
c && this.unmount();
- const t = p(), n = y(), a = new URLSearchParams();
+ const t = l(), n = y(), a = new URLSearchParams();
e != null && e.placeholder && a.set("placeholder", e.placeholder), e != null && e.theme && a.set("theme", e.theme), e != null && e.borderless && a.set("borderless", "1"), e != null && e.contextId && a.set("ctx", e.contextId), e != null && e.scope && a.set("scope", e.scope);
- const o = a.toString(), h = `${n}/chat-embed/${t}${o ? "?" + o : ""}`, r = document.createElement("iframe");
- r.src = h, r.style.width = "100%", r.style.height = (e == null ? void 0 : e.height) || "400px", r.style.border = e != null && e.borderless ? "none" : "", r.style.borderRadius = e != null && e.borderless ? "0" : "0.5rem", r.style.colorScheme = "normal", r.setAttribute("allow", "microphone"), s.appendChild(r), c = r, S = s, m = C, window.addEventListener("message", m);
+ const o = a.toString(), u = `${n}/chat-embed/${t}${o ? "?" + o : ""}`, r = document.createElement("iframe");
+ r.src = u, r.style.width = "100%", r.style.height = (e == null ? void 0 : e.height) || "400px", r.style.border = e != null && e.borderless ? "none" : "", r.style.borderRadius = e != null && e.borderless ? "0" : "0.5rem", r.style.colorScheme = "normal", r.setAttribute("allow", "microphone"), s.appendChild(r), c = r, S = s, m = A, window.addEventListener("message", m);
},
unmount() {
c && S && S.removeChild(c), m && (window.removeEventListener("message", m), m = null), c = null, S = null, g = [];
@@ -454,34 +487,34 @@ const M = {
);
}
};
-class R {
+class L {
constructor() {
- this.fetch = k, this.WebSocket = O, this.storage = D, this.agents = j, this.janus = W, this.surfaces = _, this.a2ui = v, this.identity = P, this.chat = M, _._a2uiHandler = (e) => v._handleMessage(e), v._setSendFn((e) => _._rawSend(e));
+ this.fetch = j, this.WebSocket = $, this.storage = C, this.agents = U, this.janus = P, this.surfaces = O, this.a2ui = N, this.identity = R, this.chat = H, O._a2uiHandler = (e) => N._handleMessage(e), N._setSendFn((e) => O._rawSend(e));
}
/**
* Manually configure the SDK (optional — auto-detection works in most cases).
*/
configure(e) {
- e.appId && E(e.appId), e.baseUrl && x(e.baseUrl);
+ e.appId && k(e.appId), e.baseUrl && D(e.baseUrl);
}
}
-const A = new R();
+const B = new L();
export {
- U as NeboA2UI,
- R as NeboSDK,
- J as NeboSurfaces,
- O as NeboWebSocket,
- v as a2ui,
- j as agents,
- M as chat,
- p as getAppId,
+ M as NeboA2UI,
+ L as NeboSDK,
+ F as NeboSurfaces,
+ $ as NeboWebSocket,
+ N as a2ui,
+ U as agents,
+ H as chat,
+ l as getAppId,
y as getBaseUrl,
- P as identity,
- W as janus,
- A as nebo,
- k as neboFetch,
- E as setAppId,
- x as setBaseUrl,
- D as storage,
- _ as surfaces
+ R as identity,
+ P as janus,
+ B as nebo,
+ j as neboFetch,
+ k as setAppId,
+ D as setBaseUrl,
+ C as storage,
+ O as surfaces
};
diff --git a/dist/nebo.global.js b/dist/nebo.global.js
index c1b2b46..fdd2a7e 100644
--- a/dist/nebo.global.js
+++ b/dist/nebo.global.js
@@ -1,3 +1,3 @@
-var NeboAppSDK=(function(r){"use strict";let l=null,d=null;function h(){if(l)return l;if(window.location.protocol==="neboapp:")return l=window.location.hostname,l;const s=window.location.pathname.match(/^\/apps\/([^/]+)\/ui/);if(s)return l=s[1],l;const e=document.querySelector('meta[name="nebo-app-id"]');if(e)return l=e.getAttribute("content")||"",l;throw new Error('[nebo-sdk] Cannot detect app ID. In production, serve from /apps/{id}/ui/. In dev, add to index.html.')}function u(){if(d)return d;if(window.location.protocol==="neboapp:")return d="http://localhost:27895",d;const s=document.querySelector('meta[name="nebo-base-url"]');return s?(d=s.getAttribute("content")||"",d):(d=window.location.origin,d)}function I(s){l=s}function O(s){d=s}async function $(s,e){const t=h(),n=u();if(s.startsWith("http://")||s.startsWith("https://")){const p=`${n}/api/v1/apps/${t}/http/proxy`,c={};e!=null&&e.headers&&new Headers(e.headers).forEach((R,B)=>{c[B]=R});const y={url:s,method:(e==null?void 0:e.method)||"GET",headers:c,body:e!=null&&e.body?String(e.body):void 0},f=await(await fetch(p,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(y)})).json();return new Response(f.body,{status:f.status,headers:f.headers})}const a=s.startsWith("/")?s:`/${s}`,i=`${n}/api/v1/apps/${t}/api${a}`;return fetch(i,e)}class T{constructor(e){this.ws=null,this.reconnectTimer=null,this.reconnectDelay=1e3,this.maxReconnectDelay=3e4,this._closed=!1,this.onopen=null,this.onmessage=null,this.onerror=null,this.onclose=null;const t=h(),a=u().replace(/^http/,"ws");this.url=`${a}/ws/app/${t}`,this.connect()}connect(){this._closed||(this.ws=new WebSocket(this.url),this.ws.onopen=e=>{var t;this.reconnectDelay=1e3,(t=this.onopen)==null||t.call(this,e)},this.ws.onmessage=e=>{var t;return(t=this.onmessage)==null?void 0:t.call(this,e)},this.ws.onerror=e=>{var t;return(t=this.onerror)==null?void 0:t.call(this,e)},this.ws.onclose=e=>{var t;(t=this.onclose)==null||t.call(this,e),this._closed||this.scheduleReconnect()})}scheduleReconnect(){this.reconnectTimer||this._closed||(this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.reconnectDelay=Math.min(this.reconnectDelay*2,this.maxReconnectDelay),this.connect()},this.reconnectDelay))}get readyState(){var e;return((e=this.ws)==null?void 0:e.readyState)??WebSocket.CLOSED}send(e){var t;(t=this.ws)==null||t.send(e)}close(e,t){var n;this._closed=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),(n=this.ws)==null||n.close(e,t)}}function S(s){const e=h(),t=u();return s?`${t}/api/v1/apps/${e}/storage/${encodeURIComponent(s)}`:`${t}/api/v1/apps/${e}/storage`}const D={async getItem(s){const e=await fetch(S(s));if(e.status===404)return null;const t=await e.json();try{return JSON.parse(t.value)}catch{return t.value}},async setItem(s,e){const t=typeof e=="string"?e:JSON.stringify(e);await fetch(S(s),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({value:t})})},async removeItem(s){await fetch(S(s),{method:"DELETE"})},async clear(){const s=await this.keys();await Promise.all(s.map(e=>this.removeItem(e)))},async keys(){return((await(await fetch(S())).json()).items||[]).map(t=>t[0])}};function E(s){const e=h();return`${u()}/api/v1/apps/${e}/agents/${s}`}const j={async invoke(s,e){return(await fetch(E("invoke"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:s,agent:e==null?void 0:e.agent,data:e==null?void 0:e.data})})).json()},async*stream(s,e){const t=await fetch(E("stream"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:s,agent:e==null?void 0:e.agent,data:e==null?void 0:e.data})});if(!t.body)throw new Error("No response body for streaming");const n=t.body.getReader(),a=new TextDecoder;let i="";for(;;){const{done:p,value:c}=await n.read();if(p)break;i+=a.decode(c,{stream:!0});const y=i.split(`
-`);i=y.pop()||"";for(const w of y)if(w.startsWith("data: ")){const f=w.slice(6);if(f==="[DONE]")return;try{yield JSON.parse(f)}catch{yield{text:f,done:!1}}}}}};function k(s){const e=h();return`${u()}/api/v1/apps/${e}/janus/${s}`}const U={async complete(s){const t=await(await fetch(k("complete"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)})).json();return t.text||t.content||""},async*stream(s){const e=await fetch(k("stream"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!e.body)throw new Error("No response body for streaming");const t=e.body.getReader(),n=new TextDecoder;let a="";for(;;){const{done:i,value:p}=await t.read();if(i)break;a+=n.decode(p,{stream:!0});const c=a.split(`
-`);a=c.pop()||"";for(const y of c)if(y.startsWith("data: ")){const w=y.slice(6);if(w==="[DONE]")return;try{yield JSON.parse(w).text}catch{yield w}}}}};class W{constructor(){this.ws=null,this.listeners=new Map,this._connected=!1,this._a2uiHandler=null,this.state={}}get connected(){return this._connected}connect(){this.ws||(this.ws=new T,this.ws.onopen=()=>{this._connected=!0},this.ws.onmessage=e=>{var t,n;try{const a=JSON.parse(e.data);if(a.type==="a2ui_message"&&((t=a.data)!=null&&t.message)){(n=this._a2uiHandler)==null||n.call(this,a.data.message);return}this.handleEvent(a)}catch{}},this.ws.onclose=()=>{this._connected=!1},this.ws.onerror=()=>{this._connected=!1})}disconnect(){this.ws&&(this.ws.close(),this.ws=null),this._connected=!1}on(e,t){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t),()=>{var n;(n=this.listeners.get(e))==null||n.delete(t)}}off(e,t){var n;(n=this.listeners.get(e))==null||n.delete(t)}send(e,t){!this.ws||this.ws.readyState!==WebSocket.OPEN||this.ws.send(JSON.stringify({type:"action",name:e,...t}))}requestState(){this.send("request_state")}_rawSend(e){this.ws&&this.ws.readyState===WebSocket.OPEN&&this.ws.send(e)}handleEvent(e){e.type==="state_snapshot"?this.state={...e.snapshot}:e.type==="state_delta"&&this.applyDelta(e.delta);const t=this.listeners.get(e.type);if(t)for(const a of t)try{a(e)}catch{}const n=this.listeners.get("*");if(n)for(const a of n)try{a(e)}catch{}}applyDelta(e){for(const t of e){const n=t.path.split("/").filter(Boolean);if(n.length===0)continue;const a=this.resolveParent(n);if(!a)continue;const i=n[n.length-1];switch(t.op){case"add":case"replace":a[i]=t.value;break;case"remove":delete a[i];break}}}resolveParent(e){let t=this.state;for(let n=0;n{g=g.filter(e=>e!==s)}},setContext(s){var e;(e=o==null?void 0:o.contentWindow)==null||e.postMessage({type:"nebo:set-context",context:s},"*")},newThread(){var s;(s=o==null?void 0:o.contentWindow)==null||s.postMessage({type:"nebo:new-thread"},"*")}};class C{constructor(){this.fetch=$,this.WebSocket=T,this.storage=D,this.agents=j,this.janus=U,this.surfaces=_,this.a2ui=N,this.identity=P,this.chat=A,_._a2uiHandler=e=>N._handleMessage(e),N._setSendFn(e=>_._rawSend(e))}configure(e){e.appId&&I(e.appId),e.baseUrl&&O(e.baseUrl)}}const F=new C;return r.NeboA2UI=J,r.NeboSDK=C,r.NeboSurfaces=W,r.NeboWebSocket=T,r.a2ui=N,r.agents=j,r.chat=A,r.getAppId=h,r.getBaseUrl=u,r.identity=P,r.janus=U,r.nebo=F,r.neboFetch=$,r.setAppId=I,r.setBaseUrl=O,r.storage=D,r.surfaces=_,Object.defineProperty(r,Symbol.toStringTag,{value:"Module"}),r})({});
+var NeboAppSDK=(function(r){"use strict";let d=null,l=null;function h(){if(d)return d;if(window.location.protocol==="neboapp:")return d=window.location.hostname,d;const s=window.location.pathname.match(/^\/apps\/([^/]+)\/ui/);if(s)return d=s[1],d;const e=document.querySelector('meta[name="nebo-app-id"]');if(e)return d=e.getAttribute("content")||"",d;throw new Error('[nebo-sdk] Cannot detect app ID. In production, serve from /apps/{id}/ui/. In dev, add to index.html.')}function u(){if(l)return l;if(window.location.protocol==="neboapp:")return l="http://localhost:27895",l;const s=document.querySelector('meta[name="nebo-base-url"]');return s?(l=s.getAttribute("content")||"",l):(l=window.location.origin,l)}function $(s){d=s}function E(s){l=s}async function k(s,e){const t=h(),n=u();if(s.startsWith("http://")||s.startsWith("https://")){const p=`${n}/api/v1/apps/${t}/http/proxy`,c={};e!=null&&e.headers&&new Headers(e.headers).forEach((q,z)=>{c[z]=q});const y={url:s,method:(e==null?void 0:e.method)||"GET",headers:c,body:e!=null&&e.body?String(e.body):void 0},f=await(await fetch(p,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(y)})).json();return new Response(f.body,{status:f.status,headers:f.headers})}const a=s.startsWith("/")?s:`/${s}`,i=`${n}/api/v1/apps/${t}/api${a}`;return fetch(i,e)}class S{constructor(e){this.ws=null,this.reconnectTimer=null,this.reconnectDelay=1e3,this.maxReconnectDelay=3e4,this._closed=!1,this.onopen=null,this.onmessage=null,this.onerror=null,this.onclose=null;const t=h(),a=u().replace(/^http/,"ws");this.url=`${a}/ws/app/${t}`,this.connect()}connect(){this._closed||(this.ws=new WebSocket(this.url),this.ws.onopen=e=>{var t;this.reconnectDelay=1e3,(t=this.onopen)==null||t.call(this,e)},this.ws.onmessage=e=>{var t;return(t=this.onmessage)==null?void 0:t.call(this,e)},this.ws.onerror=e=>{var t;return(t=this.onerror)==null?void 0:t.call(this,e)},this.ws.onclose=e=>{var t;(t=this.onclose)==null||t.call(this,e),this._closed||this.scheduleReconnect()})}scheduleReconnect(){this.reconnectTimer||this._closed||(this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.reconnectDelay=Math.min(this.reconnectDelay*2,this.maxReconnectDelay),this.connect()},this.reconnectDelay))}get readyState(){var e;return((e=this.ws)==null?void 0:e.readyState)??WebSocket.CLOSED}send(e){var t;(t=this.ws)==null||t.send(e)}close(e,t){var n;this._closed=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),(n=this.ws)==null||n.close(e,t)}}const v=new Set;let T=null;function R(){T||(T=new S,T.onmessage=s=>{let e;try{e=JSON.parse(s.data)}catch{return}e.type!=="app_data_changed"||!e.data||e.data.appId&&e.data.appId!==h()||v.forEach(t=>t(e.data))})}function B(s){if(typeof s!="string")return s;let e;try{e=JSON.parse(s)}catch{return s}if(typeof e!="string")return e;try{return JSON.parse(e)}catch{return e}}function _(s){const e=h(),t=u();return s?`${t}/api/v1/apps/${e}/storage/${encodeURIComponent(s)}`:`${t}/api/v1/apps/${e}/storage`}const D={async getItem(s){const e=await fetch(_(s));if(e.status===404)return null;const t=await e.json();return B(t.value)},async setItem(s,e){const t=typeof e=="string"?e:JSON.stringify(e);await fetch(_(s),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({value:t})})},async removeItem(s){await fetch(_(s),{method:"DELETE"})},async clear(){const s=await this.keys();await Promise.all(s.map(e=>this.removeItem(e)))},async keys(){return((await(await fetch(_())).json()).items||[]).map(t=>t[0])},onChange(s){return v.add(s),R(),()=>{v.delete(s)}}};function j(s){const e=h();return`${u()}/api/v1/apps/${e}/agents/${s}`}const U={async invoke(s,e){return(await fetch(j("invoke"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:s,agent:e==null?void 0:e.agent,data:e==null?void 0:e.data})})).json()},async*stream(s,e){const t=await fetch(j("stream"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:s,agent:e==null?void 0:e.agent,data:e==null?void 0:e.data})});if(!t.body)throw new Error("No response body for streaming");const n=t.body.getReader(),a=new TextDecoder;let i="";for(;;){const{done:p,value:c}=await n.read();if(p)break;i+=a.decode(c,{stream:!0});const y=i.split(`
+`);i=y.pop()||"";for(const w of y)if(w.startsWith("data: ")){const f=w.slice(6);if(f==="[DONE]")return;try{yield JSON.parse(f)}catch{yield{text:f,done:!1}}}}}};function J(s){const e=h();return`${u()}/api/v1/apps/${e}/janus/${s}`}const W={async complete(s){const t=await(await fetch(J("complete"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)})).json();return t.text||t.content||""},async*stream(s){const e=await fetch(J("stream"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!e.body)throw new Error("No response body for streaming");const t=e.body.getReader(),n=new TextDecoder;let a="";for(;;){const{done:i,value:p}=await t.read();if(i)break;a+=n.decode(p,{stream:!0});const c=a.split(`
+`);a=c.pop()||"";for(const y of c)if(y.startsWith("data: ")){const w=y.slice(6);if(w==="[DONE]")return;try{yield JSON.parse(w).text}catch{yield w}}}}};class C{constructor(){this.ws=null,this.listeners=new Map,this._connected=!1,this._a2uiHandler=null,this.state={}}get connected(){return this._connected}connect(){this.ws||(this.ws=new S,this.ws.onopen=()=>{this._connected=!0},this.ws.onmessage=e=>{var t,n;try{const a=JSON.parse(e.data);if(a.type==="a2ui_message"&&((t=a.data)!=null&&t.message)){(n=this._a2uiHandler)==null||n.call(this,a.data.message);return}this.handleEvent(a)}catch{}},this.ws.onclose=()=>{this._connected=!1},this.ws.onerror=()=>{this._connected=!1})}disconnect(){this.ws&&(this.ws.close(),this.ws=null),this._connected=!1}on(e,t){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t),()=>{var n;(n=this.listeners.get(e))==null||n.delete(t)}}off(e,t){var n;(n=this.listeners.get(e))==null||n.delete(t)}send(e,t){!this.ws||this.ws.readyState!==WebSocket.OPEN||this.ws.send(JSON.stringify({type:"action",name:e,...t}))}requestState(){this.send("request_state")}_rawSend(e){this.ws&&this.ws.readyState===WebSocket.OPEN&&this.ws.send(e)}handleEvent(e){e.type==="state_snapshot"?this.state={...e.snapshot}:e.type==="state_delta"&&this.applyDelta(e.delta);const t=this.listeners.get(e.type);if(t)for(const a of t)try{a(e)}catch{}const n=this.listeners.get("*");if(n)for(const a of n)try{a(e)}catch{}}applyDelta(e){for(const t of e){const n=t.path.split("/").filter(Boolean);if(n.length===0)continue;const a=this.resolveParent(n);if(!a)continue;const i=n[n.length-1];switch(t.op){case"add":case"replace":a[i]=t.value;break;case"remove":delete a[i];break}}}resolveParent(e){let t=this.state;for(let n=0;n{m=m.filter(e=>e!==s)}},setContext(s){var e;(e=o==null?void 0:o.contentWindow)==null||e.postMessage({type:"nebo:set-context",context:s},"*")},newThread(){var s;(s=o==null?void 0:o.contentWindow)==null||s.postMessage({type:"nebo:new-thread"},"*")}};class M{constructor(){this.fetch=k,this.WebSocket=S,this.storage=D,this.agents=U,this.janus=W,this.surfaces=N,this.a2ui=I,this.identity=A,this.chat=F,N._a2uiHandler=e=>I._handleMessage(e),I._setSendFn(e=>N._rawSend(e))}configure(e){e.appId&&$(e.appId),e.baseUrl&&E(e.baseUrl)}}const L=new M;return r.NeboA2UI=P,r.NeboSDK=M,r.NeboSurfaces=C,r.NeboWebSocket=S,r.a2ui=I,r.agents=U,r.chat=F,r.getAppId=h,r.getBaseUrl=u,r.identity=A,r.janus=W,r.nebo=L,r.neboFetch=k,r.setAppId=$,r.setBaseUrl=E,r.storage=D,r.surfaces=N,Object.defineProperty(r,Symbol.toStringTag,{value:"Module"}),r})({});
diff --git a/dist/sdk.d.ts b/dist/sdk.d.ts
index ec64586..2a79b3f 100644
--- a/dist/sdk.d.ts
+++ b/dist/sdk.d.ts
@@ -12,6 +12,7 @@ export declare class NeboSDK {
removeItem(key: string): Promise;
clear(): Promise;
keys(): Promise;
+ onChange(handler: (change: import("./storage").StorageChange) => void): () => void;
};
agents: {
invoke(message: string, options?: import("./types").InvokeOptions): Promise;
diff --git a/dist/storage.d.ts b/dist/storage.d.ts
index e8b7e02..03c6f45 100644
--- a/dist/storage.d.ts
+++ b/dist/storage.d.ts
@@ -1,10 +1,29 @@
/**
* nebo.storage — mirrors localStorage API with server-persisted async KV.
+ *
+ * The same store the app's employee reads and writes with its `app_data`
+ * tool: same keys, same values. `onChange` hears every write to it, the
+ * employee's and other open views'.
*/
+/** One change to the app's store (the `app_data_changed` event). */
+export interface StorageChange {
+ appId: string;
+ keys: string[];
+ action: 'set' | 'delete';
+ /** `employee`: the app's employee wrote it; `page`: an open view did. */
+ source: 'employee' | 'page';
+}
+type ChangeHandler = (change: StorageChange) => void;
export declare const storage: {
getItem(key: string): Promise;
setItem(key: string, value: unknown): Promise;
removeItem(key: string): Promise;
clear(): Promise;
keys(): Promise;
+ /**
+ * Call `handler` after every change to the store: the employee's writes
+ * and other open views'. Returns a function that stops listening.
+ */
+ onChange(handler: ChangeHandler): () => void;
};
+export {};
diff --git a/src/index.ts b/src/index.ts
index f0d9334..2d61d1a 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -22,6 +22,7 @@ export { NeboWebSocket } from './websocket';
export { NeboSurfaces, surfaces } from './surfaces';
export { NeboA2UI, a2ui } from './a2ui';
export { storage } from './storage';
+export type { StorageChange } from './storage';
export { agents } from './agents';
export { janus } from './janus';
export { neboFetch } from './fetch';
diff --git a/src/storage.ts b/src/storage.ts
index 0c67c88..cd04d79 100644
--- a/src/storage.ts
+++ b/src/storage.ts
@@ -1,8 +1,60 @@
/**
* nebo.storage — mirrors localStorage API with server-persisted async KV.
+ *
+ * The same store the app's employee reads and writes with its `app_data`
+ * tool: same keys, same values. `onChange` hears every write to it, the
+ * employee's and other open views'.
*/
import { getAppId, getBaseUrl } from './config';
+import { NeboWebSocket } from './websocket';
+
+/** One change to the app's store (the `app_data_changed` event). */
+export interface StorageChange {
+ appId: string;
+ keys: string[];
+ action: 'set' | 'delete';
+ /** `employee`: the app's employee wrote it; `page`: an open view did. */
+ source: 'employee' | 'page';
+}
+
+type ChangeHandler = (change: StorageChange) => void;
+
+const changeHandlers = new Set();
+let changeSocket: NeboWebSocket | null = null;
+
+function listenForChanges(): void {
+ if (changeSocket) return;
+ changeSocket = new NeboWebSocket();
+ changeSocket.onmessage = (msg) => {
+ let parsed: { type?: string; data?: StorageChange };
+ try {
+ parsed = JSON.parse(msg.data);
+ } catch {
+ return;
+ }
+ if (parsed.type !== 'app_data_changed' || !parsed.data) return;
+ if (parsed.data.appId && parsed.data.appId !== getAppId()) return;
+ changeHandlers.forEach((h) => h(parsed.data as StorageChange));
+ };
+}
+
+/** The value the server holds: the text setItem sent, then that text parsed. */
+function decode(stored: unknown): unknown {
+ if (typeof stored !== 'string') return stored;
+ let text: unknown;
+ try {
+ text = JSON.parse(stored);
+ } catch {
+ return stored;
+ }
+ if (typeof text !== 'string') return text;
+ try {
+ return JSON.parse(text);
+ } catch {
+ return text;
+ }
+}
function storageUrl(key?: string): string {
const appId = getAppId();
@@ -18,11 +70,7 @@ export const storage = {
const resp = await fetch(storageUrl(key));
if (resp.status === 404) return null;
const data = await resp.json();
- try {
- return JSON.parse(data.value);
- } catch {
- return data.value;
- }
+ return decode(data.value);
},
async setItem(key: string, value: unknown): Promise {
@@ -47,5 +95,17 @@ export const storage = {
const resp = await fetch(storageUrl());
const data = await resp.json();
return (data.items || []).map((item: [string, string]) => item[0]);
+ },
+
+ /**
+ * Call `handler` after every change to the store: the employee's writes
+ * and other open views'. Returns a function that stops listening.
+ */
+ onChange(handler: ChangeHandler): () => void {
+ changeHandlers.add(handler);
+ listenForChanges();
+ return () => {
+ changeHandlers.delete(handler);
+ };
}
};