@quantiya/codevibe-codex-plugin 1.0.18 → 1.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/server.js +1 -1
  2. package/package.json +2 -2
package/dist/server.js CHANGED
@@ -6,7 +6,7 @@ ${a}`,metadata:{toolName:p,toolOutput:o,callId:s,status:"completed"}}}if(i==="cu
6
6
  `).slice(-20).join(`
7
7
  `);return/\[(?:y\/n|Y\/n|y\/N)\]|^\s*\d+\.\s+/im.test(i)}hashPromptSnapshot(e){let i=e.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g,"").replace(/\r/g,`
8
8
  `).replace(/[ \t]+\n/g,`
9
- `).trim();return(0,ft.createHash)("sha256").update(i).digest("hex")}};var _=require("@quantiya/codevibe-core");var z=g(require("express")),E=g(require("fs")),V=g(require("path")),q=g(require("os"));I();var L=class{constructor(){this.assignedPort=0;this.app=(0,z.default)(),this.setupMiddleware(),this.setupRoutes(),this.tmuxSession=process.env.CODEVIBE_CODEX_TMUX_SESSION}getPort(){return this.assignedPort}setupMiddleware(){this.app.use(z.default.json({limit:"1mb"})),this.app.use((t,e,i)=>{n.debug(`${t.method} ${t.path}`,{body:t.body}),i()})}setupRoutes(){this.app.get("/health",(t,e)=>{e.json({success:!0,data:{status:"healthy",uptime:process.uptime()}})}),this.app.post("/event",this.handleEvent.bind(this))}async handleEvent(t,e){try{let i=t.body;if(!i.session_id||!i.hook_event_name){e.status(400).json({success:!1,error:"Missing session_id or hook_event_name"});return}let s=this.transformHookToEvent(i);n.info("Received hook event",{sessionId:i.session_id,hookEvent:i.hook_event_name,type:s.type}),this.eventHandler&&await this.eventHandler(s),e.json({success:!0})}catch(i){n.error("Error handling event:",i),e.status(500).json({success:!1,error:i instanceof Error?i.message:"Unknown error"})}}transformHookToEvent(t){let e={cwd:t.cwd,hook_event_name:t.hook_event_name,...t.metadata||{}},i,s;switch(t.hook_event_name){case"SessionStart":i="NOTIFICATION",s="Session started",e.source=t.source;break;case"UserPromptSubmit":i="USER_PROMPT",s=t.prompt||"";break;case"PreToolUse":i="INTERACTIVE_PROMPT",s=`${t.tool_name||"Tool"} requires approval`,e.tool_name=t.tool_name,e.tool_input=t.tool_input,e.tool_use_id=t.tool_use_id;break;case"PostToolUse":i="TOOL_USE",s=JSON.stringify({tool_name:t.tool_name,tool_input:t.tool_input,tool_response:t.tool_response}),e.tool_name=t.tool_name;break;case"Stop":i="ASSISTANT_RESPONSE",s=t.last_assistant_message||"";break;default:i="NOTIFICATION",s=`Hook: ${t.hook_event_name}`}return{session_id:t.session_id,hook_event_name:t.hook_event_name,type:i,source:"DESKTOP",content:s,metadata:e}}onEvent(t){this.eventHandler=t}async start(){return new Promise((t,e)=>{try{this.server=this.app.listen(0,"localhost",()=>{let i=this.server.address();this.assignedPort=i.port,n.info(`HTTP API listening on http://localhost:${this.assignedPort}`),this.writePortFile(this.assignedPort),t(this.assignedPort)}),this.server.on("error",i=>{n.error("HTTP server error:",i),e(i)})}catch(i){e(i)}})}writePortFile(t){if(!this.tmuxSession){n.warn("No CODEVIBE_CODEX_TMUX_SESSION set, skipping port file");return}let e=V.join(q.tmpdir(),`codevibe-codex-${this.tmuxSession}.port`);try{E.writeFileSync(e,t.toString()),n.info(`Port file written: ${e} -> ${t}`)}catch(i){n.error(`Failed to write port file: ${e}`,i)}}removePortFile(){if(!this.tmuxSession)return;let t=V.join(q.tmpdir(),`codevibe-codex-${this.tmuxSession}.port`);try{E.existsSync(t)&&(E.unlinkSync(t),n.info(`Port file removed: ${t}`))}catch(e){n.warn(`Failed to remove port file: ${t}`,e)}}async stop(){return this.removePortFile(),new Promise(t=>{this.server?this.server.close(()=>{n.info("HTTP API stopped"),t()}):t()})}};var G=class{constructor(){this.sessionState=null;this.unsubscribe=null;this.sessionKey=null;this.pendingInteractivePrompt=null;this.isInitializingSession=!1;this.bufferedLogEntries=[];this.hooksActive=!1;this.subscribedSessionId=null;this.httpApi=new L,this.sessionWatcher=new O,this.approvalDetector=new A,this.promptResponder=new N,this.tmuxPaneObserver=new M}async start(){n.info("Starting CodeVibe Codex companion server",{environment:(0,c.getEnvironment)()}),this.appSyncClient=new c.AppSyncClient,await this.appSyncClient.authenticateWithStoredTokens()||(n.error('Authentication failed. Run "codevibe-codex login" first.'),console.error('Not authenticated. Run "codevibe-codex login" to sign in.'),process.exit(1)),n.info("Authenticated successfully",{userId:this.appSyncClient.getCurrentUserId(),email:this.appSyncClient.getCurrentUserEmail()}),await(0,c.registerDeviceEncryptionKey)(this.appSyncClient,n),(0,c.startDeviceKeyWatcher)(this.appSyncClient,n),this.httpApi.onEvent(this.handleEventFromHook.bind(this));let e=await this.httpApi.start();n.info("HTTP API started for hooks",{port:e}),await this.createLaunchSession(),this.setupEventHandlers(),this.sessionWatcher.start(),n.info("CodeVibe Codex companion server started")}async createLaunchSession(){let t=process.env.CODEVIBE_CODEX_TMUX_SESSION;if(!t){n.warn("No CODEVIBE_CODEX_TMUX_SESSION \u2014 skipping launch session");return}let e=process.env.CODEX_WORKING_DIRECTORY||process.cwd(),i=this.generateSessionId(t),s=this.appSyncClient.getCurrentUserId();n.info("Creating launch session",{sessionId:i,projectPath:e});try{let o=await(0,c.resumeOrCreateSession)({sessionId:i,userId:s,agentType:c.AgentType.CODEX,projectPath:e,metadata:{launchSession:!0}},this.appSyncClient,n);this.sessionKey=o.sessionKey,this.sessionState={sessionId:i,userId:s,projectPath:e,cwd:e,createdAt:new Date,subscriptionActive:!1,metadata:{launchSession:!0},codexSessionId:t,codexLogFile:void 0},this.subscribeToMobileEvents(i),this.appSyncClient.startHeartbeat(i),n.info("Launch session created",{sessionId:i})}catch(o){n.error("Failed to create launch session (non-fatal)",{error:o})}}async handleEventFromHook(t){let{session_id:e,hook_event_name:i,type:s,content:o,metadata:r}=t;if(this.hooksActive=!0,n.info("[Hooks] Received event",{sessionId:e,hookEvent:i,type:s,contentLength:o?.length}),i==="SessionStart"){if(this.sessionState)n.info("[Hooks] SessionStart \u2014 launch session already exists, updating codexSessionId",{existingSessionId:this.sessionState.sessionId,codexSessionId:e}),this.sessionState.codexSessionId=e,this.sessionState.metadata={...this.sessionState.metadata,codexSessionId:e,cliVersion:r?.model||"unknown",modelProvider:r?.model||"unknown",launchSession:void 0},this.appSyncClient.updateSession({sessionId:this.sessionState.sessionId,metadata:this.sessionState.metadata}).catch(a=>n.warn("Failed to update session metadata",{error:a}));else{let a={id:e,timestamp:new Date().toISOString(),cwd:r?.cwd||process.cwd(),originator:"hook",cli_version:r?.model||"unknown",instructions:null,source:r?.source||"startup",model_provider:r?.model||"unknown"};await this.handleSessionStarted(a)}return}if(!this.sessionState){n.warn("[Hooks] Session not initialized, buffering event",{hook_event_name:i});return}let p=this.sessionState.sessionId;if(s==="USER_PROMPT"&&o&&this.isRecentMobilePrompt(o)){n.info("[Hooks] Skipping duplicate USER_PROMPT from mobile");return}if(i==="PreToolUse"){let a=r?.tool_name||"unknown",d=r?.tool_input,u={tool_name:this.mapToolName(a),tool_input:d||{}};if(a==="apply_patch"&&typeof d=="string"){let{extractOldNewFromPatch:x,extractFileFromPatch:D}=(j(),Tt(pt)),w=x(d),U=D(d);w&&(u.tool_input={file_path:U||"",old_string:w.oldString,new_string:w.newString})}let h=process.env.CODEVIBE_CODEX_TMUX_SESSION;if(h)try{await new Promise(U=>setTimeout(U,500));let{execSync:x}=require("child_process"),D=x(`tmux capture-pane -p -e -S -30 -t '${h}'`,{timeout:5e3,encoding:"utf8"}),w=(0,_.parseInteractivePrompt)(D);w&&w.options.length>0&&(u.options=w.options,u.submitMap=w.submitMap)}catch{n.debug("[Hooks] Tmux capture failed, using default options")}let m=o,P=u,b=!1;this.sessionKey&&(m=c.cryptoService.encryptContent(o,this.sessionKey),P={encrypted:c.cryptoService.encryptMetadata(u,this.sessionKey)},b=!0),await this.appSyncClient.createEvent({sessionId:p,type:c.EventType.INTERACTIVE_PROMPT,source:c.EventSource.DESKTOP,content:m,metadata:P,isEncrypted:b}),this.pendingInteractivePrompt={promptId:(0,X.v4)(),kind:"yes_no",options:u.options||[],submitMap:u.submitMap||{},promptText:o,createdAt:Date.now(),source:"tmux",requiresFollowUpText:!1},n.info("[Hooks] INTERACTIVE_PROMPT sent",{toolName:a,sessionId:p});return}if(i==="PostToolUse"){let a=o,d=r,u=!1;this.sessionKey&&(a=c.cryptoService.encryptContent(o,this.sessionKey),r&&(d={encrypted:c.cryptoService.encryptMetadata(r,this.sessionKey)}),u=!0),await this.appSyncClient.createEvent({sessionId:p,type:c.EventType.TOOL_USE,source:c.EventSource.DESKTOP,content:a,metadata:d,isEncrypted:u});return}if(s==="ASSISTANT_RESPONSE"||s==="USER_PROMPT"){let a=o,d=!1;this.sessionKey&&o&&(a=c.cryptoService.encryptContent(o,this.sessionKey),d=!0),await this.appSyncClient.createEvent({sessionId:p,type:s==="ASSISTANT_RESPONSE"?c.EventType.ASSISTANT_RESPONSE:c.EventType.USER_PROMPT,source:c.EventSource.DESKTOP,content:a,isEncrypted:d});return}}mapToolName(t){return{shell_command:"Bash",shell:"Bash",apply_patch:"Edit",create_file:"Write",read_file:"Read"}[t]||t}isRecentMobilePrompt(t){return!1}setupEventHandlers(){this.sessionWatcher.on("session-started",async t=>{if(this.sessionState){n.info("[JSONL] Session already active, skipping",{currentSessionId:this.sessionState.sessionId,codexSessionId:t.id});return}await this.handleSessionStarted(t)}),this.sessionWatcher.on("log-entry",async t=>{await this.handleLogEntry(t)}),this.approvalDetector.on("approval-pending",async t=>{await this.handleApprovalPending(t)}),this.tmuxPaneObserver.on("prompt-candidate",async t=>{await this.handleTmuxPromptCandidate(t.snapshot)}),this.tmuxPaneObserver.on("observer-error",t=>{n.debug("Tmux pane observer error",{error:t})}),this.sessionWatcher.on("error",t=>{n.error("Session watcher error:",t)})}async handleSessionStarted(t){n.info("Handling new Codex session",{codexSessionId:t.id}),this.isInitializingSession=!0,this.bufferedLogEntries=[],this.sessionState&&await this.endActiveSession("new-codex-session-started");let e=process.env.CODEX_WORKING_DIRECTORY||t.cwd||process.cwd(),i=this.generateSessionId(t.id),s=this.appSyncClient.getCurrentUserId(),o={codexSessionId:t.id,cliVersion:t.cli_version,modelProvider:t.model_provider};try{let r=await(0,c.resumeOrCreateSession)({sessionId:i,userId:s,agentType:c.AgentType.CODEX,projectPath:e,metadata:o},this.appSyncClient,n);this.sessionKey=r.sessionKey}catch(r){throw n.error("Failed to create/resume session:",r),r}try{this.sessionState={sessionId:i,userId:s,projectPath:e,cwd:t.cwd,createdAt:new Date,subscriptionActive:!1,metadata:o,codexSessionId:t.id,codexLogFile:this.sessionWatcher.getActiveLogFile()||void 0},await this.flushBufferedLogEntries(),await this.startTmuxObserver(),this.subscribeToMobileEvents(i),this.appSyncClient.startHeartbeat(i)}catch(r){n.error("Failed to create session:",r),this.bufferedLogEntries=[]}finally{this.isInitializingSession=!1}}async flushBufferedLogEntries(){if(this.bufferedLogEntries.length===0)return;let t=this.bufferedLogEntries;this.bufferedLogEntries=[],n.info("Flushing buffered log entries after session initialization",{count:t.length,sessionId:this.sessionState?.sessionId});for(let e of t)await this.handleLogEntry(e)}async handleLogEntry(t){if(!this.sessionState){if(this.isInitializingSession){this.bufferedLogEntries.push(t),n.debug("Buffering log entry until session initialization completes",{type:t.type,bufferedCount:this.bufferedLogEntries.length});return}n.warn("Received log entry but no active session");return}if(t.type==="response_item"&&t.payload){let i=t.payload.type;i==="function_call"||i==="custom_tool_call"?this.approvalDetector.onToolCallStart(t.payload.call_id,t.payload.name,t.payload.arguments||t.payload.input||""):(i==="function_call_output"||i==="custom_tool_call_output")&&(this.approvalDetector.onToolCallComplete(t.payload.call_id),this.pendingInteractivePrompt?.callId===t.payload.call_id&&(this.pendingInteractivePrompt=null))}let e=K(t,this.sessionState.sessionId);if(e){if(this.hooksActive){if(e.type===c.EventType.USER_PROMPT||e.type===c.EventType.ASSISTANT_RESPONSE){n.debug("[JSONL] Skipping \u2014 hooks deliver this event type",{type:e.type});return}let i=t.payload?.type;if((i==="function_call"||i==="function_call_output")&&(e.type===c.EventType.TOOL_USE||e.type===c.EventType.INTERACTIVE_PROMPT)){n.debug("[JSONL] Skipping function_call \u2014 hooks deliver this",{type:e.type,tool:t.payload?.name});return}}try{if(this.sessionKey){if(e.content=c.cryptoService.encryptContent(e.content,this.sessionKey),e.metadata){let i=c.cryptoService.encryptMetadata(e.metadata,this.sessionKey);e.metadata={encrypted:i}}e.isEncrypted=!0,n.debug("Event encrypted",{type:e.type})}await this.appSyncClient.createEvent(e),n.debug("Event synced to backend",{type:e.type,encrypted:!!this.sessionKey})}catch(i){n.error("Failed to sync event:",i)}}}async handleApprovalPending(t){if(this.sessionState){n.info("Sending approval pending interactive prompt",t);try{let e=await this.tryParseInteractivePromptFromTmux(),i=e?.parsedPrompt??null;if(i&&this.pendingInteractivePrompt&&this.pendingInteractivePrompt.source==="tmux"&&this.pendingInteractivePrompt.promptText===i.promptText){n.debug("Skipping heuristic prompt because tmux prompt is already active",{promptText:i.promptText});return}let s=this.buildToolDetailsForInteractivePrompt(t,e?.snapshot),o=s.tool_name||this.mapToolNameForApproval(t.toolName),r=s.tool_input||this.buildFallbackToolInput(t),p=!!(o&&r),a=this.buildPromptPresentation(i),d=a.options,u=t.filePath?`File: ${t.filePath}`:void 0,h=a.content||`Codex is waiting for approval.
9
+ `).trim();return(0,ft.createHash)("sha256").update(i).digest("hex")}};var _=require("@quantiya/codevibe-core");var z=g(require("express")),E=g(require("fs")),V=g(require("path")),q=g(require("os"));I();var L=class{constructor(){this.assignedPort=0;this.app=(0,z.default)(),this.setupMiddleware(),this.setupRoutes(),this.tmuxSession=process.env.CODEVIBE_CODEX_TMUX_SESSION}getPort(){return this.assignedPort}setupMiddleware(){this.app.use(z.default.json({limit:"1mb"})),this.app.use((t,e,i)=>{n.debug(`${t.method} ${t.path}`,{body:t.body}),i()})}setupRoutes(){this.app.get("/health",(t,e)=>{e.json({success:!0,data:{status:"healthy",uptime:process.uptime()}})}),this.app.post("/event",this.handleEvent.bind(this))}async handleEvent(t,e){try{let i=t.body;if(!i.session_id||!i.hook_event_name){e.status(400).json({success:!1,error:"Missing session_id or hook_event_name"});return}let s=this.transformHookToEvent(i);n.info("Received hook event",{sessionId:i.session_id,hookEvent:i.hook_event_name,type:s.type}),this.eventHandler&&await this.eventHandler(s),e.json({success:!0})}catch(i){n.error("Error handling event:",i),e.status(500).json({success:!1,error:i instanceof Error?i.message:"Unknown error"})}}transformHookToEvent(t){let e={cwd:t.cwd,hook_event_name:t.hook_event_name,...t.metadata||{}},i,s;switch(t.hook_event_name){case"SessionStart":i="NOTIFICATION",s="Session started",e.source=t.source;break;case"UserPromptSubmit":i="USER_PROMPT",s=t.prompt||"";break;case"PreToolUse":i="INTERACTIVE_PROMPT",s=`${t.tool_name||"Tool"} requires approval`,e.tool_name=t.tool_name,e.tool_input=t.tool_input,e.tool_use_id=t.tool_use_id;break;case"PostToolUse":i="TOOL_USE",s=JSON.stringify({tool_name:t.tool_name,tool_input:t.tool_input,tool_response:t.tool_response}),e.tool_name=t.tool_name;break;case"Stop":i="ASSISTANT_RESPONSE",s=t.last_assistant_message||"";break;default:i="NOTIFICATION",s=`Hook: ${t.hook_event_name}`}return{session_id:t.session_id,hook_event_name:t.hook_event_name,type:i,source:"DESKTOP",content:s,metadata:e}}onEvent(t){this.eventHandler=t}async start(){return new Promise((t,e)=>{try{this.server=this.app.listen(0,"localhost",()=>{let i=this.server.address();this.assignedPort=i.port,n.info(`HTTP API listening on http://localhost:${this.assignedPort}`),this.writePortFile(this.assignedPort),t(this.assignedPort)}),this.server.on("error",i=>{n.error("HTTP server error:",i),e(i)})}catch(i){e(i)}})}writePortFile(t){if(!this.tmuxSession){n.warn("No CODEVIBE_CODEX_TMUX_SESSION set, skipping port file");return}let e=V.join(q.tmpdir(),`codevibe-codex-${this.tmuxSession}.port`);try{E.writeFileSync(e,t.toString()),n.info(`Port file written: ${e} -> ${t}`)}catch(i){n.error(`Failed to write port file: ${e}`,i)}}removePortFile(){if(!this.tmuxSession)return;let t=V.join(q.tmpdir(),`codevibe-codex-${this.tmuxSession}.port`);try{E.existsSync(t)&&(E.unlinkSync(t),n.info(`Port file removed: ${t}`))}catch(e){n.warn(`Failed to remove port file: ${t}`,e)}}async stop(){return this.removePortFile(),new Promise(t=>{this.server?this.server.close(()=>{n.info("HTTP API stopped"),t()}):t()})}};var G=class{constructor(){this.sessionState=null;this.unsubscribe=null;this.sessionKey=null;this.pendingInteractivePrompt=null;this.isInitializingSession=!1;this.bufferedLogEntries=[];this.hooksActive=!1;this.subscribedSessionId=null;this.httpApi=new L,this.sessionWatcher=new O,this.approvalDetector=new A,this.promptResponder=new N,this.tmuxPaneObserver=new M}async start(){n.info("Starting CodeVibe Codex companion server",{environment:(0,c.getEnvironment)()}),this.appSyncClient=new c.AppSyncClient,await this.appSyncClient.authenticateWithStoredTokens()||(n.error('Authentication failed. Run "codevibe-codex login" first.'),console.error('Not authenticated. Run "codevibe-codex login" to sign in.'),process.exit(1)),n.info("Authenticated successfully",{userId:this.appSyncClient.getCurrentUserId(),email:this.appSyncClient.getCurrentUserEmail()}),await(0,c.registerDeviceEncryptionKey)(this.appSyncClient,n),(0,c.startDeviceKeyWatcher)(this.appSyncClient,n);try{let i=await this.appSyncClient.sweepOrphanSessions({agentType:"CODEX"});i>0&&n.info("Orphan sweep: marked stale Codex sessions INACTIVE",{swept:i})}catch(i){n.warn("Orphan sweep failed, continuing startup",{error:i instanceof Error?i.message:String(i)})}this.httpApi.onEvent(this.handleEventFromHook.bind(this));let e=await this.httpApi.start();n.info("HTTP API started for hooks",{port:e}),await this.createLaunchSession(),this.setupEventHandlers(),this.sessionWatcher.start(),n.info("CodeVibe Codex companion server started")}async createLaunchSession(){let t=process.env.CODEVIBE_CODEX_TMUX_SESSION;if(!t){n.warn("No CODEVIBE_CODEX_TMUX_SESSION \u2014 skipping launch session");return}let e=process.env.CODEX_WORKING_DIRECTORY||process.cwd(),i=this.generateSessionId(t),s=this.appSyncClient.getCurrentUserId();n.info("Creating launch session",{sessionId:i,projectPath:e});try{let o=await(0,c.resumeOrCreateSession)({sessionId:i,userId:s,agentType:c.AgentType.CODEX,projectPath:e,metadata:{launchSession:!0}},this.appSyncClient,n);this.sessionKey=o.sessionKey,this.sessionState={sessionId:i,userId:s,projectPath:e,cwd:e,createdAt:new Date,subscriptionActive:!1,metadata:{launchSession:!0},codexSessionId:t,codexLogFile:void 0},this.subscribeToMobileEvents(i),this.appSyncClient.startHeartbeat(i),n.info("Launch session created",{sessionId:i})}catch(o){n.error("Failed to create launch session (non-fatal)",{error:o})}}async handleEventFromHook(t){let{session_id:e,hook_event_name:i,type:s,content:o,metadata:r}=t;if(this.hooksActive=!0,n.info("[Hooks] Received event",{sessionId:e,hookEvent:i,type:s,contentLength:o?.length}),i==="SessionStart"){if(this.sessionState)n.info("[Hooks] SessionStart \u2014 launch session already exists, updating codexSessionId",{existingSessionId:this.sessionState.sessionId,codexSessionId:e}),this.sessionState.codexSessionId=e,this.sessionState.metadata={...this.sessionState.metadata,codexSessionId:e,cliVersion:r?.model||"unknown",modelProvider:r?.model||"unknown",launchSession:void 0},this.appSyncClient.updateSession({sessionId:this.sessionState.sessionId,metadata:this.sessionState.metadata}).catch(a=>n.warn("Failed to update session metadata",{error:a}));else{let a={id:e,timestamp:new Date().toISOString(),cwd:r?.cwd||process.cwd(),originator:"hook",cli_version:r?.model||"unknown",instructions:null,source:r?.source||"startup",model_provider:r?.model||"unknown"};await this.handleSessionStarted(a)}return}if(!this.sessionState){n.warn("[Hooks] Session not initialized, buffering event",{hook_event_name:i});return}let p=this.sessionState.sessionId;if(s==="USER_PROMPT"&&o&&this.isRecentMobilePrompt(o)){n.info("[Hooks] Skipping duplicate USER_PROMPT from mobile");return}if(i==="PreToolUse"){let a=r?.tool_name||"unknown",d=r?.tool_input,u={tool_name:this.mapToolName(a),tool_input:d||{}};if(a==="apply_patch"&&typeof d=="string"){let{extractOldNewFromPatch:x,extractFileFromPatch:D}=(j(),Tt(pt)),w=x(d),U=D(d);w&&(u.tool_input={file_path:U||"",old_string:w.oldString,new_string:w.newString})}let h=process.env.CODEVIBE_CODEX_TMUX_SESSION;if(h)try{await new Promise(U=>setTimeout(U,500));let{execSync:x}=require("child_process"),D=x(`tmux capture-pane -p -e -S -30 -t '${h}'`,{timeout:5e3,encoding:"utf8"}),w=(0,_.parseInteractivePrompt)(D);w&&w.options.length>0&&(u.options=w.options,u.submitMap=w.submitMap)}catch{n.debug("[Hooks] Tmux capture failed, using default options")}let m=o,P=u,b=!1;this.sessionKey&&(m=c.cryptoService.encryptContent(o,this.sessionKey),P={encrypted:c.cryptoService.encryptMetadata(u,this.sessionKey)},b=!0),await this.appSyncClient.createEvent({sessionId:p,type:c.EventType.INTERACTIVE_PROMPT,source:c.EventSource.DESKTOP,content:m,metadata:P,isEncrypted:b}),this.pendingInteractivePrompt={promptId:(0,X.v4)(),kind:"yes_no",options:u.options||[],submitMap:u.submitMap||{},promptText:o,createdAt:Date.now(),source:"tmux",requiresFollowUpText:!1},n.info("[Hooks] INTERACTIVE_PROMPT sent",{toolName:a,sessionId:p});return}if(i==="PostToolUse"){let a=o,d=r,u=!1;this.sessionKey&&(a=c.cryptoService.encryptContent(o,this.sessionKey),r&&(d={encrypted:c.cryptoService.encryptMetadata(r,this.sessionKey)}),u=!0),await this.appSyncClient.createEvent({sessionId:p,type:c.EventType.TOOL_USE,source:c.EventSource.DESKTOP,content:a,metadata:d,isEncrypted:u});return}if(s==="ASSISTANT_RESPONSE"||s==="USER_PROMPT"){let a=o,d=!1;this.sessionKey&&o&&(a=c.cryptoService.encryptContent(o,this.sessionKey),d=!0),await this.appSyncClient.createEvent({sessionId:p,type:s==="ASSISTANT_RESPONSE"?c.EventType.ASSISTANT_RESPONSE:c.EventType.USER_PROMPT,source:c.EventSource.DESKTOP,content:a,isEncrypted:d});return}}mapToolName(t){return{shell_command:"Bash",shell:"Bash",apply_patch:"Edit",create_file:"Write",read_file:"Read"}[t]||t}isRecentMobilePrompt(t){return!1}setupEventHandlers(){this.sessionWatcher.on("session-started",async t=>{if(this.sessionState){n.info("[JSONL] Session already active, skipping",{currentSessionId:this.sessionState.sessionId,codexSessionId:t.id});return}await this.handleSessionStarted(t)}),this.sessionWatcher.on("log-entry",async t=>{await this.handleLogEntry(t)}),this.approvalDetector.on("approval-pending",async t=>{await this.handleApprovalPending(t)}),this.tmuxPaneObserver.on("prompt-candidate",async t=>{await this.handleTmuxPromptCandidate(t.snapshot)}),this.tmuxPaneObserver.on("observer-error",t=>{n.debug("Tmux pane observer error",{error:t})}),this.sessionWatcher.on("error",t=>{n.error("Session watcher error:",t)})}async handleSessionStarted(t){n.info("Handling new Codex session",{codexSessionId:t.id}),this.isInitializingSession=!0,this.bufferedLogEntries=[],this.sessionState&&await this.endActiveSession("new-codex-session-started");let e=process.env.CODEX_WORKING_DIRECTORY||t.cwd||process.cwd(),i=this.generateSessionId(t.id),s=this.appSyncClient.getCurrentUserId(),o={codexSessionId:t.id,cliVersion:t.cli_version,modelProvider:t.model_provider};try{let r=await(0,c.resumeOrCreateSession)({sessionId:i,userId:s,agentType:c.AgentType.CODEX,projectPath:e,metadata:o},this.appSyncClient,n);this.sessionKey=r.sessionKey}catch(r){throw n.error("Failed to create/resume session:",r),r}try{this.sessionState={sessionId:i,userId:s,projectPath:e,cwd:t.cwd,createdAt:new Date,subscriptionActive:!1,metadata:o,codexSessionId:t.id,codexLogFile:this.sessionWatcher.getActiveLogFile()||void 0},await this.flushBufferedLogEntries(),await this.startTmuxObserver(),this.subscribeToMobileEvents(i),this.appSyncClient.startHeartbeat(i)}catch(r){n.error("Failed to create session:",r),this.bufferedLogEntries=[]}finally{this.isInitializingSession=!1}}async flushBufferedLogEntries(){if(this.bufferedLogEntries.length===0)return;let t=this.bufferedLogEntries;this.bufferedLogEntries=[],n.info("Flushing buffered log entries after session initialization",{count:t.length,sessionId:this.sessionState?.sessionId});for(let e of t)await this.handleLogEntry(e)}async handleLogEntry(t){if(!this.sessionState){if(this.isInitializingSession){this.bufferedLogEntries.push(t),n.debug("Buffering log entry until session initialization completes",{type:t.type,bufferedCount:this.bufferedLogEntries.length});return}n.warn("Received log entry but no active session");return}if(t.type==="response_item"&&t.payload){let i=t.payload.type;i==="function_call"||i==="custom_tool_call"?this.approvalDetector.onToolCallStart(t.payload.call_id,t.payload.name,t.payload.arguments||t.payload.input||""):(i==="function_call_output"||i==="custom_tool_call_output")&&(this.approvalDetector.onToolCallComplete(t.payload.call_id),this.pendingInteractivePrompt?.callId===t.payload.call_id&&(this.pendingInteractivePrompt=null))}let e=K(t,this.sessionState.sessionId);if(e){if(this.hooksActive){if(e.type===c.EventType.USER_PROMPT||e.type===c.EventType.ASSISTANT_RESPONSE){n.debug("[JSONL] Skipping \u2014 hooks deliver this event type",{type:e.type});return}let i=t.payload?.type;if((i==="function_call"||i==="function_call_output")&&(e.type===c.EventType.TOOL_USE||e.type===c.EventType.INTERACTIVE_PROMPT)){n.debug("[JSONL] Skipping function_call \u2014 hooks deliver this",{type:e.type,tool:t.payload?.name});return}}try{if(this.sessionKey){if(e.content=c.cryptoService.encryptContent(e.content,this.sessionKey),e.metadata){let i=c.cryptoService.encryptMetadata(e.metadata,this.sessionKey);e.metadata={encrypted:i}}e.isEncrypted=!0,n.debug("Event encrypted",{type:e.type})}await this.appSyncClient.createEvent(e),n.debug("Event synced to backend",{type:e.type,encrypted:!!this.sessionKey})}catch(i){n.error("Failed to sync event:",i)}}}async handleApprovalPending(t){if(this.sessionState){n.info("Sending approval pending interactive prompt",t);try{let e=await this.tryParseInteractivePromptFromTmux(),i=e?.parsedPrompt??null;if(i&&this.pendingInteractivePrompt&&this.pendingInteractivePrompt.source==="tmux"&&this.pendingInteractivePrompt.promptText===i.promptText){n.debug("Skipping heuristic prompt because tmux prompt is already active",{promptText:i.promptText});return}let s=this.buildToolDetailsForInteractivePrompt(t,e?.snapshot),o=s.tool_name||this.mapToolNameForApproval(t.toolName),r=s.tool_input||this.buildFallbackToolInput(t),p=!!(o&&r),a=this.buildPromptPresentation(i),d=a.options,u=t.filePath?`File: ${t.filePath}`:void 0,h=a.content||`Codex is waiting for approval.
10
10
  ${t.hint}`;u&&!h.includes(u)&&(h=`${h}
11
11
  ${u}`),this.pendingInteractivePrompt={promptId:t.callId,callId:t.callId,kind:a.kind,options:d,submitMap:a.submitMap,promptText:a.promptText,createdAt:Date.now(),source:i?"tmux":"heuristic",requiresFollowUpText:a.requiresFollowUpText};let m={isApprovalHint:!0,toolName:t.toolName,toolInput:t.toolInput,hint:t.hint,callId:t.callId,filePath:t.filePath,diff:t.diff,rawInput:t.rawInput,tool_name:o,tool_input:r,has_details:p,options:d,instructions:a.instructions,prompt_source:i?"tmux":"heuristic"},P=!1;n.debug("Interactive prompt (pre-encryption)",{sessionId:this.sessionState.sessionId,callId:t.callId,contentPreview:h.substring(0,200),toolDetails:s,metadata:m}),this.sessionKey&&(h=c.cryptoService.encryptContent(h,this.sessionKey),m={encrypted:c.cryptoService.encryptMetadata(m,this.sessionKey)},P=!0),await this.appSyncClient.createEvent({sessionId:this.sessionState.sessionId,type:c.EventType.INTERACTIVE_PROMPT,source:c.EventSource.DESKTOP,content:h,metadata:m,promptId:t.callId,...P?{isEncrypted:!0}:{}})}catch(e){n.error("Failed to send approval interactive prompt:",e)}}}async handleTmuxPromptCandidate(t){if(!this.sessionState)return;let e=(0,_.parseInteractivePrompt)(t);if(!e||this.pendingInteractivePrompt&&this.pendingInteractivePrompt.source==="tmux"&&this.pendingInteractivePrompt.promptText===e.promptText)return;let i=this.buildPromptPresentation(e),s=this.getMostRecentPendingToolCall();s||(await new Promise(b=>setTimeout(b,500)),s=this.getMostRecentPendingToolCall());let o=s?this.buildApprovalPromptContextFromPendingCall(s):null,r=o?this.buildToolDetailsForInteractivePrompt(o,t):{},p=r.tool_name||this.mapToolNameForApproval(s?.name),a=r.tool_input||(o?this.buildFallbackToolInput(o):void 0),d=!!(p&&a),u=this.pendingInteractivePrompt?.callId||s?.callId||(0,X.v4)();this.pendingInteractivePrompt={promptId:u,callId:this.pendingInteractivePrompt?.callId||s?.callId,kind:i.kind,options:i.options,submitMap:i.submitMap,promptText:i.promptText,createdAt:Date.now(),source:"tmux",requiresFollowUpText:i.requiresFollowUpText};let h={options:i.options,instructions:i.instructions,prompt_source:"tmux_live",tool_name:p,tool_input:a,has_details:d},m=i.content,P=!1;this.sessionKey&&(m=c.cryptoService.encryptContent(m,this.sessionKey),h={encrypted:c.cryptoService.encryptMetadata(h,this.sessionKey)},P=!0);try{await this.appSyncClient.createEvent({sessionId:this.sessionState.sessionId,type:c.EventType.INTERACTIVE_PROMPT,source:c.EventSource.DESKTOP,content:m,metadata:h,promptId:u,...P?{isEncrypted:!0}:{}}),n.info("Sent tmux-detected interactive prompt",{sessionId:this.sessionState.sessionId,promptText:e.promptText,kind:e.kind})}catch(b){n.error("Failed to send tmux-detected interactive prompt",{error:b})}}async startTmuxObserver(){let t=process.env.CODEVIBE_CODEX_TMUX_SESSION;if(!t){n.debug("Skipping tmux pane observer start - no tmux session in environment");return}try{await this.tmuxPaneObserver.start(t)}catch(e){n.warn("Failed to start tmux pane observer",{tmuxSession:t,error:e})}}async tryParseInteractivePromptFromTmux(){try{let t=await this.tmuxPaneObserver.captureSnapshot(),e=(0,_.parseInteractivePrompt)(t);return n.debug("tmux prompt parse result",{parsed:!!e,kind:e?.kind,promptText:e?.promptText,snapshotPreview:this.summarizePromptSnapshot(t)}),{parsedPrompt:e,snapshot:t}}catch(t){return n.debug("tmux prompt parsing unavailable",{error:t}),null}}buildPromptPresentation(t){return t?{content:t.promptText,promptText:t.promptText,kind:t.kind,options:t.options,submitMap:t.submitMap,instructions:this.buildPromptInstructions(t),requiresFollowUpText:t.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}}getMostRecentPendingToolCall(){let t=this.approvalDetector.getPendingCalls();return t.length===0?null:t.reduce((e,i)=>i.timestamp>e.timestamp?i:e)}buildApprovalPromptContextFromPendingCall(t){return{toolName:t.name,filePath:t.filePath,diff:t.diff,toolInput:t.parsedInput,rawInput:t.input,hint:t.filePath?`File: ${t.filePath}`:`Tool: ${this.mapToolNameForApproval(t.name)||t.name}`}}buildPromptInstructions(t){return t.kind==="yes_no"&&t.requiresFollowUpText?"Reply with 1 to approve, or 2 followed by what to change":t.kind==="yes_no"?"Reply with 1 for yes or 2 for no":t.kind==="numbered"?"Reply with the number of the option you want":"Reply with your response"}summarizePromptSnapshot(t){return t.split(`
12
12
  `).map(e=>e.trimEnd()).filter(e=>e.length>0).slice(-12).map(e=>e.slice(0,160)).join(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quantiya/codevibe-codex-plugin",
3
- "version": "1.0.18",
3
+ "version": "1.0.19",
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": {
@@ -47,7 +47,7 @@
47
47
  "node": ">=18.0.0"
48
48
  },
49
49
  "dependencies": {
50
- "@quantiya/codevibe-core": "^1.0.15",
50
+ "@quantiya/codevibe-core": "^1.0.16",
51
51
  "chokidar": "^4.0.0",
52
52
  "dotenv": "^16.6.1",
53
53
  "express": "^5.1.0",