@quantiya/codevibe-gemini-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.
@@ -43,6 +43,65 @@ done
43
43
  SCRIPT_DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
44
44
  PLUGIN_DIR="$(dirname "$SCRIPT_DIR")"
45
45
 
46
+ # ─── Wrapper telemetry (GA4 Measurement Protocol) ─────────────────────
47
+ # Diagnoses agent CLI failures: pre-flight bailouts, fast-die patterns,
48
+ # whether SessionStart hook fired, exit code. Background curl, fail
49
+ # silently, no PII (hashed hostname + per-run random id only). Honors
50
+ # CODEVIBE_TELEMETRY_SOURCE=test for internal testing.
51
+ _CV_MID="G-GS74YEQTB8"
52
+ _CV_SEC="lAfOF6OxRzSQ-NsLBRjhAg"
53
+ _CV_CID="$(echo "$(uname -n)-$(id -u)" | (sha256sum 2>/dev/null || shasum -a 256 2>/dev/null || echo "anonymous-fallback ") | cut -c1-36)"
54
+ _CV_RUN_ID="$(head -c 16 /dev/urandom 2>/dev/null | od -An -tx1 | tr -d ' \n' | cut -c1-32)"
55
+ [ -z "$_CV_RUN_ID" ] && _CV_RUN_ID="fallback-$(date +%s)-$$"
56
+ _CV_AGENT="gemini"
57
+ _CV_SOURCE="${CODEVIBE_TELEMETRY_SOURCE:-production}"
58
+ _CV_STARTED_AT="$(date +%s)"
59
+ _CV_EXITED="" # set by terminal events; suppresses trap double-fire
60
+ _CV_PLUGIN_VERSION="$(node -p "require('$PLUGIN_DIR/package.json').version" 2>/dev/null || echo unknown)"
61
+ _CV_MCP_LOG="${CODEVIBE_TMPDIR}/codevibe-gemini-mcp.log"
62
+ _CV_MCP_LOG_BASELINE=0
63
+ if [ -f "$_CV_MCP_LOG" ]; then
64
+ _CV_MCP_LOG_BASELINE=$(wc -l < "$_CV_MCP_LOG" 2>/dev/null | tr -d ' ')
65
+ [ -z "$_CV_MCP_LOG_BASELINE" ] && _CV_MCP_LOG_BASELINE=0
66
+ fi
67
+ _CV_TMUX_STARTED="false"
68
+ _CV_AGENT_INVOKED="false"
69
+ _CV_AGENT_STARTED_AT=0
70
+ _CV_GEMINI_EXIT_FILE="${CODEVIBE_TMPDIR}/codevibe-gemini-exit-$$"
71
+
72
+ # Strip an arbitrary string down to a JSON-safe identifier alphabet.
73
+ # Removes anything that could break the hand-built JSON payload below
74
+ # (quotes, backslashes, ANSI escapes, control bytes, tabs, newlines).
75
+ # Truncates to 40 chars to bound the impact of pathological CLI version
76
+ # output. Caller is responsible for emptiness check after sanitize.
77
+ cv_sanitize() {
78
+ printf '%s' "$1" | LC_ALL=C tr -cd 'A-Za-z0-9._\- ' | cut -c1-40
79
+ }
80
+
81
+ # Sanitize trusted-but-still-string values that go into the payload
82
+ # (plugin version, source label) so future schema additions can't
83
+ # accidentally reintroduce a JSON-injection path.
84
+ _CV_PLUGIN_VERSION="$(cv_sanitize "$_CV_PLUGIN_VERSION")"
85
+ [ -z "$_CV_PLUGIN_VERSION" ] && _CV_PLUGIN_VERSION="unknown"
86
+ _CV_SOURCE="$(cv_sanitize "$_CV_SOURCE")"
87
+ [ -z "$_CV_SOURCE" ] && _CV_SOURCE="production"
88
+
89
+ cv_telem() {
90
+ local event="$1"; shift
91
+ local params="$*"
92
+ curl -s -X POST \
93
+ "https://www.google-analytics.com/mp/collect?measurement_id=${_CV_MID}&api_secret=${_CV_SEC}" \
94
+ -H "Content-Type: application/json" \
95
+ -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}}}]}" \
96
+ </dev/null >/dev/null 2>&1 &
97
+ }
98
+
99
+ cv_failed() {
100
+ [ -n "$_CV_EXITED" ] && return 0
101
+ _CV_EXITED="failed"
102
+ cv_telem "wrapper_failed" "\"reason\":\"$1\",\"lifetime_seconds\":$(( $(date +%s) - _CV_STARTED_AT ))"
103
+ }
104
+
46
105
  # Handle auth commands (login, logout, status, reset-device)
47
106
  # Delegate to codevibe-core CLI (shared auth across all plugins)
48
107
  case "$1" in
@@ -53,14 +112,42 @@ case "$1" in
53
112
  CORE_CLI="$PLUGIN_DIR/../codevibe-core/bin/codevibe.js"
54
113
  fi
55
114
  if [ -f "$CORE_CLI" ]; then
115
+ cv_telem "wrapper_started" "\"invocation\":\"auth_$1\",\"os\":\"$(uname -s | cv_sanitize)\",\"arch\":\"$(uname -m | cv_sanitize)\""
56
116
  exec node "$CORE_CLI" "$1"
57
117
  else
58
118
  echo "Error: codevibe-core not found. Try reinstalling: npm install -g @quantiya/codevibe"
119
+ cv_failed "core_not_found"
120
+ sleep 1
59
121
  exit 1
60
122
  fi
61
123
  ;;
62
124
  esac
63
125
 
126
+ # Capture environment facts for the session-flow wrapper_started event.
127
+ # Each probe is non-fatal — if a CLI is missing we record "missing" rather
128
+ # than aborting; pre-flight checks below still gate execution. Every
129
+ # string that lands in the JSON payload goes through cv_sanitize so an
130
+ # agent CLI emitting ANSI escapes or quotes in `--version` can't break
131
+ # the hand-built payload.
132
+ _CV_GEMINI_VER="missing"
133
+ command -v gemini >/dev/null 2>&1 && _CV_GEMINI_VER="$(gemini --version 2>/dev/null | cv_sanitize)"
134
+ [ -z "$_CV_GEMINI_VER" ] && _CV_GEMINI_VER="unknown"
135
+ _CV_NODE_VER="missing"
136
+ command -v node >/dev/null 2>&1 && _CV_NODE_VER="$(node -v 2>/dev/null | cv_sanitize)"
137
+ [ -z "$_CV_NODE_VER" ] && _CV_NODE_VER="unknown"
138
+ _CV_TMUX_VER="missing"
139
+ command -v tmux >/dev/null 2>&1 && _CV_TMUX_VER="$(tmux -V 2>/dev/null | cv_sanitize)"
140
+ [ -z "$_CV_TMUX_VER" ] && _CV_TMUX_VER="unknown"
141
+ _CV_OS_VER="$(uname -s | cv_sanitize)"
142
+ [ -z "$_CV_OS_VER" ] && _CV_OS_VER="unknown"
143
+ _CV_ARCH_VER="$(uname -m | cv_sanitize)"
144
+ [ -z "$_CV_ARCH_VER" ] && _CV_ARCH_VER="unknown"
145
+ _CV_GEMINI_AUTH="false"; [ -f "$HOME/.gemini/oauth_creds.json" ] && _CV_GEMINI_AUTH="true"
146
+ _CV_GEMINI_SETTINGS="false"; [ -f "$HOME/.gemini/settings.json" ] && _CV_GEMINI_SETTINGS="true"
147
+ _CV_INSIDE_TMUX="false"; [ -n "$TMUX" ] && _CV_INSIDE_TMUX="true"
148
+ _CV_IS_TTY="false"; { [ -t 0 ] && [ -t 1 ]; } && _CV_IS_TTY="true"
149
+ cv_telem "wrapper_started" "\"invocation\":\"session\",\"os\":\"$_CV_OS_VER\",\"arch\":\"$_CV_ARCH_VER\",\"gemini_version\":\"$_CV_GEMINI_VER\",\"node_version\":\"$_CV_NODE_VER\",\"tmux_version\":\"$_CV_TMUX_VER\",\"gemini_auth_present\":$_CV_GEMINI_AUTH,\"gemini_settings_present\":$_CV_GEMINI_SETTINGS,\"inside_tmux\":$_CV_INSIDE_TMUX,\"is_terminal\":$_CV_IS_TTY"
150
+
64
151
  # Export hooks directory for hook scripts to use
65
152
  export CODEVIBE_HOOKS_DIR="$PLUGIN_DIR/hooks"
66
153
 
