@quantiya/codevibe-gemini-plugin 1.0.21 → 1.0.23

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 +219 -37
  2. package/package.json +2 -2
@@ -172,10 +172,49 @@ _CV_INSIDE_TMUX="false"; [ -n "$TMUX" ] && _CV_INSIDE_TMUX="true"
172
172
  _CV_IS_TTY="false"; { [ -t 0 ] && [ -t 1 ]; } && _CV_IS_TTY="true"
173
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
174
 
175
+ # Configuration — hoisted ABOVE configure_hooks_if_needed because the
176
+ # function logs to $LOG_FILE on failure paths. With LOG_FILE undefined,
177
+ # `2>>"$LOG_FILE"` becomes `2>>""` and the redirection silently fails
178
+ # (or, on stricter shells, breaks the function entirely). Codified
179
+ # 2026-04-30 after R1 caught the ordering bug.
180
+ TMUX_SESSION_PREFIX="codevibe-gemini"
181
+ LOG_FILE="${CODEVIBE_TMPDIR}/codevibe-gemini-wrapper.log"
182
+ MCP_LOG_FILE="${CODEVIBE_TMPDIR}/codevibe-gemini-mcp.log"
183
+
184
+ log() {
185
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"
186
+ }
187
+
188
+ # jq is required at HOOK RUNTIME (not just install time) by every Gemini
189
+ # hook script — see hooks/*.sh: jq parses the stdin JSON Gemini CLI feeds
190
+ # into each hook. The wrapper's own hook merger (below) was ported off jq
191
+ # but the bundled hook scripts still require it. Without jq, hooks fire
192
+ # but produce empty payloads, which lands as a 0-event ghost session in
193
+ # DDB — same silent-failure pattern as the codex jq-merge bug. Until the
194
+ # hooks are ported to a no-jq parser (see follow-up task), fail closed
195
+ # with a clear reason so users get an actionable error instead of a
196
+ # session that quietly never syncs.
197
+ #
198
+ # Codex hooks are unaffected — they don't use jq at runtime.
199
+ if ! command -v jq &> /dev/null; then
200
+ echo "Error: jq is required by CodeVibe Gemini hooks but is not installed."
201
+ echo "Install with: brew install jq (macOS) or apt-get install jq (Debian/Ubuntu) or dnf install jq (Fedora)"
202
+ cv_failed "jq_missing_for_hooks"
203
+ sleep 1
204
+ exit 1
205
+ fi
206
+
175
207
  # Export hooks directory for hook scripts to use
176
208
  export CODEVIBE_HOOKS_DIR="$PLUGIN_DIR/hooks"
177
209
 
