diff --git a/README.md b/README.md index c2dd4a2..07f0f1a 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,46 @@ const project = await fetch(nebo.files.url('Media/Projects/reel/project.json')). can take it back in the app's settings, and Nebo asks again when an update adds a permission. Until then the route answers 403. +### `nebo.plugins.run(slug, command, args?, options?)` +Run a plugin installed on the bot directly, with no AI in the middle: the +same command and flags the app's employee passes to its `plugin__` +tool, run by Nebo the same way (the installed version, the employee's +account, the same billing), with the result straight back. Use it for work a +page needs at once: a scrub strip, a waveform, a thumbnail, a quick preview. + +```js +const strip = await nebo.plugins.run('nebo-media', 'strip', { in: 'Media/Projects/reel/cut.mp4', every: 2 }); +// strip → { ok: true, output: '…Result: /…/strip.jpg (…)', files: ['/…/strip.jpg'] } + +// Long work: each line the plugin writes, as it writes it. +const preview = await nebo.plugins.run('nebo-media', 'preview', { in: cut }, { + timeout: 600, + onProgress: (line) => (status.textContent = line), +}); +``` + +- The manifest grants the plugin: `"permissions": ["plugin:nebo-media"]` for + every command, or `"plugin:nebo-media:strip"` for one command each. +- An app made on the bot has what it declares. An app installed from the + marketplace needs the owner's OK first ("Media Studio wants to use Nebo + Media"). +- `args`: each key becomes `--key`; a number, true/false, an object or a list + goes as its JSON. +- `timeout`: seconds, 120 unless given, at most 600. An app has at most 4 + runs going at once (429 beyond that). It runs as its own employee and never + signs in or out of an account; the owner does that in the employee's settings. +- `ok: false` is the plugin's own failure, with its words in `output`. +- Throws an `Error` whose message is the reason when Nebo refuses the call: + 403 (the manifest grants no such plugin or command, or the owner has not + allowed it), 404 (the plugin is not installed, or it is turned off), 429 + (4 runs already going). +- An app's sidecar makes the same call with its `NEBO_APP_TOKEN`: + `POST /api/v1/apps//plugins//run` with `{command, args, timeout, + background}`; `background: true` answers `202 {job}`, and + `GET /api/v1/apps//plugins/jobs/` reads it (JSON, or a + `text/event-stream` of `progress` events and one `result`). +- Types: `PluginResult`, `PluginRunOptions`. + ### `nebo.chat` The page's conversation with its own employee. The page talks to that one employee only, with its own pass, wherever it is open (the desktop window, a diff --git a/dist/index.cjs b/dist/index.cjs index 41b5fad..6233c3f 100644 --- a/dist/index.cjs +++ b/dist/index.cjs @@ -1,6 +1,9 @@ -"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});let y=null,w=null;function u(){if(y)return y;if(window.location.protocol==="neboapp:")return y=window.location.hostname,y;const t=window.location.pathname.match(/^(?:\/t\/[^/]+)?(?:\/k\/[^/]+)?\/apps\/([^/]+)\/ui(?:\/|$)/);if(t)return y=decodeURIComponent(t[1]),y;const e=document.querySelector('meta[name="nebo-app-id"]');if(e)return y=e.getAttribute("content")||"",y;throw new Error('[nebo-sdk] Cannot detect app ID. In production, serve from /apps/{id}/ui/. In dev, add to index.html.')}function m(){if(w)return w;const t=document.querySelector('meta[name="nebo-base-url"]');return t?(w=t.getAttribute("content")||"",w):window.location.protocol==="neboapp:"?(w="http://localhost:27895",w):(w=window.location.origin,w)}function V(t){y=t}function X(t){w=t}async function Z(t,e){const n=u(),a=m();if(t.startsWith("http://")||t.startsWith("https://")){const d=`${a}/api/v1/apps/${n}/http/proxy`,i={};e!=null&&e.headers&&new Headers(e.headers).forEach((U,k)=>{i[k]=U});const h={url:t,method:(e==null?void 0:e.method)||"GET",headers:i,body:e!=null&&e.body?String(e.body):void 0},l=await fetch(d,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(h)});if(!l.ok)return l;const c=await l.json();return new Response(c.body,{status:c.status,headers:c.headers})}const s=t.startsWith("/")?t:`/${t}`,o=`${a}/api/v1/apps/${n}/api${s}`;return fetch(o,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 n=u(),s=m().replace(/^http/,"ws");this.url=`${s}/ws/app/${n}`,this.connect()}connect(){this._closed||(this.ws=new WebSocket(this.url),this.ws.onopen=e=>{var n;this.reconnectDelay=1e3,(n=this.onopen)==null||n.call(this,e)},this.ws.onmessage=e=>{var n;return(n=this.onmessage)==null?void 0:n.call(this,e)},this.ws.onerror=e=>{var n;return(n=this.onerror)==null?void 0:n.call(this,e)},this.ws.onclose=e=>{var n;(n=this.onclose)==null||n.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 n;(n=this.ws)==null||n.send(e)}close(e,n){var a;this._closed=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),(a=this.ws)==null||a.close(e,n)}}const R=new Set;let J=null;function fe(){J||(J=new T,J.onmessage=t=>{let e;try{e=JSON.parse(t.data)}catch{return}e.type!=="app_data_changed"||!e.data||e.data.appId&&e.data.appId!==u()||R.forEach(n=>n(e.data))})}function pe(t){if(typeof t!="string")return t;let e;try{e=JSON.parse(t)}catch{return t}if(typeof e!="string")return e;try{return JSON.parse(e)}catch{return e}}function N(t){const e=u(),n=m();return t?`${n}/api/v1/apps/${e}/storage/${encodeURIComponent(t)}`:`${n}/api/v1/apps/${e}/storage`}const ee={async getItem(t){const e=await fetch(N(t));if(e.status===404)return null;const n=await e.json();return pe(n.value)},async setItem(t,e){const n=typeof e=="string"?e:JSON.stringify(e);await fetch(N(t),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({value:n})})},async removeItem(t){await fetch(N(t),{method:"DELETE"})},async clear(){const t=await this.keys();await Promise.all(t.map(e=>this.removeItem(e)))},async keys(){return((await(await fetch(N())).json()).items||[]).map(n=>n[0])},onChange(t){return R.add(t),fe(),()=>{R.delete(t)}}};function B(t){const e=u();return`${m()}/api/v1/apps/${e}/agents/${t}`}const te={async invoke(t,e){return(await fetch(B("invoke"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:t,agent:e==null?void 0:e.agent,data:e==null?void 0:e.data})})).json()},async*stream(t,e){const n=await fetch(B("stream"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:t,agent:e==null?void 0:e.agent,data:e==null?void 0:e.data})});if(!n.body)throw new Error("No response body for streaming");const a=n.body.getReader(),s=new TextDecoder;let o="";for(;;){const{done:d,value:i}=await a.read();if(d)break;o+=s.decode(i,{stream:!0});const h=o.split(` -`);o=h.pop()||"";for(const l of h)if(l.startsWith("data: ")){const c=l.slice(6);if(c==="[DONE]")return;try{yield JSON.parse(c)}catch{yield{text:c,done:!1}}}}}};function q(t){const e=u();return`${m()}/api/v1/apps/${e}/janus/${t}`}const ne={async complete(t){const n=await(await fetch(q("complete"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json();return n.text||n.content||""},async*stream(t){const e=await fetch(q("stream"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!e.body)throw new Error("No response body for streaming");const n=e.body.getReader(),a=new TextDecoder;let s="";for(;;){const{done:o,value:d}=await n.read();if(o)break;s+=a.decode(d,{stream:!0});const i=s.split(` -`);s=i.pop()||"";for(const h of i)if(h.startsWith("data: ")){const l=h.slice(6);if(l==="[DONE]")return;try{yield JSON.parse(l).text}catch{yield l}}}}};async function ae(t){const e=await fetch(`${m()}/api/v1/apps/${u()}/janus/decide`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}),n=await e.json().catch(()=>({}));if(!e.ok)throw new Error(n.error||`decide failed (${e.status})`);return n}async function se(t){const e=new URLSearchParams(window.location.search),n=e.get("thread")||"",a=e.get("client")||"",s=await fetch(`${m()}/api/v1/apps/${u()}/share`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:t.name,content:t.content,thread:n,client:a})}),o=await s.json().catch(()=>({}));if(!s.ok)throw new Error(o.error||`share failed (${s.status})`);return o}const re={url(t){const e=t.split("/").filter(s=>s!=="");if(e.some(s=>s==="."||s===".."))throw new Error(`[nebo-sdk] files.url: "${t}" climbs out of its folder; name the file inside the granted folder`);const n=encodeURIComponent(u()),a=e.map(encodeURIComponent).join("/");return`${m()}/api/v1/apps/${n}/files/${a}`}};class oe{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 n,a;try{const s=JSON.parse(e.data);if(s.type==="a2ui_message"&&((n=s.data)!=null&&n.message)){(a=this._a2uiHandler)==null||a.call(this,s.data.message);return}this.handleEvent(s)}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,n){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(n),()=>{var a;(a=this.listeners.get(e))==null||a.delete(n)}}off(e,n){var a;(a=this.listeners.get(e))==null||a.delete(n)}send(e,n){!this.ws||this.ws.readyState!==WebSocket.OPEN||this.ws.send(JSON.stringify({type:"action",name:e,...n}))}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 n=this.listeners.get(e.type);if(n)for(const s of n)try{s(e)}catch{}const a=this.listeners.get("*");if(a)for(const s of a)try{s(e)}catch{}}applyDelta(e){for(const n of e){const a=n.path.split("/").filter(Boolean);if(a.length===0)continue;const s=this.resolveParent(a);if(!s)continue;const o=a[a.length-1];switch(n.op){case"add":case"replace":s[o]=n.value;break;case"remove":delete s[o];break}}}resolveParent(e){let n=this.state;for(let a=0;ae(t))}let A=!1;function E(){v||(A=!1,v=new T,v.onopen=()=>{A&&x({type:"reconnected"}),A=!0},v.onmessage=t=>{let e;try{e=JSON.parse(t.data)}catch{return}const n=e.data??{},a=me(n.session_id);if(a!==null)switch(e.type){case"chat_user_message":x({type:"message",thread:a,message:{id:String(n.id??""),role:"user",content:String(n.content??""),createdAt:Number(n.createdAt??Date.now())}});break;case"chat_stream":typeof n.content=="string"&&n.content&&x({type:"stream",thread:a,text:n.content});break;case"chat_complete":if(n.stop_reason==="queued_into_running_turn")break;x({type:"complete",thread:a,notice:typeof n.stop_notice=="string"?n.stop_notice:void 0});break;case"chat_error":x({type:"error",thread:a,error:String(n.error??"Something went wrong. Try again.")});break;case"chat_cancelled":x({type:"stopped",thread:a});break;case"chat_waiting":x({type:"waiting",thread:a});break}})}class W extends Error{constructor(e,n){super(n),this.status=e}}async function P(t,e){const n=await fetch(de(t),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(!n.ok)throw new W(n.status,await n.text()||`chat: ${n.status}`);return n}const C={async history(t){const e=new URLSearchParams;t!=null&&t.thread&&e.set("thread",t.thread),t!=null&&t.before&&e.set("before",t.before),t!=null&&t.limit&&e.set("limit",String(t.limit));const n=e.toString();E();const a=await fetch(de(n?`?${n}`:""));if(!a.ok)throw new Error(await a.text()||`chat: ${a.status}`);const s=await a.json();return{messages:s.messages??[],running:!!s.running,waiting:!!s.waiting,hasMore:!!s.hasMore}},async send(t,e){E(),await P("",{message:t,thread:e==null?void 0:e.thread,scope:e==null?void 0:e.scope,context:K})},async stop(t){E(),await P("/stop",{thread:t==null?void 0:t.thread})},async newThread(t){E(),await P("/new",{thread:t==null?void 0:t.thread}),f&&f.thread===((t==null?void 0:t.thread)||void 0)&&f.reset()},onMessage(t){return E(),j.add(t),()=>{j.delete(t),G()}},setContext(t){K=t},mount(t,e){f==null||f.destroy(),f=ye(t,e??{})},unmount(){f==null||f.destroy(),f=null,G()}};function G(){j.size||f||!v||(v.close(),v=null)}let f=null;const Y="nebo-chat-style",ge=` +"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});let w=null,_=null;function u(){if(w)return w;if(window.location.protocol==="neboapp:")return w=window.location.hostname,w;const t=window.location.pathname.match(/^(?:\/t\/[^/]+)?(?:\/k\/[^/]+)?\/apps\/([^/]+)\/ui(?:\/|$)/);if(t)return w=decodeURIComponent(t[1]),w;const e=document.querySelector('meta[name="nebo-app-id"]');if(e)return w=e.getAttribute("content")||"",w;throw new Error('[nebo-sdk] Cannot detect app ID. In production, serve from /apps/{id}/ui/. In dev, add to index.html.')}function p(){if(_)return _;const t=document.querySelector('meta[name="nebo-base-url"]');return t?(_=t.getAttribute("content")||"",_):window.location.protocol==="neboapp:"?(_="http://localhost:27895",_):(_=window.location.origin,_)}function V(t){w=t}function X(t){_=t}async function Z(t,e){const n=u(),a=p();if(t.startsWith("http://")||t.startsWith("https://")){const d=`${a}/api/v1/apps/${n}/http/proxy`,c={};e!=null&&e.headers&&new Headers(e.headers).forEach((m,$)=>{c[$]=m});const h={url:t,method:(e==null?void 0:e.method)||"GET",headers:c,body:e!=null&&e.body?String(e.body):void 0},l=await fetch(d,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(h)});if(!l.ok)return l;const i=await l.json();return new Response(i.body,{status:i.status,headers:i.headers})}const s=t.startsWith("/")?t:`/${t}`,o=`${a}/api/v1/apps/${n}/api${s}`;return fetch(o,e)}class N{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 n=u(),s=p().replace(/^http/,"ws");this.url=`${s}/ws/app/${n}`,this.connect()}connect(){this._closed||(this.ws=new WebSocket(this.url),this.ws.onopen=e=>{var n;this.reconnectDelay=1e3,(n=this.onopen)==null||n.call(this,e)},this.ws.onmessage=e=>{var n;return(n=this.onmessage)==null?void 0:n.call(this,e)},this.ws.onerror=e=>{var n;return(n=this.onerror)==null?void 0:n.call(this,e)},this.ws.onclose=e=>{var n;(n=this.onclose)==null||n.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 n;(n=this.ws)==null||n.send(e)}close(e,n){var a;this._closed=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),(a=this.ws)==null||a.close(e,n)}}const A=new Set;let J=null;function me(){J||(J=new N,J.onmessage=t=>{let e;try{e=JSON.parse(t.data)}catch{return}e.type!=="app_data_changed"||!e.data||e.data.appId&&e.data.appId!==u()||A.forEach(n=>n(e.data))})}function be(t){if(typeof t!="string")return t;let e;try{e=JSON.parse(t)}catch{return t}if(typeof e!="string")return e;try{return JSON.parse(e)}catch{return e}}function O(t){const e=u(),n=p();return t?`${n}/api/v1/apps/${e}/storage/${encodeURIComponent(t)}`:`${n}/api/v1/apps/${e}/storage`}const ee={async getItem(t){const e=await fetch(O(t));if(e.status===404)return null;const n=await e.json();return be(n.value)},async setItem(t,e){const n=typeof e=="string"?e:JSON.stringify(e);await fetch(O(t),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({value:n})})},async removeItem(t){await fetch(O(t),{method:"DELETE"})},async clear(){const t=await this.keys();await Promise.all(t.map(e=>this.removeItem(e)))},async keys(){return((await(await fetch(O())).json()).items||[]).map(n=>n[0])},onChange(t){return A.add(t),me(),()=>{A.delete(t)}}};function B(t){const e=u();return`${p()}/api/v1/apps/${e}/agents/${t}`}const te={async invoke(t,e){return(await fetch(B("invoke"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:t,agent:e==null?void 0:e.agent,data:e==null?void 0:e.data})})).json()},async*stream(t,e){const n=await fetch(B("stream"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:t,agent:e==null?void 0:e.agent,data:e==null?void 0:e.data})});if(!n.body)throw new Error("No response body for streaming");const a=n.body.getReader(),s=new TextDecoder;let o="";for(;;){const{done:d,value:c}=await a.read();if(d)break;o+=s.decode(c,{stream:!0});const h=o.split(` +`);o=h.pop()||"";for(const l of h)if(l.startsWith("data: ")){const i=l.slice(6);if(i==="[DONE]")return;try{yield JSON.parse(i)}catch{yield{text:i,done:!1}}}}}};function q(t){const e=u();return`${p()}/api/v1/apps/${e}/janus/${t}`}const ne={async complete(t){const n=await(await fetch(q("complete"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json();return n.text||n.content||""},async*stream(t){const e=await fetch(q("stream"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!e.body)throw new Error("No response body for streaming");const n=e.body.getReader(),a=new TextDecoder;let s="";for(;;){const{done:o,value:d}=await n.read();if(o)break;s+=a.decode(d,{stream:!0});const c=s.split(` +`);s=c.pop()||"";for(const h of c)if(h.startsWith("data: ")){const l=h.slice(6);if(l==="[DONE]")return;try{yield JSON.parse(l).text}catch{yield l}}}}};async function ae(t){const e=await fetch(`${p()}/api/v1/apps/${u()}/janus/decide`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}),n=await e.json().catch(()=>({}));if(!e.ok)throw new Error(n.error||`decide failed (${e.status})`);return n}async function se(t){const e=new URLSearchParams(window.location.search),n=e.get("thread")||"",a=e.get("client")||"",s=await fetch(`${p()}/api/v1/apps/${u()}/share`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:t.name,content:t.content,thread:n,client:a})}),o=await s.json().catch(()=>({}));if(!s.ok)throw new Error(o.error||`share failed (${s.status})`);return o}const re={url(t){const e=t.split("/").filter(s=>s!=="");if(e.some(s=>s==="."||s===".."))throw new Error(`[nebo-sdk] files.url: "${t}" climbs out of its folder; name the file inside the granted folder`);const n=encodeURIComponent(u()),a=e.map(encodeURIComponent).join("/");return`${p()}/api/v1/apps/${n}/files/${a}`}};function oe(t){return`${p()}/api/v1/apps/${encodeURIComponent(u())}/plugins/${t}`}async function ce(t){const e=await t.text().catch(()=>"");return new Error(e||`plugin call failed (${t.status})`)}async function ye(t,e){const n=await fetch(oe(`jobs/${encodeURIComponent(t)}`),{headers:{Accept:"text/event-stream"}});if(!n.ok||!n.body)throw await ce(n);const a=n.body.getReader(),s=new TextDecoder;let o="",d="",c=[];for(;;){const{done:h,value:l}=await a.read();if(h)break;o+=s.decode(l,{stream:!0});const i=o.split(` +`);o=i.pop()||"";for(const x of i){const m=x.replace(/\r$/,"");if(m===""){if(d==="result")return JSON.parse(c.join(` +`));d==="progress"&&e(c.join(` +`)),d="",c=[]}else m.startsWith("event:")?d=m.slice(6).trim():m.startsWith("data:")&&c.push(m.slice(5).replace(/^ /,""))}}throw new Error("the plugin job ended without a result")}const ie={async run(t,e,n={},a={}){const s=typeof a.onProgress=="function",o=await fetch(oe(`${encodeURIComponent(t)}/run`),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({command:e,args:n,timeout:a.timeout,background:s})});if(!o.ok)throw await ce(o);const d=await o.json();return s?ye(d.job,a.onProgress):d}};class de{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 N,this.ws.onopen=()=>{this._connected=!0},this.ws.onmessage=e=>{var n,a;try{const s=JSON.parse(e.data);if(s.type==="a2ui_message"&&((n=s.data)!=null&&n.message)){(a=this._a2uiHandler)==null||a.call(this,s.data.message);return}this.handleEvent(s)}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,n){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(n),()=>{var a;(a=this.listeners.get(e))==null||a.delete(n)}}off(e,n){var a;(a=this.listeners.get(e))==null||a.delete(n)}send(e,n){!this.ws||this.ws.readyState!==WebSocket.OPEN||this.ws.send(JSON.stringify({type:"action",name:e,...n}))}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 n=this.listeners.get(e.type);if(n)for(const s of n)try{s(e)}catch{}const a=this.listeners.get("*");if(a)for(const s of a)try{s(e)}catch{}}applyDelta(e){for(const n of e){const a=n.path.split("/").filter(Boolean);if(a.length===0)continue;const s=this.resolveParent(a);if(!s)continue;const o=a[a.length-1];switch(n.op){case"add":case"replace":s[o]=n.value;break;case"remove":delete s[o];break}}}resolveParent(e){let n=this.state;for(let a=0;ae(t))}let R=!1;function E(){k||(R=!1,k=new N,k.onopen=()=>{R&&S({type:"reconnected"}),R=!0},k.onmessage=t=>{let e;try{e=JSON.parse(t.data)}catch{return}const n=e.data??{},a=we(n.session_id);if(a!==null)switch(e.type){case"chat_user_message":S({type:"message",thread:a,message:{id:String(n.id??""),role:"user",content:String(n.content??""),createdAt:Number(n.createdAt??Date.now())}});break;case"chat_stream":typeof n.content=="string"&&n.content&&S({type:"stream",thread:a,text:n.content});break;case"chat_complete":if(n.stop_reason==="queued_into_running_turn")break;S({type:"complete",thread:a,notice:typeof n.stop_notice=="string"?n.stop_notice:void 0});break;case"chat_error":S({type:"error",thread:a,error:String(n.error??"Something went wrong. Try again.")});break;case"chat_cancelled":S({type:"stopped",thread:a});break;case"chat_waiting":S({type:"waiting",thread:a});break}})}class W extends Error{constructor(e,n){super(n),this.status=e}}async function P(t,e){const n=await fetch(ue(t),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(!n.ok)throw new W(n.status,await n.text()||`chat: ${n.status}`);return n}const C={async history(t){const e=new URLSearchParams;t!=null&&t.thread&&e.set("thread",t.thread),t!=null&&t.before&&e.set("before",t.before),t!=null&&t.limit&&e.set("limit",String(t.limit));const n=e.toString();E();const a=await fetch(ue(n?`?${n}`:""));if(!a.ok)throw new Error(await a.text()||`chat: ${a.status}`);const s=await a.json();return{messages:s.messages??[],running:!!s.running,waiting:!!s.waiting,hasMore:!!s.hasMore}},async send(t,e){E(),await P("",{message:t,thread:e==null?void 0:e.thread,scope:e==null?void 0:e.scope,context:K})},async stop(t){E(),await P("/stop",{thread:t==null?void 0:t.thread})},async newThread(t){E(),await P("/new",{thread:t==null?void 0:t.thread}),f&&f.thread===((t==null?void 0:t.thread)||void 0)&&f.reset()},onMessage(t){return E(),U.add(t),()=>{U.delete(t),G()}},setContext(t){K=t},mount(t,e){f==null||f.destroy(),f=ve(t,e??{})},unmount(){f==null||f.destroy(),f=null,G()}};function G(){U.size||f||!k||(k.close(),k=null)}let f=null;const Y="nebo-chat-style",_e=` .nebo-chat{--nc-bg:#fff;--nc-fg:#16181d;--nc-muted:#6b7280;--nc-line:#e5e7eb;--nc-me:#16181d;--nc-me-fg:#fff;--nc-them:#f3f4f6;--nc-accent:#4f46e5; display:flex;flex-direction:column;height:100%;min-height:0;background:var(--nc-bg);color:var(--nc-fg); font:16px/1.6 "Geist",ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;border:1px solid var(--nc-line);border-radius:12px;overflow:hidden;box-sizing:border-box} @@ -30,4 +33,4 @@ .nebo-chat__button{flex:none;height:44px;min-width:44px;padding:0 16px;border:0;border-radius:22px;background:var(--nc-accent);color:#fff;font:600 15px/1 inherit;font-family:inherit;cursor:pointer} .nebo-chat__button:disabled{opacity:.4;cursor:default} .nebo-chat__button--stop{background:var(--nc-fg);color:var(--nc-bg)} -`;function be(){if(document.getElementById(Y))return;const t=document.createElement("style");t.id=Y,t.textContent=ge,document.head.appendChild(t)}function b(t,e){const n=document.createElement(t);return n.className=e,n}function Q(t,e){t.textContent="";const n=/(`[^`\n]+`|\*\*[^*\n]+\*\*|https?:\/\/[^\s<>()]+[^\s<>().,;:!?'"])/g;let a=0;for(const s of e.matchAll(n)){const o=s.index??0;o>a&&t.appendChild(document.createTextNode(e.slice(a,o)));const d=s[0];if(d.startsWith("`"))t.appendChild(document.createElement("code")).textContent=d.slice(1,-1);else if(d.startsWith("**"))t.appendChild(document.createElement("strong")).textContent=d.slice(2,-2);else{const i=t.appendChild(document.createElement("a"));i.href=d,i.target="_blank",i.rel="noopener noreferrer",i.textContent=d}a=o+d.length}ao.scrollHeight-o.scrollTop-o.clientHeight<48,k=(r=!1)=>{(r||U())&&(o.scrollTop=o.scrollHeight)},M=()=>{o.querySelector(".nebo-chat__msg")?S.remove():o.appendChild(S)},_=(r,g)=>{S.remove();const D=b("div",`nebo-chat__msg nebo-chat__msg--${r}`);return Q(D,g),o.appendChild(D),D},he=()=>{const r=b("div","nebo-chat__msg nebo-chat__msg--assistant");return r.appendChild(b("span","nebo-chat__dots")).append(document.createElement("i"),document.createElement("i"),document.createElement("i")),S.remove(),o.appendChild(r),r},F=()=>{!c||c.text||(c.node.classList.add("nebo-chat__msg--waiting"),c.node.textContent=e.name?`${e.name} is waiting for your OK in Nebo.`:"Waiting for your OK in Nebo.")},p=r=>{l=r,h.textContent=r?"Stop":"Send",h.classList.toggle("nebo-chat__button--stop",r),h.disabled=!r&&!i.value.trim(),r&&!c&&(c={node:he(),text:""},k(!0)),r||(c&&!c.text&&c.node.remove(),c=null,M())},L=()=>{i.style.height="auto",i.style.height=`${Math.min(i.scrollHeight,160)}px`,l||(h.disabled=!i.value.trim())},z=async()=>{p(!1),o.textContent="";try{const r=await C.history({thread:n});for(const g of r.messages)_(g.role,g.content);M(),p(r.running),r.running&&r.waiting&&F(),k(!0)}catch{_("error","Could not load the conversation. Try again.")}},ue=C.onMessage(r=>{if(r.type==="reconnected"){z();return}r.thread===n&&(r.type==="waiting"?(c||p(!0),F()):r.type==="message"?l||(_("user",r.message.content),p(!0)):r.type==="stream"?(c||p(!0),c&&(c.text+=r.text,c.node.classList.remove("nebo-chat__msg--waiting"),c.node.textContent="",Q(c.node,c.text),k())):r.type==="error"?(p(!1),_("error",r.error),k()):(p(!1),r.type==="complete"&&r.notice&&_("note",r.notice)))}),H=async()=>{if(l){try{await C.stop({thread:n})}catch{}return}const r=i.value.trim();if(r){i.value="",L(),_("user",r),p(!0);try{await C.send(r,{thread:n,scope:e.scope})}catch(g){p(!1),_("error",g instanceof W&&g.status===409?g.message:"Could not send. Try again.")}}};return d.addEventListener("submit",r=>{r.preventDefault(),H()}),i.addEventListener("input",L),i.addEventListener("keydown",r=>{r.key==="Enter"&&!r.shiftKey&&!r.isComposing&&(r.preventDefault(),H())}),p(!1),z(),{thread:n,reset(){o.textContent="",p(!1)},destroy(){ue(),s.remove()}}}class le{constructor(){this.fetch=Z,this.WebSocket=T,this.storage=ee,this.agents=te,this.janus=ne,this.decide=ae,this.share=se,this.files=re,this.surfaces=O,this.a2ui=I,this.identity=ie,this.chat=C,O._a2uiHandler=e=>I._handleMessage(e),I._setSendFn(e=>O._rawSend(e))}configure(e){e.appId&&V(e.appId),e.baseUrl&&X(e.baseUrl)}}const we=new le;exports.ChatError=W;exports.NeboA2UI=ce;exports.NeboSDK=le;exports.NeboSurfaces=oe;exports.NeboWebSocket=T;exports.a2ui=I;exports.agents=te;exports.chat=C;exports.decide=ae;exports.files=re;exports.getAppId=u;exports.getBaseUrl=m;exports.identity=ie;exports.janus=ne;exports.nebo=we;exports.neboFetch=Z;exports.setAppId=V;exports.setBaseUrl=X;exports.share=se;exports.storage=ee;exports.surfaces=O; +`;function xe(){if(document.getElementById(Y))return;const t=document.createElement("style");t.id=Y,t.textContent=_e,document.head.appendChild(t)}function y(t,e){const n=document.createElement(t);return n.className=e,n}function Q(t,e){t.textContent="";const n=/(`[^`\n]+`|\*\*[^*\n]+\*\*|https?:\/\/[^\s<>()]+[^\s<>().,;:!?'"])/g;let a=0;for(const s of e.matchAll(n)){const o=s.index??0;o>a&&t.appendChild(document.createTextNode(e.slice(a,o)));const d=s[0];if(d.startsWith("`"))t.appendChild(document.createElement("code")).textContent=d.slice(1,-1);else if(d.startsWith("**"))t.appendChild(document.createElement("strong")).textContent=d.slice(2,-2);else{const c=t.appendChild(document.createElement("a"));c.href=d,c.target="_blank",c.rel="noopener noreferrer",c.textContent=d}a=o+d.length}ao.scrollHeight-o.scrollTop-o.clientHeight<48,$=(r=!1)=>{(r||m())&&(o.scrollTop=o.scrollHeight)},M=()=>{o.querySelector(".nebo-chat__msg")?x.remove():o.appendChild(x)},v=(r,b)=>{x.remove();const D=y("div",`nebo-chat__msg nebo-chat__msg--${r}`);return Q(D,b),o.appendChild(D),D},pe=()=>{const r=y("div","nebo-chat__msg nebo-chat__msg--assistant");return r.appendChild(y("span","nebo-chat__dots")).append(document.createElement("i"),document.createElement("i"),document.createElement("i")),x.remove(),o.appendChild(r),r},F=()=>{!i||i.text||(i.node.classList.add("nebo-chat__msg--waiting"),i.node.textContent=e.name?`${e.name} is waiting for your OK in Nebo.`:"Waiting for your OK in Nebo.")},g=r=>{l=r,h.textContent=r?"Stop":"Send",h.classList.toggle("nebo-chat__button--stop",r),h.disabled=!r&&!c.value.trim(),r&&!i&&(i={node:pe(),text:""},$(!0)),r||(i&&!i.text&&i.node.remove(),i=null,M())},L=()=>{c.style.height="auto",c.style.height=`${Math.min(c.scrollHeight,160)}px`,l||(h.disabled=!c.value.trim())},z=async()=>{g(!1),o.textContent="";try{const r=await C.history({thread:n});for(const b of r.messages)v(b.role,b.content);M(),g(r.running),r.running&&r.waiting&&F(),$(!0)}catch{v("error","Could not load the conversation. Try again.")}},ge=C.onMessage(r=>{if(r.type==="reconnected"){z();return}r.thread===n&&(r.type==="waiting"?(i||g(!0),F()):r.type==="message"?l||(v("user",r.message.content),g(!0)):r.type==="stream"?(i||g(!0),i&&(i.text+=r.text,i.node.classList.remove("nebo-chat__msg--waiting"),i.node.textContent="",Q(i.node,i.text),$())):r.type==="error"?(g(!1),v("error",r.error),$()):(g(!1),r.type==="complete"&&r.notice&&v("note",r.notice)))}),H=async()=>{if(l){try{await C.stop({thread:n})}catch{}return}const r=c.value.trim();if(r){c.value="",L(),v("user",r),g(!0);try{await C.send(r,{thread:n,scope:e.scope})}catch(b){g(!1),v("error",b instanceof W&&b.status===409?b.message:"Could not send. Try again.")}}};return d.addEventListener("submit",r=>{r.preventDefault(),H()}),c.addEventListener("input",L),c.addEventListener("keydown",r=>{r.key==="Enter"&&!r.shiftKey&&!r.isComposing&&(r.preventDefault(),H())}),g(!1),z(),{thread:n,reset(){o.textContent="",g(!1)},destroy(){ge(),s.remove()}}}class fe{constructor(){this.fetch=Z,this.WebSocket=N,this.storage=ee,this.agents=te,this.janus=ne,this.decide=ae,this.share=se,this.files=re,this.plugins=ie,this.surfaces=I,this.a2ui=j,this.identity=he,this.chat=C,I._a2uiHandler=e=>j._handleMessage(e),j._setSendFn(e=>I._rawSend(e))}configure(e){e.appId&&V(e.appId),e.baseUrl&&X(e.baseUrl)}}const Se=new fe;exports.ChatError=W;exports.NeboA2UI=le;exports.NeboSDK=fe;exports.NeboSurfaces=de;exports.NeboWebSocket=N;exports.a2ui=j;exports.agents=te;exports.chat=C;exports.decide=ae;exports.files=re;exports.getAppId=u;exports.getBaseUrl=p;exports.identity=he;exports.janus=ne;exports.nebo=Se;exports.neboFetch=Z;exports.plugins=ie;exports.setAppId=V;exports.setBaseUrl=X;exports.share=se;exports.storage=ee;exports.surfaces=I; diff --git a/dist/index.d.ts b/dist/index.d.ts index beb660e..cb71de5 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -12,6 +12,7 @@ * - nebo.a2ui → agent-driven UI via @a2ui/web_core (A2UI v0.9) * - nebo.share → a file into the owner's Work, and Nebo's Share dialog on it * - nebo.files → the address of an owner's file in a folder the app is granted + * - nebo.plugins → run a plugin installed on the bot directly, no AI in the middle * - nebo.identity → agent context (name, persona, skills, inputs) * - nebo.chat → the page's conversation with its own employee, and a ready chat UI */ @@ -30,11 +31,13 @@ export { neboFetch } from './fetch'; export { identity } from './identity'; export { share } from './share'; export { files } from './files'; +export { plugins } from './plugins'; export { chat, ChatError } from './chat'; export { setAppId, setBaseUrl, getAppId, getBaseUrl } from './config'; export type { AgentResponse, InvokeOptions, JanusMessage, JanusOptions, StreamChunk, DecideQuestion, DecideRequest, DecideAnswer, Decision, } from './types'; export type { AgentIdentity } from './identity'; export type { ShareFile, Shared } from './share'; +export type { PluginResult, PluginRunOptions } from './plugins'; export type { ChatOptions, ChatContext, ChatMessage, ChatEvent, ChatHistory, SendOptions } from './chat'; export type { A2UIMessageProcessor } from './a2ui'; export type { SurfaceEvent, NeboSurfaceEvent, SurfaceEventMap, RunStartedEvent, RunFinishedEvent, RunErrorEvent, TextStartEvent, TextContentEvent, TextEndEvent, ToolCallStartEvent, ToolCallEndEvent, StateSnapshotEvent, StateDeltaEvent, SurfaceCreateEvent, SurfaceUpdateEvent, SurfaceDeleteEvent, DataUpdateEvent, CustomEvent, } from './surfaces'; diff --git a/dist/index.js b/dist/index.js index f35c865..2684b2c 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,40 +1,40 @@ -let y = null, w = null; -function f() { - if (y) return y; +let w = null, _ = null; +function u() { + if (w) return w; if (window.location.protocol === "neboapp:") - return y = window.location.hostname, y; + return w = window.location.hostname, w; const t = window.location.pathname.match(/^(?:\/t\/[^/]+)?(?:\/k\/[^/]+)?\/apps\/([^/]+)\/ui(?:\/|$)/); if (t) - return y = decodeURIComponent(t[1]), y; + return w = decodeURIComponent(t[1]), w; const e = document.querySelector('meta[name="nebo-app-id"]'); if (e) - return y = e.getAttribute("content") || "", y; + return w = e.getAttribute("content") || "", w; throw new Error( '[nebo-sdk] Cannot detect app ID. In production, serve from /apps/{id}/ui/. In dev, add to index.html.' ); } function m() { - if (w) return w; + if (_) return _; const t = document.querySelector('meta[name="nebo-base-url"]'); - return t ? (w = t.getAttribute("content") || "", w) : window.location.protocol === "neboapp:" ? (w = "http://localhost:27895", w) : (w = window.location.origin, w); -} -function ee(t) { - y = t; + return t ? (_ = t.getAttribute("content") || "", _) : window.location.protocol === "neboapp:" ? (_ = "http://localhost:27895", _) : (_ = window.location.origin, _); } -function te(t) { +function ne(t) { w = t; } -async function ne(t, e) { - const n = f(), a = m(); +function ae(t) { + _ = t; +} +async function se(t, e) { + const n = u(), a = m(); if (t.startsWith("http://") || t.startsWith("https://")) { - const d = `${a}/api/v1/apps/${n}/http/proxy`, i = {}; - e != null && e.headers && new Headers(e.headers).forEach((I, k) => { - i[k] = I; + const d = `${a}/api/v1/apps/${n}/http/proxy`, c = {}; + e != null && e.headers && new Headers(e.headers).forEach((g, $) => { + c[$] = g; }); const h = { url: t, method: (e == null ? void 0 : e.method) || "GET", - headers: i, + headers: c, body: e != null && e.body ? String(e.body) : void 0 }, l = await fetch(d, { method: "POST", @@ -42,19 +42,19 @@ async function ne(t, e) { body: JSON.stringify(h) }); if (!l.ok) return l; - const c = await l.json(); - return new Response(c.body, { - status: c.status, - headers: c.headers + const i = await l.json(); + return new Response(i.body, { + status: i.status, + headers: i.headers }); } const s = t.startsWith("/") ? t : `/${t}`, o = `${a}/api/v1/apps/${n}/api${s}`; return fetch(o, e); } -class N { +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 n = f(), s = m().replace(/^http/, "ws"); + const n = u(), s = m().replace(/^http/, "ws"); this.url = `${s}/ws/app/${n}`, this.connect(); } connect() { @@ -91,19 +91,19 @@ class N { } } const W = /* @__PURE__ */ new Set(); -let D = null; -function ae() { - D || (D = new N(), D.onmessage = (t) => { +let U = null; +function re() { + U || (U = new I(), U.onmessage = (t) => { let e; try { e = JSON.parse(t.data); } catch { return; } - e.type !== "app_data_changed" || !e.data || e.data.appId && e.data.appId !== f() || W.forEach((n) => n(e.data)); + e.type !== "app_data_changed" || !e.data || e.data.appId && e.data.appId !== u() || W.forEach((n) => n(e.data)); }); } -function se(t) { +function oe(t) { if (typeof t != "string") return t; let e; try { @@ -118,50 +118,50 @@ function se(t) { return e; } } -function T(t) { - const e = f(), n = m(); +function O(t) { + const e = u(), n = m(); return t ? `${n}/api/v1/apps/${e}/storage/${encodeURIComponent(t)}` : `${n}/api/v1/apps/${e}/storage`; } -const re = { +const ce = { async getItem(t) { - const e = await fetch(T(t)); + const e = await fetch(O(t)); if (e.status === 404) return null; const n = await e.json(); - return se(n.value); + return oe(n.value); }, async setItem(t, e) { const n = typeof e == "string" ? e : JSON.stringify(e); - await fetch(T(t), { + await fetch(O(t), { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ value: n }) }); }, async removeItem(t) { - await fetch(T(t), { method: "DELETE" }); + await fetch(O(t), { method: "DELETE" }); }, async clear() { const t = await this.keys(); await Promise.all(t.map((e) => this.removeItem(e))); }, async keys() { - return ((await (await fetch(T())).json()).items || []).map((n) => n[0]); + return ((await (await fetch(O())).json()).items || []).map((n) => n[0]); }, /** * Call `handler` after every change to the store: the employee's writes * and other open views'. Returns a function that stops listening. */ onChange(t) { - return W.add(t), ae(), () => { + return W.add(t), re(), () => { W.delete(t); }; } }; function H(t) { - const e = f(); + const e = u(); return `${m()}/api/v1/apps/${e}/agents/${t}`; } -const oe = { +const ie = { async invoke(t, e) { return (await fetch(H("invoke"), { method: "POST", @@ -188,30 +188,30 @@ const oe = { const a = n.body.getReader(), s = new TextDecoder(); let o = ""; for (; ; ) { - const { done: d, value: i } = await a.read(); + const { done: d, value: c } = await a.read(); if (d) break; - o += s.decode(i, { stream: !0 }); + o += s.decode(c, { stream: !0 }); const h = o.split(` `); o = h.pop() || ""; for (const l of h) if (l.startsWith("data: ")) { - const c = l.slice(6); - if (c === "[DONE]") return; + const i = l.slice(6); + if (i === "[DONE]") return; try { - yield JSON.parse(c); + yield JSON.parse(i); } catch { - yield { text: c, done: !1 }; + yield { text: i, done: !1 }; } } } } }; function q(t) { - const e = f(); + const e = u(); return `${m()}/api/v1/apps/${e}/janus/${t}`; } -const ce = { +const de = { async complete(t) { const n = await (await fetch(q("complete"), { method: "POST", @@ -234,10 +234,10 @@ const ce = { const { done: o, value: d } = await n.read(); if (o) break; s += a.decode(d, { stream: !0 }); - const i = s.split(` + const c = s.split(` `); - s = i.pop() || ""; - for (const h of i) + s = c.pop() || ""; + for (const h of c) if (h.startsWith("data: ")) { const l = h.slice(6); if (l === "[DONE]") return; @@ -250,8 +250,8 @@ const ce = { } } }; -async function ie(t) { - const e = await fetch(`${m()}/api/v1/apps/${f()}/janus/decide`, { +async function le(t) { + const e = await fetch(`${m()}/api/v1/apps/${u()}/janus/decide`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(t) @@ -260,8 +260,8 @@ async function ie(t) { throw new Error(n.error || `decide failed (${e.status})`); return n; } -async function de(t) { - const e = new URLSearchParams(window.location.search), n = e.get("thread") || "", a = e.get("client") || "", s = await fetch(`${m()}/api/v1/apps/${f()}/share`, { +async function he(t) { + const e = new URLSearchParams(window.location.search), n = e.get("thread") || "", a = e.get("client") || "", s = await fetch(`${m()}/api/v1/apps/${u()}/share`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: t.name, content: t.content, thread: n, client: a }) @@ -270,7 +270,7 @@ async function de(t) { throw new Error(o.error || `share failed (${s.status})`); return o; } -const le = { +const ue = { /** * `files.url('Media/Projects/reel/render.mp4')`. A path with a `.` or `..` * part names nothing inside a granted folder and is refused here. @@ -279,11 +279,60 @@ const le = { const e = t.split("/").filter((s) => s !== ""); if (e.some((s) => s === "." || s === "..")) throw new Error(`[nebo-sdk] files.url: "${t}" climbs out of its folder; name the file inside the granted folder`); - const n = encodeURIComponent(f()), a = e.map(encodeURIComponent).join("/"); + const n = encodeURIComponent(u()), a = e.map(encodeURIComponent).join("/"); return `${m()}/api/v1/apps/${n}/files/${a}`; } }; -class he { +function Q(t) { + return `${m()}/api/v1/apps/${encodeURIComponent(u())}/plugins/${t}`; +} +async function V(t) { + const e = await t.text().catch(() => ""); + return new Error(e || `plugin call failed (${t.status})`); +} +async function fe(t, e) { + const n = await fetch(Q(`jobs/${encodeURIComponent(t)}`), { + headers: { Accept: "text/event-stream" } + }); + if (!n.ok || !n.body) throw await V(n); + const a = n.body.getReader(), s = new TextDecoder(); + let o = "", d = "", c = []; + for (; ; ) { + const { done: h, value: l } = await a.read(); + if (h) break; + o += s.decode(l, { stream: !0 }); + const i = o.split(` +`); + o = i.pop() || ""; + for (const x of i) { + const g = x.replace(/\r$/, ""); + if (g === "") { + if (d === "result") return JSON.parse(c.join(` +`)); + d === "progress" && e(c.join(` +`)), d = "", c = []; + } else g.startsWith("event:") ? d = g.slice(6).trim() : g.startsWith("data:") && c.push(g.slice(5).replace(/^ /, "")); + } + } + throw new Error("the plugin job ended without a result"); +} +const pe = { + /** + * Run `command` on the installed plugin `slug`, with `args` as its flags + * (each key becomes `--key`; objects and lists go as JSON). + */ + async run(t, e, n = {}, a = {}) { + const s = typeof a.onProgress == "function", o = await fetch(Q(`${encodeURIComponent(t)}/run`), { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ command: e, args: n, timeout: a.timeout, background: s }) + }); + if (!o.ok) throw await V(o); + const d = await o.json(); + return s ? fe(d.job, a.onProgress) : d; + } +}; +class me { constructor() { this.ws = null, this.listeners = /* @__PURE__ */ new Map(), this._connected = !1, this._a2uiHandler = null, this.state = {}; } @@ -293,7 +342,7 @@ class he { } /** Connect to the app's surface WebSocket */ connect() { - this.ws || (this.ws = new N(), this.ws.onopen = () => { + this.ws || (this.ws = new I(), this.ws.onopen = () => { this._connected = !0; }, this.ws.onmessage = (e) => { var n, a; @@ -385,8 +434,8 @@ class he { return n; } } -const U = new he(); -class ue { +const D = new me(); +class ge { constructor() { this.processor = null, this.sendFn = null; } @@ -453,49 +502,49 @@ class ue { })); } } -const J = new ue(); -let $ = null; -const fe = { +const J = new ge(); +let C = null; +const be = { async get() { - if ($) return $; - const t = f(), e = m(), n = await fetch(`${e}/api/v1/apps/${t}/identity`); + if (C) return C; + const t = u(), e = m(), n = await fetch(`${e}/api/v1/apps/${t}/identity`); if (!n.ok) throw new Error(`[nebo-sdk] identity fetch failed: ${n.status}`); - return $ = await n.json(), $; + return C = await n.json(), C; }, invalidate() { - $ = null; + C = null; } -}, O = /* @__PURE__ */ new Set(); -let v = null, B = null; -function Q(t = "") { - return `${m()}/api/v1/apps/${encodeURIComponent(f())}/chat${t}`; +}, N = /* @__PURE__ */ new Set(); +let k = null, B = null; +function X(t = "") { + return `${m()}/api/v1/apps/${encodeURIComponent(u())}/chat${t}`; } -function pe(t) { +function ye(t) { if (typeof t != "string") return null; - const e = `agent:${f()}:page`; + const e = `agent:${u()}:page`; if (t !== e) return t.startsWith(e + ":") ? t.slice(e.length + 1) : null; } -function x(t) { - O.forEach((e) => e(t)); +function S(t) { + N.forEach((e) => e(t)); } let R = !1; -function C() { - v || (R = !1, v = new N(), v.onopen = () => { - R && x({ type: "reconnected" }), R = !0; - }, v.onmessage = (t) => { +function T() { + k || (R = !1, k = new I(), k.onopen = () => { + R && S({ type: "reconnected" }), R = !0; + }, k.onmessage = (t) => { let e; try { e = JSON.parse(t.data); } catch { return; } - const n = e.data ?? {}, a = pe(n.session_id); + const n = e.data ?? {}, a = ye(n.session_id); if (a !== null) switch (e.type) { case "chat_user_message": - x({ + S({ type: "message", thread: a, message: { @@ -507,36 +556,36 @@ function C() { }); break; case "chat_stream": - typeof n.content == "string" && n.content && x({ type: "stream", thread: a, text: n.content }); + typeof n.content == "string" && n.content && S({ type: "stream", thread: a, text: n.content }); break; case "chat_complete": if (n.stop_reason === "queued_into_running_turn") break; - x({ type: "complete", thread: a, notice: typeof n.stop_notice == "string" ? n.stop_notice : void 0 }); + S({ type: "complete", thread: a, notice: typeof n.stop_notice == "string" ? n.stop_notice : void 0 }); break; case "chat_error": - x({ type: "error", thread: a, error: String(n.error ?? "Something went wrong. Try again.") }); + S({ type: "error", thread: a, error: String(n.error ?? "Something went wrong. Try again.") }); break; case "chat_cancelled": - x({ type: "stopped", thread: a }); + S({ type: "stopped", thread: a }); break; case "chat_waiting": - x({ type: "waiting", thread: a }); + S({ type: "waiting", thread: a }); break; } }); } -class V extends Error { +class Z extends Error { constructor(e, n) { super(n), this.status = e; } } async function P(t, e) { - const n = await fetch(Q(t), { + const n = await fetch(X(t), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(e) }); - if (!n.ok) throw new V(n.status, await n.text() || `chat: ${n.status}`); + if (!n.ok) throw new Z(n.status, await n.text() || `chat: ${n.status}`); return n; } const E = { @@ -545,8 +594,8 @@ const E = { const e = new URLSearchParams(); t != null && t.thread && e.set("thread", t.thread), t != null && t.before && e.set("before", t.before), t != null && t.limit && e.set("limit", String(t.limit)); const n = e.toString(); - C(); - const a = await fetch(Q(n ? `?${n}` : "")); + T(); + const a = await fetch(X(n ? `?${n}` : "")); if (!a.ok) throw new Error(await a.text() || `chat: ${a.status}`); const s = await a.json(); return { messages: s.messages ?? [], running: !!s.running, waiting: !!s.waiting, hasMore: !!s.hasMore }; @@ -555,21 +604,21 @@ const E = { * One message at a time: while a reply is on its way, a send is refused * (stop it or wait). */ async send(t, e) { - C(), await P("", { message: t, thread: e == null ? void 0 : e.thread, scope: e == null ? void 0 : e.scope, context: B }); + T(), await P("", { message: t, thread: e == null ? void 0 : e.thread, scope: e == null ? void 0 : e.scope, context: B }); }, /** Stop the turn running in the conversation. */ async stop(t) { - C(), await P("/stop", { thread: t == null ? void 0 : t.thread }); + T(), await P("/stop", { thread: t == null ? void 0 : t.thread }); }, /** Start the conversation over; the one before stays with the owner. */ async newThread(t) { - C(), await P("/new", { thread: t == null ? void 0 : t.thread }), u && u.thread === ((t == null ? void 0 : t.thread) || void 0) && u.reset(); + T(), await P("/new", { thread: t == null ? void 0 : t.thread }), f && f.thread === ((t == null ? void 0 : t.thread) || void 0) && f.reset(); }, /** Hear the conversation live: words sent (from any open view), the * reply as it streams, and how each turn ends. Returns an unsubscribe. */ onMessage(t) { - return C(), O.add(t), () => { - O.delete(t), K(); + return T(), N.add(t), () => { + N.delete(t), K(); }; }, /** What is on the page, sent beside every message. `null` clears it. */ @@ -578,17 +627,17 @@ const E = { }, /** A ready chat with the employee, drawn inside `element`. */ mount(t, e) { - u == null || u.destroy(), u = be(t, e ?? {}); + f == null || f.destroy(), f = xe(t, e ?? {}); }, unmount() { - u == null || u.destroy(), u = null, K(); + f == null || f.destroy(), f = null, K(); } }; function K() { - O.size || u || !v || (v.close(), v = null); + N.size || f || !k || (k.close(), k = null); } -let u = null; -const G = "nebo-chat-style", me = ` +let f = null; +const G = "nebo-chat-style", we = ` .nebo-chat{--nc-bg:#fff;--nc-fg:#16181d;--nc-muted:#6b7280;--nc-line:#e5e7eb;--nc-me:#16181d;--nc-me-fg:#fff;--nc-them:#f3f4f6;--nc-accent:#4f46e5; display:flex;flex-direction:column;height:100%;min-height:0;background:var(--nc-bg);color:var(--nc-fg); font:16px/1.6 "Geist",ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;border:1px solid var(--nc-line);border-radius:12px;overflow:hidden;box-sizing:border-box} @@ -619,12 +668,12 @@ const G = "nebo-chat-style", me = ` .nebo-chat__button:disabled{opacity:.4;cursor:default} .nebo-chat__button--stop{background:var(--nc-fg);color:var(--nc-bg)} `; -function ge() { +function _e() { if (document.getElementById(G)) return; const t = document.createElement("style"); - t.id = G, t.textContent = me, document.head.appendChild(t); + t.id = G, t.textContent = we, document.head.appendChild(t); } -function b(t, e) { +function y(t, e) { const n = document.createElement(t); return n.className = e, n; } @@ -641,58 +690,58 @@ function Y(t, e) { else if (d.startsWith("**")) t.appendChild(document.createElement("strong")).textContent = d.slice(2, -2); else { - const i = t.appendChild(document.createElement("a")); - i.href = d, i.target = "_blank", i.rel = "noopener noreferrer", i.textContent = d; + const c = t.appendChild(document.createElement("a")); + c.href = d, c.target = "_blank", c.rel = "noopener noreferrer", c.textContent = d; } a = o + d.length; } a < e.length && t.appendChild(document.createTextNode(e.slice(a))); } -function be(t, e) { - ge(); - const n = e.contextId || void 0, a = e.theme ?? "auto", s = b("div", `nebo-chat nebo-chat--${a}${e.borderless ? " nebo-chat--borderless" : ""}`); +function xe(t, e) { + _e(); + const n = e.contextId || void 0, a = e.theme ?? "auto", s = y("div", `nebo-chat nebo-chat--${a}${e.borderless ? " nebo-chat--borderless" : ""}`); e.height && (s.style.height = e.height); - const o = b("div", "nebo-chat__log"); + const o = y("div", "nebo-chat__log"); o.setAttribute("role", "log"), o.setAttribute("aria-live", "polite"); - const d = b("form", "nebo-chat__composer"), i = b("textarea", "nebo-chat__input"); - i.rows = 1, i.setAttribute("aria-label", "Message"), i.placeholder = e.placeholder || (e.name ? `Message ${e.name}` : "Message"); - const h = b("button", "nebo-chat__button"); - h.type = "submit", d.append(i, h), s.append(o, d), t.appendChild(s); - let l = !1, c = null; - const S = b("div", "nebo-chat__empty"); - S.textContent = e.name ? `Ask ${e.name} anything.` : "Ask anything."; - const I = () => o.scrollHeight - o.scrollTop - o.clientHeight < 48, k = (r = !1) => { - (r || I()) && (o.scrollTop = o.scrollHeight); + const d = y("form", "nebo-chat__composer"), c = y("textarea", "nebo-chat__input"); + c.rows = 1, c.setAttribute("aria-label", "Message"), c.placeholder = e.placeholder || (e.name ? `Message ${e.name}` : "Message"); + const h = y("button", "nebo-chat__button"); + h.type = "submit", d.append(c, h), s.append(o, d), t.appendChild(s); + let l = !1, i = null; + const x = y("div", "nebo-chat__empty"); + x.textContent = e.name ? `Ask ${e.name} anything.` : "Ask anything."; + const g = () => o.scrollHeight - o.scrollTop - o.clientHeight < 48, $ = (r = !1) => { + (r || g()) && (o.scrollTop = o.scrollHeight); }, A = () => { - o.querySelector(".nebo-chat__msg") ? S.remove() : o.appendChild(S); - }, _ = (r, g) => { - S.remove(); - const j = b("div", `nebo-chat__msg nebo-chat__msg--${r}`); - return Y(j, g), o.appendChild(j), j; - }, X = () => { - const r = b("div", "nebo-chat__msg nebo-chat__msg--assistant"); - return r.appendChild(b("span", "nebo-chat__dots")).append(document.createElement("i"), document.createElement("i"), document.createElement("i")), S.remove(), o.appendChild(r), r; + o.querySelector(".nebo-chat__msg") ? x.remove() : o.appendChild(x); + }, v = (r, b) => { + x.remove(); + const j = y("div", `nebo-chat__msg nebo-chat__msg--${r}`); + return Y(j, b), o.appendChild(j), j; + }, ee = () => { + const r = y("div", "nebo-chat__msg nebo-chat__msg--assistant"); + return r.appendChild(y("span", "nebo-chat__dots")).append(document.createElement("i"), document.createElement("i"), document.createElement("i")), x.remove(), o.appendChild(r), r; }, M = () => { - !c || c.text || (c.node.classList.add("nebo-chat__msg--waiting"), c.node.textContent = e.name ? `${e.name} is waiting for your OK in Nebo.` : "Waiting for your OK in Nebo."); + !i || i.text || (i.node.classList.add("nebo-chat__msg--waiting"), i.node.textContent = e.name ? `${e.name} is waiting for your OK in Nebo.` : "Waiting for your OK in Nebo."); }, p = (r) => { - l = r, h.textContent = r ? "Stop" : "Send", h.classList.toggle("nebo-chat__button--stop", r), h.disabled = !r && !i.value.trim(), r && !c && (c = { node: X(), text: "" }, k(!0)), r || (c && !c.text && c.node.remove(), c = null, A()); + l = r, h.textContent = r ? "Stop" : "Send", h.classList.toggle("nebo-chat__button--stop", r), h.disabled = !r && !c.value.trim(), r && !i && (i = { node: ee(), text: "" }, $(!0)), r || (i && !i.text && i.node.remove(), i = null, A()); }, L = () => { - i.style.height = "auto", i.style.height = `${Math.min(i.scrollHeight, 160)}px`, l || (h.disabled = !i.value.trim()); + c.style.height = "auto", c.style.height = `${Math.min(c.scrollHeight, 160)}px`, l || (h.disabled = !c.value.trim()); }, F = async () => { p(!1), o.textContent = ""; try { const r = await E.history({ thread: n }); - for (const g of r.messages) _(g.role, g.content); - A(), p(r.running), r.running && r.waiting && M(), k(!0); + for (const b of r.messages) v(b.role, b.content); + A(), p(r.running), r.running && r.waiting && M(), $(!0); } catch { - _("error", "Could not load the conversation. Try again."); + v("error", "Could not load the conversation. Try again."); } - }, Z = E.onMessage((r) => { + }, te = E.onMessage((r) => { if (r.type === "reconnected") { F(); return; } - r.thread === n && (r.type === "waiting" ? (c || p(!0), M()) : r.type === "message" ? l || (_("user", r.message.content), p(!0)) : r.type === "stream" ? (c || p(!0), c && (c.text += r.text, c.node.classList.remove("nebo-chat__msg--waiting"), c.node.textContent = "", Y(c.node, c.text), k())) : r.type === "error" ? (p(!1), _("error", r.error), k()) : (p(!1), r.type === "complete" && r.notice && _("note", r.notice))); + r.thread === n && (r.type === "waiting" ? (i || p(!0), M()) : r.type === "message" ? l || (v("user", r.message.content), p(!0)) : r.type === "stream" ? (i || p(!0), i && (i.text += r.text, i.node.classList.remove("nebo-chat__msg--waiting"), i.node.textContent = "", Y(i.node, i.text), $())) : r.type === "error" ? (p(!1), v("error", r.error), $()) : (p(!1), r.type === "complete" && r.notice && v("note", r.notice))); }), z = async () => { if (l) { try { @@ -701,19 +750,19 @@ function be(t, e) { } return; } - const r = i.value.trim(); + const r = c.value.trim(); if (r) { - i.value = "", L(), _("user", r), p(!0); + c.value = "", L(), v("user", r), p(!0); try { await E.send(r, { thread: n, scope: e.scope }); - } catch (g) { - p(!1), _("error", g instanceof V && g.status === 409 ? g.message : "Could not send. Try again."); + } catch (b) { + p(!1), v("error", b instanceof Z && b.status === 409 ? b.message : "Could not send. Try again."); } } }; return d.addEventListener("submit", (r) => { r.preventDefault(), z(); - }), i.addEventListener("input", L), i.addEventListener("keydown", (r) => { + }), c.addEventListener("input", L), c.addEventListener("keydown", (r) => { r.key === "Enter" && !r.shiftKey && !r.isComposing && (r.preventDefault(), z()); }), p(!1), F(), { thread: n, @@ -721,42 +770,43 @@ function be(t, e) { o.textContent = "", p(!1); }, destroy() { - Z(), s.remove(); + te(), s.remove(); } }; } -class ye { +class ve { constructor() { - this.fetch = ne, this.WebSocket = N, this.storage = re, this.agents = oe, this.janus = ce, this.decide = ie, this.share = de, this.files = le, this.surfaces = U, this.a2ui = J, this.identity = fe, this.chat = E, U._a2uiHandler = (e) => J._handleMessage(e), J._setSendFn((e) => U._rawSend(e)); + this.fetch = se, this.WebSocket = I, this.storage = ce, this.agents = ie, this.janus = de, this.decide = le, this.share = he, this.files = ue, this.plugins = pe, this.surfaces = D, this.a2ui = J, this.identity = be, this.chat = E, D._a2uiHandler = (e) => J._handleMessage(e), J._setSendFn((e) => D._rawSend(e)); } /** * Manually configure the SDK (optional — auto-detection works in most cases). */ configure(e) { - e.appId && ee(e.appId), e.baseUrl && te(e.baseUrl); + e.appId && ne(e.appId), e.baseUrl && ae(e.baseUrl); } } -const we = new ye(); +const Se = new ve(); export { - V as ChatError, - ue as NeboA2UI, - ye as NeboSDK, - he as NeboSurfaces, - N as NeboWebSocket, + Z as ChatError, + ge as NeboA2UI, + ve as NeboSDK, + me as NeboSurfaces, + I as NeboWebSocket, J as a2ui, - oe as agents, + ie as agents, E as chat, - ie as decide, - le as files, - f as getAppId, + le as decide, + ue as files, + u as getAppId, m as getBaseUrl, - fe as identity, - ce as janus, - we as nebo, - ne as neboFetch, - ee as setAppId, - te as setBaseUrl, - de as share, - re as storage, - U as surfaces + be as identity, + de as janus, + Se as nebo, + se as neboFetch, + pe as plugins, + ne as setAppId, + ae as setBaseUrl, + he as share, + ce as storage, + D as surfaces }; diff --git a/dist/nebo.global.js b/dist/nebo.global.js index a2ac8a0..79a9e20 100644 --- a/dist/nebo.global.js +++ b/dist/nebo.global.js @@ -1,6 +1,9 @@ -var NeboAppSDK=(function(d){"use strict";let b=null,y=null;function f(){if(b)return b;if(window.location.protocol==="neboapp:")return b=window.location.hostname,b;const t=window.location.pathname.match(/^(?:\/t\/[^/]+)?(?:\/k\/[^/]+)?\/apps\/([^/]+)\/ui(?:\/|$)/);if(t)return b=decodeURIComponent(t[1]),b;const e=document.querySelector('meta[name="nebo-app-id"]');if(e)return b=e.getAttribute("content")||"",b;throw new Error('[nebo-sdk] Cannot detect app ID. In production, serve from /apps/{id}/ui/. In dev, add to index.html.')}function m(){if(y)return y;const t=document.querySelector('meta[name="nebo-base-url"]');return t?(y=t.getAttribute("content")||"",y):window.location.protocol==="neboapp:"?(y="http://localhost:27895",y):(y=window.location.origin,y)}function F(t){b=t}function L(t){y=t}async function z(t,e){const n=f(),a=m();if(t.startsWith("http://")||t.startsWith("https://")){const l=`${a}/api/v1/apps/${n}/http/proxy`,i={};e!=null&&e.headers&&new Headers(e.headers).forEach((W,$)=>{i[$]=W});const u={url:t,method:(e==null?void 0:e.method)||"GET",headers:i,body:e!=null&&e.body?String(e.body):void 0},h=await fetch(l,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(u)});if(!h.ok)return h;const c=await h.json();return new Response(c.body,{status:c.status,headers:c.headers})}const s=t.startsWith("/")?t:`/${t}`,o=`${a}/api/v1/apps/${n}/api${s}`;return fetch(o,e)}class E{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 n=f(),s=m().replace(/^http/,"ws");this.url=`${s}/ws/app/${n}`,this.connect()}connect(){this._closed||(this.ws=new WebSocket(this.url),this.ws.onopen=e=>{var n;this.reconnectDelay=1e3,(n=this.onopen)==null||n.call(this,e)},this.ws.onmessage=e=>{var n;return(n=this.onmessage)==null?void 0:n.call(this,e)},this.ws.onerror=e=>{var n;return(n=this.onerror)==null?void 0:n.call(this,e)},this.ws.onclose=e=>{var n;(n=this.onclose)==null||n.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 n;(n=this.ws)==null||n.send(e)}close(e,n){var a;this._closed=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),(a=this.ws)==null||a.close(e,n)}}const D=new Set;let J=null;function ue(){J||(J=new E,J.onmessage=t=>{let e;try{e=JSON.parse(t.data)}catch{return}e.type!=="app_data_changed"||!e.data||e.data.appId&&e.data.appId!==f()||D.forEach(n=>n(e.data))})}function fe(t){if(typeof t!="string")return t;let e;try{e=JSON.parse(t)}catch{return t}if(typeof e!="string")return e;try{return JSON.parse(e)}catch{return e}}function O(t){const e=f(),n=m();return t?`${n}/api/v1/apps/${e}/storage/${encodeURIComponent(t)}`:`${n}/api/v1/apps/${e}/storage`}const H={async getItem(t){const e=await fetch(O(t));if(e.status===404)return null;const n=await e.json();return fe(n.value)},async setItem(t,e){const n=typeof e=="string"?e:JSON.stringify(e);await fetch(O(t),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({value:n})})},async removeItem(t){await fetch(O(t),{method:"DELETE"})},async clear(){const t=await this.keys();await Promise.all(t.map(e=>this.removeItem(e)))},async keys(){return((await(await fetch(O())).json()).items||[]).map(n=>n[0])},onChange(t){return D.add(t),ue(),()=>{D.delete(t)}}};function B(t){const e=f();return`${m()}/api/v1/apps/${e}/agents/${t}`}const q={async invoke(t,e){return(await fetch(B("invoke"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:t,agent:e==null?void 0:e.agent,data:e==null?void 0:e.data})})).json()},async*stream(t,e){const n=await fetch(B("stream"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:t,agent:e==null?void 0:e.agent,data:e==null?void 0:e.data})});if(!n.body)throw new Error("No response body for streaming");const a=n.body.getReader(),s=new TextDecoder;let o="";for(;;){const{done:l,value:i}=await a.read();if(l)break;o+=s.decode(i,{stream:!0});const u=o.split(` -`);o=u.pop()||"";for(const h of u)if(h.startsWith("data: ")){const c=h.slice(6);if(c==="[DONE]")return;try{yield JSON.parse(c)}catch{yield{text:c,done:!1}}}}}};function K(t){const e=f();return`${m()}/api/v1/apps/${e}/janus/${t}`}const G={async complete(t){const n=await(await fetch(K("complete"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json();return n.text||n.content||""},async*stream(t){const e=await fetch(K("stream"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!e.body)throw new Error("No response body for streaming");const n=e.body.getReader(),a=new TextDecoder;let s="";for(;;){const{done:o,value:l}=await n.read();if(o)break;s+=a.decode(l,{stream:!0});const i=s.split(` -`);s=i.pop()||"";for(const u of i)if(u.startsWith("data: ")){const h=u.slice(6);if(h==="[DONE]")return;try{yield JSON.parse(h).text}catch{yield h}}}}};async function Y(t){const e=await fetch(`${m()}/api/v1/apps/${f()}/janus/decide`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}),n=await e.json().catch(()=>({}));if(!e.ok)throw new Error(n.error||`decide failed (${e.status})`);return n}async function Q(t){const e=new URLSearchParams(window.location.search),n=e.get("thread")||"",a=e.get("client")||"",s=await fetch(`${m()}/api/v1/apps/${f()}/share`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:t.name,content:t.content,thread:n,client:a})}),o=await s.json().catch(()=>({}));if(!s.ok)throw new Error(o.error||`share failed (${s.status})`);return o}const V={url(t){const e=t.split("/").filter(s=>s!=="");if(e.some(s=>s==="."||s===".."))throw new Error(`[nebo-sdk] files.url: "${t}" climbs out of its folder; name the file inside the granted folder`);const n=encodeURIComponent(f()),a=e.map(encodeURIComponent).join("/");return`${m()}/api/v1/apps/${n}/files/${a}`}};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 E,this.ws.onopen=()=>{this._connected=!0},this.ws.onmessage=e=>{var n,a;try{const s=JSON.parse(e.data);if(s.type==="a2ui_message"&&((n=s.data)!=null&&n.message)){(a=this._a2uiHandler)==null||a.call(this,s.data.message);return}this.handleEvent(s)}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,n){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(n),()=>{var a;(a=this.listeners.get(e))==null||a.delete(n)}}off(e,n){var a;(a=this.listeners.get(e))==null||a.delete(n)}send(e,n){!this.ws||this.ws.readyState!==WebSocket.OPEN||this.ws.send(JSON.stringify({type:"action",name:e,...n}))}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 n=this.listeners.get(e.type);if(n)for(const s of n)try{s(e)}catch{}const a=this.listeners.get("*");if(a)for(const s of a)try{s(e)}catch{}}applyDelta(e){for(const n of e){const a=n.path.split("/").filter(Boolean);if(a.length===0)continue;const s=this.resolveParent(a);if(!s)continue;const o=a[a.length-1];switch(n.op){case"add":case"replace":s[o]=n.value;break;case"remove":delete s[o];break}}}resolveParent(e){let n=this.state;for(let a=0;ae(t))}let A=!1;function N(){v||(A=!1,v=new E,v.onopen=()=>{A&&x({type:"reconnected"}),A=!0},v.onmessage=t=>{let e;try{e=JSON.parse(t.data)}catch{return}const n=e.data??{},a=pe(n.session_id);if(a!==null)switch(e.type){case"chat_user_message":x({type:"message",thread:a,message:{id:String(n.id??""),role:"user",content:String(n.content??""),createdAt:Number(n.createdAt??Date.now())}});break;case"chat_stream":typeof n.content=="string"&&n.content&&x({type:"stream",thread:a,text:n.content});break;case"chat_complete":if(n.stop_reason==="queued_into_running_turn")break;x({type:"complete",thread:a,notice:typeof n.stop_notice=="string"?n.stop_notice:void 0});break;case"chat_error":x({type:"error",thread:a,error:String(n.error??"Something went wrong. Try again.")});break;case"chat_cancelled":x({type:"stopped",thread:a});break;case"chat_waiting":x({type:"waiting",thread:a});break}})}class P extends Error{constructor(e,n){super(n),this.status=e}}async function R(t,e){const n=await fetch(ne(t),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(!n.ok)throw new P(n.status,await n.text()||`chat: ${n.status}`);return n}const k={async history(t){const e=new URLSearchParams;t!=null&&t.thread&&e.set("thread",t.thread),t!=null&&t.before&&e.set("before",t.before),t!=null&&t.limit&&e.set("limit",String(t.limit));const n=e.toString();N();const a=await fetch(ne(n?`?${n}`:""));if(!a.ok)throw new Error(await a.text()||`chat: ${a.status}`);const s=await a.json();return{messages:s.messages??[],running:!!s.running,waiting:!!s.waiting,hasMore:!!s.hasMore}},async send(t,e){N(),await R("",{message:t,thread:e==null?void 0:e.thread,scope:e==null?void 0:e.scope,context:te})},async stop(t){N(),await R("/stop",{thread:t==null?void 0:t.thread})},async newThread(t){N(),await R("/new",{thread:t==null?void 0:t.thread}),p&&p.thread===((t==null?void 0:t.thread)||void 0)&&p.reset()},onMessage(t){return N(),U.add(t),()=>{U.delete(t),ae()}},setContext(t){te=t},mount(t,e){p==null||p.destroy(),p=be(t,e??{})},unmount(){p==null||p.destroy(),p=null,ae()}};function ae(){U.size||p||!v||(v.close(),v=null)}let p=null;const se="nebo-chat-style",me=` +var NeboAppSDK=(function(l){"use strict";let b=null,y=null;function f(){if(b)return b;if(window.location.protocol==="neboapp:")return b=window.location.hostname,b;const t=window.location.pathname.match(/^(?:\/t\/[^/]+)?(?:\/k\/[^/]+)?\/apps\/([^/]+)\/ui(?:\/|$)/);if(t)return b=decodeURIComponent(t[1]),b;const e=document.querySelector('meta[name="nebo-app-id"]');if(e)return b=e.getAttribute("content")||"",b;throw new Error('[nebo-sdk] Cannot detect app ID. In production, serve from /apps/{id}/ui/. In dev, add to index.html.')}function p(){if(y)return y;const t=document.querySelector('meta[name="nebo-base-url"]');return t?(y=t.getAttribute("content")||"",y):window.location.protocol==="neboapp:"?(y="http://localhost:27895",y):(y=window.location.origin,y)}function F(t){b=t}function L(t){y=t}async function z(t,e){const n=f(),a=p();if(t.startsWith("http://")||t.startsWith("https://")){const d=`${a}/api/v1/apps/${n}/http/proxy`,c={};e!=null&&e.headers&&new Headers(e.headers).forEach((_,T)=>{c[T]=_});const u={url:t,method:(e==null?void 0:e.method)||"GET",headers:c,body:e!=null&&e.body?String(e.body):void 0},h=await fetch(d,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(u)});if(!h.ok)return h;const i=await h.json();return new Response(i.body,{status:i.status,headers:i.headers})}const s=t.startsWith("/")?t:`/${t}`,o=`${a}/api/v1/apps/${n}/api${s}`;return fetch(o,e)}class E{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 n=f(),s=p().replace(/^http/,"ws");this.url=`${s}/ws/app/${n}`,this.connect()}connect(){this._closed||(this.ws=new WebSocket(this.url),this.ws.onopen=e=>{var n;this.reconnectDelay=1e3,(n=this.onopen)==null||n.call(this,e)},this.ws.onmessage=e=>{var n;return(n=this.onmessage)==null?void 0:n.call(this,e)},this.ws.onerror=e=>{var n;return(n=this.onerror)==null?void 0:n.call(this,e)},this.ws.onclose=e=>{var n;(n=this.onclose)==null||n.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 n;(n=this.ws)==null||n.send(e)}close(e,n){var a;this._closed=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),(a=this.ws)==null||a.close(e,n)}}const J=new Set;let R=null;function ge(){R||(R=new E,R.onmessage=t=>{let e;try{e=JSON.parse(t.data)}catch{return}e.type!=="app_data_changed"||!e.data||e.data.appId&&e.data.appId!==f()||J.forEach(n=>n(e.data))})}function me(t){if(typeof t!="string")return t;let e;try{e=JSON.parse(t)}catch{return t}if(typeof e!="string")return e;try{return JSON.parse(e)}catch{return e}}function I(t){const e=f(),n=p();return t?`${n}/api/v1/apps/${e}/storage/${encodeURIComponent(t)}`:`${n}/api/v1/apps/${e}/storage`}const H={async getItem(t){const e=await fetch(I(t));if(e.status===404)return null;const n=await e.json();return me(n.value)},async setItem(t,e){const n=typeof e=="string"?e:JSON.stringify(e);await fetch(I(t),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({value:n})})},async removeItem(t){await fetch(I(t),{method:"DELETE"})},async clear(){const t=await this.keys();await Promise.all(t.map(e=>this.removeItem(e)))},async keys(){return((await(await fetch(I())).json()).items||[]).map(n=>n[0])},onChange(t){return J.add(t),ge(),()=>{J.delete(t)}}};function B(t){const e=f();return`${p()}/api/v1/apps/${e}/agents/${t}`}const q={async invoke(t,e){return(await fetch(B("invoke"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:t,agent:e==null?void 0:e.agent,data:e==null?void 0:e.data})})).json()},async*stream(t,e){const n=await fetch(B("stream"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:t,agent:e==null?void 0:e.agent,data:e==null?void 0:e.data})});if(!n.body)throw new Error("No response body for streaming");const a=n.body.getReader(),s=new TextDecoder;let o="";for(;;){const{done:d,value:c}=await a.read();if(d)break;o+=s.decode(c,{stream:!0});const u=o.split(` +`);o=u.pop()||"";for(const h of u)if(h.startsWith("data: ")){const i=h.slice(6);if(i==="[DONE]")return;try{yield JSON.parse(i)}catch{yield{text:i,done:!1}}}}}};function K(t){const e=f();return`${p()}/api/v1/apps/${e}/janus/${t}`}const G={async complete(t){const n=await(await fetch(K("complete"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json();return n.text||n.content||""},async*stream(t){const e=await fetch(K("stream"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!e.body)throw new Error("No response body for streaming");const n=e.body.getReader(),a=new TextDecoder;let s="";for(;;){const{done:o,value:d}=await n.read();if(o)break;s+=a.decode(d,{stream:!0});const c=s.split(` +`);s=c.pop()||"";for(const u of c)if(u.startsWith("data: ")){const h=u.slice(6);if(h==="[DONE]")return;try{yield JSON.parse(h).text}catch{yield h}}}}};async function Y(t){const e=await fetch(`${p()}/api/v1/apps/${f()}/janus/decide`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}),n=await e.json().catch(()=>({}));if(!e.ok)throw new Error(n.error||`decide failed (${e.status})`);return n}async function Q(t){const e=new URLSearchParams(window.location.search),n=e.get("thread")||"",a=e.get("client")||"",s=await fetch(`${p()}/api/v1/apps/${f()}/share`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:t.name,content:t.content,thread:n,client:a})}),o=await s.json().catch(()=>({}));if(!s.ok)throw new Error(o.error||`share failed (${s.status})`);return o}const V={url(t){const e=t.split("/").filter(s=>s!=="");if(e.some(s=>s==="."||s===".."))throw new Error(`[nebo-sdk] files.url: "${t}" climbs out of its folder; name the file inside the granted folder`);const n=encodeURIComponent(f()),a=e.map(encodeURIComponent).join("/");return`${p()}/api/v1/apps/${n}/files/${a}`}};function X(t){return`${p()}/api/v1/apps/${encodeURIComponent(f())}/plugins/${t}`}async function Z(t){const e=await t.text().catch(()=>"");return new Error(e||`plugin call failed (${t.status})`)}async function be(t,e){const n=await fetch(X(`jobs/${encodeURIComponent(t)}`),{headers:{Accept:"text/event-stream"}});if(!n.ok||!n.body)throw await Z(n);const a=n.body.getReader(),s=new TextDecoder;let o="",d="",c=[];for(;;){const{done:u,value:h}=await a.read();if(u)break;o+=s.decode(h,{stream:!0});const i=o.split(` +`);o=i.pop()||"";for(const x of i){const _=x.replace(/\r$/,"");if(_===""){if(d==="result")return JSON.parse(c.join(` +`));d==="progress"&&e(c.join(` +`)),d="",c=[]}else _.startsWith("event:")?d=_.slice(6).trim():_.startsWith("data:")&&c.push(_.slice(5).replace(/^ /,""))}}throw new Error("the plugin job ended without a result")}const ee={async run(t,e,n={},a={}){const s=typeof a.onProgress=="function",o=await fetch(X(`${encodeURIComponent(t)}/run`),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({command:e,args:n,timeout:a.timeout,background:s})});if(!o.ok)throw await Z(o);const d=await o.json();return s?be(d.job,a.onProgress):d}};class te{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 E,this.ws.onopen=()=>{this._connected=!0},this.ws.onmessage=e=>{var n,a;try{const s=JSON.parse(e.data);if(s.type==="a2ui_message"&&((n=s.data)!=null&&n.message)){(a=this._a2uiHandler)==null||a.call(this,s.data.message);return}this.handleEvent(s)}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,n){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(n),()=>{var a;(a=this.listeners.get(e))==null||a.delete(n)}}off(e,n){var a;(a=this.listeners.get(e))==null||a.delete(n)}send(e,n){!this.ws||this.ws.readyState!==WebSocket.OPEN||this.ws.send(JSON.stringify({type:"action",name:e,...n}))}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 n=this.listeners.get(e.type);if(n)for(const s of n)try{s(e)}catch{}const a=this.listeners.get("*");if(a)for(const s of a)try{s(e)}catch{}}applyDelta(e){for(const n of e){const a=n.path.split("/").filter(Boolean);if(a.length===0)continue;const s=this.resolveParent(a);if(!s)continue;const o=a[a.length-1];switch(n.op){case"add":case"replace":s[o]=n.value;break;case"remove":delete s[o];break}}}resolveParent(e){let n=this.state;for(let a=0;ae(t))}let A=!1;function O(){S||(A=!1,S=new E,S.onopen=()=>{A&&k({type:"reconnected"}),A=!0},S.onmessage=t=>{let e;try{e=JSON.parse(t.data)}catch{return}const n=e.data??{},a=ye(n.session_id);if(a!==null)switch(e.type){case"chat_user_message":k({type:"message",thread:a,message:{id:String(n.id??""),role:"user",content:String(n.content??""),createdAt:Number(n.createdAt??Date.now())}});break;case"chat_stream":typeof n.content=="string"&&n.content&&k({type:"stream",thread:a,text:n.content});break;case"chat_complete":if(n.stop_reason==="queued_into_running_turn")break;k({type:"complete",thread:a,notice:typeof n.stop_notice=="string"?n.stop_notice:void 0});break;case"chat_error":k({type:"error",thread:a,error:String(n.error??"Something went wrong. Try again.")});break;case"chat_cancelled":k({type:"stopped",thread:a});break;case"chat_waiting":k({type:"waiting",thread:a});break}})}class P extends Error{constructor(e,n){super(n),this.status=e}}async function W(t,e){const n=await fetch(re(t),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(!n.ok)throw new P(n.status,await n.text()||`chat: ${n.status}`);return n}const C={async history(t){const e=new URLSearchParams;t!=null&&t.thread&&e.set("thread",t.thread),t!=null&&t.before&&e.set("before",t.before),t!=null&&t.limit&&e.set("limit",String(t.limit));const n=e.toString();O();const a=await fetch(re(n?`?${n}`:""));if(!a.ok)throw new Error(await a.text()||`chat: ${a.status}`);const s=await a.json();return{messages:s.messages??[],running:!!s.running,waiting:!!s.waiting,hasMore:!!s.hasMore}},async send(t,e){O(),await W("",{message:t,thread:e==null?void 0:e.thread,scope:e==null?void 0:e.scope,context:se})},async stop(t){O(),await W("/stop",{thread:t==null?void 0:t.thread})},async newThread(t){O(),await W("/new",{thread:t==null?void 0:t.thread}),g&&g.thread===((t==null?void 0:t.thread)||void 0)&&g.reset()},onMessage(t){return O(),D.add(t),()=>{D.delete(t),oe()}},setContext(t){se=t},mount(t,e){g==null||g.destroy(),g=ve(t,e??{})},unmount(){g==null||g.destroy(),g=null,oe()}};function oe(){D.size||g||!S||(S.close(),S=null)}let g=null;const ce="nebo-chat-style",we=` .nebo-chat{--nc-bg:#fff;--nc-fg:#16181d;--nc-muted:#6b7280;--nc-line:#e5e7eb;--nc-me:#16181d;--nc-me-fg:#fff;--nc-them:#f3f4f6;--nc-accent:#4f46e5; display:flex;flex-direction:column;height:100%;min-height:0;background:var(--nc-bg);color:var(--nc-fg); font:16px/1.6 "Geist",ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;border:1px solid var(--nc-line);border-radius:12px;overflow:hidden;box-sizing:border-box} @@ -30,4 +33,4 @@ var NeboAppSDK=(function(d){"use strict";let b=null,y=null;function f(){if(b)ret .nebo-chat__button{flex:none;height:44px;min-width:44px;padding:0 16px;border:0;border-radius:22px;background:var(--nc-accent);color:#fff;font:600 15px/1 inherit;font-family:inherit;cursor:pointer} .nebo-chat__button:disabled{opacity:.4;cursor:default} .nebo-chat__button--stop{background:var(--nc-fg);color:var(--nc-bg)} -`;function ge(){if(document.getElementById(se))return;const t=document.createElement("style");t.id=se,t.textContent=me,document.head.appendChild(t)}function w(t,e){const n=document.createElement(t);return n.className=e,n}function re(t,e){t.textContent="";const n=/(`[^`\n]+`|\*\*[^*\n]+\*\*|https?:\/\/[^\s<>()]+[^\s<>().,;:!?'"])/g;let a=0;for(const s of e.matchAll(n)){const o=s.index??0;o>a&&t.appendChild(document.createTextNode(e.slice(a,o)));const l=s[0];if(l.startsWith("`"))t.appendChild(document.createElement("code")).textContent=l.slice(1,-1);else if(l.startsWith("**"))t.appendChild(document.createElement("strong")).textContent=l.slice(2,-2);else{const i=t.appendChild(document.createElement("a"));i.href=l,i.target="_blank",i.rel="noopener noreferrer",i.textContent=l}a=o+l.length}ao.scrollHeight-o.scrollTop-o.clientHeight<48,$=(r=!1)=>{(r||W())&&(o.scrollTop=o.scrollHeight)},ce=()=>{o.querySelector(".nebo-chat__msg")?C.remove():o.appendChild(C)},S=(r,_)=>{C.remove();const M=w("div",`nebo-chat__msg nebo-chat__msg--${r}`);return re(M,_),o.appendChild(M),M},we=()=>{const r=w("div","nebo-chat__msg nebo-chat__msg--assistant");return r.appendChild(w("span","nebo-chat__dots")).append(document.createElement("i"),document.createElement("i"),document.createElement("i")),C.remove(),o.appendChild(r),r},ie=()=>{!c||c.text||(c.node.classList.add("nebo-chat__msg--waiting"),c.node.textContent=e.name?`${e.name} is waiting for your OK in Nebo.`:"Waiting for your OK in Nebo.")},g=r=>{h=r,u.textContent=r?"Stop":"Send",u.classList.toggle("nebo-chat__button--stop",r),u.disabled=!r&&!i.value.trim(),r&&!c&&(c={node:we(),text:""},$(!0)),r||(c&&!c.text&&c.node.remove(),c=null,ce())},de=()=>{i.style.height="auto",i.style.height=`${Math.min(i.scrollHeight,160)}px`,h||(u.disabled=!i.value.trim())},le=async()=>{g(!1),o.textContent="";try{const r=await k.history({thread:n});for(const _ of r.messages)S(_.role,_.content);ce(),g(r.running),r.running&&r.waiting&&ie(),$(!0)}catch{S("error","Could not load the conversation. Try again.")}},_e=k.onMessage(r=>{if(r.type==="reconnected"){le();return}r.thread===n&&(r.type==="waiting"?(c||g(!0),ie()):r.type==="message"?h||(S("user",r.message.content),g(!0)):r.type==="stream"?(c||g(!0),c&&(c.text+=r.text,c.node.classList.remove("nebo-chat__msg--waiting"),c.node.textContent="",re(c.node,c.text),$())):r.type==="error"?(g(!1),S("error",r.error),$()):(g(!1),r.type==="complete"&&r.notice&&S("note",r.notice)))}),he=async()=>{if(h){try{await k.stop({thread:n})}catch{}return}const r=i.value.trim();if(r){i.value="",de(),S("user",r),g(!0);try{await k.send(r,{thread:n,scope:e.scope})}catch(_){g(!1),S("error",_ instanceof P&&_.status===409?_.message:"Could not send. Try again.")}}};return l.addEventListener("submit",r=>{r.preventDefault(),he()}),i.addEventListener("input",de),i.addEventListener("keydown",r=>{r.key==="Enter"&&!r.shiftKey&&!r.isComposing&&(r.preventDefault(),he())}),g(!1),le(),{thread:n,reset(){o.textContent="",g(!1)},destroy(){_e(),s.remove()}}}class oe{constructor(){this.fetch=z,this.WebSocket=E,this.storage=H,this.agents=q,this.janus=G,this.decide=Y,this.share=Q,this.files=V,this.surfaces=I,this.a2ui=j,this.identity=ee,this.chat=k,I._a2uiHandler=e=>j._handleMessage(e),j._setSendFn(e=>I._rawSend(e))}configure(e){e.appId&&F(e.appId),e.baseUrl&&L(e.baseUrl)}}const ye=new oe;return d.ChatError=P,d.NeboA2UI=Z,d.NeboSDK=oe,d.NeboSurfaces=X,d.NeboWebSocket=E,d.a2ui=j,d.agents=q,d.chat=k,d.decide=Y,d.files=V,d.getAppId=f,d.getBaseUrl=m,d.identity=ee,d.janus=G,d.nebo=ye,d.neboFetch=z,d.setAppId=F,d.setBaseUrl=L,d.share=Q,d.storage=H,d.surfaces=I,Object.defineProperty(d,Symbol.toStringTag,{value:"Module"}),d})({}); +`;function _e(){if(document.getElementById(ce))return;const t=document.createElement("style");t.id=ce,t.textContent=we,document.head.appendChild(t)}function w(t,e){const n=document.createElement(t);return n.className=e,n}function ie(t,e){t.textContent="";const n=/(`[^`\n]+`|\*\*[^*\n]+\*\*|https?:\/\/[^\s<>()]+[^\s<>().,;:!?'"])/g;let a=0;for(const s of e.matchAll(n)){const o=s.index??0;o>a&&t.appendChild(document.createTextNode(e.slice(a,o)));const d=s[0];if(d.startsWith("`"))t.appendChild(document.createElement("code")).textContent=d.slice(1,-1);else if(d.startsWith("**"))t.appendChild(document.createElement("strong")).textContent=d.slice(2,-2);else{const c=t.appendChild(document.createElement("a"));c.href=d,c.target="_blank",c.rel="noopener noreferrer",c.textContent=d}a=o+d.length}ao.scrollHeight-o.scrollTop-o.clientHeight<48,T=(r=!1)=>{(r||_())&&(o.scrollTop=o.scrollHeight)},le=()=>{o.querySelector(".nebo-chat__msg")?x.remove():o.appendChild(x)},$=(r,v)=>{x.remove();const M=w("div",`nebo-chat__msg nebo-chat__msg--${r}`);return ie(M,v),o.appendChild(M),M},Se=()=>{const r=w("div","nebo-chat__msg nebo-chat__msg--assistant");return r.appendChild(w("span","nebo-chat__dots")).append(document.createElement("i"),document.createElement("i"),document.createElement("i")),x.remove(),o.appendChild(r),r},he=()=>{!i||i.text||(i.node.classList.add("nebo-chat__msg--waiting"),i.node.textContent=e.name?`${e.name} is waiting for your OK in Nebo.`:"Waiting for your OK in Nebo.")},m=r=>{h=r,u.textContent=r?"Stop":"Send",u.classList.toggle("nebo-chat__button--stop",r),u.disabled=!r&&!c.value.trim(),r&&!i&&(i={node:Se(),text:""},T(!0)),r||(i&&!i.text&&i.node.remove(),i=null,le())},ue=()=>{c.style.height="auto",c.style.height=`${Math.min(c.scrollHeight,160)}px`,h||(u.disabled=!c.value.trim())},fe=async()=>{m(!1),o.textContent="";try{const r=await C.history({thread:n});for(const v of r.messages)$(v.role,v.content);le(),m(r.running),r.running&&r.waiting&&he(),T(!0)}catch{$("error","Could not load the conversation. Try again.")}},ke=C.onMessage(r=>{if(r.type==="reconnected"){fe();return}r.thread===n&&(r.type==="waiting"?(i||m(!0),he()):r.type==="message"?h||($("user",r.message.content),m(!0)):r.type==="stream"?(i||m(!0),i&&(i.text+=r.text,i.node.classList.remove("nebo-chat__msg--waiting"),i.node.textContent="",ie(i.node,i.text),T())):r.type==="error"?(m(!1),$("error",r.error),T()):(m(!1),r.type==="complete"&&r.notice&&$("note",r.notice)))}),pe=async()=>{if(h){try{await C.stop({thread:n})}catch{}return}const r=c.value.trim();if(r){c.value="",ue(),$("user",r),m(!0);try{await C.send(r,{thread:n,scope:e.scope})}catch(v){m(!1),$("error",v instanceof P&&v.status===409?v.message:"Could not send. Try again.")}}};return d.addEventListener("submit",r=>{r.preventDefault(),pe()}),c.addEventListener("input",ue),c.addEventListener("keydown",r=>{r.key==="Enter"&&!r.shiftKey&&!r.isComposing&&(r.preventDefault(),pe())}),m(!1),fe(),{thread:n,reset(){o.textContent="",m(!1)},destroy(){ke(),s.remove()}}}class de{constructor(){this.fetch=z,this.WebSocket=E,this.storage=H,this.agents=q,this.janus=G,this.decide=Y,this.share=Q,this.files=V,this.plugins=ee,this.surfaces=j,this.a2ui=U,this.identity=ae,this.chat=C,j._a2uiHandler=e=>U._handleMessage(e),U._setSendFn(e=>j._rawSend(e))}configure(e){e.appId&&F(e.appId),e.baseUrl&&L(e.baseUrl)}}const xe=new de;return l.ChatError=P,l.NeboA2UI=ne,l.NeboSDK=de,l.NeboSurfaces=te,l.NeboWebSocket=E,l.a2ui=U,l.agents=q,l.chat=C,l.decide=Y,l.files=V,l.getAppId=f,l.getBaseUrl=p,l.identity=ae,l.janus=G,l.nebo=xe,l.neboFetch=z,l.plugins=ee,l.setAppId=F,l.setBaseUrl=L,l.share=Q,l.storage=H,l.surfaces=j,Object.defineProperty(l,Symbol.toStringTag,{value:"Module"}),l})({}); diff --git a/dist/plugins.d.ts b/dist/plugins.d.ts new file mode 100644 index 0000000..82dca1d --- /dev/null +++ b/dist/plugins.d.ts @@ -0,0 +1,37 @@ +/** + * nebo.plugins — run a plugin installed on the bot directly, with no AI in + * the middle. The call is the one the app's employee makes with its + * `plugin__` tool (a command and its flags), run by Nebo the same way + * (the same installed version, account and billing), and its result comes + * straight back. + * + * The app's manifest grants the plugin: `plugin:` for every command, + * `plugin::` for one. An app installed from the marketplace + * also needs the owner's OK. A call the app may not make is refused. + */ +/** What a plugin command came to. */ +export interface PluginResult { + /** The plugin succeeded. */ + ok: boolean; + /** What it printed: the text the employee's tool gets back. */ + output: string; + /** The files it reported making (its `Result:` lines). */ + files: string[]; +} +export interface PluginRunOptions { + /** Seconds the command may take (default 120, at most 600). */ + timeout?: number; + /** + * For long work: called with each line the plugin writes, as it writes + * it. The command then runs as a job on the bot, and `run` resolves when + * the job ends. + */ + onProgress?: (line: string) => void; +} +export declare const plugins: { + /** + * Run `command` on the installed plugin `slug`, with `args` as its flags + * (each key becomes `--key`; objects and lists go as JSON). + */ + run(slug: string, command: string, args?: Record, options?: PluginRunOptions): Promise; +}; diff --git a/dist/sdk.d.ts b/dist/sdk.d.ts index 62ab701..be7e8f5 100644 --- a/dist/sdk.d.ts +++ b/dist/sdk.d.ts @@ -29,6 +29,9 @@ export declare class NeboSDK { files: { url(path: string): string; }; + plugins: { + run(slug: string, command: string, args?: Record, options?: import("./plugins").PluginRunOptions): Promise; + }; surfaces: import("./surfaces").NeboSurfaces; a2ui: import("./a2ui").NeboA2UI; identity: { diff --git a/src/index.ts b/src/index.ts index 9eba8bc..e8e754e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ * - nebo.a2ui → agent-driven UI via @a2ui/web_core (A2UI v0.9) * - nebo.share → a file into the owner's Work, and Nebo's Share dialog on it * - nebo.files → the address of an owner's file in a folder the app is granted + * - nebo.plugins → run a plugin installed on the bot directly, no AI in the middle * - nebo.identity → agent context (name, persona, skills, inputs) * - nebo.chat → the page's conversation with its own employee, and a ready chat UI */ @@ -33,6 +34,7 @@ export { neboFetch } from './fetch'; export { identity } from './identity'; export { share } from './share'; export { files } from './files'; +export { plugins } from './plugins'; export { chat, ChatError } from './chat'; export { setAppId, setBaseUrl, getAppId, getBaseUrl } from './config'; export type { @@ -41,6 +43,7 @@ export type { } from './types'; export type { AgentIdentity } from './identity'; export type { ShareFile, Shared } from './share'; +export type { PluginResult, PluginRunOptions } from './plugins'; export type { ChatOptions, ChatContext, ChatMessage, ChatEvent, ChatHistory, SendOptions } from './chat'; export type { A2UIMessageProcessor } from './a2ui'; export type { diff --git a/src/plugins.ts b/src/plugins.ts new file mode 100644 index 0000000..8dfe413 --- /dev/null +++ b/src/plugins.ts @@ -0,0 +1,101 @@ +/** + * nebo.plugins — run a plugin installed on the bot directly, with no AI in + * the middle. The call is the one the app's employee makes with its + * `plugin__` tool (a command and its flags), run by Nebo the same way + * (the same installed version, account and billing), and its result comes + * straight back. + * + * The app's manifest grants the plugin: `plugin:` for every command, + * `plugin::` for one. An app installed from the marketplace + * also needs the owner's OK. A call the app may not make is refused. + */ + +import { getAppId, getBaseUrl } from './config'; + +/** What a plugin command came to. */ +export interface PluginResult { + /** The plugin succeeded. */ + ok: boolean; + /** What it printed: the text the employee's tool gets back. */ + output: string; + /** The files it reported making (its `Result:` lines). */ + files: string[]; +} + +export interface PluginRunOptions { + /** Seconds the command may take (default 120, at most 600). */ + timeout?: number; + /** + * For long work: called with each line the plugin writes, as it writes + * it. The command then runs as a job on the bot, and `run` resolves when + * the job ends. + */ + onProgress?: (line: string) => void; +} + +function pluginsUrl(path: string): string { + return `${getBaseUrl()}/api/v1/apps/${encodeURIComponent(getAppId())}/plugins/${path}`; +} + +async function refused(resp: Response): Promise { + const text = await resp.text().catch(() => ''); + return new Error(text || `plugin call failed (${resp.status})`); +} + +/** Read a job's event stream to its `result`, handing on each `progress` line. */ +async function follow(job: string, onProgress: (line: string) => void): Promise { + const resp = await fetch(pluginsUrl(`jobs/${encodeURIComponent(job)}`), { + headers: { Accept: 'text/event-stream' } + }); + if (!resp.ok || !resp.body) throw await refused(resp); + const reader = resp.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + let event = ''; + let data: string[] = []; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; + for (const raw of lines) { + const line = raw.replace(/\r$/, ''); + if (line === '') { + if (event === 'result') return JSON.parse(data.join('\n')) as PluginResult; + if (event === 'progress') onProgress(data.join('\n')); + event = ''; + data = []; + } else if (line.startsWith('event:')) { + event = line.slice(6).trim(); + } else if (line.startsWith('data:')) { + data.push(line.slice(5).replace(/^ /, '')); + } + } + } + throw new Error('the plugin job ended without a result'); +} + +export const plugins = { + /** + * Run `command` on the installed plugin `slug`, with `args` as its flags + * (each key becomes `--key`; objects and lists go as JSON). + */ + async run( + slug: string, + command: string, + args: Record = {}, + options: PluginRunOptions = {} + ): Promise { + const background = typeof options.onProgress === 'function'; + const resp = await fetch(pluginsUrl(`${encodeURIComponent(slug)}/run`), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ command, args, timeout: options.timeout, background }) + }); + if (!resp.ok) throw await refused(resp); + const data = await resp.json(); + if (!background) return data as PluginResult; + return follow(data.job, options.onProgress as (line: string) => void); + } +}; diff --git a/src/sdk.ts b/src/sdk.ts index 6774b5b..4c79265 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -11,6 +11,7 @@ import { janus } from './janus'; import { decide } from './decide'; import { share } from './share'; import { files } from './files'; +import { plugins } from './plugins'; import { surfaces } from './surfaces'; import { a2ui } from './a2ui'; import { identity } from './identity'; @@ -25,6 +26,7 @@ export class NeboSDK { decide = decide; share = share; files = files; + plugins = plugins; surfaces = surfaces; a2ui = a2ui; identity = identity; diff --git a/test/plugins.test.mjs b/test/plugins.test.mjs new file mode 100644 index 0000000..15729c6 --- /dev/null +++ b/test/plugins.test.mjs @@ -0,0 +1,48 @@ +// nebo.plugins.run calls the app's own plugin route, and with onProgress +// follows the job's event stream to its result. Run: pnpm test (after pnpm build). +import { test } from 'node:test'; +import assert from 'node:assert/strict'; + +/** A page on the bot, and a Nebo that answers `answer(url, init)`. */ +async function sdkWith(answer) { + globalThis.window = { location: { protocol: 'http:', hostname: 'localhost', pathname: '/apps/app-1/ui/', origin: 'http://localhost:27895' } }; + globalThis.document = { querySelector: () => null }; + const calls = []; + globalThis.fetch = async (url, init = {}) => { + calls.push({ url, init }); + return answer(url, init); + }; + const fresh = await import(`../dist/index.js?${Math.random()}`); + return { nebo: fresh.nebo, calls }; +} + +function json(status, body) { + return new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } }); +} + +test('a command runs through the app\'s own route and its result comes back', async () => { + const result = { ok: true, output: 'Result: /x/strip.jpg', files: ['/x/strip.jpg'] }; + const { nebo, calls } = await sdkWith(() => json(200, result)); + assert.deepEqual(await nebo.plugins.run('nebo-media', 'strip', { every: 2 }), result); + assert.equal(calls[0].url, 'http://localhost:27895/api/v1/apps/app-1/plugins/nebo-media/run'); + assert.deepEqual(JSON.parse(calls[0].init.body), { command: 'strip', args: { every: 2 }, background: false }); +}); + +test('a refusal throws with Nebo\'s reason', async () => { + const { nebo } = await sdkWith(() => new Response('Media Studio wants to use Nebo Media, and the owner has not allowed it.', { status: 403 })); + await assert.rejects(nebo.plugins.run('nebo-media', 'strip'), /wants to use Nebo Media/); +}); + +test('long work reports each line, then resolves with the result', async () => { + const events = 'event: progress\ndata: frame 1 of 2\n\nevent: progress\ndata: frame 2 of 2\n\nevent: result\ndata: {"ok":true,"output":"done","files":[]}\n\n'; + const { nebo, calls } = await sdkWith((url) => + url.endsWith('/run') ? json(202, { job: 'j1' }) : new Response(events, { status: 200, headers: { 'Content-Type': 'text/event-stream' } }) + ); + const lines = []; + const result = await nebo.plugins.run('nebo-media', 'preview', {}, { timeout: 600, onProgress: (l) => lines.push(l) }); + assert.deepEqual(lines, ['frame 1 of 2', 'frame 2 of 2']); + assert.deepEqual(result, { ok: true, output: 'done', files: [] }); + assert.deepEqual(JSON.parse(calls[0].init.body), { command: 'preview', args: {}, timeout: 600, background: true }); + assert.equal(calls[1].url, 'http://localhost:27895/api/v1/apps/app-1/plugins/jobs/j1'); + assert.equal(calls[1].init.headers.Accept, 'text/event-stream'); +});