grix-connector 2.2.0 → 2.2.2
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
|
@@ -147,6 +147,37 @@ grix-connector restart # Restart the service
|
|
|
147
147
|
grix-connector status # Check service status
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
+
### Ports
|
|
151
|
+
|
|
152
|
+
The daemon binds two local loopback ports (127.0.0.1 only):
|
|
153
|
+
|
|
154
|
+
| Purpose | Default | Override (env) | Override (CLI) |
|
|
155
|
+
|---|---|---|---|
|
|
156
|
+
| Health check (`/healthz`) | `19579` | `GRIX_HEALTH_PORT` | `--health-port <port>` |
|
|
157
|
+
| Admin API (used by the local CLI) | `19580` | `GRIX_ADMIN_PORT` | `--admin-port <port>` |
|
|
158
|
+
|
|
159
|
+
If a port is already in use, the daemon refuses to start and writes a clear message to `~/.grix/service/daemon.err.log` and the main log, and marks `~/.grix/daemon-status.json` as `state: "failed"` with a `reason` like `port_bind_in_use:health:19579`.
|
|
160
|
+
|
|
161
|
+
To pick different ports:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
# via environment
|
|
165
|
+
GRIX_HEALTH_PORT=29579 GRIX_ADMIN_PORT=29580 grix-connector restart
|
|
166
|
+
|
|
167
|
+
# or via CLI flags (when running the daemon directly)
|
|
168
|
+
grix-connector --health-port 29579 --admin-port 29580
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
To find what is occupying a port:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# macOS / Linux
|
|
175
|
+
lsof -nP -iTCP:19579 -sTCP:LISTEN
|
|
176
|
+
|
|
177
|
+
# Windows (PowerShell or cmd)
|
|
178
|
+
netstat -ano | findstr :19579
|
|
179
|
+
```
|
|
180
|
+
|
|
150
181
|
## OpenClaw Plugin
|
|
151
182
|
|
|
152
183
|
grix-connector can also be installed as an [OpenClaw](https://openclaw.io) plugin, providing a Grix channel transport with admin tools and operator CLI.
|
|
@@ -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=
|
|
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 +1 @@
|
|
|
1
|
-
import{HealthServer as
|
|
1
|
+
import{HealthServer as o}from"./health.js";import{writePidFile as t,removePidFile as l}from"./pidfile.js";import{bindPortOrFail as d}from"./port-bind.js";export{o as HealthServer,d as bindPortOrFail,l as removePidFile,t as writePidFile};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
async function l(n){try{return await n.start(n.port),null}catch(e){const r=i(e);if(r==="EADDRINUSE")return{kind:"in_use",port:n.port,label:n.label,message:`[grix-connector] ${n.label} \u7AEF\u53E3 ${n.port} \u5DF2\u88AB\u5360\u7528\uFF0Cdaemon \u65E0\u6CD5\u542F\u52A8\u3002
|
|
2
|
+
\u8BF7\u91CA\u653E\u8BE5\u7AEF\u53E3\uFF0C\u6216\u901A\u8FC7\u73AF\u5883\u53D8\u91CF ${n.envVar} / \u547D\u4EE4\u884C\u53C2\u6570 --${n.cliFlag} \u6307\u5B9A\u5176\u4ED6\u7AEF\u53E3\u540E\u91CD\u65B0\u542F\u52A8\u3002
|
|
3
|
+
\u67E5\u5360\u7528\u8FDB\u7A0B\u793A\u4F8B\uFF1Alsof -nP -iTCP:${n.port} -sTCP:LISTEN (Windows: netstat -ano | findstr :${n.port})`};if(r==="EACCES")return{kind:"no_permission",port:n.port,label:n.label,message:`[grix-connector] \u5F53\u524D\u7528\u6237\u65E0\u6743\u76D1\u542C ${n.label} \u7AEF\u53E3 ${n.port}\uFF08EACCES\uFF0C1024 \u4EE5\u4E0B\u7AEF\u53E3\u901A\u5E38\u9700\u8981\u7279\u6743\uFF09\u3002
|
|
4
|
+
\u8BF7\u6539\u7528 1024 \u4EE5\u4E0A\u7684\u7AEF\u53E3\uFF1A\u73AF\u5883\u53D8\u91CF ${n.envVar} \u6216\u547D\u4EE4\u884C\u53C2\u6570 --${n.cliFlag}\u3002`};const a=e instanceof Error?e.stack??e.message:String(e);return{kind:"other",port:n.port,label:n.label,message:`[grix-connector] ${n.label} \u7AEF\u53E3 ${n.port} \u7ED1\u5B9A\u5931\u8D25\uFF1A${a}
|
|
5
|
+
\u53EF\u901A\u8FC7\u73AF\u5883\u53D8\u91CF ${n.envVar} \u6216\u547D\u4EE4\u884C\u53C2\u6570 --${n.cliFlag} \u66F4\u6362\u7AEF\u53E3\u540E\u91CD\u8BD5\u3002`}}}function i(n){if(n&&typeof n=="object"&&"code"in n){const e=n.code;return typeof e=="string"?e:void 0}}export{l as bindPortOrFail};
|
package/dist/grix.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import g from"node:path";import{writeFileSync as
|
|
2
|
+
import g from"node:path";import{writeFileSync as v}from"node:fs";import{Manager as $}from"./manager.js";import{ensureGrixDirs as L,initLogger as N,log as n,installProcessLogRotation as C,setConsoleOutput as U}from"./core/log/index.js";import{HealthServer as H,bindPortOrFail as P}from"./core/runtime/index.js";import{writePidFile as G,removePidFile as f}from"./core/runtime/index.js";import{resolveRuntimePaths as R}from"./core/config/index.js";import{ServiceManager as j}from"./service/service-manager.js";import{killProcessesByCommandLine as M,isWindowsElevated as W}from"./service/process-control.js";import{acquireDaemonLock as B,releaseDaemonLock as h}from"./runtime/daemon-lock.js";import{writeDaemonStatus as E,removeDaemonStatus as X}from"./runtime/service-state.js";import{AdminServer as V,generateToken as q,writeTokenFile as J}from"./core/admin/index.js";import{initSentry as K,closeSentry as k,reportFatal as x}from"./core/observability/sentry.js";const l=process.argv.slice(2),A=[],s={};for(let t=0;t<l.length;t++)l[t].startsWith("--")&&l[t+1]&&!l[t+1].startsWith("--")?(s[l[t].slice(2)]=l[t+1],t++):l[t].startsWith("--")?s[l[t].slice(2)]="true":A.push(l[t]);s.help&&(console.log(`grix-connector \u2014 Unified AI Agent Bridge
|
|
3
3
|
|
|
4
4
|
Usage: grix-connector <command> [options]
|
|
5
5
|
|
|
@@ -29,7 +29,8 @@ Examples:
|
|
|
29
29
|
grix-connector start # Start as system service
|
|
30
30
|
grix-connector status # Check service status
|
|
31
31
|
grix-connector restart # Restart the service
|
|
32
|
-
`),process.exit(0));const
|
|
32
|
+
`),process.exit(0));const m=A[0],F=["start","stop","restart","status"];if(m&&F.includes(m)){process.platform==="win32"&&["start","stop","restart"].includes(m)&&!W()&&console.warn(`Warning: Not running as administrator. Task Scheduler registration is skipped;
|
|
33
33
|
using Startup folder auto-start instead. For full Task Scheduler integration,
|
|
34
|
-
right-click the terminal and select "Run as administrator".`);const t=
|
|
35
|
-
Valid commands: ${
|
|
34
|
+
right-click the terminal and select "Run as administrator".`);const t=R(),p=s["config-dir"]??(s.profile?g.join(t.configDir,s.profile):void 0),d=g.resolve(process.argv[1]||`${t.rootDir}/dist/grix.js`),r=new j({cliPath:d,nodePath:process.execPath});try{let a;switch(m){case"start":(await r.status({rootDir:t.rootDir})).installed?a=await r.start({rootDir:t.rootDir}):a=await r.install({rootDir:t.rootDir,configDir:p});break;case"stop":a=await r.stop({rootDir:t.rootDir});break;case"restart":(await r.status({rootDir:t.rootDir})).installed?a=await r.restart({rootDir:t.rootDir}):a=await r.install({rootDir:t.rootDir,configDir:p});break;case"status":a=await r.status({rootDir:t.rootDir});break}console.log(JSON.stringify(a,null,2)),process.exit(0)}catch(a){console.error(`${m} failed: ${a instanceof Error?a.message:a}`),process.exit(1)}}else m&&(console.error(`Unknown command: ${m}
|
|
35
|
+
Valid commands: ${F.join(", ")}`),process.exit(1));const o=R(),z=s["config-dir"]??(s.profile?`${o.configDir}/${s.profile}`:void 0),i=new $,S=new H,I=q(),u=new V(I);let T=!1;async function b(t){process.stderr.write(t.message+`
|
|
36
|
+
`),n.error("main",t.message.replace(/\n/g," \u2014 ")),await E(o.daemonStatusFile,{state:"failed",pid:process.pid,updated_at:Date.now(),reason:`port_bind_${t.kind}:${t.label}:${t.port}`}).catch(()=>{}),await k(),h(o.daemonLockFile).catch(()=>{}),f(),process.exit(1)}async function D(t){if(T)return;T=!0,n.info("main",`Received ${t}, shutting down...`),S.markShuttingDown();const p=setTimeout(()=>{n.error("main","Shutdown timed out, forcing exit"),h(o.daemonLockFile).catch(()=>{}),f(),process.exit(2)},1e4);try{await i.stop(),await u.stop(),await S.stop(),await k(),await h(o.daemonLockFile),await X(o.daemonStatusFile).catch(()=>{}),clearTimeout(p),f(),n.info("main","Shutdown complete"),process.exit(0)}catch(d){n.error("main",`Shutdown error: ${d}`),h(o.daemonLockFile).catch(()=>{}),f(),process.exit(2)}}async function Q(){L(),N(),await K(),C(o.stdoutLogFile,o.stderrLogFile),U(!1),process.platform==="win32"&&await M("GrixConnectorDaemon",{platform:"win32"});try{await B(o.daemonLockFile,o.rootDir)}catch(e){console.error(e instanceof Error?e.message:e),process.exit(1)}G(),n.info("main",`grix-connector starting (PID ${process.pid})`),await E(o.daemonStatusFile,{state:"starting",pid:process.pid,updated_at:Date.now()});const t=parseInt(s["health-port"]??process.env.GRIX_HEALTH_PORT??"19579",10);{const e=await P({label:"health",port:t,envVar:"GRIX_HEALTH_PORT",cliFlag:"health-port",start:c=>S.start(c)});e&&await b(e)}const p=g.join(o.dataDir,"health-port");v(p,String(t),"utf-8"),process.on("SIGINT",()=>D("SIGINT")),process.on("SIGTERM",()=>D("SIGTERM"));let d="",r=0,a;process.on("uncaughtException",e=>{const c=e instanceof Error?e.stack??e.message:String(e);c===d?(r++,(r<=3||r%100===0)&&n.error("main",`Uncaught exception (x${r}): ${c}`)):(r>3&&n.error("main",`Previous exception repeated ${r} times total`),d=c,r=1,n.error("main",`Uncaught exception: ${e instanceof Error?e.stack:e}`),a||(a=setTimeout(()=>{r>3&&n.error("main",`Previous exception repeated ${r} times total`),d="",r=0,a=void 0},1e4).unref())),!y(e)&&(x(e,"uncaughtException"),D("uncaughtException"))}),process.on("unhandledRejection",e=>{n.error("main",`Unhandled rejection: ${e}`),!y(e)&&(x(e,"unhandledRejection"),D("unhandledRejection"))}),S.setStatusProvider(()=>i.getAgentsStatus()),await i.start(z);const w=parseInt(s["admin-port"]??process.env.GRIX_ADMIN_PORT??"19580",10);u.setAgentHandler({list:()=>i.getAgentsStatus(),add:e=>i.addAgent(e),remove:e=>i.removeAgent(e),restart:e=>i.restartAgent(e)}),u.setUpgradeHandler({check:()=>i.checkUpgrade(),trigger:()=>i.triggerUpgrade()}),u.setProbeHandler({probeAll:e=>i.probeAll(e),probeOne:(e,c)=>i.probeOne(e,c)}),u.setInstallHandler({listInstallable:()=>i.listInstallable(),installAgent:e=>i.installAgent(e),getInstallProgress:e=>i.getInstallProgress(e)});{const e=await P({label:"admin",port:w,envVar:"GRIX_ADMIN_PORT",cliFlag:"admin-port",start:c=>u.start(c)});e&&await b(e)}const O=g.join(o.dataDir,"admin-token"),_=g.join(o.dataDir,"admin-port");J(O,I),v(_,String(w),"utf-8"),await E(o.daemonStatusFile,{state:"running",pid:process.pid,updated_at:Date.now()}),process.send&&process.send("ready"),n.info("main","grix-connector ready")}Q().catch(async t=>{n.error("main",`Fatal: ${t}`),x(t,"startup"),await k(),h(o.daemonLockFile).catch(()=>{}),f(),process.exit(1)});const Y=new Set(["ECONNRESET","ECONNREFUSED","ETIMEDOUT","EPIPE","EAI_AGAIN","ENOTFOUND","EHOSTUNREACH","ENETUNREACH","EIO"]);function y(t){return t instanceof Error&&"code"in t?Y.has(t.code):!1}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
function a(
|
|
1
|
+
function a(t){const o=new Set([`http://127.0.0.1:${t.serverPort}`,`http://localhost:${t.serverPort}`,...t.allowedOrigins]),e=new Set([`127.0.0.1:${t.serverPort}`,`localhost:${t.serverPort}`,...t.allowedHosts]);return{validateRequest(s){const r=i(s,o);if(!r.ok)return r;const n=l(s,e);return n.ok?{ok:!0}:n}}}function i(t,o){const e=t.headers.origin;return e?o.has(e)?{ok:!0}:{ok:!1,statusCode:403,message:`Origin not allowed: ${e}`}:{ok:!0}}function l(t,o){const e=t.headers.host;return e?o.has(e)?{ok:!0}:{ok:!1,statusCode:403,message:`Host not allowed: ${e}`}:{ok:!1,statusCode:403,message:"Missing Host header"}}export{a as createSecurityPolicy};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "grix-connector",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.2",
|
|
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",
|