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.

7 lines (5 loc) 11 kB
import{execFileSync as I}from"node:child_process";import{stat as _}from"node:fs/promises";import{resolveCommandPath as k,spawnCommand as E,killProcessGroup as w}from"../../core/runtime/spawn.js";import{createInterface as A}from"node:readline";import{EventEmitter as b}from"node:events";import{join as C,resolve as T}from"node:path";import{homedir as M}from"node:os";import{fileURLToPath as $}from"node:url";import{InternalApiServer as P}from"../../core/mcp/internal-api-server.js";import{syncDefaultSkillsToDir as x}from"../../default-skills/index.js";import{buildSimpleProbeReport as R}from"../shared/probe-util.js";import{log as n}from"../../core/log/index.js";import{SessionBindingStore as D}from"../../core/persistence/session-binding-store.js";const v=120*1e3;function L(f){try{return process.kill(f,0),!0}catch(e){return e.code==="EPERM"}}class V extends b{type="codewhale";config;callbacks;alive=!1;stopped=!1;internalApi=null;deepSeekSessionId=null;activeEventId=null;activeSessionId=null;chunkSeq=0;activeClientMsgId=null;idleTimer=null;activeProcess=null;bindingStore=null;aibotSessionId="";cwd;lastUsage=null;currentModel=null;constructor(e,t){super(),this.config=e,this.callbacks=t;const i=e.options??{};if(this.aibotSessionId=String(i.aibotSessionId??"").trim(),this.bindingStore=i.bindingStore instanceof D?i.bindingStore:null,this.cwd=this.resolveCwd(),this.bindingStore&&this.aibotSessionId){const o=this.bindingStore.getCodeWhaleThreadId(this.aibotSessionId);o&&(this.deepSeekSessionId=o)}}resolveCwd(){if(this.bindingStore&&this.aibotSessionId){const e=this.bindingStore.get(this.aibotSessionId);if(e?.cwd)return e.cwd}return process.cwd()}async start(){this.alive=!0,await this.startInternalApiAndRegisterMcp(),this.notifyBindingReady(),n.info("codewhale-adapter","Ready (exec mode)")}async stop(){this.stopped=!0,this.alive=!1,this.stopComposing(),this.clearIdleTimer(),this.killActiveProcess(),this.internalApi&&(await this.internalApi.stop(),this.internalApi=null)}isAlive(){return this.alive}async createSession(e){const t=this.deepSeekSessionId??`ds-${Date.now()}`;return this.notifyBindingReady(),t}async resumeSession(e,t){}async destroySession(e){this.deepSeekSessionId=null,this.persistSessionId(void 0)}sendPrompt(e){const t=new y(e.adapterSessionId);return this.runMessage(e,t).catch(i=>{t.emitError(i instanceof Error?i:new Error(String(i)))}),t}async cancel(e){this.killActiveProcess()}setPermissionHandler(e){}async ping(e){if(this.stopped||!this.alive)return!1;const t=this.activeProcess;return!(t?.pid&&!L(t.pid))}getStatus(){return{alive:this.alive,busy:this.activeEventId!==null,sessions:this.deepSeekSessionId?1:0}}getActiveEventIds(){return this.activeEventId?[this.activeEventId]:[]}clearActiveEventForShutdown(){this.clearIdleTimer(),this.killActiveProcess(),this.activeEventId=null}getMcpConfig(){if(!this.internalApi)return null;const e=T($(import.meta.url),"../../../mcp/acp-mcp-server.js");return{name:"grix-connector-tools",command:process.execPath,args:[e,"--api-url",this.internalApi.url]}}async probe(e){const t=this.getStatus();return R(this.config.command||"codewhale",{alive:t.alive,busy:t.busy,started:this.alive},e)}getUsageSnapshot(){return this.lastUsage}async startInternalApiAndRegisterMcp(){try{this.internalApi=new P,this.internalApi.setInvokeHandler(async(a,p)=>this.callbacks.agentInvoke(a,p)),await this.internalApi.start(0),n.info("codewhale-adapter",`Internal API started at ${this.internalApi.url}`);const e=this.getMcpConfig(),t={...process.env,...this.config.env},i=typeof t.PATH=="string"?t.PATH:void 0,o=k(this.config.command||"codewhale",i);try{I(o,["mcp","remove",e.name],{env:t,timeout:1e4,stdio:"ignore"})}catch{}const s=["mcp","add",e.name,"--command",e.command];for(const a of e.args)s.push("--arg",a);I(o,s,{env:t,timeout:1e4,stdio:"ignore"}),n.info("codewhale-adapter",`Registered MCP server: ${e.name}`);const r=C(M(),".codewhale","skills"),d=x(r);d.length>0&&n.info("codewhale-adapter",`Synced connector skills to ${r}: [${d.join(", ")}]`)}catch(e){n.warn("codewhale-adapter",`Failed to register MCP tools (non-fatal): ${e instanceof Error?e.message:String(e)}`)}}getSupportedCommands(){return[{name:"status",description:"Show session and working directory status"}]}async execCommand(e,t,i){return e==="status"?{status:"ok",message:`Session: ${this.deepSeekSessionId??"none"}, CWD: ${this.cwd}`,data:{sessionId:this.deepSeekSessionId,cwd:this.cwd,alive:this.alive}}:{status:"unsupported",message:`Unknown command: ${e}`}}async handleLocalAction(e){const t=e.action_type??"",i=e.params??{};switch(t){case"get_context":return this.callbacks.sendLocalActionResult(e.action_id,"ok",{sessionId:this.deepSeekSessionId,cwd:this.cwd,model:this.currentModel}),{handled:!0,kind:"get_context"};case"set_model":{const o=String(i.model_id??"").trim();return o?(this.currentModel=o,this.callbacks.sendLocalActionResult(e.action_id,"ok",{outcome:"model_set",modelId:o}),n.info("codewhale-adapter",`Model set to: ${o}`),{handled:!0,kind:"set_model"}):(this.callbacks.sendLocalActionResult(e.action_id,"failed",void 0,"invalid_params","model_id is required"),{handled:!0,kind:"set_model"})}default:return{handled:!1,kind:""}}}deliverInboundEvent(e){const t=e.content;if(this.activeEventId){n.info("codewhale-adapter",`Event ${e.event_id}: rejected, busy with ${this.activeEventId}`),this.callbacks.sendEventResult(e.event_id,"failed","agent busy");return}this.startNewMessage(e,t)}deliverStopEvent(e,t){this.activeEventId===e&&(this.killActiveProcess(),this.callbacks.sendEventResult(e,"canceled","stopped by user"),this.clearActive())}startNewMessage(e,t){this.activeEventId=e.event_id,this.activeSessionId=e.session_id,this.chunkSeq=0,this.activeClientMsgId=`ds-${Date.now()}-${Math.random().toString(36).slice(2,8)}`,this.startComposing();const i={adapterSessionId:this.deepSeekSessionId??"",text:t,contextMessages:e.context_messages_json?JSON.parse(e.context_messages_json).map(s=>({senderId:s.sender_id??"unknown",content:s.content})):void 0},o=new y(this.deepSeekSessionId??"");this.runMessage(i,o,e.event_id,e.session_id).catch(s=>{n.error("codewhale-adapter",`Message failed: ${s}`),this.callbacks.sendEventResult(e.event_id,"failed",s instanceof Error?s.message:String(s)),this.clearActive()}),this.resetIdleTimer(e.event_id)}buildExecArgs(e){const t=["exec","--output-format","stream-json"];return this.currentModel&&t.push("--model",this.currentModel),this.deepSeekSessionId&&t.push("--resume",this.deepSeekSessionId),t.push("--",e),t}async runMessage(e,t,i,o){let s=e.text;e.contextMessages&&e.contextMessages.length>0&&(s=`Conversation context: ${e.contextMessages.map(l=>`[${l.senderId??"unknown"}]: ${l.content}`).join(` `)} Latest user message: ${s}`);const r=this.config.command||"codewhale",d=this.buildExecArgs(s),a={...process.env,...this.config.env},p=k(r,typeof a.PATH=="string"?a.PATH:void 0);n.info("codewhale-adapter",`Spawning: ${p} ${d.slice(0,5).join(" ")}...`);try{if(!(await _(this.cwd)).isDirectory())throw new Error(`Bound path is not a directory: ${this.cwd}`)}catch(c){throw String(c?.code??"")==="ENOENT"?new Error(`Bound directory does not exist: ${this.cwd}. Please rebind with /grix open <valid-directory>.`):c}const u=E(p,d,{cwd:this.cwd,env:a}).process;return this.activeProcess=u,u.stderr?.on("data",c=>{const l=c.toString().trim();l&&n.info("codewhale-adapter",`[codewhale stderr] ${l}`)}),new Promise((c,l)=>{let m=!1,g="";const S=()=>{this.activeProcess=null};u.on("error",h=>{m||(m=!0,S(),l(h))}),u.on("exit",h=>{if(g.trim()&&this.handleOutputLine(g.trim(),i),g="",m){S();return}if(m=!0,S(),h!==0&&i&&this.activeEventId===i){l(new Error(`codewhale exec exited with code ${h}`));return}t.emitDone({status:"completed"}),c()}),A({input:u.stdout}).on("line",h=>{h.trim()&&this.handleOutputLine(h.trim(),i)}),u.stdin?.end()})}handleOutputLine(e,t){let i;try{i=JSON.parse(e)}catch{n.error("codewhale-adapter",`Invalid JSON: ${e.slice(0,200)}`);return}switch(i.type){case"content":{const s=i.content;s&&t&&this.activeEventId===t&&this.activeSessionId&&(this.chunkSeq++,this.callbacks.sendStreamChunk(t,this.activeSessionId,s,this.chunkSeq,!1,this.activeClientMsgId??void 0),this.startComposing(),this.resetIdleTimer(t));break}case"session_capture":{const s=i.content;s&&(this.deepSeekSessionId=s,this.persistSessionId(s),n.info("codewhale-adapter",`Session captured: ${s}`));break}case"metadata":{const s=i.meta;if(s){n.info("codewhale-adapter",`Metadata: model=${s.model}, tokens_in=${s.input_tokens}, tokens_out=${s.output_tokens}`);const r=Number(s.input_tokens??0),d=Number(s.output_tokens??0);if(r>0||d>0){const a=this.lastUsage;this.lastUsage={sampledAt:new Date().toISOString(),turns:(a?.turns??0)+1,total:{input:(a?.total.input??0)+r,output:(a?.total.output??0)+d}}}}break}case"tool_use":{const s=i.name,r=typeof i.input=="string"?i.input:JSON.stringify(i.input??{});s&&t&&this.activeEventId===t&&this.activeSessionId&&(n.info("codewhale-adapter",`Tool use: ${s}`),this.callbacks.sendToolUse(t,this.activeSessionId,s,r),this.resetIdleTimer(t));break}case"tool_result":{const s=i.name,r=i.output;t&&this.activeEventId===t&&this.activeSessionId&&(this.callbacks.sendToolResult(t,this.activeSessionId,s??"unknown",r??""),this.resetIdleTimer(t));break}case"done":{this.handleMessageCompleted(t);break}default:break}}handleMessageCompleted(e){if(this.stopComposing(),e&&this.activeEventId===e){const t=this.activeSessionId??"",i=this.activeClientMsgId??void 0;t&&(this.chunkSeq++,this.callbacks.sendStreamChunk(e,t,"",this.chunkSeq,!0,i)),this.callbacks.sendEventResult(e,"responded"),this.clearActive()}}killActiveProcess(){const e=this.activeProcess;this.activeProcess=null,e?.pid&&(w(e,"SIGTERM"),setTimeout(()=>{if(e.exitCode===null&&e.signalCode===null)try{w(e,"SIGKILL")}catch{}},5e3).unref())}notifyBindingReady(){!this.aibotSessionId||!this.cwd||this.callbacks.sendUpdateBindingCard(this.aibotSessionId,"ready",this.cwd)}persistSessionId(e){!this.bindingStore||!this.aibotSessionId||this.bindingStore.setCodeWhaleThreadId(this.aibotSessionId,e)}startComposing(){}stopComposing(){}resetIdleTimer(e){this.clearIdleTimer(),this.idleTimer=setTimeout(()=>{this.activeEventId===e&&(n.error("codewhale-adapter",`Agent idle for ${v/1e3}s: ${e}`),this.killActiveProcess(),this.callbacks.sendEventResult(e,"failed",`agent idle for ${v/1e3}s`),this.clearActive(),this.emit("stuck"))},v)}clearIdleTimer(){this.idleTimer&&(clearTimeout(this.idleTimer),this.idleTimer=null)}clearActive(){const e=this.activeEventId;this.stopComposing(),this.activeEventId=null,this.activeSessionId=null,this.chunkSeq=0,this.activeClientMsgId=null,this.clearIdleTimer(),e&&this.emit("eventDone",e)}}class y extends b{adapterSessionId;constructor(e){super(),this.adapterSessionId=e}emitDone(e){this.emit("done",e)}emitError(e){if(this.listenerCount("error")===0){n.warn("codewhale-adapter",`Prompt handle error (no listeners): ${e.message}`);return}this.emit("error",e)}async cancel(){}}export{V as CodeWhaleAdapter};