Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,5 @@ jobs:
- name: Run e2e smoke tests
env:
RUNLOOP_API_KEY: ${{ inputs.environment == 'prod' && secrets.RUNLOOP_SMOKETEST_PROD_API_KEY || secrets.RUNLOOP_SMOKETEST_DEV_API_KEY }}
RUNLOOP_ENV: ${{ inputs.environment }}
RUNLOOP_BASE_URL: ${{ inputs.environment == 'prod' && 'https://api.runloop.ai' || 'https://api.runloop.pro' }}
run: pnpm run test:e2e
8 changes: 5 additions & 3 deletions CLAUDE_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,9 @@ Press Ctrl+C to stop it.

## Advanced Configuration

### Using Development Environment
### Custom API endpoint

If you want to connect to Runloop's development environment:
To connect to a non-default Runloop deployment, set `RUNLOOP_BASE_URL` to the full API URL (must be `https://api.<domain>`):

```json
{
Expand All @@ -144,13 +144,15 @@ If you want to connect to Runloop's development environment:
"command": "rli",
"args": ["mcp", "start"],
"env": {
"RUNLOOP_ENV": "dev"
"RUNLOOP_BASE_URL": "https://api.runloop.pro"
}
}
}
}
```

See the repository [README](README.md) **Setup → Custom API endpoint (`RUNLOOP_BASE_URL`)** for the hostname table.

### Using a Specific Path

If `rli` isn't in your PATH, you can specify the full path:
Expand Down
8 changes: 5 additions & 3 deletions MCP_COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ When you run `rli mcp install`, it creates this configuration in your Claude Des
}
```

### With Environment Variables
### Custom API endpoint

To use the development environment:
To point MCP at a different deployment, set `RUNLOOP_BASE_URL` (must be `https://api.<domain>`):

```json
{
Expand All @@ -76,13 +76,15 @@ To use the development environment:
"command": "rli",
"args": ["mcp", "start"],
"env": {
"RUNLOOP_ENV": "dev"
"RUNLOOP_BASE_URL": "https://api.runloop.pro"
}
}
}
}
```

See [README.md](README.md) (Setup → Custom API endpoint) for the hostname table.

## Available Tools

Once configured, Claude can use these tools:
Expand Down
7 changes: 2 additions & 5 deletions MCP_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,6 @@ If you prefer to configure manually, add this to your Claude configuration file:
"runloop": {
"command": "rli",
"args": ["mcp", "start"],
"env": {
"RUNLOOP_ENV": "prod"
}
}
}
}
Expand Down Expand Up @@ -151,8 +148,8 @@ Claude will use the MCP tools to interact with your Runloop account and provide

## Environment Variables

- `RUNLOOP_ENV` - Set to `dev` for development environment, `prod` (or leave unset) for production
- API key is read from the CLI configuration (~/.config/rli/config.json)
- `RUNLOOP_BASE_URL` — Optional. Full API URL of the form `https://api.<domain>` (e.g. `https://api.runloop.pro`). Defaults to `https://api.runloop.ai`. See [README](README.md) **Setup → Custom API endpoint (`RUNLOOP_BASE_URL`)**.
- API key is read from the CLI configuration (`~/.runloop/config.json`)

## Troubleshooting

Expand Down
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,31 @@ pnpm add -g @runloop/rl-cli

## Setup

Configure your API key:
### API key

```bash
export RUNLOOP_API_KEY=your_api_key_here
```