178
- # Auto-configure hooks on startup if not already configured
210
+ # Auto-configure hooks on startup if not already configured.
211
+ # Sets _CV_HOOKS_OUTCOME / _CV_HOOKS_REASON for the hooks_install_outcome
212
+ # telemetry beacon fired in the caller. Gemini events flow ONLY through
213
+ # hooks (no transcript fallback for tool/approval events), so a silent
214
+ # skip here = a 0-event ghost session in DDB. Codified after the susyustc
215
+ # 2026-04 silent-failure pattern was traced to the codex twin of this bug.
216
+ _CV_HOOKS_OUTCOME="unknown"
217
+ _CV_HOOKS_REASON=""
179
218
  configure_hooks_if_needed() {
180
219
  GEMINI_SETTINGS_DIR="$HOME/.gemini"
181
220
  GEMINI_SETTINGS_FILE="$GEMINI_SETTINGS_DIR/settings.json"
@@ -193,51 +232,194 @@ configure_hooks_if_needed() {
193
232
  if [ ! -f "$TEMPLATE_FILE" ]; then
194
233
  echo "⚠ Hooks template not found at: $TEMPLATE_FILE"
195
234
  echo " Try: npm install -g @quantiya/codevibe@latest"
235
+ _CV_HOOKS_OUTCOME="template_missing"
236
+ _CV_HOOKS_REASON="bundled_template_not_found"
196
237
  return
197
238
  fi
198
239
 
199
- if [ ! -f "$GEMINI_SETTINGS_FILE" ]; then
200
- # No settings filecreate from template
201
- echo "Configuring Gemini CLI hooks for CodeVibe..."
202
- sed "s|__CODEVIBE_HOOKS_DIR__|$HOOKS_DIR|g" "$TEMPLATE_FILE" > "$GEMINI_SETTINGS_FILE"
203
- echo "✓ Hooks configured at: $GEMINI_SETTINGS_FILE"
204
- elif ! grep -q "codevibe" "$GEMINI_SETTINGS_FILE" 2>/dev/null; then
205
- # Settings exists but no CodeVibe hooks present append ours
206
- echo "Adding CodeVibe hooks to Gemini CLI settings..."
207
- if command -v jq >/dev/null 2>&1; then
208
- HOOKS_JSON=$(sed "s|__CODEVIBE_HOOKS_DIR__|$HOOKS_DIR|g" "$TEMPLATE_FILE")
209
- # Append CodeVibe hook entries to each hook array, preserving existing user hooks
210
- jq -s '
211
- .[0] as $existing | .[1] as $new |
212
- $existing | .hooks = (
213
- ($existing.hooks // {}) as $eh |
214
- ($new.hooks // {}) as $nh |
215
- ($eh | keys) + ($nh | keys) | unique | map(
216
- { (.) : (($eh[.] // []) + ($nh[.] // [])) }
217
- ) | add // {}
218
- )
219
- ' "$GEMINI_SETTINGS_FILE" <(echo "$HOOKS_JSON") > "${GEMINI_SETTINGS_FILE}.tmp" && \
220
- mv "${GEMINI_SETTINGS_FILE}.tmp" "$GEMINI_SETTINGS_FILE" && \
221
- echo "✓ CodeVibe hooks added to: $GEMINI_SETTINGS_FILE" || \
222
- echo "⚠ Failed to add hooks. Delete $GEMINI_SETTINGS_FILE and restart to auto-configure."
223
- else
224
- echo "⚠ jq not found — cannot add hooks to existing settings."
225
- echo " Delete $GEMINI_SETTINGS_FILE and restart to auto-configure."
226
- fi
240
+ # Single Node-based hook installer: handles fresh install, idempotent
241
+ # merge, and "already installed" detection all with a stable
242
+ # ownership predicate (path-prefix + filename-tail). Why one combined
243
+ # script vs three branches with grep:
244
+ #
245
+ # - The "already installed" check used to be `grep -q "codevibe"`
246
+ # over the whole file. That false-positives if any unrelated user
247
+ # setting happens to contain "codevibe" (a path, a comment field
248
+ # in JSON, etc.). The structured check looks for our specific
249
+ # hook-script files.
250
+ #
251
+ # - The merge used to unconditionally APPEND our entries to existing
252
+ # arrays. Two concurrent codevibe-gemini starts could each read
253
+ # the pre-merge state and each append, producing duplicate entries
254
+ # that then double-fire every event. The new merger filters out
255
+ # OUR previously-installed entries (any version's path) before
256
+ # appending fresh ones. Multiple runs converge to the same
257
+ # settings.json regardless of interleaving.
258
+ HOOKS_JSON=$(sed "s|__CODEVIBE_HOOKS_DIR__|$HOOKS_DIR|g" "$TEMPLATE_FILE")
259
+ # Wrapping in `if … ; then … else INSTALLER_RC=$? ; fi` is required
260
+ # under `set -e` to capture Node's non-zero exit codes; a bare
261
+ # `VAR=$(cmd)` followed by `RC=$?` would short-circuit on Node's
262
+ # exit before $? gets read, defeating the fail-closed mapping below.
263
+ if INSTALLER_OUTPUT=$(CV_EXISTING="$GEMINI_SETTINGS_FILE" \
264
+ CV_NEW="$HOOKS_JSON" \
265
+ node -e '
266
+ const fs = require("fs");
267
+ const OUR_HOOK_FILES = new Set([
268
+ "session-start.sh",
269
+ "session-end.sh",
270
+ "before-agent.sh",
271
+ "after-agent.sh",
272
+ "before-tool.sh",
273
+ "after-tool.sh",
274
+ "notification.sh",
275
+ ]);
276
+ const OWN_PATH_MARKER = "codevibe-gemini-plugin/hooks/";
277
+ const isOurCommand = (command) => {
278
+ if (typeof command !== "string") return false;
279
+ if (command.indexOf(OWN_PATH_MARKER) === -1) return false;
280
+ for (const f of OUR_HOOK_FILES) {
281
+ if (command.endsWith("/" + f)) return true;
282
+ }
283
+ return false;
284
+ };
285
+ // Strip our hooks from a single matcher entrys inner hooks[] array.
286
+ // Returns null if no non-owned hooks remain (drop the entry);
287
+ // otherwise returns a new entry with only the user hooks. Returns
288
+ // the original entry unchanged when it contains none of ours.
289
+ const stripOurHooksFromEntry = (entry) => {
290
+ if (!entry || typeof entry !== "object" || !Array.isArray(entry.hooks)) {
291
+ return entry;
292
+ }
293
+ const filtered = entry.hooks.filter((h) => !(h && isOurCommand(h.command)));
294
+ if (filtered.length === entry.hooks.length) return entry;
295
+ if (filtered.length === 0) return null;
296
+ return { ...entry, hooks: filtered };
297
+ };
298
+ const entryContainsOurs = (entry) =>
299
+ entry && typeof entry === "object" && Array.isArray(entry.hooks) &&
300
+ entry.hooks.some((h) => h && isOurCommand(h.command));
301
+
302
+ let existingObj = null;
303
+ let existed = false;
304
+ try {
305
+ if (fs.existsSync(process.env.CV_EXISTING)) {
306
+ existed = true;
307
+ existingObj = JSON.parse(fs.readFileSync(process.env.CV_EXISTING, "utf8"));
308
+ }
309
+ } catch (e) {
310
+ process.stderr.write("PARSE_EXISTING_FAILED:" + (e && e.message ? e.message : String(e)));
311
+ process.exit(2);
312
+ }
313
+ let nextObj;
314
+ try {
315
+ nextObj = JSON.parse(process.env.CV_NEW);
316
+ } catch (e) {
317
+ process.stderr.write("PARSE_NEW_FAILED:" + (e && e.message ? e.message : String(e)));
318
+ process.exit(3);
319
+ }
320
+
321
+ const existingHooks = (existingObj && typeof existingObj === "object" &&
322
+ existingObj.hooks && typeof existingObj.hooks === "object") ? existingObj.hooks : {};
323
+ const nextHooks = (nextObj && typeof nextObj === "object" &&
324
+ nextObj.hooks && typeof nextObj.hooks === "object") ? nextObj.hooks : {};
325
+ let allPresent = existed;
326
+ for (const k of Object.keys(nextHooks)) {
327
+ const arr = Array.isArray(existingHooks[k]) ? existingHooks[k] : [];
328
+ if (!arr.some(entryContainsOurs)) {
329
+ allPresent = false;
330
+ break;
331
+ }
332
+ }
333
+ if (allPresent) {
334
+ process.stdout.write("OUTCOME:already_installed\n");
335
+ process.exit(0);
336
+ }
337
+
338
+ const mergedHooks = { ...existingHooks };
339
+ for (const k of Object.keys(nextHooks)) {
340
+ const existingArr = Array.isArray(existingHooks[k]) ? existingHooks[k] : [];
341
+ const cleanedExisting = existingArr
342
+ .map(stripOurHooksFromEntry)
343
+ .filter((entry) => entry !== null && entry !== undefined);
344
+ const nextArr = Array.isArray(nextHooks[k]) ? nextHooks[k] : [];
345
+ mergedHooks[k] = [...cleanedExisting, ...nextArr];
346
+ }
347
+
348
+ const out = (existingObj && typeof existingObj === "object")
349
+ ? { ...existingObj, hooks: mergedHooks }
350
+ : { hooks: mergedHooks };
351
+ const outcome = existed ? "merged" : "fresh_install";
352
+ const target = process.env.CV_EXISTING;
353
+ // Per-process tmp suffix avoids two concurrent installs both
354
+ // writing ${target}.tmp and one renaming the others half-write.
355
+ const tmp = target + "." + process.pid + "." + Date.now() + ".tmp";
356
+ try {
357
+ fs.writeFileSync(tmp, JSON.stringify(out, null, 2));
358
+ fs.renameSync(tmp, target);
359
+ } catch (e) {
360
+ try { fs.unlinkSync(tmp); } catch {}
361
+ process.stderr.write("WRITE_FAILED:" + (e && e.message ? e.message : String(e)));
362
+ process.exit(4);
363
+ }
364
+ process.stdout.write("OUTCOME:" + outcome + "\n");
365
+ ' 2>>"$LOG_FILE"); then
366
+ INSTALLER_RC=0
367
+ else
368
+ INSTALLER_RC=$?
369
+ fi
370
+ if [ "$INSTALLER_RC" -eq 0 ]; then
371
+ case "$INSTALLER_OUTPUT" in
372
+ OUTCOME:already_installed*)
373
+ _CV_HOOKS_OUTCOME="already_installed"
374
+ ;;
375
+ OUTCOME:merged*)
376
+ echo "✓ CodeVibe hooks added to: $GEMINI_SETTINGS_FILE"
377
+ _CV_HOOKS_OUTCOME="merged"
378
+ ;;
379
+ OUTCOME:fresh_install*)
380
+ echo "✓ Hooks configured at: $GEMINI_SETTINGS_FILE"
381
+ _CV_HOOKS_OUTCOME="fresh_install"
382
+ ;;
383
+ *)
384
+ _CV_HOOKS_OUTCOME="merge_failed"
385
+ _CV_HOOKS_REASON="unrecognized_outcome"
386
+ log "ERROR: hooks installer returned unrecognized outcome: $INSTALLER_OUTPUT"
387
+ ;;
388
+ esac
389
+ else
390
+ case "$INSTALLER_RC" in
391
+ 2) _CV_HOOKS_OUTCOME="merge_failed"; _CV_HOOKS_REASON="parse_existing_error" ;;
392
+ 3) _CV_HOOKS_OUTCOME="merge_failed"; _CV_HOOKS_REASON="parse_template_error" ;;
393
+ 4) _CV_HOOKS_OUTCOME="merge_failed"; _CV_HOOKS_REASON="write_failed" ;;
394
+ *) _CV_HOOKS_OUTCOME="merge_failed"; _CV_HOOKS_REASON="installer_exit_$INSTALLER_RC" ;;
395
+ esac
396
+ echo "⚠ Failed to install/merge CodeVibe hooks (rc=$INSTALLER_RC, reason=$_CV_HOOKS_REASON)"
397
+ log "ERROR: hooks installer failed (rc=$INSTALLER_RC, reason=$_CV_HOOKS_REASON)"
227
398
  fi
