Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 5ab68f1

Browse files
cteclaude
authored andcommitted
feat(cli): improve dev experience and roo provider API key support (#11203)
- Allow --api-key and ROO_API_KEY env var for the roo provider instead of requiring cloud auth token - Switch dev/start scripts to use tsx for running directly from source without building first - Fix path resolution (version.ts, extension.ts, extension-host.ts) to work from both source and bundled locations - Disable debug log file (~/.roo/cli-debug.log) unless --debug is passed - Update README with complete env var table and dev workflow docs Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 2a313f2 commit 5ab68f1

8 files changed

Lines changed: 170 additions & 40 deletions

File tree

apps/cli/README.md

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -177,13 +177,14 @@ Tokens are valid for 90 days. The CLI will prompt you to re-authenticate when yo
177177

178178
The CLI will look for API keys in environment variables if not provided via `--api-key`:
179179

180-
| Provider | Environment Variable |
181-
| ------------- | -------------------- |
182-
| anthropic | `ANTHROPIC_API_KEY` |
183-
| openai | `OPENAI_API_KEY` |
184-
| openrouter | `OPENROUTER_API_KEY` |
185-
| google/gemini | `GOOGLE_API_KEY` |
186-
| ... | ... |
180+
| Provider | Environment Variable |
181+
| ----------------- | --------------------------- |
182+
| roo | `ROO_API_KEY` |
183+
| anthropic | `ANTHROPIC_API_KEY` |
184+
| openai-native | `OPENAI_API_KEY` |
185+
| openrouter | `OPENROUTER_API_KEY` |
186+
| gemini | `GOOGLE_API_KEY` |
187+
| vercel-ai-gateway | `VERCEL_AI_GATEWAY_API_KEY` |
187188

188189
**Authentication Environment Variables:**
189190

@@ -233,8 +234,8 @@ The CLI will look for API keys in environment variables if not provided via `--a
233234
## Development
234235

235236
```bash
236-
# Watch mode for development
237-
pnpm dev
237+
# Run directly from source (no build required)
238+
pnpm dev --provider roo --api-key $ROO_API_KEY --print "Hello"
238239

239240
# Run tests
240241
pnpm test
@@ -246,6 +247,12 @@ pnpm check-types
246247
pnpm lint
247248
```
248249

250+
By default the `start` script points `ROO_CODE_PROVIDER_URL` at `http://localhost:8080/proxy` for local development. To point at the production API instead, override the environment variable:
251+
252+
```bash
253+
ROO_CODE_PROVIDER_URL=https://api.roocode.com/proxy pnpm dev --provider roo --api-key $ROO_API_KEY --print "Hello"
254+
```
255+
249256
## Releasing
250257

251258
Official releases are created via the GitHub Actions workflow at `.github/workflows/cli-release.yml`.

apps/cli/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
"build": "tsup",
1717
"build:extension": "pnpm --filter roo-cline bundle",
1818
"build:all": "pnpm --filter roo-cline bundle && tsup",
19-
"dev": "tsup --watch",
20-
"start": "ROO_AUTH_BASE_URL=http://localhost:3000 ROO_SDK_BASE_URL=http://localhost:3001 ROO_CODE_PROVIDER_URL=http://localhost:8080/proxy node dist/index.js",
19+
"dev": "tsx src/index.ts",
20+
"start": "ROO_AUTH_BASE_URL=http://localhost:3000 ROO_SDK_BASE_URL=http://localhost:3001 ROO_CODE_PROVIDER_URL=http://localhost:8080/proxy tsx src/index.ts",
2121
"start:production": "node dist/index.js",
2222
"build:local": "scripts/build.sh",
2323
"clean": "rimraf dist .turbo"

apps/cli/src/agent/extension-host.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import type {
2424
WebviewMessage,
2525
} from "@roo-code/types"
2626
import { createVSCodeAPI, IExtensionHost, ExtensionHostEventMap, setRuntimeConfigValues } from "@roo-code/vscode-shim"
27-
import { DebugLogger } from "@roo-code/core/cli"
27+
import { DebugLogger, setDebugLogEnabled } from "@roo-code/core/cli"
2828

2929
import type { SupportedProvider } from "@/types/index.js"
3030
import type { User } from "@/lib/sdk/index.js"
@@ -43,10 +43,25 @@ const cliLogger = new DebugLogger("CLI")
4343

4444
// Get the CLI package root directory (for finding node_modules/@vscode/ripgrep)
4545
// When running from a release tarball, ROO_CLI_ROOT is set by the wrapper script.
46-
// In development, we fall back to calculating from __dirname.
47-
// After bundling with tsup, the code is in dist/index.js (flat), so we go up one level.
46+
// In development, we fall back to finding the CLI package root by walking up to package.json.
47+
// This works whether running from dist/ (bundled) or src/agent/ (tsx dev).
4848
const __dirname = path.dirname(fileURLToPath(import.meta.url))
49-
const CLI_PACKAGE_ROOT = process.env.ROO_CLI_ROOT || path.resolve(__dirname, "..")
49+
50+
function findCliPackageRoot(): string {
51+
let dir = __dirname
52+
53+
while (dir !== path.dirname(dir)) {
54+
if (fs.existsSync(path.join(dir, "package.json"))) {
55+
return dir
56+
}
57+
58+
dir = path.dirname(dir)
59+
}
60+
61+
return path.resolve(__dirname, "..")
62+
}
63+
64+
const CLI_PACKAGE_ROOT = process.env.ROO_CLI_ROOT || findCliPackageRoot()
5065

5166
export interface ExtensionHostOptions {
5267
mode: string
@@ -154,6 +169,11 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
154169

155170
this.options = options
156171

172+
// Enable file-based debug logging only when --debug is passed.
173+
if (options.debug) {
174+
setDebugLogEnabled(true)
175+
}
176+
157177
// Set up quiet mode early, before any extension code runs.
158178
// This suppresses console output from the extension during load.
159179
this.setupQuietMode()

apps/cli/src/commands/cli/run.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -112,15 +112,18 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption
112112
extensionHostOptions.apiKey = rooToken
113113
extensionHostOptions.user = me.user
114114
} catch {
115-
console.error("[CLI] Your Roo Code Router token is not valid.")
116-
console.error("[CLI] Please run: roo auth login")
117-
process.exit(1)
115+
// If an explicit API key was provided via flag or env var, fall through
116+
// to the general API key resolution below instead of exiting.
117+
if (!flagOptions.apiKey && !getApiKeyFromEnv(extensionHostOptions.provider)) {
118+
console.error("[CLI] Your Roo Code Router token is not valid.")
119+
console.error("[CLI] Please run: roo auth login")
120+
console.error("[CLI] Or use --api-key or set ROO_API_KEY to provide your own API key.")
121+
process.exit(1)
122+
}
118123
}
119-
} else {
120-
console.error("[CLI] Your Roo Code Router token is missing.")
121-
console.error("[CLI] Please run: roo auth login")
122-
process.exit(1)
123124
}
125+
// If no rooToken, fall through to the general API key resolution below
126+
// which will check flagOptions.apiKey and ROO_API_KEY env var.
124127
}
125128

