@quantiya/codevibe-gemini-plugin 1.0.19 → 1.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/codevibe-gemini +199 -7
  2. package/package.json +1 -1
@@ -43,6 +43,89 @@ done
43
43
  SCRIPT_DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
44
44
  PLUGIN_DIR="$(dirname "$SCRIPT_DIR")"
45
45
 
46
+ # ─── PATH augmentation ───────────────────────────────────────────────
47
+ # When the install one-liner runs in a fresh terminal, Homebrew's
48
+ # installer writes the shellenv eval into ~/.zprofile (and similar)
49
+ # but the user's current shell hasn't sourced it yet. Subsequent
50
+ # codevibe-* runs in that same terminal then fail tmux discovery
51
+ # because /opt/homebrew/bin isn't on PATH. Prepend common locations
52
+ # so the wrapper recovers without forcing the user to open a new
53
+ # terminal. Prepend (not append) is deliberate: the Homebrew binary
54
+ # install.sh just laid down should win over any older system binary
55
+ # (e.g. a stale /usr/bin/node on Linux) at the same name. To preserve
56
+ # the relative ordering of augmented dirs, build a single prefix
57
+ # string and prepend it once — iterating prepend-per-dir would
58
+ # reverse intended order. ${PATH:+:$PATH} keeps an empty starting
59
+ # PATH from producing a trailing colon (which makes cwd searchable).
60
+ _CV_NEW_PATHS=""
61
+ for _CV_DIR in /opt/homebrew/bin /opt/homebrew/sbin /usr/local/bin /usr/local/sbin /opt/local/bin /usr/bin /bin; do
62
+ case ":$PATH:" in
63
+ *":$_CV_DIR:"*) ;;
64
+ *) [ -d "$_CV_DIR" ] && _CV_NEW_PATHS="$_CV_NEW_PATHS:$_CV_DIR" ;;
65
+ esac
66
+ done
67
+ [ -n "$_CV_NEW_PATHS" ] && export PATH="${_CV_NEW_PATHS#:}${PATH:+:$PATH}"
68
+ unset _CV_DIR _CV_NEW_PATHS
69
+
70
+ # ─── Wrapper telemetry (GA4 Measurement Protocol) ─────────────────────
71
+ # Diagnoses agent CLI failures: pre-flight bailouts, fast-die patterns,
72
+ # whether SessionStart hook fired, exit code. Background curl, fail
73
+ # silently, no PII (hashed hostname + per-run random id only). Honors
74
+ # CODEVIBE_TELEMETRY_SOURCE=test for internal testing.
75
+ _CV_MID="G-GS74YEQTB8"
76
+ _CV_SEC="lAfOF6OxRzSQ-NsLBRjhAg"
77
+ _CV_CID="$(echo "$(uname -n)-$(id -u)" | (sha256sum 2>/dev/null || shasum -a 256 2>/dev/null || echo "anonymous-fallback ") | cut -c1-36)"
78
+ _CV_RUN_ID="$(head -c 16 /dev/urandom 2>/dev/null | od -An -tx1 | tr -d ' \n' | cut -c1-32)"
79
+ [ -z "$_CV_RUN_ID" ] && _CV_RUN_ID="fallback-$(date +%s)-$$"
80
+ _CV_AGENT="gemini"
81
+ _CV_SOURCE="${CODEVIBE_TELEMETRY_SOURCE:-production}"
82
+ _CV_STARTED_AT="$(date +%s)"
83
+ _CV_EXITED="" # set by terminal events; suppresses trap double-fire
84
+ _CV_PLUGIN_VERSION="$(node -p "require('$PLUGIN_DIR/package.json').version" 2>/dev/null || echo unknown)"
85
+ _CV_MCP_LOG="${CODEVIBE_TMPDIR}/codevibe-gemini-mcp.log"
86
+ _CV_MCP_LOG_BASELINE=0
87
+ if [ -f "$_CV_MCP_LOG" ]; then
88
+ _CV_MCP_LOG_BASELINE=$(wc -l < "$_CV_MCP_LOG" 2>/dev/null | tr -d ' ')
89
+ [ -z "$_CV_MCP_LOG_BASELINE" ] && _CV_MCP_LOG_BASELINE=0
90
+ fi
91
+ _CV_TMUX_STARTED="false"
92
+ _CV_AGENT_INVOKED="false"
93
+ _CV_AGENT_STARTED_AT=0
94
+ _CV_GEMINI_EXIT_FILE="${CODEVIBE_TMPDIR}/codevibe-gemini-exit-$$"
95
+
96
+ # Strip an arbitrary string down to a JSON-safe identifier alphabet.
97
+ # Removes anything that could break the hand-built JSON payload below
98
+ # (quotes, backslashes, ANSI escapes, control bytes, tabs, newlines).
99
+ # Truncates to 40 chars to bound the impact of pathological CLI version
100
+ # output. Caller is responsible for emptiness check after sanitize.
101
+ cv_sanitize() {
102
+ printf '%s' "$1" | LC_ALL=C tr -cd 'A-Za-z0-9._\- ' | cut -c1-40
103
+ }
104
+
105
+ # Sanitize trusted-but-still-string values that go into the payload
106
+ # (plugin version, source label) so future schema additions can't
107
+ # accidentally reintroduce a JSON-injection path.
108
+ _CV_PLUGIN_VERSION="$(cv_sanitize "$_CV_PLUGIN_VERSION")"
109
+ [ -z "$_CV_PLUGIN_VERSION" ] && _CV_PLUGIN_VERSION="unknown"
110
+ _CV_SOURCE="$(cv_sanitize "$_CV_SOURCE")"
111
+ [ -z "$_CV_SOURCE" ] && _CV_SOURCE="production"
112
+
113
+ cv_telem() {
114
+ local event="$1"; shift
115
+ local params="$*"
116
+ curl -s -X POST \
117
+ "https://www.google-analytics.com/mp/collect?measurement_id=${_CV_MID}&api_secret=${_CV_SEC}" \
118
+ -H "Content-Type: application/json" \
119
+ -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}}}]}" \
120
+ </dev/null >/dev/null 2>&1 &
121
+ }
122
+
123
+ cv_failed() {
124
+ [ -n "$_CV_EXITED" ] && return 0
125
+ _CV_EXITED="failed"
126
+ cv_telem "wrapper_failed" "\"reason\":\"$1\",\"lifetime_seconds\":$(( $(date +%s) - _CV_STARTED_AT ))"
127
+ }
128
+
46
129
  # Handle auth commands (login, logout, status, reset-device)
