@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 +28 -0
- package/README.md +112 -0
- package/antigravity-plugin.json +11 -0
- package/bin/codevibe-agy +518 -0
- package/dist/installer-cli.js +478 -0
- package/dist/server.js +3558 -0
- package/package.json +71 -0
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
|
+
}
|
package/bin/codevibe-agy
ADDED
|
@@ -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.
|