@quantiya/codevibe-codex-plugin 1.0.23 → 1.0.25

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.
@@ -237,14 +237,71 @@ cleanup() {
237
237
  rm -f "${CODEVIBE_TMPDIR}/codevibe-codex-${SESSION_NAME}.port"
238
238
  rm -f "$_CV_CODEX_EXIT_FILE"
239
239
 
240
- # Remove our hooks from ~/.codex/hooks.json (idempotent, concurrent-safe)
241
- if command -v jq &> /dev/null && [ -f "$HOME/.codex/hooks.json" ]; then
240
+ # Remove our hooks from ~/.codex/hooks.json (idempotent, concurrent-safe).
241
+ # Uses Node (always available required to run the daemon) so users
242
+ # without jq don't end up with stale CodeVibe hook entries leftover
243
+ # after the wrapper exits. Ownership check is path-prefix + filename
244
+ # tail (NOT a bare substring "codevibe-codex" match) so user paths
245
+ # that happen to contain the string codevibe-codex (e.g. a username,
246
+ # a directory like /Users/codevibe-codex/) cannot trigger removal of
247
+ # the user's unrelated hooks.
248
+ if [ -f "$HOME/.codex/hooks.json" ] && command -v node &> /dev/null; then
242
249
  # Only remove if no other codevibe-codex sessions are running
243
250
  if [ "$(pgrep -f 'codevibe-codex' | wc -l)" -le 1 ]; then
244
251
  log "Removing CodeVibe hooks from $HOME/.codex/hooks.json"
245
- jq 'del(.hooks[] | .[] | select(.hooks[]?.command | contains("codevibe-codex")))' \
246
- "$HOME/.codex/hooks.json" > "$HOME/.codex/hooks.json.tmp" 2>/dev/null && \
247
- mv "$HOME/.codex/hooks.json.tmp" "$HOME/.codex/hooks.json" || true
252
+ # Cleanup mirrors install: same ownership predicate + the
253
+ # same INNER-hooks[] split so a matcher entry mixing user
254
+ # hooks with ours keeps the user hook. Same per-process
255
+ # tmp suffix avoids two concurrent cleanups racing.
256
+ if CV_HOOKS_FILE="$HOME/.codex/hooks.json" node -e '
257
+ const fs = require("fs");
258
+ const OUR_HOOK_FILES = new Set([
259
+ "session-start.sh",
260
+ "user-prompt.sh",
261
+ "pre-tool-use.sh",
262
+ "post-tool-use.sh",
263
+ "stop.sh",
264
+ ]);
265
+ const OWN_PATH_MARKER = "codevibe-codex-plugin/hooks/";
266
+ const isOurCommand = (command) => {
267
+ if (typeof command !== "string") return false;
268
+ if (command.indexOf(OWN_PATH_MARKER) === -1) return false;
269
+ for (const f of OUR_HOOK_FILES) {
270
+ if (command.endsWith("/" + f)) return true;
271
+ }
272
+ return false;
273
+ };
274
+ const stripOurHooksFromEntry = (entry) => {
275
+ if (!entry || typeof entry !== "object" || !Array.isArray(entry.hooks)) return entry;
276
+ const filtered = entry.hooks.filter((h) => !(h && isOurCommand(h.command)));
277
+ if (filtered.length === entry.hooks.length) return entry;
278
+ if (filtered.length === 0) return null;
279
+ return { ...entry, hooks: filtered };
280
+ };
281
+ try {
282
+ const target = process.env.CV_HOOKS_FILE;
283
+ const data = JSON.parse(fs.readFileSync(target, "utf8"));
284
+ if (data && typeof data === "object" && data.hooks && typeof data.hooks === "object") {
285
+ for (const k of Object.keys(data.hooks)) {
286
+ if (Array.isArray(data.hooks[k])) {
287
+ data.hooks[k] = data.hooks[k]
288
+ .map(stripOurHooksFromEntry)
289
+ .filter((entry) => entry !== null && entry !== undefined);
290
+ }
291
+ }
292
+ }
293
+ const tmp = target + "." + process.pid + "." + Date.now() + ".tmp";
294
+ fs.writeFileSync(tmp, JSON.stringify(data, null, 2));
295
+ fs.renameSync(tmp, target);
296
+ } catch (e) {
297
+ process.stderr.write("CLEAN_FAILED:" + (e && e.message ? e.message : String(e)));
298
+ process.exit(1);
299
+ }
300
+ ' 2>>"$LOG_FILE"; then
301
+ :
302
+ else
303
+ log "WARN: failed to clean CodeVibe hooks from hooks.json (left as-is)"
304
+ fi
248
305
  else
249
306
  log "Other codevibe-codex sessions active, keeping hooks"
250
307
  fi
@@ -368,28 +425,203 @@ else
368
425
  fi
369
426
 
370
427
  mkdir -p "$HOME/.codex"
371
- if [ -f "$CODEX_HOOKS_FILE" ]; then
372
- if grep -q "codevibe-codex" "$CODEX_HOOKS_FILE" 2>/dev/null; then
373
- log "CodeVibe hooks already installed in $CODEX_HOOKS_FILE"
374
- elif command -v jq &> /dev/null; then
375
- log "Merging CodeVibe hooks into existing $CODEX_HOOKS_FILE"
376
- GENERATED=$(generate_hooks_json)
377
- jq -s '.[0] as $existing | .[1] as $new |
378
- $existing | .hooks = (
379
- ($existing.hooks // {}) as $eh |
380
- ($new.hooks // {}) as $nh |
381
- ($eh | keys) + ($nh | keys) | unique | map(
382
- { (.) : (($eh[.] // []) + ($nh[.] // [])) }
383
- ) | add // {}
384
- )' "$CODEX_HOOKS_FILE" <(echo "$GENERATED") > "${CODEX_HOOKS_FILE}.tmp" && \
385
- mv "${CODEX_HOOKS_FILE}.tmp" "$CODEX_HOOKS_FILE"
386
- else
387
- log "WARN: jq not found and existing hooks.json exists — cannot merge safely"
388
- fi
428
+ # Hook installation outcome feeds the hooks_install_outcome telemetry
429
+ # beacon below. Distinguishes fresh install, merge-into-existing,
430
+ # already-installed, and failure modes. Codex events flow ONLY through
431
+ # hooks (no JSONL fallback), so a silent skip here = a 0-event ghost
432
+ # session in DDB.
433
+ _CV_HOOKS_OUTCOME="unknown"
434
+ _CV_HOOKS_REASON=""
435
+
436
+ # Single Node-based hook installer: handles fresh install, idempotent
437
+ # merge into the user's existing hooks.json, and "already installed"
438
+ # detection all with a stable ownership predicate.
439
+ #
440
+ # Design notes:
441
+ #
442
+ # - "already installed" detection: a structured walk of the JSON
443
+ # looking for our specific hook-script filenames at expected event
444
+ # keys. Replaces the prior `grep -q "codevibe-codex"` substring
445
+ # check, which false-positived on unrelated user paths containing
446
+ # that string.
447
+ #
448
+ # - Merge dedupe: filters our previously-installed entries out of
449
+ # each event key's array, then appends the fresh template entries.
450
+ # Idempotent — multiple runs (including concurrent ones) converge
451
+ # to the same hooks.json. Filters at the INNER hooks[] item level
452
+ # so a matcher entry that combines a user hook with one of ours
453
+ # keeps the user hook (only the CodeVibe hooks get stripped, not
454
+ # the surrounding entry).
455
+ #
456
+ # - Ownership predicate: path-prefix `codevibe-codex-plugin/hooks/`
457
+ # AND filename tail in the allow-list of our 5 hook script names.
458
+ # Rejects user paths like /Users/codevibe-codex/foo.sh.
459
+ #
460
+ # - Per-process tmp file (.<pid>.<time>.tmp): two concurrent installs
461
+ # won't clobber each other's intermediate state. Final rename is
462
+ # POSIX-atomic — last writer wins with content equivalent to what
463
+ # the first writer would have produced (idempotency by construction).
464
+ #
465
+ # Wrapping in `if … ; then … else INSTALLER_RC=$? ; fi` is required to
466
+ # capture non-zero exit codes; under `set -e`, a bare `VAR=$(cmd)`
467
+ # followed by `RC=$?` would short-circuit on Node's non-zero exit
468
+ # before $? gets read, defeating the entire fail-closed mapping below.
469
+ GENERATED_HOOKS=$(generate_hooks_json)
470
+ if INSTALLER_OUTPUT=$(CV_EXISTING="$CODEX_HOOKS_FILE" \
471
+ CV_NEW="$GENERATED_HOOKS" \
472
+ node -e '
473
+ const fs = require("fs");
474
+ const OUR_HOOK_FILES = new Set([
475
+ "session-start.sh",
476
+ "user-prompt.sh",
477
+ "pre-tool-use.sh",
478
+ "post-tool-use.sh",
479
+ "stop.sh",
480
+ ]);
481
+ const OWN_PATH_MARKER = "codevibe-codex-plugin/hooks/";
482
+ const isOurCommand = (command) => {
483
+ if (typeof command !== "string") return false;
484
+ if (command.indexOf(OWN_PATH_MARKER) === -1) return false;
485
+ for (const f of OUR_HOOK_FILES) {
486
+ if (command.endsWith("/" + f)) return true;
487
+ }
488
+ return false;
489
+ };
490
+ // Strip our hooks from a single matcher entrys inner hooks[] array.
491
+ // Returns null if NO non-owned hooks remain (whole entry should be
492
+ // dropped); otherwise returns a new entry with only the user-owned
493
+ // hooks. Returns the original entry unchanged if it contains none of
494
+ // ours (preserves identity for unrelated entries).
495
+ const stripOurHooksFromEntry = (entry) => {
496
+ if (!entry || typeof entry !== "object" || !Array.isArray(entry.hooks)) {
497
+ return entry;
498
+ }
499
+ const filtered = entry.hooks.filter((h) => !(h && isOurCommand(h.command)));
500
+ if (filtered.length === entry.hooks.length) return entry;
501
+ if (filtered.length === 0) return null;
502
+ return { ...entry, hooks: filtered };
503
+ };
504
+ // Does this matcher entry contain at least one of our hooks anywhere
505
+ // in its inner hooks[]? Used by the "already installed" walk only —
506
+ // it does NOT decide whether the entry survives a strip.
507
+ const entryContainsOurs = (entry) =>
508
+ entry && typeof entry === "object" && Array.isArray(entry.hooks) &&
509
+ entry.hooks.some((h) => h && isOurCommand(h.command));
510
+
511
+ let existingObj = null;
512
+ let existed = false;
513
+ try {
514
+ if (fs.existsSync(process.env.CV_EXISTING)) {
515
+ existed = true;
516
+ existingObj = JSON.parse(fs.readFileSync(process.env.CV_EXISTING, "utf8"));
517
+ }
518
+ } catch (e) {
519
+ process.stderr.write("PARSE_EXISTING_FAILED:" + (e && e.message ? e.message : String(e)));
520
+ process.exit(2);
521
+ }
522
+ let nextObj;
523
+ try {
524
+ nextObj = JSON.parse(process.env.CV_NEW);
525
+ } catch (e) {
526
+ process.stderr.write("PARSE_NEW_FAILED:" + (e && e.message ? e.message : String(e)));
527
+ process.exit(3);
528
+ }
529
+
530
+ const existingHooks = (existingObj && typeof existingObj === "object" &&
531
+ existingObj.hooks && typeof existingObj.hooks === "object") ? existingObj.hooks : {};
532
+ const nextHooks = (nextObj && typeof nextObj === "object" &&
533
+ nextObj.hooks && typeof nextObj.hooks === "object") ? nextObj.hooks : {};
534
+ let allPresent = existed;
535
+ for (const k of Object.keys(nextHooks)) {
536
+ const arr = Array.isArray(existingHooks[k]) ? existingHooks[k] : [];
537
+ if (!arr.some(entryContainsOurs)) {
538
+ allPresent = false;
539
+ break;
540
+ }
541
+ }
542
+ if (allPresent) {
543
+ process.stdout.write("OUTCOME:already_installed\n");
544
+ process.exit(0);
545
+ }
546
+
547
+ const mergedHooks = { ...existingHooks };
548
+ for (const k of Object.keys(nextHooks)) {
549
+ const existingArr = Array.isArray(existingHooks[k]) ? existingHooks[k] : [];
550
+ const cleanedExisting = existingArr
551
+ .map(stripOurHooksFromEntry)
552
+ .filter((entry) => entry !== null && entry !== undefined);
553
+ const nextArr = Array.isArray(nextHooks[k]) ? nextHooks[k] : [];
554
+ mergedHooks[k] = [...cleanedExisting, ...nextArr];
555
+ }
556
+
557
+ const out = (existingObj && typeof existingObj === "object")
558
+ ? { ...existingObj, hooks: mergedHooks }
559
+ : { hooks: mergedHooks };
560
+ const outcome = existed ? "merged" : "fresh_install";
561
+ const target = process.env.CV_EXISTING;
562
+ // Per-process tmp suffix avoids two concurrent installs both writing
563
+ // ${target}.tmp and one mvs the others half-written file.
564
+ const tmp = target + "." + process.pid + "." + Date.now() + ".tmp";
565
+ try {
566
+ fs.writeFileSync(tmp, JSON.stringify(out, null, 2));
567
+ fs.renameSync(tmp, target);
568
+ } catch (e) {
569
+ try { fs.unlinkSync(tmp); } catch {}
570
+ process.stderr.write("WRITE_FAILED:" + (e && e.message ? e.message : String(e)));
571
+ process.exit(4);
572
+ }
573
+ process.stdout.write("OUTCOME:" + outcome + "\n");
574
+ ' 2>>"$LOG_FILE"); then
575
+ INSTALLER_RC=0
576
+ else
577
+ INSTALLER_RC=$?
578
+ fi
579
+ if [ "$INSTALLER_RC" -eq 0 ]; then
580
+ case "$INSTALLER_OUTPUT" in
581
+ OUTCOME:already_installed*)
582
+ log "CodeVibe hooks already installed in $CODEX_HOOKS_FILE"
583
+ _CV_HOOKS_OUTCOME="already_installed"
584
+ ;;
585
+ OUTCOME:merged*)
586
+ log "Merged CodeVibe hooks into existing $CODEX_HOOKS_FILE"
587
+ _CV_HOOKS_OUTCOME="merged"
588
+ ;;
589
+ OUTCOME:fresh_install*)
590
+ log "Installed CodeVibe hooks to $CODEX_HOOKS_FILE"
591
+ _CV_HOOKS_OUTCOME="fresh_install"
592
+ ;;
593
+ *)
594
+ _CV_HOOKS_OUTCOME="merge_failed"
595
+ _CV_HOOKS_REASON="unrecognized_outcome"
596
+ log "ERROR: hooks installer returned unrecognized outcome: $INSTALLER_OUTPUT"
597
+ ;;
598
+ esac
389
599
  else
390
- log "Installing CodeVibe hooks to $CODEX_HOOKS_FILE"
391
- generate_hooks_json > "$CODEX_HOOKS_FILE"
600
+ case "$INSTALLER_RC" in
601
+ 2) _CV_HOOKS_OUTCOME="merge_failed"; _CV_HOOKS_REASON="parse_existing_error" ;;
602
+ 3) _CV_HOOKS_OUTCOME="merge_failed"; _CV_HOOKS_REASON="parse_template_error" ;;
603
+ 4) _CV_HOOKS_OUTCOME="merge_failed"; _CV_HOOKS_REASON="write_failed" ;;
604
+ *) _CV_HOOKS_OUTCOME="merge_failed"; _CV_HOOKS_REASON="installer_exit_$INSTALLER_RC" ;;
605
+ esac
606
+ log "ERROR: hooks installer failed (rc=$INSTALLER_RC, reason=$_CV_HOOKS_REASON)"
392
607
  fi