126129
// Validations

apps/cli/src/lib/utils/__tests__/extension.test.ts

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,26 @@ describe("getDefaultExtensionPath", () => {
2121

2222
it("should return monorepo path when extension.js exists there", () => {
2323
const mockDirname = "/test/apps/cli/dist"
24-
const expectedMonorepoPath = path.resolve(mockDirname, "../../../src/dist")
24+
const expectedMonorepoPath = path.resolve("/test/apps/cli", "../../src/dist")
2525

26-
vi.mocked(fs.existsSync).mockReturnValue(true)
26+
// Walk-up: dist/ has no package.json, apps/cli/ does
27+
vi.mocked(fs.existsSync).mockImplementation((p) => {
28+
const s = String(p)
29+
30+
if (s === path.join(mockDirname, "package.json")) {
31+
return false
32+
}
33+
34+
if (s === path.join("/test/apps/cli", "package.json")) {
35+
return true
36+
}
37+
38+
if (s === path.join(expectedMonorepoPath, "extension.js")) {
39+
return true
40+
}
41+
42+
return false
43+
})
2744

2845
const result = getDefaultExtensionPath(mockDirname)
2946

@@ -33,22 +50,64 @@ describe("getDefaultExtensionPath", () => {
3350

3451
it("should return package path when extension.js does not exist in monorepo path", () => {
3552
const mockDirname = "/test/apps/cli/dist"
36-
const expectedPackagePath = path.resolve(mockDirname, "../extension")
53+
const expectedPackagePath = path.resolve("/test/apps/cli", "extension")
54+
55+
// Walk-up finds package.json at apps/cli/, but no extension.js in monorepo path
56+
vi.mocked(fs.existsSync).mockImplementation((p) => {
57+
const s = String(p)
3758

38-
vi.mocked(fs.existsSync).mockReturnValue(false)
59+
if (s === path.join("/test/apps/cli", "package.json")) {
60+
return true
61+
}
62+
63+
return false
64+
})
3965

4066
const result = getDefaultExtensionPath(mockDirname)
4167

4268
expect(result).toBe(expectedPackagePath)
4369
})
4470

4571
it("should check monorepo path first", () => {
46-
const mockDirname = "/some/path"
47-
vi.mocked(fs.existsSync).mockReturnValue(false)
72+
const mockDirname = "/test/apps/cli/dist"
73+
74+
vi.mocked(fs.existsSync).mockImplementation((p) => {
75+
const s = String(p)
76+
77+
if (s === path.join("/test/apps/cli", "package.json")) {
78+
return true
79+
}
80+
81+
return false
82+
})
4883

4984
getDefaultExtensionPath(mockDirname)
5085

51-
const expectedMonorepoPath = path.resolve(mockDirname, "../../../src/dist")
86+
const expectedMonorepoPath = path.resolve("/test/apps/cli", "../../src/dist")
5287
expect(fs.existsSync).toHaveBeenCalledWith(path.join(expectedMonorepoPath, "extension.js"))
5388
})
89+
90+
it("should work when called from source directory (tsx dev)", () => {
91+
const mockDirname = "/test/apps/cli/src/commands/cli"
92+
const expectedMonorepoPath = path.resolve("/test/apps/cli", "../../src/dist")
93+
94+
// Walk-up: no package.json in src subdirs, found at apps/cli/
95+
vi.mocked(fs.existsSync).mockImplementation((p) => {
96+
const s = String(p)
97+
98+
if (s === path.join("/test/apps/cli", "package.json")) {
99+
return true
100+
}
101+
102+
if (s === path.join(expectedMonorepoPath, "extension.js")) {
103+
return true
104+
}
105+
106+
return false
107+
})
108+
109+
const result = getDefaultExtensionPath(mockDirname)
110+
111+
expect(result).toBe(expectedMonorepoPath)
112+
})
54113
})

apps/cli/src/lib/utils/extension.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,26 @@ export function getDefaultExtensionPath(dirname: string): string {
1717
}
1818
}
1919

20-
// __dirname is apps/cli/dist when bundled
21-
// The extension is at src/dist (relative to monorepo root)
22-
// So from apps/cli/dist, we need to go ../../../src/dist
23-
const monorepoPath = path.resolve(dirname, "../../../src/dist")
20+
// Find the CLI package root (apps/cli) by walking up to the nearest package.json.
21+
// This works whether called from dist/ (bundled) or src/commands/cli/ (tsx dev).
22+
let packageRoot = dirname
23+
24+
while (packageRoot !== path.dirname(packageRoot)) {
25+
if (fs.existsSync(path.join(packageRoot, "package.json"))) {
26+
break
27+
}
28+
29+
packageRoot = path.dirname(packageRoot)
30+
}
31+
32+
// The extension is at ../../src/dist relative to apps/cli (monorepo/src/dist)
33+
const monorepoPath = path.resolve(packageRoot, "../../src/dist")
2434

25-
// Try monorepo path first (for development)
2635
if (fs.existsSync(path.join(monorepoPath, "extension.js"))) {
2736
return monorepoPath
2837
}
2938

30-
// Fallback: when installed via curl script, extension is at ../extension
31-
const packagePath = path.resolve(dirname, "../extension")
39+
// Fallback: when installed via curl script, extension is at apps/cli/extension
40+
const packagePath = path.resolve(packageRoot, "extension")
3241
return packagePath
3342
}

apps/cli/src/lib/utils/version.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,24 @@
1-
import { createRequire } from "module"
1+
import fs from "fs"
2+
import path from "path"
3+
import { fileURLToPath } from "url"
24

3-
const require = createRequire(import.meta.url)
4-
const packageJson = require("../package.json")
5+
// Walk up from the current file to find the nearest package.json.
6+
// This works whether running from source (tsx src/lib/utils/) or bundle (dist/).
7+
function findVersion(): string {
8+
let dir = path.dirname(fileURLToPath(import.meta.url))
59

6-
export const VERSION = packageJson.version
10+
while (dir !== path.dirname(dir)) {
11+
const candidate = path.join(dir, "package.json")
12+
13+
if (fs.existsSync(candidate)) {
14+
const packageJson = JSON.parse(fs.readFileSync(candidate, "utf-8"))
15+
return packageJson.version
16+
}
17+
18+
dir = path.dirname(dir)
19+
}
20+
21+
return "0.0.0"
22+
}
23+
24+
export const VERSION = findVersion()

packages/core/src/debug-log/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,25 @@ import * as os from "os"
2121

2222
const DEBUG_LOG_PATH = path.join(os.homedir(), ".roo", "cli-debug.log")
2323

24+
let debugLogEnabled = false
25+
26+
/**
27+
* Enable or disable file-based debug logging.
28+
* Logging is disabled by default and should only be enabled in dev/debug mode.
29+
*/
30+
export function setDebugLogEnabled(enabled: boolean): void {
31+
debugLogEnabled = enabled
32+
}
33+
2434
/**
2535
* Simple file-based debug log function.
2636
* Writes timestamped entries to ~/.roo/cli-debug.log
37+
* Only writes when enabled via setDebugLogEnabled(true).
2738
*/
2839
export function debugLog(message: string, data?: unknown): void {
40+
if (!debugLogEnabled) {
41+
return
42+
}
2943
try {
3044
const logDir = path.dirname(DEBUG_LOG_PATH)
3145

0 commit comments

Comments
 (0)