47
130
  # Delegate to codevibe-core CLI (shared auth across all plugins)
48
131
  case "$1" in
@@ -53,14 +136,42 @@ case "$1" in
53
136
  CORE_CLI="$PLUGIN_DIR/../codevibe-core/bin/codevibe.js"
54
137
  fi
55
138
  if [ -f "$CORE_CLI" ]; then
139
+ cv_telem "wrapper_started" "\"invocation\":\"auth_$1\",\"os\":\"$(uname -s | cv_sanitize)\",\"arch\":\"$(uname -m | cv_sanitize)\""
56
140
  exec node "$CORE_CLI" "$1"
57
141
  else
58
142
  echo "Error: codevibe-core not found. Try reinstalling: npm install -g @quantiya/codevibe"
143
+ cv_failed "core_not_found"
144
+ sleep 1
59
145
  exit 1
60
146
  fi
61
147
  ;;
62
148
  esac
63
149
 
150
+ # Capture environment facts for the session-flow wrapper_started event.
151
+ # Each probe is non-fatal — if a CLI is missing we record "missing" rather
152
+ # than aborting; pre-flight checks below still gate execution. Every
153
+ # string that lands in the JSON payload goes through cv_sanitize so an
154
+ # agent CLI emitting ANSI escapes or quotes in `--version` can't break
155
+ # the hand-built payload.
156
+ _CV_GEMINI_VER="missing"
157
+ command -v gemini >/dev/null 2>&1 && _CV_GEMINI_VER="$(gemini --version 2>/dev/null | cv_sanitize)"
158
+ [ -z "$_CV_GEMINI_VER" ] && _CV_GEMINI_VER="unknown"
159
+ _CV_NODE_VER="missing"
160
+ command -v node >/dev/null 2>&1 && _CV_NODE_VER="$(node -v 2>/dev/null | cv_sanitize)"
161
+ [ -z "$_CV_NODE_VER" ] && _CV_NODE_VER="unknown"
162
+ _CV_TMUX_VER="missing"
163
+ command -v tmux >/dev/null 2>&1 && _CV_TMUX_VER="$(tmux -V 2>/dev/null | cv_sanitize)"
164
+ [ -z "$_CV_TMUX_VER" ] && _CV_TMUX_VER="unknown"
165
+ _CV_OS_VER="$(uname -s | cv_sanitize)"
166
+ [ -z "$_CV_OS_VER" ] && _CV_OS_VER="unknown"
167
+ _CV_ARCH_VER="$(uname -m | cv_sanitize)"
168
+ [ -z "$_CV_ARCH_VER" ] && _CV_ARCH_VER="unknown"
169
+ _CV_GEMINI_AUTH="false"; [ -f "$HOME/.gemini/oauth_creds.json" ] && _CV_GEMINI_AUTH="true"
170
+ _CV_GEMINI_SETTINGS="false"; [ -f "$HOME/.gemini/settings.json" ] && _CV_GEMINI_SETTINGS="true"
171
+ _CV_INSIDE_TMUX="false"; [ -n "$TMUX" ] && _CV_INSIDE_TMUX="true"
172
+ _CV_IS_TTY="false"; { [ -t 0 ] && [ -t 1 ]; } && _CV_IS_TTY="true"
173
+ 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"
174
+
64
175
  # Export hooks directory for hook scripts to use