608
+ cv_telem "hooks_install_outcome" "\"outcome\":\"$_CV_HOOKS_OUTCOME\",\"reason\":\"$_CV_HOOKS_REASON\""
609
+
610
+ # If hook installation didn't land, there's no point starting the daemon —
611
+ # Codex events flow ONLY through hooks. Bail with a clear message so the
612
+ # user can fix the underlying file/permission issue rather than running
613
+ # a silent ghost session.
614
+ case "$_CV_HOOKS_OUTCOME" in
615
+ fresh_install|merged|already_installed) ;;
616
+ *)
617
+ echo "Error: failed to install CodeVibe hooks into $CODEX_HOOKS_FILE ($_CV_HOOKS_OUTCOME)."
618
+ echo " Without hooks installed, Codex sessions cannot sync to mobile."
619
+ echo " Check $LOG_FILE for the underlying error."
620
+ cv_failed "hooks_install_$_CV_HOOKS_OUTCOME"
621
+ sleep 1
622
+ exit 1
623
+ ;;
624
+ esac
393
625
 
394
626
  # Start server and capture its PID
395
627
  node "$PLUGIN_DIR/dist/server.js" >> "$MCP_LOG_FILE" 2>&1 &
package/dist/server.js CHANGED
@@ -1,22 +1,22 @@
1
- "use strict";var Pt=Object.create;var F=Object.defineProperty;var bt=Object.getOwnPropertyDescriptor;var wt=Object.getOwnPropertyNames;var It=Object.getPrototypeOf,_t=Object.prototype.hasOwnProperty;var Y=(l,t)=>()=>(l&&(t=l(l=0)),t);var Et=(l,t)=>{for(var e in t)F(l,e,{get:t[e],enumerable:!0})},Z=(l,t,e,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of wt(t))!_t.call(l,s)&&s!==e&&F(l,s,{get:()=>t[s],enumerable:!(i=bt(t,s))||i.enumerable});return l};var g=(l,t,e)=>(e=l!=null?Pt(It(l)):{},Z(t||!l||!l.__esModule?F(e,"default",{value:l,enumerable:!0}):e,l)),Tt=l=>Z(F({},"__esModule",{value:!0}),l);var Q,tt,et,n,I=Y(()=>{"use strict";Q=g(require("os")),tt=g(require("path")),et=require("@quantiya/codevibe-core"),n=(0,et.createLogger)({name:"codevibe-codex",logFile:tt.default.join(Q.default.tmpdir(),"codevibe-codex-mcp.log"),level:"debug"})});var pt={};Et(pt,{clearPendingCalls:()=>H,extractFileFromPatch:()=>at,extractOldNewFromPatch:()=>rt,getPendingCall:()=>Ot,getPendingCallsCount:()=>Ft,mapLogEntryToEvent:()=>K});function K(l,t){let e={sessionId:t,source:v.EventSource.DESKTOP};if(l.type==="event_msg"&&l.payload){let i=l.payload.type;switch(i){case"user_message":return{...e,type:v.EventType.USER_PROMPT,content:l.payload.message||"",metadata:{images:l.payload.images||[]}};case"agent_message":return{...e,type:v.EventType.ASSISTANT_RESPONSE,content:l.payload.message||""};case"agent_reasoning":return{...e,type:v.EventType.REASONING,content:l.payload.text||""};case"token_count":return n.debug("Skipping token_count entry"),null;default:return n.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:r}=l.payload,p={};try{p=JSON.parse(o||"{}")}catch{p={raw:o}}let a=(0,$.v4)();S.set(r,{name:s,input:o,eventId:a});let d=W(s),u=xt(s,p);return{...e,type:v.EventType.TOOL_USE,content:u,metadata:{toolName:d,toolInput:p,callId:r,status:"running"}}}if(i==="function_call_output"){let{call_id:s,output:o}=l.payload,r=S.get(s);S.delete(s);let p=r?.name?W(r.name):"Tool",a=Ct(o,500);return{...e,type:v.EventType.TOOL_USE,content:`${p} completed:
2
- ${a}`,metadata:{toolName:p,toolOutput:o,callId:s,status:"completed"}}}if(i==="custom_tool_call"){let{name:s,call_id:o,input:r,status:p}=l.payload;S.set(o,{name:s,input:r,eventId:(0,$.v4)()});let a=at(r),{oldString:d,newString:u}=rt(r),h=a?`Editing: ${a.filePath}`:"Applying patch";return{...e,type:v.EventType.TOOL_USE,content:h,metadata:{tool_name:"Edit",tool_input:{file_path:a?.filePath||"",old_string:d,new_string:u},callId:o,status:p||"running"}}}if(i==="custom_tool_call_output"){let{call_id:s,output:o}=l.payload,r=S.get(s);S.delete(s);let p={};try{p=JSON.parse(o||"{}")}catch{p={raw:o}}let a=p.output?.includes("Success")||!p.error;return{...e,type:v.EventType.TOOL_USE,content:a?"File edit applied successfully":`Edit failed: ${p.error||"Unknown error"}`,metadata:{toolName:"Edit",toolOutput:p,callId:s,status:"completed",success:a}}}return n.debug("Unknown response_item type",{type:i}),null}return l.type==="turn_context"?(n.debug("Skipping turn_context entry"),null):(n.debug("Unhandled log entry type",{type:l.type}),null)}function W(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 xt(l,t){switch(l){case"shell_command":case"shell":return`Running: ${t.command||"command"}`;case"read_file":return`Reading: ${t.file_path||t.path||"file"}`;case"write_file":return`Writing: ${t.file_path||t.path||"file"}`;case"list_files":return`Listing: ${t.path||"."}`;case"search_files":return`Searching for: ${t.pattern||t.query||"pattern"}`;case"web_search":return`Searching web: ${t.query||"query"}`;default:return`Running ${W(l)}`}}function rt(l){let t=[],e=[],i=/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/;for(let s of l.split(`
3
- `))if(!(i.test(s)||s.startsWith("***")||s.startsWith("---")||s.startsWith("+++"))){if(s.startsWith("-"))t.push(s.slice(1));else if(s.startsWith("+"))e.push(s.slice(1));else if(s.startsWith(" ")){let o=s.slice(1);t.push(o),e.push(o)}}return{oldString:t.join(`
1
+ "use strict";var Et=Object.create;var O=Object.defineProperty;var It=Object.getOwnPropertyDescriptor;var Tt=Object.getOwnPropertyNames;var xt=Object.getPrototypeOf,Ct=Object.prototype.hasOwnProperty;var Z=(a,t)=>()=>(a&&(t=a(a=0)),t);var Ft=(a,t)=>{for(var e in t)O(a,e,{get:t[e],enumerable:!0})},tt=(a,t,e,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of Tt(t))!Ct.call(a,i)&&i!==e&&O(a,i,{get:()=>t[i],enumerable:!(s=It(t,i))||s.enumerable});return a};var f=(a,t,e)=>(e=a!=null?Et(xt(a)):{},tt(t||!a||!a.__esModule?O(e,"default",{value:a,enumerable:!0}):e,a)),Ot=a=>tt(O({},"__esModule",{value:!0}),a);var et,st,it,n,E=Z(()=>{"use strict";et=f(require("os")),st=f(require("path")),it=require("@quantiya/codevibe-core"),n=(0,it.createLogger)({name:"codevibe-codex",logFile:st.default.join(et.default.tmpdir(),"codevibe-codex-mcp.log"),level:"debug"})});var ct={};Ft(ct,{clearPendingCalls:()=>j,extractFileFromPatch:()=>lt,extractOldNewFromPatch:()=>pt,getPendingCall:()=>Mt,getPendingCallsCount:()=>Nt,mapLogEntryToEvent:()=>B});function B(a,t){let e={sessionId:t,source:S.EventSource.DESKTOP};if(a.type==="event_msg"&&a.payload){let s=a.payload.type;switch(s){case"user_message":return{...e,type:S.EventType.USER_PROMPT,content:a.payload.message||"",metadata:{images:a.payload.images||[]}};case"agent_message":return{...e,type:S.EventType.ASSISTANT_RESPONSE,content:a.payload.message||""};case"agent_reasoning":return{...e,type:S.EventType.REASONING,content:a.payload.text||""};case"token_count":return n.debug("Skipping token_count entry"),null;default:return n.debug("Unknown event_msg type",{type:s}),null}}if(a.type==="response_item"&&a.payload){let s=a.payload.type;if(s==="function_call"){let{name:i,arguments:o,call_id:r}=a.payload,l={};try{l=JSON.parse(o||"{}")}catch{l={raw:o}}let p=(0,W.v4)();_.set(r,{name:i,input:o,eventId:p});let d=K(i),u=Rt(i,l);return{...e,type:S.EventType.TOOL_USE,content:u,metadata:{toolName:d,toolInput:l,callId:r,status:"running"}}}if(s==="function_call_output"){let{call_id:i,output:o}=a.payload,r=_.get(i);_.delete(i);let l=r?.name?K(r.name):"Tool",p=At(o,500);return{...e,type:S.EventType.TOOL_USE,content:`${l} completed:
2
+ ${p}`,metadata:{toolName:l,toolOutput:o,callId:i,status:"completed"}}}if(s==="custom_tool_call"){let{name:i,call_id:o,input:r,status:l}=a.payload;_.set(o,{name:i,input:r,eventId:(0,W.v4)()});let p=lt(r),{oldString:d,newString:u}=pt(r),h=p?`Editing: ${p.filePath}`:"Applying patch";return{...e,type:S.EventType.TOOL_USE,content:h,metadata:{tool_name:"Edit",tool_input:{file_path:p?.filePath||"",old_string:d,new_string:u},callId:o,status:l||"running"}}}if(s==="custom_tool_call_output"){let{call_id:i,output:o}=a.payload,r=_.get(i);_.delete(i);let l={};try{l=JSON.parse(o||"{}")}catch{l={raw:o}}let p=l.output?.includes("Success")||!l.error;return{...e,type:S.EventType.TOOL_USE,content:p?"File edit applied successfully":`Edit failed: ${l.error||"Unknown error"}`,metadata:{toolName:"Edit",toolOutput:l,callId:i,status:"completed",success:p}}}return n.debug("Unknown response_item type",{type:s}),null}return a.type==="turn_context"?(n.debug("Skipping turn_context entry"),null):(n.debug("Unhandled log entry type",{type:a.type}),null)}function K(a){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"}[a]||a}function Rt(a,t){switch(a){case"shell_command":case"shell":return`Running: ${t.command||"command"}`;case"read_file":return`Reading: ${t.file_path||t.path||"file"}`;case"write_file":return`Writing: ${t.file_path||t.path||"file"}`;case"list_files":return`Listing: ${t.path||"."}`;case"search_files":return`Searching for: ${t.pattern||t.query||"pattern"}`;case"web_search":return`Searching web: ${t.query||"query"}`;default:return`Running ${K(a)}`}}function pt(a){let t=[],e=[],s=/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/;for(let i of a.split(`
3
+ `))if(!(s.test(i)||i.startsWith("***")||i.startsWith("---")||i.startsWith("+++"))){if(i.startsWith("-"))t.push(i.slice(1));else if(i.startsWith("+"))e.push(i.slice(1));else if(i.startsWith(" ")){let o=i.slice(1);t.push(o),e.push(o)}}return{oldString:t.join(`
4
4
  `),newString:e.join(`
5
- `)}}function at(l){if(!l)return null;let t=l.match(/\*\*\* (?:Update|Add|Delete) File: (.+)/);return t?{filePath:t[1].trim()}:null}function Ct(l,t){return l?l.length<=t?l:l.substring(0,t)+"...":""}function H(){S.clear()}function Ft(){return S.size}function Ot(l){return S.get(l)}var $,v,S,j=Y(()=>{"use strict";$=require("uuid"),v=require("@quantiya/codevibe-core");I();S=new Map});var X=require("uuid"),T=g(require("fs")),C=g(require("path")),St=g(require("os")),c=require("@quantiya/codevibe-core");I();var it=require("events"),y=g(require("fs")),R=g(require("path")),st=g(require("readline")),nt=require("chokidar"),ot=require("@quantiya/codevibe-core");I();var O=class extends it.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}start(){if(this.isWatching){n.warn("Session log watcher already running");return}let e=(0,ot.getConfig)().codex.sessionsDir;this.sessionsDir=e,n.info("Starting Codex session log watcher",{sessionsDir:e}),y.existsSync(e)||(n.info("Codex sessions directory does not exist yet, creating...",{sessionsDir:e}),y.mkdirSync(e,{recursive:!0})),this.startTime=Date.now(),this.watcher=(0,nt.watch)(e,{persistent:!0,ignoreInitial:!0,awaitWriteFinish:{stabilityThreshold:100,pollInterval:50},depth:4,ignored:i=>{let s=R.basename(i);return y.existsSync(i)&&y.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=>{n.error("Watcher error:",i),this.emit("error",i)}),this.watcher.on("ready",()=>{n.info("Session log watcher ready"),this.bindRecentSessionFile()}),this.isWatching=!0,n.info("Session log watcher started")}stop(){this.watcher&&(this.watcher.close(),this.watcher=null),this.isWatching=!1,this.filePositions.clear(),this.activeLogFile=null,this.sessionId=null,this.sessionsDir=null,n.info("Session log watcher stopped")}getSessionId(){return this.sessionId}getActiveLogFile(){return this.activeLogFile}onFileAdded(e){try{let i=y.statSync(e),s=i.birthtimeMs||i.ctimeMs;if(s<this.startTime-5e3){n.debug("Ignoring old session file",{filePath:e,fileCreatedAt:new Date(s).toISOString(),startTime:new Date(this.startTime).toISOString()});return}}catch(i){n.warn("Could not check file creation time",{filePath:e,error:i})}if(this.activeLogFile){n.debug("Ignoring additional Codex session log for this server instance",{activeLogFile:this.activeLogFile,ignoredFile:e});return}n.info("New Codex session log detected",{filePath:e}),this.activeLogFile=e,this.filePositions.set(e,0),this.readNewLines(e)}onFileChanged(e){if(this.activeLogFile&&e!==this.activeLogFile){n.debug("Ignoring change for non-active session log",{activeLogFile:this.activeLogFile,ignoredFile:e});return}if(!this.activeLogFile){try{let i=y.statSync(e),s=i.birthtimeMs||i.ctimeMs,o=i.mtimeMs;if(s<this.startTime-5e3&&o<this.startTime-5e3){n.debug("Ignoring change for pre-existing session file",{filePath:e,fileCreatedAt:new Date(s).toISOString(),fileModifiedAt:new Date(o).toISOString(),startTime:new Date(this.startTime).toISOString()});return}}catch(i){n.warn("Could not check file creation time during change event",{filePath:e,error:i})}this.activeLogFile=e,this.filePositions.set(e,0)}this.readNewLines(e)}bindRecentSessionFile(){if(!(this.activeLogFile||!this.sessionsDir))try{let i=this.collectRecentSessionFiles(this.sessionsDir).sort((s,o)=>o.modifiedAt-s.modifiedAt)[0];if(!i)return;n.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()}),this.activeLogFile=i.filePath,this.filePositions.set(i.filePath,0),this.readNewLines(i.filePath)}catch(e){n.warn("Failed to backfill recent session file",{error:e})}}collectRecentSessionFiles(e){let i=[],s=[e];for(;s.length>0;){let o=s.pop();if(!o)continue;let r;try{r=y.readdirSync(o,{withFileTypes:!0})}catch{continue}for(let p of r){let a=R.join(o,p.name);if(p.isDirectory()){s.push(a);continue}if(!(!p.isFile()||!p.name.startsWith("rollout-")||!p.name.endsWith(".jsonl")))try{let d=y.statSync(a),u=d.birthtimeMs||d.ctimeMs,h=d.mtimeMs;(u>=this.startTime||h>=this.startTime-5e3)&&i.push({filePath:a,createdAt:u,modifiedAt:h})}catch{}}}return i}async readNewLines(e){let i=this.filePositions.get(e)||0;try{if(y.statSync(e).size<=i)return;let o=y.createReadStream(e,{start:i,encoding:"utf-8"}),r=st.createInterface({input:o,crlfDelay:1/0}),p=i;for await(let a of r)if(p+=Buffer.byteLength(a,"utf-8")+1,!!a.trim())try{let d=JSON.parse(a);this.processLogEntry(d)}catch(d){n.warn("Failed to parse log line",{filePath:e,line:a.substring(0,100),error:d})}this.filePositions.set(e,p)}catch(s){n.error("Error reading log file",{filePath:e,error:s}),this.emit("error",s)}}processLogEntry(e){if(n.debug("Processing log entry",{type:e.type}),e.type==="session_meta"){let i=e.payload;this.sessionId=i.id,n.info("Codex session started",{sessionId:i.id,cwd:i.cwd,cliVersion:i.cli_version}),this.emit("session-started",i);return}this.emit("log-entry",e),e.type==="event_msg"&&e.payload?.type?this.emit(`event:${e.payload.type}`,e):e.type==="response_item"&&e.payload?.type&&this.emit(`response:${e.payload.type}`,e)}};j();var lt=require("events"),ct=require("@quantiya/codevibe-core");I();var A=class extends lt.EventEmitter{constructor(){super();this.pendingCalls=new Map;this.timers=new Map;this.timeoutMs=(0,ct.getConfig)().codex.approvalTimeoutMs,n.info("Approval detector initialized",{timeoutMs:this.timeoutMs})}onToolCallStart(e,i,s){n.debug("Tool call started",{callId:e,name:i});let o=this.parseInput(s),r=this.extractFilePath(i,s,o),p=this.extractDiff(i,s,o),a={callId:e,name:i,input:s,filePath:r,diff:p,parsedInput:o,timestamp:Date.now(),notificationSent:!1};this.pendingCalls.set(e,a);let d=setTimeout(()=>{this.checkPendingCall(e)},this.timeoutMs);this.timers.set(e,d)}onToolCallComplete(e){n.debug("Tool call completed",{callId:e}),this.pendingCalls.delete(e);let i=this.timers.get(e);i&&(clearTimeout(i),this.timers.delete(e))}checkPendingCall(e){let i=this.pendingCalls.get(e);if(!i||i.notificationSent)return;let s=Date.now()-i.timestamp;n.info("Tool call still pending after timeout",{callId:e,name:i.name,elapsedMs:s}),i.notificationSent=!0,this.pendingCalls.set(e,i),this.emit("approval-pending",{callId:e,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(e,i,s){if(s)return`File: ${s}`;if(e==="apply_patch"&&i){let o=i.match(/\*\*\* (?:Update|Add|Delete) File: (.+)/);if(o)return`File: ${o[1].trim()}`}if(e==="shell_command"||e==="shell")try{let o=JSON.parse(i);if(o.command)return`Command: ${o.command.substring(0,50)}${o.command.length>50?"...":""}`}catch{}return`Tool: ${this.mapToolName(e)}`}mapToolName(e){return{shell_command:"Bash",shell:"Bash",apply_patch:"File Edit",write_file:"Write File",read_file:"Read File"}[e]||e}parseInput(e){if(e)try{return JSON.parse(e)}catch{return}}extractFilePath(e,i,s){if(e==="apply_patch"&&i){let r=i.match(/\*\*\* (?:Update|Add|Delete) File: (.+)/);if(r)return r[1].trim()}let o=s?.file_path||s?.path||s?.filePath;if(o&&typeof o=="string")return o}extractDiff(e,i,s){if(e==="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 e of this.timers.values())clearTimeout(e);this.timers.clear(),this.pendingCalls.clear(),n.debug("Approval detector cleared")}shutdown(){this.clear(),this.removeAllListeners(),n.info("Approval detector shutdown")}};var ut=require("child_process"),ht=require("util");I();var dt=(0,ht.promisify)(ut.exec),N=class{async sendInput(t,e){n.info("Attempting to send input to Codex",{sessionId:t,input:e});try{let i=process.env.CODEVIBE_CODEX_TMUX_SESSION;return i?(n.info("Using tmux send-keys",{tmuxSession:i}),await this.sendViaTmux(i,e),n.info("Successfully sent input to Codex",{sessionId:t,input:e}),!0):(n.error("No tmux session found - codevibe-codex wrapper is required",{sessionId:t,hint:"Start Codex CLI using the codevibe-codex wrapper script"}),!1)}catch(i){return n.error("Failed to send input to Codex",{sessionId:t,error:i instanceof Error?i.message:String(i)}),!1}}async sendViaTmux(t,e){let i=e.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\$/g,"\\$").replace(/`/g,"\\`");n.info("Sending via tmux",{sessionName:t,inputLength:e.length});try{let s=`tmux send-keys -t "${t}" -l "${i}"`;await dt(s),await this.delay(500);let o=`tmux send-keys -t "${t}" Enter`;await dt(o),n.info("tmux send-keys completed")}catch(s){throw n.error("tmux send-keys failed",{sessionName:t,error:s}),s}}delay(t){return new Promise(e=>setTimeout(e,t))}isApprovalResponse(t){let e=t.trim().toLowerCase();return["y","n","a","q","e","yes","no"].includes(e)||/^[0-9]+$/.test(e)}};var f=g(require("fs")),mt=g(require("os")),k=g(require("path")),ft=require("crypto"),gt=require("child_process"),yt=require("events"),vt=require("util");I();var B=(0,vt.promisify)(gt.exec),M=class extends yt.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(e){if(this.started&&this.sessionName===e){n.debug("Tmux pane observer already started",{sessionName:e});return}this.started&&await this.stop(),this.sessionName=e,this.started=!0,this.filePosition=0,this.lastPromptHash=null,this.pipeFilePath=k.join(mt.tmpdir(),`codevibe-codex-pane-${process.pid}.log`),f.mkdirSync(k.dirname(this.pipeFilePath),{recursive:!0}),f.writeFileSync(this.pipeFilePath,""),await this.enablePipePane(),this.startFileWatcher(),n.info("Tmux pane observer started",{sessionName:e})}async stop(){if(this.started){try{await this.disablePipePane()}catch(e){n.debug("Failed to disable tmux pipe-pane cleanly",{error:e})}if(this.watcher&&(this.watcher.close(),this.watcher=null),this.pipeFilePath)try{f.unlinkSync(this.pipeFilePath)}catch{}n.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(e=120){if(!this.sessionName)throw new Error("Tmux pane observer is not started");let i=Math.max(1,Math.floor(e)),s=this.escapeShellArg(this.sessionName),o=`tmux capture-pane -p -e -S -${i} -t '${s}'`;try{let{stdout:r}=await B(o);return r}catch(r){throw n.error("Failed to capture tmux pane snapshot",{sessionName:this.sessionName,error:r}),this.emit("observer-error",r),r}}escapeShellArg(e){return e.replace(/'/g,"'\\''")}async enablePipePane(){if(!this.sessionName||!this.pipeFilePath)throw new Error("Tmux pane observer is not initialized");let e=this.escapeShellArg(this.sessionName),i=this.escapeShellArg(this.pipeFilePath),s=`tmux pipe-pane -O -t '${e}' "cat >> '${i}'"`;await B(s),n.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=f.watch(this.pipeFilePath,e=>{e==="change"&&this.processFileChanges()}))}async processFileChanges(){if(this.pipeFilePath){if(this.processing){this.pendingRead=!0;return}this.processing=!0;try{do{this.pendingRead=!1;let e=this.readAppendedChunk();if(!e||!this.looksLikePromptDelta(e))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:e,snapshot:i,detectedAt:Date.now()}))}while(this.pendingRead)}catch(e){n.error("Failed to process tmux pane changes",{error:e}),this.emit("observer-error",e)}finally{this.processing=!1}}}readAppendedChunk(){if(!this.pipeFilePath)return"";let e=f.statSync(this.pipeFilePath);if(e.size<=this.filePosition)return"";let i=f.openSync(this.pipeFilePath,"r");try{let s=e.size-this.filePosition,o=Buffer.alloc(s);return f.readSync(i,o,0,s,this.filePosition),this.filePosition=e.size,o.toString("utf-8")}finally{f.closeSync(i)}}looksLikePromptDelta(e){return/\[(?:y\/n|Y\/n|y\/N)\]|\b(?:apply|approve|allow|reject|deny|continue)\b/i.test(e)}looksLikePromptSnapshot(e){let i=e.split(`
5
+ `)}}function lt(a){if(!a)return null;let t=a.match(/\*\*\* (?:Update|Add|Delete) File: (.+)/);return t?{filePath:t[1].trim()}:null}function At(a,t){return a?a.length<=t?a:a.substring(0,t)+"...":""}function j(){_.clear()}function Nt(){return _.size}function Mt(a){return _.get(a)}var W,S,_,z=Z(()=>{"use strict";W=require("uuid"),S=require("@quantiya/codevibe-core");E();_=new Map});var J=require("uuid"),x=f(require("fs")),F=f(require("path")),wt=f(require("os")),c=require("@quantiya/codevibe-core");E();var nt=require("events"),y=f(require("fs")),A=f(require("path")),rt=f(require("readline")),ot=require("chokidar"),at=require("@quantiya/codevibe-core");E();var R=class extends nt.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}start(){if(this.isWatching){n.warn("Session log watcher already running");return}let e=(0,at.getConfig)().codex.sessionsDir;this.sessionsDir=e,n.info("Starting Codex session log watcher",{sessionsDir:e}),y.existsSync(e)||(n.info("Codex sessions directory does not exist yet, creating...",{sessionsDir:e}),y.mkdirSync(e,{recursive:!0})),this.startTime=Date.now(),this.watcher=(0,ot.watch)(e,{persistent:!0,ignoreInitial:!0,awaitWriteFinish:{stabilityThreshold:100,pollInterval:50},depth:4,ignored:s=>{let i=A.basename(s);return y.existsSync(s)&&y.statSync(s).isDirectory()?!1:!i.startsWith("rollout-")||!i.endsWith(".jsonl")}}),this.watcher.on("add",s=>{s.endsWith(".jsonl")&&this.onFileAdded(s)}),this.watcher.on("change",s=>{s.endsWith(".jsonl")&&this.onFileChanged(s)}),this.watcher.on("error",s=>{n.error("Watcher error:",s),this.emit("error",s)}),this.watcher.on("ready",()=>{n.info("Session log watcher ready"),this.bindRecentSessionFile()}),this.isWatching=!0,n.info("Session log watcher started")}stop(){this.watcher&&(this.watcher.close(),this.watcher=null),this.isWatching=!1,this.filePositions.clear(),this.activeLogFile=null,this.sessionId=null,this.sessionsDir=null,n.info("Session log watcher stopped")}getSessionId(){return this.sessionId}getActiveLogFile(){return this.activeLogFile}onFileAdded(e){try{let s=y.statSync(e),i=s.birthtimeMs||s.ctimeMs;if(i<this.startTime-5e3){n.debug("Ignoring old session file",{filePath:e,fileCreatedAt:new Date(i).toISOString(),startTime:new Date(this.startTime).toISOString()});return}}catch(s){n.warn("Could not check file creation time",{filePath:e,error:s})}if(this.activeLogFile){n.debug("Ignoring additional Codex session log for this server instance",{activeLogFile:this.activeLogFile,ignoredFile:e});return}n.info("New Codex session log detected",{filePath:e}),this.activeLogFile=e,this.filePositions.set(e,0),this.readNewLines(e)}onFileChanged(e){if(this.activeLogFile&&e!==this.activeLogFile){n.debug("Ignoring change for non-active session log",{activeLogFile:this.activeLogFile,ignoredFile:e});return}if(!this.activeLogFile){try{let s=y.statSync(e),i=s.birthtimeMs||s.ctimeMs,o=s.mtimeMs;if(i<this.startTime-5e3&&o<this.startTime-5e3){n.debug("Ignoring change for pre-existing session file",{filePath:e,fileCreatedAt:new Date(i).toISOString(),fileModifiedAt:new Date(o).toISOString(),startTime:new Date(this.startTime).toISOString()});return}}catch(s){n.warn("Could not check file creation time during change event",{filePath:e,error:s})}this.activeLogFile=e,this.filePositions.set(e,0)}this.readNewLines(e)}bindRecentSessionFile(){if(!(this.activeLogFile||!this.sessionsDir))try{let s=this.collectRecentSessionFiles(this.sessionsDir).sort((i,o)=>o.modifiedAt-i.modifiedAt)[0];if(!s)return;n.info("Binding recent session file missed during initial watch scan",{filePath:s.filePath,fileCreatedAt:new Date(s.createdAt).toISOString(),fileModifiedAt:new Date(s.modifiedAt).toISOString(),watcherStartTime:new Date(this.startTime).toISOString()}),this.activeLogFile=s.filePath,this.filePositions.set(s.filePath,0),this.readNewLines(s.filePath)}catch(e){n.warn("Failed to backfill recent session file",{error:e})}}collectRecentSessionFiles(e){let s=[],i=[e];for(;i.length>0;){let o=i.pop();if(!o)continue;let r;try{r=y.readdirSync(o,{withFileTypes:!0})}catch{continue}for(let l of r){let p=A.join(o,l.name);if(l.isDirectory()){i.push(p);continue}if(!(!l.isFile()||!l.name.startsWith("rollout-")||!l.name.endsWith(".jsonl")))try{let d=y.statSync(p),u=d.birthtimeMs||d.ctimeMs,h=d.mtimeMs;(u>=this.startTime||h>=this.startTime-5e3)&&s.push({filePath:p,createdAt:u,modifiedAt:h})}catch{}}}return s}async readNewLines(e){let s=this.filePositions.get(e)||0;try{if(y.statSync(e).size<=s)return;let o=y.createReadStream(e,{start:s,encoding:"utf-8"}),r=rt.createInterface({input:o,crlfDelay:1/0}),l=s;for await(let p of r)if(l+=Buffer.byteLength(p,"utf-8")+1,!!p.trim())try{let d=JSON.parse(p);this.processLogEntry(d)}catch(d){n.warn("Failed to parse log line",{filePath:e,line:p.substring(0,100),error:d})}this.filePositions.set(e,l)}catch(i){n.error("Error reading log file",{filePath:e,error:i}),this.emit("error",i)}}processLogEntry(e){if(n.debug("Processing log entry",{type:e.type}),e.type==="session_meta"){let s=e.payload;this.sessionId=s.id,n.info("Codex session started",{sessionId:s.id,cwd:s.cwd,cliVersion:s.cli_version}),this.emit("session-started",s);return}this.emit("log-entry",e),e.type==="event_msg"&&e.payload?.type?this.emit(`event:${e.payload.type}`,e):e.type==="response_item"&&e.payload?.type&&this.emit(`response:${e.payload.type}`,e)}};z();var dt=require("events"),ut=require("@quantiya/codevibe-core");E();var N=class extends dt.EventEmitter{constructor(){super();this.pendingCalls=new Map;this.timers=new Map;this.timeoutMs=(0,ut.getConfig)().codex.approvalTimeoutMs,n.info("Approval detector initialized",{timeoutMs:this.timeoutMs})}onToolCallStart(e,s,i){n.debug("Tool call started",{callId:e,name:s});let o=this.parseInput(i),r=this.extractFilePath(s,i,o),l=this.extractDiff(s,i,o),p={callId:e,name:s,input:i,filePath:r,diff:l,parsedInput:o,timestamp:Date.now(),notificationSent:!1};this.pendingCalls.set(e,p);let d=setTimeout(()=>{this.checkPendingCall(e)},this.timeoutMs);this.timers.set(e,d)}onToolCallComplete(e){n.debug("Tool call completed",{callId:e}),this.pendingCalls.delete(e);let s=this.timers.get(e);s&&(clearTimeout(s),this.timers.delete(e))}checkPendingCall(e){let s=this.pendingCalls.get(e);if(!s||s.notificationSent)return;let i=Date.now()-s.timestamp;n.info("Tool call still pending after timeout",{callId:e,name:s.name,elapsedMs:i}),s.notificationSent=!0,this.pendingCalls.set(e,s),this.emit("approval-pending",{callId:e,toolName:s.name,hint:this.extractHint(s.name,s.input,s.filePath),filePath:s.filePath,diff:s.diff,toolInput:s.parsedInput,rawInput:s.input,elapsedMs:i})}extractHint(e,s,i){if(i)return`File: ${i}`;if(e==="apply_patch"&&s){let o=s.match(/\*\*\* (?:Update|Add|Delete) File: (.+)/);if(o)return`File: ${o[1].trim()}`}if(e==="shell_command"||e==="shell")try{let o=JSON.parse(s);if(o.command)return`Command: ${o.command.substring(0,50)}${o.command.length>50?"...":""}`}catch{}return`Tool: ${this.mapToolName(e)}`}mapToolName(e){return{shell_command:"Bash",shell:"Bash",apply_patch:"File Edit",write_file:"Write File",read_file:"Read File"}[e]||e}parseInput(e){if(e)try{return JSON.parse(e)}catch{return}}extractFilePath(e,s,i){if(e==="apply_patch"&&s){let r=s.match(/\*\*\* (?:Update|Add|Delete) File: (.+)/);if(r)return r[1].trim()}let o=i?.file_path||i?.path||i?.filePath;if(o&&typeof o=="string")return o}extractDiff(e,s,i){if(e==="apply_patch"&&s)return s;if(i?.diff&&typeof i.diff=="string")return i.diff}getPendingCalls(){return Array.from(this.pendingCalls.values())}hasPendingCalls(){return this.pendingCalls.size>0}clear(){for(let e of this.timers.values())clearTimeout(e);this.timers.clear(),this.pendingCalls.clear(),n.debug("Approval detector cleared")}shutdown(){this.clear(),this.removeAllListeners(),n.info("Approval detector shutdown")}};var mt=require("child_process"),ft=require("util");E();var ht=(0,ft.promisify)(mt.exec),M=class{async sendInput(t,e){n.info("Attempting to send input to Codex",{sessionId:t,input:e});try{let s=process.env.CODEVIBE_CODEX_TMUX_SESSION;return s?(n.info("Using tmux send-keys",{tmuxSession:s}),await this.sendViaTmux(s,e),n.info("Successfully sent input to Codex",{sessionId:t,input:e}),!0):(n.error("No tmux session found - codevibe-codex wrapper is required",{sessionId:t,hint:"Start Codex CLI using the codevibe-codex wrapper script"}),!1)}catch(s){return n.error("Failed to send input to Codex",{sessionId:t,error:s instanceof Error?s.message:String(s)}),!1}}async sendViaTmux(t,e){let s=e.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\$/g,"\\$").replace(/`/g,"\\`");n.info("Sending via tmux",{sessionName:t,inputLength:e.length});try{let i=`tmux send-keys -t "${t}" -l "${s}"`;await ht(i),await this.delay(500);let o=`tmux send-keys -t "${t}" Enter`;await ht(o),n.info("tmux send-keys completed")}catch(i){throw n.error("tmux send-keys failed",{sessionName:t,error:i}),i}}delay(t){return new Promise(e=>setTimeout(e,t))}isApprovalResponse(t){let e=t.trim().toLowerCase();return["y","n","a","q","e","yes","no"].includes(e)||/^[0-9]+$/.test(e)}};var g=f(require("fs")),gt=f(require("os")),L=f(require("path")),yt=require("crypto"),vt=require("child_process"),St=require("events"),_t=require("util");E();var V=(0,_t.promisify)(vt.exec),k=class extends St.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(e){if(this.started&&this.sessionName===e){n.debug("Tmux pane observer already started",{sessionName:e});return}this.started&&await this.stop(),this.sessionName=e,this.started=!0,this.filePosition=0,this.lastPromptHash=null,this.pipeFilePath=L.join(gt.tmpdir(),`codevibe-codex-pane-${process.pid}.log`),g.mkdirSync(L.dirname(this.pipeFilePath),{recursive:!0}),g.writeFileSync(this.pipeFilePath,""),await this.enablePipePane(),this.startFileWatcher(),n.info("Tmux pane observer started",{sessionName:e})}async stop(){if(this.started){try{await this.disablePipePane()}catch(e){n.debug("Failed to disable tmux pipe-pane cleanly",{error:e})}if(this.watcher&&(this.watcher.close(),this.watcher=null),this.pipeFilePath)try{g.unlinkSync(this.pipeFilePath)}catch{}n.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(e=120){if(!this.sessionName)throw new Error("Tmux pane observer is not started");let s=Math.max(1,Math.floor(e)),i=this.escapeShellArg(this.sessionName),o=`tmux capture-pane -p -e -S -${s} -t '${i}'`;try{let{stdout:r}=await V(o);return r}catch(r){throw n.error("Failed to capture tmux pane snapshot",{sessionName:this.sessionName,error:r}),this.emit("observer-error",r),r}}escapeShellArg(e){return e.replace(/'/g,"'\\''")}async enablePipePane(){if(!this.sessionName||!this.pipeFilePath)throw new Error("Tmux pane observer is not initialized");let e=this.escapeShellArg(this.sessionName),s=this.escapeShellArg(this.pipeFilePath),i=`tmux pipe-pane -O -t '${e}' "cat >> '${s}'"`;await V(i),n.debug("Enabled tmux pipe-pane mirroring",{sessionName:this.sessionName,pipeFilePath:this.pipeFilePath})}async disablePipePane(){if(!this.sessionName)return;let s=`tmux pipe-pane -t '${this.escapeShellArg(this.sessionName)}'`;await V(s)}startFileWatcher(){this.pipeFilePath&&(this.watcher=g.watch(this.pipeFilePath,e=>{e==="change"&&this.processFileChanges()}))}async processFileChanges(){if(this.pipeFilePath){if(this.processing){this.pendingRead=!0;return}this.processing=!0;try{do{this.pendingRead=!1;let e=this.readAppendedChunk();if(!e||!this.looksLikePromptDelta(e))continue;let s=await this.captureSnapshot();if(!s||!this.looksLikePromptSnapshot(s))continue;let i=this.hashPromptSnapshot(s);i!==this.lastPromptHash&&(this.lastPromptHash=i,this.emit("prompt-candidate",{rawDelta:e,snapshot:s,detectedAt:Date.now()}))}while(this.pendingRead)}catch(e){n.error("Failed to process tmux pane changes",{error:e}),this.emit("observer-error",e)}finally{this.processing=!1}}}readAppendedChunk(){if(!this.pipeFilePath)return"";let e=g.statSync(this.pipeFilePath);if(e.size<=this.filePosition)return"";let s=g.openSync(this.pipeFilePath,"r");try{let i=e.size-this.filePosition,o=Buffer.alloc(i);return g.readSync(s,o,0,i,this.filePosition),this.filePosition=e.size,o.toString("utf-8")}finally{g.closeSync(s)}}looksLikePromptDelta(e){return/\[(?:y\/n|Y\/n|y\/N)\]|\b(?:apply|approve|allow|reject|deny|continue)\b/i.test(e)}looksLikePromptSnapshot(e){let s=e.split(`
6
6
  `).slice(-20).join(`
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,`
7
+ `);return/\[(?:y\/n|Y\/n|y\/N)\]|^\s*\d+\.\s+/im.test(s)}hashPromptSnapshot(e){let s=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);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.
9
+ `).trim();return(0,yt.createHash)("sha256").update(s).digest("hex")}};var I=require("@quantiya/codevibe-core");var q=f(require("express")),T=f(require("fs")),X=f(require("path")),G=f(require("os"));E();var D=class{constructor(){this.assignedPort=0;this.app=(0,q.default)(),this.setupMiddleware(),this.setupRoutes(),this.tmuxSession=process.env.CODEVIBE_CODEX_TMUX_SESSION}getPort(){return this.assignedPort}setupMiddleware(){this.app.use(q.default.json({limit:"1mb"})),this.app.use((t,e,s)=>{n.debug(`${t.method} ${t.path}`,{body:t.body}),s()})}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 s=t.body;if(!s.session_id||!s.hook_event_name){e.status(400).json({success:!1,error:"Missing session_id or hook_event_name"});return}let i=this.transformHookToEvent(s);n.info("Received hook event",{sessionId:s.session_id,hookEvent:s.hook_event_name,type:i.type}),this.eventHandler&&await this.eventHandler(i),e.json({success:!0})}catch(s){n.error("Error handling event:",s),e.status(500).json({success:!1,error:s instanceof Error?s.message:"Unknown error"})}}transformHookToEvent(t){let e={cwd:t.cwd,hook_event_name:t.hook_event_name,...t.metadata||{}},s,i;switch(t.hook_event_name){case"SessionStart":s="NOTIFICATION",i="Session started",e.source=t.source;break;case"UserPromptSubmit":s="USER_PROMPT",i=t.prompt||"";break;case"PreToolUse":s="INTERACTIVE_PROMPT",i=`${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":s="TOOL_USE",i=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":s="ASSISTANT_RESPONSE",i=t.last_assistant_message||"";break;default:s="NOTIFICATION",i=`Hook: ${t.hook_event_name}`}return{session_id:t.session_id,hook_event_name:t.hook_event_name,type:s,source:"DESKTOP",content:i,metadata:e}}onEvent(t){this.eventHandler=t}async start(){return new Promise((t,e)=>{try{this.server=this.app.listen(0,"localhost",()=>{let s=this.server.address();this.assignedPort=s.port,n.info(`HTTP API listening on http://localhost:${this.assignedPort}`),this.writePortFile(this.assignedPort),t(this.assignedPort)}),this.server.on("error",s=>{n.error("HTTP server error:",s),e(s)})}catch(s){e(s)}})}writePortFile(t){if(!this.tmuxSession){n.warn("No CODEVIBE_CODEX_TMUX_SESSION set, skipping port file");return}let e=X.join(G.tmpdir(),`codevibe-codex-${this.tmuxSession}.port`);try{T.writeFileSync(e,t.toString()),n.info(`Port file written: ${e} -> ${t}`)}catch(s){n.error(`Failed to write port file: ${e}`,s)}}removePortFile(){if(!this.tmuxSession)return;let t=X.join(G.tmpdir(),`codevibe-codex-${this.tmuxSession}.port`);try{T.existsSync(t)&&(T.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 bt=f(require("crypto")),Pt=f(require("https")),U=f(require("os")),kt="G-GS74YEQTB8",Lt="lAfOF6OxRzSQ-NsLBRjhAg",Dt="www.google-analytics.com",Ut=`/mp/collect?measurement_id=${kt}&api_secret=${Lt}`;function $t(){let a=typeof process.getuid=="function"?process.getuid():0;return bt.createHash("sha256").update(`${U.hostname()}-${a}`).digest("hex").substring(0,36)}function Ht(a){if(!a)return"";let t=U.homedir(),e=a.replace(/[\n\r\t]/g," ");if(t&&t.length>0){let s=t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");e=e.replace(new RegExp(s,"g"),"~")}return e=e.replace(/\/Users\/[^/\s"'`]+/g,"/Users/<user>").replace(/\/home\/[^/\s"'`]+/g,"/home/<user>").replace(/[^\x20-\x7E]/g,""),e.trim().substring(0,100)}function v(a,t){try{let e={...t};typeof e.error_message=="string"&&(e.error_message=Ht(e.error_message));let s=JSON.stringify({client_id:$t(),events:[{name:a,params:{agent:"codex",platform:process.platform,source:process.env.CODEVIBE_TELEMETRY_SOURCE||"production",...e}}]}),i=Pt.request({hostname:Dt,path:Ut,method:"POST",headers:{"Content-Type":"application/json"},timeout:2e3},o=>{o.resume()});i.on("error",()=>{}),i.on("timeout",()=>{try{i.destroy()}catch{}}),i.write(s),i.end()}catch{}}var Y=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 D,this.sessionWatcher=new R,this.approvalDetector=new N,this.promptResponder=new M,this.tmuxPaneObserver=new k}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 s=await this.appSyncClient.sweepOrphanSessions({agentType:"CODEX"});s>0&&n.info("Orphan sweep: marked stale Codex sessions INACTIVE",{swept:s})}catch(s){n.warn("Orphan sweep failed, continuing startup",{error:s instanceof Error?s.message:String(s)})}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(),s=this.generateSessionId(t),i=this.appSyncClient.getCurrentUserId();n.info("Creating launch session",{sessionId:s,projectPath:e});try{let o=await(0,c.resumeOrCreateSession)({sessionId:s,userId:i,agentType:c.AgentType.CODEX,projectPath:e,metadata:{launchSession:!0}},this.appSyncClient,n);this.sessionKey=o.sessionKey,this.sessionState={sessionId:s,userId:i,projectPath:e,cwd:e,createdAt:new Date,subscriptionActive:!1,metadata:{launchSession:!0},codexSessionId:t,codexLogFile:void 0},this.subscribeToMobileEvents(s),this.appSyncClient.startHeartbeat(s),n.info("Launch session created",{sessionId:s})}catch(o){n.error("Failed to create launch session (non-fatal)",{error:o})}}async handleEventFromHook(t){let{session_id:e,hook_event_name:s,type:i,content:o,metadata:r}=t;if(this.hooksActive=!0,n.info("[Hooks] Received event",{sessionId:e,hookEvent:s,type:i,contentLength:o?.length}),s==="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(p=>n.warn("Failed to update session metadata",{error:p}));else{let p={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(p)}return}if(!this.sessionState){n.warn("[Hooks] Session not initialized, buffering event",{hook_event_name:s});return}let l=this.sessionState.sessionId;if(i==="USER_PROMPT"&&o&&this.isRecentMobilePrompt(o)){n.info("[Hooks] Skipping duplicate USER_PROMPT from mobile");return}if(s==="PreToolUse"){let p=r?.tool_name||"unknown",d=r?.tool_input,u={tool_name:this.mapToolName(p),tool_input:d||{}};if(p==="apply_patch"&&typeof d=="string"){let{extractOldNewFromPatch:C,extractFileFromPatch:$}=(z(),Ot(ct)),w=C(d),H=$(d);w&&(u.tool_input={file_path:H||"",old_string:w.oldString,new_string:w.newString})}let h=process.env.CODEVIBE_CODEX_TMUX_SESSION;if(h)try{await new Promise(H=>setTimeout(H,500));let{execSync:C}=require("child_process"),$=C(`tmux capture-pane -p -e -S -30 -t '${h}'`,{timeout:5e3,encoding:"utf8"}),w=(0,I.parseInteractivePrompt)($);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,b=u,P=!1;this.sessionKey&&(m=c.cryptoService.encryptContent(o,this.sessionKey),b={encrypted:c.cryptoService.encryptMetadata(u,this.sessionKey)},P=!0),await this.appSyncClient.createEvent({sessionId:l,type:c.EventType.INTERACTIVE_PROMPT,source:c.EventSource.DESKTOP,content:m,metadata:b,isEncrypted:P}),this.pendingInteractivePrompt={promptId:(0,J.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:p,sessionId:l});return}if(s==="PostToolUse"){let p=o,d=r,u=!1;this.sessionKey&&(p=c.cryptoService.encryptContent(o,this.sessionKey),r&&(d={encrypted:c.cryptoService.encryptMetadata(r,this.sessionKey)}),u=!0),await this.appSyncClient.createEvent({sessionId:l,type:c.EventType.TOOL_USE,source:c.EventSource.DESKTOP,content:p,metadata:d,isEncrypted:u});return}if(i==="ASSISTANT_RESPONSE"||i==="USER_PROMPT"){let p=o,d=!1;this.sessionKey&&o&&(p=c.cryptoService.encryptContent(o,this.sessionKey),d=!0),await this.appSyncClient.createEvent({sessionId:l,type:i==="ASSISTANT_RESPONSE"?c.EventType.ASSISTANT_RESPONSE:c.EventType.USER_PROMPT,source:c.EventSource.DESKTOP,content:p,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(),s=this.generateSessionId(t.id),i=this.appSyncClient.getCurrentUserId(),o={codexSessionId:t.id,cliVersion:t.cli_version,modelProvider:t.model_provider};try{let r=await(0,c.resumeOrCreateSession)({sessionId:s,userId:i,agentType:c.AgentType.CODEX,projectPath:e,metadata:o},this.appSyncClient,n);this.sessionKey=r.sessionKey,v("daemon_init_step_completed",{step:"session_resume_or_create"})}catch(r){throw this.isInitializingSession=!1,v("daemon_init_step_failed",{step:"session_resume_or_create",error_class:r?.name||"Error",error_message:r?.message||String(r)}),n.error("Failed to create/resume session:",r),r}try{this.sessionState={sessionId:s,userId:i,projectPath:e,cwd:t.cwd,createdAt:new Date,subscriptionActive:!1,metadata:o,codexSessionId:t.id,codexLogFile:this.sessionWatcher.getActiveLogFile()||void 0},v("daemon_init_step_completed",{step:"session_state_set"})}catch(r){v("daemon_init_step_failed",{step:"session_state_set",error_class:r?.name||"Error",error_message:r?.message||String(r)}),n.error("Failed to set session state:",r)}try{this.subscribeToMobileEvents(s),v("daemon_init_step_completed",{step:"subscribe_mobile_events"})}catch(r){v("daemon_init_step_failed",{step:"subscribe_mobile_events",error_class:r?.name||"Error",error_message:r?.message||String(r)}),n.error("Failed to subscribe to mobile events:",r)}try{this.appSyncClient.startHeartbeat(s),v("daemon_init_step_completed",{step:"heartbeat_start"})}catch(r){v("daemon_init_step_failed",{step:"heartbeat_start",error_class:r?.name||"Error",error_message:r?.message||String(r)}),n.error("Failed to start heartbeat:",r)}try{await this.flushBufferedLogEntries(),v("daemon_init_step_completed",{step:"flush_buffered_entries"})}catch(r){v("daemon_init_step_failed",{step:"flush_buffered_entries",error_class:r?.name||"Error",error_message:r?.message||String(r)}),n.error("Failed to flush buffered log entries:",r),this.bufferedLogEntries=[]}try{await this.startTmuxObserver(),v("daemon_init_step_completed",{step:"tmux_observer_start"})}catch(r){v("daemon_init_step_failed",{step:"tmux_observer_start",error_class:r?.name||"Error",error_message:r?.message||String(r)}),n.error("Failed to start tmux observer:",r)}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 s=t.payload.type;s==="function_call"||s==="custom_tool_call"?this.approvalDetector.onToolCallStart(t.payload.call_id,t.payload.name,t.payload.arguments||t.payload.input||""):(s==="function_call_output"||s==="custom_tool_call_output")&&(this.approvalDetector.onToolCallComplete(t.payload.call_id),this.pendingInteractivePrompt?.callId===t.payload.call_id&&(this.pendingInteractivePrompt=null))}let e=B(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 s=t.payload?.type;if((s==="function_call"||s==="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 s=c.cryptoService.encryptMetadata(e.metadata,this.sessionKey);e.metadata={encrypted:s}}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(s){n.error("Failed to sync event:",s)}}}async handleApprovalPending(t){if(this.sessionState){n.info("Sending approval pending interactive prompt",t);try{let e=await this.tryParseInteractivePromptFromTmux(),s=e?.parsedPrompt??null;if(s&&this.pendingInteractivePrompt&&this.pendingInteractivePrompt.source==="tmux"&&this.pendingInteractivePrompt.promptText===s.promptText){n.debug("Skipping heuristic prompt because tmux prompt is already active",{promptText:s.promptText});return}let i=this.buildToolDetailsForInteractivePrompt(t,e?.snapshot),o=i.tool_name||this.mapToolNameForApproval(t.toolName),r=i.tool_input||this.buildFallbackToolInput(t),l=!!(o&&r),p=this.buildPromptPresentation(s),d=p.options,u=t.filePath?`File: ${t.filePath}`:void 0,h=p.content||`Codex is waiting for approval.
10
10
  ${t.hint}`;u&&!h.includes(u)&&(h=`${h}
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(`
11
+ ${u}`),this.pendingInteractivePrompt={promptId:t.callId,callId:t.callId,kind:p.kind,options:d,submitMap:p.submitMap,promptText:p.promptText,createdAt:Date.now(),source:s?"tmux":"heuristic",requiresFollowUpText:p.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:l,options:d,instructions:p.instructions,prompt_source:s?"tmux":"heuristic"},b=!1;n.debug("Interactive prompt (pre-encryption)",{sessionId:this.sessionState.sessionId,callId:t.callId,contentPreview:h.substring(0,200),toolDetails:i,metadata:m}),this.sessionKey&&(h=c.cryptoService.encryptContent(h,this.sessionKey),m={encrypted:c.cryptoService.encryptMetadata(m,this.sessionKey)},b=!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,...b?{isEncrypted:!0}:{}})}catch(e){n.error("Failed to send approval interactive prompt:",e)}}}async handleTmuxPromptCandidate(t){if(!this.sessionState)return;let e=(0,I.parseInteractivePrompt)(t);if(!e||this.pendingInteractivePrompt&&this.pendingInteractivePrompt.source==="tmux"&&this.pendingInteractivePrompt.promptText===e.promptText)return;let s=this.buildPromptPresentation(e),i=this.getMostRecentPendingToolCall();i||(await new Promise(P=>setTimeout(P,500)),i=this.getMostRecentPendingToolCall());let o=i?this.buildApprovalPromptContextFromPendingCall(i):null,r=o?this.buildToolDetailsForInteractivePrompt(o,t):{},l=r.tool_name||this.mapToolNameForApproval(i?.name),p=r.tool_input||(o?this.buildFallbackToolInput(o):void 0),d=!!(l&&p),u=this.pendingInteractivePrompt?.callId||i?.callId||(0,J.v4)();this.pendingInteractivePrompt={promptId:u,callId:this.pendingInteractivePrompt?.callId||i?.callId,kind:s.kind,options:s.options,submitMap:s.submitMap,promptText:s.promptText,createdAt:Date.now(),source:"tmux",requiresFollowUpText:s.requiresFollowUpText};let h={options:s.options,instructions:s.instructions,prompt_source:"tmux_live",tool_name:l,tool_input:p,has_details:d},m=s.content,b=!1;this.sessionKey&&(m=c.cryptoService.encryptContent(m,this.sessionKey),h={encrypted:c.cryptoService.encryptMetadata(h,this.sessionKey)},b=!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,...b?{isEncrypted:!0}:{}}),n.info("Sent tmux-detected interactive prompt",{sessionId:this.sessionState.sessionId,promptText:e.promptText,kind:e.kind})}catch(P){n.error("Failed to send tmux-detected interactive prompt",{error:P})}}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,I.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,s)=>s.timestamp>e.timestamp?s: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(`
13
- `)}translatePromptResponse(t){let e=this.pendingInteractivePrompt;if(!e)return{primaryInput:t};let s=t.trim().match(/^(\d+)(?:[,.:;\-\s]+([\s\S]+))?$/);if(!s)return{primaryInput:t};let o=s[1],r=s[2]?.trim(),p=e.submitMap[o];return p?e.requiresFollowUpText&&r?{primaryInput:p,followUpInput:r}:{primaryInput:p}:{primaryInput:t}}buildToolDetailsForInteractivePrompt(t,e){let i=t.toolName,s=t.toolInput&&typeof t.toolInput=="object"?t.toolInput:void 0;if(i==="apply_patch"){let r=t.diff||t.rawInput;if(r){let{oldString:p,newString:a,oldStartLine:d,newStartLine:u}=this.extractOldNewFromPatch(r),h=e?this.extractDiffLineAnchorsFromSnapshot(e):{};return{tool_name:"Edit",tool_input:{file_path:t.filePath,content:r,diff:t.diff,raw_patch:t.rawInput,old_string:p,new_string:a,old_start_line:d??h.oldStartLine,new_start_line:u??h.newStartLine}}}}if(i==="shell_command"||i==="shell"){let r=s?.command||t.rawInput||t.hint;if(r)return{tool_name:"Bash",tool_input:{command:r,output:s?.output}}}let o={};return t.filePath&&(o.file_path=t.filePath),t.diff&&(o.diff=t.diff),t.rawInput&&(o.raw_input=t.rawInput),Object.keys(o).length>0?{tool_name:i||"Tool",tool_input:o}:{}}buildFallbackToolInput(t){let e={};return t.filePath&&(e.file_path=t.filePath),t.diff&&(e.diff=t.diff),t.rawInput&&(e.raw_input=t.rawInput),t.toolInput&&typeof t.toolInput=="object"&&(e.parsed_input=t.toolInput),Object.keys(e).length>0?e:void 0}mapToolNameForApproval(t){return t?{apply_patch:"Edit",shell_command:"Bash",shell:"Bash"}[t]||t:void 0}extractOldNewFromPatch(t){let e=[],i=[],s=/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/,o=0,r=0,p=0,a,d;for(let u of t.split(`
14
- `)){let h=u.match(s);if(h){o+=1,r=Number.parseInt(h[1],10),p=Number.parseInt(h[2],10);continue}if(!(u.startsWith("***")||u.startsWith("---")||u.startsWith("+++")||u.startsWith("*** End Patch"))){if(u.startsWith("-"))a===void 0&&(a=r),e.push(u.slice(1)),r+=1;else if(u.startsWith("+"))d===void 0&&(d=p),i.push(u.slice(1)),p+=1;else if(u.startsWith(" ")){let m=u.slice(1);e.push(m),i.push(m),r+=1,p+=1}}}return{oldString:e.join(`
15
- `),newString:i.join(`
16
- `),oldStartLine:o===1?a:void 0,newStartLine:o===1?d:void 0}}extractDiffLineAnchorsFromSnapshot(t){let e=(0,_.normalizeSnapshot)(t),i,s;for(let o of e.split(`
17
- `)){let r=o.match(/^\s*(\d+)\s+(.*)$/);if(!r)continue;let p=Number.parseInt(r[1],10),a=r[2];if(Number.isFinite(p)){if(a.startsWith("-")){i??=p;continue}if(a.startsWith("+")){s??=p;continue}i??=p,s??=p}}return n.debug("Recovered diff line anchors from tmux snapshot",{oldStartLine:i,newStartLine:s,snapshotPreview:this.summarizePromptSnapshot(t)}),{oldStartLine:i,newStartLine:s}}subscribeToMobileEvents(t){if(this.subscribedSessionId===t){n.info("Already subscribed to mobile events, skipping",{sessionId:t});return}if(n.info("Subscribing to mobile events",{sessionId:t}),this.unsubscribe){try{this.unsubscribe()}catch(e){n.warn("Error cleaning up previous subscription (non-fatal)",{error:e})}this.subscribedSessionId=null}try{this.unsubscribe=this.appSyncClient.subscribeToEvents(t,async e=>{await this.handleMobileEvent(e)},e=>{n.error("Subscription error:",e)}),this.subscribedSessionId=t}catch(e){n.error("Failed to subscribe to mobile events (non-fatal)",{sessionId:t,error:e});return}this.sessionState&&(this.sessionState.subscriptionActive=!0),n.info("Subscribed to mobile events")}async downloadAttachment(t,e,i){try{let s=t.isEncrypted??i??!1;n.info("Downloading attachment",{id:t.id,type:t.type,filename:t.filename,s3Key:t.s3Key,attachmentIsEncrypted:t.isEncrypted,eventIsEncrypted:i,shouldDecrypt:s});let{downloadUrl:o}=await this.appSyncClient.getAttachmentDownloadUrl(t.s3Key),r=await fetch(o);if(!r.ok)throw new Error(`Failed to download attachment: ${r.status} ${r.statusText}`);let p=Buffer.from(await r.arrayBuffer());if(s&&this.sessionKey)try{n.info("Decrypting attachment",{id:t.id}),p=c.cryptoService.decryptData(p,this.sessionKey),n.info("Attachment decrypted successfully",{id:t.id,decryptedSize:p.length})}catch(m){throw n.error("Failed to decrypt attachment:",{id:t.id,error:m}),new Error("Failed to decrypt attachment")}else s&&!this.sessionKey&&n.warn("Cannot decrypt attachment - no session key available",{id:t.id});let a=C.join(St.tmpdir(),"codevibe-codex",e);T.existsSync(a)||T.mkdirSync(a,{recursive:!0});let d="";if(t.filename){let m=C.extname(t.filename);m&&(d=m)}d||(d={"image/jpeg":".jpg","image/png":".png","image/gif":".gif","image/webp":".webp","image/heic":".heic","application/pdf":".pdf"}[t.type]||".bin");let u=`attachment-${t.id}${d}`,h=C.join(a,u);return T.writeFileSync(h,p),n.info("Attachment saved to temp file",{id:t.id,filePath:h,size:p.length}),h}catch(s){return n.error("Failed to download attachment:",{id:t.id,error:s}),null}}async handleMobileEvent(t){if(t.attachments&&t.attachments.length>0&&n.info("DEBUG: Raw attachment data from subscription",{attachments:JSON.stringify(t.attachments),eventIsEncrypted:t.isEncrypted}),n.info("Received mobile event",{eventId:t.eventId,type:t.type,content:t.content?.substring(0,50),attachmentCount:t.attachments?.length||0,isEncrypted:t.isEncrypted}),!this.sessionState){n.warn("Received mobile event but no active session");return}let e=t.content||"";if(t.isEncrypted&&this.sessionKey)try{e=c.cryptoService.decryptContent(t.content,this.sessionKey),n.debug("Event decrypted successfully",{eventId:t.eventId})}catch(i){n.error("Failed to decrypt event:",{eventId:t.eventId,error:i}),e=t.content}try{await this.appSyncClient.updateEventStatus({eventId:t.eventId,sessionId:t.sessionId,timestamp:t.timestamp,deliveryStatus:c.DeliveryStatus.DELIVERED})}catch(i){n.error("Failed to update delivery status:",i)}if(t.type===c.EventType.USER_PROMPT||t.type===c.EventType.PROMPT_RESPONSE){let i=e,s=t.attachments||[],o=[];if(s.length>0){n.info("Downloading attachments for prompt",{count:s.length});for(let a of s){let d=await this.downloadAttachment(a,this.sessionState.sessionId,t.isEncrypted);d&&o.push(d)}if(o.length>0){let a=o.map(d=>`[Attached file: ${d}]`).join(`
18
- `);i?i=`${a}
13
+ `)}translatePromptResponse(t){let e=this.pendingInteractivePrompt;if(!e)return{primaryInput:t};let i=t.trim().match(/^(\d+)(?:[,.:;\-\s]+([\s\S]+))?$/);if(!i)return{primaryInput:t};let o=i[1],r=i[2]?.trim(),l=e.submitMap[o];return l?e.requiresFollowUpText&&r?{primaryInput:l,followUpInput:r}:{primaryInput:l}:{primaryInput:t}}buildToolDetailsForInteractivePrompt(t,e){let s=t.toolName,i=t.toolInput&&typeof t.toolInput=="object"?t.toolInput:void 0;if(s==="apply_patch"){let r=t.diff||t.rawInput;if(r){let{oldString:l,newString:p,oldStartLine:d,newStartLine:u}=this.extractOldNewFromPatch(r),h=e?this.extractDiffLineAnchorsFromSnapshot(e):{};return{tool_name:"Edit",tool_input:{file_path:t.filePath,content:r,diff:t.diff,raw_patch:t.rawInput,old_string:l,new_string:p,old_start_line:d??h.oldStartLine,new_start_line:u??h.newStartLine}}}}if(s==="shell_command"||s==="shell"){let r=i?.command||t.rawInput||t.hint;if(r)return{tool_name:"Bash",tool_input:{command:r,output:i?.output}}}let o={};return t.filePath&&(o.file_path=t.filePath),t.diff&&(o.diff=t.diff),t.rawInput&&(o.raw_input=t.rawInput),Object.keys(o).length>0?{tool_name:s||"Tool",tool_input:o}:{}}buildFallbackToolInput(t){let e={};return t.filePath&&(e.file_path=t.filePath),t.diff&&(e.diff=t.diff),t.rawInput&&(e.raw_input=t.rawInput),t.toolInput&&typeof t.toolInput=="object"&&(e.parsed_input=t.toolInput),Object.keys(e).length>0?e:void 0}mapToolNameForApproval(t){return t?{apply_patch:"Edit",shell_command:"Bash",shell:"Bash"}[t]||t:void 0}extractOldNewFromPatch(t){let e=[],s=[],i=/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/,o=0,r=0,l=0,p,d;for(let u of t.split(`
14
+ `)){let h=u.match(i);if(h){o+=1,r=Number.parseInt(h[1],10),l=Number.parseInt(h[2],10);continue}if(!(u.startsWith("***")||u.startsWith("---")||u.startsWith("+++")||u.startsWith("*** End Patch"))){if(u.startsWith("-"))p===void 0&&(p=r),e.push(u.slice(1)),r+=1;else if(u.startsWith("+"))d===void 0&&(d=l),s.push(u.slice(1)),l+=1;else if(u.startsWith(" ")){let m=u.slice(1);e.push(m),s.push(m),r+=1,l+=1}}}return{oldString:e.join(`
15
+ `),newString:s.join(`
16
+ `),oldStartLine:o===1?p:void 0,newStartLine:o===1?d:void 0}}extractDiffLineAnchorsFromSnapshot(t){let e=(0,I.normalizeSnapshot)(t),s,i;for(let o of e.split(`
17
+ `)){let r=o.match(/^\s*(\d+)\s+(.*)$/);if(!r)continue;let l=Number.parseInt(r[1],10),p=r[2];if(Number.isFinite(l)){if(p.startsWith("-")){s??=l;continue}if(p.startsWith("+")){i??=l;continue}s??=l,i??=l}}return n.debug("Recovered diff line anchors from tmux snapshot",{oldStartLine:s,newStartLine:i,snapshotPreview:this.summarizePromptSnapshot(t)}),{oldStartLine:s,newStartLine:i}}subscribeToMobileEvents(t){if(this.subscribedSessionId===t){n.info("Already subscribed to mobile events, skipping",{sessionId:t});return}if(n.info("Subscribing to mobile events",{sessionId:t}),this.unsubscribe){try{this.unsubscribe()}catch(e){n.warn("Error cleaning up previous subscription (non-fatal)",{error:e})}this.subscribedSessionId=null}try{this.unsubscribe=this.appSyncClient.subscribeToEvents(t,async e=>{await this.handleMobileEvent(e)},e=>{n.error("Subscription error:",e)}),this.subscribedSessionId=t}catch(e){n.error("Failed to subscribe to mobile events (non-fatal)",{sessionId:t,error:e});return}this.sessionState&&(this.sessionState.subscriptionActive=!0),n.info("Subscribed to mobile events")}async downloadAttachment(t,e,s){try{let i=t.isEncrypted??s??!1;n.info("Downloading attachment",{id:t.id,type:t.type,filename:t.filename,s3Key:t.s3Key,attachmentIsEncrypted:t.isEncrypted,eventIsEncrypted:s,shouldDecrypt:i});let{downloadUrl:o}=await this.appSyncClient.getAttachmentDownloadUrl(t.s3Key),r=await fetch(o);if(!r.ok)throw new Error(`Failed to download attachment: ${r.status} ${r.statusText}`);let l=Buffer.from(await r.arrayBuffer());if(i&&this.sessionKey)try{n.info("Decrypting attachment",{id:t.id}),l=c.cryptoService.decryptData(l,this.sessionKey),n.info("Attachment decrypted successfully",{id:t.id,decryptedSize:l.length})}catch(m){throw n.error("Failed to decrypt attachment:",{id:t.id,error:m}),new Error("Failed to decrypt attachment")}else i&&!this.sessionKey&&n.warn("Cannot decrypt attachment - no session key available",{id:t.id});let p=F.join(wt.tmpdir(),"codevibe-codex",e);x.existsSync(p)||x.mkdirSync(p,{recursive:!0});let d="";if(t.filename){let m=F.extname(t.filename);m&&(d=m)}d||(d={"image/jpeg":".jpg","image/png":".png","image/gif":".gif","image/webp":".webp","image/heic":".heic","application/pdf":".pdf"}[t.type]||".bin");let u=`attachment-${t.id}${d}`,h=F.join(p,u);return x.writeFileSync(h,l),n.info("Attachment saved to temp file",{id:t.id,filePath:h,size:l.length}),h}catch(i){return n.error("Failed to download attachment:",{id:t.id,error:i}),null}}async handleMobileEvent(t){if(t.attachments&&t.attachments.length>0&&n.info("DEBUG: Raw attachment data from subscription",{attachments:JSON.stringify(t.attachments),eventIsEncrypted:t.isEncrypted}),n.info("Received mobile event",{eventId:t.eventId,type:t.type,content:t.content?.substring(0,50),attachmentCount:t.attachments?.length||0,isEncrypted:t.isEncrypted}),!this.sessionState){n.warn("Received mobile event but no active session");return}let e=t.content||"";if(t.isEncrypted&&this.sessionKey)try{e=c.cryptoService.decryptContent(t.content,this.sessionKey),n.debug("Event decrypted successfully",{eventId:t.eventId})}catch(s){n.error("Failed to decrypt event:",{eventId:t.eventId,error:s}),e=t.content}try{await this.appSyncClient.updateEventStatus({eventId:t.eventId,sessionId:t.sessionId,timestamp:t.timestamp,deliveryStatus:c.DeliveryStatus.DELIVERED})}catch(s){n.error("Failed to update delivery status:",s)}if(t.type===c.EventType.USER_PROMPT||t.type===c.EventType.PROMPT_RESPONSE){let s=e,i=t.attachments||[],o=[];if(i.length>0){n.info("Downloading attachments for prompt",{count:i.length});for(let p of i){let d=await this.downloadAttachment(p,this.sessionState.sessionId,t.isEncrypted);d&&o.push(d)}if(o.length>0){let p=o.map(d=>`[Attached file: ${d}]`).join(`
18
+ `);s?s=`${p}
19
19
 
20
- ${i}`:i=`${a}
20
+ ${s}`:s=`${p}
21
21
 
22
- Please analyze the attached file(s).`,n.info("Prompt updated with attachment paths",{attachmentCount:o.length,newPromptLength:i.length})}}let r=this.translatePromptResponse(i),p=await this.promptResponder.sendInput(this.sessionState.sessionId,r.primaryInput);if(p&&r.followUpInput&&await this.promptResponder.sendInput(this.sessionState.sessionId,r.followUpInput),p&&this.pendingInteractivePrompt&&t.type===c.EventType.PROMPT_RESPONSE&&(this.pendingInteractivePrompt=null),p)try{await this.appSyncClient.updateEventStatus({eventId:t.eventId,sessionId:t.sessionId,timestamp:t.timestamp,deliveryStatus:c.DeliveryStatus.EXECUTED})}catch(a){n.error("Failed to update executed status:",a)}}}generateSessionId(t){return`codex-${t}`}async endActiveSession(t){if(this.sessionState){n.info("Ending active session",{sessionId:this.sessionState.sessionId,codexSessionId:this.sessionState.codexSessionId,reason:t}),this.appSyncClient.stopHeartbeat(this.sessionState.sessionId),this.unsubscribe&&(this.unsubscribe(),this.unsubscribe=null),await this.tmuxPaneObserver.stop(),this.pendingInteractivePrompt=null,this.isInitializingSession=!1,this.bufferedLogEntries=[],this.sessionKey&&(c.keychainManager.clearSessionKey(this.sessionState.sessionId),this.sessionKey=null);try{await this.appSyncClient.updateSession({sessionId:this.sessionState.sessionId,status:c.SessionStatus.INACTIVE})}catch(e){n.error("Failed to update session status:",e)}this.sessionState=null}}async stop(){n.info("Stopping CodeVibe Codex companion server"),await this.endActiveSession("shutdown"),this.sessionWatcher.stop(),this.approvalDetector.shutdown(),H(),await this.httpApi.stop(),this.appSyncClient.cleanupSubscriptions(),n.info("CodeVibe Codex companion server stopped")}},J=new G;process.on("SIGINT",async()=>{n.info("Received SIGINT, shutting down..."),await J.stop(),process.exit(0)});process.on("SIGTERM",async()=>{n.info("Received SIGTERM, shutting down..."),await J.stop(),process.exit(0)});J.start().catch(l=>{n.error("Failed to start server:",l),process.exit(1)});
22
+ Please analyze the attached file(s).`,n.info("Prompt updated with attachment paths",{attachmentCount:o.length,newPromptLength:s.length})}}let r=this.translatePromptResponse(s),l=await this.promptResponder.sendInput(this.sessionState.sessionId,r.primaryInput);if(l&&r.followUpInput&&await this.promptResponder.sendInput(this.sessionState.sessionId,r.followUpInput),l&&this.pendingInteractivePrompt&&t.type===c.EventType.PROMPT_RESPONSE&&(this.pendingInteractivePrompt=null),l)try{await this.appSyncClient.updateEventStatus({eventId:t.eventId,sessionId:t.sessionId,timestamp:t.timestamp,deliveryStatus:c.DeliveryStatus.EXECUTED})}catch(p){n.error("Failed to update executed status:",p)}}}generateSessionId(t){return`codex-${t}`}async endActiveSession(t){if(this.sessionState){n.info("Ending active session",{sessionId:this.sessionState.sessionId,codexSessionId:this.sessionState.codexSessionId,reason:t}),this.appSyncClient.stopHeartbeat(this.sessionState.sessionId),this.unsubscribe&&(this.unsubscribe(),this.unsubscribe=null),await this.tmuxPaneObserver.stop(),this.pendingInteractivePrompt=null,this.isInitializingSession=!1,this.bufferedLogEntries=[],this.sessionKey&&(c.keychainManager.clearSessionKey(this.sessionState.sessionId),this.sessionKey=null);try{await this.appSyncClient.updateSession({sessionId:this.sessionState.sessionId,status:c.SessionStatus.INACTIVE})}catch(e){n.error("Failed to update session status:",e)}this.sessionState=null}}async stop(){n.info("Stopping CodeVibe Codex companion server"),await this.endActiveSession("shutdown"),this.sessionWatcher.stop(),this.approvalDetector.shutdown(),j(),await this.httpApi.stop(),this.appSyncClient.cleanupSubscriptions(),n.info("CodeVibe Codex companion server stopped")}},Q=new Y;process.on("SIGINT",async()=>{n.info("Received SIGINT, shutting down..."),await Q.stop(),process.exit(0)});process.on("SIGTERM",async()=>{n.info("Received SIGTERM, shutting down..."),await Q.stop(),process.exit(0)});Q.start().catch(a=>{n.error("Failed to start server:",a),process.exit(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quantiya/codevibe-codex-plugin",
3
- "version": "1.0.23",
3
+ "version": "1.0.25",
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.17",
50
+ "@quantiya/codevibe-core": "^1.0.18",
51
51
  "chokidar": "^4.0.0",
52
52
  "dotenv": "^16.6.1",
53
53
  "express": "^5.1.0",