@quantiya/codevibe-gemini-plugin 1.0.22 → 1.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/codevibe-gemini +219 -37
- package/package.json +2 -2
package/bin/codevibe-gemini
CHANGED
|
@@ -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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
#
|
|
234
|
-
|
|
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.
|
|
3
|
+
"version": "1.0.24",
|
|
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.
|
|
52
|
+
"@quantiya/codevibe-core": "^1.0.18",
|
|
53
53
|
"chokidar": "^5.0.0",
|
|
54
54
|
"dotenv": "^16.6.1",
|
|
55
55
|
"express": "^5.1.0",
|