Get your API key from [https://runloop.ai/settings](https://runloop.ai/settings)

### Custom API endpoint (`RUNLOOP_BASE_URL`, optional)

By default the CLI and MCP server connect to `https://api.runloop.ai`. To use a different deployment, set `RUNLOOP_BASE_URL` to the full API URL:

```bash
export RUNLOOP_BASE_URL=https://api.runloop.pro
```

The URL must be of the form `https://api.<domain>`. The CLI derives other service hostnames from the domain portion:

| Service | Host |
|----------|------|
| API | `https://api.<domain>` (the value of `RUNLOOP_BASE_URL`) |
| Platform | `https://platform.<domain>` |
| SSH | `ssh.<domain>:443` |
| Tunnels | `tunnel.<domain>` |

## Usage

### TUI (Interactive Mode)
Expand Down
4 changes: 3 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { exitAlternateScreenBuffer } from "./utils/screen.js";
import { processUtils } from "./utils/processUtils.js";
import { createProgram } from "./utils/commands.js";
import { getApiKeyErrorMessage } from "./utils/config.js";
import { getApiKeyErrorMessage, checkBaseDomain } from "./utils/config.js";

// Global Ctrl+C handler to ensure it always exits
processUtils.on("SIGINT", () => {
Expand All @@ -21,6 +21,8 @@ const program = createProgram();
const { initializeTheme } = await import("./utils/theme.js");
await initializeTheme();

checkBaseDomain();

// Check if API key is configured (except for mcp commands)
const args = process.argv.slice(2);
if (!process.env.RUNLOOP_API_KEY) {
Expand Down
11 changes: 6 additions & 5 deletions src/components/Breadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";
import { Box, Text, useStdout } from "ink";
import { colors } from "../utils/theme.js";
import { runloopBaseDomain } from "../utils/config.js";
import { UpdateNotification } from "./UpdateNotification.js";

export interface BreadcrumbItem {
Expand Down Expand Up @@ -38,8 +39,8 @@ export const Breadcrumb = ({
showVersionCheck = false,
compactMode: compactModeProp,
}: BreadcrumbProps) => {
const env = process.env.RUNLOOP_ENV?.toLowerCase();
const isDevEnvironment = env === "dev";
const baseDomain = runloopBaseDomain();
const isNonDefaultDomain = baseDomain !== "runloop.ai";
const { stdout } = useStdout();

const [terminalWidth, setTerminalWidth] = React.useState(() =>
Expand Down Expand Up @@ -109,10 +110,10 @@ export const Breadcrumb = ({
<Text color={colors.primary} bold>
rl
</Text>
{isDevEnvironment && mode !== "minimal" && (
<Text color={colors.error} bold>
{isNonDefaultDomain && mode !== "minimal" && (
<Text color={colors.warning} bold>
{" "}
(dev)
({baseDomain})
</Text>
)}
{displayItems.length > 0 && <Text color={colors.textDim}> › </Text>}
Expand Down
4 changes: 2 additions & 2 deletions src/components/DevboxActionsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import { colors } from "../utils/theme.js";
import { openInBrowser } from "../utils/browser.js";
import { copyToClipboard } from "../utils/clipboard.js";
import { sshGatewayHostname } from "../utils/config.js";
import { useViewportHeight } from "../hooks/useViewportHeight.js";
import { useNavigation } from "../store/navigationStore.js";
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
Expand Down Expand Up @@ -490,10 +491,10 @@
input === "o" &&
operationResult &&
typeof operationResult === "object" &&
(operationResult as any).__customRender === "tunnel"

Check warning on line 494 in src/components/DevboxActionsMenu.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
) {
// Open tunnel URL in browser
const tunnelUrl = (operationResult as any).__tunnelUrl;

Check warning on line 497 in src/components/DevboxActionsMenu.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
if (tunnelUrl) {
openInBrowser(tunnelUrl);
setCopyStatus("Opened in browser!");
Expand All @@ -503,46 +504,46 @@
(key.upArrow || input === "k") &&
operationResult &&
typeof operationResult === "object" &&
(operationResult as any).__customRender === "exec"

Check warning on line 507 in src/components/DevboxActionsMenu.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
) {
setExecScroll(Math.max(0, execScroll - 1));
} else if (
(key.downArrow || input === "j") &&
operationResult &&
typeof operationResult === "object" &&
(operationResult as any).__customRender === "exec"

Check warning on line 514 in src/components/DevboxActionsMenu.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
) {
setExecScroll(execScroll + 1);
} else if (
key.pageUp &&
operationResult &&
typeof operationResult === "object" &&
(operationResult as any).__customRender === "exec"

Check warning on line 521 in src/components/DevboxActionsMenu.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
) {
setExecScroll(Math.max(0, execScroll - 10));
} else if (
key.pageDown &&
operationResult &&
typeof operationResult === "object" &&
(operationResult as any).__customRender === "exec"

Check warning on line 528 in src/components/DevboxActionsMenu.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
) {
setExecScroll(execScroll + 10);
} else if (
input === "g" &&
operationResult &&
typeof operationResult === "object" &&
(operationResult as any).__customRender === "exec"

Check warning on line 535 in src/components/DevboxActionsMenu.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
) {
setExecScroll(0);
} else if (
input === "G" &&
operationResult &&
typeof operationResult === "object" &&
(operationResult as any).__customRender === "exec"

Check warning on line 542 in src/components/DevboxActionsMenu.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
) {
const lines = [
...((operationResult as any).stdout || "").split("\n"),

Check warning on line 545 in src/components/DevboxActionsMenu.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
...((operationResult as any).stderr || "").split("\n"),

Check warning on line 546 in src/components/DevboxActionsMenu.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
];
const maxScroll = Math.max(
0,
Expand Down Expand Up @@ -661,8 +662,7 @@

const sshUser =
devbox.launch_parameters?.user_parameters?.username || "user";
const env = process.env.RUNLOOP_ENV?.toLowerCase();
const sshHost = env === "dev" ? "ssh.runloop.pro" : "ssh.runloop.ai";
const sshHost = sshGatewayHostname();
// macOS openssl doesn't support -verify_quiet, use compatible flags
// servername should be %h (target hostname) - SSH will replace %h with the actual hostname from the SSH command
// This matches the reference implementation where servername is the target hostname
Expand Down
6 changes: 3 additions & 3 deletions src/components/DevboxDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
type DetailSection,
type ResourceOperation,
} from "./ResourceDetailPage.js";
import { getDevboxUrl, getDevboxTunnelUrlPattern } from "../utils/url.js";
import { getDevboxUrl, getTunnelUrl } from "../utils/url.js";
import { colors } from "../utils/theme.js";
import { formatTimeAgo } from "../utils/time.js";
import { getMcpConfig } from "../services/mcpConfigService.js";
Expand Down Expand Up @@ -339,7 +339,7 @@ export const DevboxDetailPage = ({ devbox, onBack }: DevboxDetailPageProps) => {
if (devbox.tunnel && devbox.tunnel.tunnel_key) {
const tunnelKey = devbox.tunnel.tunnel_key;
const authMode = devbox.tunnel.auth_mode;
const tunnelUrl = getDevboxTunnelUrlPattern(tunnelKey);
const tunnelUrl = getTunnelUrl("{port}", tunnelKey);

detailFields.push({
label: "Tunnel",
Expand Down Expand Up @@ -651,7 +651,7 @@ export const DevboxDetailPage = ({ devbox, onBack }: DevboxDetailPageProps) => {
</Text>,
);

const tunnelUrl = getDevboxTunnelUrlPattern(devbox.tunnel.tunnel_key);
const tunnelUrl = getTunnelUrl("{port}", devbox.tunnel.tunnel_key);
lines.push(
<Text key="tunnel-url" color={colors.success}>
{" "}
Expand Down
16 changes: 16 additions & 0 deletions src/components/HomeBaseUrlText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from "react";
import { Text } from "ink";
import { runloopBaseDomain } from "../utils/config.js";
import { colors } from "../utils/theme.js";

/** Shows the active domain in the home footer when it differs from the default. */
export function HomeBaseUrlText() {
const baseDomain = React.useMemo(() => runloopBaseDomain(), []);
if (baseDomain === "runloop.ai") return null;
return (
<Text color={colors.textDim} dimColor>
{"\n"}
Domain: {baseDomain}
</Text>
);
}
13 changes: 13 additions & 0 deletions src/components/MainMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useVerticalLayout } from "../hooks/useVerticalLayout.js";
import { useBetaFeatures } from "../store/betaFeatureStore.js";
import type { BetaFeature } from "../store/betaFeatureStore.js";
import { useMenuStore } from "../store/menuStore.js";
import { HomeBaseUrlText } from "./HomeBaseUrlText.js";

interface MenuItem {
key: string;
Expand Down Expand Up @@ -284,6 +285,9 @@ export const MainMenu = ({ onSelect }: MainMenuProps) => {
})}
</Box>
{navTips}
<Box paddingX={2} flexShrink={0}>
<HomeBaseUrlText />
</Box>
</Box>
);
}
Expand Down Expand Up @@ -332,6 +336,9 @@ export const MainMenu = ({ onSelect }: MainMenuProps) => {
})}
</Box>
{navTips}
<Box paddingX={2} flexShrink={0}>
<HomeBaseUrlText />
</Box>
</Box>
);
}
Expand Down Expand Up @@ -390,6 +397,9 @@ export const MainMenu = ({ onSelect }: MainMenuProps) => {
})}
</Box>
{navTips}
<Box paddingX={2} flexShrink={0}>
<HomeBaseUrlText />
</Box>
</Box>
);
}
Expand Down Expand Up @@ -494,6 +504,9 @@ export const MainMenu = ({ onSelect }: MainMenuProps) => {
</Box>

{navTips}
<Box paddingX={2} flexShrink={0}>
<HomeBaseUrlText />
</Box>
</Box>
);
};
17 changes: 2 additions & 15 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Runloop from "@runloop/api-client";
import { VERSION } from "@runloop/api-client/version.js";
import Conf from "conf";
import { processUtils } from "../utils/processUtils.js";
import { checkBaseDomain } from "../utils/config.js";

// Client configuration
interface Config {
Expand Down Expand Up @@ -45,18 +46,6 @@ function getConfig(): Config {
}
}

function getBaseUrl(): string {
const env = process.env.RUNLOOP_ENV?.toLowerCase();

switch (env) {
case "dev":
return "https://api.runloop.pro";
case "prod":
default:
return "https://api.runloop.ai";
}
}

function getClient(): Runloop {
const config = getConfig();

Expand All @@ -66,11 +55,8 @@ function getClient(): Runloop {
);
}

const baseURL = getBaseUrl();

return new Runloop({
bearerToken: config.apiKey,
baseURL,
timeout: 10000, // 10 seconds instead of default 30 seconds
maxRetries: 2, // 2 retries instead of default 5 (only for retryable errors)
defaultHeaders: {
Expand Down Expand Up @@ -489,6 +475,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
// Start the server
async function main() {
try {
checkBaseDomain();
console.error("[MCP] Starting Runloop MCP server...");
const transport = new StdioServerTransport();
console.error("[MCP] Created stdio transport");
Expand Down
4 changes: 2 additions & 2 deletions src/services/devboxService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Returns plain data objects with no SDK reference retention
*/
import { getClient } from "../utils/client.js";
import { getTunnelBaseHost } from "../utils/url.js";
import { getTunnelUrl } from "../utils/url.js";
import type { Devbox } from "../store/devboxStore.js";
import type { DevboxesCursorIDPage } from "@runloop/api-client/pagination";
import type {
Expand Down Expand Up @@ -263,7 +263,7 @@ export async function createTunnel(
): Promise<{ url: string }> {
const client = getClient();
const tunnel = await client.devboxes.enableTunnel(id);
const url = `https://${port}-${tunnel.tunnel_key}.${getTunnelBaseHost()}`;
const url = getTunnelUrl(port, tunnel.tunnel_key);

return {
url: url.substring(0, 500),
Expand Down
22 changes: 1 addition & 21 deletions src/utils/client.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,7 @@
import Runloop from "@runloop/api-client";
import { Runloop } from "@runloop/api-client";
import { VERSION } from "@runloop/api-client/version.js";
import { getConfig } from "./config.js";

/**
* Get the base URL based on RUNLOOP_ENV environment variable
* - dev: https://api.runloop.pro
* - prod or unset: https://api.runloop.ai (default)
*/
function getBaseUrl(): string {
const env = process.env.RUNLOOP_ENV?.toLowerCase();

switch (env) {
case "dev":
return "https://api.runloop.pro";
case "prod":
default:
return "https://api.runloop.ai";
}
}

export function getClient(): Runloop {
const config = getConfig();

Expand All @@ -28,11 +11,8 @@ export function getClient(): Runloop {
);
}

const baseURL = getBaseUrl();

return new Runloop({
bearerToken: config.apiKey,
baseURL,
defaultHeaders: {
"User-Agent": `Runloop/JS - CLI ${VERSION}`,
},
Expand Down
Loading
Loading