grix-connector 2.2.1 → 2.2.3

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.
@@ -13,7 +13,7 @@ Error: Claude usage limit reached -- waiting for reset`,1,!1),this.bridgeCallbac
13
13
  `,p=h(t,"hooks"),S=h(p,"hooks.json");await x(p,{recursive:!0});let k="";try{k=await w(S,"utf8")}catch{}k!==f&&(await C(S,f,"utf8"),a.info("claude-adapter",`Wrote Claude hooks config: ${S}`));const v=h(t,"skills"),b=Qe(v);return b.length>0&&a.info("claude-adapter",`Synced connector skills to plugin: [${b.join(", ")}]`),t}async ensureStdioMcpServer(){const e=this.resolveProjectRoot(),t=this.resolveStdioServerPath(e);if(this.ensureStdioServerArtifact(e,t),!D(t))throw new Error(`MCP stdio server entry point not found: ${t}`);const i=this.getInternalApiUrl(),s=this.notifyPort,n=[t,"--handle-url",i,"--notify-port",String(s)],r=h(e,"dist","mcp","mcp-bridge-server.js"),o=this.internalApi?.mcpBridgeWsUrl,l=D(r)&&o?[r,"--ws-url",o]:void 0;l||a.warn("claude-adapter",`APP MCP bridge skipped (path=${D(r)} ws=${!!o})`);const c=Ct(this.claudeCliSessionId);await It(c,n,l),this.claudeMcpConfigPath=c,this.mcpServerReady=!0,this.startActivityTracking()}resolveProjectRoot(){const e=Ne(import.meta.url);return Le(e,"..","..","..","..")}resolveStdioServerPath(e=this.resolveProjectRoot()){return h(e,"dist","mcp","stdio","server.js")}ensureStdioServerArtifact(e,t){if(D(t)||ge)return;ge=!0;const i=h(e,"node_modules","typescript","bin","tsc"),s=h(e,"tsconfig.json");if(!D(i)||!D(s))return;a.warn("claude-adapter",`MCP stdio server artifact missing, attempting build: ${t}`);const n=Ce(process.execPath,[i,"-p",s],{cwd:e,env:process.env,encoding:"utf8",timeout:6e4});if(n.status!==0){const r=`${n.stderr??""}${n.stdout??""}`.trim();throw new Error(`MCP stdio server build failed: ${r||`exit ${n.status??"unknown"}`}`)}}getInternalApiUrl(){return this.internalApi?this.internalApi.url:process.env.GRIX_CONNECTOR_INTERNAL_API?process.env.GRIX_CONNECTOR_INTERNAL_API:`http://127.0.0.1:${this.internalApiPort}`}internalApiPort=0;notifyPort=0;notifySocket=null;async waitForNotifyPort(e){if(this.notifyPort<=0)return;const t=Date.now();for(;Date.now()-t<e;)try{await new Promise((i,s)=>{const n=V.createConnection({host:"127.0.0.1",port:this.notifyPort},()=>{n.destroy(),i()});n.on("error",s),setTimeout(()=>{n.destroy(),s(new Error("probe timeout"))},2e3)});return}catch{await new Promise(i=>setTimeout(i,500))}throw new Error(`Notify port ${this.notifyPort} not ready within ${e}ms`)}async waitForChannelListening(e){const t=Date.now(),i=8e3;for(;Date.now()-t<e;){if(this.channelGateClosed)throw new Error("channels are not currently available (tengu_harbor gate closed)");if(this.startupChannelListening){const s=this.startupChannelListeningAt||Date.now(),n=Date.now()-s;if(n>=G)return;await new Promise(r=>setTimeout(r,G-n));return}if(this.alive&&this.notifyPort>0&&Date.now()-t>i){a.info("claude-adapter",`Channel listener fallback: notify port connected, assuming ready after ${Date.now()-t}ms (resume mode may skip "Listening" output)`),this.startupChannelListeningAt=Date.now(),this.startupChannelListening=!0,this.clearPendingMcpFailureTimer(),this.sendDeferredModelSwitch();return}await new Promise(s=>setTimeout(s,200))}throw new Error(`Claude channel listener not ready within ${e}ms`)}ptyAutoConfirmTimer=null;startPtyAutoConfirm(e){this.ptyAutoConfirmTimer&&clearInterval(this.ptyAutoConfirmTimer);let t=!1;const i=()=>{if(this.startupChannelListening||this.stopped){this.ptyAutoConfirmTimer&&(clearInterval(this.ptyAutoConfirmTimer),this.ptyAutoConfirmTimer=null);return}try{e.write("\r"),t||(a.info("claude-adapter","PTY auto-confirm: sending Enter for dev channels dialog"),t=!0)}catch{}};setTimeout(i,1e3),this.ptyAutoConfirmTimer=setInterval(i,2e3),setTimeout(()=>{this.ptyAutoConfirmTimer&&(clearInterval(this.ptyAutoConfirmTimer),this.ptyAutoConfirmTimer=null)},3e4).unref()}sendDeferredModelSwitch(){if(!this.deferredModelId)return;const e=this.deferredModelId;this.deferredModelId=null,setTimeout(()=>{const t=this.claudePty??this.claudeProcess?.stdin;if(!(!t||this.stopped))try{const i=`/model ${e}
14
14
  `;"write"in t?t.write(i):t.write(i,()=>{}),a.info("claude-adapter",`Deferred model switch: /model ${e}`)}catch{}},3e3)}async waitForWindowsChannelReady(e){const t=Date.now();let i=!1;const s=setInterval(()=>{if(this.startupChannelListening){clearInterval(s);return}try{this.claudePty?(this.claudePty.write("\r"),i||(a.info("claude-adapter","Windows PTY: sending Enter to auto-confirm dev channels dialog"),i=!0)):this.claudeProcess?.stdin?.writable&&(this.claudeProcess.stdin.write("\r"),i||(a.info("claude-adapter","Windows shell: sending Enter to auto-confirm dev channels dialog"),i=!0))}catch{}},3e3);try{for(;Date.now()-t<e;){if(this.channelGateClosed)throw new Error("channels are not currently available (tengu_harbor gate closed)");if(this.startupChannelListening){const o=this.startupChannelListeningAt||Date.now(),l=Date.now()-o;if(l>=G)return;await new Promise(c=>setTimeout(c,G-l));return}const n=Date.now()-t,r=this.claudePty?gt:12e3;if(this.alive&&this.mcpServerReady&&n>r){a.info("claude-adapter",`Windows ${this.claudePty?"PTY":"shell"} fallback: assuming channel ready after ${n}ms (no stdout detection, MCP server connected)`),this.startupChannelListeningAt=Date.now(),this.startupChannelListening=!0,this.clearPendingMcpFailureTimer(),this.sendDeferredModelSwitch();return}await new Promise(o=>setTimeout(o,500))}}finally{clearInterval(s)}throw new Error(`Windows channel ready check timed out within ${e}ms`)}pushNotification(e,t){if(!this.mcpServerReady||this.notifyPort<=0)return;const i=JSON.stringify({jsonrpc:"2.0",method:e,params:t});!this.notifySocket||this.notifySocket.destroyed?(this.notifySocket=V.createConnection({host:"127.0.0.1",port:this.notifyPort},()=>{this.notifySocket.write(i+`
15
15
  `)}),this.notifySocket.on("error",s=>{a.error("claude-adapter",`Notify socket error: ${s.message}`),this.notifySocket=null}),this.notifySocket.on("close",()=>{this.notifySocket=null})):this.notifySocket.write(i+`
16
- `)}stopMcpServer(){if(this.mcpServerReady=!1,this.notifySocket){try{this.notifySocket.destroy()}catch{}this.notifySocket=null}if(this.mcpServerProcess){try{this.mcpServerProcess.kill("SIGTERM")}catch{}this.mcpServerProcess=null}}releaseNotifyPortReservation(){St(this.notifyPort),this.notifyPort=0}killClaudeProcess(e){const t=this.claudeProcess,i=this.claudePty,s=this.claudeChildPid;if(this.claudeProcess=null,this.claudePty=null,this.claudeChildPid=0,this.spawnPromise=null,this.alive=!1,this.stopMcpServer(),this.releaseNotifyPortReservation(),this.mcpChannelBroken=!1,this.channelGateClosed=!1,this.startupChannelListening=!1,this.startupChannelListeningAt=0,this.deferredModelId=null,this.clearPendingMcpFailureTimer(),this.sessionIdConflictDetected=!1,this.pendingPermissions.size>0){const n=E(),r=new q(n.permissionRequestsDir);for(const[o]of this.pendingPermissions)r.resolveRequest(o,"deny").catch(()=>{});this.pendingPermissions.clear()}if(i)try{i.kill()}catch{}if(t?.pid&&N(t,"SIGTERM"),s>0)try{process.kill(s,"SIGTERM")}catch{}if(t?.pid||i||s>0){const n=s,r=t,o=i;setTimeout(()=>{if(o)try{o.kill()}catch{}if(r?.pid&&N(r,"SIGKILL"),n>0)try{process.kill(n,"SIGKILL")}catch{}},5e3).unref()}a.info("claude-adapter",`Claude process killed (reason=${e}, pid=${s}, expectPid=${t?.pid})`)}tryRecoverSessionIdConflict(){if(!this.sessionIdConflictDetected||this.stopped||!this.activeEvent||this.activeEvent.replied)return!1;const e=this.activeEvent.eventId;if(this.sessionIdConflictRetriedEventIds.has(e))return this.sessionIdConflictDetected=!1,!1;this.sessionIdConflictRetriedEventIds.add(e),this.sessionIdConflictDetected=!1;const t=this.activeEvent.rawEvent;return a.warn("claude-adapter",`Detected Claude session-id conflict, auto-retrying event once: ${e}`),this.clearActiveEventIdleTimer(),this.clearActiveEventHardTimer(),this.activeEvent=null,this.stopComposing(),this.deliverInboundEvent(t),!0}recoverMalformedToolCall(e){if(this.stopped||!this.activeEvent||this.activeEvent.replied||!this.claudeCliSessionId||!this.claudeSessionCwd)return"none";const t=B(this.claudeCliSessionId,this.claudeSessionCwd,this.activeEvent.jsonlBaseOffset);if(!t||t.stopReason!=="stop_sequence"||t.text.trim()!==at)return"none";if(this.malformedToolRetriedEventIds.has(e))return a.warn("claude-adapter",`Malformed tool-call retry exhausted for ${e}, sending clean fallback`),"exhausted";this.malformedToolRetriedEventIds.add(e);const i={...this.activeEvent.rawEvent,content:ot};return a.warn("claude-adapter",`Detected malformed tool-call, silently retrying event once: ${e}`),this.clearActiveEventIdleTimer(),this.clearActiveEventHardTimer(),this.activeEvent=null,this.stopComposing(),this.deliverInboundEvent(i),"retried"}async handleInternalInvoke(e,t,i){if(e==="event_tool_call"){const s=String(t.tool_name??""),n=ze(s,{...t.arguments??{}}),r=this.getEventToolHandle();if(this.activeEvent){const l=this.activeEvent,c=String(n.event_id??"").trim();c===""?n.event_id=l.eventId:(s==="grix_reply"||s==="grix_complete")&&c!==l.eventId&&(a.warn("claude-adapter",`${s}: event_id mismatch, correcting: supplied=${c}, active=${l.eventId}`),n.event_id=l.eventId),String(n.session_id??"").trim()===""&&(n.session_id=l.sessionId)}else if(this.lastClearedEvent&&s==="grix_reply"){const l=this.lastClearedEvent;Date.now()-l.ts<ft&&(String(n.session_id??"").trim()===""&&(n.session_id=l.sessionId),n.event_id="",a.info("claude-adapter",`Late grix_reply fallback: sending direct message for cleared event ${l.eventId} (cleared ${(Date.now()-l.ts)/1e3}s ago)`))}s==="grix_reply"&&String(n.event_id??"").trim()!==""&&this.completedEventIds.has(String(n.event_id??"").trim())&&(a.info("claude-adapter",`Late grix_reply fallback: sending direct message for completed event ${String(n.event_id??"").trim()}`),n.event_id="");const o=Be(s,n);if(!o.valid)throw new Error(`\u53C2\u6570\u6821\u9A8C\u5931\u8D25: ${o.error}`);if(r.status!=="ready")throw new Error(`\u8FDE\u63A5\u4E0D\u53EF\u7528: \u5F53\u524D\u72B6\u6001\u4E3A ${r.status}`);if(s==="grix_access_control")return this.executeAccessControl(n);if(qe(s)){const l=this.activeEvent;l&&(l.toolCallInFlight=!0);try{if(i?.aborted)throw new Error("invoke aborted by timeout");const c=await Ue(r,s,n);if(c.isError)throw new Error(c.content[0]?.text??"event tool failed");if(i?.aborted)throw new Error("invoke aborted by timeout after send");return this.postProcessEventToolCall(s,n),JSON.parse(c.content[0]?.text??"null")}finally{if(l&&(l.toolCallInFlight=!1,l.pendingStopHook&&this.activeEvent===l)){const c=l.pendingStopHook;l.pendingStopHook=void 0,this.finalizeActiveEvent(c)}}}throw new Error(`\u672A\u77E5\u4E8B\u4EF6\u5DE5\u5177: ${s}`)}return this.bridgeCallbacks.agentInvoke(e,t)}getEventToolHandle(){const e=this;return{status:"ready",getStatusSnapshot:()=>({status:"ready",connectedAt:Date.now(),reconnectAttempts:0}),sendEventAck:t=>{e.bridgeCallbacks.sendEventAck(t.event_id,t.session_id??"")},sendStreamChunk:t=>{e.bridgeCallbacks.sendStreamChunk(t.event_id??"",t.session_id,t.delta_content??"",Number(t.chunk_seq??0)||1,t.is_finish===!0,t.client_msg_id,t.quoted_message_id)},sendMsg:t=>{if(typeof e.bridgeCallbacks.sendDirectMessage!="function"){a.warn("claude-adapter","sendDirectMessage callback not provided, dropping direct message");return}e.bridgeCallbacks.sendDirectMessage({sessionId:t.session_id,clientMsgId:t.client_msg_id,content:t.content,quotedMessageId:t.quoted_message_id})},sendEventResult:t=>{e.bridgeCallbacks.sendEventResult(t.event_id,t.status,t.msg,t.code)},sendSessionActivitySet:t=>{}}}postProcessEventToolCall(e,t){const i=String(t.event_id??"").trim();if(!i||this.activeEvent?.eventId!==i){a.warn("claude-adapter",`postProcessEventToolCall: event_id mismatch (tool=${e}, eventId=${i}, activeEventId=${this.activeEvent?.eventId??"none"})`);return}if(e==="grix_complete"){this.completedEventIds.set(i,Date.now()),this.clearActiveEvent();return}if(e==="grix_reply"){const s=!this.activeEvent.replied;this.activeEvent.replied=!0;const n=String(t.text??"");n.length>0&&(this.activeEvent.lastReplyText=n),this.markActiveEventActivity(i,String(t.session_id??"").trim()||void 0),s&&(this.activeEvent.repliedAt=Date.now(),this.startPostReplyDeadline(i),this.startPostReplyJsonlWatcher(i))}}async executeAccessControl(e){const t=String(e.action??""),i=Ge[t];if(!i)throw new Error(`\u672A\u77E5 access_control action: ${t}`);const s={};return e.code!=null&&(s.code=e.code),e.sender_id!=null&&(s.sender_id=e.sender_id),e.policy!=null&&(s.policy=e.policy),this.bridgeCallbacks.agentInvoke("claude_access_control",{verb:i,payload:s},3e4)}resolveSessionRuntime(){if(!this.runtimeResolver)return{};try{return this.runtimeResolver(this.sessionId)??{}}catch(e){throw new Error(`resolve session runtime failed: ${e instanceof Error?e.message:String(e)}`)}}async validatePluginDir(e){const t=String(e??"").trim();if(!t)return;let i;try{i=await ne(t)}catch{throw new Error(`pluginDir is not accessible: ${t}`)}if(!i.isDirectory())throw new Error(`pluginDir is not a directory: ${t}`);const s=h(t,".mcp.json");try{(await ne(s)).isFile()&&(await se(s),a.info("claude-adapter",`Removed conflicting .mcp.json from pluginDir: ${s}`))}catch{}}async ensureWorkspaceTrust(e){const t=h(M(),".claude.json");try{const i=await w(t,"utf8"),s=JSON.parse(i);if(s.projects?.[e]?.hasTrustDialogAccepted===!0)return;s.projects||(s.projects={}),s.projects[e]||(s.projects[e]={}),s.projects[e].hasTrustDialogAccepted=!0,await C(t,JSON.stringify(s),"utf8"),a.info("claude-adapter",`Pre-trusted workspace: ${e}`)}catch(i){a.warn("claude-adapter",`Failed to pre-trust workspace ${e}: ${i}`)}}async ensureSkipDangerousPermissionPrompt(){const e=h(M(),".claude","settings.json");try{let t={};try{t=JSON.parse(await w(e,"utf8"))}catch{}if(t.skipDangerousModePermissionPrompt===!0)return;t.skipDangerousModePermissionPrompt=!0,await x(h(M(),".claude"),{recursive:!0}),await C(e,JSON.stringify(t,null,2),"utf8"),a.info("claude-adapter","Set skipDangerousModePermissionPrompt=true in user settings to skip BypassPermissions dialog")}catch(t){a.warn("claude-adapter",`Failed to set skipDangerousModePermissionPrompt: ${t}`)}}async isChannelGateClosed(){if(this.channelGateClosed)return!0;try{const e=await w(h(M(),".claude.json"),"utf8"),i=JSON.parse(e).cachedGrowthBookFeatures;return!i||Object.keys(i).length===0?!1:i.tengu_harbor!==!0}catch{return!1}}async ensureClaudeOnboardingFlags(e){const t=h(M(),".claude.json");try{let i;try{const r=await w(t,"utf8");i=JSON.parse(r)}catch{i={}}let s=!1;i.hasCompletedOnboarding||(i.hasCompletedOnboarding=!0,i.lastOnboardingVersion||(i.lastOnboardingVersion="2.1.31"),s=!0),i.projects||(i.projects={});const n=i.projects;if(n[e]||(n[e]={}),n[e].hasCompletedProjectOnboarding||(n[e].hasCompletedProjectOnboarding=!0,s=!0),!s)return;await C(t,JSON.stringify(i,null,2),"utf8"),a.info("claude-adapter",`Marked Claude onboarding complete: ${e}`)}catch(i){a.warn("claude-adapter",`Failed to mark Claude onboarding complete: ${i}`)}}async injectStatusLineSettings(e){try{const t=this.resolveProjectRoot(),i=h(t,"dist","scripts","status-line-forwarder.js"),s=h(e,".claude"),n=h(s,"settings.json");await x(s,{recursive:!0});let r={};try{r=JSON.parse(await w(n,"utf8"))}catch{}const l={type:"command",command:`node "${i.replace(/\\/g,"/")}"`,refreshInterval:10};r.statusLine=l;const c=`${JSON.stringify(r,null,2)}
16
+ `)}stopMcpServer(){if(this.mcpServerReady=!1,this.notifySocket){try{this.notifySocket.destroy()}catch{}this.notifySocket=null}if(this.mcpServerProcess){try{this.mcpServerProcess.kill("SIGTERM")}catch{}this.mcpServerProcess=null}}releaseNotifyPortReservation(){St(this.notifyPort),this.notifyPort=0}killClaudeProcess(e){const t=this.claudeProcess,i=this.claudePty,s=this.claudeChildPid;if(this.claudeProcess=null,this.claudePty=null,this.claudeChildPid=0,this.spawnPromise=null,this.alive=!1,this.stopMcpServer(),this.releaseNotifyPortReservation(),this.mcpChannelBroken=!1,this.channelGateClosed=!1,this.startupChannelListening=!1,this.startupChannelListeningAt=0,this.deferredModelId=null,this.clearPendingMcpFailureTimer(),this.sessionIdConflictDetected=!1,this.pendingPermissions.size>0){const n=E(),r=new q(n.permissionRequestsDir);for(const[o]of this.pendingPermissions)r.resolveRequest(o,"deny").catch(()=>{});this.pendingPermissions.clear()}if(i)try{i.kill()}catch{}if(t?.pid&&N(t,"SIGTERM"),s>0)try{process.kill(s,"SIGTERM")}catch{}if(t?.pid||i||s>0){const n=s,r=t,o=i;setTimeout(()=>{if(o)try{o.kill()}catch{}if(r?.pid&&N(r,"SIGKILL"),n>0)try{process.kill(n,"SIGKILL")}catch{}},5e3).unref()}a.info("claude-adapter",`Claude process killed (reason=${e}, pid=${s}, expectPid=${t?.pid})`)}tryRecoverSessionIdConflict(){if(!this.sessionIdConflictDetected||this.stopped||!this.activeEvent||this.activeEvent.replied)return!1;const e=this.activeEvent.eventId;if(this.sessionIdConflictRetriedEventIds.has(e))return this.sessionIdConflictDetected=!1,!1;this.sessionIdConflictRetriedEventIds.add(e),this.sessionIdConflictDetected=!1;const t=this.activeEvent.rawEvent;return a.warn("claude-adapter",`Detected Claude session-id conflict, auto-retrying event once: ${e}`),this.clearActiveEventIdleTimer(),this.clearActiveEventHardTimer(),this.activeEvent=null,this.stopComposing(),this.deliverInboundEvent(t),!0}recoverMalformedToolCall(e){if(this.stopped||!this.activeEvent||this.activeEvent.replied||!this.claudeCliSessionId||!this.claudeSessionCwd)return"none";const t=B(this.claudeCliSessionId,this.claudeSessionCwd,this.activeEvent.jsonlBaseOffset);if(!t||t.stopReason!=="stop_sequence"||t.text.trim()!==at)return"none";if(this.malformedToolRetriedEventIds.has(e))return a.warn("claude-adapter",`Malformed tool-call retry exhausted for ${e}, sending clean fallback`),"exhausted";this.malformedToolRetriedEventIds.add(e);const i={...this.activeEvent.rawEvent,content:ot};return a.warn("claude-adapter",`Detected malformed tool-call, silently retrying event once: ${e}`),this.clearActiveEventIdleTimer(),this.clearActiveEventHardTimer(),this.activeEvent=null,this.stopComposing(),this.deliverInboundEvent(i),"retried"}async handleInternalInvoke(e,t,i){if(e==="event_tool_call"){const s=String(t.tool_name??""),n=ze(s,{...t.arguments??{}}),r=this.getEventToolHandle();if(this.activeEvent){const l=this.activeEvent,c=String(n.event_id??"").trim();c===""?n.event_id=l.eventId:(s==="grix_reply"||s==="grix_complete")&&c!==l.eventId&&(a.warn("claude-adapter",`${s}: event_id mismatch, correcting: supplied=${c}, active=${l.eventId}`),n.event_id=l.eventId),String(n.session_id??"").trim()===""&&(n.session_id=l.sessionId)}else if(this.lastClearedEvent&&s==="grix_reply"){const l=this.lastClearedEvent;Date.now()-l.ts<ft&&(String(n.session_id??"").trim()===""&&(n.session_id=l.sessionId),n.event_id="",a.info("claude-adapter",`Late grix_reply fallback: sending direct message for cleared event ${l.eventId} (cleared ${(Date.now()-l.ts)/1e3}s ago)`))}s==="grix_reply"&&String(n.event_id??"").trim()!==""&&this.completedEventIds.has(String(n.event_id??"").trim())&&(a.info("claude-adapter",`Late grix_reply fallback: sending direct message for completed event ${String(n.event_id??"").trim()}`),n.event_id="");const o=Be(s,n);if(!o.valid)throw new Error(`\u53C2\u6570\u6821\u9A8C\u5931\u8D25: ${o.error}`);if(r.status!=="ready")throw new Error(`\u8FDE\u63A5\u4E0D\u53EF\u7528: \u5F53\u524D\u72B6\u6001\u4E3A ${r.status}`);if(s==="grix_access_control")return this.executeAccessControl(n);if(qe(s)){const l=this.activeEvent;l&&(l.toolCallInFlight=!0);try{if(i?.aborted)throw new Error("invoke aborted by timeout");const c=Ue(r,s,n);if(c.isError)throw new Error(c.content[0]?.text??"event tool failed");if(i?.aborted)throw new Error("invoke aborted by timeout after send");return this.postProcessEventToolCall(s,n),JSON.parse(c.content[0]?.text??"null")}finally{if(l&&(l.toolCallInFlight=!1,l.pendingStopHook&&this.activeEvent===l)){const c=l.pendingStopHook;l.pendingStopHook=void 0,this.finalizeActiveEvent(c)}}}throw new Error(`\u672A\u77E5\u4E8B\u4EF6\u5DE5\u5177: ${s}`)}return this.bridgeCallbacks.agentInvoke(e,t)}getEventToolHandle(){const e=this;return{status:"ready",getStatusSnapshot:()=>({status:"ready",connectedAt:Date.now(),reconnectAttempts:0}),sendEventAck:t=>{e.bridgeCallbacks.sendEventAck(t.event_id,t.session_id??"")},sendStreamChunk:t=>{e.bridgeCallbacks.sendStreamChunk(t.event_id??"",t.session_id,t.delta_content??"",Number(t.chunk_seq??0)||1,t.is_finish===!0,t.client_msg_id,t.quoted_message_id)},sendMsg:t=>{if(typeof e.bridgeCallbacks.sendDirectMessage!="function"){a.warn("claude-adapter","sendDirectMessage callback not provided, dropping direct message");return}e.bridgeCallbacks.sendDirectMessage({sessionId:t.session_id,clientMsgId:t.client_msg_id,content:t.content,quotedMessageId:t.quoted_message_id})},sendEventResult:t=>{e.bridgeCallbacks.sendEventResult(t.event_id,t.status,t.msg,t.code)},sendSessionActivitySet:t=>{}}}postProcessEventToolCall(e,t){const i=String(t.event_id??"").trim();if(!i||this.activeEvent?.eventId!==i){a.warn("claude-adapter",`postProcessEventToolCall: event_id mismatch (tool=${e}, eventId=${i}, activeEventId=${this.activeEvent?.eventId??"none"})`);return}if(e==="grix_complete"){this.completedEventIds.set(i,Date.now()),this.clearActiveEvent();return}if(e==="grix_reply"){const s=!this.activeEvent.replied;this.activeEvent.replied=!0;const n=String(t.text??"");n.length>0&&(this.activeEvent.lastReplyText=n),this.markActiveEventActivity(i,String(t.session_id??"").trim()||void 0),s&&(this.activeEvent.repliedAt=Date.now(),this.startPostReplyDeadline(i),this.startPostReplyJsonlWatcher(i))}}async executeAccessControl(e){const t=String(e.action??""),i=Ge[t];if(!i)throw new Error(`\u672A\u77E5 access_control action: ${t}`);const s={};return e.code!=null&&(s.code=e.code),e.sender_id!=null&&(s.sender_id=e.sender_id),e.policy!=null&&(s.policy=e.policy),this.bridgeCallbacks.agentInvoke("claude_access_control",{verb:i,payload:s},3e4)}resolveSessionRuntime(){if(!this.runtimeResolver)return{};try{return this.runtimeResolver(this.sessionId)??{}}catch(e){throw new Error(`resolve session runtime failed: ${e instanceof Error?e.message:String(e)}`)}}async validatePluginDir(e){const t=String(e??"").trim();if(!t)return;let i;try{i=await ne(t)}catch{throw new Error(`pluginDir is not accessible: ${t}`)}if(!i.isDirectory())throw new Error(`pluginDir is not a directory: ${t}`);const s=h(t,".mcp.json");try{(await ne(s)).isFile()&&(await se(s),a.info("claude-adapter",`Removed conflicting .mcp.json from pluginDir: ${s}`))}catch{}}async ensureWorkspaceTrust(e){const t=h(M(),".claude.json");try{const i=await w(t,"utf8"),s=JSON.parse(i);if(s.projects?.[e]?.hasTrustDialogAccepted===!0)return;s.projects||(s.projects={}),s.projects[e]||(s.projects[e]={}),s.projects[e].hasTrustDialogAccepted=!0,await C(t,JSON.stringify(s),"utf8"),a.info("claude-adapter",`Pre-trusted workspace: ${e}`)}catch(i){a.warn("claude-adapter",`Failed to pre-trust workspace ${e}: ${i}`)}}async ensureSkipDangerousPermissionPrompt(){const e=h(M(),".claude","settings.json");try{let t={};try{t=JSON.parse(await w(e,"utf8"))}catch{}if(t.skipDangerousModePermissionPrompt===!0)return;t.skipDangerousModePermissionPrompt=!0,await x(h(M(),".claude"),{recursive:!0}),await C(e,JSON.stringify(t,null,2),"utf8"),a.info("claude-adapter","Set skipDangerousModePermissionPrompt=true in user settings to skip BypassPermissions dialog")}catch(t){a.warn("claude-adapter",`Failed to set skipDangerousModePermissionPrompt: ${t}`)}}async isChannelGateClosed(){if(this.channelGateClosed)return!0;try{const e=await w(h(M(),".claude.json"),"utf8"),i=JSON.parse(e).cachedGrowthBookFeatures;return!i||Object.keys(i).length===0?!1:i.tengu_harbor!==!0}catch{return!1}}async ensureClaudeOnboardingFlags(e){const t=h(M(),".claude.json");try{let i;try{const r=await w(t,"utf8");i=JSON.parse(r)}catch{i={}}let s=!1;i.hasCompletedOnboarding||(i.hasCompletedOnboarding=!0,i.lastOnboardingVersion||(i.lastOnboardingVersion="2.1.31"),s=!0),i.projects||(i.projects={});const n=i.projects;if(n[e]||(n[e]={}),n[e].hasCompletedProjectOnboarding||(n[e].hasCompletedProjectOnboarding=!0,s=!0),!s)return;await C(t,JSON.stringify(i,null,2),"utf8"),a.info("claude-adapter",`Marked Claude onboarding complete: ${e}`)}catch(i){a.warn("claude-adapter",`Failed to mark Claude onboarding complete: ${i}`)}}async injectStatusLineSettings(e){try{const t=this.resolveProjectRoot(),i=h(t,"dist","scripts","status-line-forwarder.js"),s=h(e,".claude"),n=h(s,"settings.json");await x(s,{recursive:!0});let r={};try{r=JSON.parse(await w(n,"utf8"))}catch{}const l={type:"command",command:`node "${i.replace(/\\/g,"/")}"`,refreshInterval:10};r.statusLine=l;const c=`${JSON.stringify(r,null,2)}
17
17
  `;await C(n,c,"utf8"),a.info("claude-adapter",`Injected statusLine settings: ${n}`)}catch(t){a.warn("claude-adapter",`Failed to inject statusLine settings: ${t instanceof Error?t.message:t}`)}}async ensureUserMcpServer(e,t,i){const s=this.resolveServerEntryPath(t),n=process.execPath,r=[s],o=h(M(),".claude.json");let l=null;try{const p=await w(o,"utf8");l=JSON.parse(p)?.mcpServers?.[O]??null}catch{}if(l&&String(l.type||"stdio").trim()==="stdio"&&String(l.command??"").trim()===n&&Array.isArray(l.args)&&l.args.length===r.length&&l.args.every((p,S)=>p===r[S]))return;a.info("claude-adapter",`Registering user-scoped MCP server: ${O} -> ${n} ${r.join(" ")}`);try{j(`${e} mcp remove -s user ${O}`,{encoding:"utf8",timeout:1e4,env:i,stdio:"pipe"})}catch{}const c=["mcp","add","--scope","user",O,"--",n,...r],u=process.platform==="win32"?'"':"'",f=j(`${e} ${c.map(p=>`${u}${p}${u}`).join(" ")}`,{encoding:"utf8",timeout:1e4,env:i,stdio:"pipe"});a.info("claude-adapter",`MCP server registered: ${f.trim()||"ok"}`)}resolveServerEntryPath(e){const t=h(e,"server","main.js");try{if(Q(t))return t}catch{}const i=h(e,"dist","index.js");try{if(Q(i))return i}catch{}throw new Error(`Cannot find grix-claude server entry in pluginDir: ${e}`)}clearActiveEvent(){const e=this.activeEvent;this.clearActiveEventIdleTimer(),this.clearActiveEventHardTimer(),this.clearActiveEventPostReplyTimer(),this.clearActiveEventPostReplyWatcher(),e&&(this.sessionIdConflictRetriedEventIds.delete(e.eventId),this.malformedToolRetriedEventIds.delete(e.eventId),this.completedEventIds.set(e.eventId,Date.now()),this.lastClearedEvent={eventId:e.eventId,sessionId:e.sessionId,ts:Date.now()},this.emit(`reply:${e.sessionId}`,{status:"completed"}),this.emit("eventDone",e.eventId)),this.activeEvent=null,this.stopComposing()}startComposing(e,t){}stopComposing(){}clearComposingTimer(){}resolveHookSignalsPath(){const e=E();return h(e.dataDir,`hook-signals-${this.sessionId}.json`)}startActivityTracking(){this.activityManager&&this.activityManager.stop();const e=this.resolveHookSignalsPath(),t=E().hookSignalsLogPath,i=new je(e,t);a.info("claude-adapter",`Activity tracking started: watching ${e}`),this.activityManager=new Fe({hookSignalStore:i,onActivity:s=>this.onHookActivity(s),onStop:()=>this.onClaudeTurnEnd("Stop"),onStopFailure:()=>this.onClaudeTurnEnd("StopFailure"),onCompactStart:()=>this.beginCompaction(),onCompactResult:()=>this.finishCompaction("post-compact-hook"),onPermissionRequest:(s,n)=>this.handlePermissionHookEvent(s,n)}),this.activityManager.start()}onClaudeTurnEnd(e){if(a.info("claude-adapter",`Hook activity: ${e}`),!this.activeEvent){this.stopHookBarrierSessionId&&(a.info("claude-adapter",`Late Stop hook after end_turn finalize \u2014 releasing barrier (session=${this.stopHookBarrierSessionId})`),this.clearStopHookBarrier());return}if(this.activeEvent.toolCallInFlight){a.info("claude-adapter",`Stop hook deferred: toolCallInFlight for ${this.activeEvent.eventId}`),this.activeEvent.pendingStopHook=e;return}this.finalizeActiveEvent(e)}attemptRescueFromJsonl(e,t){if(!this.claudeCliSessionId||!this.claudeSessionCwd)return a.info("claude-adapter",`Rescue skipped: no claudeCliSessionId or claudeSessionCwd for ${e}`),!1;const i=this.activeEvent?.jsonlBaseOffset,s=Ve(this.claudeCliSessionId,this.claudeSessionCwd,i);if(!s)return a.info("claude-adapter",`Rescue failed: no assistant text found in JSONL for ${e}`),!1;const n=`rescue_${e}_${Date.now()}`;return this.bridgeCallbacks.sendStreamChunk(e,t,s,1,!1,n),this.bridgeCallbacks.sendStreamChunk(e,t,"",2,!0,n),a.info("claude-adapter",`Rescue succeeded for event ${e}: sent ${s.length} chars from JSONL`),!0}warnUnsentFinalIfAny(e){if(!this.claudeCliSessionId||!this.claudeSessionCwd)return;const t=this.activeEvent?.jsonlBaseOffset,i=B(this.claudeCliSessionId,this.claudeSessionCwd,t);if(!i||i.stopReason!=="end_turn")return;const s=i.text?.trim()??"";if(!s)return;const n=(this.activeEvent?.lastReplyText??"").trim();n&&(s===n||n.includes(s))||a.warn("claude-adapter",`Unsent final end_turn text for ${e} (${s.length} chars) not delivered \u2014 \u5DF2\u5E94\u7B54\u8FC7\uFF0C\u6309\u7B56\u7565\u4E0D\u8865\u53D1\uFF08\u7591\u4F3C\u56DE\u5408\u6536\u5C3E\u81EA\u8FF0\uFF1B\u82E5\u786E\u4E3A\u6F0F\u53D1\u7ED3\u8BBA\u53EF\u636E\u6B64\u6392\u67E5\uFF09`)}finalizeActiveEvent(e){if(!this.activeEvent)return;const t=this.activeEvent.eventId,i=this.activeEvent.sessionId;if(this.activeEvent.replied&&this.activeEvent.jsonlBaseOffset!==void 0&&this.claudeCliSessionId&&this.claudeSessionCwd){const n=B(this.claudeCliSessionId,this.claudeSessionCwd,this.activeEvent.jsonlBaseOffset);if(!n||n.stopReason!=="end_turn")if(oe(this.claudeCliSessionId,this.claudeSessionCwd,this.activeEvent.jsonlBaseOffset))a.info("claude-adapter",`Stop hook: terminal tool_result detected for ${t} (grix_reply as final action, no end_turn expected), finalizing as responded`);else{a.info("claude-adapter",`Stop hook suppressed for ${t}: no end_turn in JSONL after offset=${this.activeEvent.jsonlBaseOffset} \u2014 likely resume-drain hook, waiting for JSONL watcher`),this.markActiveEventActivity(t,i);return}}let s=!1;if(!this.activeEvent.apiFormatError)if(this.activeEvent.replied)this.warnUnsentFinalIfAny(t);else{const n=this.recoverMalformedToolCall(t);if(n==="retried")return;n==="exhausted"?(this.bridgeCallbacks.sendStreamChunk(t,i,lt,1,!1,`mtc_${t}`),this.bridgeCallbacks.sendStreamChunk(t,i,"",2,!0,`mtc_${t}`),s=!0):s=this.attemptRescueFromJsonl(t,i)}if(this.activeEvent.replied||s)a.info("claude-adapter",`Stop hook received, finalizing event ${t} as responded (replied=${this.activeEvent.replied}, rescued=${s})`),this.bridgeCallbacks.sendEventResult(t,"responded");else{a.warn("claude-adapter",`Active event not confirmed when ${e}: ${t} apiFormatError=${!!this.activeEvent.apiFormatError}, sending failed result`);const n=this.activeEvent.apiFormatError?"Claude hit an API format error. Please start a new conversation (/grix open <dir>).":"Claude exited before completing its reply. Please try again.";this.bridgeCallbacks.sendStreamChunk(t,i,n,1,!1,`err_${t}`),this.bridgeCallbacks.sendStreamChunk(t,i,"",2,!0,`err_${t}`),this.bridgeCallbacks.sendEventResult(t,"failed",n,"agent_stop_failure")}this.clearActiveEvent(),this.armStopHookBarrier(i)}onHookActivity(e){const t=this.activeEvent?.sessionId;if(a.info("claude-adapter",`Hook activity: tool=${e?.tool_name??"(clear)"} session=${t??"(none)"}`),e&&this.activeEvent&&(this.markActiveEventActivity(this.activeEvent.eventId,this.activeEvent.sessionId),this.activeEvent.replied&&this.startPostReplyDeadline(this.activeEvent.eventId)),e&&!this.activeEvent&&!this.compacting&&this.noteSelfDrivenActivity(),!!t)if(e){this.startComposing(t,e);const i=this.activeEvent;if(i){const s=e.event_name,n=e.tool_name,r=e.tool_input??"",o=n?Je(n):!1;if(n==="ExitPlanMode"&&s==="PreToolUse")a.info("claude-adapter","ExitPlanMode detected; waiting for user decision via permission card");else if(n==="AskUserQuestion"&&s==="PreToolUse")this.handleAskUserQuestion(i,r);else if(s==="PreToolUse"&&n)r&&(this.lastPreToolInput=r),o||this.bridgeCallbacks.sendToolUse(i.eventId,i.sessionId,n,r);else if((s==="PostToolUse"||s==="PostToolUseFailure")&&n){if(n==="AskUserQuestion")return;const l=r||this.lastPreToolInput;if(this.lastPreToolInput="",!o){const c=s==="PostToolUseFailure"?`(failed) ${l}`:l;this.bridgeCallbacks.sendToolResult(i.eventId,i.sessionId,n,c)}}}}else this.startComposing(t)}handlePermissionHookEvent(e,t){if(!this.activeEvent){a.warn("claude-adapter","PermissionRequest without active event, ignoring");return}if(e==="AskUserQuestion"){a.info("claude-adapter","Skip permission card for AskUserQuestion; handled by agent_question card flow");return}const i=this.activeEvent,s=E();new q(s.permissionRequestsDir).listPending().then(r=>{const o=r.length>0?r[r.length-1]:null;if(!o){a.warn("claude-adapter","No pending permission request found in store");return}const l=o.request_id;this.pendingPermissions.set(l,{eventId:i.eventId,sessionId:i.sessionId});const c=typeof t=="string"?t:"";if(e==="ExitPlanMode"){const p=et(c);p&&this.bridgeCallbacks.sendReply(i.eventId,i.sessionId,p)}const u=e==="ExitPlanMode"?"":c.slice(0,100),f=u?`${e}: ${u}`:e;this.bridgeCallbacks.sendPermissionCard({eventId:i.eventId,sessionId:i.sessionId,approvalId:l,toolName:e,toolTitle:f}),a.info("claude-adapter",`Sent permission card: approvalId=${l} tool=${e}`)}).catch(r=>{a.warn("claude-adapter",`Failed to send permission card: ${r}`)})}handleAskUserQuestion(e,t){const i=ce(t);if(!i){a.warn("claude-adapter","Failed to parse AskUserQuestion input, skipping agent_question card");return}const s=E(),n=new ae(s.questionRequestsDir);n.listPending().then(async r=>{const o=[...r].reverse().filter(u=>String(u.session_id??"").trim()===this.claudeCliSessionId),l=o.find(u=>String(u.event_id??"").trim()===e.eventId)??o.find(u=>String(u.event_id??"").trim()==="")??o[0]??null,c=l?.request_id??`fallback-${Date.now()}`;l&&String(l.event_id??"").trim()===""&&await n.updateRequest(l.request_id,{event_id:e.eventId}),this.pendingQuestion.set(c,{eventId:e.eventId,sessionId:e.sessionId}),e.awaitingUserQuestion=!0,this.bridgeCallbacks.sendAgentQuestionCard(e.eventId,e.sessionId,{request_id:c,mode:"form",questions:i}),a.info("claude-adapter",`Sent agent_question card: request_id=${c} questions=${i.length}`)}).catch(()=>{a.warn("claude-adapter","Failed to list pending questions from store")})}parseAskUserQuestions(e){return ce(e)}parseAskUserQuestionInput(e){let t;try{t=JSON.parse(e)}catch{return null}const i=[],s=t.questions;if(!Array.isArray(s)||s.length===0)return null;for(let n=0;n<s.length;n++){const r=s[n];if(!r||typeof r!="object")continue;const o=String(r.header??r.question??`Question ${n+1}`),l=String(r.prompt??""),c=Array.isArray(r.options)?r.options:void 0,u=r.multiSelect===!0;i.push({header:o,prompt:l,...c&&c.length>0?{options:c}:{},...u?{multi_select:!0}:{}})}return i.length>0?{questions:i}:null}pruneCompletedEvents(){const e=Date.now()-nt;for(const[t,i]of this.completedEventIds.entries())i<e&&this.completedEventIds.delete(t)}markActiveEventActivity(e,t){const i=this.activeEvent;i&&(e&&i.eventId!==e||t&&i.sessionId!==t||(this.resetActiveEventIdleTimer(i.eventId),i.replied||this.resetActiveEventHardTimer(i.eventId)))}clearActiveEventIdleTimer(){this.activeEventIdleTimer&&(clearTimeout(this.activeEventIdleTimer),this.activeEventIdleTimer=null)}clearActiveEventHardTimer(){this.activeEventHardTimer&&(clearTimeout(this.activeEventHardTimer),this.activeEventHardTimer=null)}shouldExtendByLiveness(e){const t=this.activeEvent;if(!t||t.eventId!==e||!this.claudeCliSessionId||!this.claudeSessionCwd||!this.claudeProcess&&!this.claudePty)return!1;if(this.lastPtyOutputAt>0&&Date.now()-this.lastPtyOutputAt<fe)return t.livenessExtendStartAt=Date.now(),!0;const i=le(this.claudeCliSessionId,this.claudeSessionCwd,t.jsonlBaseOffset);if(i.freshMs===null||!(t.jsonlBaseOffset===void 0?i.freshMs<X:i.lastStopReason!==null?i.lastStopReason!=="end_turn":i.freshMs<X))return!1;if(i.freshMs<X)t.livenessExtendStartAt=Date.now();else{const n=t.livenessExtendStartAt??Date.now();if(t.livenessExtendStartAt===void 0&&(t.livenessExtendStartAt=n),Date.now()-n>pe)return a.warn("claude-adapter",`Liveness extension budget exhausted for ${e} (no JSONL writes for ${Math.round((Date.now()-n)/6e4)}min), allowing close`),!1}return a.info("claude-adapter",`Liveness check: turn in progress for ${e} (lastStopReason=${i.lastStopReason??"none"}, freshMs=${i.freshMs}), extending`),!0}captureEventJsonlBaseOffset(e){const t=this.activeEvent;if(!(!t||t.eventId!==e||t.jsonlBaseOffset!==void 0)&&!(!this.claudeCliSessionId||!this.claudeSessionCwd))try{const i=Y(this.claudeCliSessionId,this.claudeSessionCwd);t.jsonlBaseOffset=D(i)?L(i).size:0}catch{t.jsonlBaseOffset=0}}noteSelfDrivenActivity(){!this.selfDrivenActive&&this.lastClearedEvent&&Date.now()-this.lastClearedEvent.ts<ht||(this.selfDrivenLastSignalAt=Date.now(),!this.selfDrivenActive&&(this.selfDrivenActive=!0,a.info("claude-adapter",`Self-driven activity detected for session ${this.sessionId} (no active event), debouncing before reporting working state`),this.selfDrivenReportTimer=setTimeout(()=>{this.selfDrivenReportTimer=null,!(!this.selfDrivenActive||this.stopped||this.activeEvent)&&(this.selfDrivenReported=!0,a.info("claude-adapter",`Self-driven working state confirmed for session ${this.sessionId} (sustained >${me/1e3}s), reporting`),this.emit("sessionActivity",this.sessionId??"",!0))},me),this.selfDrivenReportTimer.unref(),this.selfDrivenSweepTimer=setInterval(()=>this.sweepSelfDriven(),ut),this.selfDrivenSweepTimer.unref()))}sweepSelfDriven(){if(!this.selfDrivenActive)return;if(this.stopped||this.activeEvent){this.stopSelfDriven();return}const e=Date.now()-this.selfDrivenLastSignalAt;if(e<dt){this.selfDrivenReported&&this.emit("sessionActivity",this.sessionId??"",!0);return}if(this.claudeCliSessionId&&this.claudeSessionCwd){const t=le(this.claudeCliSessionId,this.claudeSessionCwd),i=t.lastStopReason!==null&&t.lastStopReason!=="end_turn",s=t.freshMs!==null&&t.freshMs<pe;if(i&&s){this.selfDrivenReported&&this.emit("sessionActivity",this.sessionId??"",!0);return}}a.info("claude-adapter",`Self-driven activity ended for session ${this.sessionId} (quiet for ${Math.round(e/1e3)}s)`),this.stopSelfDriven()}stopSelfDriven(){this.selfDrivenSweepTimer&&(clearInterval(this.selfDrivenSweepTimer),this.selfDrivenSweepTimer=null),this.selfDrivenReportTimer&&(clearTimeout(this.selfDrivenReportTimer),this.selfDrivenReportTimer=null),this.selfDrivenActive&&(this.selfDrivenActive=!1,this.selfDrivenReported&&(this.selfDrivenReported=!1,this.emit("sessionActivity",this.sessionId??"",!1)))}resetActiveEventIdleTimer(e){this.clearActiveEventIdleTimer(),this.activeEventIdleTimer=setTimeout(()=>{if(!this.stopped&&this.activeEvent?.eventId===e){if(this.activeEvent?.toolCallInFlight){a.info("claude-adapter",`Idle timeout skipped: toolCallInFlight for ${e}, resetting timer`),this.resetActiveEventIdleTimer(e);return}if(this.activeEvent?.awaitingUserQuestion){a.info("claude-adapter",`Idle timeout skipped: awaitingUserQuestion for ${e}, resetting timer`),this.resetActiveEventIdleTimer(e);return}if(this.pendingPermissions.size>0){a.info("claude-adapter",`Idle timeout skipped: pendingPermissions=${this.pendingPermissions.size} for ${e}, resetting timer`),this.resetActiveEventIdleTimer(e);return}if(this.shouldExtendByLiveness(e)){this.resetActiveEventIdleTimer(e);return}this.finalizeStuckActiveEvent(e,"idle")}},ue)}finalizeStuckActiveEvent(e,t){const i=!!this.activeEvent?.replied,s=i?"responded":"failed",n=t==="idle"?ue:he,r=i?void 0:t==="idle"?`agent idle for ${n/1e3}s`:`agent exceeded max duration ${n/1e3}s`,o=i?void 0:t==="idle"?"agent_idle_timeout":"agent_hard_timeout";a.error("claude-adapter",`Active event ${t} timeout (${n/1e3}s): ${e}, replied=${i}, sending ${s}`),this.completedEventIds.set(e,Date.now()),this.bridgeCallbacks.sendEventResult(e,s,r,o),this.clearActiveEvent(),s==="failed"&&this.emit("stuck")}resetActiveEventHardTimer(e){this.clearActiveEventHardTimer(),this.activeEventHardTimer=setTimeout(()=>{if(!this.stopped&&this.activeEvent?.eventId===e){if(this.shouldExtendByLiveness(e)){this.resetActiveEventHardTimer(e);return}this.finalizeStuckActiveEvent(e,"hard")}},he)}clearActiveEventPostReplyTimer(){this.activeEventPostReplyTimer&&(clearTimeout(this.activeEventPostReplyTimer),this.activeEventPostReplyTimer=null)}clearActiveEventPostReplyWatcher(){if(this.activeEventPostReplyWatcher){try{this.activeEventPostReplyWatcher.close()}catch{}this.activeEventPostReplyWatcher=null}this.activeEventPostReplyPoll&&(clearInterval(this.activeEventPostReplyPoll),this.activeEventPostReplyPoll=null)}startPostReplyJsonlWatcher(e){if(this.clearActiveEventPostReplyWatcher(),!this.claudeCliSessionId||!this.claudeSessionCwd)return;const t=Y(this.claudeCliSessionId,this.claudeSessionCwd);if(!this.activeEvent?.sessionId)return;if(!D(t)){a.info("claude-adapter",`JSONL watcher skipped: file not yet available for ${e}`);return}const s=L(t).size;this.activeEvent&&(this.activeEvent.jsonlBaseOffset=s);let n=null;try{this.activeEventPostReplyWatcher=xe(t,()=>{n&&clearTimeout(n),n=setTimeout(()=>this.finalizeIfEndTurn(e),300)}),this.activeEventPostReplyPoll=setInterval(()=>this.finalizeIfEndTurn(e),ct),a.info("claude-adapter",`JSONL watcher started for ${e} at ${t} (baseOffset=${s})`)}catch(r){a.warn("claude-adapter",`JSONL watcher start failed for ${e}: ${r}`)}}finalizeIfEndTurn(e){if(this.stopped||this.activeEvent?.eventId!==e||!this.claudeCliSessionId||!this.claudeSessionCwd)return;const t=B(this.claudeCliSessionId,this.claudeSessionCwd,this.activeEvent.jsonlBaseOffset);if(t&&t.stopReason==="end_turn"){a.info("claude-adapter",`JSONL watcher: end_turn detected for ${e}, finalizing as responded`);const i=this.activeEvent.sessionId;this.bridgeCallbacks.sendEventResult(e,"responded",void 0,void 0),this.clearActiveEvent(),this.armStopHookBarrier(i);return}if(this.activeEvent?.replied&&this.activeEvent.jsonlBaseOffset!==void 0&&oe(this.claudeCliSessionId,this.claudeSessionCwd,this.activeEvent.jsonlBaseOffset,!0)){if(this.lastPtyOutputAt>0&&Date.now()-this.lastPtyOutputAt<fe)return;a.info("claude-adapter",`JSONL poll: terminal tool_result detected for ${e} (file quiet, no end_turn), finalizing as responded`),this.bridgeCallbacks.sendEventResult(e,"responded",void 0,void 0),this.clearActiveEvent()}}startPostReplyDeadline(e){this.clearActiveEventPostReplyTimer(),this.clearActiveEventHardTimer(),this.activeEventPostReplyTimer=setTimeout(()=>{if(this.stopped||this.activeEvent?.eventId!==e||!this.activeEvent?.replied)return;if(this.shouldExtendByLiveness(e)){this.startPostReplyDeadline(e);return}const t=this.activeEvent.repliedAt,i=Math.round((Date.now()-t)/1e3);a.info("claude-adapter",`Post-reply deadline reached for ${e} (${i}s since reply), completing as responded`),this.bridgeCallbacks.sendEventResult(e,"responded",void 0,void 0),this.clearActiveEvent()},rt)}beginCompaction(){this.compacting||(this.compacting=!0,a.info("claude-adapter","Compaction started; gating input"),this.emit("pauseIntake","compaction"),this.resetCompactingActivityTimer())}resetCompactingActivityTimer(){this.compactingTimer&&clearTimeout(this.compactingTimer),this.compactingTimer=setTimeout(()=>{a.warn("claude-adapter","Compaction stall: no PTY activity for 90s, emitting stuck"),this.emit("stuck")},9e4),this.compactingTimer.unref?.()}finishCompaction(e){if(!this.compacting)return;this.compacting=!1,this.compactingTimer&&(clearTimeout(this.compactingTimer),this.compactingTimer=null),a.info("claude-adapter",`Compaction finished (${e}); resuming intake`),this.emit("resumeIntake","compaction");const t=this.compactionDoneResolver;this.compactionDoneResolver=null,t?.(e)}armStopHookBarrier(e){this.stopHookBarrierTimer&&clearTimeout(this.stopHookBarrierTimer),this.stopHookBarrierSessionId=e,this.emit("pauseIntake","barrier"),this.stopHookBarrierTimer=setTimeout(()=>{this.stopHookBarrierSessionId===e&&(a.warn("claude-adapter",`Stop hook barrier timeout for session=${e}`),this.clearStopHookBarrier())},mt)}clearStopHookBarrier(){const e=this.stopHookBarrierSessionId!==null;this.stopHookBarrierSessionId=null,this.stopHookBarrierTimer&&(clearTimeout(this.stopHookBarrierTimer),this.stopHookBarrierTimer=null),e&&this.emit("resumeIntake","barrier")}clearPendingMcpFailureTimer(){this.pendingMcpFailureTimer&&(clearTimeout(this.pendingMcpFailureTimer),this.pendingMcpFailureTimer=null)}markMcpStartupFailure(){if(!this.mcpStartupFailureHandled&&(this.mcpStartupFailureHandled=!0,this.mcpChannelBroken=!0,a.error("claude-adapter","Claude reported blocking MCP server startup failure"),this.activeEvent)){const e=this.activeEvent.eventId,t=this.activeEvent.sessionId;this.bridgeCallbacks.sendStreamChunk(e,t,`
18
18
 
19
19
  Error: MCP server startup failed`,1,!1),this.bridgeCallbacks.sendEventResult(e,"failed","MCP server startup failed"),this.completedEventIds.set(e,Date.now()),this.clearActiveEvent()}}}class _t extends re{adapterSessionId;constructor(e){super(),this.adapterSessionId=e}emitDone(e){this.emit("done",e)}emitError(e){if(this.listenerCount("error")===0){a.warn("claude-adapter",`Prompt handle error (no listeners): ${e.message}`);return}this.emit("error",e)}async cancel(){}}function y(d){if(d==null)return 0;const e=Number(d);return Number.isFinite(e)?e:0}function J(d){return String(d).replace(/\\/g,"\\\\").replace(/\{/g,"\\{").replace(/\}/g,"\\}")}async function Et(d,e,t){const i=h(d,"claude.expect"),s=h(d,"claude.pid"),n=["log_user 1","set timeout -1","set startup_prompt_armed 1",`set cmd_fifo {${J(h(d,"cmd.fifo"))}}`,"file delete -force $cmd_fifo","exec mkfifo $cmd_fifo","set cmd_fd [open $cmd_fifo r+]","fconfigure $cmd_fd -blocking 0 -buffering line",`set claude_command [list {${J(e)}}${t.map(r=>` {${J(r)}}`).join("")}]`,"spawn -noecho {*}$claude_command",`set pid_file [open {${J(s)}} w]`,"puts $pid_file [exp_pid -i $spawn_id]","close $pid_file","after 500",'send -- "\\r"',"expect {"," -re {(?i)(Quick.*safety.*check|trust.*folder)} {",' if {$startup_prompt_armed} { send -- "1\\r"; after 300 }; exp_continue'," }"," -re {(?i)(I am using this for local development|Please use --channels|dangerously-load-development-channels)} {",' if {$startup_prompt_armed} { send -- "\\r"; after 300 }; exp_continue'," }"," -re {(?i)(Enter.*confirm|Press.*Enter|Hit.*Enter|Continue.*Enter)} {",' if {$startup_prompt_armed} { send -- "\\r"; after 300 }; exp_continue'," }"," -re {(?i)Listening.*channel messages.*server:grix} {"," set startup_prompt_armed 0"," }"," eof {}","}","proc handle_cmd {} {"," upvar cmd_fd cmd_fd cmd_fifo cmd_fifo"," if {[catch {gets $cmd_fd} __line]} { return }"," if {[eof $cmd_fd]} {"," catch {close $cmd_fd}"," if {[catch {set cmd_fd [open $cmd_fifo r+]} err]} { return }"," fconfigure $cmd_fd -blocking 0 -buffering line"," fileevent $cmd_fd readable handle_cmd",' } elseif {$__line ne ""} {',' send -- "$__line\\r"'," }","}","fileevent $cmd_fd readable handle_cmd","expect_background {"," -re .+ { }"," eof { set ::__done 1 }","}","set ::__done 0","vwait ::__done",""];return await C(s,"","utf8"),await C(i,n.join(`
@@ -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};
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}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 re(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 ie(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(),r=s.toLowerCase();!s||t.has(r)||(t.add(r),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=re(t),a=String(n.ws_url??"").trim(),s="get_session_usage",r="get_rate_limits",c="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=o.adapterType,u=l==="acp",f=t==="qwen",m={...o.options??{}},i=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",s,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",s,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",s],adapterHint:"qwen/base"}:null,_=l==="pi"?{adapterHint:"pi/base",capabilities:["local_action_v1"],localActions:["session_control","set_model","get_context","file_list","create_folder",s]}:null,h=l==="openhuman"?{adapterHint:"openhuman/base",capabilities:["local_action_v1"],localActions:["session_control","set_model","file_list","create_folder",s]}: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",s,r]}:null,y=l==="codewhale"?{capabilities:["stream_chunk","local_action_v1"],localActions:["session_control","set_model","file_list","create_folder",s]}: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",s]}:null,T=l==="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,r]}:null,O=l==="codex"||l==="claude"||t==="gemini"?["session_control","set_model","set_mode"]:void 0,N=[s,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=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:l,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:i?.capabilities??y?.capabilities??_?.capabilities??h?.capabilities??w?.capabilities??k?.capabilities??T?.capabilities??g?.capabilities??["stream_chunk","local_action_v1","connector_upgrade"],localActions:le(i?.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",c]),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:ie(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(l,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(c=>c.endsWith(".json")).sort();if(o.length===0)throw new Error(`No config files found in ${t}`);const a=[];let s=0;for(const c of o)try{const l=Q(x(t,c),"utf-8"),u=JSON.parse(l);if(Array.isArray(u.agents)){if(u.agents.length===0){d.error("manager",`No agents array found in ${c}`),s++;continue}for(const f of u.agents)try{const m=P(f);a.push({config:m,file:c}),d.info("manager",`Loaded ${m.name} (${m.adapterType??"acp"}) from ${c}`)}catch(m){const i=typeof f?.name=="string"?f.name:"<unknown>";d.error("manager",`Invalid agent config in ${c} (name=${i}): ${m}`),s++}}else d.error("manager",`Unrecognized config format in ${c}`)}catch(l){d.error("manager",`Failed to load ${c}: ${l}`)}let r=0;if(a.length>0){const c=ue();d.info("manager",`Starting ${a.length} agent(s), startup wait=${c}ms`);const l=()=>this.upgradeChecker?.triggerCheck(),u=i=>{this.instances=this.instances.filter(p=>p!==i)},f=a.map(({config:i})=>{const p=new A(i,this.globalConfigStore);return p.setUpgradeTrigger(l),p.setShareSetHandler(g=>this.onShareSet(i,g)),this.instances.push(p),this.configMap.set(i.name,i),{config:i,instance:p,startPromise:p.start()}}),m=await Promise.all(f.map(async i=>{const p=await new Promise(g=>{let _=!1;const h=setTimeout(()=>{_||(_=!0,g({kind:"timeout"}))},c);i.startPromise.then(()=>{_||(_=!0,clearTimeout(h),g({kind:"started"}))}).catch(w=>{_||(_=!0,clearTimeout(h),g({kind:"failed",error:w}))})});return{task:i,outcome:p}}));for(const{task:i,outcome:p}of m)if(p.kind!=="started"){if(p.kind==="failed"){u(i.instance),d.error("manager",`Failed to start ${i.config.name}: ${p.error}`);continue}r++,d.warn("manager",`Startup pending for ${i.config.name}, continue retrying in background`),i.startPromise.then(()=>{d.info("manager",`Delayed start succeeded: ${i.config.name}`)}).catch(g=>{u(i.instance),d.error("manager",`Delayed start failed: ${i.config.name}: ${g}`)})}if(this.instances.length>0){const i=Math.max(0,this.instances.length-r);d.info("manager",`${i}/${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 K(a.map(({config:c})=>({apiKey:c.aibot.apiKey,wsUrl:c.aibot.url})),()=>this.instances.some(c=>c.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 r=z(e.name,s);try{const c=L(e,s),l=new A(c,this.globalConfigStore);this.sharedInstances.set(r,l),await l.start(),d.info("manager",`shared instance started: ${r}`)}catch(c){this.sharedInstances.delete(r),d.error("manager",`start shared instance failed ${r}: ${c}`)}}for(const{key:s}of a){const r=this.sharedInstances.get(s);r&&(this.sharedInstances.delete(s),r.stop().catch(c=>d.error("manager",`stop shared instance failed ${s}: ${c}`)),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(r=>r.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[r,c]of[...this.sharedInstances.entries()])r.startsWith(s)&&(this.sharedInstances.delete(r),c.stop().catch(l=>d.error("manager",`stop shared instance failed ${r}: ${l}`)));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(r=>r.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 i=n.length;if(i===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<i?p(f++):m===i&&u()}const _=Math.min(t,i);for(let h=0;h<_;h++)p(f++)});const s=a.filter(u=>u.status==="healthy").length,r=a.filter(u=>u.status==="degraded").length,c=a.filter(u=>u.status==="unavailable").length,l=await se();return{ok:s===a.length&&a.length>0,total:a.length,healthy:s,degraded:r,unavailable:c,installed_clients:l,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 +1 @@
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};
1
+ function a(e){const t=new Set([`http://127.0.0.1:${e.serverPort}`,`http://localhost:${e.serverPort}`,...e.allowedOrigins]),o=new Set([`127.0.0.1:${e.serverPort}`,`localhost:${e.serverPort}`,...e.allowedHosts]);return{validateRequest(s){const r=i(s,t);if(!r.ok)return r;const n=l(s,o);return n.ok?{ok:!0}:n}}}function i(e,t){const o=e.headers.origin;return o?t.has(o)?{ok:!0}:{ok:!1,statusCode:403,message:`Origin not allowed: ${o}`}:{ok:!0}}function l(e,t){const o=e.headers.host;return o?t.has(o)?{ok:!0}:{ok:!1,statusCode:403,message:`Host not allowed: ${o}`}:{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.1",
3
+ "version": "2.2.3",
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