@quantiya/codevibe-antigravity-plugin 1.0.13 → 1.0.14

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-agy CHANGED
@@ -6,8 +6,10 @@
6
6
  # Architecture (DESIGN.md §3 + §5.2):
7
7
  #
8
8
  # 1. ensureInstalled (idempotent): writes plugin manifest to
9
- # ~/.gemini/antigravity-cli/plugins/codevibe-antigravity/ + wires
10
- # ~/.gemini/config/mcp_config.json's mcpServers entry.
9
+ # ~/.gemini/antigravity-cli/plugins/codevibe-antigravity/ and REMOVES any
10
+ # stale codevibe-antigravity entry from ~/.gemini/config/mcp_config.json
11
+ # (P42, 2026-09-09: a registered entry made agy spawn a second, argument-
12
+ # less daemon every few minutes; the daemon is launched by this wrapper only).
11
13
  #
12
14
  # 2. Generate ephemeral port + bearer token; create runtime dir at
13
15
  # ~/.gemini/antigravity-cli/plugins/codevibe-antigravity/runtime/$$
@@ -240,9 +242,10 @@ fi
240
242
 
241
243
  # ─── Atomic install of plugin footprint (DESIGN.md §5.1) ───────────────
242
244
  # Runs ensureInstalled which writes antigravity-plugin.json to
243
- # ~/.gemini/antigravity-cli/plugins/codevibe-antigravity/ + adds the
244
- # mcpServers entry to ~/.gemini/config/mcp_config.json. Idempotent —
245
- # subsequent runs are no-ops if the manifest hash matches.
245
+ # ~/.gemini/antigravity-cli/plugins/codevibe-antigravity/ and removes any
246
+ # stale codevibe-antigravity entry from ~/.gemini/config/mcp_config.json
247
+ # (P42). Idempotent — subsequent runs are no-ops once the manifest hash
248
+ # matches and no stale entry remains.
246
249
  log "Running ensureInstalled"
247
250
  if ! node "$PLUGIN_DIR/dist/installer-cli.js"; then
248
251
  echo "Error: codevibe-antigravity installer failed."
@@ -340,21 +343,14 @@ cleanup() {
340
343
  cv_telem "wrapper_exited" "\"exit_code\":$wrapper_exit_code,\"lifetime_seconds\":$lifetime,\"agy_exit_code\":\"$agy_exit\",\"agy_lifetime_seconds\":$agy_lifetime,\"tmux_session_started\":$_CV_TMUX_STARTED,\"agent_invoked\":$_CV_AGENT_INVOKED,\"terminal_outcome\":\"$outcome\""
341
344
  fi
342
345
 
343
- # Stop the MCP server graceful first, then SIGKILL fallback.
344
- # The server's SIGTERM handler marks the session INACTIVE.
346
+ # Stop the MCP server gracefully and wait for product-owned cleanup. Never
347
+ # force-kill here: stop() must finish its hosted INACTIVE obligation before
348
+ # the process exits, even when that takes longer than the old 3s bound.
349
+ # (P42 backport of the 2.0 launcher: a daemon force-terminated 3s after TERM,
350
+ # mid-write, left an ACTIVE row that only the next launch's 6-minute sweep reaped.)
345
351
  if [ -n "$MCP_PID" ] && kill -0 "$MCP_PID" 2>/dev/null; then
346
352
  log "Stopping MCP server (PID: $MCP_PID)"
347
353
  kill -TERM "$MCP_PID" 2>/dev/null || true
348
- # Wait up to 3s for graceful shutdown.
349
- local i=0
350
- while [ $i -lt 30 ] && kill -0 "$MCP_PID" 2>/dev/null; do
351
- sleep 0.1
352
- i=$((i + 1))
353
- done
354
- if kill -0 "$MCP_PID" 2>/dev/null; then
355
- log "MCP server did not exit cleanly; sending SIGKILL"
356
- kill -KILL "$MCP_PID" 2>/dev/null || true
357
- fi
358
354
  wait "$MCP_PID" 2>/dev/null || true
359
355
  fi
360
356
 
@@ -59,8 +59,7 @@ async function ensureInstalled(options) {
59
59
  const {
60
60
  pluginJsonSourcePath,
61
61
  installDir,
62
- mcpConfigPath,
63
- serverScriptPath
62
+ mcpConfigPath
64
63
  } = options;
65
64
  const pid = options.pidOverride ?? process.pid;
66
65
  let manifestContent;
@@ -94,7 +93,7 @@ async function ensureInstalled(options) {
94
93
  await crashRecoverySweep(installDir, logger);
95
94
  const currentHash = await tryReadCurrentHash(installDir);
96
95
  if (currentHash === expectedManifestHash) {
97
- await ensureMcpConfigEntry(mcpConfigPath, serverScriptPath, logger);
96
+ await ensureMcpConfigCourtesy(mcpConfigPath, logger);
98
97
  return { installed: false, upToDate: true, installDir, manifestHash: currentHash };
99
98
  }
100
99
  await stageAndInstall({
@@ -105,7 +104,7 @@ async function ensureInstalled(options) {
105
104
  pid,
106
105
  logger
107
106
  });
108
- await ensureMcpConfigEntry(mcpConfigPath, serverScriptPath, logger);
107
+ await ensureMcpConfigCourtesy(mcpConfigPath, logger);
109
108
  logger.info("codevibe-antigravity-plugin installed", {
110
109
  installDir,
111
110
  manifestHash: expectedManifestHash
@@ -200,7 +199,7 @@ async function crashRecoverySweep(installDir, logger) {
200
199
  await rmRecursive(dir);
201
200
  }
202
201
  }
203
- async function ensureMcpConfigEntry(mcpConfigPath, serverScriptPath, logger) {
202
+ async function ensureMcpConfigCourtesy(mcpConfigPath, logger) {
204
203
  try {
205
204
  await fs.promises.mkdir(path.dirname(mcpConfigPath), { recursive: true });
206
205
  } catch (err) {
@@ -215,14 +214,15 @@ async function ensureMcpConfigEntry(mcpConfigPath, serverScriptPath, logger) {
215
214
  const lockPath = `${mcpConfigPath}.lock`;
216
215
  const releaseLock = await acquireLock(lockPath, LOCK_ACQUIRE_TIMEOUT_MS, logger, "mcp-config");
217
216
  try {
218
- await doEnsureMcpConfigEntry(mcpConfigPath, serverScriptPath, logger);
217
+ await doEnsureMcpConfigCourtesy(mcpConfigPath, logger);
219
218
  } finally {
220
219
  await releaseLock();
221
220
  }
222
221
  }
223
- async function doEnsureMcpConfigEntry(mcpConfigPath, serverScriptPath, logger) {
222
+ async function doEnsureMcpConfigCourtesy(mcpConfigPath, logger) {
224
223
  let config = {};
225
224
  let existingMode = null;
225
+ let onDiskWasValid = false;
226
226
  try {
227
227
  const stat = await fs.promises.stat(mcpConfigPath);
228
228
  existingMode = stat.mode & 511;
@@ -232,6 +232,7 @@ async function doEnsureMcpConfigEntry(mcpConfigPath, serverScriptPath, logger) {
232
232
  const parsed = JSON.parse(raw);
233
233
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
234
234
  config = parsed;
235
+ onDiskWasValid = !!config.mcpServers && typeof config.mcpServers === "object" && !Array.isArray(config.mcpServers);
235
236
  } else {
236
237
  logger.warn("mcp_config.json root is not a plain object; overwriting", {
237
238
  mcpConfigPath
@@ -250,23 +251,18 @@ async function doEnsureMcpConfigEntry(mcpConfigPath, serverScriptPath, logger) {
250
251
  if (!config.mcpServers || typeof config.mcpServers !== "object" || Array.isArray(config.mcpServers)) {
251
252
  config.mcpServers = {};
252
253
  }
253
- const expected = {
254
- command: "node",
255
- args: [serverScriptPath]
256
- };
257
- const current = config.mcpServers[MCP_SERVERS_KEY];
258
- if (current?.env && typeof current.env === "object" && !Array.isArray(current.env)) {
259
- expected.env = current.env;
254
+ const hadStaleEntry = Object.prototype.hasOwnProperty.call(config.mcpServers, MCP_SERVERS_KEY);
255
+ if (hadStaleEntry) {
256
+ delete config.mcpServers[MCP_SERVERS_KEY];
260
257
  }
261
- const envEqual = !current?.env && !expected.env ? true : JSON.stringify(current?.env ?? null) === JSON.stringify(expected.env ?? null);
262
- const needsWrite = !current || current.command !== expected.command || !arraysEqual(current.args ?? [], expected.args) || !envEqual;
258
+ const needsWrite = hadStaleEntry || !onDiskWasValid;
263
259
  if (!needsWrite) return;
264
- config.mcpServers[MCP_SERVERS_KEY] = expected;
265
260
  const targetMode = existingMode ?? 420;
266
261
  const randSuffix = `${process.pid}.${Date.now()}.${crypto.randomBytes(6).toString("hex")}`;
267
262
  const tmpPath = `${mcpConfigPath}.tmp.${randSuffix}`;
268
263
  const tmpFd = await fs.promises.open(tmpPath, "wx", targetMode);
269
264
  try {
265
+ if (existingMode !== null) await tmpFd.chmod(existingMode);
270
266
  await tmpFd.writeFile(JSON.stringify(config, null, 2) + "\n", "utf8");
271
267
  await tmpFd.close();
272
268
  } catch (writeErr) {
@@ -289,10 +285,10 @@ async function doEnsureMcpConfigEntry(mcpConfigPath, serverScriptPath, logger) {
289
285
  }
290
286
  throw err;
291
287
  }
292
- logger.info("Updated mcp_config.json with codevibe-antigravity entry", {
293
- mcpConfigPath,
294
- serverScriptPath
295
- });
288
+ logger.info(
289
+ hadStaleEntry ? "Removed the stale codevibe-antigravity MCP server entry from mcp_config.json (the daemon is launched by the wrapper only)" : "Wrote the mcp_config.json courtesy object",
290
+ { mcpConfigPath, removedStaleEntry: hadStaleEntry }
291
+ );
296
292
  }
297
293
  function sha256Hex(s) {
298
294
  return crypto.createHash("sha256").update(s, "utf8").digest("hex");
@@ -425,11 +421,6 @@ async function rmRecursive(p) {
425
421
  } catch {
426
422
  }
427
423
  }
428
- function arraysEqual(a, b) {
429
- if (a.length !== b.length) return false;
430
- for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
431
- return true;
432
- }
433
424
  function defaultLogger() {
434
425
  return {
435
426
  info: () => {
package/dist/server.js CHANGED
@@ -33,9 +33,11 @@ __export(server_exports, {
33
33
  McpServer: () => McpServer,
34
34
  SessionNotFoundError: () => SessionNotFoundError,
35
35
  __testing: () => __testing,
36
+ classifyEntrypoint: () => classifyEntrypoint,
36
37
  generateLaunchSessionId: () => generateLaunchSessionId,
37
38
  getActiveConversationFromCliLog: () => getActiveConversationFromCliLog,
38
- parseMaybeJson: () => parseMaybeJson
39
+ parseMaybeJson: () => parseMaybeJson,
40
+ runForeignSpawnIdle: () => runForeignSpawnIdle
39
41
  });
40
42
  module.exports = __toCommonJS(server_exports);
41
43
  var crypto3 = __toESM(require("crypto"));
@@ -3828,8 +3830,51 @@ function getActiveConversationFromCliLog(cliLogPath) {
3828
3830
  return null;
3829
3831
  }
3830
3832
  }
3833
+ function classifyEntrypoint(args) {
3834
+ return args.runtimeDir ? "wrapper" : "foreign";
3835
+ }
3836
+ function runForeignSpawnIdle(io) {
3837
+ let done = false;
3838
+ const finish = () => {
3839
+ if (done) return;
3840
+ done = true;
3841
+ io.exit(0);
3842
+ };
3843
+ io.writeStderr(
3844
+ "codevibe-antigravity-plugin: started without --runtime-dir, so not by the codevibe-agy wrapper (an agy mcp_config.json entry from an earlier release?). Refusing to start the daemon: no session, no backend calls. Run `codevibe-agy` once to remove the stale entry. Idling until the spawner closes stdin.\n"
3845
+ );
3846
+ void io.beacon().catch(() => void 0);
3847
+ io.onSignal(finish);
3848
+ io.stdin.on("end", finish);
3849
+ io.stdin.on("close", finish);
3850
+ io.stdin.on("error", finish);
3851
+ io.stdin.resume();
3852
+ return () => {
3853
+ done = true;
3854
+ };
3855
+ }
3831
3856
  async function main() {
3832
3857
  const args = parseArgs(process.argv.slice(2));
3858
+ if (classifyEntrypoint(args) === "foreign") {
3859
+ runForeignSpawnIdle({
3860
+ stdin: process.stdin,
3861
+ writeStderr: (line) => {
3862
+ process.stderr.write(line);
3863
+ },
3864
+ onSignal: (cb) => {
3865
+ process.once("SIGINT", cb);
3866
+ process.once("SIGTERM", cb);
3867
+ },
3868
+ exit: (code) => process.exit(code),
3869
+ beacon: () => fireDaemonBeacon("daemon_init_step", {
3870
+ step: "foreign_spawn",
3871
+ outcome: "refused",
3872
+ has_wrapper_pid: process.env.CODEVIBE_AGY_WRAPPER_PID ? "yes" : "no",
3873
+ has_tmux_target: process.env.CODEVIBE_AGY_TMUX_TARGET ? "yes" : "no"
3874
+ })
3875
+ });
3876
+ return;
3877
+ }
3833
3878
  const bearerToken = process.env.CODEVIBE_AGY_MCP_TOKEN || args.token;
3834
3879
  if (!bearerToken) {
3835
3880
  console.error("codevibe-antigravity-plugin: bearer token required via $CODEVIBE_AGY_MCP_TOKEN or --token");
@@ -3965,7 +4010,9 @@ var __testing = {
3965
4010
  McpServer,
3966
4011
  SessionNotFoundError,
3967
4012
  __testing,
4013
+ classifyEntrypoint,
3968
4014
  generateLaunchSessionId,
3969
4015
  getActiveConversationFromCliLog,
3970
- parseMaybeJson
4016
+ parseMaybeJson,
4017
+ runForeignSpawnIdle
3971
4018
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quantiya/codevibe-antigravity-plugin",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
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
  "bin": {
@@ -18,8 +18,6 @@
18
18
  "build": "rm -rf dist && npm run typecheck && esbuild src/server.ts src/installer-cli.ts --bundle --platform=node --target=node18 --packages=external --outdir=dist",
19
19
  "prepack": "npm run build",
20
20
  "postpack": "echo '[codevibe-antigravity-plugin] postpack: noop'",
21
- "dev": "node dist/server.js",
22
- "start": "node dist/server.js",
23
21
  "test": "jest --config jest.config.js --forceExit"
24
22
  },
25
23
  "keywords": [