228
399
  }
229
400
 
230
401
  # Configure hooks before starting
231
402
  configure_hooks_if_needed
403
+ cv_telem "hooks_install_outcome" "\"outcome\":\"$_CV_HOOKS_OUTCOME\",\"reason\":\"$_CV_HOOKS_REASON\""
404
+
405
+ # If hook installation didn't land, there's no point starting the daemon —
406
+ # Gemini events flow ONLY through hooks. Bail with a clear message so the
407
+ # user can fix the underlying file/permission issue rather than running a
408
+ # silent ghost session.
409
+ case "$_CV_HOOKS_OUTCOME" in
410
+ fresh_install|merged|already_installed) ;;
411
+ *)
412
+ echo "Error: failed to install CodeVibe hooks ($_CV_HOOKS_OUTCOME)."
413
+ echo " Without hooks installed, Gemini sessions cannot sync to mobile."
414
+ echo " Check $LOG_FILE for the underlying error."
415
+ cv_failed "hooks_install_$_CV_HOOKS_OUTCOME"
416
+ sleep 1
417
+ exit 1
418
+ ;;
419
+ esac
232
420
 
233
- # Configuration
234
- TMUX_SESSION_PREFIX="codevibe-gemini"
235
- LOG_FILE="${CODEVIBE_TMPDIR}/codevibe-gemini-wrapper.log"
236
- MCP_LOG_FILE="${CODEVIBE_TMPDIR}/codevibe-gemini-mcp.log"
237
-
238
- log() {
239
- echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"
240
- }
421
+ # (TMUX_SESSION_PREFIX/LOG_FILE/MCP_LOG_FILE/log() were hoisted above
422
+ # configure_hooks_if_needed.)
241
423
 
242
424
  # Cleanup function to kill MCP server when wrapper exits
243
425
  cleanup() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quantiya/codevibe-gemini-plugin",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
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.16",
52
+ "@quantiya/codevibe-core": "^1.0.17",
53
53
  "chokidar": "^5.0.0",
54
54
  "dotenv": "^16.6.1",
55
55
  "express": "^5.1.0",