From 44df098e12cef93653ade09ddab56b8ed7a0476d Mon Sep 17 00:00:00 2001 From: cte Date: Fri, 16 Jan 2026 14:58:49 -0800 Subject: [PATCH 1/2] Roo Code Router fixes for the cli --- apps/cli/package.json | 2 + .../agent/__tests__/extension-host.test.ts | 6 + apps/cli/src/agent/extension-host.ts | 11 +- apps/cli/src/commands/cli/run.ts | 169 +++++++++--------- apps/cli/src/index.ts | 4 +- apps/cli/src/types/types.ts | 2 +- apps/cli/src/ui/App.tsx | 5 +- apps/cli/src/ui/hooks/useExtensionHost.ts | 7 +- 8 files changed, 109 insertions(+), 97 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index 3939a0aa584..26588209961 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -14,6 +14,8 @@ "check-types": "tsc --noEmit", "test": "vitest run", "build": "tsup", + "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:production": "node dist/index.js", diff --git a/apps/cli/src/agent/__tests__/extension-host.test.ts b/apps/cli/src/agent/__tests__/extension-host.test.ts index 38edf50d283..c0294e7d426 100644 --- a/apps/cli/src/agent/__tests__/extension-host.test.ts +++ b/apps/cli/src/agent/__tests__/extension-host.test.ts @@ -36,6 +36,9 @@ function createTestHost({ model, workspacePath: "/test/workspace", extensionPath: "/test/extension", + ephemeral: false, + debug: false, + exitOnComplete: false, ...options, }) } @@ -94,6 +97,9 @@ describe("ExtensionHost", () => { apiKey: "test-key", provider: "openrouter", model: "test-model", + ephemeral: false, + debug: false, + exitOnComplete: false, } const host = new ExtensionHost(options) diff --git a/apps/cli/src/agent/extension-host.ts b/apps/cli/src/agent/extension-host.ts index 8ddbce2eb04..88020ae3a74 100644 --- a/apps/cli/src/agent/extension-host.ts +++ b/apps/cli/src/agent/extension-host.ts @@ -58,16 +58,17 @@ export interface ExtensionHostOptions { workspacePath: string extensionPath: string nonInteractive?: boolean - debug?: boolean + /** + * When true, uses a temporary storage directory that is cleaned up on exit. + */ + ephemeral: boolean + debug: boolean + exitOnComplete: boolean /** * When true, completely disables all direct stdout/stderr output. * Use this when running in TUI mode where Ink controls the terminal. */ disableOutput?: boolean - /** - * When true, uses a temporary storage directory that is cleaned up on exit. - */ - ephemeral?: boolean /** * When true, don't suppress node warnings and console output since we're * running in an integration test and we want to see the output. diff --git a/apps/cli/src/commands/cli/run.ts b/apps/cli/src/commands/cli/run.ts index 5b305ce2751..0aa6c73842e 100644 --- a/apps/cli/src/commands/cli/run.ts +++ b/apps/cli/src/commands/cli/run.ts @@ -4,7 +4,6 @@ import { fileURLToPath } from "url" import { createElement } from "react" -import { isProviderName } from "@roo-code/types" import { setLogger } from "@roo-code/vscode-shim" import { @@ -18,8 +17,8 @@ import { SDK_BASE_URL, } from "@/types/index.js" -import { type User, createClient } from "@/lib/sdk/index.js" -import { loadToken, hasToken, loadSettings } from "@/lib/storage/index.js" +import { createClient } from "@/lib/sdk/index.js" +import { loadToken, loadSettings } from "@/lib/storage/index.js" import { getEnvVarName, getApiKeyFromEnv } from "@/lib/utils/provider.js" import { runOnboarding } from "@/lib/utils/onboarding.js" import { getDefaultExtensionPath } from "@/lib/utils/extension.js" @@ -29,7 +28,7 @@ import { ExtensionHost, ExtensionHostOptions } from "@/agent/index.js" const __dirname = path.dirname(fileURLToPath(import.meta.url)) -export async function run(workspaceArg: string, options: FlagOptions) { +export async function run(workspaceArg: string, flagOptions: FlagOptions) { setLogger({ info: () => {}, warn: () => {}, @@ -37,23 +36,27 @@ export async function run(workspaceArg: string, options: FlagOptions) { debug: () => {}, }) - const isTuiSupported = process.stdin.isTTY && process.stdout.isTTY - const isTuiEnabled = options.tui && isTuiSupported - const extensionPath = options.extension || getDefaultExtensionPath(__dirname) - const workspacePath = path.resolve(workspaceArg) + // Options - if (!isSupportedProvider(options.provider)) { - console.error( - `[CLI] Error: Invalid provider: ${options.provider}; must be one of: ${supportedProviders.join(", ")}`, - ) - - process.exit(1) + const isTuiSupported = process.stdin.isTTY && process.stdout.isTTY + const isTuiEnabled = flagOptions.tui && isTuiSupported + const rooToken = await loadToken() + + const extensionHostOptions: ExtensionHostOptions = { + mode: flagOptions.mode || DEFAULT_FLAGS.mode, + reasoningEffort: flagOptions.reasoningEffort === "unspecified" ? undefined : flagOptions.reasoningEffort, + user: null, + provider: flagOptions.provider ?? (rooToken ? "roo" : "openrouter"), + model: flagOptions.model || DEFAULT_FLAGS.model, + workspacePath: path.resolve(workspaceArg), + extensionPath: path.resolve(flagOptions.extension || getDefaultExtensionPath(__dirname)), + nonInteractive: flagOptions.yes, + ephemeral: flagOptions.ephemeral, + debug: flagOptions.debug, + exitOnComplete: flagOptions.exitOnComplete, } - let apiKey = options.apiKey || getApiKeyFromEnv(options.provider) - let provider = options.provider - let user: User | null = null - let useCloudProvider = false + // Roo Code Cloud Authentication if (isTuiEnabled) { let { onboardingProviderChoice } = await loadSettings() @@ -64,29 +67,50 @@ export async function run(workspaceArg: string, options: FlagOptions) { } if (onboardingProviderChoice === OnboardingProviderChoice.Roo) { - useCloudProvider = true - const authenticated = await hasToken() - - if (authenticated) { - const token = await loadToken() - - if (token) { - try { - const client = createClient({ url: SDK_BASE_URL, authToken: token }) - const me = await client.auth.me.query() - provider = "roo" - apiKey = token - user = me?.type === "user" ? me.user : null - } catch { - // Token may be expired or invalid - user will need to re-authenticate. - } + extensionHostOptions.provider = "roo" + } + } + + if (extensionHostOptions.provider === "roo") { + if (rooToken) { + try { + const client = createClient({ url: SDK_BASE_URL, authToken: rooToken }) + const me = await client.auth.me.query() + + if (me?.type !== "user") { + throw new Error("Invalid token") } + + extensionHostOptions.apiKey = rooToken + extensionHostOptions.user = me.user + } catch { + console.error("[CLI] Your Roo Code Router token is not valid.") + console.error("[CLI] Please run: roo auth login") + process.exit(1) } + } else { + console.error("[CLI] Your Roo Code Router token is not missing.") + console.error("[CLI] Please run: roo auth login") + process.exit(1) } } - if (!apiKey) { - if (useCloudProvider) { + // Validations + // TODO: Validate the API key for the chosen provider. + // TODO: Validate the model for the chosen provider. + + if (!isSupportedProvider(extensionHostOptions.provider)) { + console.error( + `[CLI] Error: Invalid provider: ${extensionHostOptions.provider}; must be one of: ${supportedProviders.join(", ")}`, + ) + process.exit(1) + } + + extensionHostOptions.apiKey = + extensionHostOptions.apiKey || flagOptions.apiKey || getApiKeyFromEnv(extensionHostOptions.provider) + + if (!extensionHostOptions.apiKey) { + if (extensionHostOptions.provider === "roo") { console.error("[CLI] Error: Authentication with Roo Code Cloud failed or was cancelled.") console.error("[CLI] Please run: roo auth login") console.error("[CLI] Or use --api-key to provide your own API key.") @@ -94,40 +118,41 @@ export async function run(workspaceArg: string, options: FlagOptions) { console.error( `[CLI] Error: No API key provided. Use --api-key or set the appropriate environment variable.`, ) - console.error(`[CLI] For ${provider}, set ${getEnvVarName(provider)}`) + console.error( + `[CLI] For ${extensionHostOptions.provider}, set ${getEnvVarName(extensionHostOptions.provider)}`, + ) } process.exit(1) } - if (!fs.existsSync(workspacePath)) { - console.error(`[CLI] Error: Workspace path does not exist: ${workspacePath}`) - process.exit(1) - } - - if (!isProviderName(options.provider)) { - console.error(`[CLI] Error: Invalid provider: ${options.provider}`) + if (!fs.existsSync(extensionHostOptions.workspacePath)) { + console.error(`[CLI] Error: Workspace path does not exist: ${extensionHostOptions.workspacePath}`) process.exit(1) } - if (options.reasoningEffort && !REASONING_EFFORTS.includes(options.reasoningEffort)) { + if (extensionHostOptions.reasoningEffort && !REASONING_EFFORTS.includes(extensionHostOptions.reasoningEffort)) { console.error( - `[CLI] Error: Invalid reasoning effort: ${options.reasoningEffort}, must be one of: ${REASONING_EFFORTS.join(", ")}`, + `[CLI] Error: Invalid reasoning effort: ${extensionHostOptions.reasoningEffort}, must be one of: ${REASONING_EFFORTS.join(", ")}`, ) process.exit(1) } - if (options.tui && !isTuiSupported) { - console.log("[CLI] TUI disabled (no TTY support), falling back to plain text mode") - } + 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") + process.exit(1) + } - if (!isTuiEnabled && !options.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") - process.exit(1) + if (flagOptions.tui) { + console.warn("[CLI] TUI disabled (no TTY support), falling back to plain text mode") + } } + // Run! + if (isTuiEnabled) { try { const { render } = await import("ink") @@ -135,21 +160,9 @@ export async function run(workspaceArg: string, options: FlagOptions) { render( createElement(App, { - initialPrompt: options.prompt || "", - workspacePath: workspacePath, - extensionPath: path.resolve(extensionPath), - user, - provider, - apiKey, - model: options.model || DEFAULT_FLAGS.model, - mode: options.mode || DEFAULT_FLAGS.mode, - nonInteractive: options.yes, - debug: options.debug, - exitOnComplete: options.exitOnComplete, - reasoningEffort: options.reasoningEffort, - ephemeral: options.ephemeral, + ...extensionHostOptions, + initialPrompt: flagOptions.prompt, version: VERSION, - // Create extension host factory for dependency injection. createExtensionHost: (opts: ExtensionHostOptions) => new ExtensionHost(opts), }), // Handle Ctrl+C in App component for double-press exit. @@ -168,22 +181,10 @@ export async function run(workspaceArg: string, options: FlagOptions) { console.log(ASCII_ROO) console.log() console.log( - `[roo] Running ${options.model || "default"} (${options.reasoningEffort || "default"}) on ${provider} in ${options.mode || "default"} mode in ${workspacePath}`, + `[roo] Running ${extensionHostOptions.model || "default"} (${extensionHostOptions.reasoningEffort || "default"}) on ${extensionHostOptions.provider} in ${extensionHostOptions.mode || "default"} mode in ${extensionHostOptions.workspacePath} [debug = ${extensionHostOptions.debug}]`, ) - const host = new ExtensionHost({ - mode: options.mode || DEFAULT_FLAGS.mode, - reasoningEffort: options.reasoningEffort === "unspecified" ? undefined : options.reasoningEffort, - user, - provider, - apiKey, - model: options.model || DEFAULT_FLAGS.model, - workspacePath, - extensionPath: path.resolve(extensionPath), - nonInteractive: options.yes, - ephemeral: options.ephemeral, - debug: options.debug, - }) + const host = new ExtensionHost(extensionHostOptions) process.on("SIGINT", async () => { console.log("\n[CLI] Received SIGINT, shutting down...") @@ -199,10 +200,10 @@ export async function run(workspaceArg: string, options: FlagOptions) { try { await host.activate() - await host.runTask(options.prompt!) + await host.runTask(flagOptions.prompt!) await host.dispose() - if (!options.waitOnComplete) { + if (!flagOptions.waitOnComplete) { process.exit(0) } } catch (error) { diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 8d3f5af521e..f9c936333a1 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -14,8 +14,8 @@ program .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("-k, --api-key ", "API key for the LLM provider (defaults to OPENROUTER_API_KEY env var)") - .option("-p, --provider ", "API provider (anthropic, openai, openrouter, etc.)", "openrouter") + .option("-k, --api-key ", "API key for the LLM provider") + .option("-p, --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( diff --git a/apps/cli/src/types/types.ts b/apps/cli/src/types/types.ts index cd64c9b1629..42c4e3a6fea 100644 --- a/apps/cli/src/types/types.ts +++ b/apps/cli/src/types/types.ts @@ -23,7 +23,7 @@ export type FlagOptions = { debug: boolean yes: boolean apiKey?: string - provider: SupportedProvider + provider?: SupportedProvider model?: string mode?: string reasoningEffort?: ReasoningEffortFlagOptions diff --git a/apps/cli/src/ui/App.tsx b/apps/cli/src/ui/App.tsx index fdb8644f53b..fc2fc51addc 100644 --- a/apps/cli/src/ui/App.tsx +++ b/apps/cli/src/ui/App.tsx @@ -59,10 +59,9 @@ import ScrollIndicator from "./components/ScrollIndicator.js" const PICKER_HEIGHT = 10 export interface TUIAppProps extends ExtensionHostOptions { - initialPrompt: string - debug: boolean - exitOnComplete: boolean + initialPrompt?: string version: string + // Create extension host factory for dependency injection. createExtensionHost: (options: ExtensionHostOptions) => ExtensionHostInterface } diff --git a/apps/cli/src/ui/hooks/useExtensionHost.ts b/apps/cli/src/ui/hooks/useExtensionHost.ts index 91bdac2bf01..78074aab4fc 100644 --- a/apps/cli/src/ui/hooks/useExtensionHost.ts +++ b/apps/cli/src/ui/hooks/useExtensionHost.ts @@ -7,9 +7,9 @@ import { ExtensionHostInterface, ExtensionHostOptions } from "@/agent/index.js" import { useCLIStore } from "../store.js" +// TODO: Unify with TUIAppProps? export interface UseExtensionHostOptions extends ExtensionHostOptions { initialPrompt?: string - exitOnComplete?: boolean onExtensionMessage: (msg: ExtensionMessage) => void createExtensionHost: (options: ExtensionHostOptions) => ExtensionHostInterface } @@ -42,6 +42,7 @@ export function useExtensionHost({ extensionPath, nonInteractive, ephemeral, + debug, exitOnComplete, onExtensionMessage, createExtensionHost, @@ -73,8 +74,10 @@ export function useExtensionHost({ workspacePath, extensionPath, nonInteractive, - disableOutput: true, ephemeral, + debug, + exitOnComplete, + disableOutput: true, }) hostRef.current = host From f8662318d175dc8f53d42cd314f0803bae2319c3 Mon Sep 17 00:00:00 2001 From: Chris Estreich Date: Fri, 16 Jan 2026 17:03:40 -0800 Subject: [PATCH 2/2] Update apps/cli/src/commands/cli/run.ts Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com> --- apps/cli/src/commands/cli/run.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/cli/src/commands/cli/run.ts b/apps/cli/src/commands/cli/run.ts index 0aa6c73842e..1479217679a 100644 --- a/apps/cli/src/commands/cli/run.ts +++ b/apps/cli/src/commands/cli/run.ts @@ -89,7 +89,7 @@ export async function run(workspaceArg: string, flagOptions: FlagOptions) { process.exit(1) } } else { - console.error("[CLI] Your Roo Code Router token is not missing.") + console.error("[CLI] Your Roo Code Router token is missing.") console.error("[CLI] Please run: roo auth login") process.exit(1) }