grix-connector 1.1.5 → 1.1.7

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.
Files changed (46) hide show
  1. package/README.md +28 -5
  2. package/dist/adapter/agy/agy-adapter.js +2 -2
  3. package/dist/adapter/claude/claude-adapter.js +19 -19
  4. package/dist/adapter/claude/claude-bridge-server.js +1 -0
  5. package/dist/adapter/claude/claude-tools.js +1 -0
  6. package/dist/adapter/claude/claude-worker-client.js +1 -0
  7. package/dist/adapter/claude/mcp-http-launcher.js +2 -0
  8. package/dist/adapter/claude/result-timeout.js +1 -0
  9. package/dist/adapter/claude/usage-parser.js +4 -3
  10. package/dist/adapter/deepseek/deepseek-adapter.js +3 -3
  11. package/dist/adapter/pi/pi-adapter.js +3 -3
  12. package/dist/adapter/qwen/index.js +1 -0
  13. package/dist/adapter/qwen/qwen-adapter.js +4 -0
  14. package/dist/aibot/client.js +1 -0
  15. package/dist/aibot/index.js +1 -0
  16. package/dist/aibot/types.js +0 -0
  17. package/dist/bridge/acp-toolbar-persist.js +1 -1
  18. package/dist/bridge/adapter-pool.js +1 -1
  19. package/dist/bridge/bridge.js +9 -9
  20. package/dist/core/file-ops/handler.js +1 -0
  21. package/dist/core/file-ops/list-files.js +1 -0
  22. package/dist/core/file-ops/types.js +0 -0
  23. package/dist/core/installer/installer.js +5 -5
  24. package/dist/core/installer/npm-registry.js +2 -2
  25. package/dist/core/upgrade/npm-upgrader.js +2 -2
  26. package/dist/grix.js +0 -0
  27. package/dist/log.js +3 -0
  28. package/dist/main.js +31 -0
  29. package/dist/mcp/stream-http/config.js +1 -0
  30. package/dist/mcp/stream-http/connection-binding.js +1 -0
  31. package/dist/mcp/stream-http/event-tool-executor.js +1 -0
  32. package/dist/mcp/stream-http/gateway.js +1 -0
  33. package/dist/mcp/stream-http/index.js +1 -0
  34. package/dist/mcp/stream-http/security.js +1 -0
  35. package/dist/mcp/stream-http/session-manager.js +1 -0
  36. package/dist/mcp/stream-http/tool-executor.js +1 -0
  37. package/dist/mcp/stream-http/tool-registry.js +1 -0
  38. package/dist/mcp/stream-http/tool-schemas.js +1 -0
  39. package/dist/protocol/acp-client.js +1 -1
  40. package/dist/session/index.js +1 -0
  41. package/dist/session/manager.js +1 -0
  42. package/dist/transport/index.js +1 -0
  43. package/dist/transport/json-rpc.js +3 -0
  44. package/package.json +1 -1
  45. package/scripts/install-guardian.sh +0 -0
  46. package/scripts/upgrade-guardian.sh +0 -0
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # grix-connector
2
2
 
