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.
3 lines (2 loc) • 17.2 kB
JavaScript
import{readFileSync as Q,readdirSync as F,writeFileSync as B}from"node:fs";import{join as x}from"node:path";import{AgentInstance as A}from"./bridge/bridge.js";import{buildSharedInstanceConfig as L,diffSharedOwners as R,sharedInstanceKey as z}from"./manager-share-config.js";import{GRIX_PATHS as S,log as d}from"./core/log/index.js";import{resolveClientVersion as G}from"./core/util/client-version.js";import{UpgradeChecker as K}from"./core/upgrade/upgrade-checker.js";import{AgentGlobalConfigStore as W}from"./core/persistence/agent-global-config-store.js";import{scanSkills as V,dedupeSkills as J}from"./adapter/claude/skill-scanner.js";import{scanDefaultSkills as X,logDefaultSkillsCheck as Y,cleanupProjectedSkills as D}from"./default-skills/index.js";import{resolveCopilotCommand as q}from"./core/runtime/copilot-resolve.js";import{getCliVersion as Z,resolveCliPath as ee}from"./core/util/cli-probe.js";import{AgentInstaller as te}from"./core/installer/installer.js";import{reportInstallFailure as ne}from"./core/observability/sentry.js";const ae=8e3;function oe(){const n=q();return[{clientType:"openclaw",command:"openclaw"},{clientType:"claude",command:"claude"},{clientType:"codex",command:"codex"},{clientType:"gemini",command:"gemini"},{clientType:"qwen",command:"qwen"},{clientType:"hermes",command:"hermes"},{clientType:"reasonix",command:"reasonix"},{clientType:"codewhale",command:"codewhale"},{clientType:"opencode",command:"opencode"},{clientType:"cursor",command:"agent"},{clientType:"pi",command:"pi"},{clientType:"openhuman",command:"openhuman-core"},{clientType:"kiro",command:"kiro-cli"},{clientType:"copilot",command:n.command},{clientType:"agy",command:"agy"}]}async function se(){return Promise.all(oe().map(async n=>{const e=await ee(n.command);if(!e)return{client_type:n.clientType,command:n.command,installed:!1,path:null,version:null};const t=await Z(e,n.versionArgs??["--version"]);return{client_type:n.clientType,command:n.command,installed:!0,path:e,version:t.version,error:t.error}}))}function ie(n){switch(n){case"claude":return{adapterType:"claude",command:"claude"};case"codex":return{adapterType:"codex",command:"codex",options:{sandboxMode:"danger-full-access"}};case"gemini":return{adapterType:"acp",command:"gemini",autoInjectArgs:{acp:!0},enableSessionBinding:!0};case"qwen":return{adapterType:"acp",command:"qwen",adapterHint:"qwen/base",autoInjectArgs:{acp:!0},enableSessionBinding:!0};case"pi":return{adapterType:"pi",command:"pi"};case"cursor":return{adapterType:"cursor",command:"agent"};case"reasonix":return{adapterType:"acp",command:"reasonix",args:["acp"],enableSessionBinding:!0};case"codewhale":return{adapterType:"codewhale",command:"codewhale",enableSessionBinding:!0};case"openhuman":return{adapterType:"openhuman",command:"openhuman-core",enableSessionBinding:!0};case"kiro":return{adapterType:"acp",command:"kiro-cli",args:["acp"],enableSessionBinding:!0};case"opencode":return{adapterType:"opencode",command:"opencode",args:["serve"],enableSessionBinding:!0};case"copilot":{const e=q();return{adapterType:"acp",command:e.command,args:[...e.prefixArgs,"--acp"],enableSessionBinding:!0}}case"agy":return{adapterType:"agy",command:"agy",enableSessionBinding:!0};case"hermes":throw new Error('client_type "hermes" is not handled by grix-connector. Hermes runs as a separate project \u2014 see https://github.com/askie/grix-hermes-python');default:throw new Error(`Unsupported client_type: ${n}`)}}function re(n){const e=String(n??"").trim().toLowerCase().replace(/[^a-z0-9._-]+/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"")||"default";return x(S.data,`session-bindings-${e}.json`)}function ce(n){const e=String(n??"").trim().toLowerCase().replace(/[^a-z0-9._-]+/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"")||"default";return x(S.data,`active-events-${e}.json`)}function le(...n){const e=[],t=new Set;for(const o of n)for(const a of o??[]){const s=String(a??"").trim(),i=s.toLowerCase();!s||t.has(i)||(t.add(i),e.push(s))}return e.length>0?e:void 0}function de(n,e){const t={claude:{maxConcurrent:1,maxQueued:5,queueTimeoutMs:0},codex:{maxConcurrent:1,maxQueued:5,queueTimeoutMs:0},cursor:{maxConcurrent:1,maxQueued:3,queueTimeoutMs:0},acp:{maxConcurrent:1,maxQueued:3,queueTimeoutMs:0},pi:{maxConcurrent:1,maxQueued:5,queueTimeoutMs:0},codewhale:{maxConcurrent:1,maxQueued:3,queueTimeoutMs:0},openhuman:{maxConcurrent:1,maxQueued:3,queueTimeoutMs:0},opencode:{maxConcurrent:1,maxQueued:3,queueTimeoutMs:0},agy:{maxConcurrent:1,maxQueued:3,queueTimeoutMs:0}},o=t[n]??t.acp;return{maxConcurrent:e?.max_concurrent??o.maxConcurrent??1,maxQueued:e?.max_queued??o.maxQueued??3,queueTimeoutMs:e?.queue_timeout_ms??o.queueTimeoutMs??0,cancelableQueued:!0,cancelableRunning:!0}}function P(n){const e=G(),t=String(n.client_type??"").trim().toLowerCase(),o=ie(t),a=String(n.ws_url??"").trim(),s="get_session_usage",i="get_rate_limits",l="get_agent_global_config";if(!n.name?.trim())throw new Error("agent name is required");if(!a)throw new Error(`agent ${n.name}: ws_url is required`);if(!n.agent_id?.trim())throw new Error(`agent ${n.name}: agent_id is required`);if(!n.api_key?.trim())throw new Error(`agent ${n.name}: api_key is required`);const r=o.adapterType,u=r==="acp",f=t==="qwen",m={...o.options??{}},c=r==="codex"?{capabilities:["local_action_v1","agent_invoke"],localActions:["session_control","get_context","set_model","set_mode","set_reasoning_effort","set_sandbox_mode","exec_approve","exec_reject","file_list","create_folder","turn_interrupt","permission_approve","permission_reject","thread_compact",s,i]}:null,p=r==="claude"?{localActions:["session_control","set_mode","set_model","claude_interaction_reply","exec_approve","exec_reject","file_list","create_folder","thread_compact",s,i]}:null,g=f?{capabilities:["stream_chunk","local_action_v1"],localActions:["exec_approve","exec_reject","permission_approve","permission_reject","session_control","set_model","set_mode","file_list","create_folder",s],adapterHint:"qwen/base"}:null,_=r==="pi"?{adapterHint:"pi/base",capabilities:["local_action_v1"],localActions:["session_control","set_model","get_context","file_list","create_folder",s]}:null,h=r==="openhuman"?{adapterHint:"openhuman/base",capabilities:["local_action_v1"],localActions:["session_control","set_model","file_list","create_folder",s]}:null,w=r==="cursor"?{adapterHint:"cursor/base",capabilities:["stream_chunk","local_action_v1"],localActions:["session_control","set_model","set_mode","get_context","file_list","create_folder",s,i]}:null,y=r==="codewhale"?{capabilities:["stream_chunk","local_action_v1"],localActions:["session_control","set_model","file_list","create_folder",s]}:null,k=r==="opencode"?{adapterHint:"opencode/base",capabilities:["stream_chunk","local_action_v1"],localActions:["exec_approve","exec_reject","permission_approve","permission_reject","session_control","set_model","set_mode","file_list","create_folder",s]}:null,T=r==="agy"?{adapterHint:"agy/base",capabilities:["stream_chunk","local_action_v1"],localActions:["session_control","set_model","file_list","create_folder",s]}:null,E=u&&!f?{localActions:["exec_approve","exec_reject","permission_approve","permission_reject","session_control","set_model","set_mode","file_list","create_folder",s]}:null,H=t==="kiro"?{localActions:["exec_approve","exec_reject","permission_approve","permission_reject","session_control","set_model","set_mode","file_list","create_folder","thread_compact",s,i]}:null,O=r==="codex"||r==="claude"||t==="gemini"?["session_control","set_model","set_mode"]:void 0,N=[s,i];u&&m.raw_transport===void 0&&(m.raw_transport=t==="gemini");const U=`${t}/base`,$=r==="claude"?"claude":r==="codex"?"codex":r==="pi"?"pi":t==="kiro"?"kiro":"gemini";let b;try{const M=$==="kiro"?void 0:process.cwd();b=V({mode:$,projectDir:M})??void 0,b&&b.length===0&&(b=void 0)}catch{}const I=X();if(I.length>0){const v=J([...b??[],...I]),j=v.filter(C=>C.source==="connector").map(C=>C.name);j.length>0&&d.info("manager",`[${n.name}] injecting connector skills: [${j.join(", ")}]`),b=v.length>0?v:void 0}return{name:n.name,adapterType:r,aibot:{url:a,agentId:n.agent_id,apiKey:n.api_key,clientType:t,clientVersion:e,adapterHint:o.adapterHint??g?.adapterHint??_?.adapterHint??h?.adapterHint??w?.adapterHint??k?.adapterHint??T?.adapterHint??U,capabilities:c?.capabilities??y?.capabilities??_?.capabilities??h?.capabilities??w?.capabilities??k?.capabilities??T?.capabilities??g?.capabilities??["stream_chunk","local_action_v1","connector_upgrade"],localActions:le(c?.localActions??y?.localActions??p?.localActions??_?.localActions??h?.localActions??w?.localActions??k?.localActions??T?.localActions??g?.localActions??H?.localActions??E?.localActions??["exec_approve","exec_reject"],O,N,["connector_rollback","connector_upgrade_push",l]),skills:b},agent:{command:o.command,args:[...o.args??[],...n.args??[]],env:n.env},adapterOptions:m,acpAuthMethod:m.auth_method,acpInitialMode:m.initial_mode,acpMcpTools:m.acp_mcp_tools,promptTimeoutMs:n.prompt_timeout_ms,bindingsPath:re(n.name),activeEventStorePath:ce(n.name),...o.enableSessionBinding||u?{enableSessionBinding:!0}:{},...o.autoInjectArgs?{autoInjectArgs:o.autoInjectArgs}:{},poolMaxSize:n.pool?.maxSize,poolIdleTimeoutMs:n.pool?.idleTimeoutMs,eventQueue:de(r,n.event_queue),logDir:S.log,providerBaseUrl:n.provider_base_url?.trim()||void 0,providerApiKey:n.provider_api_key?.trim()||void 0}}function ue(){const n=process.env.GRIX_AGENT_STARTUP_WAIT_MS,e=Number(n);return Number.isFinite(e)&&e>=500?Math.floor(e):ae}class $e{instances=[];configMap=new Map;sharedInstances=new Map;shareSyncChains=new Map;stopping=!1;upgradeChecker=null;globalConfigStore;configDir=S.config;installer=new te;async start(e){const t=e??S.config;this.configDir=t,d.info("manager",`Loading configs from ${t}`),Y(),D(),this.globalConfigStore=new W(x(S.data,"agent-global-configs.json")),this.globalConfigStore.load();const o=F(t).filter(l=>l.endsWith(".json")).sort();if(o.length===0)throw new Error(`No config files found in ${t}`);const a=[];let s=0;for(const l of o)try{const r=Q(x(t,l),"utf-8"),u=JSON.parse(r);if(Array.isArray(u.agents)){if(u.agents.length===0){d.error("manager",`No agents array found in ${l}`),s++;continue}for(const f of u.agents)try{const m=P(f);a.push({config:m,file:l}),d.info("manager",`Loaded ${m.name} (${m.adapterType??"acp"}) from ${l}`)}catch(m){const c=typeof f?.name=="string"?f.name:"<unknown>";d.error("manager",`Invalid agent config in ${l} (name=${c}): ${m}`),s++}}else d.error("manager",`Unrecognized config format in ${l}`)}catch(r){d.error("manager",`Failed to load ${l}: ${r}`)}let i=0;if(a.length>0){const l=ue();d.info("manager",`Starting ${a.length} agent(s), startup wait=${l}ms`);const r=()=>this.upgradeChecker?.triggerCheck(),u=c=>{this.instances=this.instances.filter(p=>p!==c)},f=a.map(({config:c})=>{const p=new A(c,this.globalConfigStore);return p.setUpgradeTrigger(r),p.setShareSetHandler(g=>this.onShareSet(c,g)),this.instances.push(p),this.configMap.set(c.name,c),{config:c,instance:p,startPromise:p.start()}}),m=await Promise.all(f.map(async c=>{const p=await new Promise(g=>{let _=!1;const h=setTimeout(()=>{_||(_=!0,g({kind:"timeout"}))},l);c.startPromise.then(()=>{_||(_=!0,clearTimeout(h),g({kind:"started"}))}).catch(w=>{_||(_=!0,clearTimeout(h),g({kind:"failed",error:w}))})});return{task:c,outcome:p}}));for(const{task:c,outcome:p}of m)if(p.kind!=="started"){if(p.kind==="failed"){u(c.instance),d.error("manager",`Failed to start ${c.config.name}: ${p.error}`);continue}i++,d.warn("manager",`Startup pending for ${c.config.name}, continue retrying in background`),c.startPromise.then(()=>{d.info("manager",`Delayed start succeeded: ${c.config.name}`)}).catch(g=>{u(c.instance),d.error("manager",`Delayed start failed: ${c.config.name}: ${g}`)})}if(this.instances.length>0){const c=Math.max(0,this.instances.length-i);d.info("manager",`${c}/${a.length} agent(s) running now`)}i>0&&d.warn("manager",`${i} agent(s) still connecting in background`)}if(this.instances.length===0&&a.length>0)throw new Error("All agent configurations failed to start");if(a.length>0){const l=a[0].config;this.upgradeChecker=new K([{apiKey:l.aibot.apiKey,wsUrl:l.aibot.url}],()=>this.instances.some(r=>r.getStatus().busy)),await this.upgradeChecker.start()}}async stop(){d.info("manager","Stopping all agents..."),this.stopping=!0,this.upgradeChecker?.stop(),await Promise.allSettled([...this.shareSyncChains.values()]),this.shareSyncChains.clear();const e=[...this.sharedInstances.values()];this.sharedInstances.clear(),await Promise.allSettled([...this.instances.map(t=>t.stop()),...e.map(t=>t.stop())]),await this.globalConfigStore?.flush(),this.instances=[],D(),d.info("manager","All stopped")}onShareSet(e,t){const a=(this.shareSyncChains.get(e.name)??Promise.resolve()).catch(()=>{}).then(()=>this.syncSharedInstances(e,t));this.shareSyncChains.set(e.name,a)}async syncSharedInstances(e,t){if(this.stopping)return;const{toAdd:o,toRemove:a}=R(e.name,this.sharedInstances.keys(),t);for(const s of o){if(this.stopping)break;const i=z(e.name,s);try{const l=L(e,s),r=new A(l,this.globalConfigStore);this.sharedInstances.set(i,r),await r.start(),d.info("manager",`shared instance started: ${i}`)}catch(l){this.sharedInstances.delete(i),d.error("manager",`start shared instance failed ${i}: ${l}`)}}for(const{key:s}of a){const i=this.sharedInstances.get(s);i&&(this.sharedInstances.delete(s),i.stop().catch(l=>d.error("manager",`stop shared instance failed ${s}: ${l}`)),d.info("manager",`shared instance stopped: ${s}`))}}getAgentsStatus(){return this.instances.map(e=>e.getStatus())}async addAgent(e){const t=P(e);if(this.instances.some(a=>a.name===t.name))throw new Error(`Agent "${t.name}" already exists`);const o=new A(t,this.globalConfigStore);o.setUpgradeTrigger(()=>this.upgradeChecker?.triggerCheck()),o.setShareSetHandler(a=>this.onShareSet(t,a)),await o.start(),this.instances.push(o),this.configMap.set(t.name,t),this.persistAgentsConfig(),d.info("manager",`Added agent: ${t.name}`)}async removeAgent(e){const t=this.instances.findIndex(i=>i.name===e);if(t===-1)throw Object.assign(new Error(`Agent "${e}" not found`),{code:"NOT_FOUND"});const o=this.instances[t];this.instances.splice(t,1),this.configMap.delete(e);const a=this.shareSyncChains.get(e);this.shareSyncChains.delete(e),a&&await a.catch(()=>{});const s=`${e}#shared:`;for(const[i,l]of[...this.sharedInstances.entries()])i.startsWith(s)&&(this.sharedInstances.delete(i),l.stop().catch(r=>d.error("manager",`stop shared instance failed ${i}: ${r}`)));await o.stop(),this.persistAgentsConfig(),d.info("manager",`Removed agent: ${e}`)}persistAgentsConfig(){const e=x(this.configDir,"agents.json");try{const t=[];for(const[,a]of this.configMap)t.push({name:a.name,ws_url:a.aibot.url,agent_id:a.aibot.agentId,api_key:a.aibot.apiKey,client_type:a.aibot.clientType});B(e,JSON.stringify({agents:t},null,4)+`
`,"utf-8")}catch(t){d.error("manager",`Failed to persist agents config: ${t}`)}}async restartAgent(e){const t=this.instances.findIndex(i=>i.name===e);if(t===-1)throw Object.assign(new Error(`Agent "${e}" not found`),{code:"NOT_FOUND"});const o=this.configMap.get(e);if(!o)throw Object.assign(new Error(`Config for "${e}" not found`),{code:"NOT_FOUND"});await this.instances[t].stop();const s=new A(o,this.globalConfigStore);s.setUpgradeTrigger(()=>this.upgradeChecker?.triggerCheck()),await s.start(),this.instances[t]=s,d.info("manager",`Restarted agent: ${e}`)}async checkUpgrade(){return this.upgradeChecker?this.upgradeChecker.checkForUpdate():{available:!1}}triggerUpgrade(){this.upgradeChecker?.triggerCheck()}async probeAll(e={}){return pe(this.instances,e)}async probeOne(e,t={}){return me(this.instances,e,t)}listInstallable(){return this.installer.listInstallable()}async installAgent(e){const t=await this.installer.install(e);return ne(t),t}getInstallProgress(e){return this.installer.getProgress(e)??null}}async function pe(n,e){const t=e.concurrency??4,o=Date.now(),a=new Array(n.length);await new Promise(u=>{let f=0,m=0;const c=n.length;if(c===0){u();return}function p(h){const w=n[h];w.probe(e).then(y=>{a[h]=y,g()},y=>{a[h]={agent_name:w.name,client_type:"unknown",adapter_type:"acp",ok:!1,status:"error",probed_at:Date.now(),duration_ms:0,cached:!1,cli:{command:"",installed:!1,path:null,version:null,error:{code:"internal",message:y?.message??String(y)}},conversation:{attempted:!1,ok:!1,latency_ms:null},config:{model:null,base_url:null,source:{model:"unknown",base_url:"unknown"}},process:{started:!1,alive:!1,busy:!1}},g()})}function g(){m++,f<c?p(f++):m===c&&u()}const _=Math.min(t,c);for(let h=0;h<_;h++)p(f++)});const s=a.filter(u=>u.status==="healthy").length,i=a.filter(u=>u.status==="degraded").length,l=a.filter(u=>u.status==="unavailable").length,r=await se();return{ok:s===a.length&&a.length>0,total:a.length,healthy:s,degraded:i,unavailable:l,installed_clients:r,agents:a,probed_at:o,duration_ms:Date.now()-o}}async function me(n,e,t){const o=n.find(a=>a.name===e);if(!o)throw Object.assign(new Error(`Agent "${e}" not found`),{code:"NOT_FOUND"});return o.probe(t)}export{$e as Manager,se as probeInstalledClientCommands,pe as probeInstances,me as probeOneInstance};