grix-connector 2.2.2 → 2.2.4

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
@@ -6,6 +6,10 @@ A command-line daemon that connects your local AI coding agents to the [Grix](ht
6
6
 
7
7
  Grix is an AI Agent scheduling platform. It lets you manage and interact with multiple AI coding agents through a unified chat interface. Register at [grix.im](https://grix.im) to get started.
8
8
 
9
+ ## Get the Client
10
+
11
+ After installing grix-connector, download the Grix client from [grix.im](https://grix.im) to chat with your agents. Clients are available for iOS, Android, macOS, Windows, and Linux.
12
+
9
13
  ## Supported Agents
10
14
 
11
15
  Set `client_type` in your config to one of the values below. Each `client_type` maps to a built-in adapter and CLI command — you only need the corresponding CLI installed locally.
@@ -8,7 +8,7 @@ Error: ${s}`,0,!1)},sendUpdateBindingCard:(o,i,s)=>this.aibotHandle.sendUpdateBi
8
8
 
9
9
  Error: ${s}`,0,!1)},sendUpdateBindingCard:(o,i,s)=>this.aibotHandle.sendUpdateBindingCard({session_id:o,worker_status:i,cwd:s}),agentInvoke:async(o,i)=>this.platformInvoke(o,i),sendLocalActionResult:(o,i,s,r,a)=>{this.aibotHandle.sendLocalActionResult({action_id:o,status:i,...s!==void 0?{result:s}:{},...r?{error_code:r}:{},...a?{error_msg:a}:{}})}},t=this.config.adapterOptions??{};return new ee({command:this.config.agent.command,args:this.config.agent.args,env:this.config.agent.env,options:t},n,{port:t.port,hostname:t.hostname,model:t.model,agent:t.agent,permissionPolicy:t.permission_policy,enableSessionBinding:!0,aibotSessionId:e,bindingStore:this.bindingStore})}createAgyAdapter(e){const n={sendStreamChunk:(o,i,s,r,a)=>{this.sendStreamChunkByRuntimeConfig(o,i,s,r,a)},sendEventResult:(o,i,s)=>{this.sendEventResultWithCleanup(o,i,s)},sendEventAck:(o,i)=>{this.aibotHandle.sendEventAck({event_id:o,session_id:i,received_at:Date.now()})},agentInvoke:async(o,i,s)=>this.platformInvoke(o,i,s),forceCompleteInternalEvent:(o,i)=>{this.pool.eventComplete(o,i),this.pushQueueSnapshotForSession(i)},persistConversationId:(o,i)=>{this.bindingStore.setAgyConversationId(o,i)}},t=o=>{const i=this.bindingStore.get(o);return{cwd:i?.cwd,modelId:i?.modelId??this.globalConfigStore?.get(this.name)?.modelId,conversationId:i?.agyConversationId}};return new U({command:this.config.agent.command,args:this.config.agent.args,env:this.config.agent.env,options:this.config.adapterOptions??{}},n,t)}createAcpAdapter(e){const n=this.isAcpRawTransportEnabled(),t={sendStreamChunk:(a,d,c,l,h,m,v)=>{this.finalizeThinking(a,d),this.sendStreamChunkByRuntimeConfig(a,d,c,l,h,m,v)},sendFinalStreamChunkReliable:async(a,d,c,l)=>{if(this.finalizeThinking(a,d),this.resolveEventRuntimeConfig(a).responseDelivery==="single_message"&&a){this.bufferStreamChunk(a,d,"",!0);return}try{await this.aibotHandle.sendStreamChunkRequest({event_id:a,session_id:d,delta_content:"",chunk_seq:c,is_finish:!0,...l?{client_msg_id:l}:{}})}catch(m){u.warn("bridge",`[acp] sendFinalStreamChunkReliable ACK failed event=${a}: ${m}`)}u.info("bridge",`[acp] sendFinalStreamChunkReliable done event=${a} seq=${c}`)},sendEventResult:(a,d,c)=>{this.sendEventResultWithCleanup(a,d,c)},sendEventAck:(a,d)=>{this.aibotHandle.sendEventAck({event_id:a,session_id:d,received_at:Date.now()})},agentInvoke:async(a,d)=>this.platformInvoke(a,d),sendLocalActionResult:(a,d,c,l,h)=>{this.aibotHandle.sendLocalActionResult({action_id:a,status:d,...c!==void 0?{result:c}:{},...l?{error_code:l}:{},...h?{error_msg:h}:{}})},sendRawEventEnvelope:(a,d,c)=>{this.sendAcpRawEventEnvelope(a,d,c)},sendToolUse:(a,d,c,l)=>{if(n){this.sendAcpRawEventEnvelope(a,d,{type:"tool_use",payload:{tool_name:c,tool_input:l??""}});return}this.sendToolExecutionCard(a,d,E(c,l))},sendToolResult:(a,d,c,l)=>{this.sendToolExecutionCard(a,d,$(c,l))},sendThinking:(a,d,c)=>{this.sendThinkingByRuntimeConfig(a,d,c)},sendRunError:(a,d,c)=>{this.sendStreamChunkByRuntimeConfig(a,d,`
10
10
 
11
- Error: ${c}`,1,!1)},sendPermissionCard:a=>{if(n){this.sendAcpRawEventEnvelope(a.eventId,a.sessionId,{type:"permission_request",payload:{tool_call_id:a.toolCallId,tool_name:a.toolName,tool_title:a.toolTitle,options:a.options}});return}this.aibotHandle.sendMsg({event_id:a.eventId,session_id:a.sessionId,client_msg_id:`perm_${F()}`,msg_type:1,content:a.toolTitle?`Permission required: ${a.toolTitle}`:"Permission request",extra:{channel_data:{execApproval:{approvalId:a.toolCallId,approvalSlug:a.toolName},grix:{execApproval:{approval_command_id:a.toolCallId,command:a.toolTitle||a.toolName,host:"acp"}}},agent_api_origin:!0}})},sendAuthNotification:(a,d)=>{a&&this.aibotHandle.sendMsg({session_id:a,msg_type:1,content:d,extra:{biz_card:{version:1,type:"agent_error",payload:{error:{name:"AuthRequired",message:d}}}}})},sendUpdateBindingCard:(a,d,c,l)=>{const h={...l??{}};if(!h.rate_limits&&this.cachedProviderQuota?.success){this.isRateLimitsCacheFresh(this.cachedProviderQuotaSampledAtMs)||this.maybeQueryProviderQuota().catch(()=>{});const m=this.providerQuotaToRateLimits(this.cachedProviderQuota);m&&(h.rate_limits=m)}!h.provider_quota&&this.cachedProviderQuota?.success&&(h.provider_quota=this.cachedProviderQuota),this.aibotHandle.sendUpdateBindingCard({session_id:a,worker_status:d,cwd:c,...Object.keys(h).length>0?{meta:h}:{}})},onSkillsUpdate:a=>{try{this.aibotHandle.sendSkillsUpdate({skills:a})}catch(d){}},onContextWindowUpdated:a=>{this.cachedAcpContextWindow=a,this.cachedAcpContextWindowSampledAtMs=Date.now();const d="usedPercentage"in a?a.usedPercentage.toFixed(1):(a.used/a.size*100).toFixed(1);u.info(this.name,`[acp] context_window updated: ${d}%`);const c=this.bindingStore.get(e);if(c?.cwd){const l={context_window:"usedPercentage"in a?{usedPercentage:a.usedPercentage,remainingPercentage:100-a.usedPercentage}:a};if(this.config.aibot.clientType==="kiro"&&this.cachedProviderQuota?.success){this.isRateLimitsCacheFresh(this.cachedProviderQuotaSampledAtMs)||this.maybeQueryProviderQuota().catch(()=>{}),l.provider_quota=this.cachedProviderQuota;const h=this.providerQuotaToRateLimits(this.cachedProviderQuota);h&&(l.rate_limits=h)}this.aibotHandle.sendUpdateBindingCard({session_id:e,worker_status:"ready",cwd:c.cwd,meta:l})}},sendMcpFrame:a=>{this.aibotHandle.sendMcpFrame(e,a)}},o=e?this.bindingStore.get(e):void 0,i=this.globalConfigStore?.get(this.name),{initialModel:s,initialMode:r}=Ae({sessionBinding:o,globalDefaults:i,configInitialMode:this.config.acpInitialMode});return(s||r)&&u.info(this.name,`[toolbar] hydrate from binding: session=${e} model=${s??"<none>"} mode=${r??"<none>"}`),new A({command:this.config.agent.command,args:this.config.agent.args,env:this.config.agent.env},t,{acpAuthMethod:this.config.acpAuthMethod,acpInitialMode:r,acpInitialModel:s,acpMcpTools:this.config.acpMcpTools,rawTransport:n,eventResultsPath:this.config.eventResultsPath,approvalMode:this.config.approvalMode,bindingStore:this.config.enableSessionBinding?this.bindingStore:void 0,aibotSessionId:e,autoInjectArgs:this.config.autoInjectArgs,bridgeLog:this.config.logDir?new _e(this.config.logDir,`${this.name}-${e}`):null})}async connectAibot(){const e=new J;if(this.aibotHandle=await e.connect(this.aibotConfig,{aborted:()=>this.stopped,label:this.name,packetLog:this.packetLog}),this.aibotHandle.onEvent(n=>{this.handleAibotEvent(n).catch(t=>{u.error(this.name,`handleAibotEvent failed: ${t}`),this.aibotHandle.sendEventAck({event_id:n.event_id,session_id:n.session_id,received_at:Date.now()});const o=t instanceof Error?t.message:String(t);if(/CWD must be|Bound directory does not exist|Bound path is not a directory/i.test(o)&&n.session_id){this.bindingStore.delete(n.session_id),this.sessionBindings.delete(n.session_id);const s=this.pool.getSlot(n.session_id);s?.adapter instanceof A&&s.adapter.getSessionBindings().delete(n.session_id);const r=this.config.adapterType??"acp",a=this.resolveBindingChannelKey(r);this.aibotHandle.sendMsg({event_id:n.event_id,session_id:n.session_id,msg_type:1,content:o,extra:{channel_data:{[a]:{sessionBinding:{status:"missing",reason:"binding_stale",error_code:g.invalidCwd}}}},quoted_message_id:n.msg_id});return}this.aibotHandle.sendEventResult({event_id:n.event_id,status:"failed",msg:o,updated_at:Date.now()})})}),this.aibotHandle.onStop(n=>{try{this.handleAibotStop(n)}catch(t){u.error(this.name,`handleAibotStop failed: ${t}`)}}),this.aibotHandle.onShareSet(n=>{try{this.shareSetHandler?.(Array.isArray(n?.shared_to)?n.shared_to:[])}catch(t){u.error(this.name,`onShareSet failed: ${t}`)}}),this.aibotHandle.onRevoke(n=>{try{this.handleAibotRevoke(n)}catch(t){u.error(this.name,`handleAibotRevoke failed: ${t}`)}}),this.aibotHandle.onLocalAction(n=>{this.handleAibotLocalAction(n).catch(t=>{u.error(this.name,`handleAibotLocalAction failed: ${t}`)})}),this.aibotHandle.onEventCancel(n=>{u.info(this.name,`recv event_cancel event_id=${n.event_id} session_id=${n.session_id}`),this.handleEventCancel(n).catch(t=>{u.error(this.name,`handleEventCancel failed: ${t}`)})}),this.aibotHandle.onMcpFrame((n,t)=>{const o=this.pool.getSlot(n)?.adapter;o?.deliverMcpFrameToAgent?o.deliverMcpFrameToAgent(t):u.warn(this.name,`mcp_frame: no adapter for session=${n}`)}),this.aibotHandle.onQueueClear(n=>{const t=this.pool.clearQueue(n.session_id);this.aibotHandle.sendQueueClearResult({session_id:n.session_id,canceled_event_ids:t}),this.pushQueueSnapshotForSession(n.session_id)}),this.aibotHandle.onQueueSnapshotQuery(n=>{this.replyQueueSnapshotForSession(n.session_id)}),u.info(this.name,"Connected to aibot"),this.activeEventStore){const n=await this.activeEventStore.drain();if(n.length>0){u.warn(this.name,`Recovering ${n.length} stale event(s) from previous run`);for(const t of n)u.info(this.name,`Failing stale event on startup: ${t}`),this.aibotHandle.sendEventResult({event_id:t,status:"failed",msg:"process restarted, event lost",updated_at:Date.now()})}}this.pushQueueSnapshots(),this.aibotHandle.onReconnected(()=>{this.pushQueueSnapshots()}),this.aibotHandle.onStreamRejected((n,t)=>{this.sendCtrl.markEventRejected(n)})}pushQueueSnapshots(){if(!(!this.config.eventQueue||!this.pool))for(const e of this.pool.getAllSlots())this.pushQueueSnapshotForSession(e.sessionId)}buildQueueSnapshotPayload(e){const n=this.pool?.getQueueSnapshot(e)??null,t=n?[...n.running]:[],o=n?n.running_items.map(s=>({event_id:s.event_id,...s.content_preview?{content_preview:s.content_preview}:{},...s.title?{title:s.title}:{},...s.summary?{summary:s.summary}:{},actions:[{type:"stop"}]})):[],i=n?n.queued.map(s=>({event_id:s.event_id,position:s.position,...s.content_preview?{content_preview:s.content_preview}:{},...s.title?{title:s.title}:{},...s.summary?{summary:s.summary}:{},actions:[{type:"cancel"}]})):[];if(t.length===0&&this.selfDrivenSessions.has(e)&&this.pool?.getSlot(e)){const s=`selfdrive_${e}`,r="Background task in progress";t.push(s),o.push({event_id:s,content_preview:r,title:r,summary:r,actions:[]})}return{session_id:e,running:t,running_items:o,queued:i}}pushQueueSnapshotForSession(e){if(!this.config.eventQueue||!this.pool)return;const n=this.buildQueueSnapshotPayload(e);u.info(this.name,`[queue-debug] push snapshot session=${e} running=${n.running.length} queued=${n.queued.length} running_ids=[${n.running.join(",")}]`),this.aibotHandle.sendQueueSnapshot(n)}replyQueueSnapshotForSession(e){!this.config.eventQueue||!this.pool||this.aibotHandle.sendQueueSnapshot(this.buildQueueSnapshotPayload(e))}async platformInvoke(e,n,t){return e==="file_link"?$e(n):this.aibotHandle.agentInvoke(e,n,t)}sendReplyByRuntimeConfig(e,n,t,o,i){this.indexEventSession(e,n),this.sendCtrl.sendReply(e,n,t,o,i),t&&this.conversationLog?.logOutbound?.(n,e,"reply",t)}sendStreamChunkByRuntimeConfig(e,n,t,o,i,s,r){this.indexEventSession(e,n),this.sendCtrl.sendStreamChunk(e,n,t,o,i,s,r),(t||i)&&this.conversationLog?.logOutbound?.(n,e,i?"stream_chunk_finish":"stream_chunk",t)}sendRunErrorAsChunk(e,n,t){this.sendStreamChunkByRuntimeConfig(e,n,`
11
+ Error: ${c}`,1,!1)},sendPermissionCard:a=>{if(n){this.sendAcpRawEventEnvelope(a.eventId,a.sessionId,{type:"permission_request",payload:{tool_call_id:a.toolCallId,tool_name:a.toolName,tool_title:a.toolTitle,options:a.options}});return}this.aibotHandle.sendMsg({event_id:a.eventId,session_id:a.sessionId,client_msg_id:`perm_${F()}`,msg_type:1,content:a.toolTitle?`Permission required: ${a.toolTitle}`:"Permission request",extra:{channel_data:{execApproval:{approvalId:a.toolCallId,approvalSlug:a.toolName},grix:{execApproval:{approval_command_id:a.toolCallId,command:a.toolTitle||a.toolName,host:"acp"}}},agent_api_origin:!0}})},sendAuthNotification:(a,d)=>{a&&this.aibotHandle.sendMsg({session_id:a,msg_type:1,content:d,extra:{biz_card:{version:1,type:"agent_error",payload:{error:{name:"AuthRequired",message:d}}}}})},sendUpdateBindingCard:(a,d,c,l)=>{const h={...l??{}};if(!h.rate_limits&&this.cachedProviderQuota?.success){this.isRateLimitsCacheFresh(this.cachedProviderQuotaSampledAtMs)||this.maybeQueryProviderQuota().catch(()=>{});const m=this.providerQuotaToRateLimits(this.cachedProviderQuota);m&&(h.rate_limits=m)}!h.provider_quota&&this.cachedProviderQuota?.success&&(h.provider_quota=this.cachedProviderQuota),this.aibotHandle.sendUpdateBindingCard({session_id:a,worker_status:d,cwd:c,...Object.keys(h).length>0?{meta:h}:{}})},onSkillsUpdate:a=>{try{this.aibotHandle.sendSkillsUpdate({skills:a})}catch(d){}},onContextWindowUpdated:a=>{this.cachedAcpContextWindow=a,this.cachedAcpContextWindowSampledAtMs=Date.now();const d="usedPercentage"in a?a.usedPercentage.toFixed(1):(a.used/a.size*100).toFixed(1);u.info(this.name,`[acp] context_window updated: ${d}%`);const c=this.bindingStore.get(e);if(c?.cwd){const l={context_window:"usedPercentage"in a?{usedPercentage:a.usedPercentage,remainingPercentage:100-a.usedPercentage}:a};if(this.config.aibot.clientType==="kiro"&&this.cachedProviderQuota?.success){this.isRateLimitsCacheFresh(this.cachedProviderQuotaSampledAtMs)||this.maybeQueryProviderQuota().catch(()=>{}),l.provider_quota=this.cachedProviderQuota;const h=this.providerQuotaToRateLimits(this.cachedProviderQuota);h&&(l.rate_limits=h)}this.aibotHandle.sendUpdateBindingCard({session_id:e,worker_status:"ready",cwd:c.cwd,meta:l})}},sendMcpFrame:a=>{this.aibotHandle.sendMcpFrame(e,a)}},o=e?this.bindingStore.get(e):void 0,i=this.globalConfigStore?.get(this.name),{initialModel:s,initialMode:r}=Ae({sessionBinding:o,globalDefaults:i,configInitialMode:this.config.acpInitialMode});return(s||r)&&u.info(this.name,`[toolbar] hydrate from binding: session=${e} model=${s??"<none>"} mode=${r??"<none>"}`),new A({command:this.config.agent.command,args:this.config.agent.args,env:this.config.agent.env},t,{acpAuthMethod:this.config.acpAuthMethod,acpInitialMode:r,acpInitialModel:s,acpMcpTools:this.config.acpMcpTools,rawTransport:n,eventResultsPath:this.config.eventResultsPath,approvalMode:this.config.approvalMode,bindingStore:this.config.enableSessionBinding?this.bindingStore:void 0,aibotSessionId:e,autoInjectArgs:this.config.autoInjectArgs,bridgeLog:this.config.logDir?new _e(this.config.logDir,`${this.name}-${e}`):null})}async connectAibot(){const e=new J;if(this.aibotHandle=await e.connect(this.aibotConfig,{aborted:()=>this.stopped,label:this.name,packetLog:this.packetLog,maxRetries:this.config.connectMaxRetries}),this.aibotHandle.onEvent(n=>{this.handleAibotEvent(n).catch(t=>{u.error(this.name,`handleAibotEvent failed: ${t}`),this.aibotHandle.sendEventAck({event_id:n.event_id,session_id:n.session_id,received_at:Date.now()});const o=t instanceof Error?t.message:String(t);if(/CWD must be|Bound directory does not exist|Bound path is not a directory/i.test(o)&&n.session_id){this.bindingStore.delete(n.session_id),this.sessionBindings.delete(n.session_id);const s=this.pool.getSlot(n.session_id);s?.adapter instanceof A&&s.adapter.getSessionBindings().delete(n.session_id);const r=this.config.adapterType??"acp",a=this.resolveBindingChannelKey(r);this.aibotHandle.sendMsg({event_id:n.event_id,session_id:n.session_id,msg_type:1,content:o,extra:{channel_data:{[a]:{sessionBinding:{status:"missing",reason:"binding_stale",error_code:g.invalidCwd}}}},quoted_message_id:n.msg_id});return}this.aibotHandle.sendEventResult({event_id:n.event_id,status:"failed",msg:o,updated_at:Date.now()})})}),this.aibotHandle.onStop(n=>{try{this.handleAibotStop(n)}catch(t){u.error(this.name,`handleAibotStop failed: ${t}`)}}),this.aibotHandle.onShareSet(n=>{try{this.shareSetHandler?.(Array.isArray(n?.shared_to)?n.shared_to:[])}catch(t){u.error(this.name,`onShareSet failed: ${t}`)}}),this.aibotHandle.onRevoke(n=>{try{this.handleAibotRevoke(n)}catch(t){u.error(this.name,`handleAibotRevoke failed: ${t}`)}}),this.aibotHandle.onLocalAction(n=>{this.handleAibotLocalAction(n).catch(t=>{u.error(this.name,`handleAibotLocalAction failed: ${t}`)})}),this.aibotHandle.onEventCancel(n=>{u.info(this.name,`recv event_cancel event_id=${n.event_id} session_id=${n.session_id}`),this.handleEventCancel(n).catch(t=>{u.error(this.name,`handleEventCancel failed: ${t}`)})}),this.aibotHandle.onMcpFrame((n,t)=>{const o=this.pool.getSlot(n)?.adapter;o?.deliverMcpFrameToAgent?o.deliverMcpFrameToAgent(t):u.warn(this.name,`mcp_frame: no adapter for session=${n}`)}),this.aibotHandle.onQueueClear(n=>{const t=this.pool.clearQueue(n.session_id);this.aibotHandle.sendQueueClearResult({session_id:n.session_id,canceled_event_ids:t}),this.pushQueueSnapshotForSession(n.session_id)}),this.aibotHandle.onQueueSnapshotQuery(n=>{this.replyQueueSnapshotForSession(n.session_id)}),u.info(this.name,"Connected to aibot"),this.activeEventStore){const n=await this.activeEventStore.drain();if(n.length>0){u.warn(this.name,`Recovering ${n.length} stale event(s) from previous run`);for(const t of n)u.info(this.name,`Failing stale event on startup: ${t}`),this.aibotHandle.sendEventResult({event_id:t,status:"failed",msg:"process restarted, event lost",updated_at:Date.now()})}}this.pushQueueSnapshots(),this.aibotHandle.onReconnected(()=>{this.pushQueueSnapshots()}),this.aibotHandle.onStreamRejected((n,t)=>{this.sendCtrl.markEventRejected(n)})}pushQueueSnapshots(){if(!(!this.config.eventQueue||!this.pool))for(const e of this.pool.getAllSlots())this.pushQueueSnapshotForSession(e.sessionId)}buildQueueSnapshotPayload(e){const n=this.pool?.getQueueSnapshot(e)??null,t=n?[...n.running]:[],o=n?n.running_items.map(s=>({event_id:s.event_id,...s.content_preview?{content_preview:s.content_preview}:{},...s.title?{title:s.title}:{},...s.summary?{summary:s.summary}:{},actions:[{type:"stop"}]})):[],i=n?n.queued.map(s=>({event_id:s.event_id,position:s.position,...s.content_preview?{content_preview:s.content_preview}:{},...s.title?{title:s.title}:{},...s.summary?{summary:s.summary}:{},actions:[{type:"cancel"}]})):[];if(t.length===0&&this.selfDrivenSessions.has(e)&&this.pool?.getSlot(e)){const s=`selfdrive_${e}`,r="Background task in progress";t.push(s),o.push({event_id:s,content_preview:r,title:r,summary:r,actions:[]})}return{session_id:e,running:t,running_items:o,queued:i}}pushQueueSnapshotForSession(e){if(!this.config.eventQueue||!this.pool)return;const n=this.buildQueueSnapshotPayload(e);u.info(this.name,`[queue-debug] push snapshot session=${e} running=${n.running.length} queued=${n.queued.length} running_ids=[${n.running.join(",")}]`),this.aibotHandle.sendQueueSnapshot(n)}replyQueueSnapshotForSession(e){!this.config.eventQueue||!this.pool||this.aibotHandle.sendQueueSnapshot(this.buildQueueSnapshotPayload(e))}async platformInvoke(e,n,t){return e==="file_link"?$e(n):this.aibotHandle.agentInvoke(e,n,t)}sendReplyByRuntimeConfig(e,n,t,o,i){this.indexEventSession(e,n),this.sendCtrl.sendReply(e,n,t,o,i),t&&this.conversationLog?.logOutbound?.(n,e,"reply",t)}sendStreamChunkByRuntimeConfig(e,n,t,o,i,s,r){this.indexEventSession(e,n),this.sendCtrl.sendStreamChunk(e,n,t,o,i,s,r),(t||i)&&this.conversationLog?.logOutbound?.(n,e,i?"stream_chunk_finish":"stream_chunk",t)}sendRunErrorAsChunk(e,n,t){this.sendStreamChunkByRuntimeConfig(e,n,`
12
12
 
13
13
  Error: ${t}`,1,!1)}sendEventResultWithCleanup(e,n,t,o){this.sendCtrl.sendEventResult(e,n,t,o);const i=this.eventSessionIndex.get(e);i&&(this.pool.eventComplete(e,i),this.pushQueueSnapshotForSession(i),this.conversationLog?.logResult?.(i,e,n,t),this.eventSessionIndex.delete(e)),this.inflightEvents.delete(e),this.restartCount.delete(e),n==="responded"&&(this.cachedProviderQuotaSampledAtMs=null,this.maybeQueryProviderQuota().catch(()=>{}))}async handleSessionInternalError(e){const{eventId:n,sessionId:t,errorMsg:o}=e;if(this.stopped)return;const i=this.inflightEvents.get(n);if(!i){u.warn(this.name,`[recovery] no inflight event for internalError event=${n} session=${t}; surface failure directly`),this.sendRunErrorAsChunk(n,t,o),this.sendEventResultWithCleanup(n,"failed",o,"agent_stop_failure");return}const s=(this.restartCount.get(n)??0)+1;this.restartCount.set(n,s);const r=this.config.adapterType??"acp";if(s>B){u.error(this.name,`[recovery] adapter=${r} session=${t} event=${n} restart=${s}/${B} outcome=give-up err=${o}`),this.sendRunErrorAsChunk(n,t,o),this.sendEventResultWithCleanup(n,"failed",o,"agent_stop_failure");return}u.info(this.name,`[recovery] adapter=${r} session=${t} event=${n} restart=${s}/${B} outcome=restarting err=${o}`);const a=this.pool.drainQueuedForSession(t);a.length>0&&u.info(this.name,`[recovery] session=${t} preserved ${a.length} queued sibling event(s) across restart`);try{await this.pool.removeSlot(t)}catch(l){u.warn(this.name,`[recovery] removeSlot failed session=${t}: ${l instanceof Error?l.message:String(l)}`)}if(this.stopped)return;const d=this.resolveRecoveryPrompt(r,i),c={...i,content:d};try{await this.pool.deliverInboundEvent(c)}catch(l){u.error(this.name,`[recovery] redeliver failed event=${n} session=${t}: ${l instanceof Error?l.message:String(l)}`),this.sendEventResultWithCleanup(n,"failed",l instanceof Error?l.message:String(l));return}for(const l of a){if(this.stopped)break;try{await this.pool.deliverInboundEvent(l)}catch(h){u.error(this.name,`[recovery] sibling redeliver failed event=${l.event_id} session=${t}: ${h instanceof Error?h.message:String(h)}`),this.sendEventResultWithCleanup(l.event_id,"failed",h instanceof Error?h.message:String(h))}}}resolveRecoveryPrompt(e,n){return e==="acp"?"continue":n.content}sendThinkingByRuntimeConfig(e,n,t){this.sendCtrl.sendThinking(e,n,t)}bufferStreamChunk(e,n,t,o,i){this.sendCtrl.bufferOnly(e,n,t,o,i)}flushBufferedStreamText(e){}resolveEventRuntimeConfig(e){return this.sendCtrl.resolveEventRuntimeConfig(e)}captureEventRuntimeConfig(e){this.sendCtrl.captureEventRuntimeConfig(e),this.indexEventSession(e.event_id,e.session_id),e.event_id&&this.inflightEvents.set(e.event_id,e)}indexEventSession(e,n){!e||!n||this.eventSessionIndex.set(e,n)}shouldDropToolDisplayEvent(e){return this.sendCtrl.shouldDropToolDisplayEvent(e)}shouldDropThinkingDisplayEvent(e){return this.sendCtrl.shouldDropThinkingDisplayEvent(e)}shouldDropCodexDisplayEvent(e,n){return this.sendCtrl.shouldDropCodexDisplayEvent(e,n)}logCodexEventToConversation(e){if(!this.conversationLog||e.codex_method!=="item/agentMessage/delta")return;const t=e.codex_payload?.params?.delta;if(!t)return;const o=this.eventSessionIndex.get(e.event_id)??e.session_id;this.conversationLog.append(o,{ts:Date.now(),dir:"outbound",event_id:e.event_id,kind:"codex_delta",text_len:t.length,content:t})}isAcpRawTransportEnabled(){return(this.config.adapterOptions??{}).raw_transport===!0}shouldDropAcpRawDisplayEvent(e,n){return this.sendCtrl.shouldDropAcpRawDisplayEvent(e,n)}sendAcpRawEventEnvelope(e,n,t){this.shouldDropAcpRawDisplayEvent(e,t.type)||this.aibotHandle.sendMsg({event_id:e,session_id:n,msg_type:1,content:this.buildAcpRawEventFallbackText(t),extra:{channel_data:{acp:{raw_event:t}},agent_api_origin:!0}})}buildAcpRawEventFallbackText(e){const n=String(e.type??"").trim();if(!n)return"[acp] event";switch(n){case"permission_request":return`Permission required: ${String(e.payload?.tool_title??e.payload?.tool_name??"permission request")}`;case"tool_use":return`[tool] ${String(e.payload?.tool_name??"tool")}`;case"tool_result":return"[tool result]";case"thinking":return"[thinking]";case"error":return`[error] ${String(e.payload?.message??"agent error")}`;case"result":return"[result]";default:return`[acp] ${n}`}}sendToolExecutionCard(e,n,t,o){this.sendCtrl.sendToolExecutionCard(e,n,t,o)}async handleAibotEvent(e){if(this.stopped){this.aibotHandle.sendEventAck({event_id:e.event_id,session_id:e.session_id,received_at:Date.now()}),this.aibotHandle.sendEventResult({event_id:e.event_id,status:"failed",msg:"agent shutting down",updated_at:Date.now()});return}this.logInboundConversation(e);const n=this.config.adapterType??"acp";if(this.allowlistGate&&e.sender_id&&!await this.allowlistGate.checkAccess(e.sender_id)){this.aibotHandle.sendEventAck({event_id:e.event_id,session_id:e.session_id,received_at:Date.now()}),this.aibotHandle.sendEventResult({event_id:e.event_id,status:"responded",code:"access_denied",msg:`sender ${e.sender_id} is not authorized`,updated_at:Date.now()});return}const t=Me(e.extra);if(t.error){this.aibotHandle.sendEventAck({event_id:e.event_id,session_id:e.session_id,received_at:Date.now()}),this.aibotHandle.sendEventResult({event_id:e.event_id,status:"failed",code:"connector_config_invalid",msg:t.error,updated_at:Date.now()});return}const o=t.patch,i=we(e);if(i){if(i.verb===p.exec){await this.handleSessionControlCommand(i,e);return}if(i.verb===p.listSessions){await this.handleListSessionsTextCommand(e);return}if(n==="claude"){await this.handleSessionControlCommand(i,e);return}if(n==="codex"&&i.verb===p.open){await this.handleCodexSessionControlOpen(i,e);return}if(n==="pi"&&i.verb===p.open){await this.handlePiSessionControlOpen(i,e);return}if(n==="pi"&&i.verb===p.restart){await this.handlePiSessionControlRestart(e);return}if((n==="openhuman"||n==="opencode")&&i.verb===p.open){await this.handleOpenHumanSessionControlOpen(i,e);return}if(n==="codewhale"&&i.verb===p.open){await this.handleCodeWhaleSessionControlOpen(i,e);return}if(this.aibotHandle.sendEventAck({event_id:e.event_id,session_id:e.session_id,received_at:Date.now()}),i.verb===p.open){const a=i.args.trim();if(!a){this.aibotHandle.sendEventResult({event_id:e.event_id,status:"failed",code:g.cwdRequired,msg:"cwd is required",updated_at:Date.now()});return}try{const d=x.resolve(a);if(!(await M(d)).isDirectory()){this.aibotHandle.sendEventResult({event_id:e.event_id,status:"failed",code:g.invalidCwd,msg:`Path is not a directory: ${d}`,updated_at:Date.now()});return}}catch(d){const c=String(d?.code??""),l=c==="ENOENT"?`Directory does not exist: ${x.resolve(a)}`:c==="EACCES"||c==="EPERM"?"Directory is not accessible":`Invalid path: ${a}`;this.aibotHandle.sendEventResult({event_id:e.event_id,status:"failed",code:g.invalidCwd,msg:l,updated_at:Date.now()});return}}if(i.verb===p.open){const a=this.bindingStore.get(e.session_id);if(a?.cwd)try{await M(a.cwd)}catch{u.info("bridge",`Stale binding detected for session ${e.session_id}: ${a.cwd} no longer exists, clearing`),this.bindingStore.delete(e.session_id),this.sessionBindings.delete(e.session_id);const d=this.pool.getSlot(e.session_id);d?.adapter instanceof A&&d.adapter.getSessionBindings().delete(e.session_id)}}if(await this.handleSessionControlForPool(i,e),n==="acp"&&i.verb===p.stop){const d=this.bindingStore.get(e.session_id)?.cwd??"";await this.pool.removeSlot(e.session_id).catch(()=>{}),this.sessionBindings.delete(e.session_id),this.aibotHandle.sendEventResult({event_id:e.event_id,status:"responded",msg:`Session worker stopped for ${d}`,updated_at:Date.now()});return}if(O(i,e,this.sessionControlCtx(e.session_id),{...this.sessionControlSenders(),sendEventAck:()=>{}}),i.verb===p.open&&(await this.deferredMgr.release(e.session_id,this.deferredCallbacks()),n==="agy")){const a=this.bindingStore.get(e.session_id)?.cwd??"";a&&this.aibotHandle.sendUpdateBindingCard({session_id:e.session_id,worker_status:"ready",cwd:a,meta:this.buildAgyToolbarMeta(e.session_id)})}return}if(e.mirror_mode==="record_only"){this.aibotHandle.sendEventAck({event_id:e.event_id,session_id:e.session_id,received_at:Date.now()}),this.aibotHandle.sendEventResult({event_id:e.event_id,status:"responded",updated_at:Date.now()});return}if(this.isStaleEvent(e)){this.aibotHandle.sendEventAck({event_id:e.event_id,session_id:e.session_id,received_at:Date.now()}),this.aibotHandle.sendEventResult({event_id:e.event_id,status:"failed",code:"event_stale",msg:"event is stale and will not be processed",updated_at:Date.now()});return}if(We.has(n)&&!this.bindingStore.get(e.session_id)?.cwd){const d=n;u.info(this.name,`[${d}] binding missing session_id=${e.session_id} event_id=${e.event_id}`),this.deferredMgr.defer(d,String(e.session_id??"").trim(),this.buildInboundEvent(e,o));const c=this.resolveBindingChannelKey(n);this.aibotHandle.sendEventAck({event_id:e.event_id,session_id:e.session_id,received_at:Date.now()}),this.aibotHandle.sendMsg({event_id:e.event_id,session_id:e.session_id,msg_type:1,content:"Session binding missing.",extra:{channel_data:{[c]:{sessionBinding:{status:"missing",reason:"binding_missing",error_code:g.bindingMissing}}}},quoted_message_id:e.msg_id});return}if((this.config.adapterType??"acp")==="acp"){const d=String(e.content??"").trim().match(/^\/(\S+)\s*(.*)/);if(d){const[,c,l]=d,m=this.pool.getSlot(e.session_id)?.adapter;if(m?.execCommand&&(m.getSupportedCommands?.()??[]).some(_=>_.name===c||_.name===`/${c}`)){this.aibotHandle.sendEventAck({event_id:e.event_id,session_id:e.session_id,received_at:Date.now()});try{const _=await m.execCommand(c,l.trim(),e.session_id);_.status==="options"&&_.data&&this.handleExecCommandOptions(e.session_id,c,_.data),this.aibotHandle.sendEventResult({event_id:e.event_id,status:_.status==="failed"?"failed":"responded",msg:_.message,updated_at:Date.now()})}catch(_){this.aibotHandle.sendEventResult({event_id:e.event_id,status:"failed",msg:_ instanceof Error?_.message:String(_),updated_at:Date.now()})}return}}}const r=this.buildInboundEvent(e,o);try{this.captureEventRuntimeConfig(r),await this.pool.deliverInboundEvent(r)}catch(a){if(this.aibotHandle.sendEventAck({event_id:e.event_id,session_id:e.session_id,received_at:Date.now()}),this.inflightEvents.delete(e.event_id),this.restartCount.delete(e.event_id),this.eventSessionIndex.delete(e.event_id),a instanceof Se)this.aibotHandle.sendEventResult({event_id:e.event_id,status:"failed",msg:a.message,updated_at:Date.now()});else throw a}}async handleCodexSessionControlOpen(e,n){const t=n.session_id,o=e.args.trim(),i=()=>this.aibotHandle.sendEventAck({event_id:n.event_id,session_id:t,received_at:Date.now()}),s=(r,a)=>this.aibotHandle.sendEventResult({event_id:n.event_id,status:r,...a,updated_at:Date.now()});if(!o){i(),s("failed",{msg:"Usage: /grix open <working-directory>",code:g.cwdRequired});return}try{const r=await this.resolveCwdForBinding(o),a=this.bindingStore.get(t);if(a?.cwd){let d=!1;try{if(await this.resolveCwdForBinding(a.cwd)===r){i(),s("responded",{msg:`Session already bound to ${a.cwd}`});return}}catch{d=!0,u.info("bridge",`Stale codex binding for session ${t}: ${a.cwd} no longer exists, allowing rebind`)}if(!d){i(),s("failed",{msg:`Session already bound to ${a.cwd}. Rebinding is not allowed.`,code:g.rebindForbidden});return}}this.bindingStore.set(t,r),this.sessionBindings.set(t,r),this.deferredMgr.sendCodexDeferredReplayComposing(t,this.deferredCallbacks()),await this.bindSessionForPool(t,r),await this.deferredMgr.release(t,this.deferredCallbacks(),{announceComposing:!1}),this.aibotHandle.sendUpdateBindingCard({session_id:t,worker_status:"ready",cwd:r}),i(),s("responded",{msg:`Session bound to ${r}`})}catch(r){i(),s("failed",{code:g.invalidCwd,msg:r instanceof Error?r.message:String(r)})}}async handleSessionControlForPool(e,n){e.verb===p.open&&await this.bindSessionForPool(n.session_id,e.args.trim())}async replayDeferredEventsForSession(e){await this.deferredMgr.release(e,this.deferredCallbacks())}async handleSessionControlLocalActionForPool(e){if(String(e.action_type??"")!==S.sessionControl)return;const n=e.params??{};if(String(n.verb??"").trim().toLowerCase()!==p.open)return;const o=String(n.session_id??"").trim(),i=String(n.cwd??"").trim();!o||!i||await this.bindSessionForPool(o,i)}async handleCodexSessionControlLocalActionOpen(e){const n=e.params??{},t=String(n.session_id??"").trim(),o=String(n.cwd??"").trim(),i=String(n.agent_session_id??"").trim(),s=(r,a,d,c)=>{this.aibotHandle.sendLocalActionResult({action_id:e.action_id,status:r,...a!==void 0?{result:a}:{},...d?{error_code:d}:{},...c?{error_msg:c}:{}})};if(!t||!o){s("failed",void 0,g.cwdRequired,"session cwd is required");return}try{const r=await this.resolveCwdForBinding(o);this.ensureImportedAgentSession(i,r);const a=this.bindingStore.get(t);if(a?.cwd){let d=!1;try{const c=await this.resolveCwdForBinding(a.cwd);if(c===r){this.setResolvedAgentSessionId(t,i),s("ok",{outcome:"opened",binding:this.buildOpenedBindingResult(t,c)});return}}catch{d=!0,u.info("bridge",`Stale codex binding for session ${t}: ${a.cwd} no longer exists, allowing rebind`)}if(!d){s("failed",void 0,g.rebindForbidden,`Session already bound to ${a.cwd}`);return}}this.bindingStore.set(t,r),this.setResolvedAgentSessionId(t,i),this.sessionBindings.set(t,r),this.deferredMgr.sendCodexDeferredReplayComposing(t,this.deferredCallbacks()),await this.bindSessionForPool(t,r),await this.deferredMgr.release(t,this.deferredCallbacks(),{announceComposing:!1}),s("ok",{outcome:"opened",binding:this.buildOpenedBindingResult(t,r)})}catch(r){s("failed",void 0,r?.sessionControlErrorCode??g.invalidCwd,r instanceof Error?r.message:String(r))}}async handleCursorSessionControlLocalActionOpen(e){const n=e.params??{},t=String(n.session_id??"").trim(),o=String(n.cwd??"").trim(),i=String(n.agent_session_id??"").trim(),s=(r,a,d,c)=>{this.aibotHandle.sendLocalActionResult({action_id:e.action_id,status:r,...a!==void 0?{result:a}:{},...d?{error_code:d}:{},...c?{error_msg:c}:{}})};if(!t||!o){s("failed",void 0,g.cwdRequired,"session cwd is required");return}try{const r=await this.resolveCwdForBinding(o);this.ensureImportedAgentSession(i,r);const a=this.bindingStore.get(t);if(a?.cwd){let d=!1;try{const c=await this.resolveCwdForBinding(a.cwd);if(c===r){this.setResolvedAgentSessionId(t,i),s("ok",{outcome:"opened",binding:this.buildOpenedBindingResult(t,c)});return}}catch{d=!0,u.info("bridge",`Stale cursor binding for session ${t}: ${a.cwd} no longer exists, allowing rebind`)}if(!d){s("failed",void 0,g.rebindForbidden,`Session already bound to ${a.cwd}`);return}}this.bindingStore.set(t,r),this.setResolvedAgentSessionId(t,i),this.sessionBindings.set(t,r),this.deferredMgr.sendCursorDeferredReplayComposing(t,this.deferredCallbacks()),await this.bindSessionForPool(t,r),await this.deferredMgr.release(t,this.deferredCallbacks(),{announceComposing:!1}),this.aibotHandle.sendUpdateBindingCard({session_id:t,worker_status:"ready",cwd:r}),s("ok",{outcome:"opened",binding:this.buildOpenedBindingResult(t,r)})}catch(r){s("failed",void 0,r?.sessionControlErrorCode??g.invalidCwd,r instanceof Error?r.message:String(r))}}async handlePiSessionControlOpen(e,n){const t=n.session_id,o=e.args.trim(),i=()=>this.aibotHandle.sendEventAck({event_id:n.event_id,session_id:t,received_at:Date.now()}),s=(r,a)=>this.aibotHandle.sendEventResult({event_id:n.event_id,status:r,...a,updated_at:Date.now()});if(!o){i(),s("failed",{msg:"Usage: /grix open <working-directory>",code:g.cwdRequired});return}try{const r=await this.resolveCwdForBinding(o),a=this.bindingStore.get(t);if(a?.cwd){let d=!1;try{if(await this.resolveCwdForBinding(a.cwd)===r){i(),s("responded",{msg:`Session already bound to ${a.cwd}`}),this.aibotHandle.sendMsg({event_id:n.event_id,session_id:t,msg_type:1,content:`\u2705 Session already bound to \`${a.cwd}\``,quoted_message_id:n.msg_id});return}}catch{d=!0,u.info("bridge",`Stale pi binding for session ${t}: ${a.cwd} no longer exists, allowing rebind`)}if(!d){i(),s("failed",{msg:`Session already bound to ${a.cwd}. Rebinding is not allowed.`,code:g.rebindForbidden});return}}this.bindingStore.set(t,r),this.sessionBindings.set(t,r),await this.deferredMgr.release(t,this.deferredCallbacks()),this.aibotHandle.sendUpdateBindingCard({session_id:t,worker_status:"ready",cwd:r}),i(),s("responded",{msg:`Session bound to ${r}`}),this.aibotHandle.sendMsg({event_id:n.event_id,session_id:t,msg_type:1,content:`\u2705 Working directory bound: \`${r}\``,quoted_message_id:n.msg_id})}catch(r){i(),s("failed",{code:g.invalidCwd,msg:r instanceof Error?r.message:String(r)})}}async handlePiSessionControlRestart(e){const n=e.session_id,t=()=>this.aibotHandle.sendEventAck({event_id:e.event_id,session_id:n,received_at:Date.now()}),o=(r,a)=>this.aibotHandle.sendEventResult({event_id:e.event_id,status:r,...a,updated_at:Date.now()}),s=this.bindingStore.get(n)?.cwd??"";if(!s){t(),o("failed",{msg:"session binding was not found",code:g.bindingMissing});return}t(),await this.pool.removeSlot(n).catch(()=>{}),this.aibotHandle.sendUpdateBindingCard({session_id:n,worker_status:"ready",cwd:s}),o("responded",{msg:`Session worker restarted for ${s}`})}async handlePiSessionControlRestartLocalAction(e){const n=e.params??{},t=String(n.session_id??"").trim(),i=(t?this.bindingStore.get(t):void 0)?.cwd??"",s=(r,a,d,c)=>{this.aibotHandle.sendLocalActionResult({action_id:e.action_id,status:r,...a!==void 0?{result:a}:{},...d?{error_code:d}:{},...c?{error_msg:c}:{}})};if(!t){s("failed",void 0,"session_id_required","session_id is required for restart");return}if(!i){s("failed",void 0,g.bindingMissing,"Session binding missing. Open a workspace first.");return}await this.pool.removeSlot(t).catch(()=>{}),this.aibotHandle.sendUpdateBindingCard({session_id:t,worker_status:"ready",cwd:i}),s("ok",{outcome:"restarted",binding:{aibotSessionId:t,cwd:i,workerStatus:"ready"}})}async bindSessionForPool(e,n){const t=String(n??"").trim();if(!e||!t)return;const o=this.pool.getOrCreateSlot(e);if(!o||(o.startPromise&&await o.startPromise,!o.adapter))return;const i=o.adapter instanceof A?o.adapter:null;if(!i?.hasSessionBinding)return;const s=await this.resolveCwdForBinding(t);i.announceDeferredComposing(e),await i.bindSession(e,s),this.sessionBindings.set(e,s),this.sessionScanCache.invalidate(),i.replayDeferredEvents(e)}deferredCallbacks(){return{captureEventRuntimeConfig:e=>this.captureEventRuntimeConfig(e),deliverInboundEvent:e=>this.pool.deliverInboundEvent(e),sendEventResult:(e,n,t)=>this.sendEventResultWithCleanup(e,n,t),sendSessionComposing:(e,n,t)=>{const o={};n&&(o.ttl_ms=t?.ttlMs??3e4,t?.activity&&(o.activity=t.activity)),this.aibotHandle.sendSessionActivitySet({session_id:e,kind:"composing",active:n,...o})}}}async handleOpenHumanSessionControlOpen(e,n){const t=()=>this.aibotHandle.sendEventAck({event_id:n.event_id,session_id:n.session_id,received_at:Date.now()});try{await this.resolveCwdForBinding(e.args.trim()),await this.handleSessionControlForPool(e,n),O(e,n,this.sessionControlCtx(n.session_id),this.sessionControlSenders()),await this.deferredMgr.release(n.session_id,this.deferredCallbacks())}catch(o){t(),this.aibotHandle.sendEventResult({event_id:n.event_id,status:"failed",code:o?.cwdErrorCode,msg:o instanceof Error?o.message:String(o),updated_at:Date.now()})}}async handleCodeWhaleSessionControlOpen(e,n){const t=n.session_id,o=e.args.trim(),i=()=>this.aibotHandle.sendEventAck({event_id:n.event_id,session_id:t,received_at:Date.now()}),s=(r,a)=>this.aibotHandle.sendEventResult({event_id:n.event_id,status:r,...a,updated_at:Date.now()});if(!o){i(),s("failed",{msg:"Usage: /grix open <working-directory>",code:g.cwdRequired});return}try{const r=await this.resolveCwdForBinding(o),a=this.bindingStore.get(t);if(a?.cwd){if(await this.resolveCwdForBinding(a.cwd)!==r){i(),s("failed",{msg:`Session already bound to ${a.cwd}. Rebinding is not allowed.`,code:g.rebindForbidden});return}i(),s("responded",{msg:`Session already bound to ${a.cwd}`});return}this.bindingStore.set(t,r),this.sessionBindings.set(t,r),await this.bindSessionForPool(t,r),await this.deferredMgr.release(t,this.deferredCallbacks(),{announceComposing:!1}),this.aibotHandle.sendUpdateBindingCard({session_id:t,worker_status:"ready",cwd:r}),i(),s("responded",{msg:`Session bound to ${r}`})}catch(r){i(),s("failed",{code:g.invalidCwd,msg:r instanceof Error?r.message:String(r)})}}async handleCodeWhaleSessionControlLocalActionOpen(e){const n=e.params??{},t=String(n.session_id??"").trim(),o=String(n.cwd??"").trim(),i=String(n.agent_session_id??"").trim(),s=(r,a,d,c)=>{this.aibotHandle.sendLocalActionResult({action_id:e.action_id,status:r,...a!==void 0?{result:a}:{},...d?{error_code:d}:{},...c?{error_msg:c}:{}})};if(!t||!o){s("failed",void 0,g.cwdRequired,"session cwd is required");return}try{const r=await this.resolveCwdForBinding(o);this.ensureImportedAgentSession(i,r);const a=this.bindingStore.get(t);if(a?.cwd){const d=await this.resolveCwdForBinding(a.cwd);if(d!==r){s("failed",void 0,g.rebindForbidden,`Session already bound to ${a.cwd}`);return}this.setResolvedAgentSessionId(t,i),s("ok",{outcome:"opened",binding:this.buildOpenedBindingResult(t,d)});return}this.bindingStore.set(t,r),this.setResolvedAgentSessionId(t,i),this.sessionBindings.set(t,r),await this.bindSessionForPool(t,r),await this.deferredMgr.release(t,this.deferredCallbacks(),{announceComposing:!1}),this.aibotHandle.sendUpdateBindingCard({session_id:t,worker_status:"ready",cwd:r}),s("ok",{outcome:"opened",binding:this.buildOpenedBindingResult(t,r)})}catch(r){s("failed",void 0,r?.sessionControlErrorCode??g.invalidCwd,r instanceof Error?r.message:String(r))}}normalizeClaudeModeId(e){return String(e??"").trim().toLowerCase()===C.approval?C.approval:C.fullAuto}handleExecCommandOptions(e,n,t){const o=Array.isArray(t?.options)?t.options:[];if(o.length===0)return;const s=this.bindingStore.get(e)?.cwd??"",r={};if(n==="model"){r.available_models=o.map(d=>({id:d.id,displayName:d.label}));const a=o.find(d=>d.current);a&&(r.model_id=a.id)}else if(n==="mode"){r.available_modes=o.map(d=>({id:d.id,displayName:d.label}));const a=o.find(d=>d.current);a&&(r.mode_id=a.id)}else r[`${n}_options`]=o;this.aibotHandle.sendUpdateBindingCard({session_id:e,worker_status:"ready",cwd:s,meta:r})}buildAgyToolbarMeta(e){const n=this.bindingStore.get(e);if(!n)return;const t=te(this.config.agent.command),o=t.length>0?t[0].id:"";let i=n.modelId||this.globalConfigStore?.get(this.name)?.modelId||o;t.length>0&&!t.some(r=>r.id===i)&&(i=o);const s=ie();return{model_id:i,currentModelId:i,available_models:t.map(r=>({id:r.id,displayName:r.displayName})),...s.plan!==void 0&&{plan:s.plan},...s.quota_exhausted!==void 0&&{quota_exhausted:s.quota_exhausted},...s.quota_reset_at!==void 0&&{quota_reset_at:s.quota_reset_at},...s.available_credits!==void 0&&{available_credits:s.available_credits}}}async handleAgySetModel(e,n){const t=e.params??{},o=String(t.model_id??t.modelId??"").trim();if(!n){this.aibotHandle.sendLocalActionResult({action_id:e.action_id,status:"failed",error_code:"session_id_required",error_msg:"session_id is required for set_model"});return}if(!o){this.aibotHandle.sendLocalActionResult({action_id:e.action_id,status:"failed",error_code:"set_model_invalid",error_msg:"model_id is required"});return}const i=this.bindingStore.get(n);if(!i?.cwd){this.aibotHandle.sendLocalActionResult({action_id:e.action_id,status:"failed",error_code:g.bindingMissing,error_msg:"session binding was not found"});return}i.modelId!==o&&(this.bindingStore.setModelId(n,o),this.globalConfigStore?.set(this.name,{modelId:o})),this.aibotHandle.sendUpdateBindingCard({session_id:n,worker_status:"ready",cwd:i.cwd,meta:this.buildAgyToolbarMeta(n)}),this.aibotHandle.sendLocalActionResult({action_id:e.action_id,status:"ok",result:{outcome:"model_set",model_id:o,binding:{cwd:i.cwd,model_id:o}}})}buildClaudeToolbarMeta(e){const n=this.bindingStore.get(e);if(!n)return;const t=D(),o=t.length>0?t[0].id:"";let i=n.modelId||this.globalConfigStore?.get(this.name)?.modelId||o;t.length>0&&!t.some(l=>l.id===i)&&(i=o);const s=this.normalizeClaudeModeId(n.modeId),r={model_id:i,mode_id:s,currentModelId:i,currentModeId:s,available_models:t.map(l=>({id:l.id,displayName:l.displayName}))},a=this.pool.getSlot(e),d=a?.adapter instanceof T?a.adapter.getSessionState():null;(d?.rateLimits?.fiveHour||d?.rateLimits?.sevenDay)&&(this.cachedClaudeRateLimitState=d);const c=d??this.getFreshClaudeRateLimitState();if(c){const l=c.rateLimits;l&&(l.fiveHour||l.sevenDay)&&(r.rate_limits={...l.fiveHour?{fiveHour:l.fiveHour}:{},...l.sevenDay?{sevenDay:l.sevenDay}:{},sampledAt:c.sampledAt||Date.now()})}if(d){const l=d.contextWindow;l.usedPercentage!=null&&(r.context_window={usedPercentage:l.usedPercentage,remainingPercentage:l.remainingPercentage})}if(!r.rate_limits&&this.cachedProviderQuota?.success){this.isRateLimitsCacheFresh(this.cachedProviderQuotaSampledAtMs)||this.maybeQueryProviderQuota().catch(()=>{});const l=this.providerQuotaToRateLimits(this.cachedProviderQuota);l&&(r.rate_limits=l),u.info(this.name,`[toolbar-meta] provider quota fallback: hasCached=${!!this.cachedProviderQuota} fromProvider=${JSON.stringify(l)}`)}return r.rate_limits&&u.info(this.name,`[toolbar-meta] rate_limits included: ${JSON.stringify(r.rate_limits)}`),r}providerQuotaToCodexRateLimits(e){const n=e.tiers.find(i=>i.name==="five_hour"),t=e.tiers.find(i=>i.name==="weekly_limit"),o=i=>i||null;if(n||t){const i={credits:{hasCredits:!0,unlimited:!1,balance:null},sampledAt:this.cachedProviderQuotaSampledAtMs??Date.now()};let s=0,r=0,a=0,d=0;return n&&(i.primary={usedPercent:n.usedPercent,windowMinutes:300,resetsAt:o(n.resetsAt)},s=n.usedPercent,r=300),t&&(i.secondary={usedPercent:t.usedPercent,windowMinutes:10080,resetsAt:o(t.resetsAt)},a=t.usedPercent,d=10080),{rateLimits:i,primaryPercent:s,secondaryPercent:a,primaryWindowMin:r,secondaryWindowMin:d}}if(e.balance){const i=e.balance,s=i.total&&i.total>0?Math.min(100,(i.used??i.total-i.remaining)/i.total*100):0;return{rateLimits:{primary:{usedPercent:s,windowMinutes:0,resetsAt:null},secondary:{usedPercent:0,windowMinutes:0,resetsAt:null},credits:{hasCredits:!0,unlimited:!1,balance:i.remaining},sampledAt:this.cachedProviderQuotaSampledAtMs??Date.now()},primaryPercent:s,secondaryPercent:0,primaryWindowMin:0,secondaryWindowMin:0}}return null}providerQuotaToRateLimits(e){const n=i=>{if(!i)return 0;const s=Date.parse(i);return Number.isFinite(s)?Math.floor(s/1e3):0},t=e.tiers.find(i=>i.name==="five_hour"),o=e.tiers.find(i=>i.name==="weekly_limit");if(t||o)return{...t?{fiveHour:{usedPercentage:t.usedPercent,resetsAt:n(t.resetsAt)}}:{},...o?{sevenDay:{usedPercentage:o.usedPercent,resetsAt:n(o.resetsAt)}}:{},sampledAt:this.cachedProviderQuotaSampledAtMs??Date.now()};if(e.balance){const i=e.balance;return{credit:{remaining:i.remaining,total:i.total,used:i.used,unit:i.unit,resetsAt:i.resetsAt?n(i.resetsAt):0},planName:e.planName,sampledAt:this.cachedProviderQuotaSampledAtMs??Date.now()}}return null}async resolveCwdForBinding(e){const n=String(e??"").trim();if(process.platform!=="win32"&&(/^[a-zA-Z]:[\\/]/.test(n)||/^\\\\/.test(n))){const i=new Error(`Specified path is not valid on this host: ${n}`);throw i.cwdErrorCode=g.invalidCwd,i}const t=x.resolve(n);let o;try{o=await M(t)}catch(i){const s=String(i?.code??"");if(s==="ENOENT"){const r=new Error(`Specified path does not exist: ${t}`);throw r.cwdErrorCode=g.invalidCwd,r}if(s==="EACCES"||s==="EPERM"){const r=new Error("Specified path is not accessible.");throw r.cwdErrorCode=g.invalidCwd,r}throw i}if(!o.isDirectory()){const i=new Error("Specified path is not a directory.");throw i.cwdErrorCode=g.invalidCwd,i}try{return await G(t)}catch{return t}}getClaudeWorkerStatus(e){const n=this.pool.getSlot(e);return n?n.state==="starting"?"starting":n.state==="stopped"?"stopped":n.adapter.getStatus().busy?"busy":"ready":"stopped"}refreshClaudeWorkerStatusCard(e,n){const t=this.getClaudeWorkerStatus(e);return this.claudeWorkerStatus.set(e,t),this.aibotHandle.sendUpdateBindingCard({session_id:e,worker_status:t,cwd:n,meta:this.buildClaudeToolbarMeta(e)}),t}async ensureSlotStarted(e,n=6e4){const t=this.pool.getOrCreateSlot(e);if(!t)throw new Error("Failed to allocate session slot");t.startPromise&&(u.info(this.name,`ensureSlotStarted: awaiting startPromise for session=${e}`),await Promise.race([t.startPromise,new Promise((o,i)=>setTimeout(()=>i(new Error(`ensureSlotStarted timeout (${n}ms) session=${e}`)),n))]),u.info(this.name,`ensureSlotStarted: startPromise resolved for session=${e}`))}resolveAgentSessionId(e){switch(this.config.adapterType??"acp"){case"claude":return e.claudeSessionId;case"codex":return e.codexThreadId;case"pi":return e.piSessionPath;case"codewhale":return e.codewhaleThreadId;case"agy":return e.agyConversationId;default:return e.acpSessionId}}providerKeyForAdapter(){const e=this.config.adapterType??"acp";switch(e){case"claude":case"codex":case"pi":case"codewhale":return e;default:return"acp"}}setResolvedAgentSessionId(e,n){const t=String(e??"").trim(),o=String(n??"").trim();if(!t||!o)return;switch(this.config.adapterType??"acp"){case"claude":this.bindingStore.setClaudeSessionId(t,o);break;case"codex":this.bindingStore.setCodexThreadId(t,o);break;case"pi":this.bindingStore.setPiSessionPath(t,o);break;case"codewhale":this.bindingStore.setCodeWhaleThreadId(t,o);break;case"agy":this.bindingStore.setAgyConversationId(t,o);break;default:this.bindingStore.setAcpSessionId(t,o);break}this.sessionScanCache.invalidate()}normalizePathForCompare(e){const n=String(e??"").trim();if(!n)return"";const t=x.resolve(n);return process.platform==="win32"?t.toLowerCase():t}ensureImportedAgentSession(e,n){const t=String(e??"").trim();if(!t)return;const o=this.normalizePathForCompare(n);let i="";const s=this.config.adapterType??"acp";if(s==="codex"?i=this.sessionScanCache.get().find(d=>d.threadId===t)?.cwd??"":s==="claude"?i=this.sessionScanCache.get().find(d=>d.sessionId===t)?.cwd??"":s==="acp"&&(i=this.sessionScanCache.get().find(d=>d.sessionId===t)?.cwd??""),!i){for(const[,a]of this.bindingStore.entries())if(this.resolveAgentSessionId(a)===t){i=a.cwd??"";break}}if(!i){const a=new Error(`agent session not found: ${t}`);throw a.sessionControlErrorCode=g.invalidAgentSession,a}const r=this.normalizePathForCompare(i);if(r&&o&&r!==o){const a=new Error(`agent session cwd mismatch: expected ${n}, got ${i}`);throw a.sessionControlErrorCode=g.invalidAgentSession,a}}buildOpenedBindingResult(e,n,t="ready"){const o=this.bindingStore.get(e),i=o?String(this.resolveAgentSessionId(o)??"").trim():"",s={aibotSessionId:e,providerKey:this.providerKeyForAdapter(),cwd:n,workerStatus:t};return i&&(s.bindingId=i,s.agentSessionId=i),s}hasDiskScanner(){const e=this.config.adapterType??"acp";return e==="codex"||e==="claude"||e==="acp"}async handleListSessionsTextCommand(e){this.aibotHandle.sendEventAck({event_id:e.event_id,session_id:e.session_id,received_at:Date.now()});const n=this.config.adapterType??"acp",t=new Map;for(const a of this.pool.getAllSlots())t.set(a.sessionId,a);const o=Array.from(this.bindingStore.entries()),i=new Map;for(const[a,d]of o){const c=this.resolveAgentSessionId(d);if(c){const l=t.get(a),h=l?l.adapter.getStatus().busy?"busy":"ready":"inactive";i.set(c,{aibotSessionId:a,workerStatus:h})}}const s=[],r=new Set;if(n==="codex"){const a=this.sessionScanCache.get();for(const d of a){r.add(d.threadId);const c=i.get(d.threadId);d.title&&s.push(` Title: ${d.title}`),s.push(` Agent: ${d.threadId}`),c&&s.push(` AIBot: ${c.aibotSessionId}`),s.push(` CWD: ${d.cwd||"-"}`),s.push(` State: ${c?.workerStatus??(d.archived?"archived":"inactive")}`),s.push(` Created: ${new Date(d.createdAt).toISOString()}`),s.push(` Updated: ${new Date(d.updatedAt).toISOString()}`),s.push("---")}}else if(n==="claude"){const a=this.sessionScanCache.get();for(const d of a){r.add(d.sessionId);const c=i.get(d.sessionId);d.title&&s.push(` Title: ${d.title}`),s.push(` Agent: ${d.sessionId}`),c&&s.push(` AIBot: ${c.aibotSessionId}`),s.push(` CWD: ${d.cwd||"-"}`),s.push(` State: ${c?.workerStatus??"inactive"}`),s.push(` Updated: ${new Date(d.updatedAt).toISOString()}`),s.push("---")}}else if(n==="acp"){const a=this.sessionScanCache.get();for(const d of a){r.add(d.sessionId);const c=i.get(d.sessionId);d.title&&s.push(` Title: ${d.title}`),s.push(` Agent: ${d.sessionId}`),c&&s.push(` AIBot: ${c.aibotSessionId}`),s.push(` CWD: ${d.cwd||"-"}`),s.push(` State: ${c?.workerStatus??"inactive"}`),s.push(` Updated: ${new Date(d.updatedAt).toISOString()}`),s.push("---")}}for(const[a,d]of o){const c=this.resolveAgentSessionId(d);if(c&&r.has(c))continue;const l=t.get(a),h=l?l.adapter.getStatus().busy?"busy":"ready":"closed";s.push(` Title: ${c?c.slice(0,8)+"\u2026":a.slice(0,8)+"\u2026"}`),s.push(` AIBot: ${a}`),c&&s.push(` Agent: ${c}`),s.push(` CWD: ${d.cwd??"-"}`),s.push(` State: ${h}`),s.push(` Updated: ${new Date(d.updatedAt).toISOString()}`),s.push("---")}if(s.length===0){this.aibotHandle.sendEventResult({event_id:e.event_id,status:"responded",msg:"No sessions found.",updated_at:Date.now()});return}this.aibotHandle.sendEventResult({event_id:e.event_id,status:"responded",msg:`Sessions (${s.filter(a=>a==="---").length}):
14
14
  ${s.join(`
@@ -1,2 +1,2 @@
1
- import{EventEmitter as k}from"node:events";import{randomUUID as v}from"node:crypto";import p from"node:os";import m from"ws";import{log as r}from"../log/index.js";import{getMachineName as $}from"../util/index.js";import{detectTailnetIPv4 as b,ensureServerAndGetPort as w,getFileServerHttpsPort as q}from"../files/file-serve.js";function E(g){return g.replace(/(?<=[\[:,\[]\s*)(\d{16,})(?=\s*[,}\]\n])/g,'"$1"')}function P(g){const e=[...g??["stream_chunk","local_action_v1","agent_invoke"]];return e.includes("agent_invoke")||e.push("agent_invoke"),e.includes("event_result_ack")||e.push("event_result_ack"),e}const S="aibot-agent-api-v1",y=1;class f extends k{static DROPPABLE_COMMANDS=new Set(["update_binding_card"]);static BUFFER_OVERFLOW_RETAIN_COMMANDS=new Set(["event_result","codex_event","client_stream_chunk"]);static MAX_OUTBOUND_BUFFER_SIZE=1e3;static BACKPRESSURE_THRESHOLD=64*1024;ws=null;seq=0;heartbeatTimer=null;heartbeatSec=30;connected=!1;reconnecting=!1;reconnectAttempts=0;everConnected=!1;config;packetLog;pendingInvokes=new Map;seqEventMap=new Map;pendingRequests=new Map;outboundBuffer=[];ackPolicy=null;constructor(e,t){super(),this.packetLog=t?.packetLog??null,this.config={url:e.url,agentId:e.agentId,apiKey:e.apiKey,clientType:e.clientType,clientVersion:e.clientVersion??"",adapterHint:e.adapterHint??"",capabilities:P(e.capabilities),localActions:e.localActions??["exec_approve","exec_reject"],skills:e.skills}}get isConnected(){return this.connected}async connect(){let e,t,s;const o=(async()=>{try{if(e=await b(),e!==void 0)try{t=await w(e);const n=q();n>0&&(s=n)}catch(n){r.warn("aibot",`file server pre-start failed: ${n}`)}}catch(n){r.warn("aibot",`tailnet detect failed: ${n}`)}})();return new Promise((n,h)=>{const c=new m(this.config.url);this.ws=c;const a=setTimeout(()=>{h(new Error("Auth timeout: no auth_ack received within 15s")),this.cleanupSocket()},15e3),d=++this.seq,i=setTimeout(()=>{this.pendingRequests.delete(d),h(new Error("Auth request timeout")),this.cleanupSocket()},15e3);this.pendingRequests.set(d,{expected:["auth_ack"],resolve:u=>{clearTimeout(a);const l=u.payload;l.code===0?(this.connected=!0,this.everConnected=!0,this.reconnectAttempts=0,l.heartbeat_sec&&(this.heartbeatSec=l.heartbeat_sec),l.ack_policy&&(this.ackPolicy=l.ack_policy,r.info("aibot",`ack_policy received: push_ack_timeout_ms=${l.ack_policy.push_ack_timeout_ms??"default"} max_retries=${l.ack_policy.max_retries??"default"} timeout_action=${l.ack_policy.timeout_action??"default"}`)),this.startHeartbeat(),this.flushOutboundBuffer(),this.emit("auth",l),n(l)):h(new Error(`Auth failed: code=${l.code} msg=${l.msg}`))},reject:u=>{clearTimeout(a),h(u)},timer:i}),c.on("open",async()=>{await o;const u={agent_id:this.config.agentId,api_key:this.config.apiKey,client_type:this.config.clientType,protocol_version:S,contract_version:y,capabilities:this.config.capabilities??[],local_actions:this.config.localActions,skills:this.config.skills};this.config.sharedOwnerId&&(u.shared_owner_id=this.config.sharedOwnerId),this.config.clientVersion&&(u.client="grix-connector",u.client_version=this.config.clientVersion,u.host_type=this.config.clientType,u.host_version=this.config.clientVersion),this.config.adapterHint&&(u.adapter_hint=this.config.adapterHint),u.host_meta={hostname:$(),platform:p.platform(),arch:p.arch(),os_release:p.release(),...e!==void 0&&{tailnet_ip:e},...t!==void 0&&t>0&&{file_server_port:t},...s!==void 0&&s>0&&{file_server_https_port:s}},this.config.concurrency&&(u.concurrency=this.config.concurrency),this.sendPacket("auth",u,d)}),c.on("message",u=>{let l;try{l=JSON.parse(E(u.toString()))}catch{return}try{this.handlePacket(l)}catch(_){this.emitClientError(new Error(`handlePacket error: ${_}`))}}),c.on("close",(u,l)=>{this.connected=!1,this.stopHeartbeat(),this.rejectAllPendingRequests("websocket closed"),this.emit("close",u,l.toString());const _=u!==1e3&&this.everConnected;r.info("aibot",`ws closed agent=${this.config.clientType}:${this.config.agentId} code=${u} reason=${l.toString()||"<none>"} everConnected=${this.everConnected} reconnecting=${this.reconnecting} willReconnect=${_}`),_&&this.attemptReconnect()}),c.on("error",u=>{this.emitClientError(u instanceof Error?u:new Error(String(u))),this.connected||h(u)})})}handlePacket(e){if(this.packetLog?.logInboundPacket(e.cmd,e.seq,e.payload),e.seq>0&&this.pendingRequests.has(e.seq)){const t=this.pendingRequests.get(e.seq);this.pendingRequests.delete(e.seq),clearTimeout(t.timer),t.expected.includes(e.cmd)?t.resolve(e):t.reject(new Error(`unexpected response: got ${e.cmd}, expected ${t.expected.join("/")}`));return}switch(e.cmd){case"auth_ack":break;case"ping":{this.sendPacket("pong",e.payload??{});break}case"event_msg":{this.emit("event",e.payload);break}case"local_action":{this.emit("localAction",e.payload);break}case"event_stop":{this.emit("stop",e.payload);break}case"event_revoke":{this.emit("revoke",e.payload);break}case"event_edit":{this.emit("edit",e.payload);break}case"event_cancel":{this.emit("eventCancel",e.payload);break}case"queue_clear":{this.emit("queueClear",e.payload);break}case"queue_snapshot_query":{this.emit("queueSnapshotQuery",e.payload);break}case"control_share_set":{this.emit("shareSet",e.payload);break}case"kicked":{if(this.emit("kicked",e.payload),this.connected=!1,this.stopHeartbeat(),this.rejectAllPendingRequests("kicked"),this.outboundBuffer.length=0,this.reconnectAttempts=Math.max(this.reconnectAttempts,3),this.ws){try{this.ws.close(4001,"kicked")}catch{}this.ws=null}break}case"error":{const t=e.payload,s=[t.ref_cmd?`ref_cmd=${t.ref_cmd}`:"",t.ref_id?`ref_id=${t.ref_id}`:""].filter(Boolean).join(" ");this.emitClientError(new Error(`Server error: code=${t.code} msg=${t.msg}${s?` ${s}`:""}`));break}case"agent_invoke_result":{this.handleInvokeResult(e.payload);break}case"mcp_frame":{const t=e.payload;this.emit("mcpFrame",t.session_id??"",t.frame??null);break}case"send_ack":break;case"send_nack":{const t=e.payload;if(t.code===4003&&e.seq>0){const s=this.seqEventMap.get(e.seq);s&&(this.seqEventMap.delete(e.seq),this.purgeBufferedStreamChunks(s),r.warn("aibot",`stream chunk rejected (4003), purging buffered chunks for event=${s}`),this.emit("streamRejected",s,t.code))}break}case"local_action_ack":break;default:break}}sendEventAck(e){this.sendPacket("event_ack",e)||r.warn("aibot",`event_ack NOT sent (ws not open) event=${e.event_id} ws=${this.ws?`state=${this.ws.readyState}`:"null"}`)}sendStreamChunk(e){!e.delta_content&&!e.is_finish&&(r.warn("aibot",`stream_chunk delta_content empty, patched to newline event=${e.event_id??""} session=${e.session_id} chunk_seq=${e.chunk_seq} is_finish=${e.is_finish}`),e={...e,delta_content:`
2
- `}),this.sendPacket("client_stream_chunk",e)}sendMsg(e){this.sendPacket("send_msg",e)||r.warn("aibot",`send_msg NOT sent (ws not open) event=${e.event_id??""} session=${e.session_id??""} ws=${this.ws?`state=${this.ws.readyState}`:"null"}`)}editMsg(e){this.sendPacket("edit_msg",e)}sendEventResult(e){if(!this.ws||this.ws.readyState!==m.OPEN){this.sendPacket("event_result",e);return}this.sendEventResultReliable(e)}sendLocalActionResult(e){this.sendPacket("local_action_result",e)}sendEventStopAck(e){this.sendPacket("event_stop_ack",e)}sendEventStopResult(e){this.sendPacket("event_stop_result",e)}sendSessionActivitySet(e){this.sendPacket("session_activity_set",e)}sendCodexEvent(e){this.sendPacket("codex_event",e)}sendUpdateBindingCard(e){this.sendPacket("update_binding_card",e)}sendSkillsUpdate(e){this.sendPacket("agent_skills_update",e)}sendPing(){this.sendPacket("ping",{})}sendEventState(e){this.sendPacket("event_state",e)}sendEventCancelResult(e){this.sendPacket("event_cancel_result",e)}sendQueueClearResult(e){this.sendPacket("queue_clear_result",e)}sendQueueSnapshot(e){this.sendPacket("queue_snapshot",e)}agentInvoke(e,t,s=15e3){return new Promise((o,n)=>{const h=v(),c=Math.max(1e3,Math.min(s,6e4)),a=setTimeout(()=>{this.pendingInvokes.delete(h),n(new Error(`agent_invoke timeout: ${e}`))},c);this.pendingInvokes.set(h,{resolve:o,reject:n,timer:a}),this.sendPacket("agent_invoke",{invoke_id:h,action:e,params:t,timeout_ms:c})})}sendMcpFrame(e,t){this.sendPacket("mcp_frame",{session_id:e,frame:t})}request(e,t,s){return new Promise((o,n)=>{const h=++this.seq,c=setTimeout(()=>{this.pendingRequests.delete(h),n(new Error(`request timeout: ${e} (expected ${s.expected.join("/")})`))},s.timeoutMs);this.pendingRequests.set(h,{expected:s.expected,resolve:o,reject:n,timer:c}),this.sendPacket(e,t,h)||(this.pendingRequests.delete(h),clearTimeout(c),n(new Error(`send failed: ${e}`)))})}async sendStreamChunkRequest(e,t=2e4){return this.request("client_stream_chunk",e,{expected:["send_ack","send_nack","error"],timeoutMs:t})}async sendText(e,t=2e4){return this.request("send_msg",{msg_type:1,...e},{expected:["send_ack","send_nack","error"],timeoutMs:t})}async sendMedia(e,t=2e4){return this.request("send_msg",{...e,msg_type:2},{expected:["send_ack","send_nack","error"],timeoutMs:t})}async editMessage(e,t=2e4){return this.request("edit_msg",e,{expected:["send_ack","send_nack","error"],timeoutMs:t})}async deleteMessage(e,t,s=2e4){return this.request("delete_msg",{session_id:e,msg_id:t},{expected:["send_ack","send_nack","error"],timeoutMs:s})}async sendEventResultRequest(e,t=5e3){return this.request("event_result",e,{expected:["send_ack","send_nack","error"],timeoutMs:t})}disconnect(){r.info("aibot",`disconnect() agent=${this.config.clientType}:${this.config.agentId} wasConnected=${this.connected} reconnecting=${this.reconnecting} reconnectAttempts=${this.reconnectAttempts}`),this.connected=!1,this.everConnected=!1,this.reconnecting=!1,this.reconnectAttempts=0,this.stopHeartbeat(),this.rejectAllPendingInvokes("disconnect"),this.rejectAllPendingRequests("disconnect"),this.outboundBuffer.length=0,this.ws&&(this.ws.close(1e3,"client disconnect"),this.ws=null)}async attemptReconnect(){if(!this.reconnecting)for(this.reconnecting=!0,r.info("aibot",`attemptReconnect start agent=${this.config.clientType}:${this.config.agentId} fromAttempts=${this.reconnectAttempts}`),this.emit("disconnected");this.reconnecting;){const e=Math.min(1e3*2**this.reconnectAttempts,3e4),t=Math.floor(e*.2*Math.random());if(this.reconnectAttempts++,await new Promise(s=>setTimeout(s,e+t)),!this.reconnecting)return;try{const s=await this.connect(),o=this.reconnectAttempts;this.reconnectAttempts=0,this.reconnecting=!1,r.info("aibot",`reconnect succeeded agent=${this.config.clientType}:${this.config.agentId} attempt=${o}`),this.emit("auth",s);return}catch(s){r.warn("aibot",`reconnect failed agent=${this.config.clientType}:${this.config.agentId} attempt=${this.reconnectAttempts} err=${s instanceof Error?s.message:s}`)}}}sendPacket(e,t,s){if(this.ws&&this.ws.readyState===m.OPEN){const o=this.ws.bufferedAmount>f.BACKPRESSURE_THRESHOLD;if(!o||!f.DROPPABLE_COMMANDS.has(e)){if(o&&f.DROPPABLE_COMMANDS.has(e))return!1;const n=s??++this.seq;if(e==="client_stream_chunk"&&t&&typeof t=="object"){const c=t.event_id;if(c&&(this.seqEventMap.set(n,c),this.seqEventMap.size>200)){const a=this.seqEventMap.keys().next().value;a!==void 0&&this.seqEventMap.delete(a)}}const h={cmd:e,seq:n,payload:t};this.packetLog?.logOutboundPacket(e,n,t,"sent");try{const c=this.ws.readyState,a=this.ws.bufferedAmount;return this.ws.send(JSON.stringify(h),d=>{if(e==="event_result"){const i=t;d?r.warn("aibot",`event_result ws send callback failed event=${i.event_id??""} status=${i.status??""} seq=${n} readyState=${c} bufferedAmount=${a} err=${d.message}`):r.info("aibot",`event_result ws send callback ok event=${i.event_id??""} status=${i.status??""} seq=${n} readyState=${c} bufferedAmount=${a}`)}else if(e==="client_stream_chunk"){const i=t;d?r.warn("aibot",`stream_chunk ws send failed event=${i.event_id??""} session=${i.session_id??""} seq=${n} chunk_seq=${i.chunk_seq??""} is_finish=${i.is_finish??""} readyState=${c} bufferedAmount=${a} err=${d.message}`):r.info("aibot",`stream_chunk ws send ok event=${i.event_id??""} session=${i.session_id??""} seq=${n} chunk_seq=${i.chunk_seq??""} is_finish=${i.is_finish??""} readyState=${c} bufferedAmount=${a}`)}else if(e==="event_ack"){const i=t;d?r.warn("aibot",`event_ack ws send failed event=${i.event_id??""} seq=${n} readyState=${c} bufferedAmount=${a} err=${d.message}`):r.info("aibot",`event_ack ws send ok event=${i.event_id??""} seq=${n} readyState=${c} bufferedAmount=${a}`)}else if(e==="send_msg"){const i=t;d?r.warn("aibot",`send_msg ws send failed event=${i.event_id??""} session=${i.session_id??""} seq=${n} readyState=${c} bufferedAmount=${a} err=${d.message}`):r.info("aibot",`send_msg ws send ok event=${i.event_id??""} session=${i.session_id??""} seq=${n} readyState=${c} bufferedAmount=${a}`)}else if(d){const i=t;r.warn("aibot",`${e} ws send failed seq=${n} session=${i.session_id??""} event=${i.event_id??""} client_msg_id=${i.client_msg_id??""} readyState=${c} bufferedAmount=${a} err=${d.message}`)}}),!0}catch(c){return this.emitClientError(new Error(`sendPacket failed: ${c}`)),!1}}}if(f.DROPPABLE_COMMANDS.has(e))return this.packetLog?.logOutboundPacket(e,s??0,t,"dropped"),!1;if(s!==void 0)return this.packetLog?.logOutboundPacket(e,s,t,"dropped"),!1;if(this.outboundBuffer.length>=f.MAX_OUTBOUND_BUFFER_SIZE&&(this.outboundBuffer=this.outboundBuffer.filter(o=>f.BUFFER_OVERFLOW_RETAIN_COMMANDS.has(o.cmd)),this.outboundBuffer.length>=f.MAX_OUTBOUND_BUFFER_SIZE&&this.outboundBuffer.shift()),this.outboundBuffer.push({cmd:e,payload:t}),this.packetLog?.logOutboundPacket(e,s??0,t,"buffered"),e==="client_stream_chunk"){const o=t;r.info("aibot",`stream_chunk buffered (ws not open) event=${o.event_id??""} session=${o.session_id??""} chunk_seq=${o.chunk_seq??""} is_finish=${o.is_finish??""} ws=${this.ws?`state=${this.ws.readyState}`:"null"}`)}return!1}async sendEventResultReliable(e){const t=this.ackPolicy?.max_retries??3,s=this.ackPolicy?.push_ack_timeout_ms??5e3,o=750;for(let n=1;n<=t;n++){const h=this.ws?.readyState??-1,c=this.ws?.bufferedAmount??0;r.info("aibot",`event_result send attempt event=${e.event_id} status=${e.status} attempt=${n}/${t} readyState=${h} bufferedAmount=${c}`);try{const a=await this.sendEventResultRequest(e,s);if(a.cmd==="send_ack"){const i=a.payload;r.info("aibot",`event_result ack event=${e.event_id} status=${e.status} attempt=${n}/${t} ack_event=${i.event_id??""} ack_status=${i.status??""}`);return}const d=a.payload;if(r.warn("aibot",`event_result rejected event=${e.event_id} status=${e.status} attempt=${n}/${t} cmd=${a.cmd} code=${d.code??""} msg=${d.msg??""}${d.ref_cmd?` ref_cmd=${d.ref_cmd}`:""}${d.ref_id?` ref_id=${d.ref_id}`:""}`),d.code===4003){r.warn("aibot",`event_result stopping retries: 4003 ownership denied event=${e.event_id}`);return}return}catch(a){const d=a instanceof Error?a.message:String(a);if(r.warn("aibot",`event_result attempt failed event=${e.event_id} status=${e.status} attempt=${n}/${t} err=${d}`),n===t){this.emitClientError(new Error(`event_result ack failed after ${t} attempts: event=${e.event_id} status=${e.status}`));return}await new Promise(i=>setTimeout(i,o*n))}}}purgeBufferedStreamChunks(e){const t=this.outboundBuffer.length;this.outboundBuffer=this.outboundBuffer.filter(s=>s.cmd!=="client_stream_chunk"?!0:s.payload?.event_id!==e),this.outboundBuffer.length<t&&r.info("aibot",`purged ${t-this.outboundBuffer.length} buffered stream chunks for event=${e}`)}emitClientError(e){if(this.listenerCount("error")===0){r.warn("aibot",`Client error (no listeners): ${e.message}`);return}this.emit("error",e)}flushOutboundBuffer(){if(this.outboundBuffer.length===0||!this.ws||this.ws.readyState!==m.OPEN)return;const e=this.outboundBuffer;this.outboundBuffer=[];for(const{cmd:t,payload:s}of e){const o=++this.seq;if(t==="client_stream_chunk"&&s&&typeof s=="object"){const h=s.event_id;h&&this.seqEventMap.set(o,h)}const n={cmd:t,seq:o,payload:s};try{this.ws.send(JSON.stringify(n))}catch{break}}if(this.seqEventMap.size>200){const t=[...this.seqEventMap.entries()].sort((s,o)=>s[0]-o[0]);this.seqEventMap.clear();for(const[s,o]of t.slice(-100))this.seqEventMap.set(s,o)}}handleInvokeResult(e){const t=this.pendingInvokes.get(e.invoke_id);t&&(this.pendingInvokes.delete(e.invoke_id),clearTimeout(t.timer),e.code===0?t.resolve(e.data??null):t.reject(new Error(`agent_invoke error code=${e.code}: ${e.msg??""}`)))}rejectAllPendingInvokes(e){for(const[,t]of this.pendingInvokes)clearTimeout(t.timer),t.reject(new Error(`agent_invoke canceled: ${e}`));this.pendingInvokes.clear()}rejectAllPendingRequests(e){for(const[,t]of this.pendingRequests)clearTimeout(t.timer),t.reject(new Error(`request canceled: ${e}`));this.pendingRequests.clear()}cleanupSocket(){if(this.ws){try{this.ws.close()}catch{}this.ws=null}}startHeartbeat(){this.stopHeartbeat(),this.heartbeatTimer=setInterval(()=>{this.connected&&this.request("ping",{ts:Date.now()},{expected:["pong"],timeoutMs:5e3}).catch(()=>{this.connected&&(this.cleanupSocket(),this.attemptReconnect())})},this.heartbeatSec*1e3)}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}}export{f as AibotClient};
1
+ import{EventEmitter as k}from"node:events";import{randomUUID as v}from"node:crypto";import p from"node:os";import m from"ws";import{log as o}from"../log/index.js";import{getMachineName as $}from"../util/index.js";import{detectTailnetIPv4 as b,ensureServerAndGetPort as w,getFileServerHttpsPort as E}from"../files/file-serve.js";function q(g){return g.replace(/(?<=[\[:,\[]\s*)(\d{16,})(?=\s*[,}\]\n])/g,'"$1"')}function S(g){const e=[...g??["stream_chunk","local_action_v1","agent_invoke"]];return e.includes("agent_invoke")||e.push("agent_invoke"),e.includes("event_result_ack")||e.push("event_result_ack"),e}const P="aibot-agent-api-v1",y=1;class f extends k{static DROPPABLE_COMMANDS=new Set(["update_binding_card"]);static BUFFER_OVERFLOW_RETAIN_COMMANDS=new Set(["event_result","codex_event","client_stream_chunk"]);static MAX_OUTBOUND_BUFFER_SIZE=1e3;static BACKPRESSURE_THRESHOLD=64*1024;ws=null;seq=0;heartbeatTimer=null;heartbeatSec=30;connected=!1;reconnecting=!1;reconnectAttempts=0;everConnected=!1;config;packetLog;pendingInvokes=new Map;seqEventMap=new Map;pendingRequests=new Map;outboundBuffer=[];ackPolicy=null;constructor(e,t){super(),this.packetLog=t?.packetLog??null,this.config={url:e.url,agentId:e.agentId,apiKey:e.apiKey,clientType:e.clientType,clientVersion:e.clientVersion??"",adapterHint:e.adapterHint??"",capabilities:S(e.capabilities),localActions:e.localActions??["exec_approve","exec_reject"],skills:e.skills}}get isConnected(){return this.connected}async connect(){let e,t,n;const i=(async()=>{try{if(e=await b(),e!==void 0)try{t=await w(e);const s=E();s>0&&(n=s)}catch(s){o.warn("aibot",`file server pre-start failed: ${s}`)}}catch(s){o.warn("aibot",`tailnet detect failed: ${s}`)}})();return new Promise((s,h)=>{const c=new m(this.config.url);this.ws=c;const a=setTimeout(()=>{h(new Error("Auth timeout: no auth_ack received within 15s")),this.cleanupSocket()},15e3),d=++this.seq,r=setTimeout(()=>{this.pendingRequests.delete(d),h(new Error("Auth request timeout")),this.cleanupSocket()},15e3);this.pendingRequests.set(d,{expected:["auth_ack"],resolve:u=>{clearTimeout(a);const l=u.payload;l.code===0?(this.connected=!0,this.everConnected=!0,this.reconnectAttempts=0,l.heartbeat_sec&&(this.heartbeatSec=l.heartbeat_sec),l.ack_policy&&(this.ackPolicy=l.ack_policy,o.info("aibot",`ack_policy received: push_ack_timeout_ms=${l.ack_policy.push_ack_timeout_ms??"default"} max_retries=${l.ack_policy.max_retries??"default"} timeout_action=${l.ack_policy.timeout_action??"default"}`)),this.startHeartbeat(),this.flushOutboundBuffer(),this.emit("auth",l),s(l)):h(new Error(`Auth failed: code=${l.code} msg=${l.msg}`))},reject:u=>{clearTimeout(a),h(u)},timer:r}),c.on("open",async()=>{await i;const u={agent_id:this.config.agentId,api_key:this.config.apiKey,client_type:this.config.clientType,protocol_version:P,contract_version:y,capabilities:this.config.capabilities??[],local_actions:this.config.localActions,skills:this.config.skills};this.config.sharedOwnerId&&(u.shared_owner_id=this.config.sharedOwnerId),this.config.clientVersion&&(u.client="grix-connector",u.client_version=this.config.clientVersion,u.host_type=this.config.clientType,u.host_version=this.config.clientVersion),this.config.adapterHint&&(u.adapter_hint=this.config.adapterHint),u.host_meta={hostname:$(),platform:p.platform(),arch:p.arch(),os_release:p.release(),...e!==void 0&&{tailnet_ip:e},...t!==void 0&&t>0&&{file_server_port:t},...n!==void 0&&n>0&&{file_server_https_port:n}},this.config.concurrency&&(u.concurrency=this.config.concurrency),this.sendPacket("auth",u,d)}),c.on("message",u=>{if(this.ws!==c)return;let l;try{l=JSON.parse(q(u.toString()))}catch{return}try{this.handlePacket(l)}catch(_){this.emitClientError(new Error(`handlePacket error: ${_}`))}}),c.on("close",(u,l)=>{if(this.ws!==c)return;this.connected=!1,this.stopHeartbeat(),this.rejectAllPendingRequests("websocket closed"),this.emit("close",u,l.toString());const _=u!==1e3&&this.everConnected;o.info("aibot",`ws closed agent=${this.config.clientType}:${this.config.agentId} code=${u} reason=${l.toString()||"<none>"} everConnected=${this.everConnected} reconnecting=${this.reconnecting} willReconnect=${_}`),_&&this.attemptReconnect()}),c.on("error",u=>{this.ws===c&&(this.emitClientError(u instanceof Error?u:new Error(String(u))),this.connected||h(u))})})}handlePacket(e){if(this.packetLog?.logInboundPacket(e.cmd,e.seq,e.payload),e.seq>0&&this.pendingRequests.has(e.seq)){const t=this.pendingRequests.get(e.seq);this.pendingRequests.delete(e.seq),clearTimeout(t.timer),t.expected.includes(e.cmd)?t.resolve(e):t.reject(new Error(`unexpected response: got ${e.cmd}, expected ${t.expected.join("/")}`));return}switch(e.cmd){case"auth_ack":break;case"ping":{this.sendPacket("pong",e.payload??{});break}case"event_msg":{this.emit("event",e.payload);break}case"local_action":{this.emit("localAction",e.payload);break}case"event_stop":{this.emit("stop",e.payload);break}case"event_revoke":{this.emit("revoke",e.payload);break}case"event_edit":{this.emit("edit",e.payload);break}case"event_cancel":{this.emit("eventCancel",e.payload);break}case"queue_clear":{this.emit("queueClear",e.payload);break}case"queue_snapshot_query":{this.emit("queueSnapshotQuery",e.payload);break}case"control_share_set":{this.emit("shareSet",e.payload);break}case"kicked":{if(this.emit("kicked",e.payload),this.connected=!1,this.stopHeartbeat(),this.rejectAllPendingRequests("kicked"),this.outboundBuffer.length=0,this.reconnectAttempts=Math.max(this.reconnectAttempts,3),this.ws){try{this.ws.close(4001,"kicked")}catch{}this.ws=null}break}case"error":{const t=e.payload,n=[t.ref_cmd?`ref_cmd=${t.ref_cmd}`:"",t.ref_id?`ref_id=${t.ref_id}`:""].filter(Boolean).join(" ");this.emitClientError(new Error(`Server error: code=${t.code} msg=${t.msg}${n?` ${n}`:""}`));break}case"agent_invoke_result":{this.handleInvokeResult(e.payload);break}case"mcp_frame":{const t=e.payload;this.emit("mcpFrame",t.session_id??"",t.frame??null);break}case"send_ack":break;case"send_nack":{const t=e.payload;if(t.code===4003&&e.seq>0){const n=this.seqEventMap.get(e.seq);n&&(this.seqEventMap.delete(e.seq),this.purgeBufferedStreamChunks(n),o.warn("aibot",`stream chunk rejected (4003), purging buffered chunks for event=${n}`),this.emit("streamRejected",n,t.code))}break}case"local_action_ack":break;default:break}}sendEventAck(e){this.sendPacket("event_ack",e)||o.warn("aibot",`event_ack NOT sent (ws not open) event=${e.event_id} ws=${this.ws?`state=${this.ws.readyState}`:"null"}`)}sendStreamChunk(e){!e.delta_content&&!e.is_finish&&(o.warn("aibot",`stream_chunk delta_content empty, patched to newline event=${e.event_id??""} session=${e.session_id} chunk_seq=${e.chunk_seq} is_finish=${e.is_finish}`),e={...e,delta_content:`
2
+ `}),this.sendPacket("client_stream_chunk",e)}sendMsg(e){this.sendPacket("send_msg",e)||o.warn("aibot",`send_msg NOT sent (ws not open) event=${e.event_id??""} session=${e.session_id??""} ws=${this.ws?`state=${this.ws.readyState}`:"null"}`)}editMsg(e){this.sendPacket("edit_msg",e)}sendEventResult(e){if(!this.ws||this.ws.readyState!==m.OPEN){this.sendPacket("event_result",e);return}this.sendEventResultReliable(e)}sendLocalActionResult(e){this.sendPacket("local_action_result",e)}sendEventStopAck(e){this.sendPacket("event_stop_ack",e)}sendEventStopResult(e){this.sendPacket("event_stop_result",e)}sendSessionActivitySet(e){this.sendPacket("session_activity_set",e)}sendCodexEvent(e){this.sendPacket("codex_event",e)}sendUpdateBindingCard(e){this.sendPacket("update_binding_card",e)}sendSkillsUpdate(e){this.sendPacket("agent_skills_update",e)}sendPing(){this.sendPacket("ping",{})}sendEventState(e){this.sendPacket("event_state",e)}sendEventCancelResult(e){this.sendPacket("event_cancel_result",e)}sendQueueClearResult(e){this.sendPacket("queue_clear_result",e)}sendQueueSnapshot(e){this.sendPacket("queue_snapshot",e)}agentInvoke(e,t,n=15e3){return new Promise((i,s)=>{const h=v(),c=Math.max(1e3,Math.min(n,6e4)),a=setTimeout(()=>{this.pendingInvokes.delete(h),s(new Error(`agent_invoke timeout: ${e}`))},c);this.pendingInvokes.set(h,{resolve:i,reject:s,timer:a}),this.sendPacket("agent_invoke",{invoke_id:h,action:e,params:t,timeout_ms:c})})}sendMcpFrame(e,t){this.sendPacket("mcp_frame",{session_id:e,frame:t})}request(e,t,n){return new Promise((i,s)=>{const h=++this.seq,c=setTimeout(()=>{this.pendingRequests.delete(h),s(new Error(`request timeout: ${e} (expected ${n.expected.join("/")})`))},n.timeoutMs);this.pendingRequests.set(h,{expected:n.expected,resolve:i,reject:s,timer:c}),this.sendPacket(e,t,h)||(this.pendingRequests.delete(h),clearTimeout(c),s(new Error(`send failed: ${e}`)))})}async sendStreamChunkRequest(e,t=2e4){return this.request("client_stream_chunk",e,{expected:["send_ack","send_nack","error"],timeoutMs:t})}async sendText(e,t=2e4){return this.request("send_msg",{msg_type:1,...e},{expected:["send_ack","send_nack","error"],timeoutMs:t})}async sendMedia(e,t=2e4){return this.request("send_msg",{...e,msg_type:2},{expected:["send_ack","send_nack","error"],timeoutMs:t})}async editMessage(e,t=2e4){return this.request("edit_msg",e,{expected:["send_ack","send_nack","error"],timeoutMs:t})}async deleteMessage(e,t,n=2e4){return this.request("delete_msg",{session_id:e,msg_id:t},{expected:["send_ack","send_nack","error"],timeoutMs:n})}async sendEventResultRequest(e,t=5e3){return this.request("event_result",e,{expected:["send_ack","send_nack","error"],timeoutMs:t})}disconnect(){o.info("aibot",`disconnect() agent=${this.config.clientType}:${this.config.agentId} wasConnected=${this.connected} reconnecting=${this.reconnecting} reconnectAttempts=${this.reconnectAttempts}`),this.connected=!1,this.everConnected=!1,this.reconnecting=!1,this.reconnectAttempts=0,this.stopHeartbeat(),this.rejectAllPendingInvokes("disconnect"),this.rejectAllPendingRequests("disconnect"),this.outboundBuffer.length=0,this.ws&&(this.ws.close(1e3,"client disconnect"),this.ws=null)}static MAX_CONSECUTIVE_AUTH_FAILURES=5;async attemptReconnect(){if(this.reconnecting)return;this.reconnecting=!0,o.info("aibot",`attemptReconnect start agent=${this.config.clientType}:${this.config.agentId} fromAttempts=${this.reconnectAttempts}`),this.emit("disconnected");let e=0;for(;this.reconnecting;){const t=Math.min(1e3*2**this.reconnectAttempts,3e4),n=Math.floor(t*.2*Math.random());if(this.reconnectAttempts++,await new Promise(i=>setTimeout(i,t+n)),!this.reconnecting)return;try{const i=await this.connect(),s=this.reconnectAttempts;this.reconnectAttempts=0,this.reconnecting=!1,o.info("aibot",`reconnect succeeded agent=${this.config.clientType}:${this.config.agentId} attempt=${s}`),this.emit("auth",i);return}catch(i){if(this.ws){try{this.ws.close()}catch{}this.ws=null}const s=i instanceof Error?i.message:String(i);if(o.warn("aibot",`reconnect failed agent=${this.config.clientType}:${this.config.agentId} attempt=${this.reconnectAttempts} err=${s}`),/Auth failed/i.test(s)){if(e++,e>=f.MAX_CONSECUTIVE_AUTH_FAILURES){this.reconnecting=!1,o.error("aibot",`reconnect giving up after ${e} consecutive auth failures agent=${this.config.clientType}:${this.config.agentId}`);return}}else e=0}}}sendPacket(e,t,n){if(this.ws&&this.ws.readyState===m.OPEN){const i=this.ws.bufferedAmount>f.BACKPRESSURE_THRESHOLD;if(!i||!f.DROPPABLE_COMMANDS.has(e)){if(i&&f.DROPPABLE_COMMANDS.has(e))return!1;const s=n??++this.seq;if(e==="client_stream_chunk"&&t&&typeof t=="object"){const c=t.event_id;if(c&&(this.seqEventMap.set(s,c),this.seqEventMap.size>200)){const a=this.seqEventMap.keys().next().value;a!==void 0&&this.seqEventMap.delete(a)}}const h={cmd:e,seq:s,payload:t};this.packetLog?.logOutboundPacket(e,s,t,"sent");try{const c=this.ws.readyState,a=this.ws.bufferedAmount;return this.ws.send(JSON.stringify(h),d=>{if(e==="event_result"){const r=t;d?o.warn("aibot",`event_result ws send callback failed event=${r.event_id??""} status=${r.status??""} seq=${s} readyState=${c} bufferedAmount=${a} err=${d.message}`):o.info("aibot",`event_result ws send callback ok event=${r.event_id??""} status=${r.status??""} seq=${s} readyState=${c} bufferedAmount=${a}`)}else if(e==="client_stream_chunk"){const r=t;d?o.warn("aibot",`stream_chunk ws send failed event=${r.event_id??""} session=${r.session_id??""} seq=${s} chunk_seq=${r.chunk_seq??""} is_finish=${r.is_finish??""} readyState=${c} bufferedAmount=${a} err=${d.message}`):o.info("aibot",`stream_chunk ws send ok event=${r.event_id??""} session=${r.session_id??""} seq=${s} chunk_seq=${r.chunk_seq??""} is_finish=${r.is_finish??""} readyState=${c} bufferedAmount=${a}`)}else if(e==="event_ack"){const r=t;d?o.warn("aibot",`event_ack ws send failed event=${r.event_id??""} seq=${s} readyState=${c} bufferedAmount=${a} err=${d.message}`):o.info("aibot",`event_ack ws send ok event=${r.event_id??""} seq=${s} readyState=${c} bufferedAmount=${a}`)}else if(e==="send_msg"){const r=t;d?o.warn("aibot",`send_msg ws send failed event=${r.event_id??""} session=${r.session_id??""} seq=${s} readyState=${c} bufferedAmount=${a} err=${d.message}`):o.info("aibot",`send_msg ws send ok event=${r.event_id??""} session=${r.session_id??""} seq=${s} readyState=${c} bufferedAmount=${a}`)}else if(d){const r=t;o.warn("aibot",`${e} ws send failed seq=${s} session=${r.session_id??""} event=${r.event_id??""} client_msg_id=${r.client_msg_id??""} readyState=${c} bufferedAmount=${a} err=${d.message}`)}}),!0}catch(c){return this.emitClientError(new Error(`sendPacket failed: ${c}`)),!1}}}if(f.DROPPABLE_COMMANDS.has(e))return this.packetLog?.logOutboundPacket(e,n??0,t,"dropped"),!1;if(n!==void 0)return this.packetLog?.logOutboundPacket(e,n,t,"dropped"),!1;if(this.outboundBuffer.length>=f.MAX_OUTBOUND_BUFFER_SIZE&&(this.outboundBuffer=this.outboundBuffer.filter(i=>f.BUFFER_OVERFLOW_RETAIN_COMMANDS.has(i.cmd)),this.outboundBuffer.length>=f.MAX_OUTBOUND_BUFFER_SIZE&&this.outboundBuffer.shift()),this.outboundBuffer.push({cmd:e,payload:t}),this.packetLog?.logOutboundPacket(e,n??0,t,"buffered"),e==="client_stream_chunk"){const i=t;o.info("aibot",`stream_chunk buffered (ws not open) event=${i.event_id??""} session=${i.session_id??""} chunk_seq=${i.chunk_seq??""} is_finish=${i.is_finish??""} ws=${this.ws?`state=${this.ws.readyState}`:"null"}`)}return!1}async sendEventResultReliable(e){const t=this.ackPolicy?.max_retries??3,n=this.ackPolicy?.push_ack_timeout_ms??5e3,i=750;for(let s=1;s<=t;s++){const h=this.ws?.readyState??-1,c=this.ws?.bufferedAmount??0;o.info("aibot",`event_result send attempt event=${e.event_id} status=${e.status} attempt=${s}/${t} readyState=${h} bufferedAmount=${c}`);try{const a=await this.sendEventResultRequest(e,n);if(a.cmd==="send_ack"){const r=a.payload;o.info("aibot",`event_result ack event=${e.event_id} status=${e.status} attempt=${s}/${t} ack_event=${r.event_id??""} ack_status=${r.status??""}`);return}const d=a.payload;if(o.warn("aibot",`event_result rejected event=${e.event_id} status=${e.status} attempt=${s}/${t} cmd=${a.cmd} code=${d.code??""} msg=${d.msg??""}${d.ref_cmd?` ref_cmd=${d.ref_cmd}`:""}${d.ref_id?` ref_id=${d.ref_id}`:""}`),d.code===4003){o.warn("aibot",`event_result stopping retries: 4003 ownership denied event=${e.event_id}`);return}return}catch(a){const d=a instanceof Error?a.message:String(a);if(o.warn("aibot",`event_result attempt failed event=${e.event_id} status=${e.status} attempt=${s}/${t} err=${d}`),s===t){this.emitClientError(new Error(`event_result ack failed after ${t} attempts: event=${e.event_id} status=${e.status}`));return}await new Promise(r=>setTimeout(r,i*s))}}}purgeBufferedStreamChunks(e){const t=this.outboundBuffer.length;this.outboundBuffer=this.outboundBuffer.filter(n=>n.cmd!=="client_stream_chunk"?!0:n.payload?.event_id!==e),this.outboundBuffer.length<t&&o.info("aibot",`purged ${t-this.outboundBuffer.length} buffered stream chunks for event=${e}`)}emitClientError(e){if(this.listenerCount("error")===0){o.warn("aibot",`Client error (no listeners): ${e.message}`);return}this.emit("error",e)}flushOutboundBuffer(){if(this.outboundBuffer.length===0||!this.ws||this.ws.readyState!==m.OPEN)return;const e=this.outboundBuffer;this.outboundBuffer=[];for(const{cmd:t,payload:n}of e){const i=++this.seq;if(t==="client_stream_chunk"&&n&&typeof n=="object"){const h=n.event_id;h&&this.seqEventMap.set(i,h)}const s={cmd:t,seq:i,payload:n};try{this.ws.send(JSON.stringify(s))}catch{break}}if(this.seqEventMap.size>200){const t=[...this.seqEventMap.entries()].sort((n,i)=>n[0]-i[0]);this.seqEventMap.clear();for(const[n,i]of t.slice(-100))this.seqEventMap.set(n,i)}}handleInvokeResult(e){const t=this.pendingInvokes.get(e.invoke_id);t&&(this.pendingInvokes.delete(e.invoke_id),clearTimeout(t.timer),e.code===0?t.resolve(e.data??null):t.reject(new Error(`agent_invoke error code=${e.code}: ${e.msg??""}`)))}rejectAllPendingInvokes(e){for(const[,t]of this.pendingInvokes)clearTimeout(t.timer),t.reject(new Error(`agent_invoke canceled: ${e}`));this.pendingInvokes.clear()}rejectAllPendingRequests(e){for(const[,t]of this.pendingRequests)clearTimeout(t.timer),t.reject(new Error(`request canceled: ${e}`));this.pendingRequests.clear()}cleanupSocket(){if(this.ws)try{this.ws.close()}catch{}}startHeartbeat(){this.stopHeartbeat(),this.heartbeatTimer=setInterval(()=>{this.connected&&this.request("ping",{ts:Date.now()},{expected:["pong"],timeoutMs:5e3}).catch(()=>{this.connected&&(this.cleanupSocket(),this.attemptReconnect())})},this.heartbeatSec*1e3)}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}}export{f as AibotClient};
@@ -1,2 +1,2 @@
1
- import{execFileSync as p}from"node:child_process";import{existsSync as s,readFileSync as m,renameSync as _,unlinkSync as S,writeFileSync as E}from"node:fs";import{join as u}from"node:path";import{GRIX_PATHS as f}from"../log/index.js";import{appendRotatingFileSync as N}from"../log/rotation.js";import{resolveClientVersion as I}from"../util/client-version.js";import{npmInstallWithMirror as w}from"../installer/npm-registry.js";class i extends Error{code;constructor(r,n){super(n),this.name="UpgradeError",this.code=r}}function d(){return u(f.log,"upgrade.log")}function a(){return u(f.data,"upgrade-pending.json")}function P(){return s(a())}function k(){const e=a();if(!s(e))return null;try{return JSON.parse(m(e,"utf-8"))}catch{return null}}function T(e,r){const n={from_version:e,target_version:r,upgraded_at:new Date().toISOString(),crash_count:0},c=a(),o=c+".tmp";E(o,JSON.stringify(n),"utf-8"),_(o,c)}function $(){const e=a();if(s(e))try{S(e)}catch{}}function l(e){const r=`[${new Date().toISOString()}] ${e}
2
- `;try{N(d(),r)}catch{}}function O(e=4096){const r=d();if(!s(r))return"";try{const n=m(r,"utf-8");return n.length<=e?n:n.slice(-e)}catch{return""}}function g(){try{const e=process.platform==="win32";return p(e?"cmd.exe":"npm",e?["/c","npm","--version"]:["--version"],{encoding:"utf-8",timeout:1e4}).trim()}catch{throw new i("NPM_NOT_FOUND","npm is not available or timed out")}}function h(){let e;try{const r=process.platform==="win32";e=p(r?"cmd.exe":"npm",r?["/c","npm","prefix","-g"]:["prefix","-g"],{encoding:"utf-8",timeout:1e4}).trim()}catch{return Number.MAX_SAFE_INTEGER}return Number.MAX_SAFE_INTEGER}function D(){let e;try{e=g()}catch(n){return{ok:!1,errorCode:"NPM_NOT_FOUND",errorMsg:n instanceof Error?n.message:"npm not available"}}const r=h();return r<100?{ok:!1,errorCode:"DISK_FULL",errorMsg:`Only ${r}MB free disk space (need >= 100MB)`,npmVersion:e,diskFreeMb:r}:{ok:!0,npmVersion:e,diskFreeMb:r}}function b(){let e="";try{e=g()}catch{}let r=Number.MAX_SAFE_INTEGER;try{r=h()}catch{}return{npm_version:e,node_version:process.version,disk_free_mb:r,platform:process.platform,arch:process.arch}}async function U(e,r,n=12e4){const c=`${e}@${r}`;l(`npm install -g ${c} starting (with mirror fallback)`);try{const{registry:o}=await w(c,n,10485760);l(`npm install succeeded via ${o}`)}catch(o){const t=o instanceof Error?o.message:String(o);throw l(`npm install failed: ${t}`),/timed out|ETIMEDOUT/i.test(t)?new i("NPM_TIMEOUT",`npm install timed out after ${n/1e3}s (tried all mirrors): ${t}`):t.includes("EACCES")||t.includes("permission denied")?new i("NPM_INSTALL_FAILED",`Permission denied: ${t}`):t.includes("ENOSPC")||t.includes("no space left")?new i("DISK_FULL",`Disk full: ${t}`):t.includes("404")||t.includes("not found")?new i("NPM_INSTALL_FAILED",`Package not found: ${t}`):new i("NPM_INSTALL_FAILED",t)}}function C(e){const r=I();if(r!==e)throw new i("VERSION_MISMATCH",`Installed version ${r} does not match expected ${e}`);return r}export{i as UpgradeError,h as checkDiskSpace,g as checkNpmAvailable,b as collectEnvInfo,O as getUpgradeLogTail,U as npmInstall,P as pendingExists,D as preflightCheck,k as readPending,$ as removePending,l as upgradeLog,C as verifyInstalledVersion,T as writePending};
1
+ import{execFileSync as m}from"node:child_process";import{existsSync as c,readFileSync as p,renameSync as _,unlinkSync as S,writeFileSync as E}from"node:fs";import{join as f}from"node:path";import{GRIX_PATHS as u}from"../log/index.js";import{appendRotatingFileSync as I}from"../log/rotation.js";import{resolveClientVersion as N}from"../util/client-version.js";import{getMachineName as w}from"../util/host-info.js";import{resolveInstallId as x}from"../util/install-id.js";import{npmInstallWithMirror as y}from"../installer/npm-registry.js";class i extends Error{code;constructor(r,n){super(n),this.name="UpgradeError",this.code=r}}function d(){return f(u.log,"upgrade.log")}function a(){return f(u.data,"upgrade-pending.json")}function O(){return c(a())}function D(){const e=a();if(!c(e))return null;try{return JSON.parse(p(e,"utf-8"))}catch{return null}}function b(e,r){const n={from_version:e,target_version:r,upgraded_at:new Date().toISOString(),crash_count:0},s=a(),o=s+".tmp";E(o,JSON.stringify(n),"utf-8"),_(o,s)}function U(){const e=a();if(c(e))try{S(e)}catch{}}function l(e){const r=`[${new Date().toISOString()}] ${e}
2
+ `;try{I(d(),r)}catch{}}function C(e=4096){const r=d();if(!c(r))return"";try{const n=p(r,"utf-8");return n.length<=e?n:n.slice(-e)}catch{return""}}function g(){try{const e=process.platform==="win32";return m(e?"cmd.exe":"npm",e?["/c","npm","--version"]:["--version"],{encoding:"utf-8",timeout:1e4}).trim()}catch{throw new i("NPM_NOT_FOUND","npm is not available or timed out")}}function h(){let e;try{const r=process.platform==="win32";e=m(r?"cmd.exe":"npm",r?["/c","npm","prefix","-g"]:["prefix","-g"],{encoding:"utf-8",timeout:1e4}).trim()}catch{return Number.MAX_SAFE_INTEGER}return Number.MAX_SAFE_INTEGER}function R(){let e;try{e=g()}catch(n){return{ok:!1,errorCode:"NPM_NOT_FOUND",errorMsg:n instanceof Error?n.message:"npm not available"}}const r=h();return r<100?{ok:!1,errorCode:"DISK_FULL",errorMsg:`Only ${r}MB free disk space (need >= 100MB)`,npmVersion:e,diskFreeMb:r}:{ok:!0,npmVersion:e,diskFreeMb:r}}function G(){let e="";try{e=g()}catch{}let r=Number.MAX_SAFE_INTEGER;try{r=h()}catch{}return{npm_version:e,node_version:process.version,disk_free_mb:r,platform:process.platform,arch:process.arch,host_name:w(),install_id:x()}}async function V(e,r,n=12e4){const s=`${e}@${r}`;l(`npm install -g ${s} starting (with mirror fallback)`);try{const{registry:o}=await y(s,n,10485760);l(`npm install succeeded via ${o}`)}catch(o){const t=o instanceof Error?o.message:String(o);throw l(`npm install failed: ${t}`),/timed out|ETIMEDOUT/i.test(t)?new i("NPM_TIMEOUT",`npm install timed out after ${n/1e3}s (tried all mirrors): ${t}`):t.includes("EACCES")||t.includes("permission denied")?new i("NPM_INSTALL_FAILED",`Permission denied: ${t}`):t.includes("ENOSPC")||t.includes("no space left")?new i("DISK_FULL",`Disk full: ${t}`):t.includes("404")||t.includes("not found")?new i("NPM_INSTALL_FAILED",`Package not found: ${t}`):new i("NPM_INSTALL_FAILED",t)}}function X(e){const r=N();if(r!==e)throw new i("VERSION_MISMATCH",`Installed version ${r} does not match expected ${e}`);return r}export{i as UpgradeError,h as checkDiskSpace,g as checkNpmAvailable,G as collectEnvInfo,C as getUpgradeLogTail,V as npmInstall,O as pendingExists,R as preflightCheck,D as readPending,U as removePending,l as upgradeLog,X as verifyInstalledVersion,b as writePending};
@@ -1 +1 @@
1
- import{existsSync as h,mkdirSync as U,readFileSync as b,renameSync as I,writeFileSync as P}from"node:fs";import{join as m,dirname as $}from"node:path";import{spawn as E}from"node:child_process";import{log as n}from"../log/index.js";import{GRIX_PATHS as _}from"../log/index.js";import{resolveClientVersion as u}from"../util/client-version.js";import{UpgradeError as M,collectEnvInfo as D,getUpgradeLogTail as L,npmInstall as O,pendingExists as y,preflightCheck as N,readPending as R,removePending as f,upgradeLog as a,verifyInstalledVersion as F,writePending as x}from"./npm-upgrader.js";const B=360*60*1e3,G=300*1e3,v=1800*1e3,V=2,j=3,H="grix-connector",w=1e4;function S(o){return o.replace(/^wss:/,"https:").replace(/^ws:/,"http:")}function k(){return m(_.data,"upgrade-state.json")}function T(){const o=k();if(!h(o))return{daily_attempts:{},version_attempts:{}};try{return JSON.parse(b(o,"utf-8"))}catch{return{daily_attempts:{},version_attempts:{}}}}function K(o){const t=k();U($(t),{recursive:!0});const e=t+".tmp";P(e,JSON.stringify(o),"utf-8"),I(e,t)}function A(){return new Date().toISOString().slice(0,10)}class Z{agentConfigs;isBusy;timer=null;initialTimer=null;running=!1;stopped=!1;constructor(t,e){this.agentConfigs=t,this.isBusy=e}async start(){await this.handlePendingOnStartup(),this.initialTimer=setTimeout(()=>{this.stopped||(this.runCheck(),!this.stopped&&(this.timer=setInterval(()=>this.runCheck(),B)))},G)}stop(){this.stopped=!0,this.initialTimer&&(clearTimeout(this.initialTimer),this.initialTimer=null),this.timer&&(clearInterval(this.timer),this.timer=null)}triggerCheck(){this.stopped||this.runCheck()}async checkForUpdate(){const t=this.agentConfigs[0];return t?this.queryUpgrade(t):{available:!1}}async handlePendingOnStartup(){if(!y())return;const t=R();if(!t){f();return}const e=u();e===t.target_version?(a(`daemon startup: version matches target ${t.target_version}, upgrade succeeded`),await this.reportUpgrade({from_version:t.from_version,to_version:t.target_version,status:"success"})):(a(`daemon startup: version ${e} != target ${t.target_version}, rolled back`),await this.reportUpgrade({from_version:t.from_version,to_version:t.target_version,status:"rolled_back",error_code:"STARTUP_CRASH",crash_count:t.crash_count})),f()}async runCheck(){if(!this.running){this.running=!0;try{await this.check()}catch(t){n.error("upgrade",`Check failed: ${t instanceof Error?t.message:t}`)}finally{this.running=!1}}}async check(){if(y())return;const t=this.agentConfigs[0];if(!t)return;const e=await this.queryUpgrade(t);if(!e.available||!e.release)return;const r=e.release.version,i=u();if(!e.release.force&&!this.checkRateLimit(r))return;const l=N();if(!l.ok){await this.reportUpgrade({from_version:i,to_version:r,status:"failed",error_code:l.errorCode,error_msg:l.errorMsg});return}a(`upgrade start: ${i} -> ${r}`);const g=Date.now();try{if(x(i,r),await O(H,r),F(r),this.startGuardian(),a("npm install verified, reporting installed"),await this.reportUpgrade({from_version:i,to_version:r,status:"installed",duration_ms:Date.now()-g}),a("shutting down for restart"),this.isBusy?.()){a("active tasks detected, waiting for completion before restart");const c=3600*1e3,d=5e3,p=Date.now()+c;for(;this.isBusy()&&Date.now()<p&&!this.stopped;)await new Promise(C=>setTimeout(C,d));if(this.stopped){a("upgrade aborted: checker stopped during wait");return}this.isBusy()?a("active tasks still running after 1h max wait, forcing restart"):a("all tasks completed, proceeding with restart")}process.kill(process.pid,"SIGTERM")}catch(c){const d=c instanceof M?c.code:"NPM_INSTALL_FAILED",p=c instanceof Error?c.message:String(c);a(`upgrade failed: ${d} ${p}`),f(),this.recordFailure(r),await this.reportUpgrade({from_version:i,to_version:r,status:"failed",error_code:d,error_msg:p,duration_ms:Date.now()-g,upgrade_log:L()})}}checkRateLimit(t){const e=T();if(e.last_failure_at){const l=Date.now()-new Date(e.last_failure_at).getTime();if(l<v)return n.info("upgrade",`In cooldown, ${(v-l)/6e4}m remaining`),!1}const r=e.version_attempts[t]??0;if(r>=j)return n.info("upgrade",`Version ${t} already tried ${r} times, skipping`),!1;const i=A(),s=e.daily_attempts[i]??0;return s>=V?(n.info("upgrade",`Already ${s} attempts today, skipping`),!1):!0}recordFailure(t){const e=T(),r=A();e.last_failure_at=new Date().toISOString(),e.last_failure_version=t,e.daily_attempts[r]=(e.daily_attempts[r]??0)+1,e.version_attempts[t]=(e.version_attempts[t]??0)+1,K(e)}async queryUpgrade(t){const r=`${S(t.wsUrl)}/v1/agent-api/upgrade/check?`+new URLSearchParams({client_type:"grix-connector",client_version:u(),channel:t.channel??"stable",platform:process.platform,arch:process.arch}).toString();try{const i=await fetch(r,{headers:{Authorization:`Bearer ${t.apiKey}`},signal:AbortSignal.timeout(w)});if(!i.ok)return n.warn("upgrade",`Check API returned ${i.status}`),{available:!1};const s=await i.json();return s.code!==0?{available:!1}:s.data}catch(i){return n.warn("upgrade",`Check API error: ${i instanceof Error?i.message:i}`),{available:!1}}}async reportUpgrade(t){const e=this.agentConfigs[0];if(!e)return;const i=`${S(e.wsUrl)}/v1/agent-api/upgrade/report`,s=t.npm_version?t:{...t,...D()};try{await fetch(i,{method:"POST",headers:{Authorization:`Bearer ${e.apiKey}`,"Content-Type":"application/json"},body:JSON.stringify(s),signal:AbortSignal.timeout(w)})}catch{}}startGuardian(){const t=m(_.base,"bin","upgrade-guardian.sh");if(process.platform==="win32"||!h(t)){n.info("upgrade","Guardian not available on this platform, skipping");return}try{const e=E(t,[],{detached:!0,stdio:"ignore"});e.unref(),a(`guardian started (pid ${e.pid})`)}catch(e){n.warn("upgrade",`Failed to start guardian: ${e instanceof Error?e.message:e}`)}}}export{Z as UpgradeChecker};
1
+ import{existsSync as m,mkdirSync as P,readFileSync as U,renameSync as I,writeFileSync as $}from"node:fs";import{join as _,dirname as E}from"node:path";import{spawn as M}from"node:child_process";import{log as o}from"../log/index.js";import{GRIX_PATHS as y}from"../log/index.js";import{resolveClientVersion as g}from"../util/client-version.js";import{UpgradeError as D,collectEnvInfo as L,getUpgradeLogTail as O,npmInstall as N,pendingExists as v,preflightCheck as R,readPending as F,removePending as f,upgradeLog as n,verifyInstalledVersion as x,writePending as B}from"./npm-upgrader.js";const G=360*60*1e3,V=300*1e3,w=1800*1e3,j=2,H=3,K="grix-connector",S=1e4;function k(c){return c.replace(/^wss:/,"https:").replace(/^ws:/,"http:")}function T(){return _(y.data,"upgrade-state.json")}function A(){const c=T();if(!m(c))return{daily_attempts:{},version_attempts:{}};try{return JSON.parse(U(c,"utf-8"))}catch{return{daily_attempts:{},version_attempts:{}}}}function q(c){const t=T();P(E(t),{recursive:!0});const e=t+".tmp";$(e,JSON.stringify(c),"utf-8"),I(e,t)}function C(){return new Date().toISOString().slice(0,10)}class tt{agentConfigs;isBusy;timer=null;initialTimer=null;running=!1;stopped=!1;constructor(t,e){this.agentConfigs=t,this.isBusy=e}async start(){await this.handlePendingOnStartup(),this.initialTimer=setTimeout(()=>{this.stopped||(this.runCheck(),!this.stopped&&(this.timer=setInterval(()=>this.runCheck(),G)))},V)}stop(){this.stopped=!0,this.initialTimer&&(clearTimeout(this.initialTimer),this.initialTimer=null),this.timer&&(clearInterval(this.timer),this.timer=null)}triggerCheck(){this.stopped||this.runCheck()}async checkForUpdate(){return this.agentConfigs.length===0?{available:!1}:(await Promise.all(this.agentConfigs.map(a=>this.queryUpgrade(a)))).find(a=>a.available&&a.release)??{available:!1}}async handlePendingOnStartup(){if(!v())return;const t=F();if(!t){f();return}const e=g();e===t.target_version?(n(`daemon startup: version matches target ${t.target_version}, upgrade succeeded`),await this.reportUpgrade({from_version:t.from_version,to_version:t.target_version,status:"success"})):(n(`daemon startup: version ${e} != target ${t.target_version}, rolled back`),await this.reportUpgrade({from_version:t.from_version,to_version:t.target_version,status:"rolled_back",error_code:"STARTUP_CRASH",crash_count:t.crash_count})),f()}async runCheck(){if(!this.running){this.running=!0;try{await this.check()}catch(t){o.error("upgrade",`Check failed: ${t instanceof Error?t.message:t}`)}finally{this.running=!1}}}async check(){if(v()||this.agentConfigs.length===0)return;const a=(await Promise.all(this.agentConfigs.map(s=>this.queryUpgrade(s)))).find(s=>s.available&&s.release)?.release;if(!a)return;const r=a.version,i=g();if(!a.force&&!this.checkRateLimit(r))return;const d=R();if(!d.ok){await this.reportUpgrade({from_version:i,to_version:r,status:"failed",error_code:d.errorCode,error_msg:d.errorMsg});return}n(`upgrade start: ${i} -> ${r}`);const h=Date.now();try{if(B(i,r),await N(K,r),x(r),this.startGuardian(),n("npm install verified, reporting installed"),await this.reportUpgrade({from_version:i,to_version:r,status:"installed",duration_ms:Date.now()-h}),n("shutting down for restart"),this.isBusy?.()){n("active tasks detected, waiting for completion before restart");const s=3600*1e3,l=5e3,p=Date.now()+s;for(;this.isBusy()&&Date.now()<p&&!this.stopped;)await new Promise(b=>setTimeout(b,l));if(this.stopped){n("upgrade aborted: checker stopped during wait");return}this.isBusy()?n("active tasks still running after 1h max wait, forcing restart"):n("all tasks completed, proceeding with restart")}process.kill(process.pid,"SIGTERM")}catch(s){const l=s instanceof D?s.code:"NPM_INSTALL_FAILED",p=s instanceof Error?s.message:String(s);n(`upgrade failed: ${l} ${p}`),f(),this.recordFailure(r),await this.reportUpgrade({from_version:i,to_version:r,status:"failed",error_code:l,error_msg:p,duration_ms:Date.now()-h,upgrade_log:O()})}}checkRateLimit(t){const e=A();if(e.last_failure_at){const u=Date.now()-new Date(e.last_failure_at).getTime();if(u<w)return o.info("upgrade",`In cooldown, ${(w-u)/6e4}m remaining`),!1}const a=e.version_attempts[t]??0;if(a>=H)return o.info("upgrade",`Version ${t} already tried ${a} times, skipping`),!1;const r=C(),i=e.daily_attempts[r]??0;return i>=j?(o.info("upgrade",`Already ${i} attempts today, skipping`),!1):!0}recordFailure(t){const e=A(),a=C();e.last_failure_at=new Date().toISOString(),e.last_failure_version=t,e.daily_attempts[a]=(e.daily_attempts[a]??0)+1,e.version_attempts[t]=(e.version_attempts[t]??0)+1,q(e)}async queryUpgrade(t){const a=`${k(t.wsUrl)}/v1/agent-api/upgrade/check?`+new URLSearchParams({client_type:"grix-connector",client_version:g(),channel:t.channel??"stable",platform:process.platform,arch:process.arch}).toString();try{const r=await fetch(a,{headers:{Authorization:`Bearer ${t.apiKey}`},signal:AbortSignal.timeout(S)});if(!r.ok)return o.warn("upgrade",`Check API returned ${r.status}`),{available:!1};const i=await r.json();return i.code!==0?{available:!1}:i.data}catch(r){return o.warn("upgrade",`Check API error: ${r instanceof Error?r.message:r}`),{available:!1}}}async reportUpgrade(t){if(this.agentConfigs.length===0)return;const e=t.npm_version?t:{...t,...L()};await Promise.all(this.agentConfigs.map(async a=>{const i=`${k(a.wsUrl)}/v1/agent-api/upgrade/report`;try{await fetch(i,{method:"POST",headers:{Authorization:`Bearer ${a.apiKey}`,"Content-Type":"application/json"},body:JSON.stringify(e),signal:AbortSignal.timeout(S)})}catch{}}))}startGuardian(){const t=_(y.base,"bin","upgrade-guardian.sh");if(process.platform==="win32"||!m(t)){o.info("upgrade","Guardian not available on this platform, skipping");return}try{const e=M(t,[],{detached:!0,stdio:"ignore"});e.unref(),n(`guardian started (pid ${e.pid})`)}catch(e){o.warn("upgrade",`Failed to start guardian: ${e instanceof Error?e.message:e}`)}}}export{tt as UpgradeChecker};
@@ -0,0 +1 @@
1
+ import{existsSync as e,mkdirSync as c,readFileSync as o,renameSync as m,writeFileSync as a}from"node:fs";import{dirname as l,join as f}from"node:path";import{randomUUID as s}from"node:crypto";import{GRIX_PATHS as u}from"../log/index.js";function d(){return f(u.data,"install-id")}let t=null;function h(){if(t)return t;const r=d();if(e(r))try{const n=o(r,"utf-8").trim();if(n)return t=n,t}catch{}const i=s();try{c(l(r),{recursive:!0});const n=r+".tmp";a(n,i,"utf-8"),m(n,r)}catch{}return t=i,t}export{h as resolveInstallId};
@@ -1 +1 @@
1
- function l(t,e){return`${t}#shared:${e}`}function o(t,e){if(!t)return t;const i=t.lastIndexOf("."),r=Math.max(t.lastIndexOf("/"),t.lastIndexOf("\\"));return i>r?`${t.slice(0,i)}.shared.${e}${t.slice(i)}`:`${t}.shared.${e}`}function h(t,e){return{...t,name:l(t.name,e),aibot:{...t.aibot,sharedOwnerId:e},eventResultsPath:o(t.eventResultsPath,e),bindingsPath:o(t.bindingsPath,e),activeEventStorePath:o(t.activeEventStorePath,e),logDir:t.logDir?`${t.logDir}/shared-${e}`:void 0,allowlistPath:void 0}}function d(t,e,i){const r=new Set(i.map(n=>String(n).trim()).filter(n=>n.length>0)),s=[...e],a=`${t}#shared:`,f=[...r].filter(n=>!s.includes(l(t,n))),c=s.filter(n=>n.startsWith(a)).map(n=>({key:n,sharedOwnerId:n.slice(a.length)})).filter(n=>!r.has(n.sharedOwnerId));return{toAdd:f,toRemove:c}}export{h as buildSharedInstanceConfig,d as diffSharedOwners,l as sharedInstanceKey,o as suffixSharedPath};
1
+ import{readFileSync as f,writeFileSync as d,unlinkSync as u}from"node:fs";import{join as S}from"node:path";function l(e,t){return`${e}#shared:${t}`}function o(e,t){if(!e)return e;const r=e.lastIndexOf("."),i=Math.max(e.lastIndexOf("/"),e.lastIndexOf("\\"));return r>i?`${e.slice(0,r)}.shared.${t}${e.slice(r)}`:`${e}.shared.${t}`}function y(e,t){return{...e,name:l(e.name,t),aibot:{...e.aibot,sharedOwnerId:t},eventResultsPath:o(e.eventResultsPath,t),bindingsPath:o(e.bindingsPath,t),activeEventStorePath:o(e.activeEventStorePath,t),logDir:e.logDir?`${e.logDir}/shared-${t}`:void 0,allowlistPath:void 0,connectMaxRetries:e.connectMaxRetries??3}}function P(e,t,r){const i=new Set(r.map(n=>String(n).trim()).filter(n=>n.length>0)),s=[...t],a=`${e}#shared:`,c=[...i].filter(n=>!s.includes(l(e,n))),h=s.filter(n=>n.startsWith(a)).map(n=>({key:n,sharedOwnerId:n.slice(a.length)})).filter(n=>!i.has(n.sharedOwnerId));return{toAdd:c,toRemove:h}}class ${dataDir;constructor(t){this.dataDir=t}filePath(t){const r=t.replace(/[^a-zA-Z0-9_-]/g,"_");return S(this.dataDir,`shared-owners-${r}.json`)}load(t){try{const r=f(this.filePath(t),"utf-8"),i=JSON.parse(r);if(Array.isArray(i))return i.filter(s=>typeof s=="string"&&s.length>0)}catch{}return[]}save(t,r){const i=[...new Set(r.filter(s=>s.length>0))];if(i.length===0){this.delete(t);return}try{d(this.filePath(t),JSON.stringify(i),"utf-8")}catch{}}remove(t,r){const i=this.load(t).filter(s=>s!==r);i.length>0?this.save(t,i):this.delete(t)}delete(t){try{u(this.filePath(t))}catch{}}}export{$ as SharedOwnersCache,y as buildSharedInstanceConfig,P as diffSharedOwners,l as sharedInstanceKey,o as suffixSharedPath};
package/dist/manager.js CHANGED
@@ -1,2 +1,2 @@
1
- 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)+`
2
- `,"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};
1
+ 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,SharedOwnersCache as G}from"./manager-share-config.js";import{GRIX_PATHS as S,log as d}from"./core/log/index.js";import{resolveClientVersion as K}from"./core/util/client-version.js";import{UpgradeChecker as W}from"./core/upgrade/upgrade-checker.js";import{AgentGlobalConfigStore as V}from"./core/persistence/agent-global-config-store.js";import{scanSkills as J,dedupeSkills as X}from"./adapter/claude/skill-scanner.js";import{scanDefaultSkills as Y,logDefaultSkillsCheck as Z,cleanupProjectedSkills as D}from"./default-skills/index.js";import{resolveCopilotCommand as q}from"./core/runtime/copilot-resolve.js";import{getCliVersion as ee,resolveCliPath as te}from"./core/util/cli-probe.js";import{AgentInstaller as ne}from"./core/installer/installer.js";import{reportInstallFailure as ae}from"./core/observability/sentry.js";const se=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 re(){return Promise.all(oe().map(async n=>{const e=await te(n.command);if(!e)return{client_type:n.clientType,command:n.command,installed:!1,path:null,version:null};const t=await ee(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 ce(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 le(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 de(...n){const e=[],t=new Set;for(const s of n)for(const a of s??[]){const o=String(a??"").trim(),r=o.toLowerCase();!o||t.has(r)||(t.add(r),e.push(o))}return e.length>0?e:void 0}function ue(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}},s=t[n]??t.acp;return{maxConcurrent:e?.max_concurrent??s.maxConcurrent??1,maxQueued:e?.max_queued??s.maxQueued??3,queueTimeoutMs:e?.queue_timeout_ms??s.queueTimeoutMs??0,cancelableQueued:!0,cancelableRunning:!0}}function O(n){const e=K(),t=String(n.client_type??"").trim().toLowerCase(),s=ie(t),a=String(n.ws_url??"").trim(),o="get_session_usage",r="get_rate_limits",i="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 l=s.adapterType,u=l==="acp",f=t==="qwen",m={...s.options??{}},c=l==="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",o,r]}:null,p=l==="claude"?{localActions:["session_control","set_mode","set_model","claude_interaction_reply","exec_approve","exec_reject","file_list","create_folder","thread_compact",o,r]}: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",o],adapterHint:"qwen/base"}:null,_=l==="pi"?{adapterHint:"pi/base",capabilities:["local_action_v1"],localActions:["session_control","set_model","get_context","file_list","create_folder",o]}:null,h=l==="openhuman"?{adapterHint:"openhuman/base",capabilities:["local_action_v1"],localActions:["session_control","set_model","file_list","create_folder",o]}:null,w=l==="cursor"?{adapterHint:"cursor/base",capabilities:["stream_chunk","local_action_v1"],localActions:["session_control","set_model","set_mode","get_context","file_list","create_folder",o,r]}:null,y=l==="codewhale"?{capabilities:["stream_chunk","local_action_v1"],localActions:["session_control","set_model","file_list","create_folder",o]}:null,k=l==="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",o]}:null,T=l==="agy"?{adapterHint:"agy/base",capabilities:["stream_chunk","local_action_v1"],localActions:["session_control","set_model","file_list","create_folder",o]}:null,P=u&&!f?{localActions:["exec_approve","exec_reject","permission_approve","permission_reject","session_control","set_model","set_mode","file_list","create_folder",o]}:null,E=t==="kiro"?{localActions:["exec_approve","exec_reject","permission_approve","permission_reject","session_control","set_model","set_mode","file_list","create_folder","thread_compact",o,r]}:null,H=l==="codex"||l==="claude"||t==="gemini"?["session_control","set_model","set_mode"]:void 0,N=[o,r];u&&m.raw_transport===void 0&&(m.raw_transport=t==="gemini");const U=`${t}/base`,$=l==="claude"?"claude":l==="codex"?"codex":l==="pi"?"pi":t==="kiro"?"kiro":"gemini";let b;try{const M=$==="kiro"?void 0:process.cwd();b=J({mode:$,projectDir:M})??void 0,b&&b.length===0&&(b=void 0)}catch{}const I=Y();if(I.length>0){const C=X([...b??[],...I]),j=C.filter(v=>v.source==="connector").map(v=>v.name);j.length>0&&d.info("manager",`[${n.name}] injecting connector skills: [${j.join(", ")}]`),b=C.length>0?C:void 0}return{name:n.name,adapterType:l,aibot:{url:a,agentId:n.agent_id,apiKey:n.api_key,clientType:t,clientVersion:e,adapterHint:s.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:de(c?.localActions??y?.localActions??p?.localActions??_?.localActions??h?.localActions??w?.localActions??k?.localActions??T?.localActions??g?.localActions??E?.localActions??P?.localActions??["exec_approve","exec_reject"],H,N,["connector_rollback","connector_upgrade_push",i]),skills:b},agent:{command:s.command,args:[...s.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:ce(n.name),activeEventStorePath:le(n.name),...s.enableSessionBinding||u?{enableSessionBinding:!0}:{},...s.autoInjectArgs?{autoInjectArgs:s.autoInjectArgs}:{},poolMaxSize:n.pool?.maxSize,poolIdleTimeoutMs:n.pool?.idleTimeoutMs,eventQueue:ue(l,n.event_queue),logDir:S.log,providerBaseUrl:n.provider_base_url?.trim()||void 0,providerApiKey:n.provider_api_key?.trim()||void 0}}function pe(){const n=process.env.GRIX_AGENT_STARTUP_WAIT_MS,e=Number(n);return Number.isFinite(e)&&e>=500?Math.floor(e):se}class Ie{instances=[];configMap=new Map;sharedInstances=new Map;shareSyncChains=new Map;stopping=!1;upgradeChecker=null;globalConfigStore;configDir=S.config;sharedOwnersCache=new G(S.data);installer=new ne;async start(e){const t=e??S.config;this.configDir=t,d.info("manager",`Loading configs from ${t}`),Z(),D(),this.globalConfigStore=new V(x(S.data,"agent-global-configs.json")),this.globalConfigStore.load();const s=F(t).filter(i=>i.endsWith(".json")).sort();if(s.length===0)throw new Error(`No config files found in ${t}`);const a=[];let o=0;for(const i of s)try{const l=Q(x(t,i),"utf-8"),u=JSON.parse(l);if(Array.isArray(u.agents)){if(u.agents.length===0){d.error("manager",`No agents array found in ${i}`),o++;continue}for(const f of u.agents)try{const m=O(f);a.push({config:m,file:i}),d.info("manager",`Loaded ${m.name} (${m.adapterType??"acp"}) from ${i}`)}catch(m){const c=typeof f?.name=="string"?f.name:"<unknown>";d.error("manager",`Invalid agent config in ${i} (name=${c}): ${m}`),o++}}else d.error("manager",`Unrecognized config format in ${i}`)}catch(l){d.error("manager",`Failed to load ${i}: ${l}`)}let r=0;if(a.length>0){const i=pe();d.info("manager",`Starting ${a.length} agent(s), startup wait=${i}ms`);const l=()=>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(l),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"}))},i);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"){this.restoreCachedSharedInstances(c.config);continue}if(p.kind==="failed"){u(c.instance),d.error("manager",`Failed to start ${c.config.name}: ${p.error}`);continue}r++,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}`),this.restoreCachedSharedInstances(c.config)}).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-r);d.info("manager",`${c}/${a.length} agent(s) running now`)}r>0&&d.warn("manager",`${r} agent(s) still connecting in background`)}if(this.instances.length===0&&a.length>0)throw new Error("All agent configurations failed to start");a.length>0&&(this.upgradeChecker=new W(a.map(({config:i})=>({apiKey:i.aibot.apiKey,wsUrl:i.aibot.url})),()=>this.instances.some(i=>i.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){this.sharedOwnersCache.save(e.name,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:s,toRemove:a}=R(e.name,this.sharedInstances.keys(),t);for(const o of s){if(this.stopping)break;const r=z(e.name,o);try{const i=L(e,o),l=new A(i,this.globalConfigStore);this.sharedInstances.set(r,l),await l.start(),d.info("manager",`shared instance started: ${r}`)}catch(i){this.sharedInstances.delete(r);const l=i instanceof Error?i.message:String(i);/Auth failed/i.test(l)?(this.sharedOwnersCache.remove(e.name,o),d.warn("manager",`shared instance auth rejected, removed from cache: ${r}`)):d.error("manager",`start shared instance failed ${r}: ${i}`)}}for(const{key:o}of a){const r=this.sharedInstances.get(o);r&&(this.sharedInstances.delete(o),r.stop().catch(i=>d.error("manager",`stop shared instance failed ${o}: ${i}`)),d.info("manager",`shared instance stopped: ${o}`))}}restoreCachedSharedInstances(e){const t=this.sharedOwnersCache.load(e.name);t.length!==0&&(d.info("manager",`restoring ${t.length} cached shared instance(s) for ${e.name}`),this.onShareSet(e,t))}getAgentsStatus(){return this.instances.map(e=>e.getStatus())}async addAgent(e){const t=O(e);if(this.instances.some(a=>a.name===t.name))throw new Error(`Agent "${t.name}" already exists`);const s=new A(t,this.globalConfigStore);s.setUpgradeTrigger(()=>this.upgradeChecker?.triggerCheck()),s.setShareSetHandler(a=>this.onShareSet(t,a)),await s.start(),this.instances.push(s),this.configMap.set(t.name,t),this.persistAgentsConfig(),d.info("manager",`Added agent: ${t.name}`)}async removeAgent(e){const t=this.instances.findIndex(r=>r.name===e);if(t===-1)throw Object.assign(new Error(`Agent "${e}" not found`),{code:"NOT_FOUND"});const s=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 o=`${e}#shared:`;for(const[r,i]of[...this.sharedInstances.entries()])r.startsWith(o)&&(this.sharedInstances.delete(r),i.stop().catch(l=>d.error("manager",`stop shared instance failed ${r}: ${l}`)));this.sharedOwnersCache.delete(e),await s.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)+`
2
+ `,"utf-8")}catch(t){d.error("manager",`Failed to persist agents config: ${t}`)}}async restartAgent(e){const t=this.instances.findIndex(r=>r.name===e);if(t===-1)throw Object.assign(new Error(`Agent "${e}" not found`),{code:"NOT_FOUND"});const s=this.configMap.get(e);if(!s)throw Object.assign(new Error(`Config for "${e}" not found`),{code:"NOT_FOUND"});await this.instances[t].stop();const o=new A(s,this.globalConfigStore);o.setUpgradeTrigger(()=>this.upgradeChecker?.triggerCheck()),await o.start(),this.instances[t]=o,d.info("manager",`Restarted agent: ${e}`)}async checkUpgrade(){return this.upgradeChecker?this.upgradeChecker.checkForUpdate():{available:!1}}triggerUpgrade(){this.upgradeChecker?.triggerCheck()}async probeAll(e={}){return me(this.instances,e)}async probeOne(e,t={}){return ge(this.instances,e,t)}listInstallable(){return this.installer.listInstallable()}async installAgent(e){const t=await this.installer.install(e);return ae(t),t}getInstallProgress(e){return this.installer.getProgress(e)??null}}async function me(n,e){const t=e.concurrency??4,s=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 o=a.filter(u=>u.status==="healthy").length,r=a.filter(u=>u.status==="degraded").length,i=a.filter(u=>u.status==="unavailable").length,l=await re();return{ok:o===a.length&&a.length>0,total:a.length,healthy:o,degraded:r,unavailable:i,installed_clients:l,agents:a,probed_at:s,duration_ms:Date.now()-s}}async function ge(n,e,t){const s=n.find(a=>a.name===e);if(!s)throw Object.assign(new Error(`Agent "${e}" not found`),{code:"NOT_FOUND"});return s.probe(t)}export{Ie as Manager,re as probeInstalledClientCommands,me as probeInstances,ge as probeOneInstance};
@@ -1 +1 @@
1
- function a(t){const o=new Set([`http://127.0.0.1:${t.serverPort}`,`http://localhost:${t.serverPort}`,...t.allowedOrigins]),e=new Set([`127.0.0.1:${t.serverPort}`,`localhost:${t.serverPort}`,...t.allowedHosts]);return{validateRequest(s){const r=i(s,o);if(!r.ok)return r;const n=l(s,e);return n.ok?{ok:!0}:n}}}function i(t,o){const e=t.headers.origin;return e?o.has(e)?{ok:!0}:{ok:!1,statusCode:403,message:`Origin not allowed: ${e}`}:{ok:!0}}function l(t,o){const e=t.headers.host;return e?o.has(e)?{ok:!0}:{ok:!1,statusCode:403,message:`Host not allowed: ${e}`}:{ok:!1,statusCode:403,message:"Missing Host header"}}export{a as createSecurityPolicy};
1
+ function a(o){const e=new Set([`http://127.0.0.1:${o.serverPort}`,`http://localhost:${o.serverPort}`,...o.allowedOrigins]),t=new Set([`127.0.0.1:${o.serverPort}`,`localhost:${o.serverPort}`,...o.allowedHosts]);return{validateRequest(s){const r=i(s,e);if(!r.ok)return r;const n=l(s,t);return n.ok?{ok:!0}:n}}}function i(o,e){const t=o.headers.origin;return t?e.has(t)?{ok:!0}:{ok:!1,statusCode:403,message:`Origin not allowed: ${t}`}:{ok:!0}}function l(o,e){const t=o.headers.host;return t?e.has(t)?{ok:!0}:{ok:!1,statusCode:403,message:`Host not allowed: ${t}`}:{ok:!1,statusCode:403,message:"Missing Host header"}}export{a as createSecurityPolicy};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grix-connector",
3
- "version": "2.2.2",
3
+ "version": "2.2.4",
4
4
  "description": "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.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  // install-guardian — cross-platform npm postinstall script
3
- // Copies upgrade-guardian.sh to ~/.grix/bin/ (idempotent, won't overwrite existing)
3
+ // 每次 npm install 都把 upgrade-guardian.sh 强制覆盖到 ~/.grix/bin/,
4
+ // 保证老用户也能拿到新版 guardian(不再以 "用户可能自改" 为由跳过覆盖)。
4
5
  // On Windows, skips since the guardian is a bash script.
5
6
 
6
- import { existsSync, copyFileSync, mkdirSync } from 'node:fs';
7
+ import { existsSync, copyFileSync, chmodSync, mkdirSync } from 'node:fs';
7
8
  import { join, resolve } from 'node:path';
8
9
  import { homedir } from 'node:os';
9
10
 
10
- // Guardian is a bash script — skip on Windows entirely
11
11
  if (process.platform === 'win32') {
12
12
  process.exit(0);
13
13
  }
@@ -15,11 +15,7 @@ if (process.platform === 'win32') {
15
15
  const GRIX_HOME = process.env.GRIX_CONNECTOR_HOME || join(homedir(), '.grix');
16
16
  const GUARDIAN_DEST = join(GRIX_HOME, 'bin', 'upgrade-guardian.sh');
17
17
 
18
- if (existsSync(GUARDIAN_DEST)) {
19
- process.exit(0);
20
- }
21
-
22
- const INIT_CWD = process.env.INIT_CWD || resolve(import.meta.dirname);
18
+ const INIT_CWD = process.env.INIT_CWD || resolve(import.meta.dirname, '..');
23
19
  const GUARDIAN_SRC = join(INIT_CWD, 'scripts', 'upgrade-guardian.sh');
24
20
 
25
21
  if (!existsSync(GUARDIAN_SRC)) {
@@ -28,3 +24,4 @@ if (!existsSync(GUARDIAN_SRC)) {
28
24
 
29
25
  mkdirSync(join(GRIX_HOME, 'bin'), { recursive: true });
30
26
  copyFileSync(GUARDIAN_SRC, GUARDIAN_DEST);
27
+ try { chmodSync(GUARDIAN_DEST, 0o755); } catch { /* best effort */ }
@@ -1,18 +1,13 @@
1
1
  #!/bin/bash
2
2
  # install-guardian.sh — npm postinstall script
3
- # Copies upgrade-guardian.sh to ~/.grix/bin/ (idempotent, won't overwrite existing)
4
- # This ensures the guardian script exists on first install and survives npm upgrades.
3
+ # 每次 npm install 都把 upgrade-guardian.sh 强制覆盖到 ~/.grix/bin/,
4
+ # 保证老用户也能拿到新版 guardian(不再以 "用户可能自改" 为由跳过覆盖)。
5
5
 
6
6
  set -e
7
7
 
8
8
  GRIX_HOME="${GRIX_CONNECTOR_HOME:-$HOME/.grix}"
9
9
  GUARDIAN_DEST="$GRIX_HOME/bin/upgrade-guardian.sh"
10
10
 
11
- # Don't overwrite — user may have custom modifications
12
- if [ -f "$GUARDIAN_DEST" ]; then
13
- exit 0
14
- fi
15
-
16
11
  # Locate source: INIT_CWD is set by npm to the package root during lifecycle scripts
17
12
  GUARDIAN_SRC="${INIT_CWD:-$(dirname "$0")}/scripts/upgrade-guardian.sh"
18
13
  if [ ! -f "$GUARDIAN_SRC" ]; then