3
- A command-line daemon that connects your local AI coding agents to the [Grix](https://grix.dhf.pub) platform.
3
+ A command-line daemon that connects your local AI coding agents to the [Grix](https://grix.im) platform.
4
4
 
5
5
  ## What is Grix?
6
6
 
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.dhf.pub](https://grix.dhf.pub) to get started.
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
9
  ## Supported Agents
10
10
 
@@ -38,11 +38,16 @@ On Windows, `grix-connector` uses the built-in Task Scheduler with a hidden WScr
38
38
 
39
39
  ### 1. Register a Grix account
40
40
 
41
- Go to [grix.dhf.pub](https://grix.dhf.pub), sign up and get your API key.
41
+ Go to [grix.im](https://grix.im), sign up and get your API key.
42
42
 
43
43
  ### 2. Create agent config
44
44
 
45
- Create `~/.grix/config/agents.json`:
45
+ Create `~/.grix/config/agents.json`. Choose the `ws_url` for your region — the two regions use different WebSocket domains:
46
+
47
+ - China mainland: `wss://grix.dhf.pub/v1/agent-api/ws`
48
+ - Global: `wss://ws.grix.im/v1/agent-api/ws`
49
+
50
+ China mainland example:
46
51
 
47
52
  ```json
48
53
  {
@@ -58,6 +63,22 @@ Create `~/.grix/config/agents.json`:
58
63
  }
59
64
  ```
60
65
 
66
+ Global example:
67
+
68
+ ```json
69
+ {
70
+ "agents": [
71
+ {
72
+ "name": "my-agent",
73
+ "ws_url": "wss://ws.grix.im/v1/agent-api/ws",
74
+ "agent_id": "your-agent-id",
75
+ "api_key": "your-grix-api-key",
76
+ "client_type": "claude"
77
+ }
78
+ ]
79
+ }
80
+ ```
81
+
61
82
  Change `client_type` to match the agent you want to connect (see table above). You can define multiple agents in one file, or use separate files under `~/.grix/config/`.
62
83
 
63
84
  ### Config Reference
@@ -67,7 +88,7 @@ Each agent entry uses one flat structure:
67
88
  | Field | Required | Description |
68
89
  |---|---|---|
69
90
  | `name` | yes | Display name for this agent |
70
- | `ws_url` | yes | WebSocket endpoint URL, e.g. `wss://grix.dhf.pub/v1/agent-api/ws` |
91
+ | `ws_url` | yes | WebSocket endpoint URL (region-specific). China mainland: `wss://grix.dhf.pub/v1/agent-api/ws`; Global: `wss://ws.grix.im/v1/agent-api/ws` |
71
92
  | `agent_id` | yes | Agent ID from Grix platform |
72
93
  | `api_key` | yes | API key for authentication |
73
94
  | `client_type` | yes | See Supported Agents table above |
@@ -79,6 +100,8 @@ Adapter command/args/options are built in and resolved from `client_type`. To co
79
100
 
80
101
  ### Multi-agent Example
81
102
 
103
+ China mainland region (swap the `ws_url` domain to `ws.grix.im` for the global region):
104
+
82
105
  ```json
83
106
  {
84
107
  "agents": [
@@ -1,2 +1,2 @@
1
- import{spawn as m}from"node:child_process";import{randomUUID as E}from"node:crypto";import{EventEmitter as g}from"node:events";import{existsSync as y,readFileSync as S,readdirSync as I,statSync as P}from"node:fs";import{homedir as b}from"node:os";import{join as p}from"node:path";import{log as r}from"../../core/log/index.js";import{buildSimpleProbeReport as w}from"../shared/probe-util.js";const o="agy-adapter",h=p(b(),".gemini","antigravity-cli","log");class G extends g{type="agy";config;callbacks;runtimeResolver;alive=!1;stopped=!1;activeEventId=null;activeEventSessionId=null;activeProcess=null;completedEventIds=new Set;sessionConversationMap=new Map;eventQueue=[];constructor(t,e,s){super(),this.config=t,this.callbacks=e,this.runtimeResolver=s}async start(){this.alive||(this.alive=!0,this.stopped=!1,r.info(o,"AgyAdapter started"))}async stop(){if(!this.stopped){if(this.stopped=!0,this.alive=!1,this.activeProcess){try{this.activeProcess.kill("SIGKILL")}catch{}this.activeProcess=null}if(this.activeEventId&&this.activeEventSessionId)try{this.callbacks.sendEventResult(this.activeEventId,"canceled","adapter stopped")}catch{}this.activeEventId=null,this.activeEventSessionId=null,this.emit("exit",0),r.info(o,"AgyAdapter stopped")}}isAlive(){return this.alive}async createSession(t){return t.cwd??E()}async resumeSession(t,e){}async destroySession(t){}sendPrompt(t){const e=new g;return e.adapterSessionId=t.adapterSessionId,e.cancel=async()=>{e.emit("done",{status:"canceled"})},e}async cancel(t){}cancelCurrentRun(){if(!this.activeEventId||!this.activeProcess)return;const t=this.activeEventId;r.info(o,`cancelCurrentRun: killing process for event ${t}`);try{this.activeProcess.kill("SIGKILL")}catch{}this.activeProcess=null,this.callbacks.sendEventResult(t,"canceled","restarted by user"),this.clearActiveEvent(t),this.drainQueue()}deliverInboundEvent(t){if(!this.stopped){if(this.completedEventIds.has(t.event_id)){r.debug(o,`Skipping duplicate event: ${t.event_id}`);return}if(this.callbacks.sendEventAck(t.event_id,t.session_id),this.activeEventId){r.info(o,`Queuing event ${t.event_id} (active: ${this.activeEventId})`),this.eventQueue.push(t);return}this.processEvent(t)}}deliverStopEvent(t,e){if(this.activeEventId===t&&this.activeProcess){r.info(o,`Stopping event ${t}`);try{this.activeProcess.kill("SIGKILL")}catch{}this.activeProcess=null,this.callbacks.sendEventResult(t,"canceled","stopped by user"),this.clearActiveEvent(t),this.drainQueue()}}setPermissionHandler(){}async ping(t){return this.alive}getStatus(){return{alive:this.alive,busy:this.activeEventId!==null,sessions:this.sessionConversationMap.size}}async probe(t){const e=this.getStatus();return w(this.config.command||"agy",{alive:e.alive,busy:e.busy,started:e.alive},t)}getActiveEventIds(){return this.activeEventId?[this.activeEventId]:[]}clearActiveEventForShutdown(){this.activeEventId=null,this.activeEventSessionId=null,this.activeProcess=null}getMcpConfig(){return null}processEvent(t){const{event_id:e,session_id:s,content:i}=t,n=this.runtimeResolver(s);if(!n.cwd){r.warn(o,`No working directory for session ${s}, event should have been intercepted by bridge`),this.callbacks.forceCompleteInternalEvent(e,s);return}this.activeEventId=e,this.activeEventSessionId=s,this.emit("eventStarted",e,s),r.info(o,`Processing event ${e} for session ${s}`),this.spawnAgyPrint(e,s,i,n)}spawnAgyPrint(t,e,s,i){const n=this.buildPrintArgs(s,i),c={...process.env,...this.config.env??{}};r.info(o,`Spawning: agy ${n.map(a=>a.includes(" ")?`"${a}"`:a).join(" ")}`);const u=this.getLatestLogFilePath(),l=m(this.config.command,n,{cwd:i.cwd,env:c,stdio:["pipe","pipe","pipe"]});this.activeProcess=l;let d="",f="";l.stdout?.on("data",a=>{d+=a.toString("utf-8")}),l.stderr?.on("data",a=>{f+=a.toString("utf-8")}),l.on("error",a=>{r.error(o,`Process spawn error for event ${t}: ${a.message}`),this.handleProcessResult(t,e,"",`spawn error: ${a.message}`,i)}),l.on("close",a=>{if(r.info(o,`Process exited for event ${t} with code ${a}`),this.activeEventId===t)if(a!==0){const v=f.trim()||`process exited with code ${a}`;this.handleProcessResult(t,e,d,v,i)}else d.trim()?this.handleProcessResult(t,e,d,"",i):setTimeout(()=>{const v=this.extractLogError(u);this.handleProcessResult(t,e,d,v,i)},300)}),l.stdin?.end()}buildPrintArgs(t,e){const s=["-p"];e.modelId&&s.push("--model",e.modelId),e.cwd&&s.push("--add-dir",e.cwd),s.push("--dangerously-skip-permissions");const i=e.cwd?this.sessionConversationMap.get(e.cwd):void 0;return i&&s.push("--conversation",i),s.push(t),s}getLatestLogFilePath(){try{if(!y(h))return null;const t=I(h).filter(e=>e.startsWith("cli-")&&e.endsWith(".log")).map(e=>({name:e,path:p(h,e),mtime:P(p(h,e)).mtimeMs})).sort((e,s)=>s.mtime-e.mtime);return t.length>0?t[0].path:null}catch{return null}}extractLogError(t){try{const e=this.getLatestLogFilePath();if(!e)return r.info(o,"extractLogError: no log files found"),"agy completed without output";const s=S(e,"utf-8");if(!s)return r.info(o,`extractLogError: log file is empty: ${e}`),"agy completed without output";const i=s.split(`
2
- `).filter(n=>n.startsWith("E")).map(n=>{const c=n.indexOf("] ");return c>=0?n.slice(c+2):n}).filter(n=>n.length>0);if(i.length>0){const n=i[i.length-1];return r.info(o,`Extracted error from agy log: ${n.slice(0,120)}`),n.length>500?n.slice(0,500)+"...":n}return"agy completed without output (possible auth or quota issue)"}catch(e){return r.info(o,`extractLogError: ${e}`),"agy completed without output"}}handleProcessResult(t,e,s,i,n){if(this.activeEventId===t){if(i&&!s.trim()){r.error(o,`Event ${t} failed: ${i}`);const u=i.includes("RESOURCE_EXHAUSTED")||i.includes("quota")?"agy API \u914D\u989D\u5DF2\u7528\u5C3D\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5":`agy \u6267\u884C\u5931\u8D25: ${i}`;this.callbacks.sendStreamChunk(t,e,u,1,!0),this.callbacks.sendEventResult(t,"failed",i)}else{const c=s.trim();c&&this.callbacks.sendStreamChunk(t,e,c,1,!0),this.callbacks.sendEventResult(t,"responded")}this.clearActiveEvent(t),this.drainQueue()}}clearActiveEvent(t){const e=t??this.activeEventId;if(e&&(this.completedEventIds.add(e),this.completedEventIds.size>1e3)){const s=Array.from(this.completedEventIds);this.completedEventIds.clear();for(let i=500;i<s.length;i++)this.completedEventIds.add(s[i])}this.activeEventId=null,this.activeEventSessionId=null,this.activeProcess=null,e&&this.emit("eventDone",e)}drainQueue(){if(this.stopped||this.activeEventId)return;const t=this.eventQueue.shift();t&&(r.info(o,`Draining queued event ${t.event_id}`),this.processEvent(t))}}export{G as AgyAdapter};
1
+ import{spawn as b}from"node:child_process";import{randomUUID as w}from"node:crypto";import{EventEmitter as m}from"node:events";import{existsSync as E,readFileSync as y,readdirSync as A,statSync as k}from"node:fs";import{homedir as S}from"node:os";import{join as u}from"node:path";import{log as r}from"../../core/log/index.js";import{buildSimpleProbeReport as $}from"../shared/probe-util.js";const o="agy-adapter",v=u(S(),".gemini","antigravity-cli","log"),I=u(S(),".gemini","antigravity-cli","cache","last_conversations.json");class O extends m{type="agy";config;callbacks;runtimeResolver;alive=!1;stopped=!1;activeEventId=null;activeEventSessionId=null;activeProcess=null;completedEventIds=new Set;sessionConversationMap=new Map;conversationOutputCache=new Map;eventQueue=[];constructor(t,e,s){super(),this.config=t,this.callbacks=e,this.runtimeResolver=s}async start(){this.alive||(this.alive=!0,this.stopped=!1,r.info(o,"AgyAdapter started"))}async stop(){if(!this.stopped){if(this.stopped=!0,this.alive=!1,this.activeProcess){try{this.activeProcess.kill("SIGKILL")}catch{}this.activeProcess=null}if(this.activeEventId&&this.activeEventSessionId)try{this.callbacks.sendEventResult(this.activeEventId,"canceled","adapter stopped")}catch{}this.activeEventId=null,this.activeEventSessionId=null,this.emit("exit",0),r.info(o,"AgyAdapter stopped")}}isAlive(){return this.alive}async createSession(t){return t.cwd??w()}async resumeSession(t,e){}async destroySession(t){}sendPrompt(t){const e=new m;return e.adapterSessionId=t.adapterSessionId,e.cancel=async()=>{e.emit("done",{status:"canceled"})},e}async cancel(t){}cancelCurrentRun(){if(!this.activeEventId||!this.activeProcess)return;const t=this.activeEventId;r.info(o,`cancelCurrentRun: killing process for event ${t}`);try{this.activeProcess.kill("SIGKILL")}catch{}this.activeProcess=null,this.callbacks.sendEventResult(t,"canceled","restarted by user"),this.clearActiveEvent(t),this.drainQueue()}deliverInboundEvent(t){if(!this.stopped){if(this.completedEventIds.has(t.event_id)){r.debug(o,`Skipping duplicate event: ${t.event_id}`);return}if(this.callbacks.sendEventAck(t.event_id,t.session_id),this.activeEventId){r.info(o,`Queuing event ${t.event_id} (active: ${this.activeEventId})`),this.eventQueue.push(t);return}this.processEvent(t)}}deliverStopEvent(t,e){if(this.activeEventId===t&&this.activeProcess){r.info(o,`Stopping event ${t}`);try{this.activeProcess.kill("SIGKILL")}catch{}this.activeProcess=null,this.callbacks.sendEventResult(t,"canceled","stopped by user"),this.clearActiveEvent(t),this.drainQueue()}}setPermissionHandler(){}async ping(t){return this.alive}getStatus(){return{alive:this.alive,busy:this.activeEventId!==null,sessions:this.sessionConversationMap.size}}async probe(t){const e=this.getStatus();return $(this.config.command||"agy",{alive:e.alive,busy:e.busy,started:e.alive},t)}getActiveEventIds(){return this.activeEventId?[this.activeEventId]:[]}clearActiveEventForShutdown(){this.activeEventId=null,this.activeEventSessionId=null,this.activeProcess=null}getMcpConfig(){return null}processEvent(t){const{event_id:e,session_id:s,content:i}=t,n=this.runtimeResolver(s);if(!n.cwd){r.warn(o,`No working directory for session ${s}, event should have been intercepted by bridge`),this.callbacks.forceCompleteInternalEvent(e,s);return}this.activeEventId=e,this.activeEventSessionId=s,this.emit("eventStarted",e,s),r.info(o,`Processing event ${e} for session ${s}`),this.spawnAgyPrint(e,s,i,n)}spawnAgyPrint(t,e,s,i){const n=this.buildPrintArgs(s,i),c={...process.env,...this.config.env??{}};r.info(o,`Spawning: agy ${n.map(a=>a.includes(" ")?`"${a}"`:a).join(" ")}`);const d=this.getLatestLogFilePath(),l=b(this.config.command,n,{cwd:i.cwd,env:c,stdio:["pipe","pipe","pipe"]});this.activeProcess=l;const f=[],g=[];l.stdout?.on("data",a=>{f.push(a)}),l.stderr?.on("data",a=>{g.push(a)}),l.on("error",a=>{r.error(o,`Process spawn error for event ${t}: ${a.message}`),this.handleProcessResult(t,e,"",`spawn error: ${a.message}`,i)}),l.on("close",a=>{if(r.info(o,`Process exited for event ${t} with code ${a}`),this.activeEventId!==t)return;const h=Buffer.concat(f).toString("utf-8"),P=Buffer.concat(g).toString("utf-8");if(a!==0){const p=P.trim()||`process exited with code ${a}`;this.handleProcessResult(t,e,h,p,i)}else h.trim()?this.handleProcessResult(t,e,h,"",i):setTimeout(()=>{const p=this.extractLogError(d);this.handleProcessResult(t,e,h,p,i)},300)}),l.stdin?.end()}buildPrintArgs(t,e){const s=[];e.modelId&&s.push("--model",e.modelId),e.cwd&&s.push("--add-dir",e.cwd),s.push("--dangerously-skip-permissions");const i=e.cwd?this.resolveConversationId(e.cwd):void 0;return i&&s.push("--conversation",i),s.push("-p",t),s}resolveConversationId(t){try{if(E(I)){const s=JSON.parse(y(I,"utf-8"))[t];if(s)return this.sessionConversationMap.set(t,s),s}}catch(e){r.debug(o,`resolveConversationId: ${e}`)}return this.sessionConversationMap.get(t)}getLatestLogFilePath(){try{if(!E(v))return null;const t=A(v).filter(e=>e.startsWith("cli-")&&e.endsWith(".log")).map(e=>({name:e,path:u(v,e),mtime:k(u(v,e)).mtimeMs})).sort((e,s)=>s.mtime-e.mtime);return t.length>0?t[0].path:null}catch{return null}}extractLogError(t){try{const e=this.getLatestLogFilePath();if(!e)return r.info(o,"extractLogError: no log files found"),"agy completed without output";const s=y(e,"utf-8");if(!s)return r.info(o,`extractLogError: log file is empty: ${e}`),"agy completed without output";const i=s.split(`
2
+ `).filter(n=>n.startsWith("E")).map(n=>{const c=n.indexOf("] ");return c>=0?n.slice(c+2):n}).filter(n=>n.length>0);if(i.length>0){const n=i[i.length-1];return r.info(o,`Extracted error from agy log: ${n.slice(0,120)}`),n.length>500?n.slice(0,500)+"...":n}return"agy completed without output (possible auth or quota issue)"}catch(e){return r.info(o,`extractLogError: ${e}`),"agy completed without output"}}handleProcessResult(t,e,s,i,n){if(this.activeEventId===t){if(i&&!s.trim()){r.error(o,`Event ${t} failed: ${i}`);const d=i.includes("RESOURCE_EXHAUSTED")||i.includes("quota")?"agy API \u914D\u989D\u5DF2\u7528\u5C3D\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5":`agy \u6267\u884C\u5931\u8D25: ${i}`;this.callbacks.sendStreamChunk(t,e,d,1,!0),this.callbacks.sendEventResult(t,"failed",i)}else{const c=s.trim();if(c){const d=n.cwd,l=d?this.extractDelta(c,d):c;d&&this.conversationOutputCache.set(d,c),l&&this.callbacks.sendStreamChunk(t,e,l,1,!0)}this.callbacks.sendEventResult(t,"responded")}this.clearActiveEvent(t),this.drainQueue()}}extractDelta(t,e){const s=this.conversationOutputCache.get(e);return s&&t.startsWith(s)?t.slice(s.length).trim():t}clearActiveEvent(t){const e=t??this.activeEventId;if(e&&(this.completedEventIds.add(e),this.completedEventIds.size>1e3)){const s=Array.from(this.completedEventIds);this.completedEventIds.clear();for(let i=500;i<s.length;i++)this.completedEventIds.add(s[i])}this.activeEventId=null,this.activeEventSessionId=null,this.activeProcess=null,e&&this.emit("eventDone",e)}drainQueue(){if(this.stopped||this.activeEventId)return;const t=this.eventQueue.shift();t&&(r.info(o,`Draining queued event ${t.event_id}`),this.processEvent(t))}}export{O as AgyAdapter};
@@ -1,23 +1,23 @@
1
- import{spawn as Q,spawnSync as ge,execSync as j,execFile as _e}from"node:child_process";import{promisify as Ee}from"node:util";import{randomUUID as B}from"node:crypto";import{killProcessGroup as D}from"../../core/runtime/spawn.js";import{readdirSync as ye,readFileSync as J,rmSync as we,statSync as M,existsSync as L,openSync as Se,writeSync as Ce,closeSync as Pe,constants as ee,watch as ke}from"node:fs";import{mkdir as R,readFile as C,rm as te,stat as ie,writeFile as I}from"node:fs/promises";import{join as h,resolve as Ie}from"node:path";import{homedir as T,tmpdir as Ae}from"node:os";import G from"node:net";import{EventEmitter as se}from"node:events";import{fileURLToPath as be}from"node:url";let F=null;if(process.platform==="win32")try{F=await import("node-pty")}catch{F=null}import{resolveRuntimePaths as S}from"../../core/config/index.js";import{SESSION_MODE_IDS as N}from"./protocol-contract.js";import{log as a}from"../../core/log/index.js";import{ActivityStatusManager as $e}from"./activity-status-manager.js";import{HookSignalStore as Te}from"../../core/hooks/hook-signal-store.js";import{QuestionStore as ne}from"../../core/persistence/question-store.js";import{PermissionStore as O}from"../../core/persistence/permission-store.js";import{InternalApiServer as Re}from"../../core/mcp/internal-api-server.js";import{executeEventTool as xe,isEventTool as De}from"../../core/mcp/event-tool-executor.js";import{validateToolArgs as Me}from"../../core/mcp/tool-schemas.js";import{ACCESS_CONTROL_ACTION_MAP as Le,isGrixInternalToolName as Ne,normalizeEventToolArgs as je}from"../../core/mcp/tools.js";import{scanSkills as Fe}from"./skill-scanner.js";import{extractLastAssistantEntry as re,extractLastAssistantText as ae,resolveSessionJsonlPath as oe}from"./usage-parser.js";import{readSettingsEnv as Oe}from"./model-list.js";import{resolveCliPath as qe,getCliVersion as Ue}from"../../core/util/cli-probe.js";const x="grix";function He(d){if(!Array.isArray(d))return;const e=d.map(t=>{if(typeof t=="string")return t.trim();if(!t||typeof t!="object")return"";const i=t,s=String(i.label??"").trim();if(s)return s;const n=String(i.value??"").trim();if(n)return n;const r=String(i.text??"").trim();return r||""}).filter(t=>t.length>0);return e.length>0?e:void 0}function ce(d){let e;try{e=JSON.parse(d)}catch{return null}const t=e.questions;if(!Array.isArray(t)||t.length===0)return null;const i=[];for(let s=0;s<t.length;s++){const n=t[s];if(!n||typeof n!="object")continue;const r=n,o=`Question ${s+1}`,c=String(r.header??r.question??o).trim()||o,u=String(r.prompt??r.question??"").trim();if(!u)continue;const l=He(r.options),p=r.multiSelect===!0||r.multi_select===!0;i.push({header:c,prompt:u,...l?{options:l}:{},...p?{multi_select:!0}:{}})}return i.length>0?i:null}function Qe(d){if(!d||typeof d!="string")return null;try{const e=JSON.parse(d);return(typeof e.plan=="string"?e.plan.trim():"")||null}catch{return null}}const Be=[/Please run \/login/,/API Error:\s*401/,/authentication_error/,/OAuth token has expired/],Je=[/You're out of extra usage/i,/Stop and wait for limit to reset/i,/Add funds to continue with extra usage/i],Ge=/API Error:\s*400.*server_tool_use\.id/,le=300*1e3,ze=1800*1e3,z=60*1e3,W=600*1e3,We=300*1e3;function Ve(d){try{return process.kill(d,0),!0}catch(e){return e.code==="EPERM"}}const Ye=12e4,Ke=3e4,Xe=3e3,q=1e3,de=45e3,Ze=18e3,V=8192,Y=50;let ue=!1;const K=new Set;async function et(d=[]){const e=new Set(d.filter(t=>Number.isInteger(t)&&t>0));for(let t=0;t<20;t++){const i=await new Promise((s,n)=>{const r=G.createServer();r.once("error",n),r.listen(0,"127.0.0.1",()=>{const o=r.address(),c=typeof o=="object"&&o?o.port:0;r.close(u=>{if(u){n(u);return}s(c)})})});if(i>0&&!e.has(i)&&!K.has(i))return K.add(i),i}throw new Error("\u65E0\u6CD5\u5206\u914D MCP \u901A\u77E5\u7AEF\u53E3")}function tt(d){d>0&&K.delete(d)}const he=["You are connected to a chat via the grix-claude MCP server.",'Messages arrive as <channel source="grix-claude" chat_id="..." event_id="..." message_id="..." user_id="...">text</channel>.',"IMPORTANT: You MUST use the reply tool to send any response to the user.","Your plain text output is NOT delivered to the chat \u2014 only the reply tool delivers messages.","Always call the reply tool with chat_id, event_id, and your response text.","If you intentionally do not want to send a visible reply, you must call the complete tool with event_id and a final status."].join(" ");function pe(d){return String(d??"").trim().toLowerCase()===N.approval?N.approval:N.fullAuto}class me extends se{type="claude";config;bridgeCallbacks;mcpServerProcess=null;internalApi=null;claudeProcess=null;claudePty=null;spawnPromise=null;lifecycleVersion=0;sessionId="";alive=!1;stopped=!1;activeEvent=null;activeEventIdleTimer=null;activeEventHardTimer=null;activeEventPostReplyTimer=null;activeEventPostReplyWatcher=null;queuedEvents=[];queuedEventIds=new Set;stopHookBarrierSessionId=null;stopHookBarrierTimer=null;compacting=!1;compactingTimer=null;compactionDoneResolver=null;mcpServerReady=!1;mcpStartupFailureHandled=!1;mcpChannelBroken=!1;channelGateClosed=!1;startupChannelListening=!1;startupChannelListeningAt=0;pendingMcpFailureTimer=null;sessionIdConflictDetected=!1;sessionIdConflictRetriedEventIds=new Set;cachedCliVersion=null;runtimeResolver=null;activityManager=null;lastPreToolInput="";deferredModelId=null;claudeChildPid=0;claudeCliSessionId="";claudeSessionCwd="";claudeMcpConfigPath="";expectRunDir="";sessionState=null;authFailureUntil=0;usageLimitUntil=0;completedEventIds=new Map;pendingQuestion=new Map;pendingPermissions=new Map;lastClearedEvent=null;constructor(e,t){super(),this.config=e,this.bridgeCallbacks=t;const s=(e.options??{}).sessionRuntimeResolver;typeof s=="function"&&(this.runtimeResolver=s)}async start(){this.lifecycleVersion+=1,this.stopped=!1,this.alive=!0,this.internalApi=new Re,this.internalApi.setInvokeHandler(async(e,t,i)=>this.handleInternalInvoke(e,t,i)),this.internalApi.setStatusLineHandler(async e=>{this.handleStatusLineUpdate(e)}),await this.internalApi.start(0),this.internalApiPort=parseInt(new URL(this.internalApi.baseUrl).port,10),this.notifyPort=0,a.info("claude-adapter",`Adapter started (stdio MCP mode, internal API at ${this.internalApi.baseUrl})`)}async stop(){if(a.info("claude-adapter",`Stopping adapter (sessionId=${this.sessionId}, alive=${this.alive}, activeEvent=${this.activeEvent?.eventId??"none"})`),this.lifecycleVersion+=1,this.stopped=!0,this.alive=!1,this.activeEvent&&(this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,"canceled","adapter stopped"),this.clearActiveEvent()),this.stopComposing(),this.rejectQueuedEvents("adapter stopped","canceled"),this.clearStopHookBarrier(),this.compacting=!1,this.compactingTimer&&(clearTimeout(this.compactingTimer),this.compactingTimer=null),this.compactionDoneResolver&&(this.compactionDoneResolver("stopped"),this.compactionDoneResolver=null),this.mcpChannelBroken=!1,this.channelGateClosed=!1,this.startupChannelListening=!1,this.startupChannelListeningAt=0,this.deferredModelId=null,this.clearPendingMcpFailureTimer(),this.ptyAutoConfirmTimer&&(clearInterval(this.ptyAutoConfirmTimer),this.ptyAutoConfirmTimer=null),this.pendingPermissions.size>0){const s=S(),n=new O(s.permissionRequestsDir);for(const[r]of this.pendingPermissions)n.resolveRequest(r,"deny").catch(()=>{});this.pendingPermissions.clear()}if(this.activityManager&&(this.activityManager.stop(),this.activityManager=null),this.sessionId){const s=this.resolveHookSignalsPath();te(s).catch(()=>{})}this.stopMcpServer();const e=this.claudeChildPid;this.claudeChildPid=0;const t=this.claudeProcess,i=this.claudePty;if(this.claudeProcess=null,this.claudePty=null,this.spawnPromise=null,i)try{i.kill()}catch{}if(e>0)try{process.kill(e,"SIGTERM")}catch{}if(t?.pid&&(D(t,"SIGTERM"),!await Promise.race([new Promise(n=>{t.once("exit",()=>n(!0))}),new Promise(n=>{setTimeout(()=>n(!1),5e3)})]))){if(e>0)try{process.kill(e,"SIGKILL")}catch{}D(t,"SIGKILL")}if(e>0){const s=Date.now();for(;Date.now()-s<5e3;)try{process.kill(e,0),await new Promise(n=>setTimeout(n,100))}catch{break}try{process.kill(e,"SIGKILL")}catch{}}this.releaseNotifyPortReservation(),this.claudeCliSessionId&&H(this.claudeCliSessionId),this.internalApi&&(await this.internalApi.stop(),this.internalApi=null)}isAlive(){return this.alive}async createSession(e){return`claude-session-${Date.now()}`}async resumeSession(e,t){}async destroySession(e){}sendPrompt(e){const t=new it(e.adapterSessionId),i={event_id:e.adapterSessionId,session_id:e.adapterSessionId,content:e.text,context_messages_json:e.contextMessages?JSON.stringify(e.contextMessages):void 0};return this.deliverInboundEvent(i),this.once(`reply:${e.adapterSessionId}`,s=>{t.emitDone(s.status==="completed"?{status:"completed"}:{status:"failed",error:"failed"})}),t}async cancel(e){}setPermissionHandler(e){}async ping(e){if(this.stopped)return!1;if(!this.claudeProcess&&!this.claudePty)return this.alive&&this.internalApi!==null;if(!(this.alive&&this.mcpServerReady&&(this.startupChannelListening||process.platform==="win32")))return!1;const t=this.claudeProcess?.pid??this.claudePty?.pid;if(t&&!Ve(t))return!1;const i=this.claudeProcess;if(i){const s=!!(i.stdin&&!i.stdin.destroyed),n=!!(i.stdout&&!i.stdout.destroyed);if(!s||!n)return!1}return!0}getStatus(){return{alive:this.alive,busy:this.activeEvent!==null||this.compacting,sessions:0,details:this.sessionState?{sessionState:this.sessionState}:void 0}}getActiveEventIds(){return this.activeEvent?[this.activeEvent.eventId]:[]}clearActiveEventForShutdown(){this.activeEvent&&this.clearActiveEvent()}getSessionState(){return this.sessionState}getMcpConfig(){return null}async probe(e){const t=this.config.command||"claude",i=await qe(t),s=i!==null;let n=null,r;if(s){const p=await Ue(t);n=p.version,p.error&&(r=p.error)}else r={code:"cli_not_found",message:`command not found: ${t}`};const c=(Oe().ANTHROPIC_BASE_URL??process.env.ANTHROPIC_BASE_URL??"").trim()||null,u=(this.sessionState?.model?.id??(process.env.ANTHROPIC_MODEL??"").trim())||null;let l={attempted:!1,ok:!1,latency_ms:null};if(e?.conversation&&s){const p=Date.now();try{await Ee(_e)(t,["-p","ping","--output-format","json","--max-turns","1"],{timeout:e.timeoutMs??8e3,encoding:"utf-8"}),l={attempted:!0,ok:!0,latency_ms:Date.now()-p}}catch(f){const v=f,P=v.killed?"conversation_timeout":"conversation_failed";l={attempted:!0,ok:!1,latency_ms:Date.now()-p,error:{code:P,message:v.message??String(f)}}}}return{cli:{command:t,installed:s,path:i,version:n,...r?{error:r}:{}},conversation:l,config:{model:u,base_url:c,source:{model:this.sessionState?.model?.id?"runtime":process.env.ANTHROPIC_MODEL?"env":"unknown",base_url:c?"env":"unknown"}},process:{started:!!(this.claudeProcess||this.claudePty),alive:this.alive,busy:this.activeEvent!==null},...this.probeSessionRecord()}}probeSessionRecord(){const e=this.claudeCliSessionId;if(!e||!this.claudeSessionCwd)return{session:{recordPath:null,lastActivityMs:null,freshMs:null}};const t=oe(e,this.claudeSessionCwd);try{const i=M(t);return{session:{recordPath:t,lastActivityMs:i.mtimeMs,freshMs:Date.now()-i.mtimeMs}}}catch{return{session:{recordPath:t,lastActivityMs:null,freshMs:null}}}}getSupportedCommands(){return[{name:"compact",description:"\u538B\u7F29\u4E0A\u4E0B\u6587",args:"[instructions]"},{name:"clear",description:"\u6E05\u9664\u5BF9\u8BDD"},{name:"model",description:"\u5207\u6362\u6A21\u578B",args:"<model-id>"},{name:"cost",description:"\u663E\u793A\u8D39\u7528"},{name:"rewind",description:"\u56DE\u9000\u5BF9\u8BDD"},{name:"memory",description:"\u8BB0\u5FC6\u7BA1\u7406"},{name:"doctor",description:"\u8BCA\u65AD"},{name:"status",description:"\u72B6\u6001\u663E\u793A"},{name:"skills",description:"\u83B7\u53D6 skills \u6E05\u5355"}]}async execCommand(e,t,i){if(e==="skills")try{const n=Fe({mode:"claude",projectDir:this.claudeSessionCwd}),r=n.map(o=>{const c=o.trigger?` (${o.trigger})`:"",u=o.pluginName?` [plugin:${o.pluginName}]`:` [${o.source}]`;return`- ${o.name}${c}${u}: ${o.description}`});return{status:"ok",message:r.length>0?r.join(`
2
- `):"No skills found",data:n}}catch(n){return{status:"failed",message:`Failed to scan skills: ${n instanceof Error?n.message:n}`}}if(!this.claudeProcess&&!this.claudePty)if(e==="compact"){try{await this.ensureClaudeProcessReady()}catch(n){return{status:"failed",message:`Failed to start Claude for compact: ${n instanceof Error?n.message:n}`}}if(await new Promise(n=>setTimeout(n,2e3)),!this.claudeProcess&&!this.claudePty)return{status:"failed",message:"Claude process exited during startup"}}else return{status:"failed",message:"Claude process is not running"};if(this.getStatus().busy)return{status:"failed",message:"Claude is busy, try again later"};const s=t?`/${e} ${t}`:`/${e}`;try{if(this.claudePty)this.claudePty.write(`${s}\r`);else if(this.expectRunDir){const n=h(this.expectRunDir,"cmd.fifo");let r;try{r=Se(n,ee.O_WRONLY|ee.O_NONBLOCK)}catch(o){const c=o.code;throw new Error(c==="ENXIO"?"expect process is not running \u2014 cannot inject command":`FIFO open failed: ${o instanceof Error?o.message:o}`)}try{Ce(r,`${s}
3
- `)}finally{Pe(r)}}else this.claudeProcess?.stdin?.write(`${s}
4
- `);if(e==="compact"){const n=new Promise(o=>{this.compactionDoneResolver=o});this.beginCompaction();const r=await n;return r==="fallback-timeout"?{status:"failed",message:"Compact timed out (60s) \u2014 context was not compressed"}:r==="process-exit"?{status:"failed",message:"Claude exited during compaction"}:r==="stopped"?{status:"failed",message:"Adapter stopped during compaction"}:{status:"ok",message:"Compacted"}}return{status:"ok",message:`Sent: ${s}`}}catch(n){return{status:"failed",message:`Failed to send: ${n instanceof Error?n.message:n}`}}}handleStatusLineUpdate(e){try{const t=e.context_window,i=e.cost,s=e.model,n=e.rate_limits??e.rateLimits,r=t?.current_usage;if(this.sessionState={contextWindow:{usedPercentage:t?.used_percentage!=null?_(t.used_percentage):null,remainingPercentage:t?.remaining_percentage!=null?_(t.remaining_percentage):null,totalInputTokens:_(t?.total_input_tokens),totalOutputTokens:_(t?.total_output_tokens),contextWindowSize:_(t?.context_window_size)||2e5,currentUsage:r?{inputTokens:_(r.input_tokens),outputTokens:_(r.output_tokens),cacheCreationInputTokens:_(r.cache_creation_input_tokens),cacheReadInputTokens:_(r.cache_read_input_tokens)}:null},cost:{totalCostUsd:_(i?.total_cost_usd),totalDurationMs:_(i?.total_duration_ms),totalApiDurationMs:_(i?.total_api_duration_ms),totalLinesAdded:_(i?.total_lines_added),totalLinesRemoved:_(i?.total_lines_removed)},rateLimits:this.parseStatusRateLimits(n),model:{id:String(s?.id??""),displayName:String(s?.display_name??"")},fastMode:e.fast_mode===!0,effort:e.effort!=null?String(e.effort):void 0,thinkingEnabled:e.thinking_enabled===!0,exceeds200kTokens:e.exceeds_200k_tokens===!0,version:String(e.version??""),sampledAt:Date.now()},n){const o=this.sessionState.rateLimits?.fiveHour,c=this.sessionState.rateLimits?.sevenDay;a.info("claude-adapter",`[rate-limits] statusLine parsed: fiveHour=${o?`${o.usedPercentage}% resetsAt=${o.resetsAt}`:"n/a"} sevenDay=${c?`${c.usedPercentage}% resetsAt=${c.resetsAt}`:"n/a"}`)}else a.debug("claude-adapter","[rate-limits] statusLine: no rate_limits in payload");try{this.bridgeCallbacks.onStatusLineUpdated?.(this.sessionState)}catch(o){a.warn("claude-adapter",`onStatusLineUpdated callback error: ${o instanceof Error?o.message:o}`)}}catch(t){a.warn("claude-adapter",`Failed to parse statusLine payload: ${t instanceof Error?t.message:t}`)}}parseStatusRateLimits(e){if(!e)return;const t=n=>{if(!n||typeof n!="object")return;const r=n,o=_(r.used_percentage??r.usedPercent),c=_(r.resets_at??r.resetsAt);if(!(o<=0&&c<=0))return{usedPercentage:o,resetsAt:c}},i=t(e.five_hour??e.fiveHour),s=t(e.seven_day??e.sevenDay);if(!(!i&&!s))return{...i?{fiveHour:i}:{},...s?{sevenDay:s}:{}}}deliverInboundEvent(e){if(this.sessionId||(this.sessionId=e.session_id),this.pruneCompletedEvents(),this.completedEventIds.has(e.event_id)){a.info("claude-adapter",`Event ${e.event_id} rejected: duplicate`),this.bridgeCallbacks.sendEventResult(e.event_id,"responded","duplicate event");return}if(Date.now()<this.authFailureUntil){a.info("claude-adapter",`Event ${e.event_id} rejected: auth cooldown`),this.bridgeCallbacks.sendEventResult(e.event_id,"failed","Claude authentication failed \u2014 please re-login");return}if(Date.now()<this.usageLimitUntil){a.info("claude-adapter",`Event ${e.event_id} rejected: usage limit cooldown`),this.bridgeCallbacks.sendEventResult(e.event_id,"failed","Claude usage limit reached \u2014 waiting for reset");return}if(this.compacting){if(this.queuedEventIds.has(e.event_id))return;if(this.queuedEvents.length>=Y){a.warn("claude-adapter",`Event ${e.event_id} rejected: queue full during compaction (size=${this.queuedEvents.length})`),this.bridgeCallbacks.sendEventResult(e.event_id,"failed","agent queue full");return}this.queuedEvents.push(e),this.queuedEventIds.add(e.event_id),a.info("claude-adapter",`Queue event ${e.event_id} (compacting, size=${this.queuedEvents.length})`);return}if(this.activeEvent){if(this.activeEvent.eventId===e.event_id){a.info("claude-adapter",`Event ${e.event_id} ignored: same as active event`);return}if(this.queuedEventIds.has(e.event_id))return;if(this.queuedEvents.length>=Y){a.warn("claude-adapter",`Event ${e.event_id} rejected: queue full (size=${this.queuedEvents.length})`),this.bridgeCallbacks.sendEventResult(e.event_id,"failed","agent queue full");return}this.queuedEvents.push(e),this.queuedEventIds.add(e.event_id),a.info("claude-adapter",`Queue event ${e.event_id} (active=${this.activeEvent.eventId}, size=${this.queuedEvents.length})`);return}if(this.stopHookBarrierSessionId===e.session_id){if(this.queuedEventIds.has(e.event_id))return;if(this.queuedEvents.length>=Y){a.warn("claude-adapter",`Event ${e.event_id} rejected: queue full during barrier (size=${this.queuedEvents.length})`),this.bridgeCallbacks.sendEventResult(e.event_id,"failed","agent queue full");return}this.queuedEvents.push(e),this.queuedEventIds.add(e.event_id),a.info("claude-adapter",`Queue event ${e.event_id} (barrier session=${e.session_id})`);return}this.activeEvent={eventId:e.event_id,sessionId:e.session_id,rawEvent:e},this.lastClearedEvent=null,this.markActiveEventActivity(e.event_id,e.session_id),this.resetActiveEventHardTimer(e.event_id),this.emit("eventStarted",e.event_id,e.session_id),this.ensureClaudeAndPushEvent(e)}deliverStopEvent(e,t){if(this.activeEvent?.eventId!==e)return;const i=this.activeEvent.sessionId;a.info("claude-adapter",`Stop requested for active event=${e} \u2014 killing Claude process`),this.mcpServerReady&&this.pushNotification("notifications/event_stop",{event_id:e,session_id:i,stop_id:B()}),this.bridgeCallbacks.sendEventResult(e,"canceled","stopped by user"),this.clearActiveEvent(),this.killClaudeProcess("stop")}async handleLocalAction(e){if(!this.mcpServerReady)return{handled:!1,kind:""};const t=String(e.action_type??"");if(t==="claude_interaction_reply"){const i=e.params??{};if(String(i.kind??"")==="permission"){const n=i.resolution??{},r=String(n.value??""),o={...e,action_type:r==="allow"?"exec_approve":"exec_reject",params:{...i,approval_command_id:i.request_id}};return this.handlePermissionApproval(o)}return this.handleQuestionReply(e)}return t==="exec_approve"||t==="exec_reject"?this.handlePermissionApproval(e):(this.pushNotification("notifications/local_action",e),{handled:!0,kind:""})}async handleQuestionReply(e){const t=e.params??{},i=String(t.request_id??"");if(!i)return a.warn("claude-adapter","Question reply missing request_id"),this.bridgeCallbacks.sendLocalActionResult(e.action_id,"failed",void 0,"request_id_required","request_id is required"),{handled:!0,kind:"question_reply_no_id"};const s=S(),n=new ne(s.questionRequestsDir),r=t.resolution??{},o=String(r.type??""),c=r;if(o==="action"&&String(r.value??"")==="cancel"){a.info("claude-adapter",`Question cancelled by user: request_id=${i}`),await n.resolveRequest(i,"cancel",void 0,c);const f=this.pendingQuestion.get(i);return f&&this.activeEvent?.eventId===f.eventId&&(this.activeEvent.awaitingUserQuestion=!1),this.pendingQuestion.delete(i),this.bridgeCallbacks.sendLocalActionResult(e.action_id,"ok",{request_id:i,resolution:"cancel"}),{handled:!0,kind:"question_reply_cancel"}}let u="";if(o==="text"?u=String(r.value??""):o==="map"&&(u=(Array.isArray(r.entries)?r.entries:[]).map(f=>f.value).join(", ")),!u){a.warn("claude-adapter",`Empty answer for question reply request_id=${i}`),await n.resolveRequest(i,"cancel",void 0,c);const p=this.pendingQuestion.get(i);return p&&this.activeEvent?.eventId===p.eventId&&(this.activeEvent.awaitingUserQuestion=!1),this.pendingQuestion.delete(i),this.bridgeCallbacks.sendLocalActionResult(e.action_id,"ok",{request_id:i,resolution:"cancel"}),{handled:!0,kind:"question_reply_empty"}}a.info("claude-adapter",`Resolving question in store: request_id=${i} answer=${u.slice(0,80)}`),await n.resolveRequest(i,"answer",u,c);const l=this.pendingQuestion.get(i);return l&&this.activeEvent?.eventId===l.eventId&&(this.activeEvent.awaitingUserQuestion=!1),this.pendingQuestion.delete(i),this.bridgeCallbacks.sendLocalActionResult(e.action_id,"ok",{request_id:i,resolution:"answer"}),{handled:!0,kind:"question_reply"}}async handlePermissionApproval(e){const t=e.params??{},i=String(t.approval_id??t.approval_command_id??t.tool_call_id??""),s=String(e.action_type??"")==="exec_approve";if(!i)return this.bridgeCallbacks.sendLocalActionResult(e.action_id,"failed",void 0,"approval_id_required","approval_id is required"),{handled:!0,kind:"permission_approval"};if(!this.pendingPermissions.get(i))return this.bridgeCallbacks.sendLocalActionResult(e.action_id,"failed",void 0,"approval_not_found",`no pending permission for approval_id: ${i}`),{handled:!0,kind:"permission_approval"};const r=S(),o=new O(r.permissionRequestsDir),c=s?"allow":"deny";return await o.resolveRequest(i,c),this.pendingPermissions.delete(i),this.bridgeCallbacks.sendLocalActionResult(e.action_id,"ok"),a.info("claude-adapter",`Permission ${c}: approvalId=${i}`),{handled:!0,kind:"permission_approval"}}async ensureClaudeAndPushEvent(e,t=0){t===0&&!this.claudeProcess&&this.startComposing(e.session_id,"preparing");try{if(this.mcpChannelBroken){await this.runSingleTurnFallback(e,"mcp channel unavailable");return}if(await this.isChannelGateClosed()){await this.runSingleTurnFallback(e,"channel gate closed: tengu_harbor not enabled in cachedGrowthBookFeatures");return}if(await this.ensureClaudeProcessReady(),!this.mcpServerReady)throw new Error("MCP stdio server not available");if(await this.waitForNotifyPort(3e4),process.platform==="win32"?await this.waitForWindowsChannelReady(de):await this.waitForChannelListening(de),this.activeEvent?.eventId!==e.event_id||this.mcpChannelBroken)return;const s={},n=[["chat_id",e.session_id],["event_id",e.event_id],["event_type","user_chat"],["message_id",e.msg_id],["sender_id",e.sender_id],["user",e.sender_id],["user_id",e.sender_id],["msg_id",e.msg_id],["session_type",e.session_type!=null?String(e.session_type):void 0],["msg_type",e.msg_type!=null?String(e.msg_type):void 0],["quoted_message_id",e.quoted_message_id],["ts",e.created_at!=null?new Date(Number(e.created_at)).toISOString():void 0],["owner_id",e.owner_id],["agent_id",e.agent_id],["attachments_json",e.attachments_json],["context_messages_json",e.context_messages_json]];for(const[r,o]of n)o!=null&&o!==""&&(s[r]=o);this.pushNotification("notifications/claude/channel",{content:e.content??"",meta:s}),a.info("claude-adapter",`Event ${e.event_id} pushed to MCP stdio server`)}catch(s){if(this.stopped){this.activeEvent?.eventId===e.event_id&&(this.bridgeCallbacks.sendEventResult(e.event_id,"canceled","adapter stopped"),this.clearActiveEvent());return}if(s instanceof Error&&(s.message.includes("exited before")||s.message.includes("timeout")||s.message.includes("spawn failed"))&&t<3)return a.warn("claude-adapter",`Spawn failed (attempt ${t+1}/4), retrying: ${s}`),this.claudeProcess=null,this.claudeChildPid=0,this.alive=!1,this.releaseNotifyPortReservation(),this.stopMcpServer(),H(this.claudeCliSessionId),X(this.claudeCliSessionId,[]),await new Promise(r=>setTimeout(r,(t+1)*2e3)),this.ensureClaudeAndPushEvent(e,t+1);if(a.error("claude-adapter",`Failed to deliver event ${e.event_id}: ${s}`),this.shouldFallbackToSingleTurn(s)){a.warn("claude-adapter",`Channel path failed, switching to single-turn fallback: ${s instanceof Error?s.message:String(s)}`);try{await this.runSingleTurnFallback(e,s instanceof Error?s.message:String(s));return}catch(r){a.error("claude-adapter",`Single-turn fallback failed for ${e.event_id}: ${r instanceof Error?r.message:String(r)}`)}}this.activeEvent?.eventId===e.event_id&&(this.bridgeCallbacks.sendEventResult(e.event_id,"failed",s instanceof Error?s.message:String(s)),this.clearActiveEvent())}}shouldFallbackToSingleTurn(e){const t=(e instanceof Error?e.message:String(e)).toLowerCase();return t?t.includes("mcp server startup failed")||t.includes("claude channel listener not ready")||t.includes("channel ready check timed out")||t.includes("channels are not currently available")||t.includes("mcp stdio server not available")||t.includes("notify port not ready")||t.includes("file not found:"):!1}buildSingleTurnPrompt(e){const t=e.context_messages_json?String(e.context_messages_json):"",i=["Reply to the latest user message directly and concisely."];return t&&(i.push("Conversation context (JSON):"),i.push(t)),i.push("Latest user message:"),i.push(e.content??""),i.join(`
5
- `)}extractSingleTurnDelta(e){const t=e.trim();if(!t)return"";try{const i=JSON.parse(t),s=String(i.type??"");if(s==="content_block_delta"){const n=i.delta;if(n&&typeof n=="object"){const r=String(n.text??"");if(r)return r}}return s==="result"?String(i.result??""):typeof i.text=="string"&&i.text?i.text:""}catch{return t}}getCliVersion(){if(this.cachedCliVersion)return this.cachedCliVersion;const e=this.config.command||"claude";try{const i=j(`${e} --version`,{encoding:"utf-8",timeout:5e3}).trim().match(/^([\d.]+)/);this.cachedCliVersion=i?.[1]??"0.0.0"}catch{this.cachedCliVersion="0.0.0"}return this.cachedCliVersion}static versionGte(e,t){const i=e.split(".").map(Number),s=t.split(".").map(Number);for(let n=0;n<Math.max(i.length,s.length);n++){const r=i[n]??0,o=s[n]??0;if(r>o)return!0;if(r<o)return!1}return!0}buildClaudeRuntimeEnv(){const e={...process.env,...this.config.env};return delete e.CLAUDECODE,e.CLAUDE_PLUGIN_DATA=S().dataDir,e.GRIX_HOOK_SIGNALS_PATH=this.resolveHookSignalsPath(),this.internalApi&&(e.GRIX_CLAUDE_INTERNAL_API_URL=this.internalApi.baseUrl,e.GRIX_CLAUDE_INTERNAL_API_TOKEN=this.internalApi.token),e}async runSingleTurnFallback(e,t){const i=this.resolveSessionRuntime(),s=this.config.options??{},n=this.config.command||"claude",r=(this.config.args??[]).filter(P=>{const m=String(P).trim().toLowerCase();return m!=="--dangerously-load-development-channels"&&m!=="--session-id"&&m!=="--resume"}),o=String(i.cwd??"").trim(),c=String(i.modelId??"").trim(),u=pe(i.modeId);await this.ensureWorkspaceTrust(o),await this.ensureClaudeOnboardingFlags(o),await this.ensureSkipDangerousPermissionPrompt(),await this.injectStatusLineSettings(o),await this.ensureStdioMcpServer();const l=this.buildClaudeRuntimeEnv(),p=i.pluginDir??s.pluginDir??await this.ensureClaudePluginDir(),f=this.buildSingleTurnPrompt(e),v=[...r,"-p","--output-format","stream-json","--include-partial-messages","--no-session-persistence"];me.versionGte(this.getCliVersion(),"2.1.150")&&v.push("--verbose"),v.push("--tools","","--plugin-dir",p,"--strict-mcp-config","--mcp-config",this.claudeMcpConfigPath,"--permission-mode",u===N.fullAuto?"bypassPermissions":"default"),c&&v.push("--model",c),v.push(f),a.warn("claude-adapter",`Running single-turn fallback for event ${e.event_id}: ${t}`),this.bridgeCallbacks.sendEventAck(e.event_id,e.session_id),this.startComposing(e.session_id,"fallback_single_turn"),await new Promise((P,m)=>{const b=Q(n,v,{cwd:o||process.cwd(),env:l,stdio:["ignore","pipe","pipe"],detached:!0,windowsHide:!0});let k="",$=0,E=!1;const g=`fallback_${e.event_id}_${Date.now()}`;let y="";b.stdout?.setEncoding("utf8"),b.stdout?.on("data",w=>{y+=w;let A=y.indexOf(`
6
- `);for(;A>=0;){const ve=y.slice(0,A);y=y.slice(A+1);const Z=this.extractSingleTurnDelta(ve);Z&&($+=1,E=!0,this.bridgeCallbacks.sendStreamChunk(e.event_id,e.session_id,Z,$,!1,g,e.quoted_message_id)),A=y.indexOf(`
7
- `)}}),b.stderr?.setEncoding("utf8"),b.stderr?.on("data",w=>{k.length>=V||(k+=w,k.length>V&&(k=k.slice(0,V)))}),b.on("error",w=>m(w)),b.on("close",w=>{if(y.trim()){const A=this.extractSingleTurnDelta(y);A&&($+=1,E=!0,this.bridgeCallbacks.sendStreamChunk(e.event_id,e.session_id,A,$,!1,g,e.quoted_message_id))}if(w!==0&&!E){const A=k.trim();m(new Error(A||`claude single-turn exited with code ${w}`));return}$+=1,this.bridgeCallbacks.sendStreamChunk(e.event_id,e.session_id,"",$,!0,g,e.quoted_message_id),P()})}),this.activeEvent?.eventId===e.event_id&&(this.bridgeCallbacks.sendEventResult(e.event_id,"responded"),this.clearActiveEvent())}async ensureClaudeProcessReady(){this.claudeProcess||this.claudePty||(this.spawnPromise||(this.spawnPromise=this.spawnClaude().finally(()=>{this.spawnPromise=null})),await this.spawnPromise)}async spawnClaude(){if(this.claudeProcess)return;const e=this.lifecycleVersion,t=()=>{if(this.stopped||e!==this.lifecycleVersion)throw new Error("adapter stopped")},i=this.config.options??{};this.sessionIdConflictDetected=!1;const s=this.resolveSessionRuntime(),n=this.config.command||"claude",r=this.config.args??[],o=this.buildClaudeRuntimeEnv(),c=pe(s.modeId),u=String(s.modelId??"").trim(),l=String(s.cwd??"").trim();this.claudeSessionCwd=l;const p=String(s.claudeSessionId??"").trim()||B();this.claudeCliSessionId=p,!s.claudeSessionId&&s.onSessionIdAssigned&&s.onSessionIdAssigned(p);const f=dt(p,l||void 0);if(f||(X(p,[]),H(p)),this.runtimeResolver&&!l)throw new Error("Claude session binding missing cwd \u2014 run /grix open <working-directory> first");if(!l)throw new Error("Claude runtime cwd is required");await this.ensureWorkspaceTrust(l),await this.ensureClaudeOnboardingFlags(l),await this.ensureSkipDangerousPermissionPrompt(),await this.injectStatusLineSettings(l);let v=!1,P="",m=null;try{t(),this.notifyPort=await et([this.internalApiPort]),v=!0,t(),a.info("claude-adapter",`Allocated MCP notify port ${this.notifyPort} (internal API ${this.internalApiPort})`),await this.ensureStdioMcpServer(),t();const k=i.pluginDir||await this.ensureClaudePluginDir(),$=f?["--resume",p]:["--session-id",p],E=[...r,"--name",`grix-${this.sessionId}`,...$,"--plugin-dir",k,"--strict-mcp-config","--mcp-config",this.claudeMcpConfigPath];if(u&&(/^claude/i.test(u)?E.push("--model",u):(a.warn("claude-adapter",`Skipping --model ${u}: non-Anthropic models are incompatible with development channels. Using default model so channels can load. Switch models after startup via /model if needed.`),this.deferredModelId=u)),c===N.fullAuto&&E.push("--dangerously-skip-permissions"),E.push("--dangerously-load-development-channels",`server:${x}`),process.platform==="win32"){const g=he.replace(/</g,"[").replace(/>/g,"]").replace(/"/g,"'"),y=ot(this.claudeCliSessionId);await lt(y,g),E.push("--append-system-prompt-file",y)}else E.push("--append-system-prompt",he);if(process.platform==="win32")if(F){const g="cmd.exe",y=["/c",n,...E];a.info("claude-adapter",`Spawning Claude via PTY on win32: cwd=${l} mode=${c} ${f?"resume":"new"} ${n} ${E.join(" ")}`);const w=F.spawn(g,y,{name:"xterm-256color",cols:120,rows:30,cwd:l,env:o,useConpty:!0,conptyInheritCursor:!1});this.claudePty=w,this.claudeChildPid=w.pid,this.startPtyAutoConfirm(w),m=null}else a.info("claude-adapter",`Spawning Claude via shell on win32 (node-pty unavailable): cwd=${l} mode=${c} ${f?"resume":"new"} ${n} ${E.join(" ")}`),m=Q(n,E,{cwd:l,env:o,stdio:["pipe","pipe","pipe"],detached:!0,shell:!0,windowsHide:!0});else{a.info("claude-adapter",`Spawning via expect PTY: cwd=${l} mode=${c} ${f?"resume":"new"} ${n} ${E.join(" ")}`);try{M("/usr/bin/expect")}catch{throw new Error("/usr/bin/expect not found. Install it with: apt install expect / dnf install expect / brew install expect")}const g=h(Ae(),`grix-claude-${B()}`);await R(g,{recursive:!0}),this.expectRunDir=g;const{expectPath:y,pidPath:w}=await st(g,n,E);P=w,t(),m=Q("/usr/bin/expect",[y],{cwd:l,env:o,stdio:["pipe","pipe","pipe"],detached:!0})}if(this.claudeProcess=m,this.clearPendingMcpFailureTimer(),this.mcpStartupFailureHandled=!1,this.mcpChannelBroken=!1,this.channelGateClosed=!1,this.startupChannelListening=!1,this.startupChannelListeningAt=0,m&&m.on("error",g=>{a.error("claude-adapter","Claude process spawn error: "+g),this.claudeProcess=null,this.claudeChildPid=0,this.alive=!1,this.releaseNotifyPortReservation(),this.stopMcpServer(),this.clearPendingMcpFailureTimer(),this.activeEvent&&(this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,"failed","Claude process spawn failed"),this.clearActiveEvent())}),t(),!this.claudePty)if(process.platform==="win32")this.claudeChildPid=m?.pid??0;else{const g=await rt(m,P);if(t(),!Number.isFinite(g)||g<=0)throw new Error("failed to determine spawned Claude pid");this.claudeChildPid=g}if(!this.claudeChildPid||this.claudeChildPid<=0)throw new Error("failed to determine spawned Claude pid")}catch(k){if(m?.pid&&D(m,"SIGTERM"),this.claudePty){try{this.claudePty.kill()}catch{}this.claudePty=null}throw this.claudeProcess===m&&(this.claudeProcess=null),this.claudeChildPid=0,v&&this.releaseNotifyPortReservation(),this.stopMcpServer(),k}const b=this.claudeChildPid;a.info("claude-adapter","Claude child PID: "+b+(this.claudePty?" (via PTY)":"")),this.setupProcessOrPtyHandlers(),this.alive=!0}setupProcessOrPtyHandlers(){this.claudeProcess&&(this.claudeProcess.on("exit",(e,t)=>{if(a.info("claude-adapter",`Claude process exited (code=${e}, signal=${t})`),this.claudeProcess=null,this.claudeChildPid=0,this.alive=!1,this.releaseNotifyPortReservation(),this.stopMcpServer(),this.clearPendingMcpFailureTimer(),!this.tryRecoverSessionIdConflict()){if(this.activeEvent&&!this.stopped){const i=this.activeEvent.replied?"responded":"failed",s=this.activeEvent.replied?void 0:"Claude process exited";a.error("claude-adapter",`Claude process exited with active event ${this.activeEvent.eventId}, replied=${!!this.activeEvent.replied}, sending ${i}`),this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,i,s),this.clearActiveEvent()}this.compacting&&this.finishCompaction("process-exit"),this.stopped||this.emit("exit",e)}}),this.claudeProcess.stdout?.on("data",e=>{this.handleClaudeOutput(e.toString())}),this.claudeProcess.stderr?.on("data",e=>{const t=e.toString().trim();t&&a.info("claude-adapter",`[claude stderr] ${t}`),this.checkFailurePatterns(t)})),this.claudePty&&(this.claudePty.onExit(({exitCode:e,signal:t})=>{if(a.info("claude-adapter",`Claude PTY process exited (code=${e}, signal=${t})`),this.claudePty=null,this.claudeChildPid=0,this.alive=!1,this.releaseNotifyPortReservation(),this.stopMcpServer(),this.clearPendingMcpFailureTimer(),!this.tryRecoverSessionIdConflict()){if(this.activeEvent&&!this.stopped){const i=this.activeEvent.replied?"responded":"failed",s=this.activeEvent.replied?void 0:"Claude process exited";a.error("claude-adapter",`Claude PTY exited with active event ${this.activeEvent.eventId}, replied=${!!this.activeEvent.replied}, sending ${i}`),this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,i,s),this.clearActiveEvent()}this.compacting&&this.finishCompaction("process-exit"),this.stopped||this.emit("exit",e)}}),this.claudePty.onData(e=>{this.handleClaudeOutput(e),this.checkFailurePatterns(e)}))}handleClaudeOutput(e){this.compacting&&this.resetCompactingActivityTimer();const t=e.trim();if(t){const o=t.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g,"").replace(/[\x00-\x1f\x7f-\x9f\u200b-\u200f\u2028-\u202f\ufeff]/g,"").trim();o?o.length<=3||/^[✳✶✻✽✵❋✺✴❈❖✦✧✢◉◎⬥⬦◇◆▸▹►▻→←↑↓⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏·…\d]*still\s+thinking/i.test(o)?a.debug("claude-adapter",`[claude output] ${o}`):a.info("claude-adapter",`[claude output] ${o.slice(0,500)}`):a.debug("claude-adapter",`[claude output] ${t.slice(0,200).replace(/[^\x20-\x7e]/g,c=>`\\x${c.charCodeAt(0).toString(16).padStart(2,"0")}`)}`)}this.activeEvent&&this.markActiveEventActivity(this.activeEvent.eventId,this.activeEvent.sessionId);const i=t.match(/Session ID (\S+) is already in use/i);if(i){const o=i[1];this.sessionIdConflictDetected=!0;const c=[this.claudeProcess?.pid,this.claudeChildPid,this.claudePty?.pid].filter(u=>!!u&&u>0);X(o,c)}const s=t.replace(/\[[0-9;?]*[ -/]*[@-~]/g," ").replace(/[^a-zA-Z]+/g," ").toLowerCase(),n=s.replace(/ /g,"");if(this.claudePty&&!this.startupChannelListening&&/trust.*folder|quick.*safety.*check/.test(n)&&(a.info("claude-adapter","Auto-accepting workspace trust dialog"),this.claudePty.write("1\r")),(/listeningforch\w*nelmessages/.test(n)||/nnelmessagesfrom/.test(n)||/inboundmessageswillbepushedintothissession/.test(n))&&(this.startupChannelListening||(this.startupChannelListeningAt=Date.now(),this.startupChannelListening=!0,this.sendDeferredModelSwitch()),this.clearPendingMcpFailureTimer()),!this.startupChannelListening&&/channels?arenotcurrentlyavailable/.test(n)&&(this.channelGateClosed=!0,a.warn("claude-adapter",'Claude reports "Channels are not currently available" \u2014 channel gate (tengu_harbor) is closed for this account/model; switching to single-turn fallback')),!this.mcpStartupFailureHandled&&s.includes("mcp server failed")){if(this.startupChannelListening){a.warn("claude-adapter","Claude reported MCP server failed for one channel, but channel listening is active; ignoring non-blocking failure");return}this.pendingMcpFailureTimer||(this.pendingMcpFailureTimer=setTimeout(()=>{this.pendingMcpFailureTimer=null,!(this.startupChannelListening||this.mcpStartupFailureHandled)&&this.markMcpStartupFailure()},Xe))}}checkFailurePatterns(e){/Session ID (\S+) is already in use/i.test(e)&&(this.sessionIdConflictDetected=!0),Be.some(t=>t.test(e))&&(a.error("claude-adapter",`Auth failure: ${e}`),this.authFailureUntil=Date.now()+le,this.activeEvent&&(this.bridgeCallbacks.sendStreamChunk(this.activeEvent.eventId,this.activeEvent.sessionId,`
1
+ import{spawn as B,spawnSync as Se,execSync as O,execFile as ye}from"node:child_process";import{promisify as we}from"node:util";import{randomUUID as Q}from"node:crypto";import{killProcessGroup as L}from"../../core/runtime/spawn.js";import{readdirSync as Ce,readFileSync as J,rmSync as Pe,statSync as D,existsSync as x,openSync as ke,writeSync as Ie,closeSync as Ae,constants as ie,watch as $e}from"node:fs";import{mkdir as R,readFile as C,rm as se,stat as ne,writeFile as I}from"node:fs/promises";import{join as h,resolve as be}from"node:path";import{homedir as T,tmpdir as Te}from"node:os";import G from"node:net";import{EventEmitter as re}from"node:events";import{fileURLToPath as Re}from"node:url";let j=null;if(process.platform==="win32")try{j=await import("node-pty")}catch{j=null}import{resolveRuntimePaths as w}from"../../core/config/index.js";import{SESSION_MODE_IDS as N}from"./protocol-contract.js";import{log as a}from"../../core/log/index.js";import{ActivityStatusManager as De}from"./activity-status-manager.js";import{HookSignalStore as xe}from"../../core/hooks/hook-signal-store.js";import{QuestionStore as ae}from"../../core/persistence/question-store.js";import{PermissionStore as F}from"../../core/persistence/permission-store.js";import{InternalApiServer as Me}from"../../core/mcp/internal-api-server.js";import{executeEventTool as Le,isEventTool as Ne}from"../../core/mcp/event-tool-executor.js";import{validateToolArgs as Oe}from"../../core/mcp/tool-schemas.js";import{ACCESS_CONTROL_ACTION_MAP as je,isGrixInternalToolName as Fe,normalizeEventToolArgs as qe}from"../../core/mcp/tools.js";import{scanSkills as He}from"./skill-scanner.js";import{extractLastAssistantEntry as oe,extractLastAssistantText as ce,probeSessionTurnState as le,resolveSessionJsonlPath as z}from"./usage-parser.js";import{readSettingsEnv as Ue}from"./model-list.js";import{resolveCliPath as Be,getCliVersion as Qe}from"../../core/util/cli-probe.js";const M="grix";function Je(u){if(!Array.isArray(u))return;const e=u.map(t=>{if(typeof t=="string")return t.trim();if(!t||typeof t!="object")return"";const i=t,s=String(i.label??"").trim();if(s)return s;const n=String(i.value??"").trim();if(n)return n;const r=String(i.text??"").trim();return r||""}).filter(t=>t.length>0);return e.length>0?e:void 0}function de(u){let e;try{e=JSON.parse(u)}catch{return null}const t=e.questions;if(!Array.isArray(t)||t.length===0)return null;const i=[];for(let s=0;s<t.length;s++){const n=t[s];if(!n||typeof n!="object")continue;const r=n,o=`Question ${s+1}`,c=String(r.header??r.question??o).trim()||o,d=String(r.prompt??r.question??"").trim();if(!d)continue;const l=Je(r.options),p=r.multiSelect===!0||r.multi_select===!0;i.push({header:c,prompt:d,...l?{options:l}:{},...p?{multi_select:!0}:{}})}return i.length>0?i:null}function Ge(u){if(!u||typeof u!="string")return null;try{const e=JSON.parse(u);return(typeof e.plan=="string"?e.plan.trim():"")||null}catch{return null}}const ze=[/Please run \/login/,/API Error:\s*401/,/authentication_error/,/OAuth token has expired/],We=[/You're out of extra usage/i,/Stop and wait for limit to reset/i,/Add funds to continue with extra usage/i],Ve=/API Error:\s*400.*server_tool_use\.id/,ue=300*1e3,Ye=1800*1e3,W=60*1e3,V=600*1e3,Xe=300*1e3,Y=90*1e3,he=1800*1e3,Ke=300*1e3,Ze=30*1e3,et=30*1e3;function tt(u){try{return process.kill(u,0),!0}catch(e){return e.code==="EPERM"}}const it=12e4,st=3e4,nt=3e3,q=1e3,pe=45e3,rt=18e3,X=8192,K=50;let fe=!1;const Z=new Set;async function at(u=[]){const e=new Set(u.filter(t=>Number.isInteger(t)&&t>0));for(let t=0;t<20;t++){const i=await new Promise((s,n)=>{const r=G.createServer();r.once("error",n),r.listen(0,"127.0.0.1",()=>{const o=r.address(),c=typeof o=="object"&&o?o.port:0;r.close(d=>{if(d){n(d);return}s(c)})})});if(i>0&&!e.has(i)&&!Z.has(i))return Z.add(i),i}throw new Error("\u65E0\u6CD5\u5206\u914D MCP \u901A\u77E5\u7AEF\u53E3")}function ot(u){u>0&&Z.delete(u)}const me=["You are connected to a chat via the grix-claude MCP server.",'Messages arrive as <channel source="grix-claude" chat_id="..." event_id="..." message_id="..." user_id="...">text</channel>.',"IMPORTANT: You MUST use the reply tool to send any response to the user.","Your plain text output is NOT delivered to the chat \u2014 only the reply tool delivers messages.","Always call the reply tool with chat_id, event_id, and your response text.","If you intentionally do not want to send a visible reply, you must call the complete tool with event_id and a final status."].join(" ");function ve(u){return String(u??"").trim().toLowerCase()===N.approval?N.approval:N.fullAuto}class Ee extends re{type="claude";config;bridgeCallbacks;mcpServerProcess=null;internalApi=null;claudeProcess=null;claudePty=null;spawnPromise=null;lifecycleVersion=0;sessionId="";alive=!1;stopped=!1;activeEvent=null;activeEventIdleTimer=null;activeEventHardTimer=null;activeEventPostReplyTimer=null;activeEventPostReplyWatcher=null;queuedEvents=[];queuedEventIds=new Set;stopHookBarrierSessionId=null;stopHookBarrierTimer=null;selfDrivenActive=!1;selfDrivenLastSignalAt=0;selfDrivenSweepTimer=null;compacting=!1;compactingTimer=null;compactionDoneResolver=null;mcpServerReady=!1;mcpStartupFailureHandled=!1;mcpChannelBroken=!1;channelGateClosed=!1;startupChannelListening=!1;startupChannelListeningAt=0;pendingMcpFailureTimer=null;sessionIdConflictDetected=!1;sessionIdConflictRetriedEventIds=new Set;cachedCliVersion=null;runtimeResolver=null;activityManager=null;lastPreToolInput="";deferredModelId=null;claudeChildPid=0;claudeCliSessionId="";claudeSessionCwd="";claudeMcpConfigPath="";expectRunDir="";sessionState=null;authFailureUntil=0;usageLimitUntil=0;completedEventIds=new Map;pendingQuestion=new Map;pendingPermissions=new Map;lastClearedEvent=null;constructor(e,t){super(),this.config=e,this.bridgeCallbacks=t;const s=(e.options??{}).sessionRuntimeResolver;typeof s=="function"&&(this.runtimeResolver=s)}async start(){this.lifecycleVersion+=1,this.stopped=!1,this.alive=!0,this.internalApi=new Me,this.internalApi.setInvokeHandler(async(e,t,i)=>this.handleInternalInvoke(e,t,i)),this.internalApi.setStatusLineHandler(async e=>{this.handleStatusLineUpdate(e)}),await this.internalApi.start(0),this.internalApiPort=parseInt(new URL(this.internalApi.baseUrl).port,10),this.notifyPort=0,a.info("claude-adapter",`Adapter started (stdio MCP mode, internal API at ${this.internalApi.baseUrl})`)}async stop(){if(a.info("claude-adapter",`Stopping adapter (sessionId=${this.sessionId}, alive=${this.alive}, activeEvent=${this.activeEvent?.eventId??"none"})`),this.lifecycleVersion+=1,this.stopped=!0,this.alive=!1,this.activeEvent&&(this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,"canceled","adapter stopped"),this.clearActiveEvent()),this.stopComposing(),this.rejectQueuedEvents("adapter stopped","canceled"),this.stopSelfDriven(),this.clearStopHookBarrier(),this.compacting=!1,this.compactingTimer&&(clearTimeout(this.compactingTimer),this.compactingTimer=null),this.compactionDoneResolver&&(this.compactionDoneResolver("stopped"),this.compactionDoneResolver=null),this.mcpChannelBroken=!1,this.channelGateClosed=!1,this.startupChannelListening=!1,this.startupChannelListeningAt=0,this.deferredModelId=null,this.clearPendingMcpFailureTimer(),this.ptyAutoConfirmTimer&&(clearInterval(this.ptyAutoConfirmTimer),this.ptyAutoConfirmTimer=null),this.pendingPermissions.size>0){const s=w(),n=new F(s.permissionRequestsDir);for(const[r]of this.pendingPermissions)n.resolveRequest(r,"deny").catch(()=>{});this.pendingPermissions.clear()}if(this.activityManager&&(this.activityManager.stop(),this.activityManager=null),this.sessionId){const s=this.resolveHookSignalsPath();se(s).catch(()=>{})}this.stopMcpServer();const e=this.claudeChildPid;this.claudeChildPid=0;const t=this.claudeProcess,i=this.claudePty;if(this.claudeProcess=null,this.claudePty=null,this.spawnPromise=null,i)try{i.kill()}catch{}if(e>0)try{process.kill(e,"SIGTERM")}catch{}if(t?.pid&&(L(t,"SIGTERM"),!await Promise.race([new Promise(n=>{t.once("exit",()=>n(!0))}),new Promise(n=>{setTimeout(()=>n(!1),5e3)})]))){if(e>0)try{process.kill(e,"SIGKILL")}catch{}L(t,"SIGKILL")}if(e>0){const s=Date.now();for(;Date.now()-s<5e3;)try{process.kill(e,0),await new Promise(n=>setTimeout(n,100))}catch{break}try{process.kill(e,"SIGKILL")}catch{}}this.releaseNotifyPortReservation(),this.claudeCliSessionId&&U(this.claudeCliSessionId),this.internalApi&&(await this.internalApi.stop(),this.internalApi=null)}isAlive(){return this.alive}async createSession(e){return`claude-session-${Date.now()}`}async resumeSession(e,t){}async destroySession(e){}sendPrompt(e){const t=new ct(e.adapterSessionId),i={event_id:e.adapterSessionId,session_id:e.adapterSessionId,content:e.text,context_messages_json:e.contextMessages?JSON.stringify(e.contextMessages):void 0};return this.deliverInboundEvent(i),this.once(`reply:${e.adapterSessionId}`,s=>{t.emitDone(s.status==="completed"?{status:"completed"}:{status:"failed",error:"failed"})}),t}async cancel(e){}setPermissionHandler(e){}async ping(e){if(this.stopped)return!1;if(!this.claudeProcess&&!this.claudePty)return this.alive&&this.internalApi!==null;if(!(this.alive&&this.mcpServerReady&&(this.startupChannelListening||process.platform==="win32")))return!1;const t=this.claudeProcess?.pid??this.claudePty?.pid;if(t&&!tt(t))return!1;const i=this.claudeProcess;if(i){const s=!!(i.stdin&&!i.stdin.destroyed),n=!!(i.stdout&&!i.stdout.destroyed);if(!s||!n)return!1}return!0}getStatus(){return{alive:this.alive,busy:this.activeEvent!==null||this.compacting,sessions:0,details:this.sessionState?{sessionState:this.sessionState}:void 0}}getActiveEventIds(){return this.activeEvent?[this.activeEvent.eventId]:[]}clearActiveEventForShutdown(){this.activeEvent&&this.clearActiveEvent()}getSessionState(){return this.sessionState}getMcpConfig(){return null}async probe(e){const t=this.config.command||"claude",i=await Be(t),s=i!==null;let n=null,r;if(s){const p=await Qe(t);n=p.version,p.error&&(r=p.error)}else r={code:"cli_not_found",message:`command not found: ${t}`};const c=(Ue().ANTHROPIC_BASE_URL??process.env.ANTHROPIC_BASE_URL??"").trim()||null,d=(this.sessionState?.model?.id??(process.env.ANTHROPIC_MODEL??"").trim())||null;let l={attempted:!1,ok:!1,latency_ms:null};if(e?.conversation&&s){const p=Date.now();try{await we(ye)(t,["-p","ping","--output-format","json","--max-turns","1"],{timeout:e.timeoutMs??8e3,encoding:"utf-8"}),l={attempted:!0,ok:!0,latency_ms:Date.now()-p}}catch(f){const v=f,P=v.killed?"conversation_timeout":"conversation_failed";l={attempted:!0,ok:!1,latency_ms:Date.now()-p,error:{code:P,message:v.message??String(f)}}}}return{cli:{command:t,installed:s,path:i,version:n,...r?{error:r}:{}},conversation:l,config:{model:d,base_url:c,source:{model:this.sessionState?.model?.id?"runtime":process.env.ANTHROPIC_MODEL?"env":"unknown",base_url:c?"env":"unknown"}},process:{started:!!(this.claudeProcess||this.claudePty),alive:this.alive,busy:this.activeEvent!==null},...this.probeSessionRecord()}}probeSessionRecord(){const e=this.claudeCliSessionId;if(!e||!this.claudeSessionCwd)return{session:{recordPath:null,lastActivityMs:null,freshMs:null}};const t=z(e,this.claudeSessionCwd);try{const i=D(t);return{session:{recordPath:t,lastActivityMs:i.mtimeMs,freshMs:Date.now()-i.mtimeMs}}}catch{return{session:{recordPath:t,lastActivityMs:null,freshMs:null}}}}getSupportedCommands(){return[{name:"compact",description:"\u538B\u7F29\u4E0A\u4E0B\u6587",args:"[instructions]"},{name:"clear",description:"\u6E05\u9664\u5BF9\u8BDD"},{name:"model",description:"\u5207\u6362\u6A21\u578B",args:"<model-id>"},{name:"cost",description:"\u663E\u793A\u8D39\u7528"},{name:"rewind",description:"\u56DE\u9000\u5BF9\u8BDD"},{name:"memory",description:"\u8BB0\u5FC6\u7BA1\u7406"},{name:"doctor",description:"\u8BCA\u65AD"},{name:"status",description:"\u72B6\u6001\u663E\u793A"},{name:"skills",description:"\u83B7\u53D6 skills \u6E05\u5355"}]}async execCommand(e,t,i){if(e==="skills")try{const n=He({mode:"claude",projectDir:this.claudeSessionCwd}),r=n.map(o=>{const c=o.trigger?` (${o.trigger})`:"",d=o.pluginName?` [plugin:${o.pluginName}]`:` [${o.source}]`;return`- ${o.name}${c}${d}: ${o.description}`});return{status:"ok",message:r.length>0?r.join(`
2
+ `):"No skills found",data:n}}catch(n){return{status:"failed",message:`Failed to scan skills: ${n instanceof Error?n.message:n}`}}if(!this.claudeProcess&&!this.claudePty)if(e==="compact"){try{await this.ensureClaudeProcessReady()}catch(n){return{status:"failed",message:`Failed to start Claude for compact: ${n instanceof Error?n.message:n}`}}if(await new Promise(n=>setTimeout(n,2e3)),!this.claudeProcess&&!this.claudePty)return{status:"failed",message:"Claude process exited during startup"}}else return{status:"failed",message:"Claude process is not running"};if(this.getStatus().busy)return{status:"failed",message:"Claude is busy, try again later"};const s=t?`/${e} ${t}`:`/${e}`;try{if(this.claudePty)this.claudePty.write(`${s}\r`);else if(this.expectRunDir){const n=h(this.expectRunDir,"cmd.fifo");let r;try{r=ke(n,ie.O_WRONLY|ie.O_NONBLOCK)}catch(o){const c=o.code;throw new Error(c==="ENXIO"?"expect process is not running \u2014 cannot inject command":`FIFO open failed: ${o instanceof Error?o.message:o}`)}try{Ie(r,`${s}
3
+ `)}finally{Ae(r)}}else this.claudeProcess?.stdin?.write(`${s}
4
+ `);if(e==="compact"){const n=new Promise(o=>{this.compactionDoneResolver=o});this.beginCompaction();const r=await n;return r==="fallback-timeout"?{status:"failed",message:"Compact timed out (60s) \u2014 context was not compressed"}:r==="process-exit"?{status:"failed",message:"Claude exited during compaction"}:r==="stopped"?{status:"failed",message:"Adapter stopped during compaction"}:{status:"ok",message:"Compacted"}}return{status:"ok",message:`Sent: ${s}`}}catch(n){return{status:"failed",message:`Failed to send: ${n instanceof Error?n.message:n}`}}}handleStatusLineUpdate(e){try{const t=e.context_window,i=e.cost,s=e.model,n=e.rate_limits??e.rateLimits,r=t?.current_usage;if(this.sessionState={contextWindow:{usedPercentage:t?.used_percentage!=null?E(t.used_percentage):null,remainingPercentage:t?.remaining_percentage!=null?E(t.remaining_percentage):null,totalInputTokens:E(t?.total_input_tokens),totalOutputTokens:E(t?.total_output_tokens),contextWindowSize:E(t?.context_window_size)||2e5,currentUsage:r?{inputTokens:E(r.input_tokens),outputTokens:E(r.output_tokens),cacheCreationInputTokens:E(r.cache_creation_input_tokens),cacheReadInputTokens:E(r.cache_read_input_tokens)}:null},cost:{totalCostUsd:E(i?.total_cost_usd),totalDurationMs:E(i?.total_duration_ms),totalApiDurationMs:E(i?.total_api_duration_ms),totalLinesAdded:E(i?.total_lines_added),totalLinesRemoved:E(i?.total_lines_removed)},rateLimits:this.parseStatusRateLimits(n),model:{id:String(s?.id??""),displayName:String(s?.display_name??"")},fastMode:e.fast_mode===!0,effort:e.effort!=null?String(e.effort):void 0,thinkingEnabled:e.thinking_enabled===!0,exceeds200kTokens:e.exceeds_200k_tokens===!0,version:String(e.version??""),sampledAt:Date.now()},n){const o=this.sessionState.rateLimits?.fiveHour,c=this.sessionState.rateLimits?.sevenDay;a.info("claude-adapter",`[rate-limits] statusLine parsed: fiveHour=${o?`${o.usedPercentage}% resetsAt=${o.resetsAt}`:"n/a"} sevenDay=${c?`${c.usedPercentage}% resetsAt=${c.resetsAt}`:"n/a"}`)}else a.debug("claude-adapter","[rate-limits] statusLine: no rate_limits in payload");try{this.bridgeCallbacks.onStatusLineUpdated?.(this.sessionState)}catch(o){a.warn("claude-adapter",`onStatusLineUpdated callback error: ${o instanceof Error?o.message:o}`)}}catch(t){a.warn("claude-adapter",`Failed to parse statusLine payload: ${t instanceof Error?t.message:t}`)}}parseStatusRateLimits(e){if(!e)return;const t=n=>{if(!n||typeof n!="object")return;const r=n,o=E(r.used_percentage??r.usedPercent),c=E(r.resets_at??r.resetsAt);if(!(o<=0&&c<=0))return{usedPercentage:o,resetsAt:c}},i=t(e.five_hour??e.fiveHour),s=t(e.seven_day??e.sevenDay);if(!(!i&&!s))return{...i?{fiveHour:i}:{},...s?{sevenDay:s}:{}}}deliverInboundEvent(e){if(this.sessionId||(this.sessionId=e.session_id),this.pruneCompletedEvents(),this.completedEventIds.has(e.event_id)){a.info("claude-adapter",`Event ${e.event_id} rejected: duplicate`),this.bridgeCallbacks.sendEventResult(e.event_id,"responded","duplicate event");return}if(Date.now()<this.authFailureUntil){a.info("claude-adapter",`Event ${e.event_id} rejected: auth cooldown`),this.bridgeCallbacks.sendEventResult(e.event_id,"failed","Claude authentication failed \u2014 please re-login");return}if(Date.now()<this.usageLimitUntil){a.info("claude-adapter",`Event ${e.event_id} rejected: usage limit cooldown`),this.bridgeCallbacks.sendEventResult(e.event_id,"failed","Claude usage limit reached \u2014 waiting for reset");return}if(this.compacting){if(this.queuedEventIds.has(e.event_id))return;if(this.queuedEvents.length>=K){a.warn("claude-adapter",`Event ${e.event_id} rejected: queue full during compaction (size=${this.queuedEvents.length})`),this.bridgeCallbacks.sendEventResult(e.event_id,"failed","agent queue full");return}this.queuedEvents.push(e),this.queuedEventIds.add(e.event_id),a.info("claude-adapter",`Queue event ${e.event_id} (compacting, size=${this.queuedEvents.length})`);return}if(this.activeEvent){if(this.activeEvent.eventId===e.event_id){a.info("claude-adapter",`Event ${e.event_id} ignored: same as active event`);return}if(this.queuedEventIds.has(e.event_id))return;if(this.queuedEvents.length>=K){a.warn("claude-adapter",`Event ${e.event_id} rejected: queue full (size=${this.queuedEvents.length})`),this.bridgeCallbacks.sendEventResult(e.event_id,"failed","agent queue full");return}this.queuedEvents.push(e),this.queuedEventIds.add(e.event_id),a.info("claude-adapter",`Queue event ${e.event_id} (active=${this.activeEvent.eventId}, size=${this.queuedEvents.length})`);return}if(this.stopHookBarrierSessionId===e.session_id){if(this.queuedEventIds.has(e.event_id))return;if(this.queuedEvents.length>=K){a.warn("claude-adapter",`Event ${e.event_id} rejected: queue full during barrier (size=${this.queuedEvents.length})`),this.bridgeCallbacks.sendEventResult(e.event_id,"failed","agent queue full");return}this.queuedEvents.push(e),this.queuedEventIds.add(e.event_id),a.info("claude-adapter",`Queue event ${e.event_id} (barrier session=${e.session_id})`);return}this.activeEvent={eventId:e.event_id,sessionId:e.session_id,rawEvent:e},this.stopSelfDriven(),this.lastClearedEvent=null,this.markActiveEventActivity(e.event_id,e.session_id),this.resetActiveEventHardTimer(e.event_id),this.emit("eventStarted",e.event_id,e.session_id),this.ensureClaudeAndPushEvent(e)}deliverStopEvent(e,t){if(this.activeEvent?.eventId!==e)return;const i=this.activeEvent.sessionId;a.info("claude-adapter",`Stop requested for active event=${e} \u2014 killing Claude process`),this.mcpServerReady&&this.pushNotification("notifications/event_stop",{event_id:e,session_id:i,stop_id:Q()}),this.bridgeCallbacks.sendEventResult(e,"canceled","stopped by user"),this.clearActiveEvent(),this.killClaudeProcess("stop")}async handleLocalAction(e){if(!this.mcpServerReady)return{handled:!1,kind:""};const t=String(e.action_type??"");if(t==="claude_interaction_reply"){const i=e.params??{};if(String(i.kind??"")==="permission"){const n=i.resolution??{},r=String(n.value??""),o={...e,action_type:r==="allow"?"exec_approve":"exec_reject",params:{...i,approval_command_id:i.request_id}};return this.handlePermissionApproval(o)}return this.handleQuestionReply(e)}return t==="exec_approve"||t==="exec_reject"?this.handlePermissionApproval(e):(this.pushNotification("notifications/local_action",e),{handled:!0,kind:""})}async handleQuestionReply(e){const t=e.params??{},i=String(t.request_id??"");if(!i)return a.warn("claude-adapter","Question reply missing request_id"),this.bridgeCallbacks.sendLocalActionResult(e.action_id,"failed",void 0,"request_id_required","request_id is required"),{handled:!0,kind:"question_reply_no_id"};const s=w(),n=new ae(s.questionRequestsDir),r=t.resolution??{},o=String(r.type??""),c=r;if(o==="action"&&String(r.value??"")==="cancel"){a.info("claude-adapter",`Question cancelled by user: request_id=${i}`),await n.resolveRequest(i,"cancel",void 0,c);const f=this.pendingQuestion.get(i);return f&&this.activeEvent?.eventId===f.eventId&&(this.activeEvent.awaitingUserQuestion=!1),this.pendingQuestion.delete(i),this.bridgeCallbacks.sendLocalActionResult(e.action_id,"ok",{request_id:i,resolution:"cancel"}),{handled:!0,kind:"question_reply_cancel"}}let d="";if(o==="text"?d=String(r.value??""):o==="map"&&(d=(Array.isArray(r.entries)?r.entries:[]).map(f=>f.value).join(", ")),!d){a.warn("claude-adapter",`Empty answer for question reply request_id=${i}`),await n.resolveRequest(i,"cancel",void 0,c);const p=this.pendingQuestion.get(i);return p&&this.activeEvent?.eventId===p.eventId&&(this.activeEvent.awaitingUserQuestion=!1),this.pendingQuestion.delete(i),this.bridgeCallbacks.sendLocalActionResult(e.action_id,"ok",{request_id:i,resolution:"cancel"}),{handled:!0,kind:"question_reply_empty"}}a.info("claude-adapter",`Resolving question in store: request_id=${i} answer=${d.slice(0,80)}`),await n.resolveRequest(i,"answer",d,c);const l=this.pendingQuestion.get(i);return l&&this.activeEvent?.eventId===l.eventId&&(this.activeEvent.awaitingUserQuestion=!1),this.pendingQuestion.delete(i),this.bridgeCallbacks.sendLocalActionResult(e.action_id,"ok",{request_id:i,resolution:"answer"}),{handled:!0,kind:"question_reply"}}async handlePermissionApproval(e){const t=e.params??{},i=String(t.approval_id??t.approval_command_id??t.tool_call_id??""),s=String(e.action_type??"")==="exec_approve";if(!i)return this.bridgeCallbacks.sendLocalActionResult(e.action_id,"failed",void 0,"approval_id_required","approval_id is required"),{handled:!0,kind:"permission_approval"};if(!this.pendingPermissions.get(i))return this.bridgeCallbacks.sendLocalActionResult(e.action_id,"failed",void 0,"approval_not_found",`no pending permission for approval_id: ${i}`),{handled:!0,kind:"permission_approval"};const r=w(),o=new F(r.permissionRequestsDir),c=s?"allow":"deny";return await o.resolveRequest(i,c),this.pendingPermissions.delete(i),this.bridgeCallbacks.sendLocalActionResult(e.action_id,"ok"),a.info("claude-adapter",`Permission ${c}: approvalId=${i}`),{handled:!0,kind:"permission_approval"}}async ensureClaudeAndPushEvent(e,t=0){t===0&&!this.claudeProcess&&this.startComposing(e.session_id,"preparing");try{if(this.mcpChannelBroken){await this.runSingleTurnFallback(e,"mcp channel unavailable");return}if(await this.isChannelGateClosed()){await this.runSingleTurnFallback(e,"channel gate closed: tengu_harbor not enabled in cachedGrowthBookFeatures");return}if(await this.ensureClaudeProcessReady(),!this.mcpServerReady)throw new Error("MCP stdio server not available");if(await this.waitForNotifyPort(3e4),process.platform==="win32"?await this.waitForWindowsChannelReady(pe):await this.waitForChannelListening(pe),this.activeEvent?.eventId!==e.event_id||this.mcpChannelBroken)return;const s={},n=[["chat_id",e.session_id],["event_id",e.event_id],["event_type","user_chat"],["message_id",e.msg_id],["sender_id",e.sender_id],["user",e.sender_id],["user_id",e.sender_id],["msg_id",e.msg_id],["session_type",e.session_type!=null?String(e.session_type):void 0],["msg_type",e.msg_type!=null?String(e.msg_type):void 0],["quoted_message_id",e.quoted_message_id],["ts",e.created_at!=null?new Date(Number(e.created_at)).toISOString():void 0],["owner_id",e.owner_id],["agent_id",e.agent_id],["attachments_json",e.attachments_json],["context_messages_json",e.context_messages_json]];for(const[r,o]of n)o!=null&&o!==""&&(s[r]=o);this.captureEventJsonlBaseOffset(e.event_id),this.pushNotification("notifications/claude/channel",{content:e.content??"",meta:s}),a.info("claude-adapter",`Event ${e.event_id} pushed to MCP stdio server`)}catch(s){if(this.stopped){this.activeEvent?.eventId===e.event_id&&(this.bridgeCallbacks.sendEventResult(e.event_id,"canceled","adapter stopped"),this.clearActiveEvent());return}if(s instanceof Error&&(s.message.includes("exited before")||s.message.includes("timeout")||s.message.includes("spawn failed"))&&t<3)return a.warn("claude-adapter",`Spawn failed (attempt ${t+1}/4), retrying: ${s}`),this.claudeProcess=null,this.claudeChildPid=0,this.alive=!1,this.releaseNotifyPortReservation(),this.stopMcpServer(),U(this.claudeCliSessionId),ee(this.claudeCliSessionId,[]),await new Promise(r=>setTimeout(r,(t+1)*2e3)),this.ensureClaudeAndPushEvent(e,t+1);if(a.error("claude-adapter",`Failed to deliver event ${e.event_id}: ${s}`),this.shouldFallbackToSingleTurn(s)){a.warn("claude-adapter",`Channel path failed, switching to single-turn fallback: ${s instanceof Error?s.message:String(s)}`);try{await this.runSingleTurnFallback(e,s instanceof Error?s.message:String(s));return}catch(r){a.error("claude-adapter",`Single-turn fallback failed for ${e.event_id}: ${r instanceof Error?r.message:String(r)}`)}}this.activeEvent?.eventId===e.event_id&&(this.bridgeCallbacks.sendEventResult(e.event_id,"failed",s instanceof Error?s.message:String(s)),this.clearActiveEvent())}}shouldFallbackToSingleTurn(e){const t=(e instanceof Error?e.message:String(e)).toLowerCase();return t?t.includes("mcp server startup failed")||t.includes("claude channel listener not ready")||t.includes("channel ready check timed out")||t.includes("channels are not currently available")||t.includes("mcp stdio server not available")||t.includes("notify port not ready")||t.includes("file not found:"):!1}buildSingleTurnPrompt(e){const t=e.context_messages_json?String(e.context_messages_json):"",i=["Reply to the latest user message directly and concisely."];return t&&(i.push("Conversation context (JSON):"),i.push(t)),i.push("Latest user message:"),i.push(e.content??""),i.join(`
5
+ `)}extractSingleTurnDelta(e){const t=e.trim();if(!t)return"";try{const i=JSON.parse(t),s=String(i.type??"");if(s==="content_block_delta"){const n=i.delta;if(n&&typeof n=="object"){const r=String(n.text??"");if(r)return r}}return s==="result"?String(i.result??""):typeof i.text=="string"&&i.text?i.text:""}catch{return t}}getCliVersion(){if(this.cachedCliVersion)return this.cachedCliVersion;const e=this.config.command||"claude";try{const i=O(`${e} --version`,{encoding:"utf-8",timeout:5e3}).trim().match(/^([\d.]+)/);this.cachedCliVersion=i?.[1]??"0.0.0"}catch{this.cachedCliVersion="0.0.0"}return this.cachedCliVersion}static versionGte(e,t){const i=e.split(".").map(Number),s=t.split(".").map(Number);for(let n=0;n<Math.max(i.length,s.length);n++){const r=i[n]??0,o=s[n]??0;if(r>o)return!0;if(r<o)return!1}return!0}buildClaudeRuntimeEnv(){const e={...process.env,...this.config.env};return delete e.CLAUDECODE,e.CLAUDE_PLUGIN_DATA=w().dataDir,e.GRIX_HOOK_SIGNALS_PATH=this.resolveHookSignalsPath(),this.internalApi&&(e.GRIX_CLAUDE_INTERNAL_API_URL=this.internalApi.baseUrl,e.GRIX_CLAUDE_INTERNAL_API_TOKEN=this.internalApi.token),e}async runSingleTurnFallback(e,t){const i=this.resolveSessionRuntime(),s=this.config.options??{},n=this.config.command||"claude",r=(this.config.args??[]).filter(P=>{const m=String(P).trim().toLowerCase();return m!=="--dangerously-load-development-channels"&&m!=="--session-id"&&m!=="--resume"}),o=String(i.cwd??"").trim(),c=String(i.modelId??"").trim(),d=ve(i.modeId);await this.ensureWorkspaceTrust(o),await this.ensureClaudeOnboardingFlags(o),await this.ensureSkipDangerousPermissionPrompt(),await this.injectStatusLineSettings(o),await this.ensureStdioMcpServer();const l=this.buildClaudeRuntimeEnv(),p=i.pluginDir??s.pluginDir??await this.ensureClaudePluginDir(),f=this.buildSingleTurnPrompt(e),v=[...r,"-p","--output-format","stream-json","--include-partial-messages","--no-session-persistence"];Ee.versionGte(this.getCliVersion(),"2.1.150")&&v.push("--verbose"),v.push("--tools","","--plugin-dir",p,"--strict-mcp-config","--mcp-config",this.claudeMcpConfigPath,"--permission-mode",d===N.fullAuto?"bypassPermissions":"default"),c&&v.push("--model",c),v.push(f),a.warn("claude-adapter",`Running single-turn fallback for event ${e.event_id}: ${t}`),this.bridgeCallbacks.sendEventAck(e.event_id,e.session_id),this.startComposing(e.session_id,"fallback_single_turn"),await new Promise((P,m)=>{const $=B(n,v,{cwd:o||process.cwd(),env:l,stdio:["ignore","pipe","pipe"],detached:!0,windowsHide:!0});let k="",b=0,_=!1;const g=`fallback_${e.event_id}_${Date.now()}`;let S="";$.stdout?.setEncoding("utf8"),$.stdout?.on("data",y=>{S+=y;let A=S.indexOf(`
6
+ `);for(;A>=0;){const _e=S.slice(0,A);S=S.slice(A+1);const te=this.extractSingleTurnDelta(_e);te&&(b+=1,_=!0,this.bridgeCallbacks.sendStreamChunk(e.event_id,e.session_id,te,b,!1,g,e.quoted_message_id)),A=S.indexOf(`
7
+ `)}}),$.stderr?.setEncoding("utf8"),$.stderr?.on("data",y=>{k.length>=X||(k+=y,k.length>X&&(k=k.slice(0,X)))}),$.on("error",y=>m(y)),$.on("close",y=>{if(S.trim()){const A=this.extractSingleTurnDelta(S);A&&(b+=1,_=!0,this.bridgeCallbacks.sendStreamChunk(e.event_id,e.session_id,A,b,!1,g,e.quoted_message_id))}if(y!==0&&!_){const A=k.trim();m(new Error(A||`claude single-turn exited with code ${y}`));return}b+=1,this.bridgeCallbacks.sendStreamChunk(e.event_id,e.session_id,"",b,!0,g,e.quoted_message_id),P()})}),this.activeEvent?.eventId===e.event_id&&(this.bridgeCallbacks.sendEventResult(e.event_id,"responded"),this.clearActiveEvent())}async ensureClaudeProcessReady(){this.claudeProcess||this.claudePty||(this.spawnPromise||(this.spawnPromise=this.spawnClaude().finally(()=>{this.spawnPromise=null})),await this.spawnPromise)}async spawnClaude(){if(this.claudeProcess)return;const e=this.lifecycleVersion,t=()=>{if(this.stopped||e!==this.lifecycleVersion)throw new Error("adapter stopped")},i=this.config.options??{};this.sessionIdConflictDetected=!1;const s=this.resolveSessionRuntime(),n=this.config.command||"claude",r=this.config.args??[],o=this.buildClaudeRuntimeEnv(),c=ve(s.modeId),d=String(s.modelId??"").trim(),l=String(s.cwd??"").trim();this.claudeSessionCwd=l;const p=String(s.claudeSessionId??"").trim()||Q();this.claudeCliSessionId=p,!s.claudeSessionId&&s.onSessionIdAssigned&&s.onSessionIdAssigned(p);const f=vt(p,l||void 0);if(f||(ee(p,[]),U(p)),this.runtimeResolver&&!l)throw new Error("Claude session binding missing cwd \u2014 run /grix open <working-directory> first");if(!l)throw new Error("Claude runtime cwd is required");await this.ensureWorkspaceTrust(l),await this.ensureClaudeOnboardingFlags(l),await this.ensureSkipDangerousPermissionPrompt(),await this.injectStatusLineSettings(l);let v=!1,P="",m=null;try{t(),this.notifyPort=await at([this.internalApiPort]),v=!0,t(),a.info("claude-adapter",`Allocated MCP notify port ${this.notifyPort} (internal API ${this.internalApiPort})`),await this.ensureStdioMcpServer(),t();const k=i.pluginDir||await this.ensureClaudePluginDir(),b=f?["--resume",p]:["--session-id",p],_=[...r,"--name",`grix-${this.sessionId}`,...b,"--plugin-dir",k,"--strict-mcp-config","--mcp-config",this.claudeMcpConfigPath];if(d&&(/^claude/i.test(d)?_.push("--model",d):(a.warn("claude-adapter",`Skipping --model ${d}: non-Anthropic models are incompatible with development channels. Using default model so channels can load. Switch models after startup via /model if needed.`),this.deferredModelId=d)),c===N.fullAuto&&_.push("--dangerously-skip-permissions"),_.push("--dangerously-load-development-channels",`server:${M}`),process.platform==="win32"){const g=me.replace(/</g,"[").replace(/>/g,"]").replace(/"/g,"'"),S=pt(this.claudeCliSessionId);await mt(S,g),_.push("--append-system-prompt-file",S)}else _.push("--append-system-prompt",me);if(process.platform==="win32")if(j){const g="cmd.exe",S=["/c",n,..._];a.info("claude-adapter",`Spawning Claude via PTY on win32: cwd=${l} mode=${c} ${f?"resume":"new"} ${n} ${_.join(" ")}`);const y=j.spawn(g,S,{name:"xterm-256color",cols:120,rows:30,cwd:l,env:o,useConpty:!0,conptyInheritCursor:!1});this.claudePty=y,this.claudeChildPid=y.pid,this.startPtyAutoConfirm(y),m=null}else a.info("claude-adapter",`Spawning Claude via shell on win32 (node-pty unavailable): cwd=${l} mode=${c} ${f?"resume":"new"} ${n} ${_.join(" ")}`),m=B(n,_,{cwd:l,env:o,stdio:["pipe","pipe","pipe"],detached:!0,shell:!0,windowsHide:!0});else{a.info("claude-adapter",`Spawning via expect PTY: cwd=${l} mode=${c} ${f?"resume":"new"} ${n} ${_.join(" ")}`);try{D("/usr/bin/expect")}catch{throw new Error("/usr/bin/expect not found. Install it with: apt install expect / dnf install expect / brew install expect")}const g=h(Te(),`grix-claude-${Q()}`);await R(g,{recursive:!0}),this.expectRunDir=g;const{expectPath:S,pidPath:y}=await lt(g,n,_);P=y,t(),m=B("/usr/bin/expect",[S],{cwd:l,env:o,stdio:["pipe","pipe","pipe"],detached:!0})}if(this.claudeProcess=m,this.clearPendingMcpFailureTimer(),this.mcpStartupFailureHandled=!1,this.mcpChannelBroken=!1,this.channelGateClosed=!1,this.startupChannelListening=!1,this.startupChannelListeningAt=0,m&&m.on("error",g=>{a.error("claude-adapter","Claude process spawn error: "+g),this.claudeProcess=null,this.claudeChildPid=0,this.alive=!1,this.releaseNotifyPortReservation(),this.stopMcpServer(),this.clearPendingMcpFailureTimer(),this.activeEvent&&(this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,"failed","Claude process spawn failed"),this.clearActiveEvent())}),t(),!this.claudePty)if(process.platform==="win32")this.claudeChildPid=m?.pid??0;else{const g=await ut(m,P);if(t(),!Number.isFinite(g)||g<=0)throw new Error("failed to determine spawned Claude pid");this.claudeChildPid=g}if(!this.claudeChildPid||this.claudeChildPid<=0)throw new Error("failed to determine spawned Claude pid")}catch(k){if(m?.pid&&L(m,"SIGTERM"),this.claudePty){try{this.claudePty.kill()}catch{}this.claudePty=null}throw this.claudeProcess===m&&(this.claudeProcess=null),this.claudeChildPid=0,v&&this.releaseNotifyPortReservation(),this.stopMcpServer(),k}const $=this.claudeChildPid;a.info("claude-adapter","Claude child PID: "+$+(this.claudePty?" (via PTY)":"")),this.setupProcessOrPtyHandlers(),this.alive=!0}setupProcessOrPtyHandlers(){this.claudeProcess&&(this.claudeProcess.on("exit",(e,t)=>{if(a.info("claude-adapter",`Claude process exited (code=${e}, signal=${t})`),this.claudeProcess=null,this.claudeChildPid=0,this.alive=!1,this.releaseNotifyPortReservation(),this.stopMcpServer(),this.clearPendingMcpFailureTimer(),!this.tryRecoverSessionIdConflict()){if(this.activeEvent&&!this.stopped){const i=this.activeEvent.replied?"responded":"failed",s=this.activeEvent.replied?void 0:"Claude process exited";a.error("claude-adapter",`Claude process exited with active event ${this.activeEvent.eventId}, replied=${!!this.activeEvent.replied}, sending ${i}`),this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,i,s),this.clearActiveEvent()}this.compacting&&this.finishCompaction("process-exit"),this.stopped||this.emit("exit",e)}}),this.claudeProcess.stdout?.on("data",e=>{this.handleClaudeOutput(e.toString())}),this.claudeProcess.stderr?.on("data",e=>{const t=e.toString().trim();t&&a.info("claude-adapter",`[claude stderr] ${t}`),this.checkFailurePatterns(t)})),this.claudePty&&(this.claudePty.onExit(({exitCode:e,signal:t})=>{if(a.info("claude-adapter",`Claude PTY process exited (code=${e}, signal=${t})`),this.claudePty=null,this.claudeChildPid=0,this.alive=!1,this.releaseNotifyPortReservation(),this.stopMcpServer(),this.clearPendingMcpFailureTimer(),!this.tryRecoverSessionIdConflict()){if(this.activeEvent&&!this.stopped){const i=this.activeEvent.replied?"responded":"failed",s=this.activeEvent.replied?void 0:"Claude process exited";a.error("claude-adapter",`Claude PTY exited with active event ${this.activeEvent.eventId}, replied=${!!this.activeEvent.replied}, sending ${i}`),this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,i,s),this.clearActiveEvent()}this.compacting&&this.finishCompaction("process-exit"),this.stopped||this.emit("exit",e)}}),this.claudePty.onData(e=>{this.handleClaudeOutput(e),this.checkFailurePatterns(e)}))}handleClaudeOutput(e){this.compacting&&this.resetCompactingActivityTimer();const t=e.trim();if(t){const o=t.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g,"").replace(/[\x00-\x1f\x7f-\x9f\u200b-\u200f\u2028-\u202f\ufeff]/g,"").trim();o?o.length<=3||/^[✳✶✻✽✵❋✺✴❈❖✦✧✢◉◎⬥⬦◇◆▸▹►▻→←↑↓⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏·…\d]*still\s+thinking/i.test(o)?a.debug("claude-adapter",`[claude output] ${o}`):a.info("claude-adapter",`[claude output] ${o.slice(0,500)}`):a.debug("claude-adapter",`[claude output] ${t.slice(0,200).replace(/[^\x20-\x7e]/g,c=>`\\x${c.charCodeAt(0).toString(16).padStart(2,"0")}`)}`)}this.activeEvent&&this.markActiveEventActivity(this.activeEvent.eventId,this.activeEvent.sessionId);const i=t.match(/Session ID (\S+) is already in use/i);if(i){const o=i[1];this.sessionIdConflictDetected=!0;const c=[this.claudeProcess?.pid,this.claudeChildPid,this.claudePty?.pid].filter(d=>!!d&&d>0);ee(o,c)}const s=t.replace(/\[[0-9;?]*[ -/]*[@-~]/g," ").replace(/[^a-zA-Z]+/g," ").toLowerCase(),n=s.replace(/ /g,"");if(this.claudePty&&!this.startupChannelListening&&/trust.*folder|quick.*safety.*check/.test(n)&&(a.info("claude-adapter","Auto-accepting workspace trust dialog"),this.claudePty.write("1\r")),(/listeningforch\w*nelmessages/.test(n)||/nnelmessagesfrom/.test(n)||/inboundmessageswillbepushedintothissession/.test(n))&&(this.startupChannelListening||(this.startupChannelListeningAt=Date.now(),this.startupChannelListening=!0,this.sendDeferredModelSwitch()),this.clearPendingMcpFailureTimer()),!this.startupChannelListening&&/channels?arenotcurrentlyavailable/.test(n)&&(this.channelGateClosed=!0,a.warn("claude-adapter",'Claude reports "Channels are not currently available" \u2014 channel gate (tengu_harbor) is closed for this account/model; switching to single-turn fallback')),!this.mcpStartupFailureHandled&&s.includes("mcp server failed")){if(this.startupChannelListening){a.warn("claude-adapter","Claude reported MCP server failed for one channel, but channel listening is active; ignoring non-blocking failure");return}this.pendingMcpFailureTimer||(this.pendingMcpFailureTimer=setTimeout(()=>{this.pendingMcpFailureTimer=null,!(this.startupChannelListening||this.mcpStartupFailureHandled)&&this.markMcpStartupFailure()},nt))}}checkFailurePatterns(e){/Session ID (\S+) is already in use/i.test(e)&&(this.sessionIdConflictDetected=!0),ze.some(t=>t.test(e))&&(a.error("claude-adapter",`Auth failure: ${e}`),this.authFailureUntil=Date.now()+ue,this.activeEvent&&(this.bridgeCallbacks.sendStreamChunk(this.activeEvent.eventId,this.activeEvent.sessionId,`
8
8
 
9
- Error: Claude authentication failed -- please re-login`,1,!1),this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,"failed","Claude authentication failed -- please re-login"),this.clearActiveEvent())),Je.some(t=>t.test(e))&&(a.error("claude-adapter",`Usage limit: ${e}`),this.usageLimitUntil=Date.now()+le,this.activeEvent&&(this.bridgeCallbacks.sendStreamChunk(this.activeEvent.eventId,this.activeEvent.sessionId,`
9
+ Error: Claude authentication failed -- please re-login`,1,!1),this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,"failed","Claude authentication failed -- please re-login"),this.clearActiveEvent())),We.some(t=>t.test(e))&&(a.error("claude-adapter",`Usage limit: ${e}`),this.usageLimitUntil=Date.now()+ue,this.activeEvent&&(this.bridgeCallbacks.sendStreamChunk(this.activeEvent.eventId,this.activeEvent.sessionId,`
10
10
 
11
- Error: Claude usage limit reached -- waiting for reset`,1,!1),this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,"failed","Claude usage limit reached -- waiting for reset"),this.clearActiveEvent())),Ge.test(e)&&this.activeEvent&&(a.error("claude-adapter",`API format error (400 server_tool_use.id): ${e.slice(0,200)}`),this.activeEvent.apiFormatError=!0)}async ensureClaudePluginDir(){const e=this.resolveProjectRoot(),t=h(S().dataDir,"claude-plugin"),i=h(t,".claude-plugin"),s=h(i,"plugin.json"),n=h(e,".claude-plugin","plugin.json");await R(i,{recursive:!0});let r="";try{r=await C(n,"utf8")}catch{r=`${JSON.stringify({name:"grix-connector",version:"0.1.0",description:"Claude Code channel plugin for grix-connector.",license:"MIT"},null,2)}
12
- `}let o="";try{o=await C(s,"utf8")}catch{}o!==r&&(await I(s,r,"utf8"),a.info("claude-adapter",`Wrote Claude plugin manifest: ${s}`));const c=h(e,"dist","scripts"),u=m=>`"${String(m).replace(/"/g,'\\"')}"`,l=m=>`${u(process.execPath)} ${u(h(c,m))}`,p=`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",command:l("lifecycle-hook.js")}]}],Elicitation:[{hooks:[{type:"command",command:l("elicitation-hook.js")}]}],ElicitationResult:[{matcher:"",hooks:[{type:"command",command:l("lifecycle-hook.js")}]}],UserPromptSubmit:[{hooks:[{type:"command",command:l("user-prompt-submit-hook.js")}]}],PreToolUse:[{matcher:"ExitPlanMode",hooks:[{type:"command",command:l("approve-plan-hook.js")}]},{matcher:"",hooks:[{type:"command",command:l("lifecycle-hook.js")}]}],PostToolUse:[{matcher:"",hooks:[{type:"command",command:l("lifecycle-hook.js")}]}],PostToolUseFailure:[{matcher:"",hooks:[{type:"command",command:l("lifecycle-hook.js")}]}],PermissionRequest:[{matcher:"",hooks:[{type:"command",command:l("permission-hook.js")}]}],Notification:[{matcher:"idle_prompt",hooks:[{type:"command",command:l("notification-hook.js")}]}],PermissionDenied:[{matcher:"",hooks:[{type:"command",command:l("lifecycle-hook.js")}]}],Stop:[{hooks:[{type:"command",command:l("lifecycle-hook.js")}]}],StopFailure:[{hooks:[{type:"command",command:l("lifecycle-hook.js")}]}],PreCompact:[{hooks:[{type:"command",command:l("lifecycle-hook.js")}]}],PostCompact:[{hooks:[{type:"command",command:l("lifecycle-hook.js")}]}],ConfigChange:[{hooks:[{type:"command",command:l("lifecycle-hook.js")}]}]}},null,2)}
13
- `,f=h(t,"hooks"),v=h(f,"hooks.json");await R(f,{recursive:!0});let P="";try{P=await C(v,"utf8")}catch{}return P!==p&&(await I(v,p,"utf8"),a.info("claude-adapter",`Wrote Claude hooks config: ${v}`)),t}async ensureStdioMcpServer(){const e=this.resolveProjectRoot(),t=this.resolveStdioServerPath(e);if(this.ensureStdioServerArtifact(e,t),!L(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=at(this.claudeCliSessionId);await ct(r,n),this.claudeMcpConfigPath=r,this.mcpServerReady=!0,this.startActivityTracking()}resolveProjectRoot(){const e=be(import.meta.url);return Ie(e,"..","..","..","..")}resolveStdioServerPath(e=this.resolveProjectRoot()){return h(e,"dist","mcp","stdio","server.js")}ensureStdioServerArtifact(e,t){if(L(t)||ue)return;ue=!0;const i=h(e,"node_modules","typescript","bin","tsc"),s=h(e,"tsconfig.json");if(!L(i)||!L(s))return;a.warn("claude-adapter",`MCP stdio server artifact missing, attempting build: ${t}`);const n=ge(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=G.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>=q)return;await new Promise(r=>setTimeout(r,q-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
- `;"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(),c=Date.now()-o;if(c>=q)return;await new Promise(u=>setTimeout(u,q-c));return}const n=Date.now()-t,r=this.claudePty?Ze: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=G.createConnection({host:"127.0.0.1",port:this.notifyPort},()=>{this.notifySocket.write(i+`
11
+ Error: Claude usage limit reached -- waiting for reset`,1,!1),this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,"failed","Claude usage limit reached -- waiting for reset"),this.clearActiveEvent())),Ve.test(e)&&this.activeEvent&&(a.error("claude-adapter",`API format error (400 server_tool_use.id): ${e.slice(0,200)}`),this.activeEvent.apiFormatError=!0)}async ensureClaudePluginDir(){const e=this.resolveProjectRoot(),t=h(w().dataDir,"claude-plugin"),i=h(t,".claude-plugin"),s=h(i,"plugin.json"),n=h(e,".claude-plugin","plugin.json");await R(i,{recursive:!0});let r="";try{r=await C(n,"utf8")}catch{r=`${JSON.stringify({name:"grix-connector",version:"0.1.0",description:"Claude Code channel plugin for grix-connector.",license:"MIT"},null,2)}
12
+ `}let o="";try{o=await C(s,"utf8")}catch{}o!==r&&(await I(s,r,"utf8"),a.info("claude-adapter",`Wrote Claude plugin manifest: ${s}`));const c=h(e,"dist","scripts"),d=m=>`"${String(m).replace(/"/g,'\\"')}"`,l=m=>`${d(process.execPath)} ${d(h(c,m))}`,p=`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",command:l("lifecycle-hook.js")}]}],Elicitation:[{hooks:[{type:"command",command:l("elicitation-hook.js")}]}],ElicitationResult:[{matcher:"",hooks:[{type:"command",command:l("lifecycle-hook.js")}]}],UserPromptSubmit:[{hooks:[{type:"command",command:l("user-prompt-submit-hook.js")}]}],PreToolUse:[{matcher:"ExitPlanMode",hooks:[{type:"command",command:l("approve-plan-hook.js")}]},{matcher:"",hooks:[{type:"command",command:l("lifecycle-hook.js")}]}],PostToolUse:[{matcher:"",hooks:[{type:"command",command:l("lifecycle-hook.js")}]}],PostToolUseFailure:[{matcher:"",hooks:[{type:"command",command:l("lifecycle-hook.js")}]}],PermissionRequest:[{matcher:"",hooks:[{type:"command",command:l("permission-hook.js")}]}],Notification:[{matcher:"idle_prompt",hooks:[{type:"command",command:l("notification-hook.js")}]}],PermissionDenied:[{matcher:"",hooks:[{type:"command",command:l("lifecycle-hook.js")}]}],Stop:[{hooks:[{type:"command",command:l("lifecycle-hook.js")}]}],StopFailure:[{hooks:[{type:"command",command:l("lifecycle-hook.js")}]}],PreCompact:[{hooks:[{type:"command",command:l("lifecycle-hook.js")}]}],PostCompact:[{hooks:[{type:"command",command:l("lifecycle-hook.js")}]}],ConfigChange:[{hooks:[{type:"command",command:l("lifecycle-hook.js")}]}]}},null,2)}
13
+ `,f=h(t,"hooks"),v=h(f,"hooks.json");await R(f,{recursive:!0});let P="";try{P=await C(v,"utf8")}catch{}return P!==p&&(await I(v,p,"utf8"),a.info("claude-adapter",`Wrote Claude hooks config: ${v}`)),t}async ensureStdioMcpServer(){const e=this.resolveProjectRoot(),t=this.resolveStdioServerPath(e);if(this.ensureStdioServerArtifact(e,t),!x(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=ht(this.claudeCliSessionId);await ft(r,n),this.claudeMcpConfigPath=r,this.mcpServerReady=!0,this.startActivityTracking()}resolveProjectRoot(){const e=Re(import.meta.url);return be(e,"..","..","..","..")}resolveStdioServerPath(e=this.resolveProjectRoot()){return h(e,"dist","mcp","stdio","server.js")}ensureStdioServerArtifact(e,t){if(x(t)||fe)return;fe=!0;const i=h(e,"node_modules","typescript","bin","tsc"),s=h(e,"tsconfig.json");if(!x(i)||!x(s))return;a.warn("claude-adapter",`MCP stdio server artifact missing, attempting build: ${t}`);const n=Se(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=G.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>=q)return;await new Promise(r=>setTimeout(r,q-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
+ `;"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(),c=Date.now()-o;if(c>=q)return;await new Promise(d=>setTimeout(d,q-c));return}const n=Date.now()-t,r=this.claudePty?rt: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=G.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(){tt(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=S(),r=new O(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&&D(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&&D(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}async handleInternalInvoke(e,t,i){if(e==="event_tool_call"){const s=String(t.tool_name??""),n=je(s,{...t.arguments??{}}),r=this.getEventToolHandle();if(this.activeEvent){const c=this.activeEvent;String(n.event_id??"").trim()===""&&(n.event_id=c.eventId),String(n.session_id??"").trim()===""&&(n.session_id=c.sessionId)}else if(this.lastClearedEvent&&s==="grix_reply"){const c=this.lastClearedEvent;Date.now()-c.ts<Ye&&(String(n.session_id??"").trim()===""&&(n.session_id=c.sessionId),n.event_id="",a.info("claude-adapter",`Late grix_reply fallback: sending direct message for cleared event ${c.eventId} (cleared ${(Date.now()-c.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=Me(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(De(s)){const c=this.activeEvent;c&&(c.toolCallInFlight=!0);try{if(i?.aborted)throw new Error("invoke aborted by timeout");const u=await xe(r,s,n);if(u.isError)throw new Error(u.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(u.content[0]?.text??"null")}finally{if(c&&(c.toolCallInFlight=!1,c.pendingStopHook&&this.activeEvent===c)){const u=c.pendingStopHook;c.pendingStopHook=void 0,this.finalizeActiveEvent(u)}}}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.lastReplyTextLen=n.length),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=Le[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 ie(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 ie(s)).isFile()&&(await te(s),a.info("claude-adapter",`Removed conflicting .mcp.json from pluginDir: ${s}`))}catch{}}async ensureWorkspaceTrust(e){const t=h(T(),".claude.json");try{const i=await C(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 I(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(T(),".claude","settings.json");try{let t={};try{t=JSON.parse(await C(e,"utf8"))}catch{}if(t.skipDangerousModePermissionPrompt===!0)return;t.skipDangerousModePermissionPrompt=!0,await R(h(T(),".claude"),{recursive:!0}),await I(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 C(h(T(),".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(T(),".claude.json");try{let i;try{const r=await C(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 I(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 R(s,{recursive:!0});let r={};try{r=JSON.parse(await C(n,"utf8"))}catch{}const o={type:"command",command:`node ${i}`,refreshInterval:10};r.statusLine=o;const c=`${JSON.stringify(r,null,2)}
17
- `;await I(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(T(),".claude.json");let c=null;try{const f=await C(o,"utf8");c=JSON.parse(f)?.mcpServers?.[x]??null}catch{}if(c&&String(c.type||"stdio").trim()==="stdio"&&String(c.command??"").trim()===n&&Array.isArray(c.args)&&c.args.length===r.length&&c.args.every((f,v)=>f===r[v]))return;a.info("claude-adapter",`Registering user-scoped MCP server: ${x} -> ${n} ${r.join(" ")}`);try{j(`${e} mcp remove -s user ${x}`,{encoding:"utf8",timeout:1e4,env:i,stdio:"pipe"})}catch{}const u=["mcp","add","--scope","user",x,"--",n,...r],l=process.platform==="win32"?'"':"'",p=j(`${e} ${u.map(f=>`${l}${f}${l}`).join(" ")}`,{encoding:"utf8",timeout:1e4,env:i,stdio:"pipe"});a.info("claude-adapter",`MCP server registered: ${p.trim()||"ok"}`)}resolveServerEntryPath(e){const t=h(e,"server","main.js");try{if(J(t))return t}catch{}const i=h(e,"dist","index.js");try{if(J(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.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(),this.tryDeliverQueuedEvent()}startComposing(e,t){}stopComposing(){}clearComposingTimer(){}resolveHookSignalsPath(){const e=S();return h(e.dataDir,`hook-signals-${this.sessionId}.json`)}startActivityTracking(){this.activityManager&&this.activityManager.stop();const e=this.resolveHookSignalsPath(),t=S().hookSignalsLogPath,i=new Te(e,t);a.info("claude-adapter",`Activity tracking started: watching ${e}`),this.activityManager=new $e({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){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=ae(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}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=re(this.claudeCliSessionId,this.claudeSessionCwd,this.activeEvent.jsonlBaseOffset);if(!n||n.stopReason!=="end_turn"){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}}const s=!this.activeEvent.replied&&!this.activeEvent.apiFormatError&&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 \u9047\u5230 API \u683C\u5F0F\u9519\u8BEF\uFF0C\u8BF7\u91CD\u65B0\u5F00\u59CB\u5BF9\u8BDD\uFF08/grix open \u76EE\u5F55\uFF09\u3002":"Claude \u9000\u51FA\u4F46\u672A\u5B8C\u6210\u56DE\u590D\uFF0C\u8BF7\u91CD\u8BD5\u3002";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)),!!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?Ne(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 c=r||this.lastPreToolInput;if(this.lastPreToolInput="",!o){const u=s==="PostToolUseFailure"?`(failed) ${c}`:c;this.bridgeCallbacks.sendToolResult(i.eventId,i.sessionId,n,u)}}}}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=S();new O(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 c=o.request_id;this.pendingPermissions.set(c,{eventId:i.eventId,sessionId:i.sessionId});const u=typeof t=="string"?t:"";if(e==="ExitPlanMode"){const f=Qe(u);f&&this.bridgeCallbacks.sendReply(i.eventId,i.sessionId,f)}const l=e==="ExitPlanMode"?"":u.slice(0,100),p=l?`${e}: ${l}`:e;this.bridgeCallbacks.sendPermissionCard({eventId:i.eventId,sessionId:i.sessionId,approvalId:c,toolName:e,toolTitle:p}),a.info("claude-adapter",`Sent permission card: approvalId=${c} 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=S(),n=new ne(s.questionRequestsDir);n.listPending().then(async r=>{const o=[...r].reverse().filter(l=>String(l.session_id??"").trim()===this.claudeCliSessionId),c=o.find(l=>String(l.event_id??"").trim()===e.eventId)??o.find(l=>String(l.event_id??"").trim()==="")??o[0]??null,u=c?.request_id??`fallback-${Date.now()}`;c&&String(c.event_id??"").trim()===""&&await n.updateRequest(c.request_id,{event_id:e.eventId}),this.pendingQuestion.set(u,{eventId:e.eventId,sessionId:e.sessionId}),e.awaitingUserQuestion=!0,this.bridgeCallbacks.sendAgentQuestionCard(e.eventId,e.sessionId,{request_id:u,mode:"form",questions:i}),a.info("claude-adapter",`Sent agent_question card: request_id=${u} 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}`),c=String(r.prompt??""),u=Array.isArray(r.options)?r.options:void 0,l=r.multiSelect===!0;i.push({header:o,prompt:c,...u&&u.length>0?{options:u}:{},...l?{multi_select:!0}:{}})}return i.length>0?{questions:i}:null}pruneCompletedEvents(){const e=Date.now()-ze;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)}resetActiveEventIdleTimer(e){this.clearActiveEventIdleTimer(),this.activeEventIdleTimer=setTimeout(()=>{if(this.stopped||this.activeEvent?.eventId!==e)return;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}const t=this.activeEvent?.replied?"responded":"failed",i=this.activeEvent?.replied?void 0:`agent idle for ${z/1e3}s`,s=this.activeEvent?.replied?void 0:"agent_idle_timeout";a.error("claude-adapter",`Active event idle timeout (${z/1e3}s): ${e}, replied=${!!this.activeEvent?.replied}, sending ${t}`),this.completedEventIds.set(e,Date.now()),this.bridgeCallbacks.sendEventResult(e,t,i,s),this.clearActiveEvent(),t==="failed"&&this.emit("stuck")},z)}resetActiveEventHardTimer(e){this.clearActiveEventHardTimer(),this.activeEventHardTimer=setTimeout(()=>{if(this.stopped||this.activeEvent?.eventId!==e)return;const t=this.activeEvent?.replied?"responded":"failed",i=this.activeEvent?.replied?void 0:`agent exceeded max duration ${W/1e3}s`,s=this.activeEvent?.replied?void 0:"agent_hard_timeout";a.error("claude-adapter",`Active event hard timeout (${W/1e3}s): ${e}, replied=${!!this.activeEvent?.replied}, sending ${t}`),this.completedEventIds.set(e,Date.now()),this.bridgeCallbacks.sendEventResult(e,t,i,s),this.clearActiveEvent(),t==="failed"&&this.emit("stuck")},W)}clearActiveEventPostReplyTimer(){this.activeEventPostReplyTimer&&(clearTimeout(this.activeEventPostReplyTimer),this.activeEventPostReplyTimer=null)}clearActiveEventPostReplyWatcher(){if(this.activeEventPostReplyWatcher){try{this.activeEventPostReplyWatcher.close()}catch{}this.activeEventPostReplyWatcher=null}}startPostReplyJsonlWatcher(e){if(this.clearActiveEventPostReplyWatcher(),!this.claudeCliSessionId||!this.claudeSessionCwd)return;const t=oe(this.claudeCliSessionId,this.claudeSessionCwd),i=this.activeEvent?.sessionId;if(!i)return;if(!L(t)){a.info("claude-adapter",`JSONL watcher skipped: file not yet available for ${e}`);return}const s=M(t).size;this.activeEvent&&(this.activeEvent.jsonlBaseOffset=s);let n=null;const r=()=>{if(this.stopped||this.activeEvent?.eventId!==e)return;const o=this.activeEvent.jsonlBaseOffset,c=re(this.claudeCliSessionId,this.claudeSessionCwd,o);if(!c||c.stopReason!=="end_turn")return;const u=this.activeEvent.lastReplyTextLen??0;if(c.text.length<=u){a.info("claude-adapter",`JSONL watcher: end_turn for ${e}, text covered (len=${c.text.length} lastReply=${u})`);return}a.info("claude-adapter",`JSONL watcher: end_turn rescue ${c.text.length} chars (lastReply=${u}) for ${e}`);const l=`rescue_jsonl_${e}_${Date.now()}`;this.bridgeCallbacks.sendStreamChunk(e,i,c.text,1,!1,l),this.bridgeCallbacks.sendStreamChunk(e,i,"",2,!0,l),this.bridgeCallbacks.sendEventResult(e,"responded",void 0,void 0),this.clearActiveEvent()};try{this.activeEventPostReplyWatcher=ke(t,()=>{n&&clearTimeout(n),n=setTimeout(r,300)}),a.info("claude-adapter",`JSONL watcher started for ${e} at ${t} (baseOffset=${s})`)}catch(o){a.warn("claude-adapter",`JSONL watcher start failed for ${e}: ${o}`)}}startPostReplyDeadline(e){this.clearActiveEventPostReplyTimer(),this.clearActiveEventHardTimer(),this.activeEventPostReplyTimer=setTimeout(()=>{if(this.stopped||this.activeEvent?.eventId!==e||!this.activeEvent?.replied)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`);const s=this.activeEvent.sessionId,n=this.activeEvent.lastReplyTextLen??0;if(this.claudeCliSessionId&&this.claudeSessionCwd){const r=this.activeEvent.jsonlBaseOffset,o=ae(this.claudeCliSessionId,this.claudeSessionCwd,r);if(o&&o.length>n){const c=`rescue_deadline_${e}_${Date.now()}`;this.bridgeCallbacks.sendStreamChunk(e,s,o,1,!1,c),this.bridgeCallbacks.sendStreamChunk(e,s,"",2,!0,c),a.info("claude-adapter",`Post-reply deadline rescue: sent ${o.length} chars from JSONL (lastReplyLen=${n})`)}}this.bridgeCallbacks.sendEventResult(e,"responded",void 0,void 0),this.clearActiveEvent()},We)}beginCompaction(){this.compacting||(this.compacting=!0,a.info("claude-adapter","Compaction started; gating input"),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}); draining ${this.queuedEvents.length} queued events`),this.tryDeliverQueuedEvent();const t=this.compactionDoneResolver;this.compactionDoneResolver=null,t?.(e)}tryDeliverQueuedEvent(){if(this.activeEvent||this.compacting||this.queuedEvents.length===0)return;const e=this.queuedEvents.shift();this.queuedEventIds.delete(e.event_id),this.deliverInboundEvent(e)}rejectQueuedEvents(e,t="failed"){if(this.queuedEvents.length===0)return;const i=this.queuedEvents.splice(0);this.queuedEventIds.clear();for(const s of i)this.bridgeCallbacks.sendEventResult(s.event_id,t,e)}armStopHookBarrier(e){this.stopHookBarrierSessionId=e,this.stopHookBarrierTimer=setTimeout(()=>{this.stopHookBarrierSessionId===e&&(a.warn("claude-adapter",`Stop hook barrier timeout for session=${e}`),this.clearStopHookBarrier(),this.tryDeliverQueuedEvent())},Ke)}clearStopHookBarrier(){this.stopHookBarrierSessionId=null,this.stopHookBarrierTimer&&(clearTimeout(this.stopHookBarrierTimer),this.stopHookBarrierTimer=null)}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,`
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(){ot(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=w(),r=new F(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&&L(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&&L(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}async handleInternalInvoke(e,t,i){if(e==="event_tool_call"){const s=String(t.tool_name??""),n=qe(s,{...t.arguments??{}}),r=this.getEventToolHandle();if(this.activeEvent){const c=this.activeEvent;String(n.event_id??"").trim()===""&&(n.event_id=c.eventId),String(n.session_id??"").trim()===""&&(n.session_id=c.sessionId)}else if(this.lastClearedEvent&&s==="grix_reply"){const c=this.lastClearedEvent;Date.now()-c.ts<it&&(String(n.session_id??"").trim()===""&&(n.session_id=c.sessionId),n.event_id="",a.info("claude-adapter",`Late grix_reply fallback: sending direct message for cleared event ${c.eventId} (cleared ${(Date.now()-c.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=Oe(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(Ne(s)){const c=this.activeEvent;c&&(c.toolCallInFlight=!0);try{if(i?.aborted)throw new Error("invoke aborted by timeout");const d=await Le(r,s,n);if(d.isError)throw new Error(d.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(d.content[0]?.text??"null")}finally{if(c&&(c.toolCallInFlight=!1,c.pendingStopHook&&this.activeEvent===c)){const d=c.pendingStopHook;c.pendingStopHook=void 0,this.finalizeActiveEvent(d)}}}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.lastReplyTextLen=n.length),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=je[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(T(),".claude.json");try{const i=await C(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 I(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(T(),".claude","settings.json");try{let t={};try{t=JSON.parse(await C(e,"utf8"))}catch{}if(t.skipDangerousModePermissionPrompt===!0)return;t.skipDangerousModePermissionPrompt=!0,await R(h(T(),".claude"),{recursive:!0}),await I(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 C(h(T(),".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(T(),".claude.json");try{let i;try{const r=await C(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 I(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 R(s,{recursive:!0});let r={};try{r=JSON.parse(await C(n,"utf8"))}catch{}const c={type:"command",command:`node "${i.replace(/\\/g,"/")}"`,refreshInterval:10};r.statusLine=c;const d=`${JSON.stringify(r,null,2)}
17
+ `;await I(n,d,"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(T(),".claude.json");let c=null;try{const f=await C(o,"utf8");c=JSON.parse(f)?.mcpServers?.[M]??null}catch{}if(c&&String(c.type||"stdio").trim()==="stdio"&&String(c.command??"").trim()===n&&Array.isArray(c.args)&&c.args.length===r.length&&c.args.every((f,v)=>f===r[v]))return;a.info("claude-adapter",`Registering user-scoped MCP server: ${M} -> ${n} ${r.join(" ")}`);try{O(`${e} mcp remove -s user ${M}`,{encoding:"utf8",timeout:1e4,env:i,stdio:"pipe"})}catch{}const d=["mcp","add","--scope","user",M,"--",n,...r],l=process.platform==="win32"?'"':"'",p=O(`${e} ${d.map(f=>`${l}${f}${l}`).join(" ")}`,{encoding:"utf8",timeout:1e4,env:i,stdio:"pipe"});a.info("claude-adapter",`MCP server registered: ${p.trim()||"ok"}`)}resolveServerEntryPath(e){const t=h(e,"server","main.js");try{if(J(t))return t}catch{}const i=h(e,"dist","index.js");try{if(J(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.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(),this.tryDeliverQueuedEvent()}startComposing(e,t){}stopComposing(){}clearComposingTimer(){}resolveHookSignalsPath(){const e=w();return h(e.dataDir,`hook-signals-${this.sessionId}.json`)}startActivityTracking(){this.activityManager&&this.activityManager.stop();const e=this.resolveHookSignalsPath(),t=w().hookSignalsLogPath,i=new xe(e,t);a.info("claude-adapter",`Activity tracking started: watching ${e}`),this.activityManager=new De({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){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=ce(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}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=oe(this.claudeCliSessionId,this.claudeSessionCwd,this.activeEvent.jsonlBaseOffset);if(!n||n.stopReason!=="end_turn"){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}}const s=!this.activeEvent.replied&&!this.activeEvent.apiFormatError&&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 \u9047\u5230 API \u683C\u5F0F\u9519\u8BEF\uFF0C\u8BF7\u91CD\u65B0\u5F00\u59CB\u5BF9\u8BDD\uFF08/grix open \u76EE\u5F55\uFF09\u3002":"Claude \u9000\u51FA\u4F46\u672A\u5B8C\u6210\u56DE\u590D\uFF0C\u8BF7\u91CD\u8BD5\u3002";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?Fe(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 c=r||this.lastPreToolInput;if(this.lastPreToolInput="",!o){const d=s==="PostToolUseFailure"?`(failed) ${c}`:c;this.bridgeCallbacks.sendToolResult(i.eventId,i.sessionId,n,d)}}}}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=w();new F(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 c=o.request_id;this.pendingPermissions.set(c,{eventId:i.eventId,sessionId:i.sessionId});const d=typeof t=="string"?t:"";if(e==="ExitPlanMode"){const f=Ge(d);f&&this.bridgeCallbacks.sendReply(i.eventId,i.sessionId,f)}const l=e==="ExitPlanMode"?"":d.slice(0,100),p=l?`${e}: ${l}`:e;this.bridgeCallbacks.sendPermissionCard({eventId:i.eventId,sessionId:i.sessionId,approvalId:c,toolName:e,toolTitle:p}),a.info("claude-adapter",`Sent permission card: approvalId=${c} tool=${e}`)}).catch(r=>{a.warn("claude-adapter",`Failed to send permission card: ${r}`)})}handleAskUserQuestion(e,t){const i=de(t);if(!i){a.warn("claude-adapter","Failed to parse AskUserQuestion input, skipping agent_question card");return}const s=w(),n=new ae(s.questionRequestsDir);n.listPending().then(async r=>{const o=[...r].reverse().filter(l=>String(l.session_id??"").trim()===this.claudeCliSessionId),c=o.find(l=>String(l.event_id??"").trim()===e.eventId)??o.find(l=>String(l.event_id??"").trim()==="")??o[0]??null,d=c?.request_id??`fallback-${Date.now()}`;c&&String(c.event_id??"").trim()===""&&await n.updateRequest(c.request_id,{event_id:e.eventId}),this.pendingQuestion.set(d,{eventId:e.eventId,sessionId:e.sessionId}),e.awaitingUserQuestion=!0,this.bridgeCallbacks.sendAgentQuestionCard(e.eventId,e.sessionId,{request_id:d,mode:"form",questions:i}),a.info("claude-adapter",`Sent agent_question card: request_id=${d} questions=${i.length}`)}).catch(()=>{a.warn("claude-adapter","Failed to list pending questions from store")})}parseAskUserQuestions(e){return de(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}`),c=String(r.prompt??""),d=Array.isArray(r.options)?r.options:void 0,l=r.multiSelect===!0;i.push({header:o,prompt:c,...d&&d.length>0?{options:d}:{},...l?{multi_select:!0}:{}})}return i.length>0?{questions:i}:null}pruneCompletedEvents(){const e=Date.now()-Ye;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;const i=le(this.claudeCliSessionId,this.claudeSessionCwd,t.jsonlBaseOffset);if(i.freshMs===null||!(t.jsonlBaseOffset===void 0?i.freshMs<Y:i.lastStopReason!==null?i.lastStopReason!=="end_turn":i.freshMs<Y))return!1;if(i.freshMs<Y)t.livenessExtendStartAt=Date.now();else{const n=t.livenessExtendStartAt??Date.now();if(t.livenessExtendStartAt===void 0&&(t.livenessExtendStartAt=n),Date.now()-n>he)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=z(this.claudeCliSessionId,this.claudeSessionCwd);t.jsonlBaseOffset=x(i)?D(i).size:0}catch{t.jsonlBaseOffset=0}}noteSelfDrivenActivity(){!this.selfDrivenActive&&this.lastClearedEvent&&Date.now()-this.lastClearedEvent.ts<et||(this.selfDrivenLastSignalAt=Date.now(),!this.selfDrivenActive&&(this.selfDrivenActive=!0,a.info("claude-adapter",`Self-driven activity detected for session ${this.sessionId} (no active event), reporting working state`),this.emit("sessionActivity",this.sessionId??"",!0),this.selfDrivenSweepTimer=setInterval(()=>this.sweepSelfDriven(),Ze),this.selfDrivenSweepTimer.unref()))}sweepSelfDriven(){if(!this.selfDrivenActive)return;if(this.stopped||this.activeEvent){this.stopSelfDriven();return}const e=Date.now()-this.selfDrivenLastSignalAt;if(e<Ke){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<he;if(i&&s){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.selfDrivenActive&&(this.selfDrivenActive=!1,this.emit("sessionActivity",this.sessionId??"",!1))}resetActiveEventIdleTimer(e){this.clearActiveEventIdleTimer(),this.activeEventIdleTimer=setTimeout(()=>{if(this.stopped||this.activeEvent?.eventId!==e)return;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}const t=this.activeEvent?.replied?"responded":"failed",i=this.activeEvent?.replied?void 0:`agent idle for ${W/1e3}s`,s=this.activeEvent?.replied?void 0:"agent_idle_timeout";a.error("claude-adapter",`Active event idle timeout (${W/1e3}s): ${e}, replied=${!!this.activeEvent?.replied}, sending ${t}`),this.completedEventIds.set(e,Date.now()),this.bridgeCallbacks.sendEventResult(e,t,i,s),this.clearActiveEvent(),t==="failed"&&this.emit("stuck")},W)}resetActiveEventHardTimer(e){this.clearActiveEventHardTimer(),this.activeEventHardTimer=setTimeout(()=>{if(this.stopped||this.activeEvent?.eventId!==e)return;if(this.shouldExtendByLiveness(e)){this.resetActiveEventHardTimer(e);return}const t=this.activeEvent?.replied?"responded":"failed",i=this.activeEvent?.replied?void 0:`agent exceeded max duration ${V/1e3}s`,s=this.activeEvent?.replied?void 0:"agent_hard_timeout";a.error("claude-adapter",`Active event hard timeout (${V/1e3}s): ${e}, replied=${!!this.activeEvent?.replied}, sending ${t}`),this.completedEventIds.set(e,Date.now()),this.bridgeCallbacks.sendEventResult(e,t,i,s),this.clearActiveEvent(),t==="failed"&&this.emit("stuck")},V)}clearActiveEventPostReplyTimer(){this.activeEventPostReplyTimer&&(clearTimeout(this.activeEventPostReplyTimer),this.activeEventPostReplyTimer=null)}clearActiveEventPostReplyWatcher(){if(this.activeEventPostReplyWatcher){try{this.activeEventPostReplyWatcher.close()}catch{}this.activeEventPostReplyWatcher=null}}startPostReplyJsonlWatcher(e){if(this.clearActiveEventPostReplyWatcher(),!this.claudeCliSessionId||!this.claudeSessionCwd)return;const t=z(this.claudeCliSessionId,this.claudeSessionCwd),i=this.activeEvent?.sessionId;if(!i)return;if(!x(t)){a.info("claude-adapter",`JSONL watcher skipped: file not yet available for ${e}`);return}const s=D(t).size;this.activeEvent&&(this.activeEvent.jsonlBaseOffset=s);let n=null;const r=()=>{if(this.stopped||this.activeEvent?.eventId!==e)return;const o=this.activeEvent.jsonlBaseOffset,c=oe(this.claudeCliSessionId,this.claudeSessionCwd,o);if(!c||c.stopReason!=="end_turn")return;const d=this.activeEvent.lastReplyTextLen??0;if(c.text.length<=d){a.info("claude-adapter",`JSONL watcher: end_turn for ${e}, text covered (len=${c.text.length} lastReply=${d})`);return}a.info("claude-adapter",`JSONL watcher: end_turn rescue ${c.text.length} chars (lastReply=${d}) for ${e}`);const l=`rescue_jsonl_${e}_${Date.now()}`;this.bridgeCallbacks.sendStreamChunk(e,i,c.text,1,!1,l),this.bridgeCallbacks.sendStreamChunk(e,i,"",2,!0,l),this.bridgeCallbacks.sendEventResult(e,"responded",void 0,void 0),this.clearActiveEvent()};try{this.activeEventPostReplyWatcher=$e(t,()=>{n&&clearTimeout(n),n=setTimeout(r,300)}),a.info("claude-adapter",`JSONL watcher started for ${e} at ${t} (baseOffset=${s})`)}catch(o){a.warn("claude-adapter",`JSONL watcher start failed for ${e}: ${o}`)}}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`);const s=this.activeEvent.sessionId,n=this.activeEvent.lastReplyTextLen??0;if(this.claudeCliSessionId&&this.claudeSessionCwd){const r=this.activeEvent.jsonlBaseOffset,o=ce(this.claudeCliSessionId,this.claudeSessionCwd,r);if(o&&o.length>n){const c=`rescue_deadline_${e}_${Date.now()}`;this.bridgeCallbacks.sendStreamChunk(e,s,o,1,!1,c),this.bridgeCallbacks.sendStreamChunk(e,s,"",2,!0,c),a.info("claude-adapter",`Post-reply deadline rescue: sent ${o.length} chars from JSONL (lastReplyLen=${n})`)}}this.bridgeCallbacks.sendEventResult(e,"responded",void 0,void 0),this.clearActiveEvent()},Xe)}beginCompaction(){this.compacting||(this.compacting=!0,a.info("claude-adapter","Compaction started; gating input"),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}); draining ${this.queuedEvents.length} queued events`),this.tryDeliverQueuedEvent();const t=this.compactionDoneResolver;this.compactionDoneResolver=null,t?.(e)}tryDeliverQueuedEvent(){if(this.activeEvent||this.compacting||this.queuedEvents.length===0)return;const e=this.queuedEvents.shift();this.queuedEventIds.delete(e.event_id),this.deliverInboundEvent(e)}rejectQueuedEvents(e,t="failed"){if(this.queuedEvents.length===0)return;const i=this.queuedEvents.splice(0);this.queuedEventIds.clear();for(const s of i)this.bridgeCallbacks.sendEventResult(s.event_id,t,e)}armStopHookBarrier(e){this.stopHookBarrierSessionId=e,this.stopHookBarrierTimer=setTimeout(()=>{this.stopHookBarrierSessionId===e&&(a.warn("claude-adapter",`Stop hook barrier timeout for session=${e}`),this.clearStopHookBarrier(),this.tryDeliverQueuedEvent())},st)}clearStopHookBarrier(){this.stopHookBarrierSessionId=null,this.stopHookBarrierTimer&&(clearTimeout(this.stopHookBarrierTimer),this.stopHookBarrierTimer=null)}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
- 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 it extends se{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 _(d){if(d==null)return 0;const e=Number(d);return Number.isFinite(e)?e:0}function U(d){return String(d).replace(/\\/g,"\\\\").replace(/\{/g,"\\{").replace(/\}/g,"\\}")}async function st(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 claude_command [list {${U(e)}}${t.map(r=>` {${U(r)}}`).join("")}]`,"spawn -noecho {*}$claude_command",`set pid_file [open {${U(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 {}","}",`set cmd_fifo {${U(h(d,"cmd.fifo"))}}`,"file delete -force $cmd_fifo","exec mkfifo $cmd_fifo","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"'," }","}","set cmd_fd [open $cmd_fifo r]","fconfigure $cmd_fd -blocking 0 -buffering line","fileevent $cmd_fd readable handle_cmd","expect_background {"," -re .+ { }"," eof { set ::__done 1 }","}","set ::__done 0","vwait ::__done",""];return await I(s,"","utf8"),await I(i,n.join(`
20
- `),"utf8"),{expectPath:i,pidPath:s}}async function nt(d,e=50,t=100){for(let i=0;i<e;i++){try{const s=await C(d,"utf8"),n=parseInt(String(s).trim(),10);if(Number.isFinite(n)&&n>0)return n}catch{}await new Promise(s=>setTimeout(s,t))}return 0}function rt(d,e){return new Promise((t,i)=>{let s=!1;const n=o=>{s||(s=!0,d.off("error",r),o())},r=o=>n(()=>i(o));d.once("error",r),nt(e).then(o=>n(()=>t(o))).catch(o=>n(()=>i(o)))})}function fe(d){return d.replace(/[/\\]/g,"-")}function at(d){const e=S();return h(e.dataDir,"claude-mcp-configs",`${fe(d)}.json`)}function ot(d){const e=S();return h(e.dataDir,"claude-system-prompts",`${fe(d)}.txt`)}async function ct(d,e){await R(h(S().dataDir,"claude-mcp-configs"),{recursive:!0});const t={mcpServers:{[x]:{type:"stdio",command:process.execPath,args:e}}},i=`${JSON.stringify(t,null,2)}
21
- `;let s="";try{s=await C(d,"utf8")}catch{}s!==i&&(await I(d,i,"utf8"),a.info("claude-adapter",`Wrote MCP config: ${d}`))}async function lt(d,e){await R(h(S().dataDir,"claude-system-prompts"),{recursive:!0});const t=`${e}
22
- `;let i="";try{i=await C(d,"utf8")}catch{}i!==t&&await I(d,t,"utf8")}function X(d,e){if(process.platform==="win32")return;let t=!1;try{const i=j(`ps ax -o pid,command | grep -E -- '--(session-id|resume) ${d}' | grep -v grep`,{encoding:"utf8",timeout:5e3}).trim();if(i)for(const s of i.split(`
23
- `)){const n=parseInt(s.trim(),10);if(n>0&&n!==process.pid&&!e.includes(n)){a.info("claude-adapter",`Killing stale Claude process PID=${n} holding session ${d}`);try{process.kill(n,"SIGTERM"),t=!0}catch{}}}}catch{}t||H(d)}function H(d){const e=h(T(),".claude"),t=[h(e,"session-env",d)];try{const i=h(e,"sessions");for(const s of ye(i))if(s.endsWith(".json"))try{const n=h(i,s),r=JSON.parse(J(n,"utf8"));r&&r.sessionId===d&&t.push(n)}catch{}}catch{}for(const i of t)try{M(i),we(i,{recursive:!0,force:!0}),a.info("claude-adapter",`Removed stale session file: ${i}`)}catch{}}function dt(d,e){if(!e)return!1;const t=h(T(),".claude"),i=ut(e),s=h(t,"projects",i,`${d}.jsonl`);try{return M(s),!0}catch{return!1}}function ut(d){const e=d.replace(/\\/g,"/").replace(/:\/\//g,"/");return/^[a-zA-Z]:\//.test(e)||e.startsWith("//")?e.toLowerCase().replace(/:/g,"-").replace(/^\//,"").replace(/\//g,"-"):e.replace(/:/g,"-").replace(/\//g,"-")}export{me as ClaudeAdapter,ut as deriveClaudeProjectDirName,ce as parseAskUserQuestionPayload};
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 ct 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 E(u){if(u==null)return 0;const e=Number(u);return Number.isFinite(e)?e:0}function H(u){return String(u).replace(/\\/g,"\\\\").replace(/\{/g,"\\{").replace(/\}/g,"\\}")}async function lt(u,e,t){const i=h(u,"claude.expect"),s=h(u,"claude.pid"),n=["log_user 1","set timeout -1","set startup_prompt_armed 1",`set cmd_fifo {${H(h(u,"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 {${H(e)}}${t.map(r=>` {${H(r)}}`).join("")}]`,"spawn -noecho {*}$claude_command",`set pid_file [open {${H(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 I(s,"","utf8"),await I(i,n.join(`
20
+ `),"utf8"),{expectPath:i,pidPath:s}}async function dt(u,e=50,t=100){for(let i=0;i<e;i++){try{const s=await C(u,"utf8"),n=parseInt(String(s).trim(),10);if(Number.isFinite(n)&&n>0)return n}catch{}await new Promise(s=>setTimeout(s,t))}return 0}function ut(u,e){return new Promise((t,i)=>{let s=!1;const n=o=>{s||(s=!0,u.off("error",r),o())},r=o=>n(()=>i(o));u.once("error",r),dt(e).then(o=>n(()=>t(o))).catch(o=>n(()=>i(o)))})}function ge(u){return u.replace(/[/\\]/g,"-")}function ht(u){const e=w();return h(e.dataDir,"claude-mcp-configs",`${ge(u)}.json`)}function pt(u){const e=w();return h(e.dataDir,"claude-system-prompts",`${ge(u)}.txt`)}async function ft(u,e){await R(h(w().dataDir,"claude-mcp-configs"),{recursive:!0});const t={mcpServers:{[M]:{type:"stdio",command:process.execPath,args:e}}},i=`${JSON.stringify(t,null,2)}
21
+ `;let s="";try{s=await C(u,"utf8")}catch{}s!==i&&(await I(u,i,"utf8"),a.info("claude-adapter",`Wrote MCP config: ${u}`))}async function mt(u,e){await R(h(w().dataDir,"claude-system-prompts"),{recursive:!0});const t=`${e}
22
+ `;let i="";try{i=await C(u,"utf8")}catch{}i!==t&&await I(u,t,"utf8")}function ee(u,e){if(process.platform==="win32")return;let t=!1;try{const i=O(`ps ax -o pid,command | grep -E -- '--(session-id|resume) ${u}' | grep -v grep`,{encoding:"utf8",timeout:5e3}).trim();if(i)for(const s of i.split(`
23
+ `)){const n=parseInt(s.trim(),10);if(n>0&&n!==process.pid&&!e.includes(n)){a.info("claude-adapter",`Killing stale Claude process PID=${n} holding session ${u}`);try{process.kill(n,"SIGTERM"),t=!0}catch{}}}}catch{}t||U(u)}function U(u){const e=h(T(),".claude"),t=[h(e,"session-env",u)];try{const i=h(e,"sessions");for(const s of Ce(i))if(s.endsWith(".json"))try{const n=h(i,s),r=JSON.parse(J(n,"utf8"));r&&r.sessionId===u&&t.push(n)}catch{}}catch{}for(const i of t)try{D(i),Pe(i,{recursive:!0,force:!0}),a.info("claude-adapter",`Removed stale session file: ${i}`)}catch{}}function vt(u,e){if(!e)return!1;const t=h(T(),".claude"),i=gt(e),s=h(t,"projects",i,`${u}.jsonl`);try{return D(s),!0}catch{return!1}}function gt(u){const e=u.replace(/\\/g,"/").replace(/:\/\//g,"/");return/^[a-zA-Z]:\//.test(e)||e.startsWith("//")?e.toLowerCase().replace(/:/g,"-").replace(/^\//,"").replace(/\//g,"-"):e.replace(/:/g,"-").replace(/\//g,"-")}export{Ee as ClaudeAdapter,gt as deriveClaudeProjectDirName,de as parseAskUserQuestionPayload};
@@ -0,0 +1 @@
1
+ import c from"node:http";import{randomUUID as d}from"node:crypto";import{log as o}from"../../core/log/index.js";function l(t){t.writeHead(401,{"content-type":"application/json"}),t.end(JSON.stringify({error:"unauthorized"}))}function u(t){t.writeHead(404,{"content-type":"application/json"}),t.end(JSON.stringify({error:"not_found"}))}function h(t,e){t.writeHead(400,{"content-type":"application/json"}),t.end(JSON.stringify({error:e}))}function p(t,e={ok:!0}){t.writeHead(200,{"content-type":"application/json"}),t.end(JSON.stringify(e))}async function v(t){const e=[];for await(const n of t)e.push(n);const r=Buffer.concat(e).toString("utf8").trim();return r?JSON.parse(r):{}}function k(t){const e=(t.headers.authorization??"").trim();return e.toLowerCase().startsWith("bearer ")?e.slice(7).trim():""}class w{host="127.0.0.1";port=0;token;callbacks;server=null;address=null;constructor(e){this.token=d(),this.callbacks=e}getToken(){return this.token}getURL(){return this.address?`http://${this.address.address}:${this.address.port}`:""}async start(){this.server||(this.server=c.createServer(async(e,r)=>{try{await this.handleRequest(e,r)}catch(n){h(r,n instanceof Error?n.message:String(n))}}),await new Promise((e,r)=>{this.server.once("error",r),this.server.listen(this.port,this.host,()=>{this.server.off("error",r),e()})}),this.address=this.server.address(),o.info("claude-bridge",`Bridge server listening on ${this.getURL()}`))}async stop(){if(!this.server)return;const e=this.server;this.server=null,this.address=null,e.closeIdleConnections?.(),e.closeAllConnections?.(),await new Promise((r,n)=>{e.close(s=>s?n(s):r())})}async handleRequest(e,r){if(k(e)!==this.token){l(r);return}if(e.method!=="POST"){r.writeHead(405,{"content-type":"application/json"}),r.end(JSON.stringify({error:"method_not_allowed"}));return}const n=new URL(e.url,"http://localhost").pathname,s=await v(e),i=f.get(n);if(!i){u(r);return}const a=await i(this.callbacks,s);p(r,a??{ok:!0})}}const f=new Map([["/v1/worker/register",async(t,e)=>(o.info("claude-bridge",`Worker registered: ${e.worker_id} (pid=${e.pid})`),t.onRegisterWorker(e))],["/v1/worker/status",async(t,e)=>(o.info("claude-bridge",`Worker status: ${e.status}`),t.onStatusUpdate(e))],["/v1/worker/send-text",async(t,e)=>t.onSendText(e)],["/v1/worker/send-stream-chunk",async(t,e)=>t.onSendStreamChunk(e)],["/v1/worker/send-media",async(t,e)=>t.onSendMedia(e)],["/v1/worker/delete-message",async(t,e)=>t.onDeleteMessage(e)],["/v1/worker/ack-event",async(t,e)=>t.onAckEvent(e)],["/v1/worker/event-result",async(t,e)=>t.onSendEventResult(e)],["/v1/worker/event-stop-ack",async(t,e)=>t.onSendEventStopAck(e)],["/v1/worker/event-stop-result",async(t,e)=>t.onSendEventStopResult(e)],["/v1/worker/session-composing",async(t,e)=>t.onSetSessionComposing(e)],["/v1/worker/agent-invoke",async(t,e)=>t.onAgentInvoke(e)],["/v1/worker/local-action-result",async(t,e)=>t.onLocalActionResult(e)]]);export{w as ClaudeBridgeServer};