@@ -130,7 +217,52 @@ log() {
130
217
 
131
218
  # Cleanup function to kill MCP server when wrapper exits
132
219
  cleanup() {
220
+ local wrapper_exit_code=$?
133
221
  log "Cleanup triggered"
222
+
223
+ # Fire wrapper_exited telemetry BEFORE killing the server so the MCP
224
+ # log is intact when we grep for SessionStart. cv_failed sets
225
+ # _CV_EXITED on pre-flight failures so this block won't double-fire.
226
+ if [ -z "$_CV_EXITED" ]; then
227
+ _CV_EXITED="exited"
228
+ local gemini_exit="unknown"
229
+ if [ -f "$_CV_GEMINI_EXIT_FILE" ]; then
230
+ gemini_exit="$(cat "$_CV_GEMINI_EXIT_FILE" 2>/dev/null | head -c 10 | tr -d '\n\r ')"
231
+ [ -z "$gemini_exit" ] && gemini_exit="unknown"
232
+ fi
233
+ local lifetime=$(( $(date +%s) - _CV_STARTED_AT ))
234
+ local gemini_lifetime=0
235
+ if [ "$_CV_AGENT_STARTED_AT" -gt 0 ] 2>/dev/null; then
236
+ gemini_lifetime=$(( $(date +%s) - _CV_AGENT_STARTED_AT ))
237
+ fi
238
+ local hook_fired="false"
239
+ if [ -f "$_CV_MCP_LOG" ]; then
240
+ if tail -n "+$((_CV_MCP_LOG_BASELINE + 1))" "$_CV_MCP_LOG" 2>/dev/null \
241
+ | grep -q "SessionStart" 2>/dev/null; then
242
+ hook_fired="true"
243
+ fi
244
+ fi
245
+ # Outcome priority: SIGINT/SIGTERM beats everything (user intent).
246
+ # Then "we never got far enough to invoke gemini" — distinct from
247
+ # "we invoked gemini via passthrough but never started a tmux of
248
+ # our own" (the latter is a normal direct-run, not an abort).
249
+ local outcome
250
+ if [ "$wrapper_exit_code" = "130" ] || [ "$wrapper_exit_code" = "143" ]; then
251
+ outcome="interrupted"
252
+ elif [ "$_CV_AGENT_INVOKED" = "false" ]; then
253
+ outcome="pre_invoke_abort"
254
+ elif [ "$gemini_exit" != "unknown" ] && [ "$gemini_exit" != "0" ]; then
255
+ outcome="error_exit"
256
+ elif [ "$gemini_lifetime" -lt 5 ] 2>/dev/null; then
257
+ outcome="early_exit"
258
+ elif [ "$gemini_lifetime" -lt 60 ] 2>/dev/null; then
259
+ outcome="clean_short"
260
+ else
261
+ outcome="clean_long"
262
+ fi
263
+ cv_telem "wrapper_exited" "\"exit_code\":$wrapper_exit_code,\"lifetime_seconds\":$lifetime,\"gemini_exit_code\":\"$gemini_exit\",\"gemini_lifetime_seconds\":$gemini_lifetime,\"tmux_session_started\":$_CV_TMUX_STARTED,\"agent_invoked\":$_CV_AGENT_INVOKED,\"session_start_hook_fired\":$hook_fired,\"terminal_outcome\":\"$outcome\""
264
+ fi
265
+
134
266
  if [ -n "$MCP_PID" ] && kill -0 "$MCP_PID" 2>/dev/null; then
135
267
  log "Stopping MCP server (PID: $MCP_PID)"
136
268
  kill "$MCP_PID" 2>/dev/null || true
@@ -138,6 +270,7 @@ cleanup() {
138
270
  fi
139
271
  # Remove PID file
140
272
  rm -f "${CODEVIBE_TMPDIR}/codevibe-gemini-mcp-$$.pid"
273
+ rm -f "$_CV_GEMINI_EXIT_FILE"
141
274
  }
142
275
 
143
276
  # Set up trap for cleanup
@@ -147,6 +280,8 @@ trap cleanup EXIT INT TERM
147
280
  if ! command -v tmux &> /dev/null; then
148
281
  echo "Error: tmux is required but not installed."
149
282
  echo "Install with: brew install tmux"
283
+ cv_failed "tmux_missing"
284
+ sleep 1
150
285
  exit 1
151
286
  fi
152
287
 
@@ -154,18 +289,24 @@ fi
154
289
  if ! command -v gemini &> /dev/null; then
155
290
  echo "Error: gemini CLI is not installed."
156
291
  echo "Install from: https://github.com/google-gemini/gemini-cli"
292
+ cv_failed "gemini_missing"
293
+ sleep 1
157
294
  exit 1
158
295
  fi
159
296
 
160
297
  # Check if node is installed
161
298
  if ! command -v node &> /dev/null; then
162
299
  echo "Error: Node.js is required but not installed."
300
+ cv_failed "node_missing"
301
+ sleep 1
163
302
  exit 1
164
303
  fi
165
304
 
166
305
  # Check if MCP server is built
167
306
  if [ ! -f "$PLUGIN_DIR/dist/server.js" ]; then
168
307
  echo "Error: MCP server not built. Run 'npm run build' in the plugin directory first."
308
+ cv_failed "server_not_built"
309
+ sleep 1
169
310
  exit 1
170
311
  fi
171
312
 
@@ -177,17 +318,36 @@ log "Starting codevibe-gemini with session: $SESSION_NAME"
177
318
  log "Working directory: $WORKING_DIR"
178
319
  log "Arguments: $*"
179
320
 
180
- # Check if we're already inside tmux
321
+ # Check if we're already inside tmux.
322
+ # We deliberately do NOT `exec` here — running gemini as a child process
323
+ # lets the EXIT trap fire after it returns so wrapper_exited still gets
324
+ # emitted on these direct-run paths. Behaviorally identical for the user
325
+ # (gemini remains the foreground process for the duration).
181
326
  if [ -n "$TMUX" ]; then
182
327
  log "Already inside tmux, running gemini directly"
183
- # Already in tmux, just run gemini
184
- exec gemini "$@"
328
+ _CV_AGENT_INVOKED="true"
329
+ _CV_AGENT_STARTED_AT="$(date +%s)"
330
+ # `|| _CV_RC=$?` is load-bearing: with `set -e`, a non-zero exit
331
+ # from gemini would abort the wrapper before we capture the exit
332
+ # code, leaving wrapper_exited with gemini_exit_code="unknown". The
333
+ # `||` form catches non-zero without triggering set -e, while exit
334
+ # 0 leaves _CV_RC at its 0 default. printf's `|| true` keeps a
335
+ # disk-full failure from clobbering diagnostics.
336
+ _CV_RC=0
337
+ gemini "$@" || _CV_RC=$?
338
+ printf '%s' "$_CV_RC" > "$_CV_GEMINI_EXIT_FILE" 2>/dev/null || true
339
+ exit "$_CV_RC"
185
340
  fi
186
341
 
187
- # Check if running in a terminal
342
+ # Check if running in a terminal — same direct-run treatment as above.
188
343
  if [ ! -t 0 ] || [ ! -t 1 ]; then
189
344
  log "Not running in a terminal, running gemini directly"
190
- exec gemini "$@"
345
+ _CV_AGENT_INVOKED="true"
346
+ _CV_AGENT_STARTED_AT="$(date +%s)"
347
+ _CV_RC=0
348
+ gemini "$@" || _CV_RC=$?
349
+ printf '%s' "$_CV_RC" > "$_CV_GEMINI_EXIT_FILE" 2>/dev/null || true
350
+ exit "$_CV_RC"
191
351
  fi
192
352
 
193
353
  # Start MCP server in background BEFORE launching Gemini
@@ -214,6 +374,8 @@ if ! kill -0 "$MCP_PID" 2>/dev/null; then
214
374
  tail -3 "$MCP_LOG_FILE" 2>/dev/null | grep -v '^\[' | head -1
215
375
  echo ""
216
376
  echo "Server failed to start. Check $MCP_LOG_FILE for details."
377
+ cv_failed "server_died_on_startup"
378
+ sleep 1
217
379
  exit 1
218
380
  fi
219
381
 
@@ -232,10 +394,16 @@ done
232
394
  # We use a wrapper that:
233
395
  # 1. Exports the session name so prompts can find it
234
396
  # 2. Runs gemini
235
- # 3. Exits the tmux session when gemini exits
397
+ # 3. Captures gemini's exit code to $_CV_GEMINI_EXIT_FILE for the
398
+ # wrapper's cleanup trap (tmux's own attach exit code is independent
399
+ # of the inner process exit, so the file is the only reliable signal)
400
+ # 4. Exits the tmux session when gemini exits
236
401
 
237
402
  tmux new-session -d -s "$SESSION_NAME" -x "$(tput cols)" -y "$(tput lines)" \
238
- "export CODEVIBE_GEMINI_TMUX_SESSION='$SESSION_NAME'; export ENVIRONMENT='$ENVIRONMENT'; $GEMINI_CMD; exit"
403
+ "export CODEVIBE_GEMINI_TMUX_SESSION='$SESSION_NAME'; export ENVIRONMENT='$ENVIRONMENT'; $GEMINI_CMD; printf '%s' \"\$?\" > '$_CV_GEMINI_EXIT_FILE'; exit"
404
+ _CV_TMUX_STARTED="true"
405
+ _CV_AGENT_INVOKED="true"
406
+ _CV_AGENT_STARTED_AT="$(date +%s)"
239
407
 
240
408
  # Enable mouse support for scrolling
241
409
  tmux set-option -t "$SESSION_NAME" -g mouse on
package/dist/server.js CHANGED
@@ -1,13 +1,13 @@
1
- "use strict";var V=Object.create;var E=Object.defineProperty;var q=Object.getOwnPropertyDescriptor;var W=Object.getOwnPropertyNames;var X=Object.getPrototypeOf,Y=Object.prototype.hasOwnProperty;var J=(m,e)=>{for(var t in e)E(m,t,{get:e[t],enumerable:!0})},A=(m,e,t,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of W(e))!Y.call(m,i)&&i!==t&&E(m,i,{get:()=>e[i],enumerable:!(s=q(e,i))||s.enumerable});return m};var P=(m,e,t)=>(t=m!=null?V(X(m)):{},A(e||!m||!m.__esModule?E(t,"default",{value:m,enumerable:!0}):t,m)),z=m=>A(E({},"__esModule",{value:!0}),m);var Z={};J(Z,{parseInteractivePromptInput:()=>K});module.exports=z(Z);var v=P(require("fs")),y=P(require("path")),S=P(require("os")),B=P(require("crypto"));var F=P(require("os")),N=P(require("path")),O=require("@quantiya/codevibe-core"),n=(0,O.createLogger)({name:"codevibe-gemini",logFile:N.default.join(F.default.tmpdir(),"codevibe-gemini-mcp.log"),level:"info"});var p=require("@quantiya/codevibe-core");var C=P(require("express")),I=P(require("fs")),_=P(require("path")),x=P(require("os")),k=require("@quantiya/codevibe-core");var d=require("@quantiya/codevibe-core"),w=[{number:"1",text:"Allow once"},{number:"2",text:"Allow for this session"},{number:"3",text:"Deny"}];var b=class{constructor(){this.assignedPort=0;this.app=(0,C.default)(),this.setupMiddleware(),this.setupRoutes()}setSessionId(e){this.sessionId=e}getPort(){return this.assignedPort}setupMiddleware(){this.app.use(C.default.json({limit:"1mb"})),this.app.use((e,t,s)=>{n.debug(`${e.method} ${e.path}`,{body:e.body,query:e.query}),s()}),this.app.use((e,t,s,i)=>{n.error("Express error:",e);let o={success:!1,error:e.message||"Internal server error"};s.status(500).json(o)})}setupRoutes(){this.app.get("/health",this.handleHealth.bind(this)),this.app.post("/event",this.handleEvent.bind(this)),this.app.post("/interactive-prompt",this.handleInteractivePrompt.bind(this)),this.app.get("/prompt-response/:promptId",this.handleGetPromptResponse.bind(this)),process.env.NODE_ENV!=="production"&&this.app.post("/test/execute",this.handleTestExecute.bind(this))}handleHealth(e,t){let s={success:!0,data:{status:"healthy",uptime:process.uptime(),version:"0.1.0",timestamp:new Date().toISOString()}};t.json(s)}async handleEvent(e,t){try{let s=e.body;if(!s.session_id){let r={success:!1,error:"Missing required field: session_id"};t.status(400).json(r);return}if(!s.hook_event_name){let r={success:!1,error:"Missing required field: hook_event_name"};t.status(400).json(r);return}let i=this.transformHookToEvent(s);n.info("Received event from hook",{sessionId:s.session_id,hookEvent:s.hook_event_name,type:i.type}),this.eventHandler?await this.eventHandler(i):n.warn("No event handler registered");let o={success:!0,message:"Event processed successfully"};t.json(o)}catch(s){n.error("Error handling event:",s);let i={success:!1,error:s instanceof Error?s.message:"Unknown error"};t.status(500).json(i)}}async handleTestExecute(e,t){try{let{sessionId:s,prompt:i}=e.body;if(!s||!i){let r={success:!1,error:"Missing required fields: sessionId, prompt"};t.status(400).json(r);return}n.info("Test execute request",{sessionId:s,prompt:i});let o={success:!0,message:"Test execution endpoint - not implemented yet",data:{sessionId:s,prompt:i}};t.json(o)}catch(s){n.error("Error in test execute:",s);let i={success:!1,error:s instanceof Error?s.message:"Unknown error"};t.status(500).json(i)}}async handleInteractivePrompt(e,t){try{let s=e.body;if(!s.sessionId){t.status(400).json({success:!1,error:"Missing required field: sessionId"});return}if(!s.toolName){t.status(400).json({success:!1,error:"Missing required field: toolName"});return}if(n.info("Received interactive prompt request",{sessionId:s.sessionId,toolName:s.toolName}),!this.interactivePromptHandler){t.status(503).json({success:!1,error:"Interactive prompt handler not registered"});return}let i=await this.interactivePromptHandler(s);t.json({success:!0,promptId:i.promptId,status:i.status})}catch(s){n.error("Error handling interactive prompt:",s),t.status(500).json({success:!1,error:s instanceof Error?s.message:"Unknown error"})}}handleGetPromptResponse(e,t){try{let{promptId:s}=e.params;if(!s){t.status(400).json({success:!1,error:"Missing required parameter: promptId"});return}if(!this.getPromptResponseHandler){t.status(503).json({success:!1,error:"Prompt response handler not registered"});return}let i=this.getPromptResponseHandler(s);if(!i){t.status(404).json({success:!1,error:"Prompt not found or expired"});return}t.json({success:!0,promptId:i.promptId,decision:i.decision,reason:i.reason})}catch(s){n.error("Error getting prompt response:",s),t.status(500).json({success:!1,error:s instanceof Error?s.message:"Unknown error"})}}transformHookToEvent(e){let t,s,i={cwd:e.cwd,hook_event_name:e.hook_event_name,...e.metadata||{}};if(e.type&&e.content!==void 0)t=e.type,s=e.content;else switch(e.hook_event_name){case"SessionStart":t=d.EventType.NOTIFICATION,s="Session started",i.source=e.source;break;case"SessionEnd":t=d.EventType.NOTIFICATION,s=`Session ended: ${e.reason||"unknown"}`,i.reason=e.reason;break;case"UserPromptSubmit":case"BeforeAgent":t=d.EventType.USER_PROMPT,s=e.prompt||"";break;case"AfterAgent":t=d.EventType.ASSISTANT_RESPONSE,s=e.content||e.prompt_response||"";break;case"PostToolUse":t=d.EventType.TOOL_USE,s=JSON.stringify({tool_name:e.tool_name,tool_input:e.tool_input,tool_response:e.tool_response}),i.tool_name=e.tool_name;break;case"Notification":if(e.notification_type==="ToolPermission"&&e.details){t=d.EventType.INTERACTIVE_PROMPT;let o=e.details.type||"edit",r=this.mapGeminiToolName(o),a={};if(o==="exec"||o==="shell"?a.command=e.details.command||"":(a.file_path=e.details.filePath||e.details.fileName,o==="edit"?e.details.fileDiff?(a.old_string=this.extractOldLinesFromDiff(e.details.fileDiff),a.new_string=this.extractNewLinesFromDiff(e.details.fileDiff)):(a.old_string=e.details.originalContent||"",a.new_string=e.details.newContent||""):o==="write"&&(a.content=e.details.newContent||"")),o==="exec"||o==="shell"){let c=e.details.rootCommand;s=c?`Allow execution of: '${c}'?`:e.details.title||`Command: ${a.command}`}else{let c=e.details.fileName||e.details.filePath?.split("/").pop()||"file";s=e.details.title||`Gemini wants to ${r.toLowerCase()} ${c}`}i.tool_name=r,i.tool_input=a,i.options=w,i.instructions="Select an option",i.notification_type=e.notification_type}else t=d.EventType.NOTIFICATION,s=e.message||"",i.notification_type=e.notification_type;break;default:t=d.EventType.NOTIFICATION,s=`Hook event: ${e.hook_event_name}`}return{session_id:e.session_id,hook_event_name:e.hook_event_name,type:t,source:d.EventSource.DESKTOP,content:s,metadata:i}}mapGeminiToolName(e){switch(e.toLowerCase()){case"exec":case"shell":return"Bash";case"edit":case"edit_file":return"Edit";case"write":case"write_file":return"Write";case"read":case"read_file":return"Read";default:return e}}extractOldLinesFromDiff(e){let t=e.split(`
1
+ "use strict";var V=Object.create;var E=Object.defineProperty;var q=Object.getOwnPropertyDescriptor;var W=Object.getOwnPropertyNames;var X=Object.getPrototypeOf,Y=Object.prototype.hasOwnProperty;var J=(m,e)=>{for(var t in e)E(m,t,{get:e[t],enumerable:!0})},A=(m,e,t,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of W(e))!Y.call(m,i)&&i!==t&&E(m,i,{get:()=>e[i],enumerable:!(s=q(e,i))||s.enumerable});return m};var y=(m,e,t)=>(t=m!=null?V(X(m)):{},A(e||!m||!m.__esModule?E(t,"default",{value:m,enumerable:!0}):t,m)),z=m=>A(E({},"__esModule",{value:!0}),m);var Z={};J(Z,{parseInteractivePromptInput:()=>K});module.exports=z(Z);var v=y(require("fs")),P=y(require("path")),S=y(require("os")),B=y(require("crypto"));var F=y(require("os")),O=y(require("path")),N=require("@quantiya/codevibe-core"),n=(0,N.createLogger)({name:"codevibe-gemini",logFile:O.default.join(F.default.tmpdir(),"codevibe-gemini-mcp.log"),level:"info"});var p=require("@quantiya/codevibe-core");var C=y(require("express")),I=y(require("fs")),_=y(require("path")),x=y(require("os")),k=require("@quantiya/codevibe-core");var d=require("@quantiya/codevibe-core"),w=[{number:"1",text:"Allow once"},{number:"2",text:"Allow for this session"},{number:"3",text:"Deny"}];var b=class{constructor(){this.assignedPort=0;this.app=(0,C.default)(),this.setupMiddleware(),this.setupRoutes()}setSessionId(e){this.sessionId=e}getPort(){return this.assignedPort}setupMiddleware(){this.app.use(C.default.json({limit:"1mb"})),this.app.use((e,t,s)=>{n.debug(`${e.method} ${e.path}`,{body:e.body,query:e.query}),s()}),this.app.use((e,t,s,i)=>{n.error("Express error:",e);let o={success:!1,error:e.message||"Internal server error"};s.status(500).json(o)})}setupRoutes(){this.app.get("/health",this.handleHealth.bind(this)),this.app.post("/event",this.handleEvent.bind(this)),this.app.post("/interactive-prompt",this.handleInteractivePrompt.bind(this)),this.app.get("/prompt-response/:promptId",this.handleGetPromptResponse.bind(this)),process.env.NODE_ENV!=="production"&&this.app.post("/test/execute",this.handleTestExecute.bind(this))}handleHealth(e,t){let s={success:!0,data:{status:"healthy",uptime:process.uptime(),version:"0.1.0",timestamp:new Date().toISOString()}};t.json(s)}async handleEvent(e,t){try{let s=e.body;if(!s.session_id){let r={success:!1,error:"Missing required field: session_id"};t.status(400).json(r);return}if(!s.hook_event_name){let r={success:!1,error:"Missing required field: hook_event_name"};t.status(400).json(r);return}let i=this.transformHookToEvent(s);n.info("Received event from hook",{sessionId:s.session_id,hookEvent:s.hook_event_name,type:i.type}),this.eventHandler?await this.eventHandler(i):n.warn("No event handler registered");let o={success:!0,message:"Event processed successfully"};t.json(o)}catch(s){n.error("Error handling event:",s);let i={success:!1,error:s instanceof Error?s.message:"Unknown error"};t.status(500).json(i)}}async handleTestExecute(e,t){try{let{sessionId:s,prompt:i}=e.body;if(!s||!i){let r={success:!1,error:"Missing required fields: sessionId, prompt"};t.status(400).json(r);return}n.info("Test execute request",{sessionId:s,prompt:i});let o={success:!0,message:"Test execution endpoint - not implemented yet",data:{sessionId:s,prompt:i}};t.json(o)}catch(s){n.error("Error in test execute:",s);let i={success:!1,error:s instanceof Error?s.message:"Unknown error"};t.status(500).json(i)}}async handleInteractivePrompt(e,t){try{let s=e.body;if(!s.sessionId){t.status(400).json({success:!1,error:"Missing required field: sessionId"});return}if(!s.toolName){t.status(400).json({success:!1,error:"Missing required field: toolName"});return}if(n.info("Received interactive prompt request",{sessionId:s.sessionId,toolName:s.toolName}),!this.interactivePromptHandler){t.status(503).json({success:!1,error:"Interactive prompt handler not registered"});return}let i=await this.interactivePromptHandler(s);t.json({success:!0,promptId:i.promptId,status:i.status})}catch(s){n.error("Error handling interactive prompt:",s),t.status(500).json({success:!1,error:s instanceof Error?s.message:"Unknown error"})}}handleGetPromptResponse(e,t){try{let{promptId:s}=e.params;if(!s){t.status(400).json({success:!1,error:"Missing required parameter: promptId"});return}if(!this.getPromptResponseHandler){t.status(503).json({success:!1,error:"Prompt response handler not registered"});return}let i=this.getPromptResponseHandler(s);if(!i){t.status(404).json({success:!1,error:"Prompt not found or expired"});return}t.json({success:!0,promptId:i.promptId,decision:i.decision,reason:i.reason})}catch(s){n.error("Error getting prompt response:",s),t.status(500).json({success:!1,error:s instanceof Error?s.message:"Unknown error"})}}transformHookToEvent(e){let t,s,i={cwd:e.cwd,hook_event_name:e.hook_event_name,...e.metadata||{}};if(e.type&&e.content!==void 0)t=e.type,s=e.content;else switch(e.hook_event_name){case"SessionStart":t=d.EventType.NOTIFICATION,s="Session started",i.source=e.source;break;case"SessionEnd":t=d.EventType.NOTIFICATION,s=`Session ended: ${e.reason||"unknown"}`,i.reason=e.reason;break;case"UserPromptSubmit":case"BeforeAgent":t=d.EventType.USER_PROMPT,s=e.prompt||"";break;case"AfterAgent":t=d.EventType.ASSISTANT_RESPONSE,s=e.content||e.prompt_response||"";break;case"PostToolUse":t=d.EventType.TOOL_USE,s=JSON.stringify({tool_name:e.tool_name,tool_input:e.tool_input,tool_response:e.tool_response}),i.tool_name=e.tool_name;break;case"Notification":if(e.notification_type==="ToolPermission"&&e.details){t=d.EventType.INTERACTIVE_PROMPT;let o=e.details.type||"edit",r=this.mapGeminiToolName(o),a={};if(o==="exec"||o==="shell"?a.command=e.details.command||"":(a.file_path=e.details.filePath||e.details.fileName,o==="edit"?e.details.fileDiff?(a.old_string=this.extractOldLinesFromDiff(e.details.fileDiff),a.new_string=this.extractNewLinesFromDiff(e.details.fileDiff)):(a.old_string=e.details.originalContent||"",a.new_string=e.details.newContent||""):o==="write"&&(a.content=e.details.newContent||"")),o==="exec"||o==="shell"){let c=e.details.rootCommand;s=c?`Allow execution of: '${c}'?`:e.details.title||`Command: ${a.command}`}else{let c=e.details.fileName||e.details.filePath?.split("/").pop()||"file";s=e.details.title||`Gemini wants to ${r.toLowerCase()} ${c}`}i.tool_name=r,i.tool_input=a,i.options=w,i.instructions="Select an option",i.notification_type=e.notification_type}else t=d.EventType.NOTIFICATION,s=e.message||"",i.notification_type=e.notification_type;break;default:t=d.EventType.NOTIFICATION,s=`Hook event: ${e.hook_event_name}`}return{session_id:e.session_id,hook_event_name:e.hook_event_name,type:t,source:d.EventSource.DESKTOP,content:s,metadata:i}}mapGeminiToolName(e){switch(e.toLowerCase()){case"exec":case"shell":return"Bash";case"edit":case"edit_file":return"Edit";case"write":case"write_file":return"Write";case"read":case"read_file":return"Read";default:return e}}extractOldLinesFromDiff(e){let t=e.split(`
2
2
  `),s=[];for(let i of t)i.startsWith("---")||i.startsWith("+++")||i.startsWith("Index:")||i.startsWith("===")||i.startsWith("@@")||(i.startsWith("-")?s.push(i.substring(1)):!i.startsWith("+")&&i.length>0&&(i.startsWith(" ")?s.push(i.substring(1)):s.push(i)));return s.join(`
3
3
  `)}extractNewLinesFromDiff(e){let t=e.split(`
4
4
  `),s=[];for(let i of t)i.startsWith("---")||i.startsWith("+++")||i.startsWith("Index:")||i.startsWith("===")||i.startsWith("@@")||(i.startsWith("+")?s.push(i.substring(1)):!i.startsWith("-")&&i.length>0&&(i.startsWith(" ")?s.push(i.substring(1)):s.push(i)));return s.join(`
5
- `)}onEvent(e){this.eventHandler=e}onInteractivePrompt(e){this.interactivePromptHandler=e}onGetPromptResponse(e){this.getPromptResponseHandler=e}async start(e){let t=e||this.sessionId;return t&&(this.sessionId=t),new Promise((s,i)=>{try{let o=(0,k.getConfig)(),r=o.server.dynamicPort?0:o.server.port;this.server=this.app.listen(r,o.server.host,()=>{let a=this.server.address();this.assignedPort=a.port,n.info(`HTTP API listening on http://${o.server.host}:${this.assignedPort}`),this.sessionId&&this.writePortFile(this.sessionId,this.assignedPort),s(this.assignedPort)}),this.server.on("error",a=>{n.error("HTTP server error:",a),i(a)})}catch(o){i(o)}})}writePortFile(e,t){let s=_.join(x.tmpdir(),`codevibe-gemini-${e}.port`);try{I.writeFileSync(s,t.toString()),n.info(`Port file written: ${s} -> ${t}`)}catch(i){n.error(`Failed to write port file: ${s}`,i)}}removePortFile(){if(this.sessionId){let e=_.join(x.tmpdir(),`codevibe-gemini-${this.sessionId}.port`);try{I.existsSync(e)&&(I.unlinkSync(e),n.info(`Port file removed: ${e}`))}catch(t){n.warn(`Failed to remove port file: ${e}`,t)}}}async stop(){return new Promise((e,t)=>{this.removePortFile(),this.server?this.server.close(s=>{s?(n.error("Error stopping HTTP server:",s),t(s)):(n.info("HTTP API stopped"),e())}):e()})}};var D=require("child_process"),$=require("@quantiya/codevibe-core");var T=class{async executePrompt(e,t){let s=(0,$.getConfig)(),i=s.gemini.defaultTimeout;return n.info("Executing prompt from mobile",{sessionId:e,promptLength:t.length,timeout:i}),new Promise(o=>{let r=["--resume",e,"--print","--output-format","stream-json",t];n.debug("Spawning Gemini command",{command:s.gemini.command,args:r});let a=(0,D.spawn)(s.gemini.command,r,{stdio:["pipe","pipe","pipe"],shell:!0}),c="",l="",f=!1,h=setTimeout(()=>{f=!0,n.warn("Command execution timed out",{sessionId:e,timeout:i}),a.kill("SIGTERM")},i);a.stdout?.on("data",g=>{let u=g.toString();c+=u,n.debug("Command stdout",{output:u.slice(0,200)})}),a.stderr?.on("data",g=>{let u=g.toString();l+=u,n.debug("Command stderr",{output:u.slice(0,200)})}),a.on("close",g=>{clearTimeout(h);let u={success:g===0&&!f,output:c,error:l,exitCode:g||void 0,timedOut:f};u.success?n.info("Command executed successfully",{sessionId:e,exitCode:g,outputLength:c.length}):n.error("Command execution failed",{sessionId:e,exitCode:g,timedOut:f,error:l.slice(0,500)}),o(u)}),a.on("error",g=>{clearTimeout(h),n.error("Failed to spawn command",{error:g.message}),o({success:!1,error:g.message,timedOut:!1})})})}detectInteractivePrompt(e){return[/\[Y\/n\]/i,/\[y\/N\]/i,/\(y\/n\)/i,/Continue\?/i,/Proceed\?/i].some(s=>s.test(e))}extractPromptText(e){let t=e.split(`
6
- `);for(let s=t.length-1;s>=0;s--){let i=t[s].trim();if(this.detectInteractivePrompt(i))return i}return null}};var L=require("child_process"),U=require("util");var j=(0,U.promisify)(L.exec),R=class{async answerInteractivePrompt(e,t){n.info("Attempting to answer interactive prompt",{sessionId:e,response:t});try{let s=process.env.CODEVIBE_GEMINI_TMUX_SESSION;return n.info("Checking tmux session environment",{tmuxSession:s||"(not set)",allEnvKeys:Object.keys(process.env).filter(i=>i.includes("CODEVIBE")||i.includes("TMUX"))}),s?(n.info("Using tmux send-keys",{tmuxSession:s}),await this.sendViaTmux(s,t),n.info("Successfully sent response to interactive prompt",{sessionId:e,response:t}),!0):(n.error("No tmux session found - codevibe-gemini wrapper is required",{sessionId:e,hint:"Start Gemini CLI using the codevibe-gemini wrapper script"}),!1)}catch(s){return n.error("Failed to answer interactive prompt",{sessionId:e,error:s instanceof Error?s.message:String(s)}),!1}}async sendViaTmux(e,t){let s=t.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\$/g,"\\$").replace(/`/g,"\\`");n.info("Sending via tmux",{sessionName:e,inputLength:t.length});try{let i=`tmux send-keys -t "${e}" -l "${s}"`,o=await j(i);n.info("tmux send-keys (text) completed",{stdout:o.stdout||"(empty)",stderr:o.stderr||"(empty)"}),await this.delay(500);let r=`tmux send-keys -t "${e}" Enter`,a=await j(r);n.info("tmux send-keys (Enter) completed",{stdout:a.stdout||"(empty)",stderr:a.stderr||"(empty)"})}catch(i){throw n.error("tmux send-keys failed",{sessionName:e,error:i}),i}}delay(e){return new Promise(t=>setTimeout(t,e))}isPromptResponse(e){let t=e.trim().toLowerCase();return!!(t==="y"||t==="n"||t==="yes"||t==="no"||/^[0-9]+$/.test(t)||/^[a-z]$/.test(t)||["exit","quit","q","continue","skip","abort","retry","cancel"].includes(t))}};var M=class m{constructor(e,t){this.activeSessions=new Map;this.assignedPort=0;this.pendingMobilePrompts=new Map;this.geminiToBackendSessionId=new Map;this.sessionSetupPromises=new Map;this.pendingInteractivePrompts=new Map;this.sessionKey=null;this.currentBackendSessionId=null;this.httpApi=new b,this.commandExecutor=new T,this.promptResponder=new R,this.initialSessionId=e,this.workingDirectory=t||process.cwd()}static{this.MOBILE_PROMPT_EXPIRY_MS=1e4}static{this.INTERACTIVE_PROMPT_TIMEOUT_MS=3e5}getPort(){return this.assignedPort}generateBackendSessionId(e){return`gemini-${B.createHash("sha256").update(e).digest("hex").substring(0,16)}`}getBackendSessionId(e){let t=this.geminiToBackendSessionId.get(e);return t||(t=this.generateBackendSessionId(e),this.geminiToBackendSessionId.set(e,t),n.info("Generated backend session ID",{geminiSessionId:e,backendSessionId:t})),t}trackMobilePrompt(e,t){this.pendingMobilePrompts.has(e)||this.pendingMobilePrompts.set(e,[]),this.pendingMobilePrompts.get(e).push({prompt:t.trim(),timestamp:Date.now()}),n.debug("Tracking mobile prompt for deduplication",{sessionId:e,promptLength:t.length})}isRecentMobilePrompt(e,t){let s=this.pendingMobilePrompts.get(e);if(!s)return!1;let i=Date.now(),o=t.trim(),r=[],a=!1;for(let c of s)if(!(i-c.timestamp>m.MOBILE_PROMPT_EXPIRY_MS)){if(!a&&c.prompt===o){a=!0,n.debug("Found matching mobile prompt, filtering duplicate",{sessionId:e});continue}r.push(c)}return r.length>0?this.pendingMobilePrompts.set(e,r):this.pendingMobilePrompts.delete(e),a}writePortFile(e){let t=y.join(S.tmpdir(),`codevibe-gemini-${e}.port`);try{v.writeFileSync(t,this.assignedPort.toString()),n.info(`Port file written: ${t} -> ${this.assignedPort}`)}catch(s){n.error(`Failed to write port file: ${t}`,s)}}removePortFile(e){let t=y.join(S.tmpdir(),`codevibe-gemini-${e}.port`);try{v.existsSync(t)&&(v.unlinkSync(t),n.info(`Port file removed: ${t}`))}catch(s){n.warn(`Failed to remove port file: ${t}`,s)}}async start(){try{n.info("Starting Gemini Companion MCP Server...",{environment:(0,p.getEnvironment)()}),this.appSyncClient=new p.AppSyncClient,await this.appSyncClient.authenticateWithStoredTokens()?(n.info("Authenticated with stored OAuth tokens",{userId:this.appSyncClient.getCurrentUserId(),email:this.appSyncClient.getCurrentUserEmail()}),await(0,p.registerDeviceEncryptionKey)(this.appSyncClient,n),(0,p.startDeviceKeyWatcher)(this.appSyncClient,n)):(n.error('Authentication failed. Run "codevibe-gemini login" first.'),console.error('Not authenticated. Run "codevibe-gemini login" to sign in.'),process.exit(1)),this.httpApi.onEvent(this.handleEventFromHook.bind(this)),this.httpApi.onInteractivePrompt(this.handleInteractivePromptRequest.bind(this)),this.httpApi.onGetPromptResponse(this.getInteractivePromptResponse.bind(this)),this.assignedPort=await this.httpApi.start(this.initialSessionId),n.info("MCP Server started successfully",{port:this.assignedPort,host:(0,p.getConfig)().server.host,dynamicPort:(0,p.getConfig)().server.dynamicPort,sessionId:this.initialSessionId,authenticated:this.appSyncClient.isAuthenticated(),userId:this.appSyncClient.getCurrentUserId()});let t=y.join(S.tmpdir(),"codevibe-gemini-default.port");v.writeFileSync(t,this.assignedPort.toString()),n.info(`Default port file written: ${t} -> ${this.assignedPort}`)}catch(e){throw n.error("Failed to start MCP Server:",e),e}}async createEncryptedEvent(e){let t=e.content,s=e.metadata,i=!1;this.sessionKey&&(t=p.cryptoService.encryptContent(e.content,this.sessionKey),s&&(s={encrypted:p.cryptoService.encryptMetadata(s,this.sessionKey)}),i=!0),await this.appSyncClient.createEvent({sessionId:e.sessionId,type:e.type,source:e.source,content:t,metadata:s,promptId:e.promptId,timestamp:e.timestamp,isEncrypted:i})}async stop(){n.info("Stopping MCP Server...");let e=Array.from(this.activeSessions.keys());n.info(`Marking ${e.length} active session(s) as INACTIVE...`);for(let s of e)try{await this.appSyncClient.updateSession({sessionId:s,status:p.SessionStatus.INACTIVE}),n.info("Session marked as INACTIVE during shutdown",{sessionId:s}),this.removePortFile(s)}catch(i){n.warn("Failed to mark session as INACTIVE during shutdown",{sessionId:s,error:i})}this.appSyncClient.cleanupSubscriptions(),this.activeSessions.clear(),this.currentBackendSessionId=null;let t=y.join(S.tmpdir(),"codevibe-gemini-default.port");try{v.unlinkSync(t)}catch{}await this.httpApi.stop(),n.info("MCP Server stopped")}async handleEventFromHook(e){let{session_id:t,hook_event_name:s,type:i}=e,{content:o}=e;n.info("Processing hook event",{sessionId:t,hookEvent:s,type:i});try{if(s==="SessionStart"){let a=this.handleSessionStart(e).finally(()=>{this.sessionSetupPromises.delete(t)});this.sessionSetupPromises.set(t,a),await a}else if(s==="SessionEnd")await this.handleSessionEnd(e);else{let a=this.sessionSetupPromises.get(t);a&&(n.debug("Waiting for in-flight session setup before processing event",{sessionId:t,hookEvent:s,type:i}),await a)}let r=this.geminiToBackendSessionId.get(t);if(!r)if(this.activeSessions.has(t))r=t;else{let a=this.generateBackendSessionId(t);this.activeSessions.has(a)?(r=a,this.geminiToBackendSessionId.set(t,a),n.info("Mapped unknown session ID to existing active session",{geminiSessionId:t,backendSessionId:a})):(n.info("Detected resumed session from hook, switching backend session",{geminiSessionId:t,newBackendSessionId:a,previousBackendSessionId:this.currentBackendSessionId}),await this.switchToResumedSession(t,a),r=a)}if(i===p.EventType.USER_PROMPT&&e.source===p.EventSource.DESKTOP&&(s==="UserPromptSubmit"||s==="BeforeAgent")&&o&&this.isRecentMobilePrompt(r,o)){n.info("Skipping duplicate USER_PROMPT from mobile-originated prompt",{sessionId:r,contentLength:o.length});return}if(i===p.EventType.INTERACTIVE_PROMPT&&s==="Notification"){let a=this.activeSessions.get(r);a&&(a.waitingForPromptResponse=!0,a.pendingPromptId=e.prompt_id),this.sendInteractivePromptAsync(r,e,o).catch(c=>{n.error("Failed to send interactive prompt with dynamic options",{error:c})});return}if(await this.createEncryptedEvent({sessionId:r,type:i,source:e.source||p.EventSource.DESKTOP,content:o,metadata:e.metadata,promptId:e.prompt_id}),i===p.EventType.INTERACTIVE_PROMPT){let a=this.activeSessions.get(r);a&&(a.waitingForPromptResponse=!0,a.pendingPromptId=e.prompt_id,e.metadata?.submitMap&&(a.pendingSubmitMap=e.metadata.submitMap),n.info("Interactive prompt detected - waiting for response",{sessionId:r,promptId:e.prompt_id}))}if(i===p.EventType.USER_PROMPT&&e.source===p.EventSource.DESKTOP){let a=this.activeSessions.get(r);a?.waitingForPromptResponse&&(a.waitingForPromptResponse=!1,a.pendingPromptId=void 0,n.info("Clearing prompt wait state - new desktop prompt received",{sessionId:r}))}n.info("Event sent to AppSync successfully",{sessionId:r,type:i,hookEvent:s})}catch(r){throw n.error("Failed to process hook event:",r),r}}async handleSessionStart(e){let t=e.session_id,s=this.getBackendSessionId(t),i=e.metadata?.cwd||process.cwd();n.info("Session started",{backendSessionId:s,geminiSessionId:t,cwd:i,source:e.metadata?.source});let o=Array.from(this.activeSessions.keys()).filter(c=>c!==s);if(o.length>0){n.info(`Marking ${o.length} previous session(s) as INACTIVE`);for(let c of o){try{await this.appSyncClient.updateSession({sessionId:c,status:p.SessionStatus.INACTIVE}),n.info("Previous session marked INACTIVE",{prevId:c,newSessionId:s})}catch(l){n.warn("Failed to mark previous session as INACTIVE",{prevId:c,error:l})}this.removePortFile(c),this.activeSessions.delete(c)}}this.writePortFile(s),this.writePortFile(t);let r=this.appSyncClient.getCurrentUserId(),a={sessionId:s,userId:r,projectPath:i,cwd:i,createdAt:new Date,subscriptionActive:!1,waitingForPromptResponse:!1,metadata:e.metadata||{}};this.activeSessions.set(s,a);try{let c=await(0,p.resumeOrCreateSession)({sessionId:s,userId:r,agentType:p.AgentType.GEMINI,projectPath:i,metadata:e.metadata||{}},this.appSyncClient,n);this.sessionKey=c.sessionKey}catch(c){if(n.error("Failed to create/resume session:",c),this.isSessionLimitExceeded(c)){this.displaySubscriptionLimitError(c,"session"),this.activeSessions.delete(s),this.removePortFile(s);return}n.warn("Session creation failed but continuing...",{error:c.message})}this.currentBackendSessionId=s,this.subscribeToMobileEvents(s),this.appSyncClient.startHeartbeat(s)}async switchToResumedSession(e,t){this.geminiToBackendSessionId.set(e,t);let s=this.appSyncClient.getCurrentUserId();if(this.currentBackendSessionId&&this.currentBackendSessionId!==t){try{await this.appSyncClient.updateSession({sessionId:this.currentBackendSessionId,status:p.SessionStatus.INACTIVE}),n.info("Previous session marked INACTIVE during resume switch",{previousSessionId:this.currentBackendSessionId,newSessionId:t})}catch(o){n.warn("Failed to mark previous session INACTIVE",{error:o})}this.appSyncClient.stopHeartbeat(this.currentBackendSessionId),this.removePortFile(this.currentBackendSessionId),this.activeSessions.delete(this.currentBackendSessionId)}try{let o=await(0,p.resumeOrCreateSession)({sessionId:t,userId:s,agentType:p.AgentType.GEMINI,projectPath:this.workingDirectory,metadata:{}},this.appSyncClient,n);this.sessionKey=o.sessionKey,n.info("Resumed session via switchToResumedSession",{backendSessionId:t,resumed:o.resumed,hasSessionKey:!!o.sessionKey})}catch(o){if(n.error("Failed to resume/create session during switch:",o),this.isSessionLimitExceeded(o)){this.displaySubscriptionLimitError(o,"session");return}}let i={sessionId:t,userId:s,projectPath:this.workingDirectory,cwd:this.workingDirectory,createdAt:new Date,subscriptionActive:!1,waitingForPromptResponse:!1,metadata:{}};this.activeSessions.set(t,i),this.writePortFile(t),this.writePortFile(e),this.currentBackendSessionId=t,this.subscribeToMobileEvents(t),this.appSyncClient.startHeartbeat(t)}async handleSessionEnd(e){let t=e.session_id,s=this.geminiToBackendSessionId.get(t);if(!s){let o=this.generateBackendSessionId(t);this.activeSessions.has(o)?s=o:s=t}n.info("Session ended",{sessionId:s,geminiSessionId:t,reason:e.metadata?.reason}),this.appSyncClient.stopHeartbeat(s),this.removePortFile(s),this.removePortFile(t);let i=this.activeSessions.get(s);if(i?.waitingForPromptResponse&&(n.info("Clearing prompt wait state - session ending",{sessionId:s}),i.waitingForPromptResponse=!1,i.pendingPromptId=void 0),i)try{await this.appSyncClient.updateSession({sessionId:s,status:p.SessionStatus.INACTIVE}),n.info("Session marked as INACTIVE in AppSync",{sessionId:s})}catch(o){n.warn("Failed to update session in AppSync:",o)}else n.warn("Cannot update session - session state not found",{sessionId:s});this.activeSessions.delete(s),n.debug("Session cleanup completed",{sessionId:s})}async handleInteractivePromptRequest(e){let t=`prompt-${Date.now()}-${Math.random().toString(36).substring(2,9)}`;n.info("Handling interactive prompt request",{promptId:t,sessionId:e.sessionId,toolName:e.toolName});let s=this.geminiToBackendSessionId.get(e.sessionId)||e.sessionId,i=this.mapGeminiToolName(e.toolName),o=this.buildToolDescription(i,e.toolInput),r=`Gemini wants to use ${i}:
7
- ${o}`,a={tool_name:i,tool_input:e.toolInput,options:w,instructions:"Select an option"};try{await this.createEncryptedEvent({sessionId:s,type:p.EventType.INTERACTIVE_PROMPT,source:p.EventSource.DESKTOP,content:r,metadata:a,promptId:t}),n.info("Interactive prompt event sent to iOS",{promptId:t,backendSessionId:s,toolName:e.toolName});let c={promptId:t,sessionId:s,toolName:e.toolName,toolInput:e.toolInput,createdAt:new Date,resolve:()=>{},reject:()=>{}};this.pendingInteractivePrompts.set(t,c);let l=this.activeSessions.get(s);return l&&(l.waitingForPromptResponse=!0,l.pendingPromptId=t),c.timeoutId=setTimeout(()=>{if(this.pendingInteractivePrompts.get(t)){n.warn("Interactive prompt timed out",{promptId:t}),this.pendingInteractivePrompts.delete(t);let h=this.activeSessions.get(s);h?.pendingPromptId===t&&(h.waitingForPromptResponse=!1,h.pendingPromptId=void 0)}},m.INTERACTIVE_PROMPT_TIMEOUT_MS),{promptId:t,status:"pending"}}catch(c){throw n.error("Failed to send interactive prompt event:",c),c}}async sendInteractivePromptAsync(e,t,s){await new Promise(r=>setTimeout(r,500));let i=process.env.CODEVIBE_GEMINI_TMUX_SESSION;if(i)try{let{exec:r}=await import("child_process"),a=f=>new Promise((h,g)=>{r(f,{timeout:5e3},(u,H,G)=>{u?g(u):h({stdout:H,stderr:G})})}),{stdout:c}=await a(`tmux capture-pane -p -e -S -30 -t '${i}'`),l=(0,p.parseInteractivePrompt)(c);l&&l.options.length>0?(t.metadata=t.metadata||{},t.metadata.options=l.options,t.metadata.submitMap=l.submitMap,n.info("Parsed dynamic options from tmux",{optionCount:l.options.length,kind:l.kind,options:l.options})):n.debug("No dynamic options parsed from tmux, using defaults from hook data")}catch(r){n.warn("Failed to capture tmux pane for options",{error:r})}await this.createEncryptedEvent({sessionId:e,type:p.EventType.INTERACTIVE_PROMPT,source:t.source||p.EventSource.DESKTOP,content:s,metadata:t.metadata,promptId:t.prompt_id});let o=this.activeSessions.get(e);o&&t.metadata?.submitMap&&(o.pendingSubmitMap=t.metadata.submitMap),n.info("Interactive prompt sent to AppSync with dynamic options",{sessionId:e,promptId:t.prompt_id})}mapGeminiToolName(e){switch(e.toLowerCase()){case"exec":case"shell":return"Bash";case"edit":case"edit_file":return"Edit";case"write":case"write_file":return"Write";case"read":case"read_file":return"Read";default:return e}}buildToolDescription(e,t){switch(e.toLowerCase()){case"edit":case"write":return t.file_path?`File: ${t.file_path}`:JSON.stringify(t,null,2);case"shell":case"bash":return t.command?`Command: ${t.command}`:JSON.stringify(t,null,2);case"read":return t.file_path?`Reading: ${t.file_path}`:JSON.stringify(t,null,2);default:return JSON.stringify(t,null,2)}}getInteractivePromptResponse(e){let t=this.pendingInteractivePrompts.get(e);if(!t)return n.debug("No pending prompt found",{promptId:e}),null;if(this.activeSessions.get(t.sessionId)?.waitingForPromptResponse)return{promptId:e,decision:"pending"};let i=this.pendingInteractivePrompts.get(e);if(i){let o=i.decision||"ask",r=i.reason;return i.timeoutId&&clearTimeout(i.timeoutId),this.pendingInteractivePrompts.delete(e),n.info("Returning interactive prompt decision",{promptId:e,decision:o,reason:r}),{promptId:e,decision:o,reason:r}}return null}resolveInteractivePrompt(e,t,s){let i=this.pendingInteractivePrompts.get(e);if(!i){n.warn("Cannot resolve - prompt not found",{promptId:e});return}n.info("Resolving interactive prompt",{promptId:e,decision:t,reason:s}),i.decision=t,i.reason=s;let o=this.activeSessions.get(i.sessionId);o&&(o.waitingForPromptResponse=!1,o.pendingPromptId=void 0)}subscribeToMobileEvents(e){n.info("Subscribing to mobile events",{sessionId:e});let t=this.activeSessions.get(e);if(!t){n.error("Session not found",{sessionId:e});return}this.appSyncClient.subscribeToEvents(e,async s=>{n.info("Received mobile event",{eventId:s.eventId,type:s.type,sessionId:s.sessionId,isEncrypted:s.isEncrypted});let i=s.content||"";if(s.isEncrypted&&this.sessionKey)try{i=p.cryptoService.decryptContent(s.content,this.sessionKey),n.debug("Event decrypted successfully",{eventId:s.eventId})}catch(r){n.error("Failed to decrypt event:",{eventId:s.eventId,error:r}),i=s.content}let o={...s,content:i};try{await this.appSyncClient.updateEventStatus({eventId:s.eventId,sessionId:s.sessionId,timestamp:s.timestamp,deliveryStatus:p.DeliveryStatus.DELIVERED}),n.info("Event marked as DELIVERED",{eventId:s.eventId})}catch(r){n.warn("Failed to mark event as DELIVERED",{eventId:s.eventId,error:r})}if(s.type===p.EventType.USER_PROMPT){let r=this.activeSessions.get(e);if(r?.waitingForPromptResponse){let a=i.trim(),c=this.parseInteractivePromptInput(a);if(n.info("Parsed interactive prompt input",{sessionId:e,content:a,parsed:c}),c.action==="select_option"){let l=r.pendingSubmitMap,f=l?.[c.option]||c.option;if(n.info("User selected option",{option:c.option,terminalInput:f,hasSubmitMap:!!l}),r.pendingPromptId){let g=c.option==="1"||c.option==="2"?"allow":"deny",u=c.option==="2"?"allowed_for_session":void 0;this.resolveInteractivePrompt(r.pendingPromptId,g,u)}await this.promptResponder.answerInteractivePrompt(e,f)?(await this.markEventExecuted(s),r.waitingForPromptResponse=!1,r.pendingPromptId=void 0,delete r.pendingSubmitMap,await this.createEncryptedEvent({sessionId:e,type:p.EventType.NOTIFICATION,source:p.EventSource.DESKTOP,content:`Selected option ${c.option}`,metadata:{promptAnswered:!0}})):await this.sendPromptError(e,"Failed to select option")}else if(c.action==="reject_and_prompt"){n.info("User rejecting prompt and sending new prompt",{newPrompt:c.newPrompt}),r.pendingPromptId&&this.resolveInteractivePrompt(r.pendingPromptId,"deny");let l=await this.promptResponder.answerInteractivePrompt(e,"3");if(r.waitingForPromptResponse=!1,r.pendingPromptId=void 0,delete r.pendingSubmitMap,l){if(await this.createEncryptedEvent({sessionId:e,type:p.EventType.NOTIFICATION,source:p.EventSource.DESKTOP,content:"Interactive prompt rejected",metadata:{promptRejected:!0}}),c.newPrompt){await new Promise(h=>setTimeout(h,1e3));let f={...s,content:c.newPrompt};await this.executeMobilePrompt(e,f)}await this.markEventExecuted(s)}else await this.sendPromptError(e,"Failed to reject prompt")}else n.info("Sending as free-form response to interactive prompt",{response:a}),await this.promptResponder.answerInteractivePrompt(e,a)?(await this.markEventExecuted(s),r.waitingForPromptResponse=!1,r.pendingPromptId=void 0,await this.createEncryptedEvent({sessionId:e,type:p.EventType.NOTIFICATION,source:p.EventSource.DESKTOP,content:`Response "${a}" sent to interactive prompt`,metadata:{promptAnswered:!0}})):await this.sendPromptError(e,"Failed to send response")}else await this.executeMobilePrompt(e,o)}},s=>{n.error("Subscription error",{sessionId:e,error:s})}),t.subscriptionActive=!0,n.info("Subscription active",{sessionId:e})}parseInteractivePromptInput(e){return K(e)}async markEventExecuted(e){try{await this.appSyncClient.updateEventStatus({eventId:e.eventId,sessionId:e.sessionId,timestamp:e.timestamp,deliveryStatus:p.DeliveryStatus.EXECUTED}),n.info("Event marked as EXECUTED",{eventId:e.eventId})}catch(t){n.warn("Failed to mark event as EXECUTED",{eventId:e.eventId,error:t})}}async sendPromptError(e,t){await this.createEncryptedEvent({sessionId:e,type:p.EventType.NOTIFICATION,source:p.EventSource.DESKTOP,content:t,metadata:{error:!0}})}isSessionLimitExceeded(e){return this.getErrorMessage(e).includes("SESSION_LIMIT_EXCEEDED")}isUsageLimitExceeded(e){let t=this.getErrorMessage(e);return t.includes("MESSAGE_LIMIT_EXCEEDED")||t.includes("IMAGE_LIMIT_EXCEEDED")}getErrorMessage(e){if(e instanceof Error)return e.message;if(typeof e=="object"&&e!==null){let t=e;if(t.errors&&Array.isArray(t.errors))return t.errors.map(s=>s.message||"").join(" ");if(typeof t.message=="string")return t.message}return String(e)}displaySubscriptionLimitError(e,t){let s=this.getErrorMessage(e),i="",o=s.match(/for your (\w+) plan/i);o&&(i=` (${o[1]} tier)`);let r="",a=s.match(/of (\d+)/);switch(a&&(r=` [Limit: ${a[1]}]`),console.log(`
5
+ `)}onEvent(e){this.eventHandler=e}onInteractivePrompt(e){this.interactivePromptHandler=e}onGetPromptResponse(e){this.getPromptResponseHandler=e}async start(e){let t=e||this.sessionId;return t&&(this.sessionId=t),new Promise((s,i)=>{try{let o=(0,k.getConfig)(),r=o.server.dynamicPort?0:o.server.port;this.server=this.app.listen(r,o.server.host,()=>{let a=this.server.address();this.assignedPort=a.port,n.info(`HTTP API listening on http://${o.server.host}:${this.assignedPort}`),this.sessionId&&this.writePortFile(this.sessionId,this.assignedPort),s(this.assignedPort)}),this.server.on("error",a=>{n.error("HTTP server error:",a),i(a)})}catch(o){i(o)}})}writePortFile(e,t){let s=_.join(x.tmpdir(),`codevibe-gemini-${e}.port`);try{I.writeFileSync(s,t.toString()),n.info(`Port file written: ${s} -> ${t}`)}catch(i){n.error(`Failed to write port file: ${s}`,i)}}removePortFile(){if(this.sessionId){let e=_.join(x.tmpdir(),`codevibe-gemini-${this.sessionId}.port`);try{I.existsSync(e)&&(I.unlinkSync(e),n.info(`Port file removed: ${e}`))}catch(t){n.warn(`Failed to remove port file: ${e}`,t)}}}async stop(){return new Promise((e,t)=>{this.removePortFile(),this.server?this.server.close(s=>{s?(n.error("Error stopping HTTP server:",s),t(s)):(n.info("HTTP API stopped"),e())}):e()})}};var D=require("child_process"),$=require("@quantiya/codevibe-core");var T=class{async executePrompt(e,t){let s=(0,$.getConfig)(),i=s.gemini.defaultTimeout;return n.info("Executing prompt from mobile",{sessionId:e,promptLength:t.length,timeout:i}),new Promise(o=>{let r=["--resume",e,"--print","--output-format","stream-json",t];n.debug("Spawning Gemini command",{command:s.gemini.command,args:r});let a=(0,D.spawn)(s.gemini.command,r,{stdio:["pipe","pipe","pipe"],shell:!0}),c="",l="",h=!1,f=setTimeout(()=>{h=!0,n.warn("Command execution timed out",{sessionId:e,timeout:i}),a.kill("SIGTERM")},i);a.stdout?.on("data",g=>{let u=g.toString();c+=u,n.debug("Command stdout",{output:u.slice(0,200)})}),a.stderr?.on("data",g=>{let u=g.toString();l+=u,n.debug("Command stderr",{output:u.slice(0,200)})}),a.on("close",g=>{clearTimeout(f);let u={success:g===0&&!h,output:c,error:l,exitCode:g||void 0,timedOut:h};u.success?n.info("Command executed successfully",{sessionId:e,exitCode:g,outputLength:c.length}):n.error("Command execution failed",{sessionId:e,exitCode:g,timedOut:h,error:l.slice(0,500)}),o(u)}),a.on("error",g=>{clearTimeout(f),n.error("Failed to spawn command",{error:g.message}),o({success:!1,error:g.message,timedOut:!1})})})}detectInteractivePrompt(e){return[/\[Y\/n\]/i,/\[y\/N\]/i,/\(y\/n\)/i,/Continue\?/i,/Proceed\?/i].some(s=>s.test(e))}extractPromptText(e){let t=e.split(`
6
+ `);for(let s=t.length-1;s>=0;s--){let i=t[s].trim();if(this.detectInteractivePrompt(i))return i}return null}};var L=require("child_process"),U=require("util");var j=(0,U.promisify)(L.exec),R=class{async answerInteractivePrompt(e,t){n.info("Attempting to answer interactive prompt",{sessionId:e,response:t});try{let s=process.env.CODEVIBE_GEMINI_TMUX_SESSION;return n.info("Checking tmux session environment",{tmuxSession:s||"(not set)",allEnvKeys:Object.keys(process.env).filter(i=>i.includes("CODEVIBE")||i.includes("TMUX"))}),s?(n.info("Using tmux send-keys",{tmuxSession:s}),await this.sendViaTmux(s,t),n.info("Successfully sent response to interactive prompt",{sessionId:e,response:t}),!0):(n.error("No tmux session found - codevibe-gemini wrapper is required",{sessionId:e,hint:"Start Gemini CLI using the codevibe-gemini wrapper script"}),!1)}catch(s){return n.error("Failed to answer interactive prompt",{sessionId:e,error:s instanceof Error?s.message:String(s)}),!1}}async sendViaTmux(e,t){let s=t.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\$/g,"\\$").replace(/`/g,"\\`");n.info("Sending via tmux",{sessionName:e,inputLength:t.length});try{let i=`tmux send-keys -t "${e}" -l "${s}"`,o=await j(i);n.info("tmux send-keys (text) completed",{stdout:o.stdout||"(empty)",stderr:o.stderr||"(empty)"}),await this.delay(500);let r=`tmux send-keys -t "${e}" Enter`,a=await j(r);n.info("tmux send-keys (Enter) completed",{stdout:a.stdout||"(empty)",stderr:a.stderr||"(empty)"})}catch(i){throw n.error("tmux send-keys failed",{sessionName:e,error:i}),i}}delay(e){return new Promise(t=>setTimeout(t,e))}isPromptResponse(e){let t=e.trim().toLowerCase();return!!(t==="y"||t==="n"||t==="yes"||t==="no"||/^[0-9]+$/.test(t)||/^[a-z]$/.test(t)||["exit","quit","q","continue","skip","abort","retry","cancel"].includes(t))}};var M=class m{constructor(e,t){this.activeSessions=new Map;this.assignedPort=0;this.pendingMobilePrompts=new Map;this.geminiToBackendSessionId=new Map;this.sessionSetupPromises=new Map;this.pendingInteractivePrompts=new Map;this.sessionKey=null;this.currentBackendSessionId=null;this.httpApi=new b,this.commandExecutor=new T,this.promptResponder=new R,this.initialSessionId=e,this.workingDirectory=t||process.cwd()}static{this.MOBILE_PROMPT_EXPIRY_MS=1e4}static{this.INTERACTIVE_PROMPT_TIMEOUT_MS=3e5}getPort(){return this.assignedPort}generateBackendSessionId(e){return`gemini-${B.createHash("sha256").update(e).digest("hex").substring(0,16)}`}getBackendSessionId(e){let t=this.geminiToBackendSessionId.get(e);return t||(t=this.generateBackendSessionId(e),this.geminiToBackendSessionId.set(e,t),n.info("Generated backend session ID",{geminiSessionId:e,backendSessionId:t})),t}trackMobilePrompt(e,t){this.pendingMobilePrompts.has(e)||this.pendingMobilePrompts.set(e,[]),this.pendingMobilePrompts.get(e).push({prompt:t.trim(),timestamp:Date.now()}),n.debug("Tracking mobile prompt for deduplication",{sessionId:e,promptLength:t.length})}isRecentMobilePrompt(e,t){let s=this.pendingMobilePrompts.get(e);if(!s)return!1;let i=Date.now(),o=t.trim(),r=[],a=!1;for(let c of s)if(!(i-c.timestamp>m.MOBILE_PROMPT_EXPIRY_MS)){if(!a&&c.prompt===o){a=!0,n.debug("Found matching mobile prompt, filtering duplicate",{sessionId:e});continue}r.push(c)}return r.length>0?this.pendingMobilePrompts.set(e,r):this.pendingMobilePrompts.delete(e),a}writePortFile(e){let t=P.join(S.tmpdir(),`codevibe-gemini-${e}.port`);try{v.writeFileSync(t,this.assignedPort.toString()),n.info(`Port file written: ${t} -> ${this.assignedPort}`)}catch(s){n.error(`Failed to write port file: ${t}`,s)}}removePortFile(e){let t=P.join(S.tmpdir(),`codevibe-gemini-${e}.port`);try{v.existsSync(t)&&(v.unlinkSync(t),n.info(`Port file removed: ${t}`))}catch(s){n.warn(`Failed to remove port file: ${t}`,s)}}async start(){try{if(n.info("Starting Gemini Companion MCP Server...",{environment:(0,p.getEnvironment)()}),this.appSyncClient=new p.AppSyncClient,await this.appSyncClient.authenticateWithStoredTokens()){n.info("Authenticated with stored OAuth tokens",{userId:this.appSyncClient.getCurrentUserId(),email:this.appSyncClient.getCurrentUserEmail()}),await(0,p.registerDeviceEncryptionKey)(this.appSyncClient,n),(0,p.startDeviceKeyWatcher)(this.appSyncClient,n);try{let s=await this.appSyncClient.sweepOrphanSessions({agentType:"GEMINI"});s>0&&n.info("Orphan sweep: marked stale Gemini sessions INACTIVE",{swept:s})}catch(s){n.warn("Orphan sweep failed, continuing startup",{error:s instanceof Error?s.message:String(s)})}}else n.error('Authentication failed. Run "codevibe-gemini login" first.'),console.error('Not authenticated. Run "codevibe-gemini login" to sign in.'),process.exit(1);this.httpApi.onEvent(this.handleEventFromHook.bind(this)),this.httpApi.onInteractivePrompt(this.handleInteractivePromptRequest.bind(this)),this.httpApi.onGetPromptResponse(this.getInteractivePromptResponse.bind(this)),this.assignedPort=await this.httpApi.start(this.initialSessionId),n.info("MCP Server started successfully",{port:this.assignedPort,host:(0,p.getConfig)().server.host,dynamicPort:(0,p.getConfig)().server.dynamicPort,sessionId:this.initialSessionId,authenticated:this.appSyncClient.isAuthenticated(),userId:this.appSyncClient.getCurrentUserId()});let t=P.join(S.tmpdir(),"codevibe-gemini-default.port");v.writeFileSync(t,this.assignedPort.toString()),n.info(`Default port file written: ${t} -> ${this.assignedPort}`)}catch(e){throw n.error("Failed to start MCP Server:",e),e}}async createEncryptedEvent(e){let t=e.content,s=e.metadata,i=!1;this.sessionKey&&(t=p.cryptoService.encryptContent(e.content,this.sessionKey),s&&(s={encrypted:p.cryptoService.encryptMetadata(s,this.sessionKey)}),i=!0),await this.appSyncClient.createEvent({sessionId:e.sessionId,type:e.type,source:e.source,content:t,metadata:s,promptId:e.promptId,timestamp:e.timestamp,isEncrypted:i})}async stop(){n.info("Stopping MCP Server...");let e=Array.from(this.activeSessions.keys());n.info(`Marking ${e.length} active session(s) as INACTIVE...`);for(let s of e)try{await this.appSyncClient.updateSession({sessionId:s,status:p.SessionStatus.INACTIVE}),n.info("Session marked as INACTIVE during shutdown",{sessionId:s}),this.removePortFile(s)}catch(i){n.warn("Failed to mark session as INACTIVE during shutdown",{sessionId:s,error:i})}this.appSyncClient.cleanupSubscriptions(),this.activeSessions.clear(),this.currentBackendSessionId=null;let t=P.join(S.tmpdir(),"codevibe-gemini-default.port");try{v.unlinkSync(t)}catch{}await this.httpApi.stop(),n.info("MCP Server stopped")}async handleEventFromHook(e){let{session_id:t,hook_event_name:s,type:i}=e,{content:o}=e;n.info("Processing hook event",{sessionId:t,hookEvent:s,type:i});try{if(s==="SessionStart"){let a=this.handleSessionStart(e).finally(()=>{this.sessionSetupPromises.delete(t)});this.sessionSetupPromises.set(t,a),await a}else if(s==="SessionEnd")await this.handleSessionEnd(e);else{let a=this.sessionSetupPromises.get(t);a&&(n.debug("Waiting for in-flight session setup before processing event",{sessionId:t,hookEvent:s,type:i}),await a)}let r=this.geminiToBackendSessionId.get(t);if(!r)if(this.activeSessions.has(t))r=t;else{let a=this.generateBackendSessionId(t);this.activeSessions.has(a)?(r=a,this.geminiToBackendSessionId.set(t,a),n.info("Mapped unknown session ID to existing active session",{geminiSessionId:t,backendSessionId:a})):(n.info("Detected resumed session from hook, switching backend session",{geminiSessionId:t,newBackendSessionId:a,previousBackendSessionId:this.currentBackendSessionId}),await this.switchToResumedSession(t,a),r=a)}if(i===p.EventType.USER_PROMPT&&e.source===p.EventSource.DESKTOP&&(s==="UserPromptSubmit"||s==="BeforeAgent")&&o&&this.isRecentMobilePrompt(r,o)){n.info("Skipping duplicate USER_PROMPT from mobile-originated prompt",{sessionId:r,contentLength:o.length});return}if(i===p.EventType.INTERACTIVE_PROMPT&&s==="Notification"){let a=this.activeSessions.get(r);a&&(a.waitingForPromptResponse=!0,a.pendingPromptId=e.prompt_id),this.sendInteractivePromptAsync(r,e,o).catch(c=>{n.error("Failed to send interactive prompt with dynamic options",{error:c})});return}if(await this.createEncryptedEvent({sessionId:r,type:i,source:e.source||p.EventSource.DESKTOP,content:o,metadata:e.metadata,promptId:e.prompt_id}),i===p.EventType.INTERACTIVE_PROMPT){let a=this.activeSessions.get(r);a&&(a.waitingForPromptResponse=!0,a.pendingPromptId=e.prompt_id,e.metadata?.submitMap&&(a.pendingSubmitMap=e.metadata.submitMap),n.info("Interactive prompt detected - waiting for response",{sessionId:r,promptId:e.prompt_id}))}if(i===p.EventType.USER_PROMPT&&e.source===p.EventSource.DESKTOP){let a=this.activeSessions.get(r);a?.waitingForPromptResponse&&(a.waitingForPromptResponse=!1,a.pendingPromptId=void 0,n.info("Clearing prompt wait state - new desktop prompt received",{sessionId:r}))}n.info("Event sent to AppSync successfully",{sessionId:r,type:i,hookEvent:s})}catch(r){throw n.error("Failed to process hook event:",r),r}}async handleSessionStart(e){let t=e.session_id,s=this.getBackendSessionId(t),i=e.metadata?.cwd||process.cwd();n.info("Session started",{backendSessionId:s,geminiSessionId:t,cwd:i,source:e.metadata?.source});let o=Array.from(this.activeSessions.keys()).filter(c=>c!==s);if(o.length>0){n.info(`Marking ${o.length} previous session(s) as INACTIVE`);for(let c of o){try{await this.appSyncClient.updateSession({sessionId:c,status:p.SessionStatus.INACTIVE}),n.info("Previous session marked INACTIVE",{prevId:c,newSessionId:s})}catch(l){n.warn("Failed to mark previous session as INACTIVE",{prevId:c,error:l})}this.removePortFile(c),this.activeSessions.delete(c)}}this.writePortFile(s),this.writePortFile(t);let r=this.appSyncClient.getCurrentUserId(),a={sessionId:s,userId:r,projectPath:i,cwd:i,createdAt:new Date,subscriptionActive:!1,waitingForPromptResponse:!1,metadata:e.metadata||{}};this.activeSessions.set(s,a);try{let c=await(0,p.resumeOrCreateSession)({sessionId:s,userId:r,agentType:p.AgentType.GEMINI,projectPath:i,metadata:e.metadata||{}},this.appSyncClient,n);this.sessionKey=c.sessionKey}catch(c){if(n.error("Failed to create/resume session:",c),this.isSessionLimitExceeded(c)){this.displaySubscriptionLimitError(c,"session"),this.activeSessions.delete(s),this.removePortFile(s);return}n.warn("Session creation failed but continuing...",{error:c.message})}this.currentBackendSessionId=s,this.subscribeToMobileEvents(s),this.appSyncClient.startHeartbeat(s)}async switchToResumedSession(e,t){this.geminiToBackendSessionId.set(e,t);let s=this.appSyncClient.getCurrentUserId();if(this.currentBackendSessionId&&this.currentBackendSessionId!==t){try{await this.appSyncClient.updateSession({sessionId:this.currentBackendSessionId,status:p.SessionStatus.INACTIVE}),n.info("Previous session marked INACTIVE during resume switch",{previousSessionId:this.currentBackendSessionId,newSessionId:t})}catch(o){n.warn("Failed to mark previous session INACTIVE",{error:o})}this.appSyncClient.stopHeartbeat(this.currentBackendSessionId),this.removePortFile(this.currentBackendSessionId),this.activeSessions.delete(this.currentBackendSessionId)}try{let o=await(0,p.resumeOrCreateSession)({sessionId:t,userId:s,agentType:p.AgentType.GEMINI,projectPath:this.workingDirectory,metadata:{}},this.appSyncClient,n);this.sessionKey=o.sessionKey,n.info("Resumed session via switchToResumedSession",{backendSessionId:t,resumed:o.resumed,hasSessionKey:!!o.sessionKey})}catch(o){if(n.error("Failed to resume/create session during switch:",o),this.isSessionLimitExceeded(o)){this.displaySubscriptionLimitError(o,"session");return}}let i={sessionId:t,userId:s,projectPath:this.workingDirectory,cwd:this.workingDirectory,createdAt:new Date,subscriptionActive:!1,waitingForPromptResponse:!1,metadata:{}};this.activeSessions.set(t,i),this.writePortFile(t),this.writePortFile(e),this.currentBackendSessionId=t,this.subscribeToMobileEvents(t),this.appSyncClient.startHeartbeat(t)}async handleSessionEnd(e){let t=e.session_id,s=this.geminiToBackendSessionId.get(t);if(!s){let o=this.generateBackendSessionId(t);this.activeSessions.has(o)?s=o:s=t}n.info("Session ended",{sessionId:s,geminiSessionId:t,reason:e.metadata?.reason}),this.appSyncClient.stopHeartbeat(s),this.removePortFile(s),this.removePortFile(t);let i=this.activeSessions.get(s);if(i?.waitingForPromptResponse&&(n.info("Clearing prompt wait state - session ending",{sessionId:s}),i.waitingForPromptResponse=!1,i.pendingPromptId=void 0),i)try{await this.appSyncClient.updateSession({sessionId:s,status:p.SessionStatus.INACTIVE}),n.info("Session marked as INACTIVE in AppSync",{sessionId:s})}catch(o){n.warn("Failed to update session in AppSync:",o)}else n.warn("Cannot update session - session state not found",{sessionId:s});this.activeSessions.delete(s),n.debug("Session cleanup completed",{sessionId:s})}async handleInteractivePromptRequest(e){let t=`prompt-${Date.now()}-${Math.random().toString(36).substring(2,9)}`;n.info("Handling interactive prompt request",{promptId:t,sessionId:e.sessionId,toolName:e.toolName});let s=this.geminiToBackendSessionId.get(e.sessionId)||e.sessionId,i=this.mapGeminiToolName(e.toolName),o=this.buildToolDescription(i,e.toolInput),r=`Gemini wants to use ${i}:
7
+ ${o}`,a={tool_name:i,tool_input:e.toolInput,options:w,instructions:"Select an option"};try{await this.createEncryptedEvent({sessionId:s,type:p.EventType.INTERACTIVE_PROMPT,source:p.EventSource.DESKTOP,content:r,metadata:a,promptId:t}),n.info("Interactive prompt event sent to iOS",{promptId:t,backendSessionId:s,toolName:e.toolName});let c={promptId:t,sessionId:s,toolName:e.toolName,toolInput:e.toolInput,createdAt:new Date,resolve:()=>{},reject:()=>{}};this.pendingInteractivePrompts.set(t,c);let l=this.activeSessions.get(s);return l&&(l.waitingForPromptResponse=!0,l.pendingPromptId=t),c.timeoutId=setTimeout(()=>{if(this.pendingInteractivePrompts.get(t)){n.warn("Interactive prompt timed out",{promptId:t}),this.pendingInteractivePrompts.delete(t);let f=this.activeSessions.get(s);f?.pendingPromptId===t&&(f.waitingForPromptResponse=!1,f.pendingPromptId=void 0)}},m.INTERACTIVE_PROMPT_TIMEOUT_MS),{promptId:t,status:"pending"}}catch(c){throw n.error("Failed to send interactive prompt event:",c),c}}async sendInteractivePromptAsync(e,t,s){await new Promise(r=>setTimeout(r,500));let i=process.env.CODEVIBE_GEMINI_TMUX_SESSION;if(i)try{let{exec:r}=await import("child_process"),a=h=>new Promise((f,g)=>{r(h,{timeout:5e3},(u,G,H)=>{u?g(u):f({stdout:G,stderr:H})})}),{stdout:c}=await a(`tmux capture-pane -p -e -S -30 -t '${i}'`),l=(0,p.parseInteractivePrompt)(c);l&&l.options.length>0?(t.metadata=t.metadata||{},t.metadata.options=l.options,t.metadata.submitMap=l.submitMap,n.info("Parsed dynamic options from tmux",{optionCount:l.options.length,kind:l.kind,options:l.options})):n.debug("No dynamic options parsed from tmux, using defaults from hook data")}catch(r){n.warn("Failed to capture tmux pane for options",{error:r})}await this.createEncryptedEvent({sessionId:e,type:p.EventType.INTERACTIVE_PROMPT,source:t.source||p.EventSource.DESKTOP,content:s,metadata:t.metadata,promptId:t.prompt_id});let o=this.activeSessions.get(e);o&&t.metadata?.submitMap&&(o.pendingSubmitMap=t.metadata.submitMap),n.info("Interactive prompt sent to AppSync with dynamic options",{sessionId:e,promptId:t.prompt_id})}mapGeminiToolName(e){switch(e.toLowerCase()){case"exec":case"shell":return"Bash";case"edit":case"edit_file":return"Edit";case"write":case"write_file":return"Write";case"read":case"read_file":return"Read";default:return e}}buildToolDescription(e,t){switch(e.toLowerCase()){case"edit":case"write":return t.file_path?`File: ${t.file_path}`:JSON.stringify(t,null,2);case"shell":case"bash":return t.command?`Command: ${t.command}`:JSON.stringify(t,null,2);case"read":return t.file_path?`Reading: ${t.file_path}`:JSON.stringify(t,null,2);default:return JSON.stringify(t,null,2)}}getInteractivePromptResponse(e){let t=this.pendingInteractivePrompts.get(e);if(!t)return n.debug("No pending prompt found",{promptId:e}),null;if(this.activeSessions.get(t.sessionId)?.waitingForPromptResponse)return{promptId:e,decision:"pending"};let i=this.pendingInteractivePrompts.get(e);if(i){let o=i.decision||"ask",r=i.reason;return i.timeoutId&&clearTimeout(i.timeoutId),this.pendingInteractivePrompts.delete(e),n.info("Returning interactive prompt decision",{promptId:e,decision:o,reason:r}),{promptId:e,decision:o,reason:r}}return null}resolveInteractivePrompt(e,t,s){let i=this.pendingInteractivePrompts.get(e);if(!i){n.warn("Cannot resolve - prompt not found",{promptId:e});return}n.info("Resolving interactive prompt",{promptId:e,decision:t,reason:s}),i.decision=t,i.reason=s;let o=this.activeSessions.get(i.sessionId);o&&(o.waitingForPromptResponse=!1,o.pendingPromptId=void 0)}subscribeToMobileEvents(e){n.info("Subscribing to mobile events",{sessionId:e});let t=this.activeSessions.get(e);if(!t){n.error("Session not found",{sessionId:e});return}this.appSyncClient.subscribeToEvents(e,async s=>{n.info("Received mobile event",{eventId:s.eventId,type:s.type,sessionId:s.sessionId,isEncrypted:s.isEncrypted});let i=s.content||"";if(s.isEncrypted&&this.sessionKey)try{i=p.cryptoService.decryptContent(s.content,this.sessionKey),n.debug("Event decrypted successfully",{eventId:s.eventId})}catch(r){n.error("Failed to decrypt event:",{eventId:s.eventId,error:r}),i=s.content}let o={...s,content:i};try{await this.appSyncClient.updateEventStatus({eventId:s.eventId,sessionId:s.sessionId,timestamp:s.timestamp,deliveryStatus:p.DeliveryStatus.DELIVERED}),n.info("Event marked as DELIVERED",{eventId:s.eventId})}catch(r){n.warn("Failed to mark event as DELIVERED",{eventId:s.eventId,error:r})}if(s.type===p.EventType.USER_PROMPT){let r=this.activeSessions.get(e);if(r?.waitingForPromptResponse){let a=i.trim(),c=this.parseInteractivePromptInput(a);if(n.info("Parsed interactive prompt input",{sessionId:e,content:a,parsed:c}),c.action==="select_option"){let l=r.pendingSubmitMap,h=l?.[c.option]||c.option;if(n.info("User selected option",{option:c.option,terminalInput:h,hasSubmitMap:!!l}),r.pendingPromptId){let g=c.option==="1"||c.option==="2"?"allow":"deny",u=c.option==="2"?"allowed_for_session":void 0;this.resolveInteractivePrompt(r.pendingPromptId,g,u)}await this.promptResponder.answerInteractivePrompt(e,h)?(await this.markEventExecuted(s),r.waitingForPromptResponse=!1,r.pendingPromptId=void 0,delete r.pendingSubmitMap,await this.createEncryptedEvent({sessionId:e,type:p.EventType.NOTIFICATION,source:p.EventSource.DESKTOP,content:`Selected option ${c.option}`,metadata:{promptAnswered:!0}})):await this.sendPromptError(e,"Failed to select option")}else if(c.action==="reject_and_prompt"){n.info("User rejecting prompt and sending new prompt",{newPrompt:c.newPrompt}),r.pendingPromptId&&this.resolveInteractivePrompt(r.pendingPromptId,"deny");let l=await this.promptResponder.answerInteractivePrompt(e,"3");if(r.waitingForPromptResponse=!1,r.pendingPromptId=void 0,delete r.pendingSubmitMap,l){if(await this.createEncryptedEvent({sessionId:e,type:p.EventType.NOTIFICATION,source:p.EventSource.DESKTOP,content:"Interactive prompt rejected",metadata:{promptRejected:!0}}),c.newPrompt){await new Promise(f=>setTimeout(f,1e3));let h={...s,content:c.newPrompt};await this.executeMobilePrompt(e,h)}await this.markEventExecuted(s)}else await this.sendPromptError(e,"Failed to reject prompt")}else n.info("Sending as free-form response to interactive prompt",{response:a}),await this.promptResponder.answerInteractivePrompt(e,a)?(await this.markEventExecuted(s),r.waitingForPromptResponse=!1,r.pendingPromptId=void 0,await this.createEncryptedEvent({sessionId:e,type:p.EventType.NOTIFICATION,source:p.EventSource.DESKTOP,content:`Response "${a}" sent to interactive prompt`,metadata:{promptAnswered:!0}})):await this.sendPromptError(e,"Failed to send response")}else await this.executeMobilePrompt(e,o)}},s=>{n.error("Subscription error",{sessionId:e,error:s})}),t.subscriptionActive=!0,n.info("Subscription active",{sessionId:e})}parseInteractivePromptInput(e){return K(e)}async markEventExecuted(e){try{await this.appSyncClient.updateEventStatus({eventId:e.eventId,sessionId:e.sessionId,timestamp:e.timestamp,deliveryStatus:p.DeliveryStatus.EXECUTED}),n.info("Event marked as EXECUTED",{eventId:e.eventId})}catch(t){n.warn("Failed to mark event as EXECUTED",{eventId:e.eventId,error:t})}}async sendPromptError(e,t){await this.createEncryptedEvent({sessionId:e,type:p.EventType.NOTIFICATION,source:p.EventSource.DESKTOP,content:t,metadata:{error:!0}})}isSessionLimitExceeded(e){return this.getErrorMessage(e).includes("SESSION_LIMIT_EXCEEDED")}isUsageLimitExceeded(e){let t=this.getErrorMessage(e);return t.includes("MESSAGE_LIMIT_EXCEEDED")||t.includes("IMAGE_LIMIT_EXCEEDED")}getErrorMessage(e){if(e instanceof Error)return e.message;if(typeof e=="object"&&e!==null){let t=e;if(t.errors&&Array.isArray(t.errors))return t.errors.map(s=>s.message||"").join(" ");if(typeof t.message=="string")return t.message}return String(e)}displaySubscriptionLimitError(e,t){let s=this.getErrorMessage(e),i="",o=s.match(/for your (\w+) plan/i);o&&(i=` (${o[1]} tier)`);let r="",a=s.match(/of (\d+)/);switch(a&&(r=` [Limit: ${a[1]}]`),console.log(`
8
8
  `+"=".repeat(60)),console.log("\u26A0\uFE0F SUBSCRIPTION LIMIT REACHED"),console.log("=".repeat(60)),t){case"session":console.log(`You have reached the maximum number of active sessions${i}.`),console.log(`${r}`),console.log(`
9
9
  To continue, please:`),console.log(" \u2022 Close an existing Gemini CLI session, or"),console.log(" \u2022 Upgrade your subscription in the CodeVibe iOS app");break;case"message":console.log(`You have reached your monthly message limit${i}.`),console.log(`${r}`),console.log(`
10
10
  To continue, please:`),console.log(" \u2022 Wait until your usage resets next month, or"),console.log(" \u2022 Upgrade your subscription in the CodeVibe iOS app");break;case"image":console.log(`You have reached your monthly image attachment limit${i}.`),console.log(`${r}`),console.log(`
11
11
  To continue, please:`),console.log(" \u2022 Wait until your usage resets next month, or"),console.log(" \u2022 Upgrade your subscription in the CodeVibe iOS app");break}console.log(`
12
12
  Note: You can still use Gemini CLI normally from your desktop.`),console.log("This limit only affects syncing with the mobile app."),console.log("=".repeat(60)+`
13
- `),n.error("Subscription limit exceeded",{limitType:t,errorMessage:s})}async downloadAttachment(e,t,s){try{let i=e.isEncrypted??s??!1;n.info("Downloading attachment",{id:e.id,type:e.type,filename:e.filename,s3Key:e.s3Key,attachmentIsEncrypted:e.isEncrypted,eventIsEncrypted:s,shouldDecrypt:i});let{downloadUrl:o}=await this.appSyncClient.getAttachmentDownloadUrl(e.s3Key),r=await fetch(o);if(!r.ok)throw new Error(`Failed to download attachment: ${r.status} ${r.statusText}`);let a=Buffer.from(await r.arrayBuffer());if(i&&this.sessionKey)try{n.info("Decrypting attachment",{id:e.id}),a=p.cryptoService.decryptData(a,this.sessionKey),n.info("Attachment decrypted successfully",{id:e.id,decryptedSize:a.length})}catch(u){throw n.error("Failed to decrypt attachment:",{id:e.id,error:u}),new Error("Failed to decrypt attachment")}else i&&!this.sessionKey&&n.warn("Cannot decrypt attachment - no session key available",{id:e.id});let c=y.join(this.workingDirectory,".codevibe-attachments");v.existsSync(c)||v.mkdirSync(c,{recursive:!0});let l="";if(e.filename){let u=y.extname(e.filename);u&&(l=u)}l||(l={"image/jpeg":".jpg","image/png":".png","image/gif":".gif","image/webp":".webp","image/heic":".heic","application/pdf":".pdf"}[e.type]||".bin");let f=`attachment-${e.id}${l}`,h=y.join(c,f);v.writeFileSync(h,a);let g=`./.codevibe-attachments/${f}`;return n.info("Attachment saved to working directory",{id:e.id,absolutePath:h,relativePath:g,size:a.length}),g}catch(i){return n.error("Failed to download attachment:",{id:e.id,error:i}),null}}async executeMobilePrompt(e,t){let s=t.content||"",i=t.attachments||[];n.info("Executing mobile prompt via tmux",{sessionId:e,promptLength:s.length,attachmentCount:i.length});let o=[];if(i.length>0){n.info("Downloading attachments for prompt",{count:i.length});for(let r of i){let a=await this.downloadAttachment(r,e,t.isEncrypted);a&&o.push(a)}if(o.length>0){let r=o.map(a=>`@${a}`).join(" ");s?s=`${r} ${s}`:s=`${r} Please analyze the attached file(s).`,n.info("Prompt updated with attachment paths",{attachmentCount:o.length,newPromptLength:s.length})}}this.trackMobilePrompt(e,s);try{if(await this.promptResponder.answerInteractivePrompt(e,s)){try{await this.appSyncClient.updateEventStatus({eventId:t.eventId,sessionId:t.sessionId,timestamp:t.timestamp,deliveryStatus:p.DeliveryStatus.EXECUTED}),n.info("Event marked as EXECUTED",{eventId:t.eventId})}catch(a){n.warn("Failed to mark event as EXECUTED",{eventId:t.eventId,error:a})}n.info("Mobile prompt sent successfully",{sessionId:e})}else n.error("Failed to send mobile prompt",{sessionId:e}),await this.createEncryptedEvent({sessionId:e,type:p.EventType.NOTIFICATION,source:p.EventSource.DESKTOP,content:"Failed to send prompt to Gemini CLI",metadata:{error:!0}})}catch(r){n.error("Failed to execute mobile prompt:",r)}}};async function Q(){let m=process.argv[2]||process.env.GEMINI_SESSION_ID,e=process.env.GEMINI_WORKING_DIRECTORY||process.cwd();m?n.info(`Starting MCP server for session: ${m}`):n.info("Starting MCP server without initial session ID (will be discovered from transcript)"),n.info(`Working directory: ${e}`);let t=new M(m,e);try{await t.start();let s=t.getPort();console.log(`PORT=${s}`);let i=!1,o=async r=>{if(i){n.info("Shutdown already in progress, ignoring additional signal");return}i=!0,n.info(`Received ${r} signal, stopping server...`);try{await t.stop(),n.info("Graceful shutdown completed"),process.exit(0)}catch(a){n.error("Error during shutdown:",a),process.exit(1)}};process.on("SIGINT",()=>o("SIGINT")),process.on("SIGTERM",()=>o("SIGTERM")),process.on("SIGHUP",()=>o("SIGHUP")),process.on("uncaughtException",async r=>{n.error("Uncaught exception:",r),await o("uncaughtException")}),process.on("unhandledRejection",async r=>{n.error("Unhandled rejection:",r),await o("unhandledRejection")})}catch(s){n.error("Failed to start MCP Server:",s),process.exit(1)}}function K(m){let e=m.trim();if(e==="1"||e==="2")return{action:"select_option",option:e};if(e==="3")return{action:"reject_and_prompt",newPrompt:void 0};let t=e.match(/^3[,.:;\-\s\n]+(.+)$/s);return t?{action:"reject_and_prompt",newPrompt:t[1].trim()}:{action:"send_as_response"}}Q().catch(m=>{n.error("Unhandled error in main:",m),process.exit(1)});0&&(module.exports={parseInteractivePromptInput});
13
+ `),n.error("Subscription limit exceeded",{limitType:t,errorMessage:s})}async downloadAttachment(e,t,s){try{let i=e.isEncrypted??s??!1;n.info("Downloading attachment",{id:e.id,type:e.type,filename:e.filename,s3Key:e.s3Key,attachmentIsEncrypted:e.isEncrypted,eventIsEncrypted:s,shouldDecrypt:i});let{downloadUrl:o}=await this.appSyncClient.getAttachmentDownloadUrl(e.s3Key),r=await fetch(o);if(!r.ok)throw new Error(`Failed to download attachment: ${r.status} ${r.statusText}`);let a=Buffer.from(await r.arrayBuffer());if(i&&this.sessionKey)try{n.info("Decrypting attachment",{id:e.id}),a=p.cryptoService.decryptData(a,this.sessionKey),n.info("Attachment decrypted successfully",{id:e.id,decryptedSize:a.length})}catch(u){throw n.error("Failed to decrypt attachment:",{id:e.id,error:u}),new Error("Failed to decrypt attachment")}else i&&!this.sessionKey&&n.warn("Cannot decrypt attachment - no session key available",{id:e.id});let c=P.join(this.workingDirectory,".codevibe-attachments");v.existsSync(c)||v.mkdirSync(c,{recursive:!0});let l="";if(e.filename){let u=P.extname(e.filename);u&&(l=u)}l||(l={"image/jpeg":".jpg","image/png":".png","image/gif":".gif","image/webp":".webp","image/heic":".heic","application/pdf":".pdf"}[e.type]||".bin");let h=`attachment-${e.id}${l}`,f=P.join(c,h);v.writeFileSync(f,a);let g=`./.codevibe-attachments/${h}`;return n.info("Attachment saved to working directory",{id:e.id,absolutePath:f,relativePath:g,size:a.length}),g}catch(i){return n.error("Failed to download attachment:",{id:e.id,error:i}),null}}async executeMobilePrompt(e,t){let s=t.content||"",i=t.attachments||[];n.info("Executing mobile prompt via tmux",{sessionId:e,promptLength:s.length,attachmentCount:i.length});let o=[];if(i.length>0){n.info("Downloading attachments for prompt",{count:i.length});for(let r of i){let a=await this.downloadAttachment(r,e,t.isEncrypted);a&&o.push(a)}if(o.length>0){let r=o.map(a=>`@${a}`).join(" ");s?s=`${r} ${s}`:s=`${r} Please analyze the attached file(s).`,n.info("Prompt updated with attachment paths",{attachmentCount:o.length,newPromptLength:s.length})}}this.trackMobilePrompt(e,s);try{if(await this.promptResponder.answerInteractivePrompt(e,s)){try{await this.appSyncClient.updateEventStatus({eventId:t.eventId,sessionId:t.sessionId,timestamp:t.timestamp,deliveryStatus:p.DeliveryStatus.EXECUTED}),n.info("Event marked as EXECUTED",{eventId:t.eventId})}catch(a){n.warn("Failed to mark event as EXECUTED",{eventId:t.eventId,error:a})}n.info("Mobile prompt sent successfully",{sessionId:e})}else n.error("Failed to send mobile prompt",{sessionId:e}),await this.createEncryptedEvent({sessionId:e,type:p.EventType.NOTIFICATION,source:p.EventSource.DESKTOP,content:"Failed to send prompt to Gemini CLI",metadata:{error:!0}})}catch(r){n.error("Failed to execute mobile prompt:",r)}}};async function Q(){let m=process.argv[2]||process.env.GEMINI_SESSION_ID,e=process.env.GEMINI_WORKING_DIRECTORY||process.cwd();m?n.info(`Starting MCP server for session: ${m}`):n.info("Starting MCP server without initial session ID (will be discovered from transcript)"),n.info(`Working directory: ${e}`);let t=new M(m,e);try{await t.start();let s=t.getPort();console.log(`PORT=${s}`);let i=!1,o=async r=>{if(i){n.info("Shutdown already in progress, ignoring additional signal");return}i=!0,n.info(`Received ${r} signal, stopping server...`);try{await t.stop(),n.info("Graceful shutdown completed"),process.exit(0)}catch(a){n.error("Error during shutdown:",a),process.exit(1)}};process.on("SIGINT",()=>o("SIGINT")),process.on("SIGTERM",()=>o("SIGTERM")),process.on("SIGHUP",()=>o("SIGHUP")),process.on("uncaughtException",async r=>{n.error("Uncaught exception:",r),await o("uncaughtException")}),process.on("unhandledRejection",async r=>{n.error("Unhandled rejection:",r),await o("unhandledRejection")})}catch(s){n.error("Failed to start MCP Server:",s),process.exit(1)}}function K(m){let e=m.trim();if(e==="1"||e==="2")return{action:"select_option",option:e};if(e==="3")return{action:"reject_and_prompt",newPrompt:void 0};let t=e.match(/^3[,.:;\-\s\n]+(.+)$/s);return t?{action:"reject_and_prompt",newPrompt:t[1].trim()}:{action:"send_as_response"}}Q().catch(m=>{n.error("Unhandled error in main:",m),process.exit(1)});0&&(module.exports={parseInteractivePromptInput});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quantiya/codevibe-gemini-plugin",
3
- "version": "1.0.18",
3
+ "version": "1.0.20",
4
4
  "description": "Control Gemini 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": {
@@ -49,7 +49,7 @@
49
49
  "node": ">=18.0.0"
50
50
  },
51
51
  "dependencies": {
52
- "@quantiya/codevibe-core": "^1.0.15",
52
+ "@quantiya/codevibe-core": "^1.0.16",
53
53
  "chokidar": "^5.0.0",
54
54
  "dotenv": "^16.6.1",
55
55
  "express": "^5.1.0",