@quantiya/codevibe-codex-plugin 1.0.18 → 1.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/codevibe-codex +175 -6
- package/dist/server.js +1 -1
- package/package.json +2 -2
package/bin/codevibe-codex
CHANGED
|
@@ -41,6 +41,65 @@ done
|
|
|
41
41
|
SCRIPT_DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
|
|
42
42
|
PLUGIN_DIR="$(dirname "$SCRIPT_DIR")"
|
|
43
43
|
|
|
44
|
+
# ─── Wrapper telemetry (GA4 Measurement Protocol) ─────────────────────
|
|
45
|
+
# Diagnoses agent CLI failures: pre-flight bailouts, fast-die patterns,
|
|
46
|
+
# whether SessionStart hook fired, exit code. Background curl, fail
|
|
47
|
+
# silently, no PII (hashed hostname + per-run random id only). Honors
|
|
48
|
+
# CODEVIBE_TELEMETRY_SOURCE=test for internal testing.
|
|
49
|
+
_CV_MID="G-GS74YEQTB8"
|
|
50
|
+
_CV_SEC="lAfOF6OxRzSQ-NsLBRjhAg"
|
|
51
|
+
_CV_CID="$(echo "$(uname -n)-$(id -u)" | (sha256sum 2>/dev/null || shasum -a 256 2>/dev/null || echo "anonymous-fallback ") | cut -c1-36)"
|
|
52
|
+
_CV_RUN_ID="$(head -c 16 /dev/urandom 2>/dev/null | od -An -tx1 | tr -d ' \n' | cut -c1-32)"
|
|
53
|
+
[ -z "$_CV_RUN_ID" ] && _CV_RUN_ID="fallback-$(date +%s)-$$"
|
|
54
|
+
_CV_AGENT="codex"
|
|
55
|
+
_CV_SOURCE="${CODEVIBE_TELEMETRY_SOURCE:-production}"
|
|
56
|
+
_CV_STARTED_AT="$(date +%s)"
|
|
57
|
+
_CV_EXITED="" # set by terminal events; suppresses trap double-fire
|
|
58
|
+
_CV_PLUGIN_VERSION="$(node -p "require('$PLUGIN_DIR/package.json').version" 2>/dev/null || echo unknown)"
|
|
59
|
+
_CV_MCP_LOG="${CODEVIBE_TMPDIR}/codevibe-codex-mcp.log"
|
|
60
|
+
_CV_MCP_LOG_BASELINE=0
|
|
61
|
+
if [ -f "$_CV_MCP_LOG" ]; then
|
|
62
|
+
_CV_MCP_LOG_BASELINE=$(wc -l < "$_CV_MCP_LOG" 2>/dev/null | tr -d ' ')
|
|
63
|
+
[ -z "$_CV_MCP_LOG_BASELINE" ] && _CV_MCP_LOG_BASELINE=0
|
|
64
|
+
fi
|
|
65
|
+
_CV_TMUX_STARTED="false"
|
|
66
|
+
_CV_AGENT_INVOKED="false"
|
|
67
|
+
_CV_AGENT_STARTED_AT=0
|
|
68
|
+
_CV_CODEX_EXIT_FILE="${CODEVIBE_TMPDIR}/codevibe-codex-exit-$$"
|
|
69
|
+
|
|
70
|
+
# Strip an arbitrary string down to a JSON-safe identifier alphabet.
|
|
71
|
+
# Removes anything that could break the hand-built JSON payload below
|
|
72
|
+
# (quotes, backslashes, ANSI escapes, control bytes, tabs, newlines).
|
|
73
|
+
# Truncates to 40 chars to bound the impact of pathological CLI version
|
|
74
|
+
# output. Caller is responsible for emptiness check after sanitize.
|
|
75
|
+
cv_sanitize() {
|
|
76
|
+
printf '%s' "$1" | LC_ALL=C tr -cd 'A-Za-z0-9._\- ' | cut -c1-40
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Sanitize trusted-but-still-string values that go into the payload
|
|
80
|
+
# (plugin version, source label) so future schema additions can't
|
|
81
|
+
# accidentally reintroduce a JSON-injection path.
|
|
82
|
+
_CV_PLUGIN_VERSION="$(cv_sanitize "$_CV_PLUGIN_VERSION")"
|
|
83
|
+
[ -z "$_CV_PLUGIN_VERSION" ] && _CV_PLUGIN_VERSION="unknown"
|
|
84
|
+
_CV_SOURCE="$(cv_sanitize "$_CV_SOURCE")"
|
|
85
|
+
[ -z "$_CV_SOURCE" ] && _CV_SOURCE="production"
|
|
86
|
+
|
|
87
|
+
cv_telem() {
|
|
88
|
+
local event="$1"; shift
|
|
89
|
+
local params="$*"
|
|
90
|
+
curl -s -X POST \
|
|
91
|
+
"https://www.google-analytics.com/mp/collect?measurement_id=${_CV_MID}&api_secret=${_CV_SEC}" \
|
|
92
|
+
-H "Content-Type: application/json" \
|
|
93
|
+
-d "{\"client_id\":\"${_CV_CID}\",\"events\":[{\"name\":\"${event}\",\"params\":{\"agent\":\"${_CV_AGENT}\",\"plugin_version\":\"${_CV_PLUGIN_VERSION}\",\"source\":\"${_CV_SOURCE}\",\"run_id\":\"${_CV_RUN_ID}\"${params:+,$params}}}]}" \
|
|
94
|
+
</dev/null >/dev/null 2>&1 &
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
cv_failed() {
|
|
98
|
+
[ -n "$_CV_EXITED" ] && return 0
|
|
99
|
+
_CV_EXITED="failed"
|
|
100
|
+
cv_telem "wrapper_failed" "\"reason\":\"$1\",\"lifetime_seconds\":$(( $(date +%s) - _CV_STARTED_AT ))"
|
|
101
|
+
}
|
|
102
|
+
|
|
44
103
|
# Handle auth commands (login, logout, status, reset-device)
|
|
45
104
|
# Delegate to codevibe-core CLI (shared auth across all plugins)
|
|
46
105
|
case "$1" in
|
|
@@ -51,14 +110,42 @@ case "$1" in
|
|
|
51
110
|
CORE_CLI="$PLUGIN_DIR/../codevibe-core/bin/codevibe.js"
|
|
52
111
|
fi
|
|
53
112
|
if [ -f "$CORE_CLI" ]; then
|
|
113
|
+
cv_telem "wrapper_started" "\"invocation\":\"auth_$1\",\"os\":\"$(uname -s | cv_sanitize)\",\"arch\":\"$(uname -m | cv_sanitize)\""
|
|
54
114
|
exec node "$CORE_CLI" "$1"
|
|
55
115
|
else
|
|
56
116
|
echo "Error: codevibe-core not found. Try reinstalling: npm install -g @quantiya/codevibe"
|
|
117
|
+
cv_failed "core_not_found"
|
|
118
|
+
sleep 1
|
|
57
119
|
exit 1
|
|
58
120
|
fi
|
|
59
121
|
;;
|
|
60
122
|
esac
|
|
61
123
|
|
|
124
|
+
# Capture environment facts for the session-flow wrapper_started event.
|
|
125
|
+
# Each probe is non-fatal — if a CLI is missing we record "missing" rather
|
|
126
|
+
# than aborting; pre-flight checks below still gate execution. Every
|
|
127
|
+
# string that lands in the JSON payload goes through cv_sanitize so an
|
|
128
|
+
# agent CLI emitting ANSI escapes or quotes in `--version` can't break
|
|
129
|
+
# the hand-built payload.
|
|
130
|
+
_CV_CODEX_VER="missing"
|
|
131
|
+
command -v codex >/dev/null 2>&1 && _CV_CODEX_VER="$(codex --version 2>/dev/null | cv_sanitize)"
|
|
132
|
+
[ -z "$_CV_CODEX_VER" ] && _CV_CODEX_VER="unknown"
|
|
133
|
+
_CV_NODE_VER="missing"
|
|
134
|
+
command -v node >/dev/null 2>&1 && _CV_NODE_VER="$(node -v 2>/dev/null | cv_sanitize)"
|
|
135
|
+
[ -z "$_CV_NODE_VER" ] && _CV_NODE_VER="unknown"
|
|
136
|
+
_CV_TMUX_VER="missing"
|
|
137
|
+
command -v tmux >/dev/null 2>&1 && _CV_TMUX_VER="$(tmux -V 2>/dev/null | cv_sanitize)"
|
|
138
|
+
[ -z "$_CV_TMUX_VER" ] && _CV_TMUX_VER="unknown"
|
|
139
|
+
_CV_OS_VER="$(uname -s | cv_sanitize)"
|
|
140
|
+
[ -z "$_CV_OS_VER" ] && _CV_OS_VER="unknown"
|
|
141
|
+
_CV_ARCH_VER="$(uname -m | cv_sanitize)"
|
|
142
|
+
[ -z "$_CV_ARCH_VER" ] && _CV_ARCH_VER="unknown"
|
|
143
|
+
_CV_CODEX_AUTH="false"; [ -f "$HOME/.codex/auth.json" ] && _CV_CODEX_AUTH="true"
|
|
144
|
+
_CV_CODEX_CONFIG="false"; [ -f "$HOME/.codex/config.toml" ] && _CV_CODEX_CONFIG="true"
|
|
145
|
+
_CV_INSIDE_TMUX="false"; [ -n "$TMUX" ] && _CV_INSIDE_TMUX="true"
|
|
146
|
+
_CV_IS_TTY="false"; { [ -t 0 ] && [ -t 1 ]; } && _CV_IS_TTY="true"
|
|
147
|
+
cv_telem "wrapper_started" "\"invocation\":\"session\",\"os\":\"$_CV_OS_VER\",\"arch\":\"$_CV_ARCH_VER\",\"codex_version\":\"$_CV_CODEX_VER\",\"node_version\":\"$_CV_NODE_VER\",\"tmux_version\":\"$_CV_TMUX_VER\",\"codex_auth_present\":$_CV_CODEX_AUTH,\"codex_config_present\":$_CV_CODEX_CONFIG,\"inside_tmux\":$_CV_INSIDE_TMUX,\"is_terminal\":$_CV_IS_TTY"
|
|
148
|
+
|
|
62
149
|
# Configuration
|
|
63
150
|
TMUX_SESSION_PREFIX="codevibe-codex"
|
|
64
151
|
LOG_FILE="${CODEVIBE_TMPDIR}/codevibe-codex-wrapper.log"
|
|
@@ -70,7 +157,52 @@ log() {
|
|
|
70
157
|
|
|
71
158
|
# Cleanup function to kill server when wrapper exits
|
|
72
159
|
cleanup() {
|
|
160
|
+
local wrapper_exit_code=$?
|
|
73
161
|
log "Cleanup triggered"
|
|
162
|
+
|
|
163
|
+
# Fire wrapper_exited telemetry BEFORE killing the server so the MCP
|
|
164
|
+
# log is intact when we grep for SessionStart. cv_failed sets
|
|
165
|
+
# _CV_EXITED on pre-flight failures so this block won't double-fire.
|
|
166
|
+
if [ -z "$_CV_EXITED" ]; then
|
|
167
|
+
_CV_EXITED="exited"
|
|
168
|
+
local codex_exit="unknown"
|
|
169
|
+
if [ -f "$_CV_CODEX_EXIT_FILE" ]; then
|
|
170
|
+
codex_exit="$(cat "$_CV_CODEX_EXIT_FILE" 2>/dev/null | head -c 10 | tr -d '\n\r ')"
|
|
171
|
+
[ -z "$codex_exit" ] && codex_exit="unknown"
|
|
172
|
+
fi
|
|
173
|
+
local lifetime=$(( $(date +%s) - _CV_STARTED_AT ))
|
|
174
|
+
local codex_lifetime=0
|
|
175
|
+
if [ "$_CV_AGENT_STARTED_AT" -gt 0 ] 2>/dev/null; then
|
|
176
|
+
codex_lifetime=$(( $(date +%s) - _CV_AGENT_STARTED_AT ))
|
|
177
|
+
fi
|
|
178
|
+
local hook_fired="false"
|
|
179
|
+
if [ -f "$_CV_MCP_LOG" ]; then
|
|
180
|
+
if tail -n "+$((_CV_MCP_LOG_BASELINE + 1))" "$_CV_MCP_LOG" 2>/dev/null \
|
|
181
|
+
| grep -q "SessionStart" 2>/dev/null; then
|
|
182
|
+
hook_fired="true"
|
|
183
|
+
fi
|
|
184
|
+
fi
|
|
185
|
+
# Outcome priority: SIGINT/SIGTERM beats everything (user intent).
|
|
186
|
+
# Then "we never got far enough to invoke codex" — distinct from
|
|
187
|
+
# "we invoked codex via passthrough but never started a tmux of
|
|
188
|
+
# our own" (the latter is a normal direct-run, not an abort).
|
|
189
|
+
local outcome
|
|
190
|
+
if [ "$wrapper_exit_code" = "130" ] || [ "$wrapper_exit_code" = "143" ]; then
|
|
191
|
+
outcome="interrupted"
|
|
192
|
+
elif [ "$_CV_AGENT_INVOKED" = "false" ]; then
|
|
193
|
+
outcome="pre_invoke_abort"
|
|
194
|
+
elif [ "$codex_exit" != "unknown" ] && [ "$codex_exit" != "0" ]; then
|
|
195
|
+
outcome="error_exit"
|
|
196
|
+
elif [ "$codex_lifetime" -lt 5 ] 2>/dev/null; then
|
|
197
|
+
outcome="early_exit"
|
|
198
|
+
elif [ "$codex_lifetime" -lt 60 ] 2>/dev/null; then
|
|
199
|
+
outcome="clean_short"
|
|
200
|
+
else
|
|
201
|
+
outcome="clean_long"
|
|
202
|
+
fi
|
|
203
|
+
cv_telem "wrapper_exited" "\"exit_code\":$wrapper_exit_code,\"lifetime_seconds\":$lifetime,\"codex_exit_code\":\"$codex_exit\",\"codex_lifetime_seconds\":$codex_lifetime,\"tmux_session_started\":$_CV_TMUX_STARTED,\"agent_invoked\":$_CV_AGENT_INVOKED,\"session_start_hook_fired\":$hook_fired,\"terminal_outcome\":\"$outcome\""
|
|
204
|
+
fi
|
|
205
|
+
|
|
74
206
|
if [ -n "$SERVER_PID" ] && kill -0 "$SERVER_PID" 2>/dev/null; then
|
|
75
207
|
log "Stopping server (PID: $SERVER_PID)"
|
|
76
208
|
kill "$SERVER_PID" 2>/dev/null || true
|
|
@@ -79,6 +211,7 @@ cleanup() {
|
|
|
79
211
|
# Remove PID file and port file
|
|
80
212
|
rm -f "${CODEVIBE_TMPDIR}/codevibe-codex-server-$$.pid"
|
|
81
213
|
rm -f "${CODEVIBE_TMPDIR}/codevibe-codex-${SESSION_NAME}.port"
|
|
214
|
+
rm -f "$_CV_CODEX_EXIT_FILE"
|
|
82
215
|
|
|
83
216
|
# Remove our hooks from ~/.codex/hooks.json (idempotent, concurrent-safe)
|
|
84
217
|
if command -v jq &> /dev/null && [ -f "$HOME/.codex/hooks.json" ]; then
|
|
@@ -101,6 +234,8 @@ trap cleanup EXIT INT TERM
|
|
|
101
234
|
if ! command -v tmux &> /dev/null; then
|
|
102
235
|
echo "Error: tmux is required but not installed."
|
|
103
236
|
echo "Install with: brew install tmux"
|
|
237
|
+
cv_failed "tmux_missing"
|
|
238
|
+
sleep 1
|
|
104
239
|
exit 1
|
|
105
240
|
fi
|
|
106
241
|
|
|
@@ -108,18 +243,24 @@ fi
|
|
|
108
243
|
if ! command -v codex &> /dev/null; then
|
|
109
244
|
echo "Error: codex CLI is not installed."
|
|
110
245
|
echo "Install with: npm install -g @openai/codex"
|
|
246
|
+
cv_failed "codex_missing"
|
|
247
|
+
sleep 1
|
|
111
248
|
exit 1
|
|
112
249
|
fi
|
|
113
250
|
|
|
114
251
|
# Check if node is installed
|
|
115
252
|
if ! command -v node &> /dev/null; then
|
|
116
253
|
echo "Error: Node.js is required but not installed."
|
|
254
|
+
cv_failed "node_missing"
|
|
255
|
+
sleep 1
|
|
117
256
|
exit 1
|
|
118
257
|
fi
|
|
119
258
|
|
|
120
259
|
# Check if server is built
|
|
121
260
|
if [ ! -f "$PLUGIN_DIR/dist/server.js" ]; then
|
|
122
261
|
echo "Error: Server not built. Run 'npm run build' in the plugin directory first."
|
|
262
|
+
cv_failed "server_not_built"
|
|
263
|
+
sleep 1
|
|
123
264
|
exit 1
|
|
124
265
|
fi
|
|
125
266
|
|
|
@@ -131,16 +272,36 @@ log "Starting codevibe-codex with session: $SESSION_NAME"
|
|
|
131
272
|
log "Working directory: $WORKING_DIR"
|
|
132
273
|
log "Arguments: $*"
|
|
133
274
|
|
|
134
|
-
# Check if we're already inside tmux
|
|
275
|
+
# Check if we're already inside tmux.
|
|
276
|
+
# We deliberately do NOT `exec` here — running codex as a child process
|
|
277
|
+
# lets the EXIT trap fire after it returns so wrapper_exited still gets
|
|
278
|
+
# emitted on these direct-run paths. Behaviorally identical for the user
|
|
279
|
+
# (codex remains the foreground process for the duration).
|
|
135
280
|
if [ -n "$TMUX" ]; then
|
|
136
281
|
log "Already inside tmux, running codex directly"
|
|
137
|
-
|
|
282
|
+
_CV_AGENT_INVOKED="true"
|
|
283
|
+
_CV_AGENT_STARTED_AT="$(date +%s)"
|
|
284
|
+
# `|| _CV_RC=$?` is load-bearing: with `set -e`, a non-zero exit from
|
|
285
|
+
# codex would abort the wrapper before we capture the exit code,
|
|
286
|
+
# leaving wrapper_exited with codex_exit_code="unknown". The `||`
|
|
287
|
+
# form catches non-zero without triggering set -e, while exit 0
|
|
288
|
+
# leaves _CV_RC at its 0 default. printf's `|| true` keeps a
|
|
289
|
+
# disk-full failure from clobbering diagnostics.
|
|
290
|
+
_CV_RC=0
|
|
291
|
+
codex "$@" || _CV_RC=$?
|
|
292
|
+
printf '%s' "$_CV_RC" > "$_CV_CODEX_EXIT_FILE" 2>/dev/null || true
|
|
293
|
+
exit "$_CV_RC"
|
|
138
294
|
fi
|
|
139
295
|
|
|
140
|
-
# Check if running in a terminal
|
|
296
|
+
# Check if running in a terminal — same direct-run treatment as above.
|
|
141
297
|
if [ ! -t 0 ] || [ ! -t 1 ]; then
|
|
142
298
|
log "Not running in a terminal, running codex directly"
|
|
143
|
-
|
|
299
|
+
_CV_AGENT_INVOKED="true"
|
|
300
|
+
_CV_AGENT_STARTED_AT="$(date +%s)"
|
|
301
|
+
_CV_RC=0
|
|
302
|
+
codex "$@" || _CV_RC=$?
|
|
303
|
+
printf '%s' "$_CV_RC" > "$_CV_CODEX_EXIT_FILE" 2>/dev/null || true
|
|
304
|
+
exit "$_CV_RC"
|
|
144
305
|
fi
|
|
145
306
|
|
|
146
307
|
# Start the session log watcher server in background
|
|
@@ -224,6 +385,8 @@ if ! kill -0 "$SERVER_PID" 2>/dev/null; then
|
|
|
224
385
|
tail -3 "$MCP_LOG_FILE" 2>/dev/null | grep -v '^\[' | head -1
|
|
225
386
|
echo ""
|
|
226
387
|
echo "Server failed to start. Check $MCP_LOG_FILE for details."
|
|
388
|
+
cv_failed "server_died_on_startup"
|
|
389
|
+
sleep 1
|
|
227
390
|
exit 1
|
|
228
391
|
fi
|
|
229
392
|
|
|
@@ -238,9 +401,15 @@ for arg in "$@"; do
|
|
|
238
401
|
CODEX_CMD="$CODEX_CMD '$escaped_arg'"
|
|
239
402
|
done
|
|
240
403
|
|
|
241
|
-
# Create the session running codex
|
|
404
|
+
# Create the session running codex.
|
|
405
|
+
# The inner shell writes codex's exit code to $_CV_CODEX_EXIT_FILE so the
|
|
406
|
+
# wrapper's cleanup trap can report it via `wrapper_exited` telemetry —
|
|
407
|
+
# tmux's own attach exit code is independent of the inner process exit.
|
|
242
408
|
tmux new-session -d -s "$SESSION_NAME" -x "$(tput cols)" -y "$(tput lines)" \
|
|
243
|
-
"export CODEVIBE_CODEX_TMUX_SESSION='$SESSION_NAME'; export ENVIRONMENT='$ENVIRONMENT'; $CODEX_CMD; exit"
|
|
409
|
+
"export CODEVIBE_CODEX_TMUX_SESSION='$SESSION_NAME'; export ENVIRONMENT='$ENVIRONMENT'; $CODEX_CMD; printf '%s' \"\$?\" > '$_CV_CODEX_EXIT_FILE'; exit"
|
|
410
|
+
_CV_TMUX_STARTED="true"
|
|
411
|
+
_CV_AGENT_INVOKED="true"
|
|
412
|
+
_CV_AGENT_STARTED_AT="$(date +%s)"
|
|
244
413
|
|
|
245
414
|
# Enable mouse support for scrolling
|
|
246
415
|
tmux set-option -t "$SESSION_NAME" -g mouse on
|
package/dist/server.js
CHANGED
|
@@ -6,7 +6,7 @@ ${a}`,metadata:{toolName:p,toolOutput:o,callId:s,status:"completed"}}}if(i==="cu
|
|
|
6
6
|
`).slice(-20).join(`
|
|
7
7
|
`);return/\[(?:y\/n|Y\/n|y\/N)\]|^\s*\d+\.\s+/im.test(i)}hashPromptSnapshot(e){let i=e.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g,"").replace(/\r/g,`
|
|
8
8
|
`).replace(/[ \t]+\n/g,`
|
|
9
|
-
`).trim();return(0,ft.createHash)("sha256").update(i).digest("hex")}};var _=require("@quantiya/codevibe-core");var z=g(require("express")),E=g(require("fs")),V=g(require("path")),q=g(require("os"));I();var L=class{constructor(){this.assignedPort=0;this.app=(0,z.default)(),this.setupMiddleware(),this.setupRoutes(),this.tmuxSession=process.env.CODEVIBE_CODEX_TMUX_SESSION}getPort(){return this.assignedPort}setupMiddleware(){this.app.use(z.default.json({limit:"1mb"})),this.app.use((t,e,i)=>{n.debug(`${t.method} ${t.path}`,{body:t.body}),i()})}setupRoutes(){this.app.get("/health",(t,e)=>{e.json({success:!0,data:{status:"healthy",uptime:process.uptime()}})}),this.app.post("/event",this.handleEvent.bind(this))}async handleEvent(t,e){try{let i=t.body;if(!i.session_id||!i.hook_event_name){e.status(400).json({success:!1,error:"Missing session_id or hook_event_name"});return}let s=this.transformHookToEvent(i);n.info("Received hook event",{sessionId:i.session_id,hookEvent:i.hook_event_name,type:s.type}),this.eventHandler&&await this.eventHandler(s),e.json({success:!0})}catch(i){n.error("Error handling event:",i),e.status(500).json({success:!1,error:i instanceof Error?i.message:"Unknown error"})}}transformHookToEvent(t){let e={cwd:t.cwd,hook_event_name:t.hook_event_name,...t.metadata||{}},i,s;switch(t.hook_event_name){case"SessionStart":i="NOTIFICATION",s="Session started",e.source=t.source;break;case"UserPromptSubmit":i="USER_PROMPT",s=t.prompt||"";break;case"PreToolUse":i="INTERACTIVE_PROMPT",s=`${t.tool_name||"Tool"} requires approval`,e.tool_name=t.tool_name,e.tool_input=t.tool_input,e.tool_use_id=t.tool_use_id;break;case"PostToolUse":i="TOOL_USE",s=JSON.stringify({tool_name:t.tool_name,tool_input:t.tool_input,tool_response:t.tool_response}),e.tool_name=t.tool_name;break;case"Stop":i="ASSISTANT_RESPONSE",s=t.last_assistant_message||"";break;default:i="NOTIFICATION",s=`Hook: ${t.hook_event_name}`}return{session_id:t.session_id,hook_event_name:t.hook_event_name,type:i,source:"DESKTOP",content:s,metadata:e}}onEvent(t){this.eventHandler=t}async start(){return new Promise((t,e)=>{try{this.server=this.app.listen(0,"localhost",()=>{let i=this.server.address();this.assignedPort=i.port,n.info(`HTTP API listening on http://localhost:${this.assignedPort}`),this.writePortFile(this.assignedPort),t(this.assignedPort)}),this.server.on("error",i=>{n.error("HTTP server error:",i),e(i)})}catch(i){e(i)}})}writePortFile(t){if(!this.tmuxSession){n.warn("No CODEVIBE_CODEX_TMUX_SESSION set, skipping port file");return}let e=V.join(q.tmpdir(),`codevibe-codex-${this.tmuxSession}.port`);try{E.writeFileSync(e,t.toString()),n.info(`Port file written: ${e} -> ${t}`)}catch(i){n.error(`Failed to write port file: ${e}`,i)}}removePortFile(){if(!this.tmuxSession)return;let t=V.join(q.tmpdir(),`codevibe-codex-${this.tmuxSession}.port`);try{E.existsSync(t)&&(E.unlinkSync(t),n.info(`Port file removed: ${t}`))}catch(e){n.warn(`Failed to remove port file: ${t}`,e)}}async stop(){return this.removePortFile(),new Promise(t=>{this.server?this.server.close(()=>{n.info("HTTP API stopped"),t()}):t()})}};var G=class{constructor(){this.sessionState=null;this.unsubscribe=null;this.sessionKey=null;this.pendingInteractivePrompt=null;this.isInitializingSession=!1;this.bufferedLogEntries=[];this.hooksActive=!1;this.subscribedSessionId=null;this.httpApi=new L,this.sessionWatcher=new O,this.approvalDetector=new A,this.promptResponder=new N,this.tmuxPaneObserver=new M}async start(){n.info("Starting CodeVibe Codex companion server",{environment:(0,c.getEnvironment)()}),this.appSyncClient=new c.AppSyncClient,await this.appSyncClient.authenticateWithStoredTokens()||(n.error('Authentication failed. Run "codevibe-codex login" first.'),console.error('Not authenticated. Run "codevibe-codex login" to sign in.'),process.exit(1)),n.info("Authenticated successfully",{userId:this.appSyncClient.getCurrentUserId(),email:this.appSyncClient.getCurrentUserEmail()}),await(0,c.registerDeviceEncryptionKey)(this.appSyncClient,n),(0,c.startDeviceKeyWatcher)(this.appSyncClient,n),this.httpApi.onEvent(this.handleEventFromHook.bind(this));let e=await this.httpApi.start();n.info("HTTP API started for hooks",{port:e}),await this.createLaunchSession(),this.setupEventHandlers(),this.sessionWatcher.start(),n.info("CodeVibe Codex companion server started")}async createLaunchSession(){let t=process.env.CODEVIBE_CODEX_TMUX_SESSION;if(!t){n.warn("No CODEVIBE_CODEX_TMUX_SESSION \u2014 skipping launch session");return}let e=process.env.CODEX_WORKING_DIRECTORY||process.cwd(),i=this.generateSessionId(t),s=this.appSyncClient.getCurrentUserId();n.info("Creating launch session",{sessionId:i,projectPath:e});try{let o=await(0,c.resumeOrCreateSession)({sessionId:i,userId:s,agentType:c.AgentType.CODEX,projectPath:e,metadata:{launchSession:!0}},this.appSyncClient,n);this.sessionKey=o.sessionKey,this.sessionState={sessionId:i,userId:s,projectPath:e,cwd:e,createdAt:new Date,subscriptionActive:!1,metadata:{launchSession:!0},codexSessionId:t,codexLogFile:void 0},this.subscribeToMobileEvents(i),this.appSyncClient.startHeartbeat(i),n.info("Launch session created",{sessionId:i})}catch(o){n.error("Failed to create launch session (non-fatal)",{error:o})}}async handleEventFromHook(t){let{session_id:e,hook_event_name:i,type:s,content:o,metadata:r}=t;if(this.hooksActive=!0,n.info("[Hooks] Received event",{sessionId:e,hookEvent:i,type:s,contentLength:o?.length}),i==="SessionStart"){if(this.sessionState)n.info("[Hooks] SessionStart \u2014 launch session already exists, updating codexSessionId",{existingSessionId:this.sessionState.sessionId,codexSessionId:e}),this.sessionState.codexSessionId=e,this.sessionState.metadata={...this.sessionState.metadata,codexSessionId:e,cliVersion:r?.model||"unknown",modelProvider:r?.model||"unknown",launchSession:void 0},this.appSyncClient.updateSession({sessionId:this.sessionState.sessionId,metadata:this.sessionState.metadata}).catch(a=>n.warn("Failed to update session metadata",{error:a}));else{let a={id:e,timestamp:new Date().toISOString(),cwd:r?.cwd||process.cwd(),originator:"hook",cli_version:r?.model||"unknown",instructions:null,source:r?.source||"startup",model_provider:r?.model||"unknown"};await this.handleSessionStarted(a)}return}if(!this.sessionState){n.warn("[Hooks] Session not initialized, buffering event",{hook_event_name:i});return}let p=this.sessionState.sessionId;if(s==="USER_PROMPT"&&o&&this.isRecentMobilePrompt(o)){n.info("[Hooks] Skipping duplicate USER_PROMPT from mobile");return}if(i==="PreToolUse"){let a=r?.tool_name||"unknown",d=r?.tool_input,u={tool_name:this.mapToolName(a),tool_input:d||{}};if(a==="apply_patch"&&typeof d=="string"){let{extractOldNewFromPatch:x,extractFileFromPatch:D}=(j(),Tt(pt)),w=x(d),U=D(d);w&&(u.tool_input={file_path:U||"",old_string:w.oldString,new_string:w.newString})}let h=process.env.CODEVIBE_CODEX_TMUX_SESSION;if(h)try{await new Promise(U=>setTimeout(U,500));let{execSync:x}=require("child_process"),D=x(`tmux capture-pane -p -e -S -30 -t '${h}'`,{timeout:5e3,encoding:"utf8"}),w=(0,_.parseInteractivePrompt)(D);w&&w.options.length>0&&(u.options=w.options,u.submitMap=w.submitMap)}catch{n.debug("[Hooks] Tmux capture failed, using default options")}let m=o,P=u,b=!1;this.sessionKey&&(m=c.cryptoService.encryptContent(o,this.sessionKey),P={encrypted:c.cryptoService.encryptMetadata(u,this.sessionKey)},b=!0),await this.appSyncClient.createEvent({sessionId:p,type:c.EventType.INTERACTIVE_PROMPT,source:c.EventSource.DESKTOP,content:m,metadata:P,isEncrypted:b}),this.pendingInteractivePrompt={promptId:(0,X.v4)(),kind:"yes_no",options:u.options||[],submitMap:u.submitMap||{},promptText:o,createdAt:Date.now(),source:"tmux",requiresFollowUpText:!1},n.info("[Hooks] INTERACTIVE_PROMPT sent",{toolName:a,sessionId:p});return}if(i==="PostToolUse"){let a=o,d=r,u=!1;this.sessionKey&&(a=c.cryptoService.encryptContent(o,this.sessionKey),r&&(d={encrypted:c.cryptoService.encryptMetadata(r,this.sessionKey)}),u=!0),await this.appSyncClient.createEvent({sessionId:p,type:c.EventType.TOOL_USE,source:c.EventSource.DESKTOP,content:a,metadata:d,isEncrypted:u});return}if(s==="ASSISTANT_RESPONSE"||s==="USER_PROMPT"){let a=o,d=!1;this.sessionKey&&o&&(a=c.cryptoService.encryptContent(o,this.sessionKey),d=!0),await this.appSyncClient.createEvent({sessionId:p,type:s==="ASSISTANT_RESPONSE"?c.EventType.ASSISTANT_RESPONSE:c.EventType.USER_PROMPT,source:c.EventSource.DESKTOP,content:a,isEncrypted:d});return}}mapToolName(t){return{shell_command:"Bash",shell:"Bash",apply_patch:"Edit",create_file:"Write",read_file:"Read"}[t]||t}isRecentMobilePrompt(t){return!1}setupEventHandlers(){this.sessionWatcher.on("session-started",async t=>{if(this.sessionState){n.info("[JSONL] Session already active, skipping",{currentSessionId:this.sessionState.sessionId,codexSessionId:t.id});return}await this.handleSessionStarted(t)}),this.sessionWatcher.on("log-entry",async t=>{await this.handleLogEntry(t)}),this.approvalDetector.on("approval-pending",async t=>{await this.handleApprovalPending(t)}),this.tmuxPaneObserver.on("prompt-candidate",async t=>{await this.handleTmuxPromptCandidate(t.snapshot)}),this.tmuxPaneObserver.on("observer-error",t=>{n.debug("Tmux pane observer error",{error:t})}),this.sessionWatcher.on("error",t=>{n.error("Session watcher error:",t)})}async handleSessionStarted(t){n.info("Handling new Codex session",{codexSessionId:t.id}),this.isInitializingSession=!0,this.bufferedLogEntries=[],this.sessionState&&await this.endActiveSession("new-codex-session-started");let e=process.env.CODEX_WORKING_DIRECTORY||t.cwd||process.cwd(),i=this.generateSessionId(t.id),s=this.appSyncClient.getCurrentUserId(),o={codexSessionId:t.id,cliVersion:t.cli_version,modelProvider:t.model_provider};try{let r=await(0,c.resumeOrCreateSession)({sessionId:i,userId:s,agentType:c.AgentType.CODEX,projectPath:e,metadata:o},this.appSyncClient,n);this.sessionKey=r.sessionKey}catch(r){throw n.error("Failed to create/resume session:",r),r}try{this.sessionState={sessionId:i,userId:s,projectPath:e,cwd:t.cwd,createdAt:new Date,subscriptionActive:!1,metadata:o,codexSessionId:t.id,codexLogFile:this.sessionWatcher.getActiveLogFile()||void 0},await this.flushBufferedLogEntries(),await this.startTmuxObserver(),this.subscribeToMobileEvents(i),this.appSyncClient.startHeartbeat(i)}catch(r){n.error("Failed to create session:",r),this.bufferedLogEntries=[]}finally{this.isInitializingSession=!1}}async flushBufferedLogEntries(){if(this.bufferedLogEntries.length===0)return;let t=this.bufferedLogEntries;this.bufferedLogEntries=[],n.info("Flushing buffered log entries after session initialization",{count:t.length,sessionId:this.sessionState?.sessionId});for(let e of t)await this.handleLogEntry(e)}async handleLogEntry(t){if(!this.sessionState){if(this.isInitializingSession){this.bufferedLogEntries.push(t),n.debug("Buffering log entry until session initialization completes",{type:t.type,bufferedCount:this.bufferedLogEntries.length});return}n.warn("Received log entry but no active session");return}if(t.type==="response_item"&&t.payload){let i=t.payload.type;i==="function_call"||i==="custom_tool_call"?this.approvalDetector.onToolCallStart(t.payload.call_id,t.payload.name,t.payload.arguments||t.payload.input||""):(i==="function_call_output"||i==="custom_tool_call_output")&&(this.approvalDetector.onToolCallComplete(t.payload.call_id),this.pendingInteractivePrompt?.callId===t.payload.call_id&&(this.pendingInteractivePrompt=null))}let e=K(t,this.sessionState.sessionId);if(e){if(this.hooksActive){if(e.type===c.EventType.USER_PROMPT||e.type===c.EventType.ASSISTANT_RESPONSE){n.debug("[JSONL] Skipping \u2014 hooks deliver this event type",{type:e.type});return}let i=t.payload?.type;if((i==="function_call"||i==="function_call_output")&&(e.type===c.EventType.TOOL_USE||e.type===c.EventType.INTERACTIVE_PROMPT)){n.debug("[JSONL] Skipping function_call \u2014 hooks deliver this",{type:e.type,tool:t.payload?.name});return}}try{if(this.sessionKey){if(e.content=c.cryptoService.encryptContent(e.content,this.sessionKey),e.metadata){let i=c.cryptoService.encryptMetadata(e.metadata,this.sessionKey);e.metadata={encrypted:i}}e.isEncrypted=!0,n.debug("Event encrypted",{type:e.type})}await this.appSyncClient.createEvent(e),n.debug("Event synced to backend",{type:e.type,encrypted:!!this.sessionKey})}catch(i){n.error("Failed to sync event:",i)}}}async handleApprovalPending(t){if(this.sessionState){n.info("Sending approval pending interactive prompt",t);try{let e=await this.tryParseInteractivePromptFromTmux(),i=e?.parsedPrompt??null;if(i&&this.pendingInteractivePrompt&&this.pendingInteractivePrompt.source==="tmux"&&this.pendingInteractivePrompt.promptText===i.promptText){n.debug("Skipping heuristic prompt because tmux prompt is already active",{promptText:i.promptText});return}let s=this.buildToolDetailsForInteractivePrompt(t,e?.snapshot),o=s.tool_name||this.mapToolNameForApproval(t.toolName),r=s.tool_input||this.buildFallbackToolInput(t),p=!!(o&&r),a=this.buildPromptPresentation(i),d=a.options,u=t.filePath?`File: ${t.filePath}`:void 0,h=a.content||`Codex is waiting for approval.
|
|
9
|
+
`).trim();return(0,ft.createHash)("sha256").update(i).digest("hex")}};var _=require("@quantiya/codevibe-core");var z=g(require("express")),E=g(require("fs")),V=g(require("path")),q=g(require("os"));I();var L=class{constructor(){this.assignedPort=0;this.app=(0,z.default)(),this.setupMiddleware(),this.setupRoutes(),this.tmuxSession=process.env.CODEVIBE_CODEX_TMUX_SESSION}getPort(){return this.assignedPort}setupMiddleware(){this.app.use(z.default.json({limit:"1mb"})),this.app.use((t,e,i)=>{n.debug(`${t.method} ${t.path}`,{body:t.body}),i()})}setupRoutes(){this.app.get("/health",(t,e)=>{e.json({success:!0,data:{status:"healthy",uptime:process.uptime()}})}),this.app.post("/event",this.handleEvent.bind(this))}async handleEvent(t,e){try{let i=t.body;if(!i.session_id||!i.hook_event_name){e.status(400).json({success:!1,error:"Missing session_id or hook_event_name"});return}let s=this.transformHookToEvent(i);n.info("Received hook event",{sessionId:i.session_id,hookEvent:i.hook_event_name,type:s.type}),this.eventHandler&&await this.eventHandler(s),e.json({success:!0})}catch(i){n.error("Error handling event:",i),e.status(500).json({success:!1,error:i instanceof Error?i.message:"Unknown error"})}}transformHookToEvent(t){let e={cwd:t.cwd,hook_event_name:t.hook_event_name,...t.metadata||{}},i,s;switch(t.hook_event_name){case"SessionStart":i="NOTIFICATION",s="Session started",e.source=t.source;break;case"UserPromptSubmit":i="USER_PROMPT",s=t.prompt||"";break;case"PreToolUse":i="INTERACTIVE_PROMPT",s=`${t.tool_name||"Tool"} requires approval`,e.tool_name=t.tool_name,e.tool_input=t.tool_input,e.tool_use_id=t.tool_use_id;break;case"PostToolUse":i="TOOL_USE",s=JSON.stringify({tool_name:t.tool_name,tool_input:t.tool_input,tool_response:t.tool_response}),e.tool_name=t.tool_name;break;case"Stop":i="ASSISTANT_RESPONSE",s=t.last_assistant_message||"";break;default:i="NOTIFICATION",s=`Hook: ${t.hook_event_name}`}return{session_id:t.session_id,hook_event_name:t.hook_event_name,type:i,source:"DESKTOP",content:s,metadata:e}}onEvent(t){this.eventHandler=t}async start(){return new Promise((t,e)=>{try{this.server=this.app.listen(0,"localhost",()=>{let i=this.server.address();this.assignedPort=i.port,n.info(`HTTP API listening on http://localhost:${this.assignedPort}`),this.writePortFile(this.assignedPort),t(this.assignedPort)}),this.server.on("error",i=>{n.error("HTTP server error:",i),e(i)})}catch(i){e(i)}})}writePortFile(t){if(!this.tmuxSession){n.warn("No CODEVIBE_CODEX_TMUX_SESSION set, skipping port file");return}let e=V.join(q.tmpdir(),`codevibe-codex-${this.tmuxSession}.port`);try{E.writeFileSync(e,t.toString()),n.info(`Port file written: ${e} -> ${t}`)}catch(i){n.error(`Failed to write port file: ${e}`,i)}}removePortFile(){if(!this.tmuxSession)return;let t=V.join(q.tmpdir(),`codevibe-codex-${this.tmuxSession}.port`);try{E.existsSync(t)&&(E.unlinkSync(t),n.info(`Port file removed: ${t}`))}catch(e){n.warn(`Failed to remove port file: ${t}`,e)}}async stop(){return this.removePortFile(),new Promise(t=>{this.server?this.server.close(()=>{n.info("HTTP API stopped"),t()}):t()})}};var G=class{constructor(){this.sessionState=null;this.unsubscribe=null;this.sessionKey=null;this.pendingInteractivePrompt=null;this.isInitializingSession=!1;this.bufferedLogEntries=[];this.hooksActive=!1;this.subscribedSessionId=null;this.httpApi=new L,this.sessionWatcher=new O,this.approvalDetector=new A,this.promptResponder=new N,this.tmuxPaneObserver=new M}async start(){n.info("Starting CodeVibe Codex companion server",{environment:(0,c.getEnvironment)()}),this.appSyncClient=new c.AppSyncClient,await this.appSyncClient.authenticateWithStoredTokens()||(n.error('Authentication failed. Run "codevibe-codex login" first.'),console.error('Not authenticated. Run "codevibe-codex login" to sign in.'),process.exit(1)),n.info("Authenticated successfully",{userId:this.appSyncClient.getCurrentUserId(),email:this.appSyncClient.getCurrentUserEmail()}),await(0,c.registerDeviceEncryptionKey)(this.appSyncClient,n),(0,c.startDeviceKeyWatcher)(this.appSyncClient,n);try{let i=await this.appSyncClient.sweepOrphanSessions({agentType:"CODEX"});i>0&&n.info("Orphan sweep: marked stale Codex sessions INACTIVE",{swept:i})}catch(i){n.warn("Orphan sweep failed, continuing startup",{error:i instanceof Error?i.message:String(i)})}this.httpApi.onEvent(this.handleEventFromHook.bind(this));let e=await this.httpApi.start();n.info("HTTP API started for hooks",{port:e}),await this.createLaunchSession(),this.setupEventHandlers(),this.sessionWatcher.start(),n.info("CodeVibe Codex companion server started")}async createLaunchSession(){let t=process.env.CODEVIBE_CODEX_TMUX_SESSION;if(!t){n.warn("No CODEVIBE_CODEX_TMUX_SESSION \u2014 skipping launch session");return}let e=process.env.CODEX_WORKING_DIRECTORY||process.cwd(),i=this.generateSessionId(t),s=this.appSyncClient.getCurrentUserId();n.info("Creating launch session",{sessionId:i,projectPath:e});try{let o=await(0,c.resumeOrCreateSession)({sessionId:i,userId:s,agentType:c.AgentType.CODEX,projectPath:e,metadata:{launchSession:!0}},this.appSyncClient,n);this.sessionKey=o.sessionKey,this.sessionState={sessionId:i,userId:s,projectPath:e,cwd:e,createdAt:new Date,subscriptionActive:!1,metadata:{launchSession:!0},codexSessionId:t,codexLogFile:void 0},this.subscribeToMobileEvents(i),this.appSyncClient.startHeartbeat(i),n.info("Launch session created",{sessionId:i})}catch(o){n.error("Failed to create launch session (non-fatal)",{error:o})}}async handleEventFromHook(t){let{session_id:e,hook_event_name:i,type:s,content:o,metadata:r}=t;if(this.hooksActive=!0,n.info("[Hooks] Received event",{sessionId:e,hookEvent:i,type:s,contentLength:o?.length}),i==="SessionStart"){if(this.sessionState)n.info("[Hooks] SessionStart \u2014 launch session already exists, updating codexSessionId",{existingSessionId:this.sessionState.sessionId,codexSessionId:e}),this.sessionState.codexSessionId=e,this.sessionState.metadata={...this.sessionState.metadata,codexSessionId:e,cliVersion:r?.model||"unknown",modelProvider:r?.model||"unknown",launchSession:void 0},this.appSyncClient.updateSession({sessionId:this.sessionState.sessionId,metadata:this.sessionState.metadata}).catch(a=>n.warn("Failed to update session metadata",{error:a}));else{let a={id:e,timestamp:new Date().toISOString(),cwd:r?.cwd||process.cwd(),originator:"hook",cli_version:r?.model||"unknown",instructions:null,source:r?.source||"startup",model_provider:r?.model||"unknown"};await this.handleSessionStarted(a)}return}if(!this.sessionState){n.warn("[Hooks] Session not initialized, buffering event",{hook_event_name:i});return}let p=this.sessionState.sessionId;if(s==="USER_PROMPT"&&o&&this.isRecentMobilePrompt(o)){n.info("[Hooks] Skipping duplicate USER_PROMPT from mobile");return}if(i==="PreToolUse"){let a=r?.tool_name||"unknown",d=r?.tool_input,u={tool_name:this.mapToolName(a),tool_input:d||{}};if(a==="apply_patch"&&typeof d=="string"){let{extractOldNewFromPatch:x,extractFileFromPatch:D}=(j(),Tt(pt)),w=x(d),U=D(d);w&&(u.tool_input={file_path:U||"",old_string:w.oldString,new_string:w.newString})}let h=process.env.CODEVIBE_CODEX_TMUX_SESSION;if(h)try{await new Promise(U=>setTimeout(U,500));let{execSync:x}=require("child_process"),D=x(`tmux capture-pane -p -e -S -30 -t '${h}'`,{timeout:5e3,encoding:"utf8"}),w=(0,_.parseInteractivePrompt)(D);w&&w.options.length>0&&(u.options=w.options,u.submitMap=w.submitMap)}catch{n.debug("[Hooks] Tmux capture failed, using default options")}let m=o,P=u,b=!1;this.sessionKey&&(m=c.cryptoService.encryptContent(o,this.sessionKey),P={encrypted:c.cryptoService.encryptMetadata(u,this.sessionKey)},b=!0),await this.appSyncClient.createEvent({sessionId:p,type:c.EventType.INTERACTIVE_PROMPT,source:c.EventSource.DESKTOP,content:m,metadata:P,isEncrypted:b}),this.pendingInteractivePrompt={promptId:(0,X.v4)(),kind:"yes_no",options:u.options||[],submitMap:u.submitMap||{},promptText:o,createdAt:Date.now(),source:"tmux",requiresFollowUpText:!1},n.info("[Hooks] INTERACTIVE_PROMPT sent",{toolName:a,sessionId:p});return}if(i==="PostToolUse"){let a=o,d=r,u=!1;this.sessionKey&&(a=c.cryptoService.encryptContent(o,this.sessionKey),r&&(d={encrypted:c.cryptoService.encryptMetadata(r,this.sessionKey)}),u=!0),await this.appSyncClient.createEvent({sessionId:p,type:c.EventType.TOOL_USE,source:c.EventSource.DESKTOP,content:a,metadata:d,isEncrypted:u});return}if(s==="ASSISTANT_RESPONSE"||s==="USER_PROMPT"){let a=o,d=!1;this.sessionKey&&o&&(a=c.cryptoService.encryptContent(o,this.sessionKey),d=!0),await this.appSyncClient.createEvent({sessionId:p,type:s==="ASSISTANT_RESPONSE"?c.EventType.ASSISTANT_RESPONSE:c.EventType.USER_PROMPT,source:c.EventSource.DESKTOP,content:a,isEncrypted:d});return}}mapToolName(t){return{shell_command:"Bash",shell:"Bash",apply_patch:"Edit",create_file:"Write",read_file:"Read"}[t]||t}isRecentMobilePrompt(t){return!1}setupEventHandlers(){this.sessionWatcher.on("session-started",async t=>{if(this.sessionState){n.info("[JSONL] Session already active, skipping",{currentSessionId:this.sessionState.sessionId,codexSessionId:t.id});return}await this.handleSessionStarted(t)}),this.sessionWatcher.on("log-entry",async t=>{await this.handleLogEntry(t)}),this.approvalDetector.on("approval-pending",async t=>{await this.handleApprovalPending(t)}),this.tmuxPaneObserver.on("prompt-candidate",async t=>{await this.handleTmuxPromptCandidate(t.snapshot)}),this.tmuxPaneObserver.on("observer-error",t=>{n.debug("Tmux pane observer error",{error:t})}),this.sessionWatcher.on("error",t=>{n.error("Session watcher error:",t)})}async handleSessionStarted(t){n.info("Handling new Codex session",{codexSessionId:t.id}),this.isInitializingSession=!0,this.bufferedLogEntries=[],this.sessionState&&await this.endActiveSession("new-codex-session-started");let e=process.env.CODEX_WORKING_DIRECTORY||t.cwd||process.cwd(),i=this.generateSessionId(t.id),s=this.appSyncClient.getCurrentUserId(),o={codexSessionId:t.id,cliVersion:t.cli_version,modelProvider:t.model_provider};try{let r=await(0,c.resumeOrCreateSession)({sessionId:i,userId:s,agentType:c.AgentType.CODEX,projectPath:e,metadata:o},this.appSyncClient,n);this.sessionKey=r.sessionKey}catch(r){throw n.error("Failed to create/resume session:",r),r}try{this.sessionState={sessionId:i,userId:s,projectPath:e,cwd:t.cwd,createdAt:new Date,subscriptionActive:!1,metadata:o,codexSessionId:t.id,codexLogFile:this.sessionWatcher.getActiveLogFile()||void 0},await this.flushBufferedLogEntries(),await this.startTmuxObserver(),this.subscribeToMobileEvents(i),this.appSyncClient.startHeartbeat(i)}catch(r){n.error("Failed to create session:",r),this.bufferedLogEntries=[]}finally{this.isInitializingSession=!1}}async flushBufferedLogEntries(){if(this.bufferedLogEntries.length===0)return;let t=this.bufferedLogEntries;this.bufferedLogEntries=[],n.info("Flushing buffered log entries after session initialization",{count:t.length,sessionId:this.sessionState?.sessionId});for(let e of t)await this.handleLogEntry(e)}async handleLogEntry(t){if(!this.sessionState){if(this.isInitializingSession){this.bufferedLogEntries.push(t),n.debug("Buffering log entry until session initialization completes",{type:t.type,bufferedCount:this.bufferedLogEntries.length});return}n.warn("Received log entry but no active session");return}if(t.type==="response_item"&&t.payload){let i=t.payload.type;i==="function_call"||i==="custom_tool_call"?this.approvalDetector.onToolCallStart(t.payload.call_id,t.payload.name,t.payload.arguments||t.payload.input||""):(i==="function_call_output"||i==="custom_tool_call_output")&&(this.approvalDetector.onToolCallComplete(t.payload.call_id),this.pendingInteractivePrompt?.callId===t.payload.call_id&&(this.pendingInteractivePrompt=null))}let e=K(t,this.sessionState.sessionId);if(e){if(this.hooksActive){if(e.type===c.EventType.USER_PROMPT||e.type===c.EventType.ASSISTANT_RESPONSE){n.debug("[JSONL] Skipping \u2014 hooks deliver this event type",{type:e.type});return}let i=t.payload?.type;if((i==="function_call"||i==="function_call_output")&&(e.type===c.EventType.TOOL_USE||e.type===c.EventType.INTERACTIVE_PROMPT)){n.debug("[JSONL] Skipping function_call \u2014 hooks deliver this",{type:e.type,tool:t.payload?.name});return}}try{if(this.sessionKey){if(e.content=c.cryptoService.encryptContent(e.content,this.sessionKey),e.metadata){let i=c.cryptoService.encryptMetadata(e.metadata,this.sessionKey);e.metadata={encrypted:i}}e.isEncrypted=!0,n.debug("Event encrypted",{type:e.type})}await this.appSyncClient.createEvent(e),n.debug("Event synced to backend",{type:e.type,encrypted:!!this.sessionKey})}catch(i){n.error("Failed to sync event:",i)}}}async handleApprovalPending(t){if(this.sessionState){n.info("Sending approval pending interactive prompt",t);try{let e=await this.tryParseInteractivePromptFromTmux(),i=e?.parsedPrompt??null;if(i&&this.pendingInteractivePrompt&&this.pendingInteractivePrompt.source==="tmux"&&this.pendingInteractivePrompt.promptText===i.promptText){n.debug("Skipping heuristic prompt because tmux prompt is already active",{promptText:i.promptText});return}let s=this.buildToolDetailsForInteractivePrompt(t,e?.snapshot),o=s.tool_name||this.mapToolNameForApproval(t.toolName),r=s.tool_input||this.buildFallbackToolInput(t),p=!!(o&&r),a=this.buildPromptPresentation(i),d=a.options,u=t.filePath?`File: ${t.filePath}`:void 0,h=a.content||`Codex is waiting for approval.
|
|
10
10
|
${t.hint}`;u&&!h.includes(u)&&(h=`${h}
|
|
11
11
|
${u}`),this.pendingInteractivePrompt={promptId:t.callId,callId:t.callId,kind:a.kind,options:d,submitMap:a.submitMap,promptText:a.promptText,createdAt:Date.now(),source:i?"tmux":"heuristic",requiresFollowUpText:a.requiresFollowUpText};let m={isApprovalHint:!0,toolName:t.toolName,toolInput:t.toolInput,hint:t.hint,callId:t.callId,filePath:t.filePath,diff:t.diff,rawInput:t.rawInput,tool_name:o,tool_input:r,has_details:p,options:d,instructions:a.instructions,prompt_source:i?"tmux":"heuristic"},P=!1;n.debug("Interactive prompt (pre-encryption)",{sessionId:this.sessionState.sessionId,callId:t.callId,contentPreview:h.substring(0,200),toolDetails:s,metadata:m}),this.sessionKey&&(h=c.cryptoService.encryptContent(h,this.sessionKey),m={encrypted:c.cryptoService.encryptMetadata(m,this.sessionKey)},P=!0),await this.appSyncClient.createEvent({sessionId:this.sessionState.sessionId,type:c.EventType.INTERACTIVE_PROMPT,source:c.EventSource.DESKTOP,content:h,metadata:m,promptId:t.callId,...P?{isEncrypted:!0}:{}})}catch(e){n.error("Failed to send approval interactive prompt:",e)}}}async handleTmuxPromptCandidate(t){if(!this.sessionState)return;let e=(0,_.parseInteractivePrompt)(t);if(!e||this.pendingInteractivePrompt&&this.pendingInteractivePrompt.source==="tmux"&&this.pendingInteractivePrompt.promptText===e.promptText)return;let i=this.buildPromptPresentation(e),s=this.getMostRecentPendingToolCall();s||(await new Promise(b=>setTimeout(b,500)),s=this.getMostRecentPendingToolCall());let o=s?this.buildApprovalPromptContextFromPendingCall(s):null,r=o?this.buildToolDetailsForInteractivePrompt(o,t):{},p=r.tool_name||this.mapToolNameForApproval(s?.name),a=r.tool_input||(o?this.buildFallbackToolInput(o):void 0),d=!!(p&&a),u=this.pendingInteractivePrompt?.callId||s?.callId||(0,X.v4)();this.pendingInteractivePrompt={promptId:u,callId:this.pendingInteractivePrompt?.callId||s?.callId,kind:i.kind,options:i.options,submitMap:i.submitMap,promptText:i.promptText,createdAt:Date.now(),source:"tmux",requiresFollowUpText:i.requiresFollowUpText};let h={options:i.options,instructions:i.instructions,prompt_source:"tmux_live",tool_name:p,tool_input:a,has_details:d},m=i.content,P=!1;this.sessionKey&&(m=c.cryptoService.encryptContent(m,this.sessionKey),h={encrypted:c.cryptoService.encryptMetadata(h,this.sessionKey)},P=!0);try{await this.appSyncClient.createEvent({sessionId:this.sessionState.sessionId,type:c.EventType.INTERACTIVE_PROMPT,source:c.EventSource.DESKTOP,content:m,metadata:h,promptId:u,...P?{isEncrypted:!0}:{}}),n.info("Sent tmux-detected interactive prompt",{sessionId:this.sessionState.sessionId,promptText:e.promptText,kind:e.kind})}catch(b){n.error("Failed to send tmux-detected interactive prompt",{error:b})}}async startTmuxObserver(){let t=process.env.CODEVIBE_CODEX_TMUX_SESSION;if(!t){n.debug("Skipping tmux pane observer start - no tmux session in environment");return}try{await this.tmuxPaneObserver.start(t)}catch(e){n.warn("Failed to start tmux pane observer",{tmuxSession:t,error:e})}}async tryParseInteractivePromptFromTmux(){try{let t=await this.tmuxPaneObserver.captureSnapshot(),e=(0,_.parseInteractivePrompt)(t);return n.debug("tmux prompt parse result",{parsed:!!e,kind:e?.kind,promptText:e?.promptText,snapshotPreview:this.summarizePromptSnapshot(t)}),{parsedPrompt:e,snapshot:t}}catch(t){return n.debug("tmux prompt parsing unavailable",{error:t}),null}}buildPromptPresentation(t){return t?{content:t.promptText,promptText:t.promptText,kind:t.kind,options:t.options,submitMap:t.submitMap,instructions:this.buildPromptInstructions(t),requiresFollowUpText:t.requiresFollowUpText}:{content:"Codex is waiting for approval.",promptText:"Codex is waiting for approval.",kind:"yes_no",options:[{number:"1",text:'Yes (sends "y")'},{number:"2",text:'No, tell Codex what to change (sends "n <instructions>")'}],submitMap:{1:"y",2:"n"},instructions:"Reply with 1 to approve, or 2 followed by what to change",requiresFollowUpText:!0}}getMostRecentPendingToolCall(){let t=this.approvalDetector.getPendingCalls();return t.length===0?null:t.reduce((e,i)=>i.timestamp>e.timestamp?i:e)}buildApprovalPromptContextFromPendingCall(t){return{toolName:t.name,filePath:t.filePath,diff:t.diff,toolInput:t.parsedInput,rawInput:t.input,hint:t.filePath?`File: ${t.filePath}`:`Tool: ${this.mapToolNameForApproval(t.name)||t.name}`}}buildPromptInstructions(t){return t.kind==="yes_no"&&t.requiresFollowUpText?"Reply with 1 to approve, or 2 followed by what to change":t.kind==="yes_no"?"Reply with 1 for yes or 2 for no":t.kind==="numbered"?"Reply with the number of the option you want":"Reply with your response"}summarizePromptSnapshot(t){return t.split(`
|
|
12
12
|
`).map(e=>e.trimEnd()).filter(e=>e.length>0).slice(-12).map(e=>e.slice(0,160)).join(`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quantiya/codevibe-codex-plugin",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.20",
|
|
4
4
|
"description": "Control OpenAI Codex CLI from your iPhone and Android — real-time sync, approve file edits, send prompts by voice. Part of CodeVibe.",
|
|
5
5
|
"main": "dist/server.js",
|
|
6
6
|
"bin": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"node": ">=18.0.0"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@quantiya/codevibe-core": "^1.0.
|
|
50
|
+
"@quantiya/codevibe-core": "^1.0.16",
|
|
51
51
|
"chokidar": "^4.0.0",
|
|
52
52
|
"dotenv": "^16.6.1",
|
|
53
53
|
"express": "^5.1.0",
|