@quantiya/codevibe-antigravity-plugin 2.0.86 → 2.0.88

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/README.md CHANGED
@@ -45,7 +45,7 @@ install the unified `codevibe --agent` interface yet.
45
45
  - **macOS, Linux, or WSL Ubuntu** — Windows without WSL is not supported
46
46
  - **Node.js** 22.0.0+
47
47
  - **tmux** — `brew install tmux` on macOS, `apt install tmux` on Linux/WSL
48
- - **Antigravity CLI (`agy`)** — install from [Google's documentation](https://antigravity.google/docs)
48
+ - **Antigravity CLI (`agy`) 1.4.3+** with `--log-file` advertised by `agy --help` — install from [Google's documentation](https://antigravity.google/docs). Companion checks this before creating a session and tells unsupported clients to update; direct `agy` is unchanged.
49
49
 
50
50
  > Antigravity CLI is distributed by Google as a binary; it is not on npm. After installing `agy`, run `codevibe --agent agy` and the plugin handles the rest.
51
51
 
@@ -66,7 +66,7 @@ All agy sessions on a computer write to the same folders. CodeVibe tells them ap
66
66
 
67
67
  On each launch, `codevibe --agent agy` registers one named entry, `codevibe-antigravity`, in `~/.gemini/config/hooks.json`, and keeps the hook scripts in `~/.codevibe/codevibe-agy/hooks/`:
68
68
  - Other entries in that file are left as they are.
69
- - A file that is not valid JSON, or is a symlink, is never changed. CodeVibe then falls back to agy's log files, which cannot tell concurrent sessions apart.
69
+ - A file that is not valid JSON, or is a symlink, is never changed. Without hooks, CodeVibe uses only this launch's pinned native log and a `USER_INPUT`-first main transcript; unknown ownership waits, and `SYSTEM_MESSAGE`-first subagents are excluded.
70
70
  - For other agy sessions on the machine, `PreToolUse` immediately permits execution with `{"decision":"allow"}`, and other hook events are skipped.
71
71
  - To switch it off, add `"enabled": false` to the `codevibe-antigravity` entry. CodeVibe leaves a switched-off entry alone. Deleting the entry only lasts until the next `codevibe --agent agy` launch, which writes it again. After uninstalling CodeVibe you can delete the entry; see INSTALLATION.md.
72
72
 
@@ -6,6 +6,6 @@
6
6
  "implementation": {
7
7
  "architecture": "transcript+tmux+approval-heuristic",
8
8
  "schema_version": "v1.0",
9
- "note": "Monitors ~/.gemini/antigravity-cli/brain/<UUID>/.system_generated/logs/transcript.jsonl and the tmux pane. Where agy JSON hooks fire (agy 1.3.1+, hook gate on), the companion launcher registers a `codevibe-antigravity` entry in ~/.gemini/config/hooks.json (PreInvocation + PostToolUse) so each session routes only its own conversations; otherwise it falls back to agy's CLI log."
9
+ "note": "Requires native agy 1.4.3+ advertising --log-file; checked before Companion session creation. Monitors ~/.gemini/antigravity-cli/brain/<UUID>/.system_generated/logs/transcript.jsonl and the tmux pane. Own agy JSON hooks identify main conversations. Without hooks, ownership requires this launch's pinned native log and a USER_INPUT-first main transcript; SYSTEM_MESSAGE-first subagents and shared/newest logs are excluded. Unknown ownership defers sync."
10
10
  }
11
11
  }
package/dist/server.js CHANGED
@@ -790,6 +790,20 @@ var TranscriptTailer = class extends import_events.EventEmitter {
790
790
  getActiveConversations() {
791
791
  return Array.from(this.activeConversations);
792
792
  }
793
+ /** Retry a deferred native read after THIS launcher's hook proves ownership.
794
+ * No offset reset or legacy fallback. Join an in-flight read first so a late
795
+ * hook cannot lose its wakeup in that read's rejection/finally cleanup. */
796
+ retryOwnedConversation(conversationId) {
797
+ if (!this.admission || !this.isWatching || !CONVERSATION_DIR_REGEX.test(conversationId)) return;
798
+ const generation = this.readGeneration, filePath = this.transcriptPathFor(conversationId);
799
+ const pending = this.activeReads.get(filePath) ?? Promise.resolve();
800
+ void pending.then(() => {
801
+ if (!this.isWatching || generation !== this.readGeneration) return;
802
+ if (!fs2.existsSync(filePath)) return;
803
+ this.ensureOffset(filePath);
804
+ void this.scheduleReadNewLines(filePath, conversationId);
805
+ }).catch((error) => logger.warn("Owned transcript wakeup failed", { conversationId, error: String(error) }));
806
+ }
793
807
  // ─── follower event handlers ─────────────────────────────────────────────
794
808
  onFileAdded(filePath) {
795
809
  if (this.isShuttingDown) return;
@@ -1234,6 +1248,10 @@ var TranscriptTailer = class extends import_events.EventEmitter {
1234
1248
  }
1235
1249
  const generation = this.readGeneration;
1236
1250
  const read = this.drainReadQueue(filePath, conversationId, generation).catch((error) => {
1251
+ if (error?.code === "SYNC_SOURCE_OWNERSHIP_PENDING") {
1252
+ logger.debug("Transcript waiting for its own hook or process-bound log", { conversationId });
1253
+ return;
1254
+ }
1237
1255
  logger.error("Transcript read chain failed (non-fatal) \u2014 next change event retries", {
1238
1256
  filePath,
1239
1257
  conversationId,
@@ -7018,6 +7036,7 @@ ${commandErrorText(error, "stdout")}`;
7018
7036
  }
7019
7037
  var McpServer = class _McpServer {
7020
7038
  constructor(options) {
7039
+ this.lastNativeLogConversation = null;
7021
7040
  /** Per-session causal floor for transcript-event timestamps emitted right
7022
7041
  * after a MOBILE prompt. The mobile USER_PROMPT is created by the iOS app
7023
7042
  * (real ms clock) and never flows through prepareEventTimestamp, while the
@@ -7210,6 +7229,15 @@ var McpServer = class _McpServer {
7210
7229
  this.tmuxTarget = options.tmuxTarget ?? process.env.CODEVIBE_AGY_TMUX_TARGET ?? null;
7211
7230
  this.wrapperPid = options.wrapperPid ?? null;
7212
7231
  this.cliLogPath = options.cliLogPath ?? null;
7232
+ this.nativeCliLogPath = options.nativeCliLogPath ?? null;
7233
+ this.nativeCliLogIdentity = null;
7234
+ if (this.nativeCliLogPath) {
7235
+ const st = fs7.lstatSync(this.nativeCliLogPath);
7236
+ if (!st.isFile() || st.isSymbolicLink() || st.nlink !== 1 || st.uid !== process.getuid?.()) {
7237
+ throw Error("AGY_NATIVE_LOG_NOT_OWNED");
7238
+ }
7239
+ this.nativeCliLogIdentity = { dev: st.dev, ino: st.ino };
7240
+ }
7213
7241
  this.launchSettleTimeoutMs = options.launchSettleTimeoutMs ?? LAUNCH_SETTLE_TIMEOUT_MS;
7214
7242
  this.appSyncClient = options.appSyncClient ?? new import_codevibe_core7.AppSyncClient();
7215
7243
  this.toolActivity = options.toolActivityIntegration === void 0 ? new AgyToolActivityIntegration({
@@ -7837,13 +7865,14 @@ var McpServer = class _McpServer {
7837
7865
  }
7838
7866
  }
7839
7867
  /**
7840
- * The pre-F1 rule, now only the fallback for conversations no hook has
7841
- * claimed when hooks do not fire (see conversation-router.ts): the newest
7842
- * agy CLI log names the active conversation. F1 amendment: a conversation
7843
- * another live CodeVibe daemon has claimed is never "the active one" here,
7844
- * so another session's busy log cannot make us drop our own turn.
7868
+ * Production launchers supply a pinned native log. Hookless routing admits
7869
+ * only its positively identified main transcript. The unbound pre-F1 rule
7870
+ * below remains for legacy/test callers, not durable source allocation.
7845
7871
  */
7846
7872
  legacyAcceptsConversation(conversationId) {
7873
+ if (this.nativeCliLogPath) {
7874
+ return !this.claims.isClaimedElsewhere(conversationId) && this.boundNativeLogConversation() === conversationId;
7875
+ }
7847
7876
  if (process.env.JEST_WORKER_ID && !this.cliLogPath) {
7848
7877
  return true;
7849
7878
  }
@@ -7857,6 +7886,38 @@ var McpServer = class _McpServer {
7857
7886
  }
7858
7887
  return activeId === conversationId;
7859
7888
  }
7889
+ /** Never resolve the shared cli.log symlink or scan for another process's log. */
7890
+ boundNativeLogConversation() {
7891
+ if (!this.nativeCliLogPath || !this.nativeCliLogIdentity) return null;
7892
+ let fd;
7893
+ try {
7894
+ fd = fs7.openSync(this.nativeCliLogPath, fs7.constants.O_RDONLY | fs7.constants.O_NOFOLLOW);
7895
+ const st = fs7.fstatSync(fd), expected = this.nativeCliLogIdentity;
7896
+ if (!st.isFile() || st.nlink !== 1 || st.dev !== expected.dev || st.ino !== expected.ino) return null;
7897
+ const matches = [...fs7.readFileSync(fd, "utf8").matchAll(/(?:Created conversation|Streaming conversation|Starting conversation update stream for)\s+([0-9a-fA-F-]{36})/g)];
7898
+ const checked = /* @__PURE__ */ new Set();
7899
+ for (let i = matches.length - 1; i >= 0; i--) {
7900
+ const conversationId = matches[i][1];
7901
+ if (checked.has(conversationId)) continue;
7902
+ checked.add(conversationId);
7903
+ if (this.router.roleOf(conversationId) !== "subagent" && !this.claims.isClaimedElsewhere(conversationId) && this.readConversationFirstType(conversationId) === "USER_INPUT") return conversationId;
7904
+ }
7905
+ return null;
7906
+ } catch {
7907
+ return null;
7908
+ } finally {
7909
+ if (fd !== void 0) fs7.closeSync(fd);
7910
+ }
7911
+ }
7912
+ refreshNativeLogOwnership() {
7913
+ const conversationId = this.boundNativeLogConversation();
7914
+ if (!conversationId || conversationId === this.lastNativeLogConversation) return;
7915
+ this.lastNativeLogConversation = conversationId;
7916
+ if (this.router.roleOf(conversationId) !== "subagent" && !this.claims.isClaimedElsewhere(conversationId)) {
7917
+ logger.info("Agy launch log identified its native conversation", { conversationId, ownership: "process-bound-log" });
7918
+ this.transcriptTailer.retryOwnedConversation(conversationId);
7919
+ }
7920
+ }
7860
7921
  /**
7861
7922
  * F1 moved the runtime dir under ~/.codevibe, so it outlives reboots. The
7862
7923
  * wrapper's EXIT trap removes it, except when the outer terminal closed while
@@ -7867,6 +7928,12 @@ var McpServer = class _McpServer {
7867
7928
  removeOrphanedRuntimeDir() {
7868
7929
  if (!this.runtimeDir || this.wrapperPid === null || isPidAlive(this.wrapperPid)) return;
7869
7930
  try {
7931
+ if (this.nativeCliLogPath && this.nativeCliLogIdentity) {
7932
+ const st = fs7.lstatSync(this.nativeCliLogPath);
7933
+ if (st.isFile() && !st.isSymbolicLink() && st.nlink === 1 && st.dev === this.nativeCliLogIdentity.dev && st.ino === this.nativeCliLogIdentity.ino) {
7934
+ fs7.unlinkSync(this.nativeCliLogPath);
7935
+ }
7936
+ }
7870
7937
  fs7.rmdirSync(this.runtimeDir);
7871
7938
  logger.info("Removed runtime dir left by a closed terminal", { runtimeDir: this.runtimeDir });
7872
7939
  } catch (error) {
@@ -7938,6 +8005,7 @@ var McpServer = class _McpServer {
7938
8005
  });
7939
8006
  if (role === "main") {
7940
8007
  this.observedMainConversationIds.add(conversationId);
8008
+ this.transcriptTailer.retryOwnedConversation(conversationId);
7941
8009
  if (!wasAlreadyAdmitted) {
7942
8010
  await this.drainHeldHookDeliveries(conversationId, "process", delivery.initialNumSteps);
7943
8011
  }
@@ -7986,6 +8054,7 @@ var McpServer = class _McpServer {
7986
8054
  logger.warn("PostToolUse prompt resolution failed", { error: String(error) });
7987
8055
  });
7988
8056
  this.router.onPostToolUse(conversationId);
8057
+ if (this.router.roleOf(conversationId) === "main") this.transcriptTailer.retryOwnedConversation(conversationId);
7989
8058
  }
7990
8059
  resolveLingeringQuestionPrompts(conversationId) {
7991
8060
  const lingeringPrompts = this.approvalDetector.getPendingPromptsForConversation(conversationId).filter((p) => p.kind === "question");
@@ -8411,8 +8480,9 @@ var McpServer = class _McpServer {
8411
8480
  const selection = await this.selectNativeSource(session);
8412
8481
  if (!this.started || this.session !== session) throw Error("SYNC_SOURCE_OWNER_CHANGED");
8413
8482
  if (selection.protocol === "LEGACY") return null;
8414
- if (!this.legacyAcceptsConversation(source.conversationId) || this.router.roleOf(source.conversationId) === "subagent") {
8415
- throw Error("SYNC_SOURCE_OWNERSHIP_UNAVAILABLE");
8483
+ const ownsConversation = () => !this.claims.isClaimedElsewhere(source.conversationId) && (this.router.roleOf(source.conversationId) === "main" || this.router.roleOf(source.conversationId) === void 0 && this.boundNativeLogConversation() === source.conversationId);
8484
+ if (!ownsConversation()) {
8485
+ throw Object.assign(Error("SYNC_SOURCE_OWNERSHIP_UNAVAILABLE"), { code: "SYNC_SOURCE_OWNERSHIP_PENDING" });
8416
8486
  }
8417
8487
  if (!this.toolActivity?.isActive(session.sessionId)) {
8418
8488
  if (this.nativeToolStart && this.nativeToolStart.session !== session) throw Error("SYNC_SOURCE_OWNER_CHANGED");
@@ -8426,7 +8496,12 @@ var McpServer = class _McpServer {
8426
8496
  }
8427
8497
  await this.nativeToolStart.pending;
8428
8498
  }
8429
- if (!this.toolActivity?.isActive(session.sessionId) || this.session !== session || !this.started) throw Error("SYNC_SOURCE_OWNERSHIP_UNAVAILABLE");
8499
+ if (!ownsConversation()) {
8500
+ throw Object.assign(Error("SYNC_SOURCE_OWNERSHIP_UNAVAILABLE"), { code: "SYNC_SOURCE_OWNERSHIP_PENDING" });
8501
+ }
8502
+ if (!this.toolActivity?.isActive(session.sessionId) || this.session !== session || !this.started) {
8503
+ throw Error("SYNC_SOURCE_OWNERSHIP_UNAVAILABLE");
8504
+ }
8430
8505
  const streamSha256 = crypto3.createHash("sha256").update(JSON.stringify([source.filePath, source.conversationId])).digest("hex");
8431
8506
  const key = `${selection.sessionScope.sessionId}:${selection.sessionScope.sessionGenerationId}:${streamSha256}`;
8432
8507
  const existing = this.nativeSources.get(key);
@@ -8470,7 +8545,7 @@ var McpServer = class _McpServer {
8470
8545
  this.assertNativeOwner(owner);
8471
8546
  if ((await owner.cursor.open(0)).byteOffset >= source.endOffset) return;
8472
8547
  const admission = { outputs: [null, null] };
8473
- await this.handleTranscriptEmit(emit, "legacy", admission);
8548
+ await this.handleTranscriptEmit(emit, "process", admission);
8474
8549
  this.assertNativeOwner(owner);
8475
8550
  const echoFrame = {
8476
8551
  streamSha256: owner.streamSha256,
@@ -9283,7 +9358,7 @@ var McpServer = class _McpServer {
9283
9358
  if (bound && this.observedMainConversationIds.has(bound)) return bound;
9284
9359
  if (this.observedMainConversationIds.size === 0) return null;
9285
9360
  const cliLogPath = this.cliLogPath ?? path8.join(os7.homedir(), ".gemini", "antigravity-cli", "cli.log");
9286
- const cliActive = getActiveConversationFromCliLog(cliLogPath);
9361
+ const cliActive = this.nativeCliLogPath ? this.boundNativeLogConversation() : getActiveConversationFromCliLog(cliLogPath);
9287
9362
  if (cliActive && this.observedMainConversationIds.has(cliActive)) {
9288
9363
  return cliActive;
9289
9364
  }
@@ -9914,6 +9989,7 @@ var McpServer = class _McpServer {
9914
9989
  const state = await this.tmuxSessionState(tmuxTarget);
9915
9990
  if (state === "alive") {
9916
9991
  this.tmuxLifecycleObservedAlive = true;
9992
+ this.refreshNativeLogOwnership();
9917
9993
  return;
9918
9994
  }
9919
9995
  const ownershipEstablished = this.tmuxLifecycleObservedAlive || Date.now() >= this.tmuxLifecycleInitDeadlineMs;
@@ -10241,6 +10317,7 @@ async function main() {
10241
10317
  bearerToken,
10242
10318
  preferredPort: args.port,
10243
10319
  runtimeDir: args.runtimeDir,
10320
+ nativeCliLogPath: args.runtimeDir ? path8.join(args.runtimeDir, "native-cli.log") : void 0,
10244
10321
  tmuxTarget: process.env.CODEVIBE_AGY_TMUX_TARGET ?? void 0,
10245
10322
  wrapperPid: process.env.CODEVIBE_AGY_WRAPPER_PID ? parseInt(process.env.CODEVIBE_AGY_WRAPPER_PID, 10) : void 0
10246
10323
  });
@@ -50,6 +50,7 @@
50
50
  # Companion limitations:
51
51
  # - Requires tmux. Print mode (`--print` / `-p`) is NOT supported.
52
52
  # - Requires Node.js 22 or newer for the MCP server.
53
+ # - Requires agy 1.4.3+ advertising --log-file for launch ownership.
53
54
  #
54
55
 
55
56
  set -e
@@ -182,6 +183,37 @@ case "${1:-}" in
182
183
  ;;
183
184
  esac
184
185
 
186
+ # Companion owns the log used as native transcript ownership proof. Do not
187
+ # silently override a user's log or let a second flag redirect that proof.
188
+ for _CV_ARG in "$@"; do
189
+ case "$_CV_ARG" in
190
+ --log-file|--log-file=*|-log-file|-log-file=*)
191
+ echo 'Error: CodeVibe Companion owns --log-file for session isolation. Run agy directly to use a custom log.' >&2
192
+ exit 2
193
+ ;;
194
+ esac
195
+ done
196
+ unset _CV_ARG
197
+
198
+ # Require the tested native contract before installer, runtime, or session work.
199
+ # Metadata passthrough above and direct `agy` use remain unchanged.
200
+ require_native_sync_support() {
201
+ local version="$1" binary="$2" help major minor patch
202
+ if [[ "$version" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
203
+ major=$((10#${BASH_REMATCH[1]}))
204
+ minor=$((10#${BASH_REMATCH[2]}))
205
+ patch=$((10#${BASH_REMATCH[3]}))
206
+ if (( major > 1 || (major == 1 && (minor > 4 || (minor == 4 && patch >= 3))) )); then
207
+ if help="$("$binary" --help 2>&1)" &&
208
+ [[ "$help" =~ (^|[[:space:]])--log-file([=[:space:]]|$) ]]; then
209
+ return 0
210
+ fi
211
+ fi
212
+ fi
213
+ echo 'Error: CodeVibe Companion requires Antigravity CLI 1.4.3+ with --log-file support. Update agy, then retry. Direct agy use is unchanged.' >&2
214
+ return 1
215
+ }
216
+
185
217
  # ─── Environment probes ───────────────────────────────────────────────
186
218
  _CV_NODE_VER="missing"
187
219
  command -v node >/dev/null 2>&1 && _CV_NODE_VER="$(node -v 2>/dev/null | cv_sanitize)"
@@ -191,7 +223,7 @@ command -v tmux >/dev/null 2>&1 && _CV_TMUX_VER="$(tmux -V 2>/dev/null | cv_sani
191
223
  [ -z "$_CV_TMUX_VER" ] && _CV_TMUX_VER="unknown"
192
224
  _CV_AGY_BIN="$(resolve_agy_bin)"
193
225
  _CV_AGY_VER="missing"
194
- [ -n "$_CV_AGY_BIN" ] && _CV_AGY_VER="$("$_CV_AGY_BIN" --version 2>/dev/null | cv_sanitize)"
226
+ [ -n "$_CV_AGY_BIN" ] && _CV_AGY_VER="$(cv_sanitize "$("$_CV_AGY_BIN" --version 2>/dev/null)")"
195
227
  [ -z "$_CV_AGY_VER" ] && _CV_AGY_VER="unknown"
196
228
  _CV_OS_VER="$(uname -s | cv_sanitize)"
197
229
  [ -z "$_CV_OS_VER" ] && _CV_OS_VER="unknown"
@@ -262,6 +294,11 @@ if [ -z "$_CV_AGY_BIN" ] || ! [ -f "$_CV_AGY_BIN" ] || ! [ -x "$_CV_AGY_BIN" ];
262
294
  exit 1
263
295
  fi
264
296
 
297
+ if ! require_native_sync_support "$_CV_AGY_VER" "$_CV_AGY_BIN"; then
298
+ cv_failed "agy_sync_contract_unsupported"
299
+ exit 2
300
+ fi
301
+
265
302
  if ! command -v node &> /dev/null; then
266
303
  echo "Error: Node.js is required but not installed."
267
304
  echo "Install Node.js 22+ from: https://nodejs.org"
@@ -348,6 +385,14 @@ rm -f "$RUNTIME_DIR/conn.json" "$RUNTIME_DIR/claims.json"
348
385
  # Also tighten the runtime base — best-effort, no fatal on failure.
349
386
  chmod 0700 "$RUNTIME_BASE" 2>/dev/null || true
350
387
 
388
+ # The native process and daemon share only this launch's fresh regular log.
389
+ # Refuse a stale PID-reuse file rather than trusting its conversation identity.
390
+ NATIVE_CLI_LOG="$RUNTIME_DIR/native-cli.log"
391
+ if ! (umask 077; set -o noclobber; : > "$NATIVE_CLI_LOG"); then
392
+ echo 'Error: this Companion runtime already contains a native log; refusing ambiguous ownership.' >&2
393
+ exit 1
394
+ fi
395
+
351
396
  log "Wrapper PID=$WRAPPER_PID; token=$(printf '%s' "$BEARER_TOKEN" | cut -c1-8)...; port=$MCP_PORT; runtime=$RUNTIME_DIR"
352
397
 
353
398
  # Export env vars consumed by the MCP server + any in-tree tooling.
@@ -481,7 +526,8 @@ escaped_exit_file="$(escape_sq "$_CV_AGY_EXIT_FILE")"
481
526
  escaped_conn_file="$(escape_sq "$RUNTIME_DIR/conn.json")"
482
527
  escaped_mcp_log="$(escape_sq "$MCP_LOG_FILE")"
483
528
  escaped_node_bin="$(escape_sq "$(command -v node)")"
484
- AGY_CMD="'$escaped_bin'"
529
+ escaped_native_log="$(escape_sq "$NATIVE_CLI_LOG")"
530
+ AGY_CMD="'$escaped_bin' --log-file '$escaped_native_log'"
485
531
  for arg in "$@"; do
486
532
  escaped_arg="$(escape_sq "$arg")"
487
533
  AGY_CMD="$AGY_CMD '$escaped_arg'"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quantiya/codevibe-antigravity-plugin",
3
- "version": "2.0.86",
3
+ "version": "2.0.88",
4
4
  "description": "Control Antigravity 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
  "codevibe": {
@@ -46,7 +46,7 @@
46
46
  "node": ">=22.0.0"
47
47
  },
48
48
  "dependencies": {
49
- "@quantiya/codevibe-core": "^2.0.75",
49
+ "@quantiya/codevibe-core": "^2.0.77",
50
50
  "chokidar": "^5.0.0",
51
51
  "dotenv": "^16.6.1",
52
52
  "express": "^5.1.0",