@quantiya/codevibe-codex-plugin 1.0.18 → 1.0.20

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.
@@ -41,6 +41,65 @@ done
41
41
  SCRIPT_DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
42
42
  PLUGIN_DIR="$(dirname "$SCRIPT_DIR")"
43
43
 
44
+ # ─── Wrapper telemetry (GA4 Measurement Protocol) ─────────────────────
45
+ # Diagnoses agent CLI failures: pre-flight bailouts, fast-die patterns,
46
+ # whether SessionStart hook fired, exit code. Background curl, fail
47
+ # silently, no PII (hashed hostname + per-run random id only). Honors
48
+ # CODEVIBE_TELEMETRY_SOURCE=test for internal testing.
49
+ _CV_MID="G-GS74YEQTB8"
50
+ _CV_SEC="lAfOF6OxRzSQ-NsLBRjhAg"
51
+ _CV_CID="$(echo "$(uname -n)-$(id -u)" | (sha256sum 2>/dev/null || shasum -a 256 2>/dev/null || echo "anonymous-fallback ") | cut -c1-36)"
52
+ _CV_RUN_ID="$(head -c 16 /dev/urandom 2>/dev/null | od -An -tx1 | tr -d ' \n' | cut -c1-32)"
53
+ [ -z "$_CV_RUN_ID" ] && _CV_RUN_ID="fallback-$(date +%s)-$$"
54
+ _CV_AGENT="codex"
55
+ _CV_SOURCE="${CODEVIBE_TELEMETRY_SOURCE:-production}"
56
+ _CV_STARTED_AT="$(date +%s)"
57
+ _CV_EXITED="" # set by terminal events; suppresses trap double-fire
58
+ _CV_PLUGIN_VERSION="$(node -p "require('$PLUGIN_DIR/package.json').version" 2>/dev/null || echo unknown)"
59
+ _CV_MCP_LOG="${CODEVIBE_TMPDIR}/codevibe-codex-mcp.log"
60
+ _CV_MCP_LOG_BASELINE=0
61
+ if [ -f "$_CV_MCP_LOG" ]; then
62
+ _CV_MCP_LOG_BASELINE=$(wc -l < "$_CV_MCP_LOG" 2>/dev/null | tr -d ' ')
63
+ [ -z "$_CV_MCP_LOG_BASELINE" ] && _CV_MCP_LOG_BASELINE=0
64
+ fi
65
+ _CV_TMUX_STARTED="false"
66
+ _CV_AGENT_INVOKED="false"
67
+ _CV_AGENT_STARTED_AT=0
68
+ _CV_CODEX_EXIT_FILE="${CODEVIBE_TMPDIR}/codevibe-codex-exit-$$"
69
+
70
+ # Strip an arbitrary string down to a JSON-safe identifier alphabet.
71
+ # Removes anything that could break the hand-built JSON payload below
72
+ # (quotes, backslashes, ANSI escapes, control bytes, tabs, newlines).
73
+ # Truncates to 40 chars to bound the impact of pathological CLI version
74
+ # output. Caller is responsible for emptiness check after sanitize.
75
+ cv_sanitize() {
76
+ printf '%s' "$1" | LC_ALL=C tr -cd 'A-Za-z0-9._\- ' | cut -c1-40
77
+ }
78
+
79
+ # Sanitize trusted-but-still-string values that go into the payload
80
+ # (plugin version, source label) so future schema additions can't
81
+ # accidentally reintroduce a JSON-injection path.
82
+ _CV_PLUGIN_VERSION="$(cv_sanitize "$_CV_PLUGIN_VERSION")"
83
+ [ -z "$_CV_PLUGIN_VERSION" ] && _CV_PLUGIN_VERSION="unknown"
84
+ _CV_SOURCE="$(cv_sanitize "$_CV_SOURCE")"
85
+ [ -z "$_CV_SOURCE" ] && _CV_SOURCE="production"
86
+
87
+ cv_telem() {
88
+ local event="$1"; shift
89
+ local params="$*"
90
+ curl -s -X POST \
91
+ "https://www.google-analytics.com/mp/collect?measurement_id=${_CV_MID}&api_secret=${_CV_SEC}" \
92
+ -H "Content-Type: application/json" \
93
+ -d "{\"client_id\":\"${_CV_CID}\",\"events\":[{\"name\":\"${event}\",\"params\":{\"agent\":\"${_CV_AGENT}\",\"plugin_version\":\"${_CV_PLUGIN_VERSION}\",\"source\":\"${_CV_SOURCE}\",\"run_id\":\"${_CV_RUN_ID}\"${params:+,$params}}}]}" \
94
+ </dev/null >/dev/null 2>&1 &
95
+ }
96
+
97
+ cv_failed() {
98
+ [ -n "$_CV_EXITED" ] && return 0
99
+ _CV_EXITED="failed"
100
+ cv_telem "wrapper_failed" "\"reason\":\"$1\",\"lifetime_seconds\":$(( $(date +%s) - _CV_STARTED_AT ))"
101
+ }
102
+
44
103
  # Handle auth commands (login, logout, status, reset-device)
