@aerostack/gateway 0.17.3 → 0.17.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -191,7 +191,7 @@ openclaw doctor
191
191
  | `AEROSTACK_WORKSPACE_URL is required` | The env vars aren't being passed. Use `export` before running `--check`, or verify your MCP config has the `env` block |
192
192
  | `npx` error: "could not determine executable" | Install globally: `npm install -g @aerostack/gateway`, then run `aerostack-gateway --check` |
193
193
  | "Token invalid" in `--check` | Regenerate token at dashboard → Workspace → Tokens |
194
- | "0 tools found" | Add MCP servers or functions to your workspace in the dashboard first |
194
+ | "0 workspace tools" | Built-in tools (Local Guardian, Chat) are always available. Add MCP servers to your workspace for more tools |
195
195
  | OpenClaw: tools not showing | Send a message via Telegram or another channel to initialize the session |
196
196
  | Dashboard shows "No agents" | Run `--check` to verify, or wait 2 minutes for the heartbeat to register |
197
197
  | "Workspace unreachable" | Check the URL format: `https://mcp.aerostack.dev/ws/your-workspace` |
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import{homedir as H}from"node:os";import{readFile as V}from"node:fs/promises";import{join as x}from"node:path";import{execFile as L}from"node:child_process";const ue=process.env.AEROSTACK_WORKSPACE_URL||"https://api.aerostack.dev/api/gateway/ws/notification",S=process.env.AEROSTACK_TOKEN||"",K=parseInt(process.env.AEROSTACK_OPENCLAW_PORT||"18789",10),Y=6e4,ge=1e3,de=3e4,ye=3e4,Q=12e4,I=`aerostack-connector-${Date.now().toString(36)}`,w=ue.replace(/\/+$/,"");function r(e,t){const n=new Date().toISOString(),s=t?` ${JSON.stringify(t)}`:"";process.stderr.write(`[${n}] [INFO] ${e}${s}
3
+ import{homedir as j}from"node:os";import{readFile as V}from"node:fs/promises";import{join as I}from"node:path";import{execFile as H}from"node:child_process";const ue=process.env.AEROSTACK_WORKSPACE_URL||"https://api.aerostack.dev/api/gateway/ws/notification",S=process.env.AEROSTACK_TOKEN||"",K=parseInt(process.env.AEROSTACK_OPENCLAW_PORT||"18789",10),Y=6e4,ge=1e3,de=3e4,ye=3e4,Q=12e4,D=`aerostack-connector-${Date.now().toString(36)}`,w=ue.replace(/\/+$/,"");function o(e,t){const n=new Date().toISOString(),s=t?` ${JSON.stringify(t)}`:"";process.stderr.write(`[${n}] [INFO] ${e}${s}
4
4
  `)}function u(e,t){const n=new Date().toISOString(),s=t?` ${JSON.stringify(t)}`:"";process.stderr.write(`[${n}] [WARN] ${e}${s}
5
5
  `)}function m(e,t){const n=new Date().toISOString(),s=t?` ${JSON.stringify(t)}`:"";process.stderr.write(`[${n}] [ERROR] ${e}${s}
6
- `)}async function C(e,t={},n=ye){const s=new AbortController,a=setTimeout(()=>s.abort(),n);try{return await fetch(e,{...t,signal:s.signal})}finally{clearTimeout(a)}}let D=null,_=!1,$=null,P=0,Z=0;const R=new Map,pe=100,fe=2e3,he=3e4,we=3e4;let B=null;async function me(){try{const e=x(H(),".openclaw","openclaw.json"),t=await V(e,"utf-8");return JSON.parse(t)?.gateway?.auth?.token??null}catch{return null}}async function E(){const e=await me();if(!e)return u("No OpenClaw gateway token found, skipping WS control"),!1;const t=`ws://127.0.0.1:${K}`;return new Promise(n=>{try{const s=new WebSocket(t);D=s;const a=setTimeout(()=>{r("Gateway WS connect timeout");try{s.close()}catch{}n(!1)},1e4);s.onopen=()=>{clearTimeout(a),r("Gateway WS connected, sending handshake"),ee({type:"req",id:String(++Z),method:"connect",params:{minProtocol:3,maxProtocol:3,client:{id:"openclaw-tui",displayName:"Aerostack Connector",version:"1.0.0",platform:process.platform,mode:"cli"},auth:{token:e},scopes:["operator.read","operator.write","operator.admin"],caps:["tool-events","sessions"]}})},s.onmessage=o=>{try{const i=JSON.parse(String(o.data));_e(i,n)}catch{}},s.onerror=()=>{clearTimeout(a),n(!1)},s.onclose=()=>{clearTimeout(a),D=null,_=!1,J||Te()}}catch{n(!1)}})}function ee(e){try{D?.send(JSON.stringify(e))}catch{}}function _e(e,t){if(e.type==="res"&&!_){e.ok?(_=!0,P=0,r("Gateway WS authenticated",{port:K}),t?.(!0),N("sessions.subscribe").then(n=>{n.ok&&r("Subscribed to gateway session events")})):(u("Gateway WS auth rejected",{error:e.error?.message}),t?.(!1));return}if(e.type==="res"&&e.id&&R.has(e.id)){const n=R.get(e.id);R.delete(e.id),n(e);return}e.type==="event"&&Se(e)}const b=new Map;function Se(e){const t=e.event,n=e.payload??{};if(t==="session.tool"){const s=n.data,a=s?.name??n.toolName??"unknown",o=s?.phase??"";(o==="start"||o==="end")&&Oe(a,o,"tool_call")}else if(t==="session.message"){const s=n.sessionKey??"",a=n.message,o=typeof a=="object"?a?.role:void 0;let i="";if(typeof a=="string")i=a;else if(a){const c=a.content;typeof c=="string"?i=c:Array.isArray(c)&&(i=c.map(l=>typeof l=="string"?l:l?.text??"").join("")),i||(i=a.text??n.text??"")}r("session.message",{sessionKey:s,role:o,textLength:i?.length??0});const g=b.get(s);g&&i&&o!=="user"&&(r("WS fast path: got agent response",{sessionKey:s,textLength:i.length}),g({text:i}))}else t==="sessions.changed"&&r("Gateway session changed",{key:n.sessionKey})}function N(e,t){return G(e,t,1e4)}function G(e,t,n=1e4){return new Promise(s=>{const a=String(++Z),o=setTimeout(()=>{R.delete(a),s({type:"res",id:a,ok:!1,error:{code:"TIMEOUT",message:"timeout"}})},n);R.set(a,i=>{clearTimeout(o),s(i)}),ee({type:"req",id:a,method:e,params:t})})}async function ke(){if(!_){U("disconnected");return}const e=await N("health");e.ok?U("healthy",e.payload):U("unhealthy",{error:e.error?.message})}async function U(e,t){try{await C(`${w}/chat/heartbeat`,{method:"POST",headers:{Authorization:`Bearer ${S}`,"Content-Type":"application/json"},body:JSON.stringify({agent_type:"openclaw-connector",user_agent:"aerostack-connector/1.0.0",bridge_id:I,gateway_status:e,gateway_data:t})})}catch{}}function Te(){if(J||$||P>=pe)return;const e=Math.min(fe*Math.pow(2,Math.min(P,5)),he);P++,r(`Gateway WS reconnecting in ${e/1e3}s`,{attempt:P}),$=setTimeout(async()=>{$=null,_=!1,R.clear(),await E()},e)}function Ce(){return new Promise(e=>{r("Restarting OpenClaw gateway"),L("openclaw",["gateway","restart"],{timeout:3e4},t=>{t?(m("Gateway restart failed",{error:t.message}),e(!1)):(r("Gateway restart triggered"),setTimeout(()=>{E()},5e3),e(!0))})})}function te(){return new Promise(e=>{r("Diagnose: restarting OpenClaw gateway (fast)"),L("openclaw",["gateway","restart"],{timeout:1e4},t=>{t?(m("Diagnose: gateway restart failed",{error:t.message}),e(!1)):(r("Diagnose: gateway restart triggered"),setTimeout(()=>{E()},3e3),e(!0))})})}async function Ae(e,t){const n=Date.now();r("Agent turn starting",{sessionKey:e,messageLength:t.length});const s=e;if(_)try{const a=`aerostack-${Date.now()}-${Math.random().toString(36).slice(2,8)}`,o=`agent:main:${s}`,i=new Promise(c=>{const l=setTimeout(()=>{b.delete(o),c(null)},Q);b.set(o,f=>{clearTimeout(l),b.delete(o),c(f)})});N("sessions.messages.subscribe",{key:o});let g=await N("sessions.send",{key:o,message:t,idempotencyKey:a});if(!g.ok&&g.error?.message?.includes("session not found")&&(r("Creating new gateway session for thread",{key:s}),(await N("sessions.create",{key:s,agentId:"main"})).ok&&(g=await N("sessions.send",{key:o,message:t,idempotencyKey:a}))),!g.ok)b.delete(o),u("Gateway WS sessions.send rejected, falling back to CLI",{error:g.error?.message});else{const c=await i,l=Date.now()-n;if(c?.text)return r("Agent turn completed (WS fast path)",{sessionKey:o,durationMs:l}),[{text:c.text}];u("No response received via WS events, falling back to CLI",{durationMs:l})}}catch(a){b.delete(`agent:main:${s}`),u("Gateway WS fast path error, falling back to CLI",{error:a?.message})}return _||(u("Gateway disconnected, attempting reconnect before agent turn"),await E()||(u("Gateway unreachable, attempting restart"),await Ce(),await new Promise(o=>setTimeout(o,8e3)))),new Promise((a,o)=>{L("openclaw",["agent","--agent","main","--json","--session-id",e,"-m",t],{timeout:Q},(g,c)=>{const l=Date.now()-n;if(g){m("Agent turn failed",{sessionKey:e,durationMs:l,error:g.message}),o(g);return}let f=[];if(c)try{const y=JSON.parse(c.trim()),h=y?.result?.payloads;Array.isArray(h)&&h.length>0?f=h:(y.text||y.reply||y.message)&&(f=[{text:y.text||y.reply||y.message}])}catch{const y=c.trim().split(`
7
- `).pop()||"";try{const h=JSON.parse(y);f=[{text:h.text||h.reply||h.message||y}]}catch{y&&(f=[{text:y}])}}f.length===0&&(f=[{text:`Processed your message (${l}ms).`}]),r("Agent turn completed (CLI fallback)",{sessionKey:e,durationMs:l,payloads:f.length}),a(f)})})}let J=!1,v=null;async function ne(){try{const e=await C(`${w}/chat/heartbeat`,{method:"POST",headers:{Authorization:`Bearer ${S}`,"Content-Type":"application/json"},body:JSON.stringify({agent_type:"openclaw-connector",user_agent:"aerostack-connector/1.0.0",bridge_id:I})});e.ok?r("Heartbeat OK"):u("Heartbeat failed",{status:e.status})}catch(e){u("Heartbeat error",{error:e?.message})}}async function z(e,t){try{const n=await C(`${w}/chat/messages`,{method:"POST",headers:{Authorization:`Bearer ${S}`,"Content-Type":"application/json"},body:JSON.stringify({content:e.slice(0,1e4),session_id:t||v})});n.ok?r("Reply sent",{length:e.length,sessionId:t}):u("Reply failed",{status:n.status})}catch(n){m("Reply error",{error:n?.message})}}async function Oe(e,t,n){try{await C(`${w}/chat/tool-status`,{method:"POST",headers:{Authorization:`Bearer ${S}`,"Content-Type":"application/json"},body:JSON.stringify({tool_name:e,phase:t,category:n})})}catch{}}const F=new Map,se=50,ae=10;function q(e,t,n){let s=F.get(e);s||(s=[],F.set(e,s)),s.push({role:t,content:n,ts:Date.now()}),s.length>se&&s.splice(0,s.length-se)}function Re(e){const t=F.get(e);return!t||t.length===0?"":`Previous conversation:
6
+ `)}async function C(e,t={},n=ye){const s=new AbortController,a=setTimeout(()=>s.abort(),n);try{return await fetch(e,{...t,signal:s.signal})}finally{clearTimeout(a)}}let G=null,_=!1,P=null,W=0,Z=0;const R=new Map,pe=100,fe=2e3,he=3e4,we=3e4;let B=null;async function me(){try{const e=I(j(),".openclaw","openclaw.json"),t=await V(e,"utf-8");return JSON.parse(t)?.gateway?.auth?.token??null}catch{return null}}async function v(){const e=await me();if(!e)return u("No OpenClaw gateway token found, skipping WS control"),!1;const t=`ws://127.0.0.1:${K}`;return new Promise(n=>{try{const s=new WebSocket(t);G=s;const a=setTimeout(()=>{o("Gateway WS connect timeout");try{s.close()}catch{}n(!1)},1e4);s.onopen=()=>{clearTimeout(a),o("Gateway WS connected, sending handshake"),ee({type:"req",id:String(++Z),method:"connect",params:{minProtocol:3,maxProtocol:3,client:{id:"openclaw-tui",displayName:"Aerostack Connector",version:"1.0.0",platform:process.platform,mode:"cli"},auth:{token:e},scopes:["operator.read","operator.write","operator.admin"],caps:["tool-events","sessions"]}})},s.onmessage=r=>{try{const i=JSON.parse(String(r.data));_e(i,n)}catch{}},s.onerror=()=>{clearTimeout(a),n(!1)},s.onclose=()=>{clearTimeout(a),G=null,_=!1,J||Ce()}}catch{n(!1)}})}function ee(e){try{G?.send(JSON.stringify(e))}catch{}}function _e(e,t){if(e.type==="res"&&!_){e.ok?(_=!0,W=0,o("Gateway WS authenticated",{port:K}),t?.(!0),N("sessions.subscribe").then(n=>{n.ok&&o("Subscribed to gateway session events")}),Se()):(u("Gateway WS auth rejected",{error:e.error?.message}),t?.(!1));return}if(e.type==="res"&&e.id&&R.has(e.id)){const n=R.get(e.id);R.delete(e.id),n(e);return}e.type==="event"&&ke(e)}async function Se(){const e="aerostack:warmup";try{const t=await $("sessions.create",{key:e,agentId:"main"},5e3);if(t.ok||t.error?.message?.includes("already exists")){const n=await $("sessions.send",{key:e,message:"List your available tools."},1e4);n.ok?o("Warmup message sent \u2014 MCP servers will load on first tool call"):o("Warmup send skipped",{reason:n.error?.message})}}catch{o("Warmup skipped (gateway not ready)")}}const b=new Map;function ke(e){const t=e.event,n=e.payload??{};if(t==="session.tool"){const s=n.data,a=s?.name??n.toolName??"unknown",r=s?.phase??"";(r==="start"||r==="end")&&Re(a,r,"tool_call")}else if(t==="session.message"){const s=n.sessionKey??"",a=n.message,r=typeof a=="object"?a?.role:void 0;let i="";if(typeof a=="string")i=a;else if(a){const c=a.content;typeof c=="string"?i=c:Array.isArray(c)&&(i=c.map(l=>typeof l=="string"?l:l?.text??"").join("")),i||(i=a.text??n.text??"")}o("session.message",{sessionKey:s,role:r,textLength:i?.length??0});const g=b.get(s);g&&i&&r!=="user"&&(o("WS fast path: got agent response",{sessionKey:s,textLength:i.length}),g({text:i}))}else t==="sessions.changed"&&o("Gateway session changed",{key:n.sessionKey})}function N(e,t){return $(e,t,1e4)}function $(e,t,n=1e4){return new Promise(s=>{const a=String(++Z),r=setTimeout(()=>{R.delete(a),s({type:"res",id:a,ok:!1,error:{code:"TIMEOUT",message:"timeout"}})},n);R.set(a,i=>{clearTimeout(r),s(i)}),ee({type:"req",id:a,method:e,params:t})})}async function Te(){if(!_){U("disconnected");return}const e=await N("health");e.ok?U("healthy",e.payload):U("unhealthy",{error:e.error?.message})}async function U(e,t){try{await C(`${w}/chat/heartbeat`,{method:"POST",headers:{Authorization:`Bearer ${S}`,"Content-Type":"application/json"},body:JSON.stringify({agent_type:"openclaw-connector",user_agent:"aerostack-connector/1.0.0",bridge_id:D,gateway_status:e,gateway_data:t})})}catch{}}function Ce(){if(J||P||W>=pe)return;const e=Math.min(fe*Math.pow(2,Math.min(W,5)),he);W++,o(`Gateway WS reconnecting in ${e/1e3}s`,{attempt:W}),P=setTimeout(async()=>{P=null,_=!1,R.clear(),await v()},e)}function Ae(){return new Promise(e=>{o("Restarting OpenClaw gateway"),H("openclaw",["gateway","restart"],{timeout:3e4},t=>{t?(m("Gateway restart failed",{error:t.message}),e(!1)):(o("Gateway restart triggered"),setTimeout(()=>{v()},5e3),e(!0))})})}function te(){return new Promise(e=>{o("Diagnose: restarting OpenClaw gateway (fast)"),H("openclaw",["gateway","restart"],{timeout:1e4},t=>{t?(m("Diagnose: gateway restart failed",{error:t.message}),e(!1)):(o("Diagnose: gateway restart triggered"),setTimeout(()=>{v()},3e3),e(!0))})})}async function Oe(e,t){const n=Date.now();o("Agent turn starting",{sessionKey:e,messageLength:t.length});const s=e;if(_)try{const a=`aerostack-${Date.now()}-${Math.random().toString(36).slice(2,8)}`,r=`agent:main:${s}`,i=new Promise(c=>{const l=setTimeout(()=>{b.delete(r),c(null)},Q);b.set(r,f=>{clearTimeout(l),b.delete(r),c(f)})});N("sessions.messages.subscribe",{key:r});let g=await N("sessions.send",{key:r,message:t,idempotencyKey:a});if(!g.ok&&g.error?.message?.includes("session not found")&&(o("Creating new gateway session for thread",{key:s}),(await N("sessions.create",{key:s,agentId:"main"})).ok&&(g=await N("sessions.send",{key:r,message:t,idempotencyKey:a}))),!g.ok)b.delete(r),u("Gateway WS sessions.send rejected, falling back to CLI",{error:g.error?.message});else{const c=await i,l=Date.now()-n;if(c?.text)return o("Agent turn completed (WS fast path)",{sessionKey:r,durationMs:l}),[{text:c.text}];u("No response received via WS events, falling back to CLI",{durationMs:l})}}catch(a){b.delete(`agent:main:${s}`),u("Gateway WS fast path error, falling back to CLI",{error:a?.message})}return _||(u("Gateway disconnected, attempting reconnect before agent turn"),await v()||(u("Gateway unreachable, attempting restart"),await Ae(),await new Promise(r=>setTimeout(r,8e3)))),new Promise((a,r)=>{H("openclaw",["agent","--agent","main","--json","--session-id",e,"-m",t],{timeout:Q},(g,c)=>{const l=Date.now()-n;if(g){m("Agent turn failed",{sessionKey:e,durationMs:l,error:g.message}),r(g);return}let f=[];if(c)try{const y=JSON.parse(c.trim()),h=y?.result?.payloads;Array.isArray(h)&&h.length>0?f=h:(y.text||y.reply||y.message)&&(f=[{text:y.text||y.reply||y.message}])}catch{const y=c.trim().split(`
7
+ `).pop()||"";try{const h=JSON.parse(y);f=[{text:h.text||h.reply||h.message||y}]}catch{y&&(f=[{text:y}])}}f.length===0&&(f=[{text:`Processed your message (${l}ms).`}]),o("Agent turn completed (CLI fallback)",{sessionKey:e,durationMs:l,payloads:f.length}),a(f)})})}let J=!1,E=null;async function ne(){try{const e=await C(`${w}/chat/heartbeat`,{method:"POST",headers:{Authorization:`Bearer ${S}`,"Content-Type":"application/json"},body:JSON.stringify({agent_type:"openclaw-connector",user_agent:"aerostack-connector/1.0.0",bridge_id:D})});e.ok?o("Heartbeat OK"):u("Heartbeat failed",{status:e.status})}catch(e){u("Heartbeat error",{error:e?.message})}}async function z(e,t){try{const n=await C(`${w}/chat/messages`,{method:"POST",headers:{Authorization:`Bearer ${S}`,"Content-Type":"application/json"},body:JSON.stringify({content:e.slice(0,1e4),session_id:t||E})});n.ok?o("Reply sent",{length:e.length,sessionId:t}):u("Reply failed",{status:n.status})}catch(n){m("Reply error",{error:n?.message})}}async function Re(e,t,n){try{await C(`${w}/chat/tool-status`,{method:"POST",headers:{Authorization:`Bearer ${S}`,"Content-Type":"application/json"},body:JSON.stringify({tool_name:e,phase:t,category:n})})}catch{}}const F=new Map,se=50,ae=10;function q(e,t,n){let s=F.get(e);s||(s=[],F.set(e,s)),s.push({role:t,content:n,ts:Date.now()}),s.length>se&&s.splice(0,s.length-se)}function be(e){const t=F.get(e);return!t||t.length===0?"":`Previous conversation:
8
8
  `+t.slice(-ae).map(n=>`[${n.role==="user"?"Admin":"You (Agent)"}] ${n.content}`).join(`
9
9
  `)+`
10
10
 
11
- `}async function be(e){try{const t=await C(`${w}/chat/history?limit=${ae}`,{headers:{Authorization:`Bearer ${S}`}});if(!t.ok)return;const n=await t.json();n.session_id&&(v=n.session_id);for(const s of n.messages??[]){const a=s.sender_type==="agent"?"agent":"user";q(n.session_id||e,a,s.content)}r("History seeded",{sessionId:n.session_id,count:n.messages?.length??0})}catch{}}let d=null,j=null,M=0,W=null;function Ne(e){return(e||"").replace(/[\r\n]+/g," ").replace(/\[/g,"(").replace(/\]/g,")").replace(/---/g,"\u2014")}import{writeFile as $e,mkdir as Pe}from"node:fs/promises";const oe=x(H(),".openclaw","media","inbound");async function Ee(e,t){try{const n=await C(`${w}/chat/media/${e}/${t.filename}`,{headers:{Authorization:`Bearer ${S}`}},15e3);if(!n.ok)return u("Media fetch failed",{messageId:e,filename:t.filename,status:n.status}),null;const s=Buffer.from(await n.arrayBuffer());await Pe(oe,{recursive:!0});const a=t.filename.split(".").pop()||"bin",o=x(oe,`file_0---${t.id||e}.${a}`);return await $e(o,s),r("Media saved locally",{localPath:o,size:s.length}),o}catch(n){return u("Media download error",{messageId:e,filename:t.filename,error:n?.message}),null}}function ve(e){return e.map(t=>`[media attached: ${t.path} (${t.mime}) | ${t.path}]`).join(`
12
- `)}async function Me(e){const t=Date.now();try{const n=JSON.parse(e);if(n.type!=="chat_message")return;const s=n.session_id,a=n.session_title||null,o=Ne(n.sender_name||"Admin"),i=n.content||"",g=n.content_type||"text",c=n.message_id;let l=[];if(n.metadata_json)try{const p=JSON.parse(n.metadata_json);p?.v===1&&Array.isArray(p.attachments)&&(l=p.attachments)}catch{}if(!i&&l.length===0)return;r(">>> Chat message received via WebSocket (ChatRelayDO)",{transport:"websocket",wsDeliveryMs:Date.now()-t,sender:o,sessionId:s,sessionTitle:a,contentType:g,textLength:i.length,attachments:l.length}),s&&(v=s);const f=`aerostack-chat-${s||"_default"}`;let y=`[Style: concise, numbered lists, bold headers, short sub-bullets. No walls of text.]
13
- [${o}]:`;if(i&&(y+=` ${i}`),l.length>0&&c){const p=[],A=await Promise.all(l.slice(0,5).map(k=>Ee(c,k).then(O=>O?{path:O,mime:k.mime_type}:null)));for(const k of A)k&&p.push(k);p.length>0&&(y=ve(p)+`
14
- `+y)}q(s||"_default","user",i||`[${l.map(p=>p.filename).join(", ")}]`),X(s,!0);const h=Date.now();r(">>> Agent turn starting",{timeSinceWsReceived:`${h-t}ms`});try{const p=await Ae(f,y),A=Date.now();X(s,!1),r(">>> Agent turn completed \u2014 timing breakdown",{transport:"websocket",agentProcessingMs:A-h,totalFromWsReceiveMs:A-t});for(const O of p){let T=(typeof O.text=="string"?O.text:String(O.text??"")).trim();T=T.replace(/\[\[reply_to_current\]\]\s*/gi,"").trim(),T&&T.length>=2&&(q(s||"_default","agent",T),We(T,s)?r(">>> Reply sent via WS fast path (no HTTP POST needed)",{textLength:T.length}):await z(T,s))}const k=Date.now();r(">>> Full round-trip complete",{wsToAgentStartMs:h-t,agentProcessingMs:A-h,replyPostMs:k-A,totalRoundTripMs:k-t})}catch(p){X(s,!1),m("Agent turn failed, sending fallback",{error:p?.message}),await z("I received your message but encountered an error processing it. Please try again.",s)}}catch(n){m("handleChatMessage error",{error:n?.message})}}function We(e,t){if(!d||d.readyState!==WebSocket.OPEN)return!1;try{const n=t||v||null;return d.send(JSON.stringify({type:"chat_reply",content:e.slice(0,1e4),session_id:n,sender_name:"Agent",content_type:"text"})),j={content:e.slice(0,1e4),sessionId:n},!0}catch{return!1}}function X(e,t=!0){if(!(!d||d.readyState!==WebSocket.OPEN))try{d.send(JSON.stringify({type:"typing",session_id:e||v,is_typing:t}))}catch{}}async function xe(e){r(">>> Diagnose requested",{requestId:e});const t=[];t.push({step:"connector",status:"ok",message:"Connector running and connected"}),_?(await G("health",void 0,5e3)).ok?t.push({step:"gateway",status:"ok",message:"OpenClaw gateway healthy"}):(r(">>> Diagnose: gateway unhealthy, attempting restart"),await te()?(await new Promise(o=>setTimeout(o,5e3)),(await G("health",void 0,5e3)).ok?t.push({step:"gateway",status:"ok",message:"Gateway restarted and healthy"}):t.push({step:"gateway",status:"fail",message:"Gateway restarted but health check still fails. Run: openclaw gateway restart"})):t.push({step:"gateway",status:"fail",message:"Gateway restart failed. Run: openclaw gateway restart"})):(r(">>> Diagnose: gateway not connected, attempting restart"),await te()?(await new Promise(s=>setTimeout(s,5e3)),_?t.push({step:"gateway",status:"ok",message:"Gateway was down, restarted successfully"}):t.push({step:"gateway",status:"fail",message:"Gateway not reachable after restart. Run: openclaw gateway start"})):t.push({step:"gateway",status:"fail",message:"OpenClaw gateway not running. Run: openclaw gateway start"}));try{const n=x(H(),".openclaw","openclaw.json"),s=await V(n,"utf-8"),a=JSON.parse(s),o=a?.mcp?.servers||a?.mcpServers||{};Object.keys(o).some(g=>{const c=o[g];return g.toLowerCase().includes("aerostack")||g.toLowerCase().includes("notification")||typeof c?.url=="string"&&c.url.includes("aerostack")||Array.isArray(c?.args)&&c.args.some(l=>typeof l=="string"&&l.includes("aerostack"))})?t.push({step:"mcp_config",status:"ok",message:"Aerostack MCP server configured in OpenClaw"}):t.push({step:"mcp_config",status:"warn",message:"Aerostack MCP server not found in OpenClaw config",detail:{fix:"Add Aerostack MCP server via workspace Connect dialog"}})}catch(n){t.push({step:"mcp_config",status:"fail",message:"Cannot read OpenClaw config (~/.openclaw/openclaw.json)",detail:{error:n?.message}})}d&&d.readyState===WebSocket.OPEN?(d.send(JSON.stringify({type:"diagnose_result",request_id:e,steps:t})),t.push({step:"data_flow",status:"ok",message:"Diagnostic result sent via WebSocket"}),r(">>> Diagnose complete",{requestId:e,steps:t.length})):u("Cannot send diagnose result \u2014 ChatRelay WS not connected")}let re="";async function ie(){if(!(!_||!d||d.readyState!==WebSocket.OPEN))try{const e=await G("sessions.list",{limit:100},5e3);if(!e.ok||!e.payload?.sessions)return;const t=e.payload.sessions.map(s=>({key:String(s.key??""),inputTokens:Number(s.inputTokens)||0,outputTokens:Number(s.outputTokens)||0,totalTokens:Number(s.totalTokens)||0,model:s.model?String(s.model):void 0,contextTokens:Number(s.contextTokens)||0,runtimeMs:Number(s.runtimeMs)||0,updatedAt:Number(s.updatedAt)||0})),n=JSON.stringify(t);if(n===re)return;re=n,d.send(JSON.stringify({type:"token_analytics",sessions:t,timestamp:Date.now()})),r("Token analytics pushed",{sessionCount:t.length})}catch{}}function ce(){if(d){try{d.close()}catch{}d=null}try{const e=w.replace(/^http/,"ws")+`/chat-relay?token=${encodeURIComponent(S)}&client_type=connector&client_id=${encodeURIComponent(I)}`;r("Chat relay WS connecting",{url:w+"/chat-relay"});const t=new WebSocket(e);d=t,t.onopen=()=>{r("Chat relay WS connected"),M=0},t.onmessage=n=>{const s=typeof n.data=="string"?n.data:"";if(s)try{const a=JSON.parse(s);if(a.type==="pong")return;if(a.type==="ping"){t.send(JSON.stringify({type:"pong",ts:Date.now()}));return}if(a.type==="chat_reply_nack"){r(">>> chat_reply_nack received, retrying last reply via HTTP",{reason:a.reason});const o=j;o&&(j=null,z(o.content,o.sessionId).catch(i=>{u("HTTP fallback after NACK failed",{error:i?.message})}));return}if(a.type==="chat_reply_ack"){j=null;return}if(a.type==="diagnose"){xe(a.request_id).catch(o=>{u("Diagnose handler error",{error:o?.message})});return}r(">>> WebSocket frame received from ChatRelayDO",{type:a.type,dataLength:s.length}),Me(s)}catch{}},t.onclose=()=>{d=null,J||le()},t.onerror=()=>{M++}}catch(e){u("Chat relay WS connect error",{error:e?.message}),M++,le()}}function le(){if(W)return;const e=Math.min(ge*Math.pow(2,M),de);r(`Chat relay WS reconnecting in ${e/1e3}s`,{failures:M}),W=setTimeout(()=>{W=null,ce()},e)}async function Ie(){try{const e=await C(`${w}/chat/connection-test`,{headers:{Authorization:`Bearer ${S}`}},1e4);if(e.ok){const t=await e.json();return r(`Connected to "${t.workspace}"`,{tools:t.mcp_servers,token:t.token_name,role:t.token_role}),!0}else return e.status===401?(m("Token invalid \u2014 regenerate at dashboard \u2192 Workspace \u2192 Tokens"),!1):(u("Connection check failed",{status:e.status}),!1)}catch(e){return m("Connection check error",{error:e?.message}),!1}}async function De(){r("Aerostack Connector starting",{workspace:w,openclawPort:K,bridgeId:I}),S||(m("AEROSTACK_TOKEN is required"),process.exit(1)),await Ie()||u("Connection verification failed, continuing anyway (will retry)"),await be("_default"),await E()?r("Gateway WS control layer active"):u("Gateway WS not reachable \u2014 will keep retrying"),B=setInterval(ke,we),await ne();const n=setInterval(ne,Y);ie().catch(()=>{});const s=setInterval(ie,Y);ce(),r("Connector ready \u2014 listening for dashboard chat messages");const a=()=>{J=!0,r("Shutting down"),clearInterval(n),clearInterval(s),B&&clearInterval(B);try{d?.close(1e3)}catch{}W&&clearTimeout(W),$&&clearTimeout($);try{D?.close(1e3)}catch{}process.exit(0)};process.on("SIGTERM",a),process.on("SIGINT",a)}De().catch(e=>{m("Fatal error",{error:e?.message}),process.exit(1)});
11
+ `}async function Ne(e){try{const t=await C(`${w}/chat/history?limit=${ae}`,{headers:{Authorization:`Bearer ${S}`}});if(!t.ok)return;const n=await t.json();n.session_id&&(E=n.session_id);for(const s of n.messages??[]){const a=s.sender_type==="agent"?"agent":"user";q(n.session_id||e,a,s.content)}o("History seeded",{sessionId:n.session_id,count:n.messages?.length??0})}catch{}}let d=null,L=null,M=0,x=null;function $e(e){return(e||"").replace(/[\r\n]+/g," ").replace(/\[/g,"(").replace(/\]/g,")").replace(/---/g,"\u2014")}import{writeFile as Pe,mkdir as We}from"node:fs/promises";const oe=I(j(),".openclaw","media","inbound");async function ve(e,t){try{const n=await C(`${w}/chat/media/${e}/${t.filename}`,{headers:{Authorization:`Bearer ${S}`}},15e3);if(!n.ok)return u("Media fetch failed",{messageId:e,filename:t.filename,status:n.status}),null;const s=Buffer.from(await n.arrayBuffer());await We(oe,{recursive:!0});const a=t.filename.split(".").pop()||"bin",r=I(oe,`file_0---${t.id||e}.${a}`);return await Pe(r,s),o("Media saved locally",{localPath:r,size:s.length}),r}catch(n){return u("Media download error",{messageId:e,filename:t.filename,error:n?.message}),null}}function Ee(e){return e.map(t=>`[media attached: ${t.path} (${t.mime}) | ${t.path}]`).join(`
12
+ `)}async function Me(e){const t=Date.now();try{const n=JSON.parse(e);if(n.type!=="chat_message")return;const s=n.session_id,a=n.session_title||null,r=$e(n.sender_name||"Admin"),i=n.content||"",g=n.content_type||"text",c=n.message_id;let l=[];if(n.metadata_json)try{const p=JSON.parse(n.metadata_json);p?.v===1&&Array.isArray(p.attachments)&&(l=p.attachments)}catch{}if(!i&&l.length===0)return;o(">>> Chat message received via WebSocket (ChatRelayDO)",{transport:"websocket",wsDeliveryMs:Date.now()-t,sender:r,sessionId:s,sessionTitle:a,contentType:g,textLength:i.length,attachments:l.length}),s&&(E=s);const f=`aerostack-chat-${s||"_default"}`;let y=`[Style: concise, numbered lists, bold headers, short sub-bullets. No walls of text.]
13
+ [${r}]:`;if(i&&(y+=` ${i}`),l.length>0&&c){const p=[],A=await Promise.all(l.slice(0,5).map(k=>ve(c,k).then(O=>O?{path:O,mime:k.mime_type}:null)));for(const k of A)k&&p.push(k);p.length>0&&(y=Ee(p)+`
14
+ `+y)}q(s||"_default","user",i||`[${l.map(p=>p.filename).join(", ")}]`),X(s,!0);const h=Date.now();o(">>> Agent turn starting",{timeSinceWsReceived:`${h-t}ms`});try{const p=await Oe(f,y),A=Date.now();X(s,!1),o(">>> Agent turn completed \u2014 timing breakdown",{transport:"websocket",agentProcessingMs:A-h,totalFromWsReceiveMs:A-t});for(const O of p){let T=(typeof O.text=="string"?O.text:String(O.text??"")).trim();T=T.replace(/\[\[reply_to_current\]\]\s*/gi,"").trim(),T&&T.length>=2&&(q(s||"_default","agent",T),xe(T,s)?o(">>> Reply sent via WS fast path (no HTTP POST needed)",{textLength:T.length}):await z(T,s))}const k=Date.now();o(">>> Full round-trip complete",{wsToAgentStartMs:h-t,agentProcessingMs:A-h,replyPostMs:k-A,totalRoundTripMs:k-t})}catch(p){X(s,!1),m("Agent turn failed, sending fallback",{error:p?.message}),await z("I received your message but encountered an error processing it. Please try again.",s)}}catch(n){m("handleChatMessage error",{error:n?.message})}}function xe(e,t){if(!d||d.readyState!==WebSocket.OPEN)return!1;try{const n=t||E||null;return d.send(JSON.stringify({type:"chat_reply",content:e.slice(0,1e4),session_id:n,sender_name:"Agent",content_type:"text"})),L={content:e.slice(0,1e4),sessionId:n},!0}catch{return!1}}function X(e,t=!0){if(!(!d||d.readyState!==WebSocket.OPEN))try{d.send(JSON.stringify({type:"typing",session_id:e||E,is_typing:t}))}catch{}}async function Ie(e){o(">>> Diagnose requested",{requestId:e});const t=[];t.push({step:"connector",status:"ok",message:"Connector running and connected"}),_?(await $("health",void 0,5e3)).ok?t.push({step:"gateway",status:"ok",message:"OpenClaw gateway healthy"}):(o(">>> Diagnose: gateway unhealthy, attempting restart"),await te()?(await new Promise(r=>setTimeout(r,5e3)),(await $("health",void 0,5e3)).ok?t.push({step:"gateway",status:"ok",message:"Gateway restarted and healthy"}):t.push({step:"gateway",status:"fail",message:"Gateway restarted but health check still fails. Run: openclaw gateway restart"})):t.push({step:"gateway",status:"fail",message:"Gateway restart failed. Run: openclaw gateway restart"})):(o(">>> Diagnose: gateway not connected, attempting restart"),await te()?(await new Promise(s=>setTimeout(s,5e3)),_?t.push({step:"gateway",status:"ok",message:"Gateway was down, restarted successfully"}):t.push({step:"gateway",status:"fail",message:"Gateway not reachable after restart. Run: openclaw gateway start"})):t.push({step:"gateway",status:"fail",message:"OpenClaw gateway not running. Run: openclaw gateway start"}));try{const n=I(j(),".openclaw","openclaw.json"),s=await V(n,"utf-8"),a=JSON.parse(s),r=a?.mcp?.servers||a?.mcpServers||{};Object.keys(r).some(g=>{const c=r[g];return g.toLowerCase().includes("aerostack")||g.toLowerCase().includes("notification")||typeof c?.url=="string"&&c.url.includes("aerostack")||Array.isArray(c?.args)&&c.args.some(l=>typeof l=="string"&&l.includes("aerostack"))})?t.push({step:"mcp_config",status:"ok",message:"Aerostack MCP server configured in OpenClaw"}):t.push({step:"mcp_config",status:"warn",message:"Aerostack MCP server not found in OpenClaw config",detail:{fix:"Add Aerostack MCP server via workspace Connect dialog"}})}catch(n){t.push({step:"mcp_config",status:"fail",message:"Cannot read OpenClaw config (~/.openclaw/openclaw.json)",detail:{error:n?.message}})}d&&d.readyState===WebSocket.OPEN?(d.send(JSON.stringify({type:"diagnose_result",request_id:e,steps:t})),t.push({step:"data_flow",status:"ok",message:"Diagnostic result sent via WebSocket"}),o(">>> Diagnose complete",{requestId:e,steps:t.length})):u("Cannot send diagnose result \u2014 ChatRelay WS not connected")}let re="";async function ie(){if(!(!_||!d||d.readyState!==WebSocket.OPEN))try{const e=await $("sessions.list",{limit:100},5e3);if(!e.ok||!e.payload?.sessions)return;const t=e.payload.sessions.map(s=>({key:String(s.key??""),inputTokens:Number(s.inputTokens)||0,outputTokens:Number(s.outputTokens)||0,totalTokens:Number(s.totalTokens)||0,model:s.model?String(s.model):void 0,contextTokens:Number(s.contextTokens)||0,runtimeMs:Number(s.runtimeMs)||0,updatedAt:Number(s.updatedAt)||0})),n=JSON.stringify(t);if(n===re)return;re=n,d.send(JSON.stringify({type:"token_analytics",sessions:t,timestamp:Date.now()})),o("Token analytics pushed",{sessionCount:t.length})}catch{}}function ce(){if(d){try{d.close()}catch{}d=null}try{const e=w.replace(/^http/,"ws")+`/chat-relay?token=${encodeURIComponent(S)}&client_type=connector&client_id=${encodeURIComponent(D)}`;o("Chat relay WS connecting",{url:w+"/chat-relay"});const t=new WebSocket(e);d=t,t.onopen=()=>{o("Chat relay WS connected"),M=0},t.onmessage=n=>{const s=typeof n.data=="string"?n.data:"";if(s)try{const a=JSON.parse(s);if(a.type==="pong")return;if(a.type==="ping"){t.send(JSON.stringify({type:"pong",ts:Date.now()}));return}if(a.type==="chat_reply_nack"){o(">>> chat_reply_nack received, retrying last reply via HTTP",{reason:a.reason});const r=L;r&&(L=null,z(r.content,r.sessionId).catch(i=>{u("HTTP fallback after NACK failed",{error:i?.message})}));return}if(a.type==="chat_reply_ack"){L=null;return}if(a.type==="diagnose"){Ie(a.request_id).catch(r=>{u("Diagnose handler error",{error:r?.message})});return}o(">>> WebSocket frame received from ChatRelayDO",{type:a.type,dataLength:s.length}),Me(s)}catch{}},t.onclose=()=>{d=null,J||le()},t.onerror=()=>{M++}}catch(e){u("Chat relay WS connect error",{error:e?.message}),M++,le()}}function le(){if(x)return;const e=Math.min(ge*Math.pow(2,M),de);o(`Chat relay WS reconnecting in ${e/1e3}s`,{failures:M}),x=setTimeout(()=>{x=null,ce()},e)}async function De(){try{const e=await C(`${w}/chat/connection-test`,{headers:{Authorization:`Bearer ${S}`}},1e4);if(e.ok){const t=await e.json();return o(`Connected to "${t.workspace}"`,{tools:t.mcp_servers,token:t.token_name,role:t.token_role}),!0}else return e.status===401?(m("Token invalid \u2014 regenerate at dashboard \u2192 Workspace \u2192 Tokens"),!1):(u("Connection check failed",{status:e.status}),!1)}catch(e){return m("Connection check error",{error:e?.message}),!1}}async function Ge(){o("Aerostack Connector starting",{workspace:w,openclawPort:K,bridgeId:D}),S||(m("AEROSTACK_TOKEN is required"),process.exit(1)),await De()||u("Connection verification failed, continuing anyway (will retry)"),await Ne("_default"),await v()?o("Gateway WS control layer active"):u("Gateway WS not reachable \u2014 will keep retrying"),B=setInterval(Te,we),await ne();const n=setInterval(ne,Y);ie().catch(()=>{});const s=setInterval(ie,Y);ce(),o("Connector ready \u2014 listening for dashboard chat messages");const a=()=>{J=!0,o("Shutting down"),clearInterval(n),clearInterval(s),B&&clearInterval(B);try{d?.close(1e3)}catch{}x&&clearTimeout(x),P&&clearTimeout(P);try{G?.close(1e3)}catch{}process.exit(0)};process.on("SIGTERM",a),process.on("SIGINT",a)}Ge().catch(e=>{m("Fatal error",{error:e?.message}),process.exit(1)});
package/dist/index.js CHANGED
@@ -51,9 +51,9 @@ ${V}`:V}}}const x=new we({name:"aerostack-gateway",version:"0.15.29"},{capabilit
51
51
  Fix: Regenerate token at your dashboard \u2192 Workspace \u2192 Tokens
52
52
  `),process.exit(1)),e(` \u274C Workspace unreachable (HTTP ${t.status})`),e(`
53
53
  Fix: Check AEROSTACK_WORKSPACE_URL is correct
54
- `),process.exit(1));const n=await t.json();e(` \u2705 Workspace URL reachable (${m})`),e(` \u2705 Token valid (${n.token_name}, role: ${n.token_role})`),e(` \u2705 Workspace connected ("${n.workspace}")`),e(` \u2705 Tools available (${n.mcp_servers} tools)`);try{const s=await fetch(`${m}/chat/heartbeat`,{method:"POST",headers:{Authorization:`Bearer ${h}`,"Content-Type":"application/json"},body:JSON.stringify({agent_type:"check",user_agent:"aerostack-gateway/check",bridge_id:"check"}),signal:AbortSignal.timeout(1e4)});e(s.ok?" \u2705 Dashboard registration (bridge visible in admin)":` \u26A0\uFE0F Dashboard registration (returned ${s.status})`)}catch{e(" \u26A0\uFE0F Dashboard registration (heartbeat failed \u2014 non-fatal)")}e(`
54
+ `),process.exit(1));const n=await t.json();e(` \u2705 Workspace URL reachable (${m})`),e(` \u2705 Token valid (${n.token_name}, role: ${n.token_role})`),e(` \u2705 Workspace connected ("${n.workspace}")`);const s=4,o=(n.mcp_servers??0)+s;e(` \u2705 Tools available (${o} tools: ${n.mcp_servers} workspace + ${s} built-in)`);try{const a=await fetch(`${m}/chat/heartbeat`,{method:"POST",headers:{Authorization:`Bearer ${h}`,"Content-Type":"application/json"},body:JSON.stringify({agent_type:"check",user_agent:"aerostack-gateway/check",bridge_id:"check"}),signal:AbortSignal.timeout(1e4)});e(a.ok?" \u2705 Dashboard registration (bridge visible in admin)":` \u26A0\uFE0F Dashboard registration (returned ${a.status})`)}catch{e(" \u26A0\uFE0F Dashboard registration (heartbeat failed \u2014 non-fatal)")}e(`
55
55
  All checks passed. Your agent is connected.`),e(` Dashboard: ${n.dashboard_url||"https://app.aerostack.dev"}
56
- `),n.mcp_servers===0&&e(` Note: 0 tools found. Add MCP servers or functions to your workspace first.
56
+ `),n.mcp_servers===0&&e(` Tip: Add MCP servers to your workspace for more tools. Built-in tools (Local Guardian, Chat) are ready.
57
57
  `),process.exit(0)}catch(t){t?.name==="TimeoutError"||t?.name==="AbortError"?(e(" \u274C Workspace unreachable (timeout after 10s)"),e(`
58
58
  Fix: Check AEROSTACK_WORKSPACE_URL and your network connection
59
59
  `)):(e(` \u274C Connection failed (${t?.message??"unknown error"})`),e(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aerostack/gateway",
3
- "version": "0.17.3",
3
+ "version": "0.17.5",
4
4
  "description": "stdio-to-HTTP bridge connecting any MCP client to Aerostack Workspaces",
5
5
  "author": "Aerostack",
6
6
  "license": "MIT",