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
19 changes: 19 additions & 0 deletions apps/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,24 @@ In approval-required mode:
- Tool, command, browser, and MCP actions prompt for yes/no approval
- Followup questions wait for manual input (no auto-timeout)

### Print Mode (`--print`)

Use `--print` for non-interactive execution and machine-readable output:

```bash
# Prompt is required
roo --print "Summarize this repository"
```

### Stdin Stream Mode (`--stdin-prompt-stream`)

For programmatic control (one process, multiple prompts), use `--stdin-prompt-stream` with `--print`.
Send one prompt per line via stdin:

```bash
printf '1+1=?\n10!=?\n' | roo --print --stdin-prompt-stream --output-format stream-json
```

### Roo Code Cloud Authentication

To use Roo Code Cloud features (like the provider proxy), you need to authenticate:
Expand Down Expand Up @@ -152,6 +170,7 @@ Tokens are valid for 90 days. The CLI will prompt you to re-authenticate when yo
| `--prompt-file <path>` | Read prompt from a file instead of command line argument | None |
| `-w, --workspace <path>` | Workspace path to operate in | Current directory |
| `-p, --print` | Print response and exit (non-interactive mode) | `false` |
| `--stdin-prompt-stream` | Read prompts from stdin (one prompt per line, requires `--print`) | `false` |
| `-e, --extension <path>` | Path to the extension bundle directory | Auto-detected |
| `-d, --debug` | Enable debug output (includes detailed debug information, prompts, paths, etc) | `false` |
| `-a, --require-approval` | Require manual approval before actions execute | `false` |
Expand Down
15 changes: 14 additions & 1 deletion apps/cli/src/agent/json-event-emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ export class JsonEventEmitter {
private previousContent = new Map<number, string>()
// Track the completion result content
private completionResultContent: string | undefined
// The first non-partial "say:text" per task is the echoed user prompt.
private expectPromptEchoAsUser = true

constructor(options: JsonEventEmitterOptions) {
this.mode = options.mode
Expand Down Expand Up @@ -227,7 +229,14 @@ export class JsonEventEmitter {
private handleSayMessage(msg: ClineMessage, contentToSend: string | null, isDone: boolean): void {
switch (msg.say) {
case "text":
this.emitEvent(this.buildTextEvent("assistant", msg.ts, contentToSend, isDone))
if (this.expectPromptEchoAsUser) {
this.emitEvent(this.buildTextEvent("user", msg.ts, contentToSend, isDone))
if (isDone) {
this.expectPromptEchoAsUser = false
}
} else {
this.emitEvent(this.buildTextEvent("assistant", msg.ts, contentToSend, isDone))
}
break

case "reasoning":
Expand Down Expand Up @@ -396,6 +405,9 @@ export class JsonEventEmitter {
if (this.mode === "json") {
this.outputFinalResult(event.success, resultContent)
}

// Next task in the same process starts with a new echoed prompt.
this.expectPromptEchoAsUser = true
}

/**
Expand Down Expand Up @@ -460,5 +472,6 @@ export class JsonEventEmitter {
this.seenMessageIds.clear()
this.previousContent.clear()
this.completionResultContent = undefined
this.expectPromptEchoAsUser = true
}
}
73 changes: 67 additions & 6 deletions apps/cli/src/commands/cli/run.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fs from "fs"
import path from "path"
import { createInterface } from "readline"
import { fileURLToPath } from "url"

import { createElement } from "react"
Expand Down Expand Up @@ -30,6 +31,24 @@ import { ExtensionHost, ExtensionHostOptions } from "@/agent/index.js"

const __dirname = path.dirname(fileURLToPath(import.meta.url))

async function* readPromptsFromStdinLines(): AsyncGenerator<string> {
const lineReader = createInterface({
input: process.stdin,
crlfDelay: Infinity,
terminal: false,
})

try {
for await (const line of lineReader) {
if (line.trim()) {
yield line
}
}
} finally {
lineReader.close()
}
}

export async function run(promptArg: string | undefined, flagOptions: FlagOptions) {
setLogger({
info: () => {},
Expand Down Expand Up @@ -185,15 +204,42 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption
// Output format only works with --print mode
if (outputFormat !== "text" && !flagOptions.print && isTuiSupported) {
console.error("[CLI] Error: --output-format requires --print mode")
console.error("[CLI] Usage: roo <prompt> --print --output-format json")
console.error("[CLI] Usage: roo --print --output-format json")
process.exit(1)
}

if (flagOptions.stdinPromptStream && !flagOptions.print) {
console.error("[CLI] Error: --stdin-prompt-stream requires --print mode")
console.error("[CLI] Usage: roo --print --stdin-prompt-stream [options]")
process.exit(1)
}

if (flagOptions.stdinPromptStream && process.stdin.isTTY) {
console.error("[CLI] Error: --stdin-prompt-stream requires piped stdin")
console.error("[CLI] Example: printf '1+1=?\\n10!=?\\n' | roo --print --stdin-prompt-stream [options]")
process.exit(1)
}

if (flagOptions.stdinPromptStream && prompt) {
console.error("[CLI] Error: cannot use positional prompt or --prompt-file with --stdin-prompt-stream")
console.error("[CLI] Usage: roo --print --stdin-prompt-stream [options]")
process.exit(1)
}

const useStdinPromptStream = flagOptions.stdinPromptStream

if (!isTuiEnabled) {
if (!prompt) {
console.error("[CLI] Error: prompt is required in print mode")
console.error("[CLI] Usage: roo <prompt> --print [options]")
console.error("[CLI] Run without -p for interactive mode")
if (!prompt && !useStdinPromptStream) {
if (flagOptions.print) {
console.error("[CLI] Error: no prompt provided")
console.error("[CLI] Usage: roo --print [options] <prompt>")
console.error("[CLI] For stdin control mode: roo --print --stdin-prompt-stream [options]")
} else {
console.error("[CLI] Error: prompt is required in non-interactive mode")
console.error("[CLI] Usage: roo <prompt> [options]")
console.error("[CLI] Run without -p for interactive mode")
}

process.exit(1)
}

Expand Down Expand Up @@ -258,7 +304,22 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption
jsonEmitter.attachToClient(host.client)
}

await host.runTask(prompt!)
if (useStdinPromptStream) {
let hasReceivedStdinPrompt = false

for await (const stdinPrompt of readPromptsFromStdinLines()) {
hasReceivedStdinPrompt = true
await host.runTask(stdinPrompt)
jsonEmitter?.clear()
}

if (!hasReceivedStdinPrompt) {
throw new Error("no prompt provided via stdin")
}
} else {
await host.runTask(prompt!)
}

jsonEmitter?.detach()
await host.dispose()
process.exit(0)
Expand Down
1 change: 1 addition & 0 deletions apps/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ program
.option("--prompt-file <path>", "Read prompt from a file instead of command line argument")
.option("-w, --workspace <path>", "Workspace directory path (defaults to current working directory)")
.option("-p, --print", "Print response and exit (non-interactive mode)", false)
.option("--stdin-prompt-stream", "Read prompts from stdin (one prompt per line, requires --print)", false)
.option("-e, --extension <path>", "Path to the extension bundle directory")
.option("-d, --debug", "Enable debug output (includes detailed debug information)", false)
.option("-a, --require-approval", "Require manual approval for actions", false)
Expand Down
1 change: 1 addition & 0 deletions apps/cli/src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type FlagOptions = {
promptFile?: string
workspace?: string
print: boolean
stdinPromptStream: boolean
extension?: string
debug: boolean
requireApproval: boolean
Expand Down
Loading