diff --git a/apps/cli/package.json b/apps/cli/package.json index 2658820996..a8fb1d1a47 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -17,7 +17,7 @@ "build:extension": "pnpm --filter roo-cline bundle", "build:all": "pnpm --filter roo-cline bundle && tsup", "dev": "tsup --watch", - "start": "ROO_SDK_BASE_URL=http://localhost:3001 ROO_AUTH_BASE_URL=http://localhost:3000 node dist/index.js", + "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", "start:production": "node dist/index.js", "release": "scripts/release.sh", "clean": "rimraf dist .turbo" diff --git a/apps/cli/src/commands/auth/login.ts b/apps/cli/src/commands/auth/login.ts index 14966f2d15..6cb452741a 100644 --- a/apps/cli/src/commands/auth/login.ts +++ b/apps/cli/src/commands/auth/login.ts @@ -11,12 +11,15 @@ export interface LoginOptions { verbose?: boolean } -export interface LoginResult { - success: boolean - error?: string - userId?: string - orgId?: string | null -} +export type LoginResult = + | { + success: true + token: string + } + | { + success: false + error: string + } const LOCALHOST = "127.0.0.1" @@ -29,49 +32,57 @@ export async function login({ timeout = 5 * 60 * 1000, verbose = false }: LoginO console.log(`[Auth] Starting local callback server on port ${port}`) } + const corsHeaders = { + "Access-Control-Allow-Origin": AUTH_BASE_URL, + "Access-Control-Allow-Methods": "POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", + } + // Create promise that will be resolved when we receive the callback. const tokenPromise = new Promise<{ token: string; state: string }>((resolve, reject) => { const server = http.createServer((req, res) => { const url = new URL(req.url!, host) - if (url.pathname === "/callback") { + // Handle CORS preflight request. + if (req.method === "OPTIONS") { + res.writeHead(204, corsHeaders) + res.end() + return + } + + if (url.pathname === "/callback" && req.method === "POST") { const receivedState = url.searchParams.get("state") const token = url.searchParams.get("token") const error = url.searchParams.get("error") + const sendJsonResponse = (status: number, body: object) => { + res.writeHead(status, { + ...corsHeaders, + "Content-Type": "application/json", + }) + res.end(JSON.stringify(body)) + } + if (error) { - const errorUrl = new URL(`${AUTH_BASE_URL}/cli/sign-in?error=error-in-callback`) - errorUrl.searchParams.set("message", error) - res.writeHead(302, { Location: errorUrl.toString() }) - res.end() - // Wait for response to be fully sent before closing server and rejecting. - // The 'close' event fires when the underlying connection is terminated, - // ensuring the browser has received the redirect before we shut down. + sendJsonResponse(400, { success: false, error }) res.on("close", () => { server.close() reject(new Error(error)) }) } else if (!token) { - const errorUrl = new URL(`${AUTH_BASE_URL}/cli/sign-in?error=missing-token`) - errorUrl.searchParams.set("message", "Missing token in callback") - res.writeHead(302, { Location: errorUrl.toString() }) - res.end() + sendJsonResponse(400, { success: false, error: "Missing token in callback" }) res.on("close", () => { server.close() reject(new Error("Missing token in callback")) }) } else if (receivedState !== state) { - const errorUrl = new URL(`${AUTH_BASE_URL}/cli/sign-in?error=invalid-state-parameter`) - errorUrl.searchParams.set("message", "Invalid state parameter (possible CSRF attack)") - res.writeHead(302, { Location: errorUrl.toString() }) - res.end() + sendJsonResponse(400, { success: false, error: "Invalid state parameter" }) res.on("close", () => { server.close() reject(new Error("Invalid state parameter")) }) } else { - res.writeHead(302, { Location: `${AUTH_BASE_URL}/cli/sign-in?success=true` }) - res.end() + sendJsonResponse(200, { success: true }) res.on("close", () => { server.close() resolve({ token, state: receivedState }) @@ -90,12 +101,7 @@ export async function login({ timeout = 5 * 60 * 1000, verbose = false }: LoginO reject(new Error("Authentication timed out")) }, timeout) - server.on("listening", () => { - console.log(`[Auth] Callback server listening on port ${port}`) - }) - server.on("close", () => { - console.log("[Auth] Callback server closed") clearTimeout(timeoutId) }) }) @@ -121,7 +127,7 @@ export async function login({ timeout = 5 * 60 * 1000, verbose = false }: LoginO const { token } = await tokenPromise await saveToken(token) console.log("✓ Successfully authenticated!") - return { success: true } + return { success: true, token } } catch (error) { const message = error instanceof Error ? error.message : String(error) console.error(`✗ Authentication failed: ${message}`) diff --git a/apps/cli/src/commands/cli/run.ts b/apps/cli/src/commands/cli/run.ts index 1479217679..5398c4e276 100644 --- a/apps/cli/src/commands/cli/run.ts +++ b/apps/cli/src/commands/cli/run.ts @@ -28,7 +28,7 @@ import { ExtensionHost, ExtensionHostOptions } from "@/agent/index.js" const __dirname = path.dirname(fileURLToPath(import.meta.url)) -export async function run(workspaceArg: string, flagOptions: FlagOptions) { +export async function run(prompt: string | undefined, flagOptions: FlagOptions) { setLogger({ info: () => {}, warn: () => {}, @@ -39,8 +39,8 @@ export async function run(workspaceArg: string, flagOptions: FlagOptions) { // Options const isTuiSupported = process.stdin.isTTY && process.stdout.isTTY - const isTuiEnabled = flagOptions.tui && isTuiSupported - const rooToken = await loadToken() + const isTuiEnabled = !flagOptions.print && isTuiSupported + let rooToken = await loadToken() const extensionHostOptions: ExtensionHostOptions = { mode: flagOptions.mode || DEFAULT_FLAGS.mode, @@ -48,12 +48,12 @@ export async function run(workspaceArg: string, flagOptions: FlagOptions) { user: null, provider: flagOptions.provider ?? (rooToken ? "roo" : "openrouter"), model: flagOptions.model || DEFAULT_FLAGS.model, - workspacePath: path.resolve(workspaceArg), + workspacePath: process.cwd(), extensionPath: path.resolve(flagOptions.extension || getDefaultExtensionPath(__dirname)), nonInteractive: flagOptions.yes, ephemeral: flagOptions.ephemeral, debug: flagOptions.debug, - exitOnComplete: flagOptions.exitOnComplete, + exitOnComplete: flagOptions.print, } // Roo Code Cloud Authentication @@ -62,8 +62,9 @@ export async function run(workspaceArg: string, flagOptions: FlagOptions) { let { onboardingProviderChoice } = await loadSettings() if (!onboardingProviderChoice) { - const result = await runOnboarding() - onboardingProviderChoice = result.choice + const { choice, token } = await runOnboarding() + onboardingProviderChoice = choice + rooToken = token ?? null } if (onboardingProviderChoice === OnboardingProviderChoice.Roo) { @@ -139,15 +140,15 @@ export async function run(workspaceArg: string, flagOptions: FlagOptions) { } if (!isTuiEnabled) { - if (!flagOptions.prompt) { - console.error("[CLI] Error: prompt is required in plain text mode") - console.error("[CLI] Usage: roo [workspace] -P [options]") - console.error("[CLI] Use TUI mode (without --no-tui) for interactive input") + if (!prompt) { + console.error("[CLI] Error: prompt is required in print mode") + console.error("[CLI] Usage: roo -p [options]") + console.error("[CLI] Run without -p for interactive mode") process.exit(1) } - if (flagOptions.tui) { - console.warn("[CLI] TUI disabled (no TTY support), falling back to plain text mode") + if (!flagOptions.print) { + console.warn("[CLI] TUI disabled (no TTY support), falling back to print mode") } } @@ -161,7 +162,7 @@ export async function run(workspaceArg: string, flagOptions: FlagOptions) { render( createElement(App, { ...extensionHostOptions, - initialPrompt: flagOptions.prompt, + initialPrompt: prompt, version: VERSION, createExtensionHost: (opts: ExtensionHostOptions) => new ExtensionHost(opts), }), @@ -200,12 +201,9 @@ export async function run(workspaceArg: string, flagOptions: FlagOptions) { try { await host.activate() - await host.runTask(flagOptions.prompt!) + await host.runTask(prompt!) await host.dispose() - - if (!flagOptions.waitOnComplete) { - process.exit(0) - } + process.exit(0) } catch (error) { console.error("[CLI] Error:", error instanceof Error ? error.message : String(error)) diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index f9c936333a..b9d9de8cb2 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -6,31 +6,27 @@ import { run, login, logout, status } from "@/commands/index.js" const program = new Command() -program.name("roo").description("Roo Code CLI - Run the Roo Code agent from the command line").version(VERSION) +program + .name("roo") + .description("Roo Code CLI - starts an interactive session by default, use -p/--print for non-interactive output") + .version(VERSION) program - .argument("[workspace]", "Workspace path to operate in", process.cwd()) - .option("-P, --prompt ", "The prompt/task to execute (optional in TUI mode)") + .argument("[prompt]", "Your prompt") + .option("-p, --print", "Print response and exit (non-interactive mode)", false) .option("-e, --extension ", "Path to the extension bundle directory") .option("-d, --debug", "Enable debug output (includes detailed debug information)", false) - .option("-y, --yes", "Auto-approve all prompts (non-interactive mode)", false) + .option("-y, --yes", "Auto-approve all prompts", false) .option("-k, --api-key ", "API key for the LLM provider") - .option("-p, --provider ", "API provider (roo, anthropic, openai, openrouter, etc.)") + .option("--provider ", "API provider (roo, anthropic, openai, openrouter, etc.)") .option("-m, --model ", "Model to use", DEFAULT_FLAGS.model) - .option("-M, --mode ", "Mode to start in (code, architect, ask, debug, etc.)", DEFAULT_FLAGS.mode) + .option("--mode ", "Mode to start in (code, architect, ask, debug, etc.)", DEFAULT_FLAGS.mode) .option( "-r, --reasoning-effort ", "Reasoning effort level (unspecified, disabled, none, minimal, low, medium, high, xhigh)", DEFAULT_FLAGS.reasoningEffort, ) - .option("-x, --exit-on-complete", "Exit the process when the task completes (applies to TUI mode only)", false) - .option( - "-w, --wait-on-complete", - "Keep the process running when the task completes (applies to plain text mode only)", - false, - ) .option("--ephemeral", "Run without persisting state (uses temporary storage)", false) - .option("--no-tui", "Disable TUI, use plain text output") .action(run) const authCommand = program.command("auth").description("Manage authentication for Roo Code Cloud") diff --git a/apps/cli/src/lib/utils/onboarding.ts b/apps/cli/src/lib/utils/onboarding.ts index 176bc6a344..15da68f540 100644 --- a/apps/cli/src/lib/utils/onboarding.ts +++ b/apps/cli/src/lib/utils/onboarding.ts @@ -17,9 +17,14 @@ export async function runOnboarding(): Promise { console.log("") if (choice === OnboardingProviderChoice.Roo) { - const { success: authenticated } = await login() + const result = await login() await saveSettings({ onboardingProviderChoice: choice }) - resolve({ choice: OnboardingProviderChoice.Roo, authenticated, skipped: false }) + + resolve({ + choice: OnboardingProviderChoice.Roo, + token: result.success ? result.token : undefined, + skipped: false, + }) } else { console.log("Using your own API key.") console.log("Set your API key via --api-key or environment variable.") diff --git a/apps/cli/src/types/types.ts b/apps/cli/src/types/types.ts index 42c4e3a6fe..f791e9aa46 100644 --- a/apps/cli/src/types/types.ts +++ b/apps/cli/src/types/types.ts @@ -18,7 +18,7 @@ export function isSupportedProvider(provider: string): provider is SupportedProv export type ReasoningEffortFlagOptions = ReasoningEffortExtended | "unspecified" | "disabled" export type FlagOptions = { - prompt?: string + print: boolean extension?: string debug: boolean yes: boolean @@ -27,10 +27,7 @@ export type FlagOptions = { model?: string mode?: string reasoningEffort?: ReasoningEffortFlagOptions - exitOnComplete: boolean - waitOnComplete: boolean ephemeral: boolean - tui: boolean } export enum OnboardingProviderChoice { @@ -40,7 +37,7 @@ export enum OnboardingProviderChoice { export interface OnboardingResult { choice: OnboardingProviderChoice - authenticated?: boolean + token?: string skipped: boolean } diff --git a/packages/evals/src/cli/runTaskInCli.ts b/packages/evals/src/cli/runTaskInCli.ts index 1f1ad79161..228522dc83 100644 --- a/packages/evals/src/cli/runTaskInCli.ts +++ b/packages/evals/src/cli/runTaskInCli.ts @@ -36,37 +36,27 @@ export const runTaskWithCli = async ({ run, task, publish, logger, jobToken }: R const controller = new AbortController() const cancelSignal = controller.signal - const cliArgs = [ - "--filter", - "@roo-code/cli", - "start", - "--yes", - "--exit-on-complete", - "--reasoning-effort", - "disabled", - "--workspace", - workspacePath, - ] + const cliArgs = ["--filter", "@roo-code/cli", "start", "--yes", "--print", "--reasoning-effort", "disabled"] if (run.settings?.mode) { - cliArgs.push("-M", run.settings.mode) + cliArgs.push("--mode", run.settings.mode) } if (run.settings?.apiProvider) { - cliArgs.push("-p", run.settings.apiProvider) + cliArgs.push("--provider", run.settings.apiProvider) } const modelId = run.settings?.apiModelId || run.settings?.openRouterModelId if (modelId) { - cliArgs.push("-m", modelId) + cliArgs.push("--model", modelId) } cliArgs.push(prompt) logger.info(`CLI command: pnpm ${cliArgs.join(" ")}`) - const subprocess = execa("pnpm", cliArgs, { env, cancelSignal, cwd: process.cwd() }) + const subprocess = execa("pnpm", cliArgs, { env, cancelSignal, cwd: workspacePath }) // Buffer for accumulating streaming output until we have complete lines. let stdoutBuffer = ""