mirror of https://github.com/garrytan/gstack.git
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:
parent
376814c3f9
commit
7ed3b12854
|
|
@ -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 ─────────────────────────────────────────────
|
// ─── Pair-Agent DX ─────────────────────────────────────────────
|
||||||
|
|
||||||
interface InstructionBlockOptions {
|
interface InstructionBlockOptions {
|
||||||
|
|
@ -586,16 +613,85 @@ async function handlePairAgent(state: ServerState, args: string[]): Promise<void
|
||||||
let serverUrl: string;
|
let serverUrl: string;
|
||||||
if (pairData.tunnel_url) {
|
if (pairData.tunnel_url) {
|
||||||
serverUrl = pairData.tunnel_url;
|
serverUrl = pairData.tunnel_url;
|
||||||
} else {
|
} else if (!localHost) {
|
||||||
// Check if ngrok is configured but tunnel isn't running
|
// No tunnel active. Check if ngrok is available and auto-start.
|
||||||
const ngrokEnvPath = path.join(process.env.HOME || '/tmp', '.gstack', 'ngrok.env');
|
const ngrokAvailable = isNgrokAvailable();
|
||||||
if (fs.existsSync(ngrokEnvPath) && !localHost) {
|
if (ngrokAvailable) {
|
||||||
console.warn('[browse] ngrok is configured but tunnel is not running.');
|
console.log('[browse] ngrok is available. Starting tunnel...');
|
||||||
console.warn('[browse] Start the tunnel: BROWSE_TUNNEL=1 $B restart');
|
// Restart server with tunnel enabled
|
||||||
console.warn('[browse] Using localhost for now (same-machine only).\n');
|
try {
|
||||||
} else if (!localHost) {
|
const restartResp = await fetch(`http://127.0.0.1:${state.port}/command`, {
|
||||||
console.warn('[browse] No tunnel active. Instructions use localhost (same-machine only).\n');
|
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;
|
serverUrl = pairData.server_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -606,9 +606,8 @@ Use AskUserQuestion:
|
||||||
> **Same machine** skips the copy-paste ceremony. Credentials are written directly to
|
> **Same machine** skips the copy-paste ceremony. Credentials are written directly to
|
||||||
> the agent's config directory. No tunnel needed.
|
> the agent's config directory. No tunnel needed.
|
||||||
>
|
>
|
||||||
> **Different machine** requires an ngrok tunnel so the remote agent can reach your
|
> **Different machine** generates a setup key and instruction block. If ngrok is
|
||||||
> browser over the internet. A setup key and instruction block are generated for
|
> installed, the tunnel starts automatically. If not, I'll walk you through setup.
|
||||||
> copy-paste.
|
|
||||||
>
|
>
|
||||||
> RECOMMENDATION: Choose A if the agent is local. It's instant, no copy-paste needed.
|
> 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):
|
### If different machine (option B):
|
||||||
|
|
||||||
Check if a tunnel is running:
|
First, detect ngrok status:
|
||||||
|
|
||||||
```bash
|
```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:
|
```bash
|
||||||
"Your browser server is localhost-only. For a remote agent to connect, you need
|
$B pair-agent --client TARGET_HOST
|
||||||
an ngrok tunnel. Here's how to set one up:
|
```
|
||||||
|
|
||||||
1. Sign up at ngrok.com (free tier works)
|
If the user also needs admin access (JS execution, cookies, storage):
|
||||||
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 you just want to test locally, choose 'Same machine' instead."
|
```bash
|
||||||
|
$B pair-agent --admin --client TARGET_HOST
|
||||||
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:
|
|
||||||
|
|
||||||
|
Show the output to the user:
|
||||||
"Copy everything between the ═══ lines and paste it into your other agent's chat.
|
"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."
|
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
|
```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,
|
Then retry `$B pair-agent --client TARGET_HOST`.
|
||||||
cookie reading, and storage access. Only do this if you trust the agent and need
|
|
||||||
these capabilities."
|
**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
|
## Step 5: Verify connection
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,9 +93,8 @@ Use AskUserQuestion:
|
||||||
> **Same machine** skips the copy-paste ceremony. Credentials are written directly to
|
> **Same machine** skips the copy-paste ceremony. Credentials are written directly to
|
||||||
> the agent's config directory. No tunnel needed.
|
> the agent's config directory. No tunnel needed.
|
||||||
>
|
>
|
||||||
> **Different machine** requires an ngrok tunnel so the remote agent can reach your
|
> **Different machine** generates a setup key and instruction block. If ngrok is
|
||||||
> browser over the internet. A setup key and instruction block are generated for
|
> installed, the tunnel starts automatically. If not, I'll walk you through setup.
|
||||||
> copy-paste.
|
|
||||||
>
|
>
|
||||||
> RECOMMENDATION: Choose A if the agent is local. It's instant, no copy-paste needed.
|
> 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):
|
### If different machine (option B):
|
||||||
|
|
||||||
Check if a tunnel is running:
|
First, detect ngrok status:
|
||||||
|
|
||||||
```bash
|
```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:
|
```bash
|
||||||
"Your browser server is localhost-only. For a remote agent to connect, you need
|
$B pair-agent --client TARGET_HOST
|
||||||
an ngrok tunnel. Here's how to set one up:
|
```
|
||||||
|
|
||||||
1. Sign up at ngrok.com (free tier works)
|
If the user also needs admin access (JS execution, cookies, storage):
|
||||||
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 you just want to test locally, choose 'Same machine' instead."
|
```bash
|
||||||
|
$B pair-agent --admin --client TARGET_HOST
|
||||||
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:
|
|
||||||
|
|
||||||
|
Show the output to the user:
|
||||||
"Copy everything between the ═══ lines and paste it into your other agent's chat.
|
"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."
|
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
|
```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,
|
Then retry `$B pair-agent --client TARGET_HOST`.
|
||||||
cookie reading, and storage access. Only do this if you trust the agent and need
|
|
||||||
these capabilities."
|
**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
|
## Step 5: Verify connection
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue