feat: smart ngrok detection + auto-tunnel in pair-agent

The pair-agent command now checks ngrok's native config (not just
~/.gstack/ngrok.env) and auto-starts the tunnel when ngrok is
available. The skill template walks users through ngrok install
and auth if not set up, instead of just printing a dead localhost
URL.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-04-04 23:59:39 -07:00
parent 376814c3f9
commit 7ed3b12854
3 changed files with 191 additions and 61 deletions
+105 -9
View File
@@ -447,6 +447,33 @@ async function sendCommand(state: ServerState, command: string, args: string[],
}
}
// ─── Ngrok Detection ───────────────────────────────────────────
/** Check if ngrok is installed and authenticated (native config or gstack env). */
function isNgrokAvailable(): boolean {
// Check gstack's own ngrok env
const ngrokEnvPath = path.join(process.env.HOME || '/tmp', '.gstack', 'ngrok.env');
if (fs.existsSync(ngrokEnvPath)) return true;
// Check NGROK_AUTHTOKEN env var
if (process.env.NGROK_AUTHTOKEN) return true;
// Check ngrok's native config (macOS + Linux)
const ngrokConfigs = [
path.join(process.env.HOME || '/tmp', 'Library', 'Application Support', 'ngrok', 'ngrok.yml'),
path.join(process.env.HOME || '/tmp', '.config', 'ngrok', 'ngrok.yml'),
path.join(process.env.HOME || '/tmp', '.ngrok2', 'ngrok.yml'),
];
for (const conf of ngrokConfigs) {
try {
const content = fs.readFileSync(conf, 'utf-8');
if (content.includes('authtoken:')) return true;
} catch {}
}
return false;
}
// ─── Pair-Agent DX ─────────────────────────────────────────────
interface InstructionBlockOptions {
@@ -586,16 +613,85 @@ async function handlePairAgent(state: ServerState, args: string[]): Promise<void
let serverUrl: string;
if (pairData.tunnel_url) {
serverUrl = pairData.tunnel_url;
} else {
// Check if ngrok is configured but tunnel isn't running
const ngrokEnvPath = path.join(process.env.HOME || '/tmp', '.gstack', 'ngrok.env');
if (fs.existsSync(ngrokEnvPath) && !localHost) {
console.warn('[browse] ngrok is configured but tunnel is not running.');
console.warn('[browse] Start the tunnel: BROWSE_TUNNEL=1 $B restart');
console.warn('[browse] Using localhost for now (same-machine only).\n');
} else if (!localHost) {
console.warn('[browse] No tunnel active. Instructions use localhost (same-machine only).\n');
} else if (!localHost) {
// No tunnel active. Check if ngrok is available and auto-start.
const ngrokAvailable = isNgrokAvailable();
if (ngrokAvailable) {
console.log('[browse] ngrok is available. Starting tunnel...');
// Restart server with tunnel enabled
try {
const restartResp = await fetch(`http://127.0.0.1:${state.port}/command`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${state.token}`,
},
body: JSON.stringify({ command: 'restart', args: [] }),
signal: AbortSignal.timeout(5000),
}).catch(() => null);
// Wait for server to come back, then restart with tunnel
await Bun.sleep(1000);
} catch {}
// Restart the server process with BROWSE_TUNNEL=1
console.log('[browse] Restarting server with tunnel...');
const serverScript = resolveServerScript();
const proc = Bun.spawn(['bun', 'run', serverScript], {
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, BROWSE_STATE_FILE: config.stateFile, BROWSE_TUNNEL: '1' },
});
proc.unref();
// Wait for server to come back with tunnel
const deadline = Date.now() + 15000;
let tunnelUrl: string | null = null;
while (Date.now() < deadline) {
await Bun.sleep(500);
const newState = readState();
if (newState && await isServerHealthy(newState.port)) {
try {
const healthResp = await fetch(`http://127.0.0.1:${newState.port}/health`, {
signal: AbortSignal.timeout(2000),
});
const health = await healthResp.json() as any;
if (health.tunnel?.url) {
tunnelUrl = health.tunnel.url;
// Update state for the rest of the function
state.port = newState.port;
state.token = newState.token;
break;
}
} catch {}
}
}
if (tunnelUrl) {
console.log(`[browse] Tunnel active: ${tunnelUrl}\n`);
serverUrl = tunnelUrl;
// Re-create setup key with the new server (old one used old root token)
const newPairResp = await fetch(`http://127.0.0.1:${state.port}/pair`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${state.token}`,
},
body: JSON.stringify({ clientId: clientName, admin }),
signal: AbortSignal.timeout(5000),
});
if (newPairResp.ok) {
const newData = await newPairResp.json() as typeof pairData;
pairData.setup_key = newData.setup_key;
pairData.expires_at = newData.expires_at;
pairData.scopes = newData.scopes;
}
} else {
console.warn('[browse] Failed to start tunnel. Using localhost (same-machine only).\n');
serverUrl = pairData.server_url;
}
} else {
console.warn('[browse] No tunnel active and ngrok is not installed/configured.');
console.warn('[browse] Instructions will use localhost (same-machine only).');
console.warn('[browse] For remote agents: install ngrok (https://ngrok.com) and run `ngrok config add-authtoken <TOKEN>`\n');
serverUrl = pairData.server_url;
}
} else {
serverUrl = pairData.server_url;
}
+43 -26
View File
@@ -606,9 +606,8 @@ Use AskUserQuestion:
> **Same machine** skips the copy-paste ceremony. Credentials are written directly to
> the agent's config directory. No tunnel needed.
>
> **Different machine** requires an ngrok tunnel so the remote agent can reach your
> browser over the internet. A setup key and instruction block are generated for
> copy-paste.
> **Different machine** generates a setup key and instruction block. If ngrok is
> installed, the tunnel starts automatically. If not, I'll walk you through setup.
>
> RECOMMENDATION: Choose A if the agent is local. It's instant, no copy-paste needed.
@@ -637,45 +636,63 @@ using the generic remote flow instead.
### If different machine (option B):
Check if a tunnel is running:
First, detect ngrok status:
```bash
$B pair-agent
which ngrok 2>/dev/null && echo "NGROK_INSTALLED" || echo "NGROK_NOT_INSTALLED"
ngrok config check 2>/dev/null && echo "NGROK_AUTHED" || echo "NGROK_NOT_AUTHED"
```
If the output shows "No tunnel active" and mentions ngrok:
**If ngrok is installed and authed:** Just run the command. The CLI will auto-detect
ngrok, start the tunnel, and print the instruction block with the tunnel URL:
Tell the user:
"Your browser server is localhost-only. For a remote agent to connect, you need
an ngrok tunnel. Here's how to set one up:
```bash
$B pair-agent --client TARGET_HOST
```
1. Sign up at ngrok.com (free tier works)
2. Copy your auth token
3. Save it: `echo 'NGROK_AUTHTOKEN=your_token_here' > ~/.gstack/ngrok.env`
4. Restart the server with tunnel: `BROWSE_TUNNEL=1 $B restart`
5. Run `/pair-agent` again
If the user also needs admin access (JS execution, cookies, storage):
If you just want to test locally, choose 'Same machine' instead."
STOP here. Wait for the user to set up ngrok and re-invoke.
If the tunnel IS active (or if the user is OK with localhost-only for same-network use),
the pair-agent command will print the instruction block. Show it to the user and tell them:
```bash
$B pair-agent --admin --client TARGET_HOST
```
Show the output to the user:
"Copy everything between the ═══ lines and paste it into your other agent's chat.
The agent will follow the instructions to connect. The setup key expires in 5 minutes."
### Admin access
**If ngrok is installed but NOT authed:** Walk the user through authentication:
If the user mentions needing JavaScript execution, cookie access, or storage access:
Tell the user:
"ngrok is installed but not logged in. Let's fix that:
1. Go to https://dashboard.ngrok.com/get-started/your-authtoken
2. Copy your auth token
3. Come back here and I'll run the auth command for you."
STOP here and wait for the user to provide their auth token.
When they provide it, run:
```bash
$B pair-agent --admin
ngrok config add-authtoken THEIR_TOKEN
```
Tell the user: "This gives the remote agent full admin access including JS execution,
cookie reading, and storage access. Only do this if you trust the agent and need
these capabilities."
Then retry `$B pair-agent --client TARGET_HOST`.
**If ngrok is NOT installed:** Walk the user through installation:
Tell the user:
"To connect a remote agent, we need ngrok (a tunnel that exposes your local
browser to the internet securely).
1. Go to https://ngrok.com and sign up (free tier works)
2. Install ngrok:
- macOS: `brew install ngrok`
- Linux: `snap install ngrok` or download from ngrok.com/download
3. Auth it: `ngrok config add-authtoken YOUR_TOKEN`
(get your token from https://dashboard.ngrok.com/get-started/your-authtoken)
4. Come back here and run `/pair-agent` again."
STOP here. Wait for the user to install ngrok and re-invoke.
## Step 5: Verify connection
+43 -26
View File
@@ -93,9 +93,8 @@ Use AskUserQuestion:
> **Same machine** skips the copy-paste ceremony. Credentials are written directly to
> the agent's config directory. No tunnel needed.
>
> **Different machine** requires an ngrok tunnel so the remote agent can reach your
> browser over the internet. A setup key and instruction block are generated for
> copy-paste.
> **Different machine** generates a setup key and instruction block. If ngrok is
> installed, the tunnel starts automatically. If not, I'll walk you through setup.
>
> RECOMMENDATION: Choose A if the agent is local. It's instant, no copy-paste needed.
@@ -124,45 +123,63 @@ using the generic remote flow instead.
### If different machine (option B):
Check if a tunnel is running:
First, detect ngrok status:
```bash
$B pair-agent
which ngrok 2>/dev/null && echo "NGROK_INSTALLED" || echo "NGROK_NOT_INSTALLED"
ngrok config check 2>/dev/null && echo "NGROK_AUTHED" || echo "NGROK_NOT_AUTHED"
```
If the output shows "No tunnel active" and mentions ngrok:
**If ngrok is installed and authed:** Just run the command. The CLI will auto-detect
ngrok, start the tunnel, and print the instruction block with the tunnel URL:
Tell the user:
"Your browser server is localhost-only. For a remote agent to connect, you need
an ngrok tunnel. Here's how to set one up:
```bash
$B pair-agent --client TARGET_HOST
```
1. Sign up at ngrok.com (free tier works)
2. Copy your auth token
3. Save it: `echo 'NGROK_AUTHTOKEN=your_token_here' > ~/.gstack/ngrok.env`
4. Restart the server with tunnel: `BROWSE_TUNNEL=1 $B restart`
5. Run `/pair-agent` again
If the user also needs admin access (JS execution, cookies, storage):
If you just want to test locally, choose 'Same machine' instead."
STOP here. Wait for the user to set up ngrok and re-invoke.
If the tunnel IS active (or if the user is OK with localhost-only for same-network use),
the pair-agent command will print the instruction block. Show it to the user and tell them:
```bash
$B pair-agent --admin --client TARGET_HOST
```
Show the output to the user:
"Copy everything between the ═══ lines and paste it into your other agent's chat.
The agent will follow the instructions to connect. The setup key expires in 5 minutes."
### Admin access
**If ngrok is installed but NOT authed:** Walk the user through authentication:
If the user mentions needing JavaScript execution, cookie access, or storage access:
Tell the user:
"ngrok is installed but not logged in. Let's fix that:
1. Go to https://dashboard.ngrok.com/get-started/your-authtoken
2. Copy your auth token
3. Come back here and I'll run the auth command for you."
STOP here and wait for the user to provide their auth token.
When they provide it, run:
```bash
$B pair-agent --admin
ngrok config add-authtoken THEIR_TOKEN
```
Tell the user: "This gives the remote agent full admin access including JS execution,
cookie reading, and storage access. Only do this if you trust the agent and need
these capabilities."
Then retry `$B pair-agent --client TARGET_HOST`.
**If ngrok is NOT installed:** Walk the user through installation:
Tell the user:
"To connect a remote agent, we need ngrok (a tunnel that exposes your local
browser to the internet securely).
1. Go to https://ngrok.com and sign up (free tier works)
2. Install ngrok:
- macOS: `brew install ngrok`
- Linux: `snap install ngrok` or download from ngrok.com/download
3. Auth it: `ngrok config add-authtoken YOUR_TOKEN`
(get your token from https://dashboard.ngrok.com/get-started/your-authtoken)
4. Come back here and run `/pair-agent` again."
STOP here. Wait for the user to install ngrok and re-invoke.
## Step 5: Verify connection