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.
2 lines (1 loc) • 11.5 kB
JavaScript
import{EventEmitter as S}from"events";import{log as l}from"../core/log/index.js";import{AgentEventType as c}from"../types/events.js";import{mapSessionUpdate as M}from"./event-mapper.js";class h extends Error{authMethods;constructor(e){super("ACP authentication required"),this.name="AcpAuthRequiredError",this.authMethods=e}}function p(a){if(!a||typeof a!="object")return!1;const e=a;return e.code===-32e3?!0:e.code===-32603&&e.data?.details?/\b(401|token\s*expired|access\s*token)\b/i.test(e.data.details):!1}function m(a){return!a||typeof a!="object"?[{id:"oauth"}]:a.data?.authMethods??[{id:"oauth"}]}class u extends S{acpSessionId="";alive=!1;pendingPermissions=new Map;availableModes=[];availableModels=[];currentMode="";currentModel="";listSupported=!1;loadSupported=!1;_supportsCommandsExecute=!1;_availableCommands=[];transport;settleTimer=null;static SETTLE_MS=3e3;pendingToolCallIds=new Set;settleSuppressedSince=0;static SETTLE_SUPPRESS_MAX_MS=900*1e3;constructor(){super(),this.transport=null}get sessionId(){return this.acpSessionId}get isAlive(){return this.alive}get modes(){return[...this.availableModes]}get mode(){return this.currentMode}get models(){return[...this.availableModels]}get model(){return this.currentModel}get sessionOptions(){return{modes:[...this.availableModes],currentModeId:this.currentMode,models:[...this.availableModels],currentModelId:this.currentModel}}get supportsCommandsExecute(){return this._supportsCommandsExecute}get availableCommands(){return[...this._availableCommands]}async connect(e){this.transport=e.transport,this.transport.on("close",()=>{this.alive&&(this.alive=!1,this.emit("session-lost"))}),this.transport.setHandlers((n,r)=>{n==="session/update"?this.handleSessionUpdate(r):n==="_kiro.dev/metadata"?this.handleKiroMetadata(r):n==="_kiro.dev/commands/available"?this.handleCommandsAvailable(r):n==="_kiro.dev/compaction/status"?this.emit("compactionStatus",r):n==="_kiro.dev/clear/status"&&this.emit("clearStatus",r)},(n,r,d)=>{n==="session/request_permission"?this.handlePermissionRequest(r,d):n.startsWith("cursor/")?this.transport.respondSuccess(r,{}).catch(()=>{}):this.transport.respondError(r,-32601,"method not implemented").catch(()=>{})});const t=await this.initialize();this.listSupported=!!t.agentCapabilities?.sessionCapabilities?.list,this.loadSupported=!!t.agentCapabilities?.loadSession;const i=t.agentCapabilities??{};if(this._supportsCommandsExecute=!!(i.extensions?.["_kiro.dev/commands"]||i.commands),e.authMethod)try{await this.transport.call("authenticate",{methodId:e.authMethod})}catch(n){throw p(n)?new h(m(n)):n}const s=e.sessionId&&e.sessionId!=="__continue__";let o=!1;if(s){if(!this.loadSupported)throw new Error(`session/load not supported by agent, cannot resume session ${e.sessionId}`);try{await this.loadSession(e.sessionId,e)&&(o=!0)}catch(n){throw p(n)?new h(m(n)):n}if(!o)throw new Error(`session/load failed for session ${e.sessionId}`)}else try{await this.newSession(e)}catch(n){throw p(n)?new h(m(n)):n}e.initialMode&&await this.setLiveMode(e.initialMode).catch(()=>{}),e.initialModel&&await this.setModel(e.initialModel).catch(()=>{}),this.alive=!0}async initialize(){return await this.transport.call("initialize",{protocolVersion:1,clientCapabilities:{fs:{readTextFile:!1,writeTextFile:!1},terminal:!1},clientInfo:{name:"grix-connector-acp",version:"0.2.0"}})}buildMcpEntries(e){return(e??[]).map(t=>{const i={name:t.name,command:t.command};return t.args&&(i.args=t.args),t.env&&Object.keys(t.env).length>0&&(i.env=Object.entries(t.env).map(([s,o])=>({name:s,value:o}))),i})}async newSession(e){const t=this.buildMcpEntries(e.mcpServers),i={cwd:e.cwd||process.cwd(),mcpServers:t};e.additionalDirectories&&e.additionalDirectories.length>0&&(i.additionalDirectories=e.additionalDirectories);const s=await this.transport.call("session/new",i);if(!s?.sessionId)throw new Error("session/new returned empty sessionId");this.acpSessionId=s.sessionId,this.absorbModes(s.modes),this.absorbModels(s.models)}async loadSession(e,t){const i=this.buildMcpEntries(t.mcpServers),s={sessionId:e,cwd:t.cwd||process.cwd(),mcpServers:i};t.additionalDirectories&&t.additionalDirectories.length>0&&(s.additionalDirectories=t.additionalDirectories);const o=await this.transport.call("session/load",s);return o?.sessionId?(this.acpSessionId=o.sessionId,this.absorbModes(o.modes),this.absorbModels(o.models),!0):o&&(o.modes||o.models)?(this.acpSessionId=e,this.absorbModes(o.modes),this.absorbModels(o.models),!0):!1}absorbModes(e){e?.availableModes?.length&&(this.availableModes=[...e.availableModes],e.currentModeId&&(this.currentMode=e.currentModeId))}absorbModels(e){e?.availableModels?.length&&(this.availableModels=[...e.availableModels],e.currentModelId&&(this.currentModel=e.currentModelId))}async send(e,t,i){if(!this.alive)throw new Error("session not active");if(!this.acpSessionId)throw new Error("no agent session id");const s=[{type:"text",text:e}];if(t)for(const o of t)s.push({type:"image",data:o.data.toString("base64"),mimeType:o.mimeType});this.pendingToolCallIds.clear(),this.settleSuppressedSince=0,await this.transport.call("session/prompt",{sessionId:this.acpSessionId,prompt:s,...this.currentModel?{modelId:this.currentModel}:{}}),this.scheduleSettledResult()}scheduleSettledResult(){this.settleTimer&&clearTimeout(this.settleTimer),this.settleTimer=setTimeout(()=>{if(this.settleTimer=null,!!this.alive){if(this.pendingToolCallIds.size>0){if(this.settleSuppressedSince===0&&(this.settleSuppressedSince=Date.now()),Date.now()-this.settleSuppressedSince<u.SETTLE_SUPPRESS_MAX_MS){l.info("acp-client",`settle suppressed: ${this.pendingToolCallIds.size} pending tool call(s) for session ${this.acpSessionId}`),this.scheduleSettledResult();return}l.warn("acp-client",`settle suppression exceeded ${u.SETTLE_SUPPRESS_MAX_MS/6e4}min with ${this.pendingToolCallIds.size} pending tool call(s), emitting Result anyway`)}this.settleSuppressedSince=0,l.info("acp-client",`settle timer fired, emitting Result for session ${this.acpSessionId}`),this.emit("event",{type:c.Result,sessionId:this.acpSessionId,done:!0})}},u.SETTLE_MS),this.settleTimer.unref()}clearSettleTimer(){this.settleTimer&&(clearTimeout(this.settleTimer),this.settleTimer=null),this.settleSuppressedSince=0}async cancel(){if(this.acpSessionId){this.pendingToolCallIds.clear(),this.settleSuppressedSince=0;try{await this.transport.notify("session/cancel",{sessionId:this.acpSessionId})}catch{}}}async authenticate(e){await this.transport.call("authenticate",{methodId:e})}async respondPermission(e,t){const i=this.pendingPermissions.get(e);if(!i)throw new Error(`unknown permission request: ${e}`);this.pendingPermissions.delete(e);const s=this.pickPermissionOptionId(t.behavior,i.options),o=this.buildPermissionResult(t.behavior,s);await this.transport.respondSuccess(i.rpcId,o)}async ping(e=1e4){if(!this.alive)return!1;try{const t=new AbortController,i=setTimeout(()=>t.abort(),e);return await this.transport.call("session/list",{},t.signal),clearTimeout(i),!0}catch(t){return t?.code===-32601}}async setLiveMode(e){if(!this.acpSessionId)return l.warn("acp-client",`setLiveMode("${e}") skipped: no active session`),!1;const t=this.matchAvailableMode(e);if(!t)return l.warn("acp-client",`setLiveMode("${e}") failed: mode not found in available [${this.availableModes.map(i=>i.id).join(",")}]`),!1;try{const i=new AbortController,s=setTimeout(()=>i.abort(),8e3);return await this.transport.call("session/set_mode",{sessionId:this.acpSessionId,modeId:t},i.signal),clearTimeout(s),this.currentMode=t,!0}catch(i){return l.warn("acp-client",`setLiveMode("${t}") RPC failed: ${i instanceof Error?i.message:i}`),!1}}async setModel(e){if(!this.acpSessionId)return l.warn("acp-client",`setModel("${e}") skipped: no active session`),!1;try{const t=new AbortController,i=setTimeout(()=>t.abort(),8e3);return await this.transport.call("session/set_model",{sessionId:this.acpSessionId,modelId:e},t.signal),clearTimeout(i),this.currentModel=e,!0}catch(t){return l.warn("acp-client",`setModel("${e}") RPC failed: ${t instanceof Error?t.message:t}`),!1}}handleSessionUpdate(e){this.emit("activity");const t=e?.update?.sessionUpdate,i=t==="usage_update";if(t==="tool_call"||t==="tool_call_update"){const o=e?.update??{},n=String(o.toolCallId??""),r=String(o.status??"").toLowerCase().trim();n&&(r==="completed"||r==="failed"?this.pendingToolCallIds.delete(n):t==="tool_call"&&this.pendingToolCallIds.add(n))}this.settleTimer&&!i&&this.scheduleSettledResult();const s=M(this.acpSessionId,e);for(const o of s)o.sessionId||(o.sessionId=this.acpSessionId),o.type===c.Result&&(this.pendingToolCallIds.clear(),this.settleTimer&&this.clearSettleTimer()),this.emit("event",o)}handleKiroMetadata(e){const i=e?.contextUsagePercentage;typeof i!="number"||!Number.isFinite(i)||this.emit("event",{type:c.ContextWindowUpdate,sessionId:this.acpSessionId,contextWindow:{usedPercentage:Math.min(100,Math.max(0,i))}})}handleCommandsAvailable(e){const t=e,i=Array.isArray(t?.commands)?t.commands:[];this._availableCommands=i.map(s=>({name:String(s.name??""),description:s.description?String(s.description):void 0,args:s.args?String(s.args):void 0})),this._supportsCommandsExecute=!0,this.emit("commandsAvailable",this._availableCommands)}async executeCommand(e,t){if(!this.alive)throw new Error("session not active");if(!this.acpSessionId)throw new Error("no agent session id");const i=new AbortController,s=setTimeout(()=>i.abort(),2e4);try{const o=await this.transport.call("_kiro.dev/commands/execute",{sessionId:this.acpSessionId,command:e,...t?{args:t}:{}},i.signal);return clearTimeout(s),{status:o?.status??"ok",message:o?.message,options:o?.options,data:o?.data}}catch(o){clearTimeout(s);const n=o instanceof Error?o.message:String(o);if(o?.code===-32601)throw this._supportsCommandsExecute=!1,o;return{status:"failed",message:n}}}handlePermissionRequest(e,t){const i=t,s=i?.toolCall??{},o=String(e),n=Array.isArray(i?.options)?i.options:[];this.pendingPermissions.set(o,{rpcId:e,options:n});const r=s.title||s.kind||"permission",d=s.toolCallId||o,f=s.title||"",w={type:c.PermissionRequest,requestId:o,toolName:r,toolInput:f||d,sessionId:this.acpSessionId,permissionRequest:{requestId:o,toolCallId:d,toolName:r,toolTitle:f,options:n,rawParams:t}};this.emit("event",w)}matchAvailableMode(e){const t=e.toLowerCase();for(const i of this.availableModes)if(i.id.toLowerCase()===t||i.name.toLowerCase()===t)return i.id;return""}pickPermissionOptionId(e,t){if(t.length===0)return"";if(e==="deny"){for(const s of t)if(s.kind==="reject_once"||s.kind==="reject_always")return s.optionId;for(const s of t)if(s.kind.toLowerCase().includes("reject")||s.kind.toLowerCase().includes("deny"))return s.optionId;for(const s of t)if(s.name.toLowerCase().includes("reject")||s.name.toLowerCase().includes("deny"))return s.optionId;return t[t.length-1].optionId}const i=e==="allow-always"?"allow_always":e==="allow-once"?"allow_once":null;if(i){for(const s of t)if(s.kind===i)return s.optionId}for(const s of t)if(s.kind.toLowerCase().includes("allow"))return s.optionId;for(const s of t)if(s.name.toLowerCase().includes("allow"))return s.optionId;return t[0].optionId}buildPermissionResult(e,t){return e==="deny"?t?{outcome:{outcome:"selected",optionId:t}}:{outcome:{outcome:"cancelled"}}:t?{outcome:{outcome:"selected",optionId:t}}:{outcome:{outcome:"cancelled"}}}}export{h as AcpAuthRequiredError,u as AcpClient,p as isAuthRequiredError};