UNPKG

grix-connector

Version:

Connect local AI coding agents (Claude, Codex, Gemini, Qwen, DeepSeek, Cursor, OpenCode, Pi, OpenHuman, Reasonix) to the Grix scheduling platform. Also serves as an OpenClaw plugin for Grix channel transport.

6 lines (4 loc) 3.2 kB
import{EventEmitter as u}from"node:events";import{log as h}from"../../core/log/index.js";const f=3e4,d=3e3,p=3e4;class w extends u{baseUrl="";directory="";abortController=null;closed=!1;sseConnected=!1;async connect(e,t){this.baseUrl=e,this.directory=t??"",await this.subscribeEvents(),h.info("opencode-transport",`connected to ${e}`)}async subscribeEvents(){if(this.closed)return;this.abortController=new AbortController;const e=new URL("/event",this.baseUrl);this.directory&&e.searchParams.set("directory",this.directory);try{const t=await fetch(e.toString(),{method:"GET",headers:{Accept:"text/event-stream"},signal:this.abortController.signal});if(!t.ok||!t.body)throw new Error(`SSE connect failed: ${t.status}`);this.sseConnected=!0;const s=t.body.getReader(),r=new TextDecoder;let n="";for(;!this.closed;){const{done:c,value:i}=await s.read();if(c)break;n+=r.decode(i,{stream:!0});const o=n.split(` `);n=o.pop()??"";for(const a of o){const l=this.parseSseFrame(a);l&&this.emit("event",l)}}}catch(t){if(this.closed)return;const s=t instanceof Error?t.message:String(t);if(s.includes("abort"))return;h.warn("opencode-transport",`SSE error: ${s}, reconnecting...`),await this.reconnectSse()}finally{this.sseConnected=!1}}parseSseFrame(e){let t="",s=[];for(const r of e.split(` `))r.startsWith("event:")?t=r.slice(6).trim():r.startsWith("data:")&&s.push(r.slice(5).trimStart());if(s.length===0)return null;try{const r=s.join(` `),n=JSON.parse(r);return t&&n.type!==t&&(n.type=t),n}catch{return null}}async reconnectSse(){if(this.closed)return;const e=d+Math.random()*d;await new Promise(t=>setTimeout(t,Math.min(e,p))),this.closed||await this.subscribeEvents()}async request(e,t,s){if(this.closed)throw new Error("transport closed");const r=new URL(t,this.baseUrl);this.directory&&r.searchParams.set("directory",this.directory);const n=new AbortController,c=setTimeout(()=>n.abort(),f);try{const i={method:e,headers:{"Content-Type":"application/json"},signal:n.signal};s!==void 0&&(i.body=JSON.stringify(s));const o=await fetch(r.toString(),i);if(o.status===204)return;const a=await o.json();if(!o.ok)throw new Error(`REST ${e} ${t}: ${o.status} ${JSON.stringify(a)}`);return a}finally{clearTimeout(c)}}async createSession(e){return this.request("POST","/session",e??{})}async getSession(e){return this.request("GET",`/session/${e}`)}async deleteSession(e){await this.request("DELETE",`/session/${e}`)}async sendPromptAsync(e,t){await this.request("POST",`/session/${e}/prompt_async`,t)}async abortSession(e){await this.request("POST",`/session/${e}/abort`)}async respondPermission(e,t,s){await this.request("POST",`/session/${e}/permissions/${t}`,{response:s})}async listProviders(){return(await this.request("GET","/config/providers"))?.providers??[]}async healthCheck(){try{return(await fetch(new URL("/session",this.baseUrl).toString(),{method:"GET",headers:{"Content-Type":"application/json"},signal:AbortSignal.timeout(3e3)})).ok}catch{return!1}}disconnect(){this.closed=!0,this.abortController&&(this.abortController.abort(),this.abortController=null),this.sseConnected=!1,this.removeAllListeners()}get isConnected(){return this.sseConnected&&!this.closed}}export{w as OpenCodeTransport};