65
176
  export CODEVIBE_HOOKS_DIR="$PLUGIN_DIR/hooks"
66
177
 
@@ -130,7 +241,52 @@ log() {
130
241
 
131
242
  # Cleanup function to kill MCP server when wrapper exits
132
243
  cleanup() {
244
+ local wrapper_exit_code=$?
133
245
  log "Cleanup triggered"
246
+
247
+ # Fire wrapper_exited telemetry BEFORE killing the server so the MCP
248
+ # log is intact when we grep for SessionStart. cv_failed sets
249
+ # _CV_EXITED on pre-flight failures so this block won't double-fire.
250
+ if [ -z "$_CV_EXITED" ]; then
251
+ _CV_EXITED="exited"
252
+ local gemini_exit="unknown"
253
+ if [ -f "$_CV_GEMINI_EXIT_FILE" ]; then
254
+ gemini_exit="$(cat "$_CV_GEMINI_EXIT_FILE" 2>/dev/null | head -c 10 | tr -d '\n\r ')"
255
+ [ -z "$gemini_exit" ] && gemini_exit="unknown"
256
+ fi
257
+ local lifetime=$(( $(date +%s) - _CV_STARTED_AT ))
258
+ local gemini_lifetime=0
259
+ if [ "$_CV_AGENT_STARTED_AT" -gt 0 ] 2>/dev/null; then
260
+ gemini_lifetime=$(( $(date +%s) - _CV_AGENT_STARTED_AT ))
261
+ fi
262
+ local hook_fired="false"
263
+ if [ -f "$_CV_MCP_LOG" ]; then
264
+ if tail -n "+$((_CV_MCP_LOG_BASELINE + 1))" "$_CV_MCP_LOG" 2>/dev/null \
265
+ | grep -q "SessionStart" 2>/dev/null; then
266
+ hook_fired="true"
267
+ fi
268
+ fi
269
+ # Outcome priority: SIGINT/SIGTERM beats everything (user intent).
270
+ # Then "we never got far enough to invoke gemini" — distinct from
271
+ # "we invoked gemini via passthrough but never started a tmux of
272
+ # our own" (the latter is a normal direct-run, not an abort).
273
+ local outcome
274
+ if [ "$wrapper_exit_code" = "130" ] || [ "$wrapper_exit_code" = "143" ]; then
275
+ outcome="interrupted"
276
+ elif [ "$_CV_AGENT_INVOKED" = "false" ]; then
277
+ outcome="pre_invoke_abort"
278
+ elif [ "$gemini_exit" != "unknown" ] && [ "$gemini_exit" != "0" ]; then
279
+ outcome="error_exit"
280
+ elif [ "$gemini_lifetime" -lt 5 ] 2>/dev/null; then
281
+ outcome="early_exit"
282
+ elif [ "$gemini_lifetime" -lt 60 ] 2>/dev/null; then
283
+ outcome="clean_short"
284
+ else
285
+ outcome="clean_long"
286
+ fi
287
+ 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\""
288
+ fi
289
+
134
290
  if [ -n "$MCP_PID" ] && kill -0 "$MCP_PID" 2>/dev/null; then
135
291
  log "Stopping MCP server (PID: $MCP_PID)"
136
292
  kill "$MCP_PID" 2>/dev/null || true
@@ -138,6 +294,7 @@ cleanup() {
138
294
  fi
139
295
  # Remove PID file
140
296
  rm -f "${CODEVIBE_TMPDIR}/codevibe-gemini-mcp-$$.pid"
297
+ rm -f "$_CV_GEMINI_EXIT_FILE"
141
298
  }
142
299
 
143
300
  # Set up trap for cleanup
@@ -147,6 +304,8 @@ trap cleanup EXIT INT TERM
147
304
  if ! command -v tmux &> /dev/null; then
148
305
  echo "Error: tmux is required but not installed."
149
306
  echo "Install with: brew install tmux"
307
+ cv_failed "tmux_missing"
308
+ sleep 1
150
309
  exit 1
151
310
  fi
152
311
 
@@ -154,18 +313,24 @@ fi
154
313
  if ! command -v gemini &> /dev/null; then
155
314
  echo "Error: gemini CLI is not installed."
156
315
  echo "Install from: https://github.com/google-gemini/gemini-cli"
316
+ cv_failed "gemini_missing"
317
+ sleep 1
157
318
  exit 1
158
319
  fi
159
320
 
160
321
  # Check if node is installed
161
322
  if ! command -v node &> /dev/null; then
162
323
  echo "Error: Node.js is required but not installed."
324
+ cv_failed "node_missing"
325
+ sleep 1
163
326
  exit 1
164
327
  fi
165
328
 
166
329
  # Check if MCP server is built
167
330
  if [ ! -f "$PLUGIN_DIR/dist/server.js" ]; then
168
331
  echo "Error: MCP server not built. Run 'npm run build' in the plugin directory first."
332
+ cv_failed "server_not_built"
333
+ sleep 1
169
334
  exit 1
170
335
  fi
171
336
 
@@ -177,17 +342,36 @@ log "Starting codevibe-gemini with session: $SESSION_NAME"
177
342
  log "Working directory: $WORKING_DIR"
178
343
  log "Arguments: $*"
179
344
 
180
- # Check if we're already inside tmux
345
+ # Check if we're already inside tmux.
346
+ # We deliberately do NOT `exec` here — running gemini as a child process
347
+ # lets the EXIT trap fire after it returns so wrapper_exited still gets
348
+ # emitted on these direct-run paths. Behaviorally identical for the user
349
+ # (gemini remains the foreground process for the duration).
181
350
  if [ -n "$TMUX" ]; then
182
351
  log "Already inside tmux, running gemini directly"
183
- # Already in tmux, just run gemini
184
- exec gemini "$@"
352
+ _CV_AGENT_INVOKED="true"
353
+ _CV_AGENT_STARTED_AT="$(date +%s)"
354
+ # `|| _CV_RC=$?` is load-bearing: with `set -e`, a non-zero exit
355
+ # from gemini would abort the wrapper before we capture the exit
356
+ # code, leaving wrapper_exited with gemini_exit_code="unknown". The
357
+ # `||` form catches non-zero without triggering set -e, while exit
358
+ # 0 leaves _CV_RC at its 0 default. printf's `|| true` keeps a
359
+ # disk-full failure from clobbering diagnostics.
360
+ _CV_RC=0
361
+ gemini "$@" || _CV_RC=$?
362
+ printf '%s' "$_CV_RC" > "$_CV_GEMINI_EXIT_FILE" 2>/dev/null || true
363
+ exit "$_CV_RC"
185
364
  fi
186
365
 
187
- # Check if running in a terminal
366
+ # Check if running in a terminal — same direct-run treatment as above.
188
367
  if [ ! -t 0 ] || [ ! -t 1 ]; then
189
368
  log "Not running in a terminal, running gemini directly"
190
- exec gemini "$@"
369
+ _CV_AGENT_INVOKED="true"
370
+ _CV_AGENT_STARTED_AT="$(date +%s)"
371
+ _CV_RC=0
372
+ gemini "$@" || _CV_RC=$?
373
+ printf '%s' "$_CV_RC" > "$_CV_GEMINI_EXIT_FILE" 2>/dev/null || true
374
+ exit "$_CV_RC"
191
375
  fi
192
376
 
193
377
  # Start MCP server in background BEFORE launching Gemini
@@ -214,6 +398,8 @@ if ! kill -0 "$MCP_PID" 2>/dev/null; then
214
398
  tail -3 "$MCP_LOG_FILE" 2>/dev/null | grep -v '^\[' | head -1
215
399
  echo ""
216
400
  echo "Server failed to start. Check $MCP_LOG_FILE for details."
401
+ cv_failed "server_died_on_startup"
402
+ sleep 1
217
403
  exit 1
218
404
  fi
219
405
 
@@ -232,10 +418,16 @@ done
232
418
  # We use a wrapper that:
233
419
  # 1. Exports the session name so prompts can find it
234
420
  # 2. Runs gemini
235
- # 3. Exits the tmux session when gemini exits
421
+ # 3. Captures gemini's exit code to $_CV_GEMINI_EXIT_FILE for the
422
+ # wrapper's cleanup trap (tmux's own attach exit code is independent
423
+ # of the inner process exit, so the file is the only reliable signal)
424
+ # 4. Exits the tmux session when gemini exits
236
425
 
237
426
  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"
427
+ "export CODEVIBE_GEMINI_TMUX_SESSION='$SESSION_NAME'; export ENVIRONMENT='$ENVIRONMENT'; $GEMINI_CMD; printf '%s' \"\$?\" > '$_CV_GEMINI_EXIT_FILE'; exit"
428
+ _CV_TMUX_STARTED="true"
429
+ _CV_AGENT_INVOKED="true"
430
+ _CV_AGENT_STARTED_AT="$(date +%s)"
239
431
 
240
432
  # Enable mouse support for scrolling
241
433
  tmux set-option -t "$SESSION_NAME" -g mouse on
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quantiya/codevibe-gemini-plugin",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
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": {