45
104
  # Delegate to codevibe-core CLI (shared auth across all plugins)
46
105
  case "$1" in
@@ -51,14 +110,42 @@ case "$1" in
51
110
  CORE_CLI="$PLUGIN_DIR/../codevibe-core/bin/codevibe.js"
52
111
  fi
53
112
  if [ -f "$CORE_CLI" ]; then
113
+ cv_telem "wrapper_started" "\"invocation\":\"auth_$1\",\"os\":\"$(uname -s | cv_sanitize)\",\"arch\":\"$(uname -m | cv_sanitize)\""
54
114
  exec node "$CORE_CLI" "$1"
55
115
  else
56
116
  echo "Error: codevibe-core not found. Try reinstalling: npm install -g @quantiya/codevibe"
117
+ cv_failed "core_not_found"
118
+ sleep 1
57
119
  exit 1
58
120
  fi
59
121
  ;;
60
122
  esac
61
123
 
124
+ # Capture environment facts for the session-flow wrapper_started event.
125
+ # Each probe is non-fatal — if a CLI is missing we record "missing" rather
126
+ # than aborting; pre-flight checks below still gate execution. Every
127
+ # string that lands in the JSON payload goes through cv_sanitize so an
128
+ # agent CLI emitting ANSI escapes or quotes in `--version` can't break
129
+ # the hand-built payload.
130
+ _CV_CODEX_VER="missing"
131
+ command -v codex >/dev/null 2>&1 && _CV_CODEX_VER="$(codex --version 2>/dev/null | cv_sanitize)"
132
+ [ -z "$_CV_CODEX_VER" ] && _CV_CODEX_VER="unknown"
133
+ _CV_NODE_VER="missing"
134
+ command -v node >/dev/null 2>&1 && _CV_NODE_VER="$(node -v 2>/dev/null | cv_sanitize)"
135
+ [ -z "$_CV_NODE_VER" ] && _CV_NODE_VER="unknown"
136
+ _CV_TMUX_VER="missing"
137
+ command -v tmux >/dev/null 2>&1 && _CV_TMUX_VER="$(tmux -V 2>/dev/null | cv_sanitize)"
138
+ [ -z "$_CV_TMUX_VER" ] && _CV_TMUX_VER="unknown"
139
+ _CV_OS_VER="$(uname -s | cv_sanitize)"
140
+ [ -z "$_CV_OS_VER" ] && _CV_OS_VER="unknown"
141
+ _CV_ARCH_VER="$(uname -m | cv_sanitize)"
142
+ [ -z "$_CV_ARCH_VER" ] && _CV_ARCH_VER="unknown"
143
+ _CV_CODEX_AUTH="false"; [ -f "$HOME/.codex/auth.json" ] && _CV_CODEX_AUTH="true"
144
+ _CV_CODEX_CONFIG="false"; [ -f "$HOME/.codex/config.toml" ] && _CV_CODEX_CONFIG="true"
145
+ _CV_INSIDE_TMUX="false"; [ -n "$TMUX" ] && _CV_INSIDE_TMUX="true"
146
+ _CV_IS_TTY="false"; { [ -t 0 ] && [ -t 1 ]; } && _CV_IS_TTY="true"
147
+ cv_telem "wrapper_started" "\"invocation\":\"session\",\"os\":\"$_CV_OS_VER\",\"arch\":\"$_CV_ARCH_VER\",\"codex_version\":\"$_CV_CODEX_VER\",\"node_version\":\"$_CV_NODE_VER\",\"tmux_version\":\"$_CV_TMUX_VER\",\"codex_auth_present\":$_CV_CODEX_AUTH,\"codex_config_present\":$_CV_CODEX_CONFIG,\"inside_tmux\":$_CV_INSIDE_TMUX,\"is_terminal\":$_CV_IS_TTY"
148
+
62
149
  # Configuration
63
150
  TMUX_SESSION_PREFIX="codevibe-codex"
64
151
  LOG_FILE="${CODEVIBE_TMPDIR}/codevibe-codex-wrapper.log"
@@ -70,7 +157,52 @@ log() {
70
157
 
71
158
  # Cleanup function to kill server when wrapper exits
72
159
  cleanup() {
160
+ local wrapper_exit_code=$?
73
161
  log "Cleanup triggered"
162
+
163
+ # Fire wrapper_exited telemetry BEFORE killing the server so the MCP
164
+ # log is intact when we grep for SessionStart. cv_failed sets
165
+ # _CV_EXITED on pre-flight failures so this block won't double-fire.
166
+ if [ -z "$_CV_EXITED" ]; then
167
+ _CV_EXITED="exited"
168
+ local codex_exit="unknown"
169
+ if [ -f "$_CV_CODEX_EXIT_FILE" ]; then
170
+ codex_exit="$(cat "$_CV_CODEX_EXIT_FILE" 2>/dev/null | head -c 10 | tr -d '\n\r ')"
171
+ [ -z "$codex_exit" ] && codex_exit="unknown"
172
+ fi
173
+ local lifetime=$(( $(date +%s) - _CV_STARTED_AT ))
174
+ local codex_lifetime=0
175
+ if [ "$_CV_AGENT_STARTED_AT" -gt 0 ] 2>/dev/null; then
176
+ codex_lifetime=$(( $(date +%s) - _CV_AGENT_STARTED_AT ))
177
+ fi
178
+ local hook_fired="false"
179
+ if [ -f "$_CV_MCP_LOG" ]; then
180
+ if tail -n "+$((_CV_MCP_LOG_BASELINE + 1))" "$_CV_MCP_LOG" 2>/dev/null \
181
+ | grep -q "SessionStart" 2>/dev/null; then
182
+ hook_fired="true"
183
+ fi
184
+ fi
185
+ # Outcome priority: SIGINT/SIGTERM beats everything (user intent).
186
+ # Then "we never got far enough to invoke codex" — distinct from
187
+ # "we invoked codex via passthrough but never started a tmux of
188
+ # our own" (the latter is a normal direct-run, not an abort).
189
+ local outcome
190
+ if [ "$wrapper_exit_code" = "130" ] || [ "$wrapper_exit_code" = "143" ]; then
191
+ outcome="interrupted"
192
+ elif [ "$_CV_AGENT_INVOKED" = "false" ]; then
193
+ outcome="pre_invoke_abort"
194
+ elif [ "$codex_exit" != "unknown" ] && [ "$codex_exit" != "0" ]; then
195
+ outcome="error_exit"
196
+ elif [ "$codex_lifetime" -lt 5 ] 2>/dev/null; then
197
+ outcome="early_exit"
198
+ elif [ "$codex_lifetime" -lt 60 ] 2>/dev/null; then
199
+ outcome="clean_short"
200
+ else
201
+ outcome="clean_long"
202
+ fi
203
+ cv_telem "wrapper_exited" "\"exit_code\":$wrapper_exit_code,\"lifetime_seconds\":$lifetime,\"codex_exit_code\":\"$codex_exit\",\"codex_lifetime_seconds\":$codex_lifetime,\"tmux_session_started\":$_CV_TMUX_STARTED,\"agent_invoked\":$_CV_AGENT_INVOKED,\"session_start_hook_fired\":$hook_fired,\"terminal_outcome\":\"$outcome\""
204
+ fi
205
+
74
206
  if [ -n "$SERVER_PID" ] && kill -0 "$SERVER_PID" 2>/dev/null; then
75
207
  log "Stopping server (PID: $SERVER_PID)"
76
208
  kill "$SERVER_PID" 2>/dev/null || true
@@ -79,6 +211,7 @@ cleanup() {
79
211
  # Remove PID file and port file
80
212
  rm -f "${CODEVIBE_TMPDIR}/codevibe-codex-server-$$.pid"
81
213
  rm -f "${CODEVIBE_TMPDIR}/codevibe-codex-${SESSION_NAME}.port"
214
+ rm -f "$_CV_CODEX_EXIT_FILE"
82
215
 
83
216
  # Remove our hooks from ~/.codex/hooks.json (idempotent, concurrent-safe)
84
217
  if command -v jq &> /dev/null && [ -f "$HOME/.codex/hooks.json" ]; then
@@ -101,6 +234,8 @@ trap cleanup EXIT INT TERM
101
234
  if ! command -v tmux &> /dev/null; then
102
235
  echo "Error: tmux is required but not installed."
103
236
  echo "Install with: brew install tmux"
237
+ cv_failed "tmux_missing"
238
+ sleep 1
104
239
  exit 1
105
240
  fi
106
241
 
@@ -108,18 +243,24 @@ fi
108
243
  if ! command -v codex &> /dev/null; then
109
244
  echo "Error: codex CLI is not installed."
110
245
  echo "Install with: npm install -g @openai/codex"
246
+ cv_failed "codex_missing"
247
+ sleep 1
111
248
  exit 1
112
249
  fi
113
250
 
114
251
  # Check if node is installed
115
252
  if ! command -v node &> /dev/null; then
116
253
  echo "Error: Node.js is required but not installed."
254
+ cv_failed "node_missing"
255
+ sleep 1
117
256
  exit 1
118
257
  fi
119
258
 
120
259
  # Check if server is built
121
260
  if [ ! -f "$PLUGIN_DIR/dist/server.js" ]; then
122
261
  echo "Error: Server not built. Run 'npm run build' in the plugin directory first."
262
+ cv_failed "server_not_built"
263
+ sleep 1
123
264
  exit 1
124
265
  fi
125
266
 
@@ -131,16 +272,36 @@ log "Starting codevibe-codex with session: $SESSION_NAME"
131
272
  log "Working directory: $WORKING_DIR"
132
273
  log "Arguments: $*"
133
274
 
134
- # Check if we're already inside tmux
275
+ # Check if we're already inside tmux.
276
+ # We deliberately do NOT `exec` here — running codex as a child process
277
+ # lets the EXIT trap fire after it returns so wrapper_exited still gets
278
+ # emitted on these direct-run paths. Behaviorally identical for the user
279
+ # (codex remains the foreground process for the duration).
135
280
  if [ -n "$TMUX" ]; then
136
281
  log "Already inside tmux, running codex directly"
137
- exec codex "$@"
282
+ _CV_AGENT_INVOKED="true"
283
+ _CV_AGENT_STARTED_AT="$(date +%s)"
284
+ # `|| _CV_RC=$?` is load-bearing: with `set -e`, a non-zero exit from
285
+ # codex would abort the wrapper before we capture the exit code,
286
+ # leaving wrapper_exited with codex_exit_code="unknown". The `||`
287
+ # form catches non-zero without triggering set -e, while exit 0
288
+ # leaves _CV_RC at its 0 default. printf's `|| true` keeps a
289
+ # disk-full failure from clobbering diagnostics.
290
+ _CV_RC=0
291
+ codex "$@" || _CV_RC=$?
292
+ printf '%s' "$_CV_RC" > "$_CV_CODEX_EXIT_FILE" 2>/dev/null || true
293
+ exit "$_CV_RC"
138
294
  fi
139
295
 
140
- # Check if running in a terminal
296
+ # Check if running in a terminal — same direct-run treatment as above.
141
297
  if [ ! -t 0 ] || [ ! -t 1 ]; then
142
298
  log "Not running in a terminal, running codex directly"
143
- exec codex "$@"
299
+ _CV_AGENT_INVOKED="true"
300
+ _CV_AGENT_STARTED_AT="$(date +%s)"
301
+ _CV_RC=0
302
+ codex "$@" || _CV_RC=$?
303
+ printf '%s' "$_CV_RC" > "$_CV_CODEX_EXIT_FILE" 2>/dev/null || true
304
+ exit "$_CV_RC"
144
305
  fi
145
306
 
146
307
  # Start the session log watcher server in background
@@ -224,6 +385,8 @@ if ! kill -0 "$SERVER_PID" 2>/dev/null; then
224
385
  tail -3 "$MCP_LOG_FILE" 2>/dev/null | grep -v '^\[' | head -1
225
386
  echo ""
226
387
  echo "Server failed to start. Check $MCP_LOG_FILE for details."
388
+ cv_failed "server_died_on_startup"
389
+ sleep 1
227
390
  exit 1
228
391
  fi
229
392
 
@@ -238,9 +401,15 @@ for arg in "$@"; do
238
401
  CODEX_CMD="$CODEX_CMD '$escaped_arg'"
239
402
  done
240
403
 
241
- # Create the session running codex
404
+ # Create the session running codex.
405
+ # The inner shell writes codex's exit code to $_CV_CODEX_EXIT_FILE so the
406
+ # wrapper's cleanup trap can report it via `wrapper_exited` telemetry —
407
+ # tmux's own attach exit code is independent of the inner process exit.
242
408
  tmux new-session -d -s "$SESSION_NAME" -x "$(tput cols)" -y "$(tput lines)" \
243
- "export CODEVIBE_CODEX_TMUX_SESSION='$SESSION_NAME'; export ENVIRONMENT='$ENVIRONMENT'; $CODEX_CMD; exit"
409
+ "export CODEVIBE_CODEX_TMUX_SESSION='$SESSION_NAME'; export ENVIRONMENT='$ENVIRONMENT'; $CODEX_CMD; printf '%s' \"\$?\" > '$_CV_CODEX_EXIT_FILE'; exit"
410
+ _CV_TMUX_STARTED="true"
411
+ _CV_AGENT_INVOKED="true"
412
+ _CV_AGENT_STARTED_AT="$(date +%s)"
244
413
 
245
414
  # Enable mouse support for scrolling
246
415
  tmux set-option -t "$SESSION_NAME" -g mouse on
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.20",
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",