@quantiya/codevibe-codex-plugin 1.0.38 → 1.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server.js +14 -13
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -1,29 +1,30 @@
|
|
|
1
|
-
"use strict";var xe=Object.create;var O=Object.defineProperty;var Te=Object.getOwnPropertyDescriptor;var Ce=Object.getOwnPropertyNames;var Re=Object.getPrototypeOf,Ae=Object.prototype.hasOwnProperty;var Oe=(l,e)=>{for(var t in e)O(l,t,{get:e[t],enumerable:!0})},J=(l,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of Ce(e))!Ae.call(l,s)&&s!==t&&O(l,s,{get:()=>e[s],enumerable:!(i=Te(e,s))||i.enumerable});return l};var
|
|
2
|
-
`);
|
|
3
|
-
`)
|
|
4
|
-
|
|
1
|
+
"use strict";var xe=Object.create;var O=Object.defineProperty;var Te=Object.getOwnPropertyDescriptor;var Ce=Object.getOwnPropertyNames;var Re=Object.getPrototypeOf,Ae=Object.prototype.hasOwnProperty;var Oe=(l,e)=>{for(var t in e)O(l,t,{get:e[t],enumerable:!0})},J=(l,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of Ce(e))!Ae.call(l,s)&&s!==t&&O(l,s,{get:()=>e[s],enumerable:!(i=Te(e,s))||i.enumerable});return l};var g=(l,e,t)=>(t=l!=null?xe(Re(l)):{},J(e||!l||!l.__esModule?O(t,"default",{value:l,enumerable:!0}):t,l)),Fe=l=>J(O({},"__esModule",{value:!0}),l);var Qe={};Oe(Qe,{CodexCompanionServer:()=>q});module.exports=Fe(Qe);var _e=require("uuid"),X=g(require("crypto")),x=g(require("fs")),C=g(require("path")),be=g(require("os")),Ie=require("util"),we=require("child_process"),d=require("@quantiya/codevibe-core");var Y=g(require("os")),Z=g(require("path")),ee=require("@quantiya/codevibe-core"),r=(0,ee.createLogger)({name:"codevibe-codex",logFile:Z.default.join(Y.default.tmpdir(),"codevibe-codex-mcp.log"),level:"debug"});var te=require("events"),f=g(require("fs")),k=g(require("path")),ie=require("chokidar"),se=require("@quantiya/codevibe-core");var F=class extends te.EventEmitter{constructor(){super();this.watcher=null;this.filePositions=new Map;this.activeLogFile=null;this.sessionId=null;this.isWatching=!1;this.startTime=0;this.sessionsDir=null;this.activeReads=new Map;this.pendingReads=new Set;this.readGeneration=0}start(){if(this.isWatching){r.warn("Session log watcher already running");return}let t=(0,se.getConfig)().codex.sessionsDir;this.sessionsDir=t,r.info("Starting Codex session log watcher",{sessionsDir:t}),f.existsSync(t)||(r.info("Codex sessions directory does not exist yet, creating...",{sessionsDir:t}),f.mkdirSync(t,{recursive:!0})),this.startTime=Date.now(),this.readGeneration++,this.watcher=(0,ie.watch)(t,{persistent:!0,ignoreInitial:!0,awaitWriteFinish:{stabilityThreshold:100,pollInterval:50},depth:4,ignored:i=>{let s=k.basename(i);return f.existsSync(i)&&f.statSync(i).isDirectory()?!1:!s.startsWith("rollout-")||!s.endsWith(".jsonl")}}),this.watcher.on("add",i=>{i.endsWith(".jsonl")&&this.onFileAdded(i)}),this.watcher.on("change",i=>{i.endsWith(".jsonl")&&this.onFileChanged(i)}),this.watcher.on("error",i=>{r.error("Watcher error:",i),this.emit("error",i)}),this.watcher.on("ready",()=>{r.info("Session log watcher ready"),this.bindRecentSessionFile()}),this.isWatching=!0,r.info("Session log watcher started")}stop(){this.watcher&&(this.watcher.close(),this.watcher=null),this.isWatching=!1,this.readGeneration++,this.filePositions.clear(),this.activeReads.clear(),this.pendingReads.clear(),this.activeLogFile=null,this.sessionId=null,this.sessionsDir=null,r.info("Session log watcher stopped")}getSessionId(){return this.sessionId}getActiveLogFile(){return this.activeLogFile}onFileAdded(t){try{let i=f.statSync(t),s=i.birthtimeMs||i.ctimeMs;if(s<this.startTime-5e3){r.debug("Ignoring old session file",{filePath:t,fileCreatedAt:new Date(s).toISOString(),startTime:new Date(this.startTime).toISOString()});return}}catch(i){r.warn("Could not check file creation time",{filePath:t,error:i})}if(this.activeLogFile){r.debug("Ignoring additional Codex session log for this server instance",{activeLogFile:this.activeLogFile,ignoredFile:t});return}r.info("New Codex session log detected",{filePath:t}),this.activeLogFile=t,this.filePositions.set(t,0),this.scheduleReadNewLines(t)}onFileChanged(t){if(this.activeLogFile&&t!==this.activeLogFile){r.debug("Ignoring change for non-active session log",{activeLogFile:this.activeLogFile,ignoredFile:t});return}if(!this.activeLogFile){let i=0;try{let s=f.statSync(t),o=this.getSessionAgeMs(t,s),n=s.mtimeMs;if(o<this.startTime-5e3&&n<this.startTime-5e3){r.debug("Ignoring change for pre-existing session file",{filePath:t,fileCreatedAt:new Date(o).toISOString(),fileModifiedAt:new Date(n).toISOString(),startTime:new Date(this.startTime).toISOString()});return}o<this.startTime-5e3&&(i=s.size,r.info("Binding resumed session file at current EOF (skipping historical content)",{filePath:t,fileCreatedAt:new Date(o).toISOString(),initialOffset:i}))}catch(s){r.warn("Could not check file creation time during change event",{filePath:t,error:s})}this.activeLogFile=t,this.filePositions.set(t,i)}this.scheduleReadNewLines(t)}getSessionAgeMs(t,i){try{let s=f.openSync(t,"r");try{let o=Buffer.alloc(1024),n=f.readSync(s,o,0,1024,0);if(n>0){let a=o.toString("utf8",0,n),p=a.indexOf(`
|
|
2
|
+
`),c=p===-1?a:a.slice(0,p);if(c.length>0)try{let u=JSON.parse(c);if(u.type==="session_meta"&&typeof u.timestamp=="string"){let m=Date.parse(u.timestamp);if(!Number.isNaN(m))return m}}catch{}}}finally{f.closeSync(s)}}catch{}return i.birthtimeMs||i.ctimeMs}bindRecentSessionFile(){if(!(this.activeLogFile||!this.sessionsDir))try{let i=this.collectRecentSessionFiles(this.sessionsDir).sort((o,n)=>n.modifiedAt-o.modifiedAt)[0];if(!i)return;let s=0;try{let o=f.statSync(i.filePath);this.getSessionAgeMs(i.filePath,o)<this.startTime-5e3&&(s=o.size)}catch(o){r.warn("Could not stat backfilled session file",{filePath:i.filePath,error:o})}r.info("Binding recent session file missed during initial watch scan",{filePath:i.filePath,fileCreatedAt:new Date(i.createdAt).toISOString(),fileModifiedAt:new Date(i.modifiedAt).toISOString(),watcherStartTime:new Date(this.startTime).toISOString(),initialOffset:s}),this.activeLogFile=i.filePath,this.filePositions.set(i.filePath,s),s===0&&this.scheduleReadNewLines(i.filePath)}catch(t){r.warn("Failed to backfill recent session file",{error:t})}}collectRecentSessionFiles(t){let i=[],s=[t];for(;s.length>0;){let o=s.pop();if(!o)continue;let n;try{n=f.readdirSync(o,{withFileTypes:!0})}catch{continue}for(let a of n){let p=k.join(o,a.name);if(a.isDirectory()){s.push(p);continue}if(!(!a.isFile()||!a.name.startsWith("rollout-")||!a.name.endsWith(".jsonl")))try{let c=f.statSync(p),u=c.birthtimeMs||c.ctimeMs,m=c.mtimeMs;(u>=this.startTime||m>=this.startTime-5e3)&&i.push({filePath:p,createdAt:u,modifiedAt:m})}catch{}}}return i}scheduleReadNewLines(t){let i=this.activeReads.get(t);if(i)return this.pendingReads.add(t),i;let s=this.readGeneration,o=this.drainReadQueue(t,s);return this.activeReads.set(t,o),o.finally(()=>{this.activeReads.get(t)===o&&(this.activeReads.delete(t),this.pendingReads.delete(t))}),o}async drainReadQueue(t,i){do this.pendingReads.delete(t),await this.readNewLines(t,i);while(i===this.readGeneration&&this.pendingReads.has(t))}async readNewLines(t,i){if(i!==this.readGeneration)return;let s=this.filePositions.get(t)||0;try{if(f.statSync(t).size<=s)return;let n=f.createReadStream(t,{start:s,encoding:"utf-8"}),a=s,p="";for await(let c of n){if(i!==this.readGeneration){n.destroy();return}p+=c;let u=p.indexOf(`
|
|
3
|
+
`);for(;u!==-1;){if(i!==this.readGeneration){n.destroy();return}let m=p.slice(0,u),v=p.slice(0,u+1);p=p.slice(u+1),a+=Buffer.byteLength(v,"utf-8");let P=m.endsWith("\r")?m.slice(0,-1):m;if(P.trim())try{let S=JSON.parse(P);this.processLogEntry(S)}catch(S){r.warn("Failed to parse log line",{filePath:t,line:P.substring(0,100),error:S})}if(i!==this.readGeneration){n.destroy();return}u=p.indexOf(`
|
|
4
|
+
`)}}i===this.readGeneration&&this.filePositions.set(t,a)}catch(o){r.error("Error reading log file",{filePath:t,error:o}),this.emit("error",o)}}processLogEntry(t){if(r.debug("Processing log entry",{type:t.type}),t.type==="session_meta"){let i=t.payload;this.sessionId=i.id,r.info("Codex session started",{sessionId:i.id,cwd:i.cwd,cliVersion:i.cli_version}),this.emit("session-started",i);return}this.emit("log-entry",t),t.type==="event_msg"&&t.payload?.type?this.emit(`event:${t.payload.type}`,t):t.type==="response_item"&&t.payload?.type&&this.emit(`response:${t.payload.type}`,t)}};var H=require("uuid"),_=require("@quantiya/codevibe-core");var b=new Map;function re(l,e){let t={sessionId:e,source:_.EventSource.DESKTOP};if(l.type==="event_msg"&&l.payload){let i=l.payload.type;switch(i){case"user_message":return{...t,type:_.EventType.USER_PROMPT,content:l.payload.message||"",metadata:{images:l.payload.images||[]}};case"agent_message":return{...t,type:_.EventType.ASSISTANT_RESPONSE,content:l.payload.message||""};case"agent_reasoning":return{...t,type:_.EventType.REASONING,content:l.payload.text||""};case"token_count":return r.debug("Skipping token_count entry"),null;default:return r.debug("Unknown event_msg type",{type:i}),null}}if(l.type==="response_item"&&l.payload){let i=l.payload.type;if(i==="function_call"){let{name:s,arguments:o,call_id:n}=l.payload,a={};try{a=JSON.parse(o||"{}")}catch{a={raw:o}}let p=(0,H.v4)();b.set(n,{name:s,input:o,eventId:p});let c=z(s),u=ke(s,a);return{...t,type:_.EventType.TOOL_USE,content:u,metadata:{toolName:c,toolInput:a,callId:n,status:"running"}}}if(i==="function_call_output"){let{call_id:s,output:o}=l.payload,n=b.get(s);b.delete(s);let a=n?.name?z(n.name):"Tool",p=De(o,500);return{...t,type:_.EventType.TOOL_USE,content:`${a} completed:
|
|
5
|
+
${p}`,metadata:{toolName:a,toolOutput:o,callId:s,status:"completed"}}}if(i==="custom_tool_call"){let{name:s,call_id:o,input:n,status:a}=l.payload;b.set(o,{name:s,input:n,eventId:(0,H.v4)()});let p=Ne(n),{oldString:c,newString:u}=Me(n),m=p?`Editing: ${p.filePath}`:"Applying patch";return{...t,type:_.EventType.TOOL_USE,content:m,metadata:{tool_name:"Edit",tool_input:{file_path:p?.filePath||"",old_string:c,new_string:u},callId:o,status:a||"running"}}}if(i==="custom_tool_call_output"){let{call_id:s,output:o}=l.payload,n=b.get(s);b.delete(s);let a={};try{a=JSON.parse(o||"{}")}catch{a={raw:o}}let p=a.output?.includes("Success")||!a.error;return{...t,type:_.EventType.TOOL_USE,content:p?"File edit applied successfully":`Edit failed: ${a.error||"Unknown error"}`,metadata:{toolName:"Edit",toolOutput:a,callId:s,status:"completed",success:p}}}return r.debug("Unknown response_item type",{type:i}),null}return l.type==="turn_context"?(r.debug("Skipping turn_context entry"),null):(r.debug("Unhandled log entry type",{type:l.type}),null)}function z(l){return{shell_command:"Bash",shell:"Bash",apply_patch:"Edit",write_file:"Write",read_file:"Read",list_files:"Glob",search_files:"Grep",web_search:"WebSearch",web_fetch:"WebFetch"}[l]||l}function ke(l,e){switch(l){case"shell_command":case"shell":return`Running: ${e.command||"command"}`;case"read_file":return`Reading: ${e.file_path||e.path||"file"}`;case"write_file":return`Writing: ${e.file_path||e.path||"file"}`;case"list_files":return`Listing: ${e.path||"."}`;case"search_files":return`Searching for: ${e.pattern||e.query||"pattern"}`;case"web_search":return`Searching web: ${e.query||"query"}`;default:return`Running ${z(l)}`}}function Me(l){let e=[],t=[],i=/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/;for(let s of l.split(`
|
|
5
6
|
`))if(!(i.test(s)||s.startsWith("***")||s.startsWith("---")||s.startsWith("+++"))){if(s.startsWith("-"))e.push(s.slice(1));else if(s.startsWith("+"))t.push(s.slice(1));else if(s.startsWith(" ")){let o=s.slice(1);e.push(o),t.push(o)}}return{oldString:e.join(`
|
|
6
7
|
`),newString:t.join(`
|
|
7
|
-
`)}}function Ne(l){if(!l)return null;let e=l.match(/\*\*\* (?:Update|Add|Delete) File: (.+)/);return e?{filePath:e[1].trim()}:null}function De(l,e){return l?l.length<=e?l:l.substring(0,e)+"...":""}function ne(){b.clear()}var oe=require("events"),ae=require("@quantiya/codevibe-core");var M=class extends oe.EventEmitter{constructor(){super();this.pendingCalls=new Map;this.timers=new Map;this.timeoutMs=(0,ae.getConfig)().codex.approvalTimeoutMs,r.info("Approval detector initialized",{timeoutMs:this.timeoutMs})}onToolCallStart(t,i,s){r.debug("Tool call started",{callId:t,name:i});let o=this.parseInput(s),n=this.extractFilePath(i,s,o),a=this.extractDiff(i,s,o),p={callId:t,name:i,input:s,filePath:n,diff:a,parsedInput:o,timestamp:Date.now(),notificationSent:!1};if(this.pendingCalls.set(t,p),!this.shouldScheduleApprovalTimeout(i,o)){r.debug("Skipping approval timeout for non-escalated tool call",{callId:t,name:i});return}let
|
|
8
|
+
`)}}function Ne(l){if(!l)return null;let e=l.match(/\*\*\* (?:Update|Add|Delete) File: (.+)/);return e?{filePath:e[1].trim()}:null}function De(l,e){return l?l.length<=e?l:l.substring(0,e)+"...":""}function ne(){b.clear()}var oe=require("events"),ae=require("@quantiya/codevibe-core");var M=class extends oe.EventEmitter{constructor(){super();this.pendingCalls=new Map;this.timers=new Map;this.timeoutMs=(0,ae.getConfig)().codex.approvalTimeoutMs,r.info("Approval detector initialized",{timeoutMs:this.timeoutMs})}onToolCallStart(t,i,s){r.debug("Tool call started",{callId:t,name:i});let o=this.parseInput(s),n=this.extractFilePath(i,s,o),a=this.extractDiff(i,s,o),p={callId:t,name:i,input:s,filePath:n,diff:a,parsedInput:o,timestamp:Date.now(),notificationSent:!1};if(this.pendingCalls.set(t,p),!this.shouldScheduleApprovalTimeout(i,o)){r.debug("Skipping approval timeout for non-escalated tool call",{callId:t,name:i});return}let c=setTimeout(()=>{this.checkPendingCall(t)},this.timeoutMs);this.timers.set(t,c)}onToolCallComplete(t){r.debug("Tool call completed",{callId:t}),this.pendingCalls.delete(t);let i=this.timers.get(t);i&&(clearTimeout(i),this.timers.delete(t))}checkPendingCall(t){let i=this.pendingCalls.get(t);if(!i||i.notificationSent)return;let s=Date.now()-i.timestamp;r.info("Tool call still pending after timeout",{callId:t,name:i.name,elapsedMs:s}),i.notificationSent=!0,this.pendingCalls.set(t,i),this.emit("approval-pending",{callId:t,toolName:i.name,hint:this.extractHint(i.name,i.input,i.filePath),filePath:i.filePath,diff:i.diff,toolInput:i.parsedInput,rawInput:i.input,elapsedMs:s})}extractHint(t,i,s){if(s)return`File: ${s}`;if(t==="apply_patch"&&i){let o=i.match(/\*\*\* (?:Update|Add|Delete) File: (.+)/);if(o)return`File: ${o[1].trim()}`}if(t==="exec_command"||t==="shell_command"||t==="shell")try{let o=JSON.parse(i),n=typeof o.command=="string"?o.command:typeof o.cmd=="string"?o.cmd:void 0;if(n)return`Command: ${n.substring(0,50)}${n.length>50?"...":""}`}catch{}return`Tool: ${this.mapToolName(t)}`}mapToolName(t){return{exec_command:"Bash",shell_command:"Bash",shell:"Bash",apply_patch:"File Edit",write_file:"Write File",read_file:"Read File"}[t]||t}parseInput(t){if(t)try{return JSON.parse(t)}catch{return}}shouldScheduleApprovalTimeout(t,i){return t!=="exec_command"&&t!=="shell_command"&&t!=="shell"?!1:i?.sandbox_permissions==="require_escalated"}extractFilePath(t,i,s){if(t==="apply_patch"&&i){let n=i.match(/\*\*\* (?:Update|Add|Delete) File: (.+)/);if(n)return n[1].trim()}let o=s?.file_path||s?.path||s?.filePath;if(o&&typeof o=="string")return o}extractDiff(t,i,s){if(t==="apply_patch"&&i)return i;if(s?.diff&&typeof s.diff=="string")return s.diff}getPendingCalls(){return Array.from(this.pendingCalls.values())}hasPendingCalls(){return this.pendingCalls.size>0}clear(){for(let t of this.timers.values())clearTimeout(t);this.timers.clear(),this.pendingCalls.clear(),r.debug("Approval detector cleared")}shutdown(){this.clear(),this.removeAllListeners(),r.info("Approval detector shutdown")}};var pe=require("child_process"),le=require("util");var W=(0,le.promisify)(pe.exec),T="__CODEVIBE_CODEX_KEY_ESCAPE__",N=class{async sendInput(e,t){r.info("Attempting to send input to Codex",{sessionId:e,input:t});try{let i=process.env.CODEVIBE_CODEX_TMUX_SESSION;return i?(r.info("Using tmux send-keys",{tmuxSession:i}),t===T?await this.sendKeyViaTmux(i,"Escape"):await this.sendViaTmux(i,t),r.info("Successfully sent input to Codex",{sessionId:e,input:t}),!0):(r.error("No tmux session found - codevibe-codex wrapper is required",{sessionId:e,hint:"Start Codex CLI using the codevibe-codex wrapper script"}),!1)}catch(i){return r.error("Failed to send input to Codex",{sessionId:e,error:i instanceof Error?i.message:String(i)}),!1}}async sendViaTmux(e,t){let i=t.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\$/g,"\\$").replace(/`/g,"\\`");r.info("Sending via tmux",{sessionName:e,inputLength:t.length});try{let s=`tmux send-keys -t "${e}" -l "${i}"`;await W(s),await this.delay(500);let o=`tmux send-keys -t "${e}" Enter`;await W(o),r.info("tmux send-keys completed")}catch(s){throw r.error("tmux send-keys failed",{sessionName:e,error:s}),s}}async sendKeyViaTmux(e,t){r.info("Sending special key via tmux",{sessionName:e,key:t});try{let i=`tmux send-keys -t "${e}" ${t}`;await W(i),r.info("tmux special-key send completed")}catch(i){throw r.error("tmux special-key send failed",{sessionName:e,key:t,error:i}),i}}delay(e){return new Promise(t=>setTimeout(t,e))}isApprovalResponse(e){let t=e.trim().toLowerCase();return["y","n","a","q","e","yes","no"].includes(t)||/^[0-9]+$/.test(t)}};var y=g(require("fs")),ce=g(require("os")),L=g(require("path")),de=require("crypto"),ue=require("child_process"),me=require("events"),he=require("util");var B=(0,he.promisify)(ue.exec),D=class extends me.EventEmitter{constructor(){super(...arguments);this.sessionName=null;this.started=!1;this.pipeFilePath=null;this.filePosition=0;this.watcher=null;this.processing=!1;this.pendingRead=!1;this.lastPromptHash=null}async start(t){if(this.started&&this.sessionName===t){r.debug("Tmux pane observer already started",{sessionName:t});return}this.started&&await this.stop(),this.sessionName=t,this.started=!0,this.filePosition=0,this.lastPromptHash=null,this.pipeFilePath=L.join(ce.tmpdir(),`codevibe-codex-pane-${process.pid}.log`),y.mkdirSync(L.dirname(this.pipeFilePath),{recursive:!0}),y.writeFileSync(this.pipeFilePath,""),await this.enablePipePane(),this.startFileWatcher(),r.info("Tmux pane observer started",{sessionName:t})}async stop(){if(this.started){try{await this.disablePipePane()}catch(t){r.debug("Failed to disable tmux pipe-pane cleanly",{error:t})}if(this.watcher&&(this.watcher.close(),this.watcher=null),this.pipeFilePath)try{y.unlinkSync(this.pipeFilePath)}catch{}r.info("Tmux pane observer stopped",{sessionName:this.sessionName}),this.started=!1,this.sessionName=null,this.pipeFilePath=null,this.filePosition=0,this.processing=!1,this.pendingRead=!1,this.lastPromptHash=null,this.removeAllListeners("prompt-candidate"),this.removeAllListeners("observer-error")}}async captureSnapshot(t=120){if(!this.sessionName)throw new Error("Tmux pane observer is not started");let i=Math.max(1,Math.floor(t)),s=this.escapeShellArg(this.sessionName),o=`tmux capture-pane -p -e -S -${i} -t '${s}'`;try{let{stdout:n}=await B(o);return n}catch(n){throw r.error("Failed to capture tmux pane snapshot",{sessionName:this.sessionName,error:n}),this.emit("observer-error",n),n}}escapeShellArg(t){return t.replace(/'/g,"'\\''")}async enablePipePane(){if(!this.sessionName||!this.pipeFilePath)throw new Error("Tmux pane observer is not initialized");let t=this.escapeShellArg(this.sessionName),i=this.escapeShellArg(this.pipeFilePath),s=`tmux pipe-pane -O -t '${t}' "cat >> '${i}'"`;await B(s),r.debug("Enabled tmux pipe-pane mirroring",{sessionName:this.sessionName,pipeFilePath:this.pipeFilePath})}async disablePipePane(){if(!this.sessionName)return;let i=`tmux pipe-pane -t '${this.escapeShellArg(this.sessionName)}'`;await B(i)}startFileWatcher(){this.pipeFilePath&&(this.watcher=y.watch(this.pipeFilePath,t=>{t==="change"&&this.processFileChanges()}))}async processFileChanges(){if(this.pipeFilePath){if(this.processing){this.pendingRead=!0;return}this.processing=!0;try{do{this.pendingRead=!1;let t=this.readAppendedChunk();if(!t||!this.looksLikePromptDelta(t))continue;let i=await this.captureSnapshot();if(!i||!this.looksLikePromptSnapshot(i))continue;let s=this.hashPromptSnapshot(i);s!==this.lastPromptHash&&(this.lastPromptHash=s,this.emit("prompt-candidate",{rawDelta:t,snapshot:i,detectedAt:Date.now()}))}while(this.pendingRead)}catch(t){r.error("Failed to process tmux pane changes",{error:t}),this.emit("observer-error",t)}finally{this.processing=!1}}}readAppendedChunk(){if(!this.pipeFilePath)return"";let t=y.statSync(this.pipeFilePath);if(t.size<=this.filePosition)return"";let i=y.openSync(this.pipeFilePath,"r");try{let s=t.size-this.filePosition,o=Buffer.alloc(s);return y.readSync(i,o,0,s,this.filePosition),this.filePosition=t.size,o.toString("utf-8")}finally{y.closeSync(i)}}looksLikePromptDelta(t){return/\[(?:y\/n|Y\/n|y\/N)\]|\b(?:apply|approve|allow|reject|deny|continue)\b/i.test(t)}looksLikePromptSnapshot(t){let i=t.split(`
|
|
8
9
|
`).slice(-20).join(`
|
|
9
10
|
`);return/\[(?:y\/n|Y\/n|y\/N)\]|^\s*\d+\.\s+/im.test(i)}hashPromptSnapshot(t){let i=t.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g,"").replace(/\r/g,`
|
|
10
11
|
`).replace(/[ \t]+\n/g,`
|
|
11
|
-
`).trim();return(0,de.createHash)("sha256").update(i).digest("hex")}};var I=require("@quantiya/codevibe-core");var j=f(require("express")),E=f(require("fs")),V=f(require("path")),G=f(require("os"));var U=class{constructor(){this.assignedPort=0;this.app=(0,j.default)(),this.setupMiddleware(),this.setupRoutes(),this.tmuxSession=process.env.CODEVIBE_CODEX_TMUX_SESSION}getPort(){return this.assignedPort}setupMiddleware(){this.app.use(j.default.json({limit:"1mb"})),this.app.use((e,t,i)=>{r.debug(`${e.method} ${e.path}`,{body:e.body}),i()})}setupRoutes(){this.app.get("/health",(e,t)=>{t.json({success:!0,data:{status:"healthy",uptime:process.uptime()}})}),this.app.post("/event",this.handleEvent.bind(this))}async handleEvent(e,t){try{let i=e.body;if(!i.session_id||!i.hook_event_name){t.status(400).json({success:!1,error:"Missing session_id or hook_event_name"});return}let s=this.transformHookToEvent(i);r.info("Received hook event",{sessionId:i.session_id,hookEvent:i.hook_event_name,type:s.type}),this.eventHandler&&await this.eventHandler(s),t.json({success:!0})}catch(i){r.error("Error handling event:",i),t.status(500).json({success:!1,error:i instanceof Error?i.message:"Unknown error"})}}transformHookToEvent(e){let t={cwd:e.cwd,hook_event_name:e.hook_event_name,...e.metadata||{}},i,s;switch(e.hook_event_name){case"SessionStart":i="NOTIFICATION",s="Session started",t.source=e.source;break;case"UserPromptSubmit":i="USER_PROMPT",s=e.prompt||"";break;case"PreToolUse":i="NOTIFICATION",s="PreToolUse observed",t.tool_name=e.tool_name,t.tool_input=e.tool_input,t.tool_use_id=e.tool_use_id,t.approval_status="observed_pre_tool",t.requires_user_action=!1;break;case"PermissionRequest":i="NOTIFICATION",s="PermissionRequest observed",t.tool_name=e.tool_name,t.tool_input=e.tool_input,t.permission_mode=e.permission_mode,t.turn_id=e.turn_id,t.requires_user_action=!0;break;case"PostToolUse":i="TOOL_USE",s=JSON.stringify({tool_name:e.tool_name,tool_input:e.tool_input,tool_response:e.tool_response}),t.tool_name=e.tool_name;break;case"Stop":i="ASSISTANT_RESPONSE",s=e.last_assistant_message||"";break;default:i="NOTIFICATION",s=`Hook: ${e.hook_event_name}`}return{session_id:e.session_id,hook_event_name:e.hook_event_name,type:i,source:"DESKTOP",content:s,metadata:t}}onEvent(e){this.eventHandler=e}async start(){return new Promise((e,t)=>{try{this.server=this.app.listen(0,"localhost",()=>{let i=this.server.address();this.assignedPort=i.port,r.info(`HTTP API listening on http://localhost:${this.assignedPort}`),this.writePortFile(this.assignedPort),e(this.assignedPort)}),this.server.on("error",i=>{r.error("HTTP server error:",i),t(i)})}catch(i){t(i)}})}writePortFile(e){if(!this.tmuxSession){r.warn("No CODEVIBE_CODEX_TMUX_SESSION set, skipping port file");return}let t=V.join(G.tmpdir(),`codevibe-codex-${this.tmuxSession}.port`);try{E.writeFileSync(t,e.toString()),r.info(`Port file written: ${t} -> ${e}`)}catch(i){r.error(`Failed to write port file: ${t}`,i)}}removePortFile(){if(!this.tmuxSession)return;let e=V.join(G.tmpdir(),`codevibe-codex-${this.tmuxSession}.port`);try{E.existsSync(e)&&(E.unlinkSync(e),r.info(`Port file removed: ${e}`))}catch(t){r.warn(`Failed to remove port file: ${e}`,t)}}async stop(){return this.removePortFile(),new Promise(e=>{this.server?this.server.close(()=>{r.info("HTTP API stopped"),e()}):e()})}};var $=class{constructor(e={}){this.pendingBySession=new Map;this.expiryMs=e.expiryMs??1e4,this.minFuzzyEchoLength=e.minFuzzyEchoLength??16,this.minFuzzyEchoRatio=e.minFuzzyEchoRatio??.35,this.now=e.now??Date.now}track(e,t){let i=this.normalize(t);if(!i)return;let s=this.validEntries(e);s.push({normalized:i,timestamp:this.now()}),this.pendingBySession.set(e,s)}forget(e,t){let i=this.normalize(t);if(!i)return;let s=!1,o=this.validEntries(e).filter(n=>!s&&n.normalized===i?(s=!0,!1):!0);this.replaceEntries(e,o)}consumeIfDuplicate(e,t){let i=this.normalize(t);if(!i)return null;let s=null,o=this.validEntries(e).filter(n=>{if(!s){let a=this.matchType(n.normalized,i);if(a)return s={matchType:a,originalLength:n.normalized.length,echoLength:i.length},!1}return!0});return this.replaceEntries(e,o),s}validEntries(e){let t=this.now()-this.expiryMs;return(this.pendingBySession.get(e)||[]).filter(i=>i.timestamp>=t)}replaceEntries(e,t){t.length>0?this.pendingBySession.set(e,t):this.pendingBySession.delete(e)}matchType(e,t){if(e===t)return"exact";let i=t.length>=this.minFuzzyEchoLength,s=t.length/e.length>=this.minFuzzyEchoRatio;return i&&s&&e.endsWith(t)?"suffix":null}normalize(e){return e.replace(/\s+/g," ").trim()}};var ge=f(require("crypto")),ve=f(require("fs")),ye=f(require("https")),K=f(require("os")),Pe=f(require("path")),Le="G-GS74YEQTB8",Ue="lAfOF6OxRzSQ-NsLBRjhAg",$e="www.google-analytics.com",Ke=`/mp/collect?measurement_id=${Le}&api_secret=${Ue}`,fe=800;function qe(){try{let l=Pe.resolve(__dirname,"..","package.json"),e=ve.readFileSync(l,"utf-8"),t=JSON.parse(e);if(typeof t.version=="string"&&t.version.length>0&&t.version.length<30)return t.version}catch{}return"unknown"}var He=qe();function ze(){let l=typeof process.getuid=="function"?process.getuid():0;return ge.createHash("sha256").update(`${K.hostname()}-${l}`).digest("hex").substring(0,36)}function We(l){if(!l)return"";let e=K.homedir(),t=l.replace(/[\n\r\t]/g," ");if(e&&e.length>0){let i=e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");t=t.replace(new RegExp(i,"g"),"~")}return t=t.replace(/\/Users\/[^/\s"'`]+/g,"/Users/<user>").replace(/\/home\/[^/\s"'`]+/g,"/home/<user>").replace(/[^\x20-\x7E]/g,""),t.trim().substring(0,100)}function h(l,e){return new Promise(t=>{let i,s=!1,o=()=>{s||(s=!0,clearTimeout(n),t())},n=setTimeout(()=>{try{i?.destroy()}catch{}o()},fe);typeof n.unref=="function"&&n.unref();try{let a={...e};typeof a.error_message=="string"&&(a.error_message=We(a.error_message));let p=JSON.stringify({client_id:ze(),events:[{name:l,params:{agent:"codex",plugin_version:He,platform:process.platform,source:process.env.CODEVIBE_TELEMETRY_SOURCE||"production",...a}}]});i=ye.request({hostname:$e,path:Ke,method:"POST",headers:{"Content-Type":"application/json"},timeout:fe},d=>{d.resume(),d.on("end",o),d.on("close",o),d.on("error",o)}),i.on("error",o),i.on("timeout",()=>{try{i?.destroy()}catch{}o()}),i.on("close",o),i.write(p),i.end()}catch{o()}})}var Be=(0,Ie.promisify)(we.exec),je="/quit",Se="CODEVIBE_CODEX_TMUX_SESSION",Ve=600*1e3,Ge=30*1e3;async function Xe(l,e){let t=async(i,s)=>{try{await Be(i)}catch(o){r.warn("tmux send-keys failed during self-terminate",{sessionName:l,label:s,error:String(o)})}};await t(`tmux send-keys -t "${l}" C-c`,"ctrl-c"),await new Promise(i=>setTimeout(i,200)),await t(`tmux send-keys -t "${l}" -l "${e}"`,"quit-text"),await new Promise(i=>setTimeout(i,500)),await t(`tmux send-keys -t "${l}" Enter`,"enter")}var q=class{constructor(){this.sessionState=null;this.unsubscribe=null;this.sessionKey=null;this.pendingInteractivePrompt=null;this.queuedInteractivePrompts=[];this.isInitializingSession=!1;this.bufferedLogEntries=[];this.hooksActive=!1;this.hooksCompatibilityNoticeSent=!1;this.resolvedApprovalDedupeKeys=new Map;this.subscribedSessionId=null;this.mobilePromptDeduper=new $({expiryMs:1e4});this.launchSessionInitPromise=null;this.sessionStartedInitPromise=null;this.httpApi=new U,this.sessionWatcher=new F,this.approvalDetector=new M,this.promptResponder=new N,this.tmuxPaneObserver=new D}async start(){r.info("Starting CodeVibe Codex companion server",{environment:(0,c.getEnvironment)()}),this.appSyncClient=new c.AppSyncClient,await this.appSyncClient.authenticateWithStoredTokens()||(r.error('Authentication failed. Run "codevibe-codex login" first.'),console.error('Not authenticated. Run "codevibe-codex login" to sign in.'),process.exit(1)),r.info("Authenticated successfully",{userId:this.appSyncClient.getCurrentUserId(),email:this.appSyncClient.getCurrentUserEmail()}),await(0,c.registerDeviceEncryptionKey)(this.appSyncClient,r),(0,c.startDeviceKeyWatcher)(this.appSyncClient,r);try{let i=await this.appSyncClient.sweepOrphanSessions({agentType:"CODEX"});i>0&&r.info("Orphan sweep: marked stale Codex sessions INACTIVE",{swept:i})}catch(i){r.warn("Orphan sweep failed, continuing startup",{error:i instanceof Error?i.message:String(i)})}this.httpApi.onEvent(this.handleEventFromHook.bind(this));let t=await this.httpApi.start();r.info("HTTP API started for hooks",{port:t}),await this.createLaunchSession(),this.setupEventHandlers(),this.sessionWatcher.start(),r.info("CodeVibe Codex companion server started")}async createLaunchSession(){let e=process.env.CODEVIBE_CODEX_TMUX_SESSION;if(!e){r.warn("No CODEVIBE_CODEX_TMUX_SESSION \u2014 skipping launch session");return}let t;this.launchSessionInitPromise=new Promise(i=>{t=i});try{let i=process.env.CODEX_WORKING_DIRECTORY||process.cwd(),s=this.generateSessionId(e),o=this.appSyncClient.getCurrentUserId();r.info("Creating launch session",{sessionId:s,projectPath:i});let n=!1;try{let a=await(0,c.resumeOrCreateSession)({sessionId:s,userId:o,agentType:c.AgentType.CODEX,projectPath:i,metadata:{launchSession:!0}},this.appSyncClient,r);this.sessionKey=a.sessionKey,n=!0,this.sessionState={sessionId:s,userId:o,projectPath:i,cwd:i,createdAt:new Date,subscriptionActive:!1,metadata:{launchSession:!0},codexSessionId:e,codexLogFile:void 0},await h("daemon_init_step_completed",{step:"session_resume_or_create",path:"launch_session"})}catch(a){await h("daemon_init_step_failed",{step:"session_resume_or_create",path:"launch_session",error_class:a?.name||"Error",error_message:a?.message||String(a)}),r.error("Failed to create/resume launch session (non-fatal)",{error:a})}if(!n)return;await h("daemon_init_step_completed",{step:"session_state_set",path:"launch_session"});try{this.subscribeToMobileEvents(s)?await h("daemon_init_step_completed",{step:"subscribe_mobile_events",path:"launch_session"}):await h("daemon_init_step_failed",{step:"subscribe_mobile_events",path:"launch_session",error_class:"SubscriptionSetupFailed",error_message:"subscribeToMobileEvents returned false"})}catch(a){await h("daemon_init_step_failed",{step:"subscribe_mobile_events",path:"launch_session",error_class:a?.name||"Error",error_message:a?.message||String(a)}),r.error("Failed to subscribe to mobile events for launch session",{error:a})}try{this.appSyncClient.startHeartbeat(s),await h("daemon_init_step_completed",{step:"heartbeat_start",path:"launch_session"})}catch(a){await h("daemon_init_step_failed",{step:"heartbeat_start",path:"launch_session",error_class:a?.name||"Error",error_message:a?.message||String(a)}),r.error("Failed to start heartbeat for launch session",{error:a})}this.startMobileEndWatcher(s),r.info("Launch session created",{sessionId:s})}finally{t()}}async handleEventFromHook(e){let{session_id:t,hook_event_name:i,type:s,content:o,metadata:n}=e;if(this.hooksActive=!0,r.info("[Hooks] Received event",{sessionId:t,hookEvent:i,type:s,contentLength:o?.length}),i==="SessionStart"){if(this.launchSessionInitPromise&&await this.launchSessionInitPromise,this.sessionState)r.info("[Hooks] SessionStart \u2014 launch session already exists, updating codexSessionId",{existingSessionId:this.sessionState.sessionId,codexSessionId:t}),this.sessionState.codexSessionId=t,this.sessionState.metadata={...this.sessionState.metadata,codexSessionId:t,cliVersion:n?.model||"unknown",modelProvider:n?.model||"unknown",launchSession:void 0},this.appSyncClient.updateSession({sessionId:this.sessionState.sessionId,metadata:this.sessionState.metadata}).catch(p=>r.warn("Failed to update session metadata",{error:p})),await this.startTmuxObserverWithBeacon("session_start_existing");else{let p={id:t,timestamp:new Date().toISOString(),cwd:n?.cwd||process.cwd(),originator:"hook",cli_version:n?.model||"unknown",instructions:null,source:n?.source||"startup",model_provider:n?.model||"unknown"};await this.ensureSessionStarted(p)}return}if(!this.sessionState){r.warn("[Hooks] Session not initialized, buffering event",{hook_event_name:i});return}let a=this.sessionState.sessionId;if(s==="USER_PROMPT"&&o){let p=this.consumeRecentMobilePrompt(a,o);if(p){r.info("[Hooks] Skipping duplicate USER_PROMPT from mobile",{sessionId:a,matchType:p.matchType,originalLength:p.originalLength,echoLength:p.echoLength});return}}if(i==="PreToolUse"){r.debug("[Hooks] PreToolUse observed; AppSync emission suppressed",{toolName:n?.tool_name||"unknown",sessionId:a});return}if(i==="PermissionRequest"){await this.handlePermissionRequestHook(e);return}if(i==="PostToolUse"){let p=o,d=n,u=!1;this.sessionKey&&(p=c.cryptoService.encryptContent(o,this.sessionKey),n&&(d={encrypted:c.cryptoService.encryptMetadata(n,this.sessionKey)}),u=!0),await this.appSyncClient.createEvent({sessionId:a,type:c.EventType.TOOL_USE,source:c.EventSource.DESKTOP,content:p,metadata:d,isEncrypted:u});return}if(s==="ASSISTANT_RESPONSE"||s==="USER_PROMPT"){let p=o,d=!1;this.sessionKey&&o&&(p=c.cryptoService.encryptContent(o,this.sessionKey),d=!0),await this.appSyncClient.createEvent({sessionId:a,type:s==="ASSISTANT_RESPONSE"?c.EventType.ASSISTANT_RESPONSE:c.EventType.USER_PROMPT,source:c.EventSource.DESKTOP,content:p,isEncrypted:d});return}}mapToolName(e){return{shell_command:"Bash",shell:"Bash",apply_patch:"Edit",create_file:"Write",read_file:"Read"}[e]||e}trackMobilePrompt(e,t){this.mobilePromptDeduper.track(e,t),r.debug("Tracking mobile prompt for USER_PROMPT echo deduplication",{sessionId:e,promptLength:t.trim().length})}forgetMobilePrompt(e,t){this.mobilePromptDeduper.forget(e,t)}consumeRecentMobilePrompt(e,t){return this.mobilePromptDeduper.consumeIfDuplicate(e,t)}setupEventHandlers(){this.sessionWatcher.on("session-started",async e=>{if(this.launchSessionInitPromise&&await this.launchSessionInitPromise,this.sessionState){r.info("[JSONL] Session already active, skipping",{currentSessionId:this.sessionState.sessionId,codexSessionId:e.id});return}await this.ensureSessionStarted(e)}),this.sessionWatcher.on("log-entry",async e=>{await this.handleLogEntry(e)}),this.approvalDetector.on("approval-pending",async e=>{await this.handleApprovalPending(e)}),this.tmuxPaneObserver.on("prompt-candidate",async e=>{await this.handleTmuxPromptCandidate(e.snapshot)}),this.tmuxPaneObserver.on("observer-error",e=>{r.debug("Tmux pane observer error",{error:e})}),this.sessionWatcher.on("error",e=>{r.error("Session watcher error:",e)})}async ensureSessionStarted(e){if(this.launchSessionInitPromise&&await this.launchSessionInitPromise,this.sessionState)return;if(this.sessionStartedInitPromise){try{await this.sessionStartedInitPromise}catch{}return}let t=this.handleSessionStarted(e);this.sessionStartedInitPromise=t.catch(()=>{});try{await t}finally{this.sessionStartedInitPromise=null}}async handleSessionStarted(e){r.info("Handling new Codex session",{codexSessionId:e.id}),this.isInitializingSession=!0,this.bufferedLogEntries=[],this.sessionState&&await this.endActiveSession("new-codex-session-started");let t=process.env.CODEX_WORKING_DIRECTORY||e.cwd||process.cwd(),i=this.generateSessionId(e.id),s=this.appSyncClient.getCurrentUserId(),o={codexSessionId:e.id,cliVersion:e.cli_version,modelProvider:e.model_provider};try{let n=await(0,c.resumeOrCreateSession)({sessionId:i,userId:s,agentType:c.AgentType.CODEX,projectPath:t,metadata:o},this.appSyncClient,r);this.sessionKey=n.sessionKey,await h("daemon_init_step_completed",{step:"session_resume_or_create",path:"session_started"})}catch(n){throw this.isInitializingSession=!1,await h("daemon_init_step_failed",{step:"session_resume_or_create",path:"session_started",error_class:n?.name||"Error",error_message:n?.message||String(n)}),r.error("Failed to create/resume session:",n),n}try{this.sessionState={sessionId:i,userId:s,projectPath:t,cwd:e.cwd,createdAt:new Date,subscriptionActive:!1,metadata:o,codexSessionId:e.id,codexLogFile:this.sessionWatcher.getActiveLogFile()||void 0},await h("daemon_init_step_completed",{step:"session_state_set",path:"session_started"})}catch(n){await h("daemon_init_step_failed",{step:"session_state_set",path:"session_started",error_class:n?.name||"Error",error_message:n?.message||String(n)}),r.error("Failed to set session state:",n)}try{this.subscribeToMobileEvents(i)?await h("daemon_init_step_completed",{step:"subscribe_mobile_events",path:"session_started"}):await h("daemon_init_step_failed",{step:"subscribe_mobile_events",path:"session_started",error_class:"SubscriptionSetupFailed",error_message:"subscribeToMobileEvents returned false"})}catch(n){await h("daemon_init_step_failed",{step:"subscribe_mobile_events",path:"session_started",error_class:n?.name||"Error",error_message:n?.message||String(n)}),r.error("Failed to subscribe to mobile events:",n)}try{this.appSyncClient.startHeartbeat(i),await h("daemon_init_step_completed",{step:"heartbeat_start",path:"session_started"})}catch(n){await h("daemon_init_step_failed",{step:"heartbeat_start",path:"session_started",error_class:n?.name||"Error",error_message:n?.message||String(n)}),r.error("Failed to start heartbeat:",n)}this.startMobileEndWatcher(i);try{await this.flushBufferedLogEntries(),await h("daemon_init_step_completed",{step:"flush_buffered_entries",path:"session_started"})}catch(n){await h("daemon_init_step_failed",{step:"flush_buffered_entries",path:"session_started",error_class:n?.name||"Error",error_message:n?.message||String(n)}),r.error("Failed to flush buffered log entries:",n),this.bufferedLogEntries=[]}await this.startTmuxObserverWithBeacon("session_started"),this.isInitializingSession=!1}async flushBufferedLogEntries(){if(this.bufferedLogEntries.length===0)return;let e=this.bufferedLogEntries;this.bufferedLogEntries=[],r.info("Flushing buffered log entries after session initialization",{count:e.length,sessionId:this.sessionState?.sessionId});for(let t of e)await this.handleLogEntry(t)}async handleLogEntry(e){if(!this.sessionState){if(this.isInitializingSession){this.bufferedLogEntries.push(e),r.debug("Buffering log entry until session initialization completes",{type:e.type,bufferedCount:this.bufferedLogEntries.length});return}r.warn("Received log entry but no active session");return}if(e.type==="response_item"&&e.payload){let n=e.payload.type;if(n==="function_call"||n==="custom_tool_call")this.approvalDetector.onToolCallStart(e.payload.call_id,e.payload.name,e.payload.arguments||e.payload.input||"");else if(n==="function_call_output"||n==="custom_tool_call_output"){let a=this.approvalDetector.getPendingCalls().find(d=>d.callId===e.payload.call_id),p=a?this.buildApprovalDedupeKey(this.buildApprovalPromptContextFromPendingCall(a)):void 0;this.approvalDetector.onToolCallComplete(e.payload.call_id),await this.clearResolvedInteractivePrompt(e.payload.call_id,p)}}let t=re(e,this.sessionState.sessionId);if(!t)return;let i=e.payload?.type,s=i==="function_call"||i==="function_call_output",o=t.type===c.EventType.USER_PROMPT||t.type===c.EventType.ASSISTANT_RESPONSE||s&&(t.type===c.EventType.TOOL_USE||t.type===c.EventType.INTERACTIVE_PROMPT);if(!this.hooksActive&&o&&await this.emitHooksCompatibilityModeNotice(),this.hooksActive){if(t.type===c.EventType.USER_PROMPT||t.type===c.EventType.ASSISTANT_RESPONSE){r.debug("[JSONL] Skipping \u2014 hooks deliver this event type",{type:t.type});return}if(s&&(t.type===c.EventType.TOOL_USE||t.type===c.EventType.INTERACTIVE_PROMPT)){r.debug("[JSONL] Skipping function_call \u2014 hooks deliver this",{type:t.type,tool:e.payload?.name});return}}if(t.type===c.EventType.USER_PROMPT&&t.source===c.EventSource.DESKTOP){let n=this.consumeRecentMobilePrompt(this.sessionState.sessionId,t.content);if(n){r.info("[JSONL] Skipping duplicate USER_PROMPT from mobile",{sessionId:this.sessionState.sessionId,matchType:n.matchType,originalLength:n.originalLength,echoLength:n.echoLength});return}}try{if(this.sessionKey){if(t.content=c.cryptoService.encryptContent(t.content,this.sessionKey),t.metadata){let n=c.cryptoService.encryptMetadata(t.metadata,this.sessionKey);t.metadata={encrypted:n}}t.isEncrypted=!0,r.debug("Event encrypted",{type:t.type})}await this.appSyncClient.createEvent(t),r.debug("Event synced to backend",{type:t.type,encrypted:!!this.sessionKey})}catch(n){r.error("Failed to sync event:",n)}}async handlePermissionRequestHook(e){if(!this.sessionState)return;let t=e.metadata||{},i=typeof t.tool_name=="string"?t.tool_name:"Tool",s=t.tool_input,o=this.stringifyToolInput(s),n={toolName:i,toolInput:s,rawInput:o,filePath:this.extractFilePathFromToolInput(i,s,o),diff:i==="apply_patch"?o:void 0,hint:this.buildPermissionRequestHint(i,s,o)},a=this.buildToolDetailsForInteractivePrompt(n),p=a.tool_name||this.mapToolNameForApproval(i),d=a.tool_input||this.buildFallbackToolInput(n),u=!!(p&&d),g=(await this.tryParsePermissionRequestPromptFromTmux(Ee=>this.parsedPromptMatchesApprovalContext(Ee,n)))?.parsedPrompt??null;if(!g){r.warn("[Hooks] Suppressing PermissionRequest mobile prompt because exact Codex options are unavailable",{sessionId:this.sessionState.sessionId,toolName:i,hint:n.hint});return}let P=this.buildApprovalDedupeKey({toolName:i,toolInput:s,filePath:n.filePath,rawInput:o,hint:n.hint});if(P&&this.hasRecentlyResolvedApprovalDedupeKey(P)){r.info("[Hooks] Skipping PermissionRequest prompt; same approval was already answered recently",{sessionId:this.sessionState.sessionId,toolName:i,dedupeKey:P});return}let S=this.buildCodexPromptPresentation(g);if(!S){r.warn("[Hooks] Suppressing PermissionRequest mobile prompt because Codex option hotkeys are unavailable",{sessionId:this.sessionState.sessionId,toolName:i,hint:n.hint});return}let R=this.buildPermissionPromptId(t,i,s,o),A=this.buildApprovalPromptContent(S.content,{toolName:p,toolInput:d,hint:n.hint,filePath:n.filePath}),w={promptId:R,kind:S.kind,options:S.options,submitMap:S.submitMap,promptText:A,createdAt:Date.now(),source:"permission_hook",dedupeKey:P,requiresFollowUpText:S.requiresFollowUpText},Q={isApprovalHint:!0,toolName:i,toolInput:s,hint:n.hint,filePath:n.filePath,diff:n.diff,rawInput:o,tool_name:p,tool_input:d,has_details:u,options:S.options,instructions:S.instructions,prompt_source:g?"permission_hook_tmux":"permission_hook",permission_mode:t.permission_mode,turn_id:t.turn_id,dedupe_key:P};r.info("[Hooks] Sending PermissionRequest interactive prompt",{sessionId:this.sessionState.sessionId,toolName:i,promptId:R,promptSource:Q.prompt_source,dedupeKey:P}),await this.enqueueOrEmitInteractivePrompt({prompt:w,content:A,metadata:Q})}async emitHooksCompatibilityModeNotice(){if(this.hooksCompatibilityNoticeSent||!this.sessionState)return;this.hooksCompatibilityNoticeSent=!0;let e="Codex hooks are not active. CodeVibe is using compatibility mode: mobile approvals still mirror the desktop prompt, but timeline events may be delayed or incomplete. Open Codex /hooks and trust/enable CodeVibe hooks for full fidelity.",t=e,i={compatibility_mode:!0,reason:"codex_hooks_inactive",action:"trust_enable_codevibe_hooks"},s=!1;this.sessionKey&&(t=c.cryptoService.encryptContent(e,this.sessionKey),i={encrypted:c.cryptoService.encryptMetadata(i,this.sessionKey)},s=!0),r.warn("Codex hooks inactive; running in compatibility mode",{sessionId:this.sessionState.sessionId});try{await this.appSyncClient.createEvent({sessionId:this.sessionState.sessionId,type:c.EventType.NOTIFICATION,source:c.EventSource.DESKTOP,content:t,metadata:i,...s?{isEncrypted:!0}:{}})}catch(o){r.warn("Failed to emit hooks compatibility mode notification",{error:o})}}async handleApprovalPending(e){if(!this.sessionState)return;let t=this.buildApprovalDedupeKey(e);if(t&&this.hasRecentlyResolvedApprovalDedupeKey(t)){r.info("Skipping heuristic approval prompt; same approval was already answered recently",{callId:e.callId,toolName:e.toolName,dedupeKey:t});return}if(t&&this.hasActiveOrQueuedPermissionHookPrompt(t)){this.mergeCallIdIntoPromptByDedupeKey(t,e.callId),r.info("Skipping heuristic approval prompt; PermissionRequest hook already emitted it",{callId:e.callId,toolName:e.toolName,dedupeKey:t});return}r.info("Sending approval pending interactive prompt",e);try{let i=await this.tryParseInteractivePromptFromTmux(),s=i?.parsedPrompt??null,o=this.buildToolDetailsForInteractivePrompt(e,i?.snapshot),n=o.tool_name||this.mapToolNameForApproval(e.toolName),a=o.tool_input||this.buildFallbackToolInput(e),p=!!(n&&a);if(!s){r.warn("Suppressing heuristic approval prompt because exact Codex options are unavailable",{callId:e.callId,toolName:e.toolName,hint:e.hint});return}if(!this.parsedPromptMatchesApprovalContext(s,e)){r.warn("Suppressing heuristic approval prompt because parsed tmux prompt does not match the active tool",{callId:e.callId,toolName:e.toolName,hint:e.hint,parsedPromptText:s.promptText});return}let d=this.buildCodexPromptPresentation(s);if(!d){r.warn("Suppressing heuristic approval prompt because Codex option hotkeys are unavailable",{callId:e.callId,toolName:e.toolName,hint:e.hint});return}let u=d.options,m=this.buildApprovalPromptContent(d.content,{toolName:n,toolInput:a,hint:e.hint,filePath:e.filePath}),g={promptId:e.callId,callId:e.callId,kind:d.kind,options:u,submitMap:d.submitMap,promptText:d.promptText,createdAt:Date.now(),source:s?"tmux":"heuristic",dedupeKey:t,requiresFollowUpText:d.requiresFollowUpText},P={isApprovalHint:!0,toolName:e.toolName,toolInput:e.toolInput,hint:e.hint,callId:e.callId,filePath:e.filePath,diff:e.diff,rawInput:e.rawInput,tool_name:n,tool_input:a,has_details:p,options:u,instructions:d.instructions,prompt_source:s?"tmux":"heuristic",dedupe_key:t};r.debug("Interactive prompt (pre-encryption)",{sessionId:this.sessionState.sessionId,callId:e.callId,contentPreview:m.substring(0,200),toolDetails:o,metadata:P}),await this.enqueueOrEmitInteractivePrompt({prompt:g,content:m,metadata:P})}catch(i){r.error("Failed to send approval interactive prompt:",i)}}async handleTmuxPromptCandidate(e){if(!this.sessionState)return;let t=(0,I.parseInteractivePrompt)(e);if(!t)return;let i=this.buildCodexPromptPresentation(t);if(!i){r.warn("Skipping tmux-detected prompt because Codex option hotkeys are unavailable",{parsedPromptText:t.promptText});return}let s=this.getMostRecentPendingToolCall();s||(await new Promise(w=>setTimeout(w,500)),s=this.getMostRecentPendingToolCall());let o=s?this.buildApprovalPromptContextFromPendingCall(s):null;if(o&&!this.parsedPromptMatchesApprovalContext(t,o)){r.warn("Skipping tmux-detected prompt because parsed prompt does not match pending tool call",{callId:s?.callId,toolName:s?.name,parsedPromptText:t.promptText});return}let n=o?null:this.buildApprovalPromptContextFromParsedPrompt(t),a=o||n;if(!a){r.warn("Skipping tmux-detected prompt because no tool context could be derived from the parsed prompt",{parsedPromptText:t.promptText});return}let p=a?this.buildApprovalDedupeKey(a):void 0;if(p&&this.hasRecentlyResolvedApprovalDedupeKey(p)){r.info("Skipping tmux-detected prompt; same approval was already answered recently",{promptText:t.promptText,dedupeKey:p});return}if(p&&this.hasActiveOrQueuedPermissionHookPrompt(p)){s?.callId&&this.mergeCallIdIntoPromptByDedupeKey(p,s.callId),r.info("Skipping tmux-detected prompt; PermissionRequest hook already emitted it",{promptText:t.promptText,dedupeKey:p});return}let d=a?this.buildToolDetailsForInteractivePrompt(a,e):{},u=d.tool_name||this.mapToolNameForApproval(s?.name),m=d.tool_input||(a?this.buildFallbackToolInput(a):void 0),g=!!(u&&m),S={promptId:s?.callId||(0,_e.v4)(),callId:s?.callId,kind:i.kind,options:i.options,submitMap:i.submitMap,promptText:i.promptText,createdAt:Date.now(),source:"tmux",dedupeKey:p,requiresFollowUpText:i.requiresFollowUpText},R={options:i.options,instructions:i.instructions,prompt_source:"tmux_live",tool_name:u,tool_input:m,has_details:g,dedupe_key:p},A=this.buildApprovalPromptContent(i.content,{toolName:u,toolInput:m,hint:a?.hint,filePath:a?.filePath});try{await this.enqueueOrEmitInteractivePrompt({prompt:S,content:A,metadata:R})&&r.info("Sent tmux-detected interactive prompt",{sessionId:this.sessionState.sessionId,promptText:t.promptText,kind:t.kind})}catch(w){r.error("Failed to send tmux-detected interactive prompt",{error:w})}}async enqueueOrEmitInteractivePrompt(e){if(e.prompt.dedupeKey&&this.hasRecentlyResolvedApprovalDedupeKey(e.prompt.dedupeKey))return r.debug("Skipping interactive prompt emission; same approval was already answered recently",{source:e.prompt.source,callId:e.prompt.callId,dedupeKey:e.prompt.dedupeKey}),!1;let t=this.pendingInteractivePrompt;if(!t){this.pendingInteractivePrompt=e.prompt;try{return await this.emitInteractivePromptEvent(e),!0}catch(i){throw this.pendingInteractivePrompt?.promptId===e.prompt.promptId&&(this.pendingInteractivePrompt=null),i}}return this.isSameInteractivePrompt(t,e.prompt)?(!t.callId&&e.prompt.callId&&(t.callId=e.prompt.callId,r.debug("Merged callId into existing interactive prompt",{source:t.source,callId:e.prompt.callId,promptText:t.promptText})),r.debug("Skipping duplicate interactive prompt emission",{existingSource:t.source,candidateSource:e.prompt.source,existingCallId:t.callId,candidateCallId:e.prompt.callId,promptText:e.prompt.promptText}),!1):this.queuedInteractivePrompts.some(i=>this.isSameInteractivePrompt(i.prompt,e.prompt))?(r.debug("Skipping duplicate queued interactive prompt",{candidateSource:e.prompt.source,candidateCallId:e.prompt.callId,promptText:e.prompt.promptText}),!1):(this.queuedInteractivePrompts.push(e),r.info("Queued interactive prompt behind active prompt",{activeCallId:t.callId,queuedCallId:e.prompt.callId,queueLength:this.queuedInteractivePrompts.length}),!1)}async emitInteractivePromptEvent(e){if(!this.sessionState)return;let t=e.content,i=e.metadata,s=!1;this.sessionKey&&(t=c.cryptoService.encryptContent(t,this.sessionKey),i={encrypted:c.cryptoService.encryptMetadata(i,this.sessionKey)},s=!0),await this.appSyncClient.createEvent({sessionId:this.sessionState.sessionId,type:c.EventType.INTERACTIVE_PROMPT,source:c.EventSource.DESKTOP,content:t,metadata:i,promptId:e.prompt.promptId,...s?{isEncrypted:!0}:{}})}async emitNextQueuedInteractivePrompt(){if(this.pendingInteractivePrompt||this.queuedInteractivePrompts.length===0)return;let e=this.queuedInteractivePrompts.shift();if(e){this.pendingInteractivePrompt=e.prompt;try{await this.emitInteractivePromptEvent(e),r.info("Emitted next queued interactive prompt",{callId:e.prompt.callId,queueLength:this.queuedInteractivePrompts.length})}catch(t){this.pendingInteractivePrompt=null,r.error("Failed to emit queued interactive prompt",{error:t}),await this.emitNextQueuedInteractivePrompt()}}}removeQueuedInteractivePromptByCallId(e){let t=this.queuedInteractivePrompts.length,i=this.queuedInteractivePrompts.filter(s=>s.prompt.callId===e);for(let s of i)this.rememberResolvedApprovalDedupeKey(s.prompt.dedupeKey);this.queuedInteractivePrompts=this.queuedInteractivePrompts.filter(s=>s.prompt.callId!==e),this.queuedInteractivePrompts.length!==t&&r.debug("Removed resolved tool call from interactive prompt queue",{callId:e,removed:t-this.queuedInteractivePrompts.length})}async clearResolvedInteractivePrompt(e,t){let i=this.pendingInteractivePrompt;if(i&&(i.callId===e||t!==void 0&&i.dedupeKey===t)){this.rememberResolvedApprovalDedupeKey(i.dedupeKey),this.pendingInteractivePrompt=null,await this.emitNextQueuedInteractivePrompt();return}this.removeQueuedInteractivePromptByCallId(e),t!==void 0&&this.removeQueuedInteractivePromptByDedupeKey(t)}removeQueuedInteractivePromptByDedupeKey(e){let t=this.queuedInteractivePrompts.length,i=this.queuedInteractivePrompts.filter(s=>s.prompt.dedupeKey===e);for(let s of i)this.rememberResolvedApprovalDedupeKey(s.prompt.dedupeKey);this.queuedInteractivePrompts=this.queuedInteractivePrompts.filter(s=>s.prompt.dedupeKey!==e),this.queuedInteractivePrompts.length!==t&&r.debug("Removed resolved deduped tool call from interactive prompt queue",{dedupeKey:e,removed:t-this.queuedInteractivePrompts.length})}mergeCallIdIntoPromptByDedupeKey(e,t){let i=this.pendingInteractivePrompt;if(i?.dedupeKey===e&&!i.callId){i.callId=t,r.debug("Merged callId into active deduped interactive prompt",{source:i.source,callId:t,dedupeKey:e});return}let s=this.queuedInteractivePrompts.find(o=>o.prompt.dedupeKey===e&&!o.prompt.callId);s&&(s.prompt.callId=t,r.debug("Merged callId into queued deduped interactive prompt",{source:s.prompt.source,callId:t,dedupeKey:e}))}isSameInteractivePrompt(e,t){return Date.now()-e.createdAt>Ve?!1:e.callId&&t.callId?e.callId===t.callId:e.dedupeKey&&t.dedupeKey?e.dedupeKey===t.dedupeKey:e.kind===t.kind&&this.normalizePromptDedupeText(e.promptText)===this.normalizePromptDedupeText(t.promptText)}normalizePromptDedupeText(e){return e.replace(/\s+/g," ").trim().toLowerCase()}async startTmuxObserver(){let e=process.env.CODEVIBE_CODEX_TMUX_SESSION;if(!e)return r.debug("Skipping tmux pane observer start - no tmux session in environment"),!0;try{return await this.tmuxPaneObserver.start(e),!0}catch(t){return r.warn("Failed to start tmux pane observer",{tmuxSession:e,error:t}),!1}}async startTmuxObserverWithBeacon(e){try{await this.startTmuxObserver()?await h("daemon_init_step_completed",{step:"tmux_observer_start",path:e}):await h("daemon_init_step_failed",{step:"tmux_observer_start",path:e,error_class:"TmuxObserverStartFailed",error_message:"tmuxPaneObserver.start threw (see plugin log)"})}catch(t){await h("daemon_init_step_failed",{step:"tmux_observer_start",path:e,error_class:t?.name||"Error",error_message:t?.message||String(t)}),r.error("Failed to start tmux observer:",t)}}async tryParseInteractivePromptFromTmux(){try{let e=await this.tmuxPaneObserver.captureSnapshot(),t=(0,I.parseInteractivePrompt)(e);return r.debug("tmux prompt parse result",{parsed:!!t,kind:t?.kind,promptText:t?.promptText,snapshotPreview:this.summarizePromptSnapshot(e)}),{parsedPrompt:t,snapshot:e}}catch(e){return r.debug("tmux prompt parsing unavailable",{error:e}),null}}async tryParsePermissionRequestPromptFromTmux(e){let t=await this.tryParseInteractivePromptFromTmux();if(t?.parsedPrompt&&(!e||e(t.parsedPrompt,t.snapshot)))return t;t?.parsedPrompt&&r.warn("Ignoring parsed PermissionRequest prompt because it does not match the active tool",{promptText:t.parsedPrompt.promptText}),await new Promise(s=>setTimeout(s,250));let i=await this.tryParseInteractivePromptFromTmux();return i?.parsedPrompt&&(!e||e(i.parsedPrompt,i.snapshot))?i:(i?.parsedPrompt&&r.warn("Ignoring retried PermissionRequest prompt because it does not match the active tool",{promptText:i.parsedPrompt.promptText}),i?.snapshot||t?.snapshot?{parsedPrompt:null,snapshot:i?.snapshot||t?.snapshot||""}:null)}buildPromptPresentation(e){return e?{content:e.promptText,promptText:e.promptText,kind:e.kind,options:e.options,submitMap:e.submitMap,instructions:this.buildPromptInstructions(e),requiresFollowUpText:e.requiresFollowUpText}:{content:"Codex is waiting for approval.",promptText:"Codex is waiting for approval.",kind:"yes_no",options:[{number:"1",text:'Yes (sends "y")'},{number:"2",text:'No, tell Codex what to change (sends "n <instructions>")'}],submitMap:{1:"y",2:"n"},instructions:"Reply with 1 to approve, or 2 followed by what to change",requiresFollowUpText:!0}}buildCodexPromptPresentation(e){let t=this.buildPromptPresentation(e),i=this.buildSubmitMapFromCodexOptionHotkeys(t.options);return i?{...t,submitMap:i,instructions:this.buildCodexPermissionInstructions(t.options),requiresFollowUpText:this.optionsRequireFollowUpText(t.options)}:null}buildSubmitMapFromCodexOptionHotkeys(e){let t={};for(let i of e){let s=i.text.match(/\(([^)]+)\)\s*$/)?.[1]?.trim().toLowerCase();if(!s)return null;s==="esc"||s==="escape"?t[i.number]=T:t[i.number]=s}return t}buildCodexPermissionInstructions(e){let t=e.find(n=>/\((?:y|yes)\)\s*$/i.test(n.text)),i=e.find(n=>/\(p\)\s*$/i.test(n.text)),s=e.find(n=>/\((?:esc|escape)\)\s*$/i.test(n.text)),o=[];return t&&o.push(`${t.number} to approve`),i&&o.push(`${i.number} to persist the desktop allow rule`),s&&o.push(`${s.number} followed by what to change`),o.length>0?`Reply with ${o.join(", ")}`:"Reply with the number of the option you want"}optionsRequireFollowUpText(e){return e.some(t=>/what to (?:do differently|change)|instructions/i.test(t.text))}getMostRecentPendingToolCall(){let e=this.approvalDetector.getPendingCalls();return e.length===0?null:e.reduce((t,i)=>i.timestamp>t.timestamp?i:t)}hasActiveOrQueuedPermissionHookPrompt(e){let t=this.pendingInteractivePrompt;return t?.source==="permission_hook"&&t.dedupeKey===e?!0:this.queuedInteractivePrompts.some(i=>i.prompt.source==="permission_hook"&&i.prompt.dedupeKey===e)}hasRecentlyResolvedApprovalDedupeKey(e){return this.pruneResolvedApprovalDedupeKeys(),this.resolvedApprovalDedupeKeys.has(e)}rememberResolvedApprovalDedupeKey(e){e&&(this.pruneResolvedApprovalDedupeKeys(),this.resolvedApprovalDedupeKeys.set(e,Date.now()))}pruneResolvedApprovalDedupeKeys(){let e=Date.now()-Ge;for(let[t,i]of this.resolvedApprovalDedupeKeys.entries())i<e&&this.resolvedApprovalDedupeKeys.delete(t)}buildApprovalDedupeKey(e){let t=e.toolName||"Tool",i=this.firstString(e.toolInput?.command,e.toolInput?.cmd,e.rawInput);if(i)return`command:${this.hashDedupeValue(`${this.mapToolNameForApproval(t)||t}:${i.trim()}`)}`;let s=this.firstString(e.filePath,e.toolInput?.file_path,e.toolInput?.path,e.toolInput?.filePath);if(s)return`file:${this.hashDedupeValue(`${t}:${s}`)}`;let o=this.firstString(e.hint);if(o)return`hint:${this.hashDedupeValue(`${t}:${o}`)}`}hashDedupeValue(e){return X.createHash("sha256").update(e.replace(/\s+/g," ").trim().toLowerCase()).digest("hex").slice(0,16)}buildPermissionPromptId(e,t,i,s){let o=typeof e.turn_id=="string"&&e.turn_id.trim().length>0?e.turn_id.trim():void 0,n=X.createHash("sha256").update(`${t}:${s||this.stringifyToolInput(i)}`).digest("hex").slice(0,12);return`permission-${o||"turn"}-${n}`}buildPermissionRequestHint(e,t,i){let s=this.firstString(t?.command,t?.cmd);if(s)return`Command: ${this.truncateApprovalDetail(s,80)}`;let o=this.extractFilePathFromToolInput(e,t,i);return o?`File: ${o}`:`Tool: ${this.mapToolNameForApproval(e)||e}`}extractFilePathFromToolInput(e,t,i){let s=this.firstString(t?.file_path,t?.path,t?.filePath);if(s)return s;if(e==="apply_patch"&&i){let o=i.match(/\*\*\* (?:Update|Add|Delete) File: (.+)/);if(o)return o[1].trim()}}stringifyToolInput(e){if(e!=null){if(typeof e=="string")return e;try{return JSON.stringify(e)}catch{return String(e)}}}buildApprovalPromptContextFromPendingCall(e){return{toolName:e.name,filePath:e.filePath,diff:e.diff,toolInput:e.parsedInput,rawInput:e.input,hint:e.filePath?`File: ${e.filePath}`:`Tool: ${this.mapToolNameForApproval(e.name)||e.name}`}}buildApprovalPromptContextFromParsedPrompt(e){let t=this.extractShellCommandFromPromptText(e.promptText);return t?{toolName:"Bash",toolInput:{command:t},rawInput:t,hint:`Command: ${this.truncateApprovalDetail(t,80)}`}:null}parsedPromptMatchesApprovalContext(e,t){let i=this.firstString(t.toolInput?.command,t.toolInput?.cmd);if(i){let o=this.extractShellCommandFromPromptText(e.promptText);return!!o&&this.normalizeApprovalCommand(o)===this.normalizeApprovalCommand(i)}let s=this.firstString(t.filePath,t.toolInput?.file_path,t.toolInput?.path,t.toolInput?.filePath);return s?e.promptText.includes(s):!1}normalizeApprovalCommand(e){return e.replace(/\s+/g," ").trim()}extractShellCommandFromPromptText(e){let t=e.split(`
|
|
12
|
+
`).trim();return(0,de.createHash)("sha256").update(i).digest("hex")}};var I=require("@quantiya/codevibe-core");var j=g(require("express")),E=g(require("fs")),V=g(require("path")),G=g(require("os"));var U=class{constructor(){this.assignedPort=0;this.app=(0,j.default)(),this.setupMiddleware(),this.setupRoutes(),this.tmuxSession=process.env.CODEVIBE_CODEX_TMUX_SESSION}getPort(){return this.assignedPort}setupMiddleware(){this.app.use(j.default.json({limit:"1mb"})),this.app.use((e,t,i)=>{r.debug(`${e.method} ${e.path}`,{body:e.body}),i()})}setupRoutes(){this.app.get("/health",(e,t)=>{t.json({success:!0,data:{status:"healthy",uptime:process.uptime()}})}),this.app.post("/event",this.handleEvent.bind(this))}async handleEvent(e,t){try{let i=e.body;if(!i.session_id||!i.hook_event_name){t.status(400).json({success:!1,error:"Missing session_id or hook_event_name"});return}let s=this.transformHookToEvent(i);r.info("Received hook event",{sessionId:i.session_id,hookEvent:i.hook_event_name,type:s.type}),this.eventHandler&&await this.eventHandler(s),t.json({success:!0})}catch(i){r.error("Error handling event:",i),t.status(500).json({success:!1,error:i instanceof Error?i.message:"Unknown error"})}}transformHookToEvent(e){let t={cwd:e.cwd,hook_event_name:e.hook_event_name,...e.metadata||{}},i,s;switch(e.hook_event_name){case"SessionStart":i="NOTIFICATION",s="Session started",t.source=e.source;break;case"UserPromptSubmit":i="USER_PROMPT",s=e.prompt||"";break;case"PreToolUse":i="NOTIFICATION",s="PreToolUse observed",t.tool_name=e.tool_name,t.tool_input=e.tool_input,t.tool_use_id=e.tool_use_id,t.approval_status="observed_pre_tool",t.requires_user_action=!1;break;case"PermissionRequest":i="NOTIFICATION",s="PermissionRequest observed",t.tool_name=e.tool_name,t.tool_input=e.tool_input,t.permission_mode=e.permission_mode,t.turn_id=e.turn_id,t.requires_user_action=!0;break;case"PostToolUse":i="TOOL_USE",s=JSON.stringify({tool_name:e.tool_name,tool_input:e.tool_input,tool_response:e.tool_response}),t.tool_name=e.tool_name;break;case"Stop":i="ASSISTANT_RESPONSE",s=e.last_assistant_message||"";break;default:i="NOTIFICATION",s=`Hook: ${e.hook_event_name}`}return{session_id:e.session_id,hook_event_name:e.hook_event_name,type:i,source:"DESKTOP",content:s,metadata:t}}onEvent(e){this.eventHandler=e}async start(){return new Promise((e,t)=>{try{this.server=this.app.listen(0,"localhost",()=>{let i=this.server.address();this.assignedPort=i.port,r.info(`HTTP API listening on http://localhost:${this.assignedPort}`),this.writePortFile(this.assignedPort),e(this.assignedPort)}),this.server.on("error",i=>{r.error("HTTP server error:",i),t(i)})}catch(i){t(i)}})}writePortFile(e){if(!this.tmuxSession){r.warn("No CODEVIBE_CODEX_TMUX_SESSION set, skipping port file");return}let t=V.join(G.tmpdir(),`codevibe-codex-${this.tmuxSession}.port`);try{E.writeFileSync(t,e.toString()),r.info(`Port file written: ${t} -> ${e}`)}catch(i){r.error(`Failed to write port file: ${t}`,i)}}removePortFile(){if(!this.tmuxSession)return;let e=V.join(G.tmpdir(),`codevibe-codex-${this.tmuxSession}.port`);try{E.existsSync(e)&&(E.unlinkSync(e),r.info(`Port file removed: ${e}`))}catch(t){r.warn(`Failed to remove port file: ${e}`,t)}}async stop(){return this.removePortFile(),new Promise(e=>{this.server?this.server.close(()=>{r.info("HTTP API stopped"),e()}):e()})}};var $=class{constructor(e={}){this.pendingBySession=new Map;this.expiryMs=e.expiryMs??1e4,this.minFuzzyEchoLength=e.minFuzzyEchoLength??16,this.minFuzzyEchoRatio=e.minFuzzyEchoRatio??.35,this.now=e.now??Date.now}track(e,t){let i=this.normalize(t);if(!i)return;let s=this.validEntries(e);s.push({normalized:i,timestamp:this.now()}),this.pendingBySession.set(e,s)}forget(e,t){let i=this.normalize(t);if(!i)return;let s=!1,o=this.validEntries(e).filter(n=>!s&&n.normalized===i?(s=!0,!1):!0);this.replaceEntries(e,o)}consumeIfDuplicate(e,t){let i=this.normalize(t);if(!i)return null;let s=null,o=this.validEntries(e).filter(n=>{if(!s){let a=this.matchType(n.normalized,i);if(a)return s={matchType:a,originalLength:n.normalized.length,echoLength:i.length},!1}return!0});return this.replaceEntries(e,o),s}validEntries(e){let t=this.now()-this.expiryMs;return(this.pendingBySession.get(e)||[]).filter(i=>i.timestamp>=t)}replaceEntries(e,t){t.length>0?this.pendingBySession.set(e,t):this.pendingBySession.delete(e)}matchType(e,t){if(e===t)return"exact";let i=t.length>=this.minFuzzyEchoLength,s=t.length/e.length>=this.minFuzzyEchoRatio;return i&&s&&e.endsWith(t)?"suffix":null}normalize(e){return e.replace(/\s+/g," ").trim()}};var ge=g(require("crypto")),ve=g(require("fs")),ye=g(require("https")),K=g(require("os")),Pe=g(require("path")),Le="G-GS74YEQTB8",Ue="lAfOF6OxRzSQ-NsLBRjhAg",$e="www.google-analytics.com",Ke=`/mp/collect?measurement_id=${Le}&api_secret=${Ue}`,fe=800;function qe(){try{let l=Pe.resolve(__dirname,"..","package.json"),e=ve.readFileSync(l,"utf-8"),t=JSON.parse(e);if(typeof t.version=="string"&&t.version.length>0&&t.version.length<30)return t.version}catch{}return"unknown"}var He=qe();function ze(){let l=typeof process.getuid=="function"?process.getuid():0;return ge.createHash("sha256").update(`${K.hostname()}-${l}`).digest("hex").substring(0,36)}function We(l){if(!l)return"";let e=K.homedir(),t=l.replace(/[\n\r\t]/g," ");if(e&&e.length>0){let i=e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");t=t.replace(new RegExp(i,"g"),"~")}return t=t.replace(/\/Users\/[^/\s"'`]+/g,"/Users/<user>").replace(/\/home\/[^/\s"'`]+/g,"/home/<user>").replace(/[^\x20-\x7E]/g,""),t.trim().substring(0,100)}function h(l,e){return new Promise(t=>{let i,s=!1,o=()=>{s||(s=!0,clearTimeout(n),t())},n=setTimeout(()=>{try{i?.destroy()}catch{}o()},fe);typeof n.unref=="function"&&n.unref();try{let a={...e};typeof a.error_message=="string"&&(a.error_message=We(a.error_message));let p=JSON.stringify({client_id:ze(),events:[{name:l,params:{agent:"codex",plugin_version:He,platform:process.platform,source:process.env.CODEVIBE_TELEMETRY_SOURCE||"production",...a}}]});i=ye.request({hostname:$e,path:Ke,method:"POST",headers:{"Content-Type":"application/json"},timeout:fe},c=>{c.resume(),c.on("end",o),c.on("close",o),c.on("error",o)}),i.on("error",o),i.on("timeout",()=>{try{i?.destroy()}catch{}o()}),i.on("close",o),i.write(p),i.end()}catch{o()}})}var Be=(0,Ie.promisify)(we.exec),je="/quit",Se="CODEVIBE_CODEX_TMUX_SESSION",Ve=600*1e3,Ge=30*1e3;async function Xe(l,e){let t=async(i,s)=>{try{await Be(i)}catch(o){r.warn("tmux send-keys failed during self-terminate",{sessionName:l,label:s,error:String(o)})}};await t(`tmux send-keys -t "${l}" C-c`,"ctrl-c"),await new Promise(i=>setTimeout(i,200)),await t(`tmux send-keys -t "${l}" -l "${e}"`,"quit-text"),await new Promise(i=>setTimeout(i,500)),await t(`tmux send-keys -t "${l}" Enter`,"enter")}var q=class{constructor(){this.sessionState=null;this.unsubscribe=null;this.sessionKey=null;this.pendingInteractivePrompt=null;this.queuedInteractivePrompts=[];this.isInitializingSession=!1;this.bufferedLogEntries=[];this.hooksActive=!1;this.hooksCompatibilityNoticeSent=!1;this.resolvedApprovalDedupeKeys=new Map;this.subscribedSessionId=null;this.mobilePromptDeduper=new $({expiryMs:1e4});this.launchSessionInitPromise=null;this.sessionStartedInitPromise=null;this.httpApi=new U,this.sessionWatcher=new F,this.approvalDetector=new M,this.promptResponder=new N,this.tmuxPaneObserver=new D}async start(){r.info("Starting CodeVibe Codex companion server",{environment:(0,d.getEnvironment)()}),this.appSyncClient=new d.AppSyncClient,await this.appSyncClient.authenticateWithStoredTokens()||(r.error('Authentication failed. Run "codevibe-codex login" first.'),console.error('Not authenticated. Run "codevibe-codex login" to sign in.'),process.exit(1)),r.info("Authenticated successfully",{userId:this.appSyncClient.getCurrentUserId(),email:this.appSyncClient.getCurrentUserEmail()}),await(0,d.registerDeviceEncryptionKey)(this.appSyncClient,r),(0,d.startDeviceKeyWatcher)(this.appSyncClient,r);try{let i=await this.appSyncClient.sweepOrphanSessions({agentType:"CODEX"});i>0&&r.info("Orphan sweep: marked stale Codex sessions INACTIVE",{swept:i})}catch(i){r.warn("Orphan sweep failed, continuing startup",{error:i instanceof Error?i.message:String(i)})}this.httpApi.onEvent(this.handleEventFromHook.bind(this));let t=await this.httpApi.start();r.info("HTTP API started for hooks",{port:t}),await this.createLaunchSession(),this.setupEventHandlers(),this.sessionWatcher.start(),r.info("CodeVibe Codex companion server started")}async createLaunchSession(){let e=process.env.CODEVIBE_CODEX_TMUX_SESSION;if(!e){r.warn("No CODEVIBE_CODEX_TMUX_SESSION \u2014 skipping launch session");return}let t;this.launchSessionInitPromise=new Promise(i=>{t=i});try{let i=process.env.CODEX_WORKING_DIRECTORY||process.cwd(),s=this.generateSessionId(e),o=this.appSyncClient.getCurrentUserId();r.info("Creating launch session",{sessionId:s,projectPath:i});let n=!1;try{let a=await(0,d.resumeOrCreateSession)({sessionId:s,userId:o,agentType:d.AgentType.CODEX,projectPath:i,metadata:{launchSession:!0}},this.appSyncClient,r);this.sessionKey=a.sessionKey,n=!0,this.sessionState={sessionId:s,userId:o,projectPath:i,cwd:i,createdAt:new Date,subscriptionActive:!1,metadata:{launchSession:!0},codexSessionId:e,codexLogFile:void 0},await h("daemon_init_step_completed",{step:"session_resume_or_create",path:"launch_session"})}catch(a){await h("daemon_init_step_failed",{step:"session_resume_or_create",path:"launch_session",error_class:a?.name||"Error",error_message:a?.message||String(a)}),r.error("Failed to create/resume launch session (non-fatal)",{error:a})}if(!n)return;await h("daemon_init_step_completed",{step:"session_state_set",path:"launch_session"});try{this.subscribeToMobileEvents(s)?await h("daemon_init_step_completed",{step:"subscribe_mobile_events",path:"launch_session"}):await h("daemon_init_step_failed",{step:"subscribe_mobile_events",path:"launch_session",error_class:"SubscriptionSetupFailed",error_message:"subscribeToMobileEvents returned false"})}catch(a){await h("daemon_init_step_failed",{step:"subscribe_mobile_events",path:"launch_session",error_class:a?.name||"Error",error_message:a?.message||String(a)}),r.error("Failed to subscribe to mobile events for launch session",{error:a})}try{this.appSyncClient.startHeartbeat(s),await h("daemon_init_step_completed",{step:"heartbeat_start",path:"launch_session"})}catch(a){await h("daemon_init_step_failed",{step:"heartbeat_start",path:"launch_session",error_class:a?.name||"Error",error_message:a?.message||String(a)}),r.error("Failed to start heartbeat for launch session",{error:a})}this.startMobileEndWatcher(s),r.info("Launch session created",{sessionId:s})}finally{t()}}async handleEventFromHook(e){let{session_id:t,hook_event_name:i,type:s,content:o,metadata:n}=e;if(this.hooksActive=!0,r.info("[Hooks] Received event",{sessionId:t,hookEvent:i,type:s,contentLength:o?.length}),i==="SessionStart"){if(this.launchSessionInitPromise&&await this.launchSessionInitPromise,this.sessionState)r.info("[Hooks] SessionStart \u2014 launch session already exists, updating codexSessionId",{existingSessionId:this.sessionState.sessionId,codexSessionId:t}),this.sessionState.codexSessionId=t,this.sessionState.metadata={...this.sessionState.metadata,codexSessionId:t,cliVersion:n?.model||"unknown",modelProvider:n?.model||"unknown",launchSession:void 0},this.appSyncClient.updateSession({sessionId:this.sessionState.sessionId,metadata:this.sessionState.metadata}).catch(p=>r.warn("Failed to update session metadata",{error:p})),await this.startTmuxObserverWithBeacon("session_start_existing");else{let p={id:t,timestamp:new Date().toISOString(),cwd:n?.cwd||process.cwd(),originator:"hook",cli_version:n?.model||"unknown",instructions:null,source:n?.source||"startup",model_provider:n?.model||"unknown"};await this.ensureSessionStarted(p)}return}if(!this.sessionState){r.warn("[Hooks] Session not initialized, buffering event",{hook_event_name:i});return}let a=this.sessionState.sessionId;if(s==="USER_PROMPT"&&o){let p=this.consumeRecentMobilePrompt(a,o);if(p){r.info("[Hooks] Skipping duplicate USER_PROMPT from mobile",{sessionId:a,matchType:p.matchType,originalLength:p.originalLength,echoLength:p.echoLength});return}}if(i==="PreToolUse"){r.debug("[Hooks] PreToolUse observed; AppSync emission suppressed",{toolName:n?.tool_name||"unknown",sessionId:a});return}if(i==="PermissionRequest"){await this.handlePermissionRequestHook(e);return}if(i==="PostToolUse"){let p=o,c=n,u=!1;this.sessionKey&&(p=d.cryptoService.encryptContent(o,this.sessionKey),n&&(c={encrypted:d.cryptoService.encryptMetadata(n,this.sessionKey)}),u=!0),await this.appSyncClient.createEvent({sessionId:a,type:d.EventType.TOOL_USE,source:d.EventSource.DESKTOP,content:p,metadata:c,isEncrypted:u});return}if(s==="ASSISTANT_RESPONSE"||s==="USER_PROMPT"){let p=o,c=!1;this.sessionKey&&o&&(p=d.cryptoService.encryptContent(o,this.sessionKey),c=!0),await this.appSyncClient.createEvent({sessionId:a,type:s==="ASSISTANT_RESPONSE"?d.EventType.ASSISTANT_RESPONSE:d.EventType.USER_PROMPT,source:d.EventSource.DESKTOP,content:p,isEncrypted:c});return}}mapToolName(e){return{shell_command:"Bash",shell:"Bash",apply_patch:"Edit",create_file:"Write",read_file:"Read"}[e]||e}trackMobilePrompt(e,t){this.mobilePromptDeduper.track(e,t),r.debug("Tracking mobile prompt for USER_PROMPT echo deduplication",{sessionId:e,promptLength:t.trim().length})}forgetMobilePrompt(e,t){this.mobilePromptDeduper.forget(e,t)}consumeRecentMobilePrompt(e,t){return this.mobilePromptDeduper.consumeIfDuplicate(e,t)}setupEventHandlers(){this.sessionWatcher.on("session-started",async e=>{if(this.launchSessionInitPromise&&await this.launchSessionInitPromise,this.sessionState){r.info("[JSONL] Session already active, skipping",{currentSessionId:this.sessionState.sessionId,codexSessionId:e.id});return}await this.ensureSessionStarted(e)}),this.sessionWatcher.on("log-entry",async e=>{await this.handleLogEntry(e)}),this.approvalDetector.on("approval-pending",async e=>{await this.handleApprovalPending(e)}),this.tmuxPaneObserver.on("prompt-candidate",async e=>{await this.handleTmuxPromptCandidate(e.snapshot)}),this.tmuxPaneObserver.on("observer-error",e=>{r.debug("Tmux pane observer error",{error:e})}),this.sessionWatcher.on("error",e=>{r.error("Session watcher error:",e)})}async ensureSessionStarted(e){if(this.launchSessionInitPromise&&await this.launchSessionInitPromise,this.sessionState)return;if(this.sessionStartedInitPromise){try{await this.sessionStartedInitPromise}catch{}return}let t=this.handleSessionStarted(e);this.sessionStartedInitPromise=t.catch(()=>{});try{await t}finally{this.sessionStartedInitPromise=null}}async handleSessionStarted(e){r.info("Handling new Codex session",{codexSessionId:e.id}),this.isInitializingSession=!0,this.bufferedLogEntries=[],this.sessionState&&await this.endActiveSession("new-codex-session-started");let t=process.env.CODEX_WORKING_DIRECTORY||e.cwd||process.cwd(),i=this.generateSessionId(e.id),s=this.appSyncClient.getCurrentUserId(),o={codexSessionId:e.id,cliVersion:e.cli_version,modelProvider:e.model_provider};try{let n=await(0,d.resumeOrCreateSession)({sessionId:i,userId:s,agentType:d.AgentType.CODEX,projectPath:t,metadata:o},this.appSyncClient,r);this.sessionKey=n.sessionKey,await h("daemon_init_step_completed",{step:"session_resume_or_create",path:"session_started"})}catch(n){throw this.isInitializingSession=!1,await h("daemon_init_step_failed",{step:"session_resume_or_create",path:"session_started",error_class:n?.name||"Error",error_message:n?.message||String(n)}),r.error("Failed to create/resume session:",n),n}try{this.sessionState={sessionId:i,userId:s,projectPath:t,cwd:e.cwd,createdAt:new Date,subscriptionActive:!1,metadata:o,codexSessionId:e.id,codexLogFile:this.sessionWatcher.getActiveLogFile()||void 0},await h("daemon_init_step_completed",{step:"session_state_set",path:"session_started"})}catch(n){await h("daemon_init_step_failed",{step:"session_state_set",path:"session_started",error_class:n?.name||"Error",error_message:n?.message||String(n)}),r.error("Failed to set session state:",n)}try{this.subscribeToMobileEvents(i)?await h("daemon_init_step_completed",{step:"subscribe_mobile_events",path:"session_started"}):await h("daemon_init_step_failed",{step:"subscribe_mobile_events",path:"session_started",error_class:"SubscriptionSetupFailed",error_message:"subscribeToMobileEvents returned false"})}catch(n){await h("daemon_init_step_failed",{step:"subscribe_mobile_events",path:"session_started",error_class:n?.name||"Error",error_message:n?.message||String(n)}),r.error("Failed to subscribe to mobile events:",n)}try{this.appSyncClient.startHeartbeat(i),await h("daemon_init_step_completed",{step:"heartbeat_start",path:"session_started"})}catch(n){await h("daemon_init_step_failed",{step:"heartbeat_start",path:"session_started",error_class:n?.name||"Error",error_message:n?.message||String(n)}),r.error("Failed to start heartbeat:",n)}this.startMobileEndWatcher(i);try{await this.flushBufferedLogEntries(),await h("daemon_init_step_completed",{step:"flush_buffered_entries",path:"session_started"})}catch(n){await h("daemon_init_step_failed",{step:"flush_buffered_entries",path:"session_started",error_class:n?.name||"Error",error_message:n?.message||String(n)}),r.error("Failed to flush buffered log entries:",n),this.bufferedLogEntries=[]}await this.startTmuxObserverWithBeacon("session_started"),this.isInitializingSession=!1}async flushBufferedLogEntries(){if(this.bufferedLogEntries.length===0)return;let e=this.bufferedLogEntries;this.bufferedLogEntries=[],r.info("Flushing buffered log entries after session initialization",{count:e.length,sessionId:this.sessionState?.sessionId});for(let t of e)await this.handleLogEntry(t)}async handleLogEntry(e){if(!this.sessionState){if(this.isInitializingSession){this.bufferedLogEntries.push(e),r.debug("Buffering log entry until session initialization completes",{type:e.type,bufferedCount:this.bufferedLogEntries.length});return}r.warn("Received log entry but no active session");return}if(e.type==="response_item"&&e.payload){let n=e.payload.type;if(n==="function_call"||n==="custom_tool_call")this.approvalDetector.onToolCallStart(e.payload.call_id,e.payload.name,e.payload.arguments||e.payload.input||"");else if(n==="function_call_output"||n==="custom_tool_call_output"){let a=this.approvalDetector.getPendingCalls().find(c=>c.callId===e.payload.call_id),p=a?this.buildApprovalDedupeKey(this.buildApprovalPromptContextFromPendingCall(a)):void 0;this.approvalDetector.onToolCallComplete(e.payload.call_id),await this.clearResolvedInteractivePrompt(e.payload.call_id,p)}}let t=re(e,this.sessionState.sessionId);if(!t)return;let i=e.payload?.type,s=i==="function_call"||i==="function_call_output",o=t.type===d.EventType.USER_PROMPT||t.type===d.EventType.ASSISTANT_RESPONSE||s&&(t.type===d.EventType.TOOL_USE||t.type===d.EventType.INTERACTIVE_PROMPT);if(!this.hooksActive&&o&&await this.emitHooksCompatibilityModeNotice(),this.hooksActive){if(t.type===d.EventType.USER_PROMPT||t.type===d.EventType.ASSISTANT_RESPONSE){r.debug("[JSONL] Skipping \u2014 hooks deliver this event type",{type:t.type});return}if(s&&(t.type===d.EventType.TOOL_USE||t.type===d.EventType.INTERACTIVE_PROMPT)){r.debug("[JSONL] Skipping function_call \u2014 hooks deliver this",{type:t.type,tool:e.payload?.name});return}}if(t.type===d.EventType.USER_PROMPT&&t.source===d.EventSource.DESKTOP){let n=this.consumeRecentMobilePrompt(this.sessionState.sessionId,t.content);if(n){r.info("[JSONL] Skipping duplicate USER_PROMPT from mobile",{sessionId:this.sessionState.sessionId,matchType:n.matchType,originalLength:n.originalLength,echoLength:n.echoLength});return}}try{if(this.sessionKey){if(t.content=d.cryptoService.encryptContent(t.content,this.sessionKey),t.metadata){let n=d.cryptoService.encryptMetadata(t.metadata,this.sessionKey);t.metadata={encrypted:n}}t.isEncrypted=!0,r.debug("Event encrypted",{type:t.type})}await this.appSyncClient.createEvent(t),r.debug("Event synced to backend",{type:t.type,encrypted:!!this.sessionKey})}catch(n){r.error("Failed to sync event:",n)}}async handlePermissionRequestHook(e){if(!this.sessionState)return;let t=e.metadata||{},i=typeof t.tool_name=="string"?t.tool_name:"Tool",s=t.tool_input,o=this.stringifyToolInput(s),n={toolName:i,toolInput:s,rawInput:o,filePath:this.extractFilePathFromToolInput(i,s,o),diff:i==="apply_patch"?o:void 0,hint:this.buildPermissionRequestHint(i,s,o)},a=this.buildToolDetailsForInteractivePrompt(n),p=a.tool_name||this.mapToolNameForApproval(i),c=a.tool_input||this.buildFallbackToolInput(n),u=!!(p&&c),v=(await this.tryParsePermissionRequestPromptFromTmux(Ee=>this.parsedPromptMatchesApprovalContext(Ee,n)))?.parsedPrompt??null;if(!v){r.warn("[Hooks] Suppressing PermissionRequest mobile prompt because exact Codex options are unavailable",{sessionId:this.sessionState.sessionId,toolName:i,hint:n.hint});return}let P=this.buildApprovalDedupeKey({toolName:i,toolInput:s,filePath:n.filePath,rawInput:o,hint:n.hint});if(P&&this.hasRecentlyResolvedApprovalDedupeKey(P)){r.info("[Hooks] Skipping PermissionRequest prompt; same approval was already answered recently",{sessionId:this.sessionState.sessionId,toolName:i,dedupeKey:P});return}let S=this.buildCodexPromptPresentation(v);if(!S){r.warn("[Hooks] Suppressing PermissionRequest mobile prompt because Codex option hotkeys are unavailable",{sessionId:this.sessionState.sessionId,toolName:i,hint:n.hint});return}let R=this.buildPermissionPromptId(t,i,s,o),A=this.buildApprovalPromptContent(S.content,{toolName:p,toolInput:c,hint:n.hint,filePath:n.filePath}),w={promptId:R,kind:S.kind,options:S.options,submitMap:S.submitMap,promptText:A,createdAt:Date.now(),source:"permission_hook",dedupeKey:P,requiresFollowUpText:S.requiresFollowUpText},Q={isApprovalHint:!0,toolName:i,toolInput:s,hint:n.hint,filePath:n.filePath,diff:n.diff,rawInput:o,tool_name:p,tool_input:c,has_details:u,options:S.options,instructions:S.instructions,prompt_source:v?"permission_hook_tmux":"permission_hook",permission_mode:t.permission_mode,turn_id:t.turn_id,dedupe_key:P};r.info("[Hooks] Sending PermissionRequest interactive prompt",{sessionId:this.sessionState.sessionId,toolName:i,promptId:R,promptSource:Q.prompt_source,dedupeKey:P}),await this.enqueueOrEmitInteractivePrompt({prompt:w,content:A,metadata:Q})}async emitHooksCompatibilityModeNotice(){if(this.hooksCompatibilityNoticeSent||!this.sessionState)return;this.hooksCompatibilityNoticeSent=!0;let e="Codex hooks are not active. CodeVibe is using compatibility mode: mobile approvals still mirror the desktop prompt, but timeline events may be delayed or incomplete. Open Codex /hooks and trust/enable CodeVibe hooks for full fidelity.",t=e,i={compatibility_mode:!0,reason:"codex_hooks_inactive",action:"trust_enable_codevibe_hooks"},s=!1;this.sessionKey&&(t=d.cryptoService.encryptContent(e,this.sessionKey),i={encrypted:d.cryptoService.encryptMetadata(i,this.sessionKey)},s=!0),r.warn("Codex hooks inactive; running in compatibility mode",{sessionId:this.sessionState.sessionId});try{await this.appSyncClient.createEvent({sessionId:this.sessionState.sessionId,type:d.EventType.NOTIFICATION,source:d.EventSource.DESKTOP,content:t,metadata:i,...s?{isEncrypted:!0}:{}})}catch(o){r.warn("Failed to emit hooks compatibility mode notification",{error:o})}}async handleApprovalPending(e){if(!this.sessionState)return;let t=this.buildApprovalDedupeKey(e);if(t&&this.hasRecentlyResolvedApprovalDedupeKey(t)){r.info("Skipping heuristic approval prompt; same approval was already answered recently",{callId:e.callId,toolName:e.toolName,dedupeKey:t});return}if(t&&this.hasActiveOrQueuedPermissionHookPrompt(t)){this.mergeCallIdIntoPromptByDedupeKey(t,e.callId),r.info("Skipping heuristic approval prompt; PermissionRequest hook already emitted it",{callId:e.callId,toolName:e.toolName,dedupeKey:t});return}r.info("Sending approval pending interactive prompt",e);try{let i=await this.tryParseInteractivePromptFromTmux(),s=i?.parsedPrompt??null,o=this.buildToolDetailsForInteractivePrompt(e,i?.snapshot),n=o.tool_name||this.mapToolNameForApproval(e.toolName),a=o.tool_input||this.buildFallbackToolInput(e),p=!!(n&&a);if(!s){r.warn("Suppressing heuristic approval prompt because exact Codex options are unavailable",{callId:e.callId,toolName:e.toolName,hint:e.hint});return}if(!this.parsedPromptMatchesApprovalContext(s,e)){r.warn("Suppressing heuristic approval prompt because parsed tmux prompt does not match the active tool",{callId:e.callId,toolName:e.toolName,hint:e.hint,parsedPromptText:s.promptText});return}let c=this.buildCodexPromptPresentation(s);if(!c){r.warn("Suppressing heuristic approval prompt because Codex option hotkeys are unavailable",{callId:e.callId,toolName:e.toolName,hint:e.hint});return}let u=c.options,m=this.buildApprovalPromptContent(c.content,{toolName:n,toolInput:a,hint:e.hint,filePath:e.filePath}),v={promptId:e.callId,callId:e.callId,kind:c.kind,options:u,submitMap:c.submitMap,promptText:c.promptText,createdAt:Date.now(),source:s?"tmux":"heuristic",dedupeKey:t,requiresFollowUpText:c.requiresFollowUpText},P={isApprovalHint:!0,toolName:e.toolName,toolInput:e.toolInput,hint:e.hint,callId:e.callId,filePath:e.filePath,diff:e.diff,rawInput:e.rawInput,tool_name:n,tool_input:a,has_details:p,options:u,instructions:c.instructions,prompt_source:s?"tmux":"heuristic",dedupe_key:t};r.debug("Interactive prompt (pre-encryption)",{sessionId:this.sessionState.sessionId,callId:e.callId,contentPreview:m.substring(0,200),toolDetails:o,metadata:P}),await this.enqueueOrEmitInteractivePrompt({prompt:v,content:m,metadata:P})}catch(i){r.error("Failed to send approval interactive prompt:",i)}}async handleTmuxPromptCandidate(e){if(!this.sessionState)return;let t=(0,I.parseInteractivePrompt)(e);if(!t)return;let i=this.buildCodexPromptPresentation(t);if(!i){r.warn("Skipping tmux-detected prompt because Codex option hotkeys are unavailable",{parsedPromptText:t.promptText});return}let s=this.getMostRecentPendingToolCall();s||(await new Promise(w=>setTimeout(w,500)),s=this.getMostRecentPendingToolCall());let o=s?this.buildApprovalPromptContextFromPendingCall(s):null;if(o&&!this.parsedPromptMatchesApprovalContext(t,o)){r.warn("Skipping tmux-detected prompt because parsed prompt does not match pending tool call",{callId:s?.callId,toolName:s?.name,parsedPromptText:t.promptText});return}let n=o?null:this.buildApprovalPromptContextFromParsedPrompt(t),a=o||n;if(!a){r.warn("Skipping tmux-detected prompt because no tool context could be derived from the parsed prompt",{parsedPromptText:t.promptText});return}let p=a?this.buildApprovalDedupeKey(a):void 0;if(p&&this.hasRecentlyResolvedApprovalDedupeKey(p)){r.info("Skipping tmux-detected prompt; same approval was already answered recently",{promptText:t.promptText,dedupeKey:p});return}if(p&&this.hasActiveOrQueuedPermissionHookPrompt(p)){s?.callId&&this.mergeCallIdIntoPromptByDedupeKey(p,s.callId),r.info("Skipping tmux-detected prompt; PermissionRequest hook already emitted it",{promptText:t.promptText,dedupeKey:p});return}let c=a?this.buildToolDetailsForInteractivePrompt(a,e):{},u=c.tool_name||this.mapToolNameForApproval(s?.name),m=c.tool_input||(a?this.buildFallbackToolInput(a):void 0),v=!!(u&&m),S={promptId:s?.callId||(0,_e.v4)(),callId:s?.callId,kind:i.kind,options:i.options,submitMap:i.submitMap,promptText:i.promptText,createdAt:Date.now(),source:"tmux",dedupeKey:p,requiresFollowUpText:i.requiresFollowUpText},R={options:i.options,instructions:i.instructions,prompt_source:"tmux_live",tool_name:u,tool_input:m,has_details:v,dedupe_key:p},A=this.buildApprovalPromptContent(i.content,{toolName:u,toolInput:m,hint:a?.hint,filePath:a?.filePath});try{await this.enqueueOrEmitInteractivePrompt({prompt:S,content:A,metadata:R})&&r.info("Sent tmux-detected interactive prompt",{sessionId:this.sessionState.sessionId,promptText:t.promptText,kind:t.kind})}catch(w){r.error("Failed to send tmux-detected interactive prompt",{error:w})}}async enqueueOrEmitInteractivePrompt(e){if(e.prompt.dedupeKey&&this.hasRecentlyResolvedApprovalDedupeKey(e.prompt.dedupeKey))return r.debug("Skipping interactive prompt emission; same approval was already answered recently",{source:e.prompt.source,callId:e.prompt.callId,dedupeKey:e.prompt.dedupeKey}),!1;let t=this.pendingInteractivePrompt;if(!t){this.pendingInteractivePrompt=e.prompt;try{return await this.emitInteractivePromptEvent(e),!0}catch(i){throw this.pendingInteractivePrompt?.promptId===e.prompt.promptId&&(this.pendingInteractivePrompt=null),i}}return this.isSameInteractivePrompt(t,e.prompt)?(!t.callId&&e.prompt.callId&&(t.callId=e.prompt.callId,r.debug("Merged callId into existing interactive prompt",{source:t.source,callId:e.prompt.callId,promptText:t.promptText})),r.debug("Skipping duplicate interactive prompt emission",{existingSource:t.source,candidateSource:e.prompt.source,existingCallId:t.callId,candidateCallId:e.prompt.callId,promptText:e.prompt.promptText}),!1):this.queuedInteractivePrompts.some(i=>this.isSameInteractivePrompt(i.prompt,e.prompt))?(r.debug("Skipping duplicate queued interactive prompt",{candidateSource:e.prompt.source,candidateCallId:e.prompt.callId,promptText:e.prompt.promptText}),!1):(this.queuedInteractivePrompts.push(e),r.info("Queued interactive prompt behind active prompt",{activeCallId:t.callId,queuedCallId:e.prompt.callId,queueLength:this.queuedInteractivePrompts.length}),!1)}async emitInteractivePromptEvent(e){if(!this.sessionState)return;let t=e.content,i=e.metadata,s=!1;this.sessionKey&&(t=d.cryptoService.encryptContent(t,this.sessionKey),i={encrypted:d.cryptoService.encryptMetadata(i,this.sessionKey)},s=!0),await this.appSyncClient.createEvent({sessionId:this.sessionState.sessionId,type:d.EventType.INTERACTIVE_PROMPT,source:d.EventSource.DESKTOP,content:t,metadata:i,promptId:e.prompt.promptId,...s?{isEncrypted:!0}:{}})}async emitNextQueuedInteractivePrompt(){if(this.pendingInteractivePrompt||this.queuedInteractivePrompts.length===0)return;let e=this.queuedInteractivePrompts.shift();if(e){this.pendingInteractivePrompt=e.prompt;try{await this.emitInteractivePromptEvent(e),r.info("Emitted next queued interactive prompt",{callId:e.prompt.callId,queueLength:this.queuedInteractivePrompts.length})}catch(t){this.pendingInteractivePrompt=null,r.error("Failed to emit queued interactive prompt",{error:t}),await this.emitNextQueuedInteractivePrompt()}}}removeQueuedInteractivePromptByCallId(e){let t=this.queuedInteractivePrompts.length,i=this.queuedInteractivePrompts.filter(s=>s.prompt.callId===e);for(let s of i)this.rememberResolvedApprovalDedupeKey(s.prompt.dedupeKey);this.queuedInteractivePrompts=this.queuedInteractivePrompts.filter(s=>s.prompt.callId!==e),this.queuedInteractivePrompts.length!==t&&r.debug("Removed resolved tool call from interactive prompt queue",{callId:e,removed:t-this.queuedInteractivePrompts.length})}async clearResolvedInteractivePrompt(e,t){let i=this.pendingInteractivePrompt;if(i&&(i.callId===e||t!==void 0&&i.dedupeKey===t)){this.rememberResolvedApprovalDedupeKey(i.dedupeKey),this.pendingInteractivePrompt=null,await this.emitNextQueuedInteractivePrompt();return}this.removeQueuedInteractivePromptByCallId(e),t!==void 0&&this.removeQueuedInteractivePromptByDedupeKey(t)}removeQueuedInteractivePromptByDedupeKey(e){let t=this.queuedInteractivePrompts.length,i=this.queuedInteractivePrompts.filter(s=>s.prompt.dedupeKey===e);for(let s of i)this.rememberResolvedApprovalDedupeKey(s.prompt.dedupeKey);this.queuedInteractivePrompts=this.queuedInteractivePrompts.filter(s=>s.prompt.dedupeKey!==e),this.queuedInteractivePrompts.length!==t&&r.debug("Removed resolved deduped tool call from interactive prompt queue",{dedupeKey:e,removed:t-this.queuedInteractivePrompts.length})}mergeCallIdIntoPromptByDedupeKey(e,t){let i=this.pendingInteractivePrompt;if(i?.dedupeKey===e&&!i.callId){i.callId=t,r.debug("Merged callId into active deduped interactive prompt",{source:i.source,callId:t,dedupeKey:e});return}let s=this.queuedInteractivePrompts.find(o=>o.prompt.dedupeKey===e&&!o.prompt.callId);s&&(s.prompt.callId=t,r.debug("Merged callId into queued deduped interactive prompt",{source:s.prompt.source,callId:t,dedupeKey:e}))}isSameInteractivePrompt(e,t){return Date.now()-e.createdAt>Ve?!1:e.callId&&t.callId?e.callId===t.callId:e.dedupeKey&&t.dedupeKey?e.dedupeKey===t.dedupeKey:e.kind===t.kind&&this.normalizePromptDedupeText(e.promptText)===this.normalizePromptDedupeText(t.promptText)}normalizePromptDedupeText(e){return e.replace(/\s+/g," ").trim().toLowerCase()}async startTmuxObserver(){let e=process.env.CODEVIBE_CODEX_TMUX_SESSION;if(!e)return r.debug("Skipping tmux pane observer start - no tmux session in environment"),!0;try{return await this.tmuxPaneObserver.start(e),!0}catch(t){return r.warn("Failed to start tmux pane observer",{tmuxSession:e,error:t}),!1}}async startTmuxObserverWithBeacon(e){try{await this.startTmuxObserver()?await h("daemon_init_step_completed",{step:"tmux_observer_start",path:e}):await h("daemon_init_step_failed",{step:"tmux_observer_start",path:e,error_class:"TmuxObserverStartFailed",error_message:"tmuxPaneObserver.start threw (see plugin log)"})}catch(t){await h("daemon_init_step_failed",{step:"tmux_observer_start",path:e,error_class:t?.name||"Error",error_message:t?.message||String(t)}),r.error("Failed to start tmux observer:",t)}}async tryParseInteractivePromptFromTmux(){try{let e=await this.tmuxPaneObserver.captureSnapshot(),t=(0,I.parseInteractivePrompt)(e);return r.debug("tmux prompt parse result",{parsed:!!t,kind:t?.kind,promptText:t?.promptText,snapshotPreview:this.summarizePromptSnapshot(e)}),{parsedPrompt:t,snapshot:e}}catch(e){return r.debug("tmux prompt parsing unavailable",{error:e}),null}}async tryParsePermissionRequestPromptFromTmux(e){let t=await this.tryParseInteractivePromptFromTmux();if(t?.parsedPrompt&&(!e||e(t.parsedPrompt,t.snapshot)))return t;t?.parsedPrompt&&r.warn("Ignoring parsed PermissionRequest prompt because it does not match the active tool",{promptText:t.parsedPrompt.promptText}),await new Promise(s=>setTimeout(s,250));let i=await this.tryParseInteractivePromptFromTmux();return i?.parsedPrompt&&(!e||e(i.parsedPrompt,i.snapshot))?i:(i?.parsedPrompt&&r.warn("Ignoring retried PermissionRequest prompt because it does not match the active tool",{promptText:i.parsedPrompt.promptText}),i?.snapshot||t?.snapshot?{parsedPrompt:null,snapshot:i?.snapshot||t?.snapshot||""}:null)}buildPromptPresentation(e){return e?{content:e.promptText,promptText:e.promptText,kind:e.kind,options:e.options,submitMap:e.submitMap,instructions:this.buildPromptInstructions(e),requiresFollowUpText:e.requiresFollowUpText}:{content:"Codex is waiting for approval.",promptText:"Codex is waiting for approval.",kind:"yes_no",options:[{number:"1",text:'Yes (sends "y")'},{number:"2",text:'No, tell Codex what to change (sends "n <instructions>")'}],submitMap:{1:"y",2:"n"},instructions:"Reply with 1 to approve, or 2 followed by what to change",requiresFollowUpText:!0}}buildCodexPromptPresentation(e){let t=this.buildPromptPresentation(e),i=this.buildSubmitMapFromCodexOptionHotkeys(t.options);return i?{...t,submitMap:i,instructions:this.buildCodexPermissionInstructions(t.options),requiresFollowUpText:this.optionsRequireFollowUpText(t.options)}:null}buildSubmitMapFromCodexOptionHotkeys(e){let t={};for(let i of e){let s=i.text.match(/\(([^)]+)\)\s*$/)?.[1]?.trim().toLowerCase();if(!s)return null;s==="esc"||s==="escape"?t[i.number]=T:t[i.number]=s}return t}buildCodexPermissionInstructions(e){let t=e.find(n=>/\((?:y|yes)\)\s*$/i.test(n.text)),i=e.find(n=>/\(p\)\s*$/i.test(n.text)),s=e.find(n=>/\((?:esc|escape)\)\s*$/i.test(n.text)),o=[];return t&&o.push(`${t.number} to approve`),i&&o.push(`${i.number} to persist the desktop allow rule`),s&&o.push(`${s.number} followed by what to change`),o.length>0?`Reply with ${o.join(", ")}`:"Reply with the number of the option you want"}optionsRequireFollowUpText(e){return e.some(t=>/what to (?:do differently|change)|instructions/i.test(t.text))}getMostRecentPendingToolCall(){let e=this.approvalDetector.getPendingCalls();return e.length===0?null:e.reduce((t,i)=>i.timestamp>t.timestamp?i:t)}hasActiveOrQueuedPermissionHookPrompt(e){let t=this.pendingInteractivePrompt;return t?.source==="permission_hook"&&t.dedupeKey===e?!0:this.queuedInteractivePrompts.some(i=>i.prompt.source==="permission_hook"&&i.prompt.dedupeKey===e)}hasRecentlyResolvedApprovalDedupeKey(e){return this.pruneResolvedApprovalDedupeKeys(),this.resolvedApprovalDedupeKeys.has(e)}rememberResolvedApprovalDedupeKey(e){e&&(this.pruneResolvedApprovalDedupeKeys(),this.resolvedApprovalDedupeKeys.set(e,Date.now()))}pruneResolvedApprovalDedupeKeys(){let e=Date.now()-Ge;for(let[t,i]of this.resolvedApprovalDedupeKeys.entries())i<e&&this.resolvedApprovalDedupeKeys.delete(t)}buildApprovalDedupeKey(e){let t=e.toolName||"Tool",i=this.firstString(e.toolInput?.command,e.toolInput?.cmd,e.rawInput);if(i)return`command:${this.hashDedupeValue(`${this.mapToolNameForApproval(t)||t}:${i.trim()}`)}`;let s=this.firstString(e.filePath,e.toolInput?.file_path,e.toolInput?.path,e.toolInput?.filePath);if(s)return`file:${this.hashDedupeValue(`${t}:${s}`)}`;let o=this.firstString(e.hint);if(o)return`hint:${this.hashDedupeValue(`${t}:${o}`)}`}hashDedupeValue(e){return X.createHash("sha256").update(e.replace(/\s+/g," ").trim().toLowerCase()).digest("hex").slice(0,16)}buildPermissionPromptId(e,t,i,s){let o=typeof e.turn_id=="string"&&e.turn_id.trim().length>0?e.turn_id.trim():void 0,n=X.createHash("sha256").update(`${t}:${s||this.stringifyToolInput(i)}`).digest("hex").slice(0,12);return`permission-${o||"turn"}-${n}`}buildPermissionRequestHint(e,t,i){let s=this.firstString(t?.command,t?.cmd);if(s)return`Command: ${this.truncateApprovalDetail(s,80)}`;let o=this.extractFilePathFromToolInput(e,t,i);return o?`File: ${o}`:`Tool: ${this.mapToolNameForApproval(e)||e}`}extractFilePathFromToolInput(e,t,i){let s=this.firstString(t?.file_path,t?.path,t?.filePath);if(s)return s;if(e==="apply_patch"&&i){let o=i.match(/\*\*\* (?:Update|Add|Delete) File: (.+)/);if(o)return o[1].trim()}}stringifyToolInput(e){if(e!=null){if(typeof e=="string")return e;try{return JSON.stringify(e)}catch{return String(e)}}}buildApprovalPromptContextFromPendingCall(e){return{toolName:e.name,filePath:e.filePath,diff:e.diff,toolInput:e.parsedInput,rawInput:e.input,hint:e.filePath?`File: ${e.filePath}`:`Tool: ${this.mapToolNameForApproval(e.name)||e.name}`}}buildApprovalPromptContextFromParsedPrompt(e){let t=this.extractShellCommandFromPromptText(e.promptText);return t?{toolName:"Bash",toolInput:{command:t},rawInput:t,hint:`Command: ${this.truncateApprovalDetail(t,80)}`}:null}parsedPromptMatchesApprovalContext(e,t){let i=this.firstString(t.toolInput?.command,t.toolInput?.cmd);if(i){let o=this.extractShellCommandFromPromptText(e.promptText);return!!o&&this.normalizeApprovalCommand(o)===this.normalizeApprovalCommand(i)}let s=this.firstString(t.filePath,t.toolInput?.file_path,t.toolInput?.path,t.toolInput?.filePath);return s?e.promptText.includes(s):!1}normalizeApprovalCommand(e){return e.replace(/\s+/g," ").trim()}extractShellCommandFromPromptText(e){let t=e.split(`
|
|
12
13
|
`);for(let i=t.length-1;i>=0;i-=1){let s=t[i].trim().match(/^\$\s+(.+)$/);if(s?.[1]?.trim())return s[1].trim()}}buildPromptInstructions(e){return e.kind==="yes_no"&&e.requiresFollowUpText?"Reply with 1 to approve, or 2 followed by what to change":e.kind==="yes_no"?"Reply with 1 for yes or 2 for no":e.kind==="numbered"?"Reply with the number of the option you want":"Reply with your response"}buildApprovalPromptContent(e,t){let i=e.trim(),s=i==="Codex is waiting for approval.",o=/^\$\s+/.test(i),n=t.toolName?`${t.toolName} requires approval.`:"Codex is waiting for approval.",a=[s||o||!i?n:i],p=typeof t.toolInput?.command=="string"?t.toolInput.command.trim():void 0;if(p&&!a.join(`
|
|
13
14
|
`).includes(p))return a.push("Command:",this.truncateApprovalDetail(p,240)),a.join(`
|
|
14
|
-
`);let
|
|
15
|
-
`).includes(
|
|
15
|
+
`);let c=t.filePath||t.toolInput?.file_path;return typeof c=="string"&&c.length>0&&!a.join(`
|
|
16
|
+
`).includes(c)?(a.push(`File: ${c}`),a.join(`
|
|
16
17
|
`)):(t.hint&&!a.join(`
|
|
17
18
|
`).includes(t.hint)&&a.push(t.hint),a.join(`
|
|
18
19
|
`))}truncateApprovalDetail(e,t){return e.length>t?`${e.slice(0,t-3)}...`:e}summarizePromptSnapshot(e){return e.split(`
|
|
19
20
|
`).map(t=>t.trimEnd()).filter(t=>t.length>0).slice(-12).map(t=>t.slice(0,160)).join(`
|
|
20
|
-
`)}translatePromptResponse(e){let t=this.pendingInteractivePrompt;if(!t)return{primaryInput:e};let s=e.trim().match(/^(\d+)(?:[,.:;\-\s]+([\s\S]+))?$/);if(!s)return{primaryInput:e};let o=s[1],n=s[2]?.trim(),a=t.submitMap[o];return a?t.requiresFollowUpText&&n?{primaryInput:a,followUpInput:n}:{primaryInput:a}:{primaryInput:e}}getEventPromptId(e){let t=e.promptId;return typeof t=="string"&&t.trim().length>0?t.trim():null}isApprovalResponseLike(e){let t=e.trim().toLowerCase();return/^(?:\d+|y|yes|n|no)(?:[\s,.:;-].*)?$/.test(t)}async emitRejectedPromptResponseNotification(e,t){if(!this.sessionState)return;let i="Response ignored because it was not tied to the current prompt. Please reply to the latest prompt again.",s={prompt_response_rejected:!0,reason:e,eventId:t.eventId,eventPromptId:this.getEventPromptId(t),activePromptId:this.pendingInteractivePrompt?.promptId},o=!1;this.sessionKey&&(i=
|
|
21
|
-
`)){let m=u.match(s);if(m){o+=1,n=Number.parseInt(m[1],10),a=Number.parseInt(m[2],10);continue}if(!(u.startsWith("***")||u.startsWith("---")||u.startsWith("+++")||u.startsWith("*** End Patch"))){if(u.startsWith("-"))p===void 0&&(p=n),t.push(u.slice(1)),n+=1;else if(u.startsWith("+"))
|
|
21
|
+
`)}translatePromptResponse(e){let t=this.pendingInteractivePrompt;if(!t)return{primaryInput:e};let s=e.trim().match(/^(\d+)(?:[,.:;\-\s]+([\s\S]+))?$/);if(!s)return{primaryInput:e};let o=s[1],n=s[2]?.trim(),a=t.submitMap[o];return a?t.requiresFollowUpText&&n?{primaryInput:a,followUpInput:n}:{primaryInput:a}:{primaryInput:e}}getEventPromptId(e){let t=e.promptId;return typeof t=="string"&&t.trim().length>0?t.trim():null}isApprovalResponseLike(e){let t=e.trim().toLowerCase();return/^(?:\d+|y|yes|n|no)(?:[\s,.:;-].*)?$/.test(t)}async emitRejectedPromptResponseNotification(e,t){if(!this.sessionState)return;let i="Response ignored because it was not tied to the current prompt. Please reply to the latest prompt again.",s={prompt_response_rejected:!0,reason:e,eventId:t.eventId,eventPromptId:this.getEventPromptId(t),activePromptId:this.pendingInteractivePrompt?.promptId},o=!1;this.sessionKey&&(i=d.cryptoService.encryptContent(i,this.sessionKey),s={encrypted:d.cryptoService.encryptMetadata(s,this.sessionKey)},o=!0);try{await this.appSyncClient.createEvent({sessionId:this.sessionState.sessionId,type:d.EventType.NOTIFICATION,source:d.EventSource.DESKTOP,content:i,metadata:s,...o?{isEncrypted:!0}:{}})}catch(n){r.warn("Failed to emit rejected prompt response notification",{reason:e,error:n})}}buildToolDetailsForInteractivePrompt(e,t){let i=e.toolName,s=e.toolInput&&typeof e.toolInput=="object"?e.toolInput:void 0;if(i==="apply_patch"){let n=e.diff||e.rawInput;if(n){let{oldString:a,newString:p,oldStartLine:c,newStartLine:u}=this.extractOldNewFromPatch(n),m=t?this.extractDiffLineAnchorsFromSnapshot(t):{};return{tool_name:"Edit",tool_input:{file_path:e.filePath,content:n,diff:e.diff,raw_patch:e.rawInput,old_string:a,new_string:p,old_start_line:c??m.oldStartLine,new_start_line:u??m.newStartLine}}}}if(i==="exec_command"||i==="shell_command"||i==="shell"||i==="Bash"){let n=this.firstString(s?.command,s?.cmd,e.rawInput,e.hint);if(n)return{tool_name:"Bash",tool_input:{command:n,output:s?.output}}}let o={};return e.filePath&&(o.file_path=e.filePath),e.diff&&(o.diff=e.diff),e.rawInput&&(o.raw_input=e.rawInput),Object.keys(o).length>0?{tool_name:i||"Tool",tool_input:o}:{}}firstString(...e){for(let t of e)if(typeof t=="string"&&t.trim().length>0)return t}buildFallbackToolInput(e){let t={};return e.filePath&&(t.file_path=e.filePath),e.diff&&(t.diff=e.diff),e.rawInput&&(t.raw_input=e.rawInput),e.toolInput&&typeof e.toolInput=="object"&&(t.parsed_input=e.toolInput),Object.keys(t).length>0?t:void 0}mapToolNameForApproval(e){return e?{exec_command:"Bash",Bash:"Bash",apply_patch:"Edit",shell_command:"Bash",shell:"Bash",Edit:"Edit",Write:"Write"}[e]||e:void 0}extractOldNewFromPatch(e){let t=[],i=[],s=/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/,o=0,n=0,a=0,p,c;for(let u of e.split(`
|
|
22
|
+
`)){let m=u.match(s);if(m){o+=1,n=Number.parseInt(m[1],10),a=Number.parseInt(m[2],10);continue}if(!(u.startsWith("***")||u.startsWith("---")||u.startsWith("+++")||u.startsWith("*** End Patch"))){if(u.startsWith("-"))p===void 0&&(p=n),t.push(u.slice(1)),n+=1;else if(u.startsWith("+"))c===void 0&&(c=a),i.push(u.slice(1)),a+=1;else if(u.startsWith(" ")){let v=u.slice(1);t.push(v),i.push(v),n+=1,a+=1}}}return{oldString:t.join(`
|
|
22
23
|
`),newString:i.join(`
|
|
23
|
-
`),oldStartLine:o===1?p:void 0,newStartLine:o===1?
|
|
24
|
-
`)){let n=o.match(/^\s*(\d+)\s+(.*)$/);if(!n)continue;let a=Number.parseInt(n[1],10),p=n[2];if(Number.isFinite(a)){if(p.startsWith("-")){i??=a;continue}if(p.startsWith("+")){s??=a;continue}i??=a,s??=a}}return r.debug("Recovered diff line anchors from tmux snapshot",{oldStartLine:i,newStartLine:s,snapshotPreview:this.summarizePromptSnapshot(e)}),{oldStartLine:i,newStartLine:s}}subscribeToMobileEvents(e){if(this.subscribedSessionId===e)return r.info("Already subscribed to mobile events, skipping",{sessionId:e}),!0;if(r.info("Subscribing to mobile events",{sessionId:e}),this.unsubscribe){try{this.unsubscribe()}catch(t){r.warn("Error cleaning up previous subscription (non-fatal)",{error:t})}this.subscribedSessionId=null}try{this.unsubscribe=this.appSyncClient.subscribeToEvents(e,async t=>{await this.handleMobileEvent(t)},t=>{r.error("Subscription error:",t)}),this.subscribedSessionId=e}catch(t){return r.error("Failed to subscribe to mobile events (non-fatal)",{sessionId:e,error:t}),!1}return this.sessionState&&(this.sessionState.subscriptionActive=!0),r.info("Subscribed to mobile events"),!0}async downloadAttachment(e,t,i){try{let s=e.isEncrypted??i??!1;r.info("Downloading attachment",{id:e.id,type:e.type,filename:e.filename,s3Key:e.s3Key,attachmentIsEncrypted:e.isEncrypted,eventIsEncrypted:i,shouldDecrypt:s});let{downloadUrl:o}=await this.appSyncClient.getAttachmentDownloadUrl(e.s3Key),n=await fetch(o);if(!n.ok)throw new Error(`Failed to download attachment: ${n.status} ${n.statusText}`);let a=Buffer.from(await n.arrayBuffer());if(s&&this.sessionKey)try{r.info("Decrypting attachment",{id:e.id}),a=
|
|
24
|
+
`),oldStartLine:o===1?p:void 0,newStartLine:o===1?c:void 0}}extractDiffLineAnchorsFromSnapshot(e){let t=(0,I.normalizeSnapshot)(e),i,s;for(let o of t.split(`
|
|
25
|
+
`)){let n=o.match(/^\s*(\d+)\s+(.*)$/);if(!n)continue;let a=Number.parseInt(n[1],10),p=n[2];if(Number.isFinite(a)){if(p.startsWith("-")){i??=a;continue}if(p.startsWith("+")){s??=a;continue}i??=a,s??=a}}return r.debug("Recovered diff line anchors from tmux snapshot",{oldStartLine:i,newStartLine:s,snapshotPreview:this.summarizePromptSnapshot(e)}),{oldStartLine:i,newStartLine:s}}subscribeToMobileEvents(e){if(this.subscribedSessionId===e)return r.info("Already subscribed to mobile events, skipping",{sessionId:e}),!0;if(r.info("Subscribing to mobile events",{sessionId:e}),this.unsubscribe){try{this.unsubscribe()}catch(t){r.warn("Error cleaning up previous subscription (non-fatal)",{error:t})}this.subscribedSessionId=null}try{this.unsubscribe=this.appSyncClient.subscribeToEvents(e,async t=>{await this.handleMobileEvent(t)},t=>{r.error("Subscription error:",t)}),this.subscribedSessionId=e}catch(t){return r.error("Failed to subscribe to mobile events (non-fatal)",{sessionId:e,error:t}),!1}return this.sessionState&&(this.sessionState.subscriptionActive=!0),r.info("Subscribed to mobile events"),!0}async downloadAttachment(e,t,i){try{let s=e.isEncrypted??i??!1;r.info("Downloading attachment",{id:e.id,type:e.type,filename:e.filename,s3Key:e.s3Key,attachmentIsEncrypted:e.isEncrypted,eventIsEncrypted:i,shouldDecrypt:s});let{downloadUrl:o}=await this.appSyncClient.getAttachmentDownloadUrl(e.s3Key),n=await fetch(o);if(!n.ok)throw new Error(`Failed to download attachment: ${n.status} ${n.statusText}`);let a=Buffer.from(await n.arrayBuffer());if(s&&this.sessionKey)try{r.info("Decrypting attachment",{id:e.id}),a=d.cryptoService.decryptData(a,this.sessionKey),r.info("Attachment decrypted successfully",{id:e.id,decryptedSize:a.length})}catch(v){throw r.error("Failed to decrypt attachment:",{id:e.id,error:v}),new Error("Failed to decrypt attachment")}else s&&!this.sessionKey&&r.warn("Cannot decrypt attachment - no session key available",{id:e.id});let p=C.join(be.tmpdir(),"codevibe-codex",t);x.existsSync(p)||x.mkdirSync(p,{recursive:!0});let c="";if(e.filename){let v=C.extname(e.filename);v&&(c=v)}c||(c={"image/jpeg":".jpg","image/png":".png","image/gif":".gif","image/webp":".webp","image/heic":".heic","application/pdf":".pdf"}[e.type]||".bin");let u=`attachment-${e.id}${c}`,m=C.join(p,u);return x.writeFileSync(m,a),r.info("Attachment saved to temp file",{id:e.id,filePath:m,size:a.length}),m}catch(s){return r.error("Failed to download attachment:",{id:e.id,error:s}),null}}async handleMobileEvent(e){if(e.attachments&&e.attachments.length>0&&r.info("DEBUG: Raw attachment data from subscription",{attachments:JSON.stringify(e.attachments),eventIsEncrypted:e.isEncrypted}),r.info("Received mobile event",{eventId:e.eventId,type:e.type,content:e.content?.substring(0,50),attachmentCount:e.attachments?.length||0,isEncrypted:e.isEncrypted}),!this.sessionState){r.warn("Received mobile event but no active session");return}let t=e.content||"";if(e.isEncrypted&&this.sessionKey)try{t=d.cryptoService.decryptContent(e.content,this.sessionKey),r.debug("Event decrypted successfully",{eventId:e.eventId})}catch(i){r.error("Failed to decrypt event:",{eventId:e.eventId,error:i}),t=e.content}try{await this.appSyncClient.updateEventStatus({eventId:e.eventId,sessionId:e.sessionId,timestamp:e.timestamp,deliveryStatus:d.DeliveryStatus.DELIVERED})}catch(i){r.error("Failed to update delivery status:",i)}if(e.type===d.EventType.USER_PROMPT||e.type===d.EventType.PROMPT_RESPONSE){let i=t,s=e.attachments||[],o=e.type===d.EventType.PROMPT_RESPONSE;if(e.type===d.EventType.PROMPT_RESPONSE){let u=this.getEventPromptId(e),m=this.pendingInteractivePrompt?.promptId;if(!u||!m||u!==m){r.warn("Rejecting stale or unbound PROMPT_RESPONSE",{eventId:e.eventId,eventPromptId:u,activePromptId:m}),await this.emitRejectedPromptResponseNotification("prompt_id_mismatch",e);return}}else this.pendingInteractivePrompt&&this.isApprovalResponseLike(i)&&(o=!0,r.info("Treating approval-shaped USER_PROMPT as active prompt response",{eventId:e.eventId,activePromptId:this.pendingInteractivePrompt.promptId,promptPreview:i.slice(0,20)}));let n=[];if(s.length>0){r.info("Downloading attachments for prompt",{count:s.length});for(let u of s){let m=await this.downloadAttachment(u,this.sessionState.sessionId,e.isEncrypted);m&&n.push(m)}if(n.length>0){let u=n.map(m=>`[Attached file: ${m}]`).join(`
|
|
25
26
|
`);i?i=`${u}
|
|
26
27
|
|
|
27
28
|
${i}`:i=`${u}
|
|
28
29
|
|
|
29
|
-
Please analyze the attached file(s).`,r.info("Prompt updated with attachment paths",{attachmentCount:n.length,newPromptLength:i.length})}}let a=this.translatePromptResponse(i),p=this.sessionState.sessionId;a.primaryInput!==T&&this.trackMobilePrompt(p,a.primaryInput);let
|
|
30
|
+
Please analyze the attached file(s).`,r.info("Prompt updated with attachment paths",{attachmentCount:n.length,newPromptLength:i.length})}}let a=this.translatePromptResponse(i),p=this.sessionState.sessionId;a.primaryInput!==T&&this.trackMobilePrompt(p,a.primaryInput);let c=await this.promptResponder.sendInput(p,a.primaryInput);if(!c&&a.primaryInput!==T&&this.forgetMobilePrompt(p,a.primaryInput),c&&a.followUpInput&&(this.trackMobilePrompt(p,a.followUpInput),await this.promptResponder.sendInput(p,a.followUpInput)||this.forgetMobilePrompt(p,a.followUpInput)),c&&this.pendingInteractivePrompt&&o){let u=this.pendingInteractivePrompt;this.rememberResolvedApprovalDedupeKey(u.dedupeKey),this.pendingInteractivePrompt=null,await this.emitNextQueuedInteractivePrompt()}if(c)try{await this.appSyncClient.updateEventStatus({eventId:e.eventId,sessionId:e.sessionId,timestamp:e.timestamp,deliveryStatus:d.DeliveryStatus.EXECUTED})}catch(u){r.error("Failed to update executed status:",u)}}}generateSessionId(e){return`codex-${e}`}startMobileEndWatcher(e){!this.sessionState||this.sessionState.sessionId!==e||(this.sessionState.mobileEndWatcher=this.appSyncClient.watchForMobileEnd(e,async()=>{r.info("Mobile ended session \u2014 sending desktop quit",{sessionId:e});let t=process.env[Se];if(!t){r.warn("No tmux session set; skipping desktop self-terminate",{sessionId:e,expectedEnv:Se});return}await Xe(t,je)}))}async endActiveSession(e){if(this.sessionState){r.info("Ending active session",{sessionId:this.sessionState.sessionId,codexSessionId:this.sessionState.codexSessionId,reason:e}),this.sessionState.mobileEndWatcher&&(this.sessionState.mobileEndWatcher.stop(),this.sessionState.mobileEndWatcher=void 0),this.appSyncClient.stopHeartbeat(this.sessionState.sessionId),this.unsubscribe&&(this.unsubscribe(),this.unsubscribe=null),await this.tmuxPaneObserver.stop(),this.pendingInteractivePrompt=null,this.queuedInteractivePrompts=[],this.isInitializingSession=!1,this.bufferedLogEntries=[],this.sessionKey&&(d.keychainManager.clearSessionKey(this.sessionState.sessionId),this.sessionKey=null);try{await this.appSyncClient.updateSession({sessionId:this.sessionState.sessionId,status:d.SessionStatus.INACTIVE})}catch(t){r.error("Failed to update session status:",t)}this.sessionState=null}}async stop(){r.info("Stopping CodeVibe Codex companion server"),await this.endActiveSession("shutdown"),this.sessionWatcher.stop(),this.approvalDetector.shutdown(),ne(),await this.httpApi.stop(),this.appSyncClient.cleanupSubscriptions(),r.info("CodeVibe Codex companion server stopped")}};if(require.main===module){let l=new q;process.on("SIGINT",async()=>{r.info("Received SIGINT, shutting down..."),await l.stop(),process.exit(0)}),process.on("SIGTERM",async()=>{r.info("Received SIGTERM, shutting down..."),await l.stop(),process.exit(0)}),l.start().catch(e=>{r.error("Failed to start server:",e),process.exit(1)})}0&&(module.exports={CodexCompanionServer});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quantiya/codevibe-codex-plugin",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.39",
|
|
4
4
|
"description": "Control OpenAI Codex CLI from your iPhone and Android — real-time sync, approve file edits, send prompts by voice. Part of CodeVibe.",
|
|
5
5
|
"main": "dist/server.js",
|
|
6
6
|
"bin": {
|