@quantiya/codevibe-antigravity-plugin 1.0.0

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/.env.example ADDED
@@ -0,0 +1,28 @@
1
+ # Gemini Companion Plugin Configuration
2
+ # ======================================
3
+ # NOTE: You typically don't need to edit this file manually.
4
+ # Configuration is auto-set after running 'codevibe-gemini login'.
5
+ #
6
+ # Copy this file to .env.production or .env.development and fill in values.
7
+
8
+ # Server Configuration
9
+ PORT=3456
10
+ HOST=localhost
11
+
12
+ # AWS Configuration
13
+ # These values are provided by the CodeVibe service
14
+ AWS_REGION=us-east-1
15
+ APPSYNC_URL=https://your-appsync-endpoint.appsync-api.us-east-1.amazonaws.com/graphql
16
+
17
+ # Cognito Configuration (for OAuth authentication)
18
+ COGNITO_USER_POOL_ID=us-east-1_xxxxxxxxx
19
+ COGNITO_CLIENT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxx
20
+ COGNITO_DOMAIN=your-domain.auth.us-east-1.amazoncognito.com
21
+
22
+ # Gemini Configuration
23
+ GEMINI_COMMAND=gemini
24
+ GEMINI_TIMEOUT=60000
25
+
26
+ # Logging Configuration
27
+ LOG_FILE=/tmp/codevibe-gemini-mcp.log
28
+ LOG_LEVEL=info
package/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # CodeVibe for Antigravity CLI
2
+
3
+ **Control Google Antigravity CLI from your iPhone and Android.** See your agent's work, approve file edits, dictate prompts by voice โ€” all from your phone, anywhere you are.
4
+
5
+ ๐ŸŒ **[quantiya.ai/codevibe](https://quantiya.ai/codevibe)** โ€” landing page, demo video, and one-liner installer
6
+
7
+ ๐Ÿ“ฑ **[Download on the App Store](https://apps.apple.com/app/id6756500217)** ยท **[Get it on Google Play](https://play.google.com/store/apps/details?id=ai.quantiya.app.codevibe)**
8
+
9
+ ---
10
+
11
+ ## Why CodeVibe for Antigravity CLI
12
+
13
+ - **๐Ÿš€ Real-time sync** โ€” every prompt, response, tool call, and file edit shows up on your phone in 100โ€“500ms
14
+ - **โœ… Approve from anywhere** โ€” review full file diffs and approve or reject tool execution from your phone
15
+ - **โ“ Answer multi-choice questions** โ€” agy's question UI (e.g. "Which project should we focus on?") renders on mobile with the full option list
16
+ - **๐Ÿง  Reasoning visibility** โ€” see agy's thinking/reasoning alongside its output
17
+ - **๐ŸŽ™๏ธ Voice prompts** โ€” dictate your next prompt with speech-to-text
18
+ - **๐Ÿ“ท Image attachments** โ€” send screenshots and photos for agy to analyze
19
+ - **๐Ÿ”” Push notifications** โ€” get notified when your agent needs input
20
+ - **๐Ÿ”’ End-to-end encrypted** โ€” AES-256-GCM with ECDH key exchange
21
+ - **๐Ÿ”“ Locked screen support** โ€” works even when your computer screen is locked (via tmux)
22
+ - **๐Ÿง  Multi-agent ready** โ€” Antigravity sessions appear alongside Claude, Gemini, and Codex sessions in the same app
23
+
24
+ ## Install in 30 seconds
25
+
26
+ ```bash
27
+ curl -fsSL https://quantiya.ai/codevibe/install.sh | bash
28
+ ```
29
+
30
+ Download the **[iOS app](https://apps.apple.com/app/id6756500217)** or **[Android app](https://play.google.com/store/apps/details?id=ai.quantiya.app.codevibe)**, sign in with the same Apple or Google account, and run:
31
+
32
+ ```bash
33
+ codevibe-agy
34
+ ```
35
+
36
+ Your session appears on your phone automatically.
37
+
38
+ ### Manual install
39
+
40
+ ```bash
41
+ npm install -g @quantiya/codevibe
42
+ codevibe login
43
+ codevibe-agy
44
+ ```
45
+
46
+ ## Requirements
47
+
48
+ - **macOS, Linux, or WSL Ubuntu** โ€” Windows without WSL is not supported
49
+ - **Node.js** 18.0.0+
50
+ - **tmux** โ€” `brew install tmux` on macOS, `apt install tmux` on Linux/WSL
51
+ - **Antigravity CLI (`agy`)** โ€” install from [Google's documentation](https://antigravity.google/docs)
52
+
53
+ > Antigravity CLI is distributed by Google as a binary; it is not on npm. After installing `agy`, run `codevibe-agy` and the plugin handles the rest.
54
+
55
+ ## How it works
56
+
57
+ Antigravity CLI writes session transcripts to `~/.gemini/antigravity-cli/brain/<uuid>/.system_generated/logs/transcript.jsonl`. CodeVibe watches those files with chokidar, parses every line of the JSONL stream, and pushes events through E2E-encrypted AWS AppSync to your phone.
58
+
59
+ Approval prompts and multi-choice questions aren't in the transcript at the moment they appear โ€” agy renders them directly into the terminal. CodeVibe observes the live tmux pane to detect both UI variants, parses the option labels, and surfaces them to mobile. Your mobile reply is delivered back via `tmux send-keys`.
60
+
61
+ A single launch session covers the entire wrapper lifetime, so `/resume` commands continue under the same backend session and your phone sees one continuous conversation.
62
+
63
+ ## What gets synced
64
+
65
+ | Direction | What |
66
+ |---|---|
67
+ | **Desktop โ†’ Mobile** | User prompts, assistant responses, agent reasoning, shell commands, file edits, tool outputs, approval prompts ("Requesting permission for: โ€ฆ"), question prompts ("Question N/M: โ€ฆ"), images |
68
+ | **Mobile โ†’ Desktop** | Text prompts, approval responses, and question answers โ€” executed via tmux |
69
+
70
+ ## CLI commands
71
+
72
+ ```bash
73
+ codevibe-agy # Start agy with mobile sync
74
+ codevibe-agy login # Authenticate via browser OAuth
75
+ codevibe-agy status # Check auth status
76
+ codevibe-agy logout # Sign out
77
+ ```
78
+
79
+ ## Troubleshooting
80
+
81
+ ```bash
82
+ # Check server logs
83
+ tail -f /var/folders/**/T/codevibe-agy-mcp.log # macOS
84
+ tail -f /tmp/codevibe-agy-mcp.log # Linux
85
+
86
+ # Verify agy transcripts exist
87
+ ls -la ~/.gemini/antigravity-cli/brain/
88
+
89
+ # Verify tmux session
90
+ tmux list-sessions | grep codevibe-agy
91
+
92
+ # Reinstall if needed
93
+ codevibe update
94
+ ```
95
+
96
+ ## Support
97
+
98
+ - **Email:** support@quantiya.ai
99
+ - **Landing page:** [quantiya.ai/codevibe](https://quantiya.ai/codevibe)
100
+ - **Privacy policy:** [quantiya.ai/privacy](https://quantiya.ai/privacy)
101
+
102
+ ## Part of the CodeVibe family
103
+
104
+ - **[@quantiya/codevibe](https://www.npmjs.com/package/@quantiya/codevibe)** โ€” meta-package (install this, not this plugin directly)
105
+ - **[@quantiya/codevibe-core](https://www.npmjs.com/package/@quantiya/codevibe-core)** โ€” shared library used by all plugins
106
+ - **[@quantiya/codevibe-claude-plugin](https://www.npmjs.com/package/@quantiya/codevibe-claude-plugin)** โ€” Claude Code support
107
+ - **[@quantiya/codevibe-gemini-plugin](https://www.npmjs.com/package/@quantiya/codevibe-gemini-plugin)** โ€” Gemini CLI support
108
+ - **[@quantiya/codevibe-codex-plugin](https://www.npmjs.com/package/@quantiya/codevibe-codex-plugin)** โ€” Codex CLI support
109
+
110
+ ## License
111
+
112
+ MIT
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "codevibe-antigravity",
3
+ "version": "1.0.0",
4
+ "description": "Mobile companion for Antigravity CLI โ€” monitor and control your sessions from your phone via the CodeVibe iOS / Android apps",
5
+ "agent": "antigravity",
6
+ "implementation": {
7
+ "architecture": "transcript+tmux+approval-heuristic",
8
+ "schema_version": "v1.0",
9
+ "note": "v1.0 ships without JSON hooks (agy backend gate `enable_json_hooks` is OFF for our account). Monitors `~/.gemini/antigravity-cli/brain/<UUID>/.system_generated/logs/transcript.jsonl` + mirrors the tmux pane for approval-UI detection. v1.1 will add hook handlers when the backend gate flips."
10
+ }
11
+ }
@@ -0,0 +1,518 @@
1
+ #!/bin/bash
2
+ #
3
+ # codevibe-agy โ€” wrapper that runs Antigravity CLI (agy) inside a tmux
4
+ # session bridged to the CodeVibe MCP server for mobile sync.
5
+ #
6
+ # Architecture (DESIGN.md ยง3 + ยง5.2):
7
+ #
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.
11
+ #
12
+ # 2. Generate ephemeral port + bearer token; create runtime dir at
13
+ # ~/.gemini/antigravity-cli/plugins/codevibe-antigravity/runtime/$$
14
+ # with mode 0700.
15
+ #
16
+ # 3. Create tmux session `codevibe-agy-$$` FIRST. Inside the tmux
17
+ # session, a holding loop polls `<runtime-dir>/conn.json` (the
18
+ # MCP server's atomic post-listen writepoint) for up to 10s, then
19
+ # execs agy. This ordering means TmuxPaneObserver.start() can
20
+ # always `tmux pipe-pane -t $SESSION_NAME` successfully.
21
+ #
22
+ # 4. Spawn MCP server (`node dist/server.js --port ... --runtime-dir ...`)
23
+ # in the background. Bearer token via CODEVIBE_AGY_MCP_TOKEN env
24
+ # (NOT --token argv โ€” keeps it out of `ps -ef`). Server's
25
+ # HttpApi.writePortFile writes conn.json atomically once listen()
26
+ # resolves, which unblocks the inner script's holding loop.
27
+ # Also exports CODEVIBE_AGY_MCP_URL / CODEVIBE_AGY_TMUX_TARGET /
28
+ # CODEVIBE_AGY_WRAPPER_PID.
29
+ #
30
+ # 5. Attach user to the tmux session. Poll `tmux has-session` after
31
+ # attach returns (handles Ctrl+B d detach without killing MCP).
32
+ #
33
+ # 6. EXIT trap: mark backend session INACTIVE (delegated to MCP
34
+ # server's signal handler), kill MCP server, remove runtime dir.
35
+ #
36
+ # Usage:
37
+ # codevibe-agy [agy-args...] # Start agy session
38
+ # codevibe-agy login # Sign in via browser
39
+ # codevibe-agy logout # Sign out
40
+ # codevibe-agy status # Show auth status
41
+ # codevibe-agy reset-device # Reset device key (re-pair)
42
+ #
43
+ # Environment:
44
+ # ENVIRONMENT Set to 'production' (default) or 'development'
45
+ # AGY_PATH Path to agy binary (default: $(command -v agy))
46
+ #
47
+ # v1.0 limitations:
48
+ # - Requires tmux. Print mode (`--print` / `-p`) is NOT supported.
49
+ # - Requires Node.js โ‰ฅ 18 for the MCP server.
50
+ #
51
+
52
+ set -e
53
+
54
+ # Whitelist ENVIRONMENT so a malicious env value can't break out of the
55
+ # single-quoted shell context where it's later interpolated into the tmux
56
+ # inner command. Only `production` and `development` are legitimate; any
57
+ # other value is silently rejected back to `production`. (Stage 1 HIGH
58
+ # finding 2026-05-20.)
59
+ case "${ENVIRONMENT:-}" in
60
+ production|development) export ENVIRONMENT="${ENVIRONMENT}" ;;
61
+ *) export ENVIRONMENT="production" ;;
62
+ esac
63
+ CODEVIBE_TMPDIR="${TMPDIR:-/tmp}"
64
+
65
+ # Resolve plugin dir via symlink-aware traversal (npm globals symlink
66
+ # bin/ into /usr/local/bin/).
67
+ SOURCE="${BASH_SOURCE[0]}"
68
+ while [ -L "$SOURCE" ]; do
69
+ DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
70
+ SOURCE="$(readlink "$SOURCE")"
71
+ [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE"
72
+ done
73
+ SCRIPT_DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
74
+ PLUGIN_DIR="$(dirname "$SCRIPT_DIR")"
75
+
76
+ # โ”€โ”€โ”€ PATH augmentation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
77
+ # When the install one-liner runs in a fresh terminal, Homebrew's
78
+ # installer writes the shellenv eval into ~/.zprofile (etc.) but the
79
+ # user's current shell hasn't sourced it yet. Subsequent codevibe-*
80
+ # runs in that same terminal then fail tmux/node/agy discovery because
81
+ # /opt/homebrew/bin isn't on PATH. Prepend common locations so the
82
+ # wrapper recovers without forcing a new terminal.
83
+ _CV_NEW_PATHS=""
84
+ for _CV_DIR in /opt/homebrew/bin /opt/homebrew/sbin /usr/local/bin /usr/local/sbin /opt/local/bin /usr/bin /bin; do
85
+ case ":$PATH:" in
86
+ *":$_CV_DIR:"*) ;;
87
+ *) [ -d "$_CV_DIR" ] && _CV_NEW_PATHS="$_CV_NEW_PATHS:$_CV_DIR" ;;
88
+ esac
89
+ done
90
+ [ -n "$_CV_NEW_PATHS" ] && export PATH="${_CV_NEW_PATHS#:}${PATH:+:$PATH}"
91
+ unset _CV_DIR _CV_NEW_PATHS
92
+
93
+ # โ”€โ”€โ”€ Wrapper telemetry (GA4 Measurement Protocol) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
94
+ # Diagnoses agent CLI failures: pre-flight bailouts, fast-die patterns,
95
+ # exit code. Background curl, fail silently, no PII (hashed hostname +
96
+ # per-run random id only). Honors CODEVIBE_TELEMETRY_SOURCE=test.
97
+ _CV_MID="G-GS74YEQTB8"
98
+ _CV_SEC="lAfOF6OxRzSQ-NsLBRjhAg"
99
+ _CV_CID="$(echo "$(uname -n)-$(id -u)" | (sha256sum 2>/dev/null || shasum -a 256 2>/dev/null || echo "anonymous-fallback ") | cut -c1-36)"
100
+ _CV_RUN_ID="$(head -c 16 /dev/urandom 2>/dev/null | od -An -tx1 | tr -d ' \n' | cut -c1-32)"
101
+ [ -z "$_CV_RUN_ID" ] && _CV_RUN_ID="fallback-$(date +%s)-$$"
102
+ _CV_AGENT="antigravity"
103
+ _CV_SOURCE="${CODEVIBE_TELEMETRY_SOURCE:-production}"
104
+ _CV_STARTED_AT="$(date +%s)"
105
+ _CV_EXITED=""
106
+ _CV_PLUGIN_VERSION="$(node -p "require('$PLUGIN_DIR/package.json').version" 2>/dev/null || echo unknown)"
107
+ _CV_MCP_LOG="${CODEVIBE_TMPDIR}/codevibe-agy-mcp.log"
108
+ _CV_TMUX_STARTED="false"
109
+ _CV_AGENT_INVOKED="false"
110
+ _CV_AGENT_STARTED_AT=0
111
+ _CV_AGY_EXIT_FILE="${CODEVIBE_TMPDIR}/codevibe-agy-exit-$$"
112
+
113
+ # Sanitize values that go into the GA4 JSON payload so a CLI emitting
114
+ # ANSI escapes or quotes in `--version` can't break the hand-built JSON.
115
+ # Output capped at 40 chars. Caller checks emptiness afterward.
116
+ cv_sanitize() {
117
+ printf '%s' "$1" | LC_ALL=C tr -cd 'A-Za-z0-9._\- ' | cut -c1-40
118
+ }
119
+ _CV_PLUGIN_VERSION="$(cv_sanitize "$_CV_PLUGIN_VERSION")"
120
+ [ -z "$_CV_PLUGIN_VERSION" ] && _CV_PLUGIN_VERSION="unknown"
121
+ _CV_SOURCE="$(cv_sanitize "$_CV_SOURCE")"
122
+ [ -z "$_CV_SOURCE" ] && _CV_SOURCE="production"
123
+
124
+ cv_telem() {
125
+ local event="$1"; shift
126
+ local params="$*"
127
+ curl -s -X POST \
128
+ "https://www.google-analytics.com/mp/collect?measurement_id=${_CV_MID}&api_secret=${_CV_SEC}" \
129
+ -H "Content-Type: application/json" \
130
+ -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}}}]}" \
131
+ </dev/null >/dev/null 2>&1 &
132
+ }
133
+
134
+ cv_failed() {
135
+ [ -n "$_CV_EXITED" ] && return 0
136
+ _CV_EXITED="failed"
137
+ cv_telem "wrapper_failed" "\"reason\":\"$1\",\"lifetime_seconds\":$(( $(date +%s) - _CV_STARTED_AT ))"
138
+ }
139
+
140
+ # โ”€โ”€โ”€ Auth commands: delegate to shared codevibe-core CLI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
141
+ case "$1" in
142
+ login|logout|status|reset-device)
143
+ CORE_CLI="$PLUGIN_DIR/node_modules/@quantiya/codevibe-core/bin/codevibe.js"
144
+ # Also check hoisted location (when installed via @quantiya/codevibe meta-package).
145
+ if [ ! -f "$CORE_CLI" ]; then
146
+ CORE_CLI="$PLUGIN_DIR/../codevibe-core/bin/codevibe.js"
147
+ fi
148
+ if [ -f "$CORE_CLI" ]; then
149
+ cv_telem "wrapper_started" "\"invocation\":\"auth_$1\",\"os\":\"$(uname -s | cv_sanitize)\",\"arch\":\"$(uname -m | cv_sanitize)\""
150
+ exec node "$CORE_CLI" "$1"
151
+ else
152
+ echo "Error: codevibe-core not found. Try reinstalling: npm install -g @quantiya/codevibe"
153
+ cv_failed "core_not_found"
154
+ sleep 1
155
+ exit 1
156
+ fi
157
+ ;;
158
+ esac
159
+
160
+ # โ”€โ”€โ”€ Environment probes โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
161
+ _CV_NODE_VER="missing"
162
+ command -v node >/dev/null 2>&1 && _CV_NODE_VER="$(node -v 2>/dev/null | cv_sanitize)"
163
+ [ -z "$_CV_NODE_VER" ] && _CV_NODE_VER="unknown"
164
+ _CV_TMUX_VER="missing"
165
+ command -v tmux >/dev/null 2>&1 && _CV_TMUX_VER="$(tmux -V 2>/dev/null | cv_sanitize)"
166
+ [ -z "$_CV_TMUX_VER" ] && _CV_TMUX_VER="unknown"
167
+ _CV_AGY_BIN="${AGY_PATH:-$(command -v agy 2>/dev/null || true)}"
168
+ _CV_AGY_VER="missing"
169
+ [ -n "$_CV_AGY_BIN" ] && _CV_AGY_VER="$("$_CV_AGY_BIN" --version 2>/dev/null | cv_sanitize)"
170
+ [ -z "$_CV_AGY_VER" ] && _CV_AGY_VER="unknown"
171
+ _CV_OS_VER="$(uname -s | cv_sanitize)"
172
+ [ -z "$_CV_OS_VER" ] && _CV_OS_VER="unknown"
173
+ _CV_ARCH_VER="$(uname -m | cv_sanitize)"
174
+ [ -z "$_CV_ARCH_VER" ] && _CV_ARCH_VER="unknown"
175
+ _CV_INSIDE_TMUX="false"; [ -n "$TMUX" ] && _CV_INSIDE_TMUX="true"
176
+ _CV_IS_TTY="false"; { [ -t 0 ] && [ -t 1 ]; } && _CV_IS_TTY="true"
177
+ cv_telem "wrapper_started" "\"invocation\":\"session\",\"os\":\"$_CV_OS_VER\",\"arch\":\"$_CV_ARCH_VER\",\"agy_version\":\"$_CV_AGY_VER\",\"node_version\":\"$_CV_NODE_VER\",\"tmux_version\":\"$_CV_TMUX_VER\",\"inside_tmux\":$_CV_INSIDE_TMUX,\"is_terminal\":$_CV_IS_TTY"
178
+
179
+ LOG_FILE="${CODEVIBE_TMPDIR}/codevibe-agy-wrapper.log"
180
+ MCP_LOG_FILE="${CODEVIBE_TMPDIR}/codevibe-agy-mcp.log"
181
+
182
+ log() {
183
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"
184
+ }
185
+
186
+ # โ”€โ”€โ”€ Reject unsupported invocations โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
187
+ # `--print` / `-p` mode bypasses the interactive TUI we depend on. Bail
188
+ # out with a clear message rather than silently producing no mobile sync.
189
+ # (DESIGN.md ยง5.2.)
190
+ for arg in "$@"; do
191
+ case "$arg" in
192
+ --print|-p|--prompt|--print=*|--prompt=*|-p=*)
193
+ echo "Error: codevibe-agy does not support --print / -p / --prompt in v1.0."
194
+ echo "Run \"agy ...\" directly for non-interactive use; mobile sync is interactive-only."
195
+ cv_failed "print_mode_unsupported"
196
+ sleep 1
197
+ exit 1
198
+ ;;
199
+ esac
200
+ done
201
+
202
+ # โ”€โ”€โ”€ Pre-flight checks โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
203
+ if ! command -v tmux &> /dev/null; then
204
+ echo "Error: tmux is required but not installed."
205
+ echo "Install with: brew install tmux (macOS) or apt-get install tmux (Debian/Ubuntu)"
206
+ cv_failed "tmux_missing"
207
+ sleep 1
208
+ exit 1
209
+ fi
210
+
211
+ if [ -z "$_CV_AGY_BIN" ] || ! [ -x "$_CV_AGY_BIN" ]; then
212
+ echo "Error: Antigravity CLI (\`agy\`) is not installed or not executable."
213
+ echo "Install instructions: https://antigravity.google/docs"
214
+ cv_failed "agy_missing"
215
+ sleep 1
216
+ exit 1
217
+ fi
218
+
219
+ if ! command -v node &> /dev/null; then
220
+ echo "Error: Node.js is required but not installed."
221
+ echo "Install Node.js 18+ from: https://nodejs.org"
222
+ cv_failed "node_missing"
223
+ sleep 1
224
+ exit 1
225
+ fi
226
+
227
+ if [ ! -f "$PLUGIN_DIR/dist/server.js" ]; then
228
+ echo "Error: MCP server not built. Run 'npm run build' in the plugin directory first."
229
+ cv_failed "server_not_built"
230
+ sleep 1
231
+ exit 1
232
+ fi
233
+
234
+ if [ ! -f "$PLUGIN_DIR/dist/installer-cli.js" ]; then
235
+ echo "Error: Installer CLI not built. Run 'npm run build' in the plugin directory first."
236
+ cv_failed "installer_not_built"
237
+ sleep 1
238
+ exit 1
239
+ fi
240
+
241
+ # โ”€โ”€โ”€ Atomic install of plugin footprint (DESIGN.md ยง5.1) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
242
+ # 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.
246
+ log "Running ensureInstalled"
247
+ if ! node "$PLUGIN_DIR/dist/installer-cli.js"; then
248
+ echo "Error: codevibe-antigravity installer failed."
249
+ cv_failed "installer_failed"
250
+ sleep 1
251
+ exit 1
252
+ fi
253
+
254
+ # โ”€โ”€โ”€ Token + port + runtime dir (DESIGN.md ยง4.5) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
255
+ WRAPPER_PID="$$"
256
+ SESSION_NAME="codevibe-agy-${WRAPPER_PID}"
257
+
258
+ # Bearer token: 48-char hex (24 bytes / 192 bits entropy) from
259
+ # /dev/urandom, fallback to date+pid. Passed to the MCP server via the
260
+ # CODEVIBE_AGY_MCP_TOKEN env var (NOT command-line argv) so it's not
261
+ # visible to other local processes via `ps -ef` / /proc/<pid>/cmdline.
262
+ # (Stage 1 HIGH finding 2026-05-20.)
263
+ BEARER_TOKEN="$(head -c 24 /dev/urandom 2>/dev/null | od -An -tx1 | tr -d ' \n' | cut -c1-48)"
264
+ [ -z "$BEARER_TOKEN" ] && BEARER_TOKEN="fallback-$(date +%s%N | cv_sanitize)-${WRAPPER_PID}"
265
+
266
+ # Ephemeral port: ask Node to pick a free one (binds + closes). This is
267
+ # inherently TOCTOU (port may be claimed between pick + bind by the MCP
268
+ # server). If that race fires, the MCP server dies on bind; we detect
269
+ # via the post-spawn `kill -0` check below and surface the tail of
270
+ # $MCP_LOG_FILE for diagnostics. Node prints the port to stdout for
271
+ # capture here.
272
+ MCP_PORT="$(node -e '
273
+ const net = require("net");
274
+ const srv = net.createServer();
275
+ srv.unref();
276
+ srv.listen(0, "127.0.0.1", () => {
277
+ const a = srv.address();
278
+ if (a && typeof a === "object") console.log(a.port);
279
+ srv.close();
280
+ });
281
+ ' 2>/dev/null)"
282
+ if ! [[ "$MCP_PORT" =~ ^[0-9]+$ ]]; then
283
+ echo "Error: failed to pick an ephemeral port for the MCP server."
284
+ cv_failed "port_pick_failed"
285
+ sleep 1
286
+ exit 1
287
+ fi
288
+
289
+ # Runtime dir at ~/.gemini/antigravity-cli/plugins/codevibe-antigravity/runtime/$$
290
+ # with mode 0700. mkdir -p first, then chmod (umask may have made it 0755).
291
+ RUNTIME_BASE="$HOME/.gemini/antigravity-cli/plugins/codevibe-antigravity/runtime"
292
+ RUNTIME_DIR="${RUNTIME_BASE}/${WRAPPER_PID}"
293
+ mkdir -p "$RUNTIME_DIR"
294
+ chmod 0700 "$RUNTIME_DIR" 2>/dev/null || true
295
+ # Also tighten the runtime base โ€” best-effort, no fatal on failure.
296
+ chmod 0700 "$RUNTIME_BASE" 2>/dev/null || true
297
+
298
+ log "Wrapper PID=$WRAPPER_PID; token=$(printf '%s' "$BEARER_TOKEN" | cut -c1-8)...; port=$MCP_PORT; runtime=$RUNTIME_DIR"
299
+
300
+ # Export env vars consumed by the MCP server + any in-tree tooling.
301
+ export CODEVIBE_AGY_MCP_TOKEN="$BEARER_TOKEN"
302
+ export CODEVIBE_AGY_MCP_URL="http://127.0.0.1:${MCP_PORT}"
303
+ export CODEVIBE_AGY_TMUX_TARGET="$SESSION_NAME"
304
+ export CODEVIBE_AGY_WRAPPER_PID="$WRAPPER_PID"
305
+
306
+ # โ”€โ”€โ”€ Cleanup trap โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
307
+ cleanup() {
308
+ local wrapper_exit_code=$?
309
+ log "Cleanup triggered"
310
+
311
+ # Fire wrapper_exited telemetry BEFORE killing the server so MCP
312
+ # logs are intact. cv_failed sets _CV_EXITED on pre-flight failures,
313
+ # so this block won't double-fire.
314
+ if [ -z "$_CV_EXITED" ]; then
315
+ _CV_EXITED="exited"
316
+ local agy_exit="unknown"
317
+ if [ -f "$_CV_AGY_EXIT_FILE" ]; then
318
+ agy_exit="$(cat "$_CV_AGY_EXIT_FILE" 2>/dev/null | head -c 10 | tr -d '\n\r ')"
319
+ [ -z "$agy_exit" ] && agy_exit="unknown"
320
+ fi
321
+ local lifetime=$(( $(date +%s) - _CV_STARTED_AT ))
322
+ local agy_lifetime=0
323
+ if [ "$_CV_AGENT_STARTED_AT" -gt 0 ] 2>/dev/null; then
324
+ agy_lifetime=$(( $(date +%s) - _CV_AGENT_STARTED_AT ))
325
+ fi
326
+ local outcome
327
+ if [ "$wrapper_exit_code" = "130" ] || [ "$wrapper_exit_code" = "143" ]; then
328
+ outcome="interrupted"
329
+ elif [ "$_CV_AGENT_INVOKED" = "false" ]; then
330
+ outcome="pre_invoke_abort"
331
+ elif [ "$agy_exit" != "unknown" ] && [ "$agy_exit" != "0" ]; then
332
+ outcome="error_exit"
333
+ elif [ "$agy_lifetime" -lt 5 ] 2>/dev/null; then
334
+ outcome="early_exit"
335
+ elif [ "$agy_lifetime" -lt 60 ] 2>/dev/null; then
336
+ outcome="clean_short"
337
+ else
338
+ outcome="clean_long"
339
+ fi
340
+ 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
+ fi
342
+
343
+ # Stop the MCP server โ€” graceful first, then SIGKILL fallback.
344
+ # The server's SIGTERM handler marks the session INACTIVE.
345
+ if [ -n "$MCP_PID" ] && kill -0 "$MCP_PID" 2>/dev/null; then
346
+ log "Stopping MCP server (PID: $MCP_PID)"
347
+ 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
+ wait "$MCP_PID" 2>/dev/null || true
359
+ fi
360
+
361
+ # Best-effort cleanup of per-wrapper runtime dir.
362
+ if [ -n "$RUNTIME_DIR" ] && [ -d "$RUNTIME_DIR" ]; then
363
+ rm -rf "$RUNTIME_DIR" 2>/dev/null || true
364
+ fi
365
+ # PID + exit files.
366
+ rm -f "${CODEVIBE_TMPDIR}/codevibe-agy-mcp-${WRAPPER_PID}.pid"
367
+ rm -f "$_CV_AGY_EXIT_FILE"
368
+ }
369
+ trap cleanup EXIT INT TERM
370
+
371
+ # โ”€โ”€โ”€ Direct-run paths (already-inside-tmux + non-TTY) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
372
+ # In these cases we skip the MCP-server + tmux orchestration and just
373
+ # run agy directly. Mobile sync is not available but the user can still
374
+ # use agy.
375
+
376
+ if [ -n "$TMUX" ]; then
377
+ log "Already inside tmux โ€” running agy directly (no mobile sync)"
378
+ _CV_AGENT_INVOKED="true"
379
+ _CV_AGENT_STARTED_AT="$(date +%s)"
380
+ _CV_RC=0
381
+ "$_CV_AGY_BIN" "$@" || _CV_RC=$?
382
+ printf '%s' "$_CV_RC" > "$_CV_AGY_EXIT_FILE" 2>/dev/null || true
383
+ exit "$_CV_RC"
384
+ fi
385
+
386
+ if [ ! -t 0 ] || [ ! -t 1 ]; then
387
+ log "Not running in a terminal โ€” running agy directly (no mobile sync)"
388
+ _CV_AGENT_INVOKED="true"
389
+ _CV_AGENT_STARTED_AT="$(date +%s)"
390
+ _CV_RC=0
391
+ "$_CV_AGY_BIN" "$@" || _CV_RC=$?
392
+ printf '%s' "$_CV_RC" > "$_CV_AGY_EXIT_FILE" 2>/dev/null || true
393
+ exit "$_CV_RC"
394
+ fi
395
+
396
+ # โ”€โ”€โ”€ tmux session + agy exec โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
397
+ # Ordering: tmux session is created BEFORE the MCP server spawn so that
398
+ # the server's TmuxPaneObserver can successfully `tmux pipe-pane -t
399
+ # $SESSION_NAME` on startup. Previously, MCP spawned first and could
400
+ # die on a `pipe-pane: can't find session` error if the wrapper hadn't
401
+ # yet created the session. (Codex Stage 2 HIGH finding 2026-05-20.)
402
+ #
403
+ # Inner-script readiness wait: agy itself doesn't launch until the
404
+ # MCP server's conn.json exists (signal that auth + HTTP bind both
405
+ # succeeded). The MCP server writes conn.json from HttpApi.start()
406
+ # atomically after listen() returns, so its presence is a reliable
407
+ # "ready" indicator. We poll every 100ms up to 10s; on timeout we run
408
+ # agy anyway (degraded mode โ€” no mobile sync) so the user isn't stuck
409
+ # at a blank pane. (Codex Stage 2 HIGH finding 2026-05-20.)
410
+ log "Creating tmux session: $SESSION_NAME"
411
+
412
+ # Single-quote-escape every user-controllable scalar that gets
413
+ # interpolated into the tmux inner single-quoted context. Apply the
414
+ # standard 'foo'\''bar' trick so an AGY_PATH or TMPDIR (both user-
415
+ # settable env vars) containing a single quote can't break out of the
416
+ # quoted context. (Stage 1 HIGH 2026-05-20.)
417
+ escape_sq() { printf '%s' "$1" | sed "s/'/'\\\\''/g"; }
418
+
419
+ escaped_bin="$(escape_sq "$_CV_AGY_BIN")"
420
+ escaped_exit_file="$(escape_sq "$_CV_AGY_EXIT_FILE")"
421
+ escaped_conn_file="$(escape_sq "$RUNTIME_DIR/conn.json")"
422
+ escaped_mcp_log="$(escape_sq "$MCP_LOG_FILE")"
423
+ AGY_CMD="'$escaped_bin'"
424
+ for arg in "$@"; do
425
+ escaped_arg="$(escape_sq "$arg")"
426
+ AGY_CMD="$AGY_CMD '$escaped_arg'"
427
+ done
428
+
429
+ # Inner script: wait for MCP readiness โ†’ run agy โ†’ record exit code.
430
+ # tmux inherits our exported env (CODEVIBE_AGY_MCP_TOKEN etc.) so no
431
+ # secrets in the inner argv.
432
+ INNER_SCRIPT="
433
+ i=0
434
+ while [ \$i -lt 100 ] && [ ! -f '$escaped_conn_file' ]; do
435
+ sleep 0.1
436
+ i=\$((i + 1))
437
+ done
438
+ if [ ! -f '$escaped_conn_file' ]; then
439
+ echo 'codevibe-agy: MCP server not ready after 10s; running agy without mobile sync.' >&2
440
+ echo 'codevibe-agy: see $escaped_mcp_log for details.' >&2
441
+ fi
442
+ export ENVIRONMENT='$ENVIRONMENT'
443
+ $AGY_CMD
444
+ printf '%s' \"\$?\" > '$escaped_exit_file'
445
+ exit"
446
+
447
+ tmux new-session -d -s "$SESSION_NAME" -x "$(tput cols)" -y "$(tput lines)" "$INNER_SCRIPT"
448
+ _CV_TMUX_STARTED="true"
449
+ _CV_AGENT_INVOKED="true"
450
+ _CV_AGENT_STARTED_AT="$(date +%s)"
451
+
452
+ # Quality-of-life: mouse scrolling + clipboard integration. These are
453
+ # optional โ€” wrap in `|| true` so a failure on an exotic tmux build
454
+ # doesn't kill the wrapper (which would orphan agy + MCP). (Codex
455
+ # Stage 2 MED finding 2026-05-20.)
456
+ tmux set-option -t "$SESSION_NAME" -g mouse on 2>/dev/null || true
457
+ tmux set-option -t "$SESSION_NAME" set-clipboard on 2>/dev/null || true
458
+ tmux set-window-option -t "$SESSION_NAME" mode-keys vi 2>/dev/null || true
459
+ if command -v pbcopy >/dev/null 2>&1; then
460
+ CLIP_CMD="pbcopy"
461
+ elif grep -qi microsoft /proc/sys/kernel/osrelease 2>/dev/null && command -v clip.exe >/dev/null 2>&1; then
462
+ CLIP_CMD="clip.exe"
463
+ elif command -v wl-copy >/dev/null 2>&1; then
464
+ CLIP_CMD="wl-copy"
465
+ elif command -v xclip >/dev/null 2>&1; then
466
+ CLIP_CMD="xclip -selection clipboard"
467
+ fi
468
+ if [ -n "${CLIP_CMD:-}" ]; then
469
+ tmux bind-key -T copy-mode-vi y send-keys -X copy-pipe-and-cancel "$CLIP_CMD" 2>/dev/null || true
470
+ tmux bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "$CLIP_CMD" 2>/dev/null || true
471
+ fi
472
+
473
+ # โ”€โ”€โ”€ Spawn MCP server (background) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
474
+ # Token passed via $CODEVIBE_AGY_MCP_TOKEN env (already exported above),
475
+ # NOT via --token argv โ€” argv is visible to other local processes via
476
+ # `ps -ef` / /proc/<pid>/cmdline. (Stage 1 HIGH 2026-05-20.)
477
+ log "Starting MCP server on port $MCP_PORT"
478
+ node "$PLUGIN_DIR/dist/server.js" \
479
+ --port "$MCP_PORT" \
480
+ --runtime-dir "$RUNTIME_DIR" \
481
+ >> "$MCP_LOG_FILE" 2>&1 &
482
+ MCP_PID=$!
483
+ echo "$MCP_PID" > "${CODEVIBE_TMPDIR}/codevibe-agy-mcp-${WRAPPER_PID}.pid"
484
+ log "MCP server PID: $MCP_PID"
485
+
486
+ # Sanity check: did the process at least survive the spawn? If it died
487
+ # already, surface diagnostics. Note this is a coarse check โ€” the inner
488
+ # tmux script polls conn.json for the real readiness signal so a slow-
489
+ # but-eventually-up server is fine here. (Codex Stage 2 HIGH 2026-05-20.)
490
+ sleep 1
491
+ if ! kill -0 "$MCP_PID" 2>/dev/null; then
492
+ log "MCP server died on startup"
493
+ echo ""
494
+ tail -3 "$MCP_LOG_FILE" 2>/dev/null | grep -v '^\[' | head -3
495
+ echo ""
496
+ echo "Server failed to start. Check $MCP_LOG_FILE for details."
497
+ echo "Common causes: not signed in (\`codevibe-agy login\`), port in use, ENV mismatch."
498
+ # tmux session is already created โ€” destroy it so the user doesn't
499
+ # attach to a degraded session that will time-out waiting for conn.json.
500
+ tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true
501
+ cv_failed "server_died_on_startup"
502
+ sleep 1
503
+ exit 1
504
+ fi
505
+
506
+ log "Attaching to tmux session: $SESSION_NAME"
507
+
508
+ # Attach to the session. When the user detaches (Ctrl+B d) `attach-session`
509
+ # returns BUT the underlying session keeps running โ€” we then poll until
510
+ # the inner agy command finishes (session disappears) so MCP stays alive
511
+ # for the user to re-attach. Without this poll, detach would trigger the
512
+ # EXIT trap and kill MCP behind the user's back. (Codex Stage 2 MED 2026-05-20.)
513
+ tmux attach-session -t "$SESSION_NAME" || true
514
+ while tmux has-session -t "$SESSION_NAME" 2>/dev/null; do
515
+ sleep 1
516
+ done
517
+
518
+ # Session is gone โ€” agy exited. Cleanup is handled by the trap.