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

Commit 27a7883

Browse files
cteroomote
andauthored
Add stdin stream mode for the cli (#11476)
* Add stdin stream mode for the cli * fix: clear jsonEmitter state between tasks in stdin-prompt-stream mode * fix: use consistent user role for prompt echo partials in stream-json mode --------- Co-authored-by: Roo Code <roomote@roocode.com>
1 parent 04ffb64 commit 27a7883

5 files changed

Lines changed: 102 additions & 7 deletions

File tree

apps/cli/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,24 @@ In approval-required mode:
100100
- Tool, command, browser, and MCP actions prompt for yes/no approval
101101
- Followup questions wait for manual input (no auto-timeout)
102102

103+
### Print Mode (`--print`)
104+
105+
Use `--print` for non-interactive execution and machine-readable output:
106+
107+
```bash
108+
# Prompt is required
109+
roo --print "Summarize this repository"
110+
```
111+
112+
### Stdin Stream Mode (`--stdin-prompt-stream`)
113+
114+
For programmatic control (one process, multiple prompts), use `--stdin-prompt-stream` with `--print`.
115+
Send one prompt per line via stdin:
116+
117+
```bash
118+
printf '1+1=?\n10!=?\n' | roo --print --stdin-prompt-stream --output-format stream-json
119+
```
120+
103121
### Roo Code Cloud Authentication
104122

105123
To use Roo Code Cloud features (like the provider proxy), you need to authenticate:
@@ -152,6 +170,7 @@ Tokens are valid for 90 days. The CLI will prompt you to re-authenticate when yo
152170
| `--prompt-file <path>` | Read prompt from a file instead of command line argument | None |
153171
| `-w, --workspace <path>` | Workspace path to operate in | Current directory |
154172
| `-p, --print` | Print response and exit (non-interactive mode) | `false` |
173+
| `--stdin-prompt-stream` | Read prompts from stdin (one prompt per line, requires `--print`) | `false` |
155174
| `-e, --extension <path>` | Path to the extension bundle directory | Auto-detected |
156175
| `-d, --debug` | Enable debug output (includes detailed debug information, prompts, paths, etc) | `false` |
157176
| `-a, --require-approval` | Require manual approval before actions execute | `false` |

apps/cli/src/agent/json-event-emitter.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ export class JsonEventEmitter {
9393
private previousContent = new Map<number, string>()
9494
// Track the completion result content
9595
private completionResultContent: string | undefined
96+
// The first non-partial "say:text" per task is the echoed user prompt.
97+
private expectPromptEchoAsUser = true
9698

9799
constructor(options: JsonEventEmitterOptions) {
98100
this.mode = options.mode
@@ -227,7 +229,14 @@ export class JsonEventEmitter {
227229
private handleSayMessage(msg: ClineMessage, contentToSend: string | null, isDone: boolean): void {
228230
switch (msg.say) {
229231
case "text":
230-
this.emitEvent(this.buildTextEvent("assistant", msg.ts, contentToSend, isDone))
232+
if (this.expectPromptEchoAsUser) {
233+
this.emitEvent(this.buildTextEvent("user", msg.ts, contentToSend, isDone))
234+
if (isDone) {
235+
this.expectPromptEchoAsUser = false
236+
}
237+
} else {
238+
this.emitEvent(this.buildTextEvent("assistant", msg.ts, contentToSend, isDone))
239+
}
231240
break
232241

233242
case "reasoning":
@@ -378,6 +387,9 @@ export class JsonEventEmitter {
378387
if (this.mode === "json") {
379388
this.outputFinalResult(event.success, resultContent)
380389
}
390+
391+
// Next task in the same process starts with a new echoed prompt.
392+
this.expectPromptEchoAsUser = true
381393
}
382394

383395
/**
@@ -442,5 +454,6 @@ export class JsonEventEmitter {
442454
this.seenMessageIds.clear()
443455
this.previousContent.clear()
444456
this.completionResultContent = undefined
457+
this.expectPromptEchoAsUser = true
445458
}
446459
}

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

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import fs from "fs"
22
import path from "path"
3+
import { createInterface } from "readline"
34
import { fileURLToPath } from "url"
45

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

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

34+
async function* readPromptsFromStdinLines(): AsyncGenerator<string> {
35+
const lineReader = createInterface({
36+
input: process.stdin,
37+
crlfDelay: Infinity,
38+
terminal: false,
39+
})
40+
41+
try {
42+
for await (const line of lineReader) {
43+
if (line.trim()) {
44+
yield line
45+
}
46+
}
47+
} finally {
48+
lineReader.close()
49+
}
50+
}
51+
3352
export async function run(promptArg: string | undefined, flagOptions: FlagOptions) {
3453
setLogger({
3554
info: () => {},
@@ -185,15 +204,42 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption
185204
// Output format only works with --print mode
186205
if (outputFormat !== "text" && !flagOptions.print && isTuiSupported) {
187206
console.error("[CLI] Error: --output-format requires --print mode")
188-
console.error("[CLI] Usage: roo <prompt> --print --output-format json")
207+
console.error("[CLI] Usage: roo --print --output-format json")
189208
process.exit(1)
190209
}
191210

211+
if (flagOptions.stdinPromptStream && !flagOptions.print) {
212+
console.error("[CLI] Error: --stdin-prompt-stream requires --print mode")
213+
console.error("[CLI] Usage: roo --print --stdin-prompt-stream [options]")
214+
process.exit(1)
215+
}
216+
217+
if (flagOptions.stdinPromptStream && process.stdin.isTTY) {
218+
console.error("[CLI] Error: --stdin-prompt-stream requires piped stdin")
219+
console.error("[CLI] Example: printf '1+1=?\\n10!=?\\n' | roo --print --stdin-prompt-stream [options]")
220+
process.exit(1)
221+
}
222+
223+
if (flagOptions.stdinPromptStream && prompt) {
224+
console.error("[CLI] Error: cannot use positional prompt or --prompt-file with --stdin-prompt-stream")
225+
console.error("[CLI] Usage: roo --print --stdin-prompt-stream [options]")
226+
process.exit(1)
227+
}
228+
229+
const useStdinPromptStream = flagOptions.stdinPromptStream
230+
192231
if (!isTuiEnabled) {
193-
if (!prompt) {
194-
console.error("[CLI] Error: prompt is required in print mode")
195-
console.error("[CLI] Usage: roo <prompt> --print [options]")
196-
console.error("[CLI] Run without -p for interactive mode")
232+
if (!prompt && !useStdinPromptStream) {
233+
if (flagOptions.print) {
234+
console.error("[CLI] Error: no prompt provided")
235+
console.error("[CLI] Usage: roo --print [options] <prompt>")
236+
console.error("[CLI] For stdin control mode: roo --print --stdin-prompt-stream [options]")
237+
} else {
238+
console.error("[CLI] Error: prompt is required in non-interactive mode")
239+
console.error("[CLI] Usage: roo <prompt> [options]")
240+
console.error("[CLI] Run without -p for interactive mode")
241+
}
242+
197243
process.exit(1)
198244
}
199245

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

261-
await host.runTask(prompt!)
307+
if (useStdinPromptStream) {
308+
let hasReceivedStdinPrompt = false
309+
310+
for await (const stdinPrompt of readPromptsFromStdinLines()) {
311+
hasReceivedStdinPrompt = true
312+
await host.runTask(stdinPrompt)
313+
jsonEmitter?.clear()
314+
}
315+
316+
if (!hasReceivedStdinPrompt) {
317+
throw new Error("no prompt provided via stdin")
318+
}
319+
} else {
320+
await host.runTask(prompt!)
321+
}
322+
262323
jsonEmitter?.detach()
263324
await host.dispose()
264325
process.exit(0)

apps/cli/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ program
1616
.option("--prompt-file <path>", "Read prompt from a file instead of command line argument")
1717
.option("-w, --workspace <path>", "Workspace directory path (defaults to current working directory)")
1818
.option("-p, --print", "Print response and exit (non-interactive mode)", false)
19+
.option("--stdin-prompt-stream", "Read prompts from stdin (one prompt per line, requires --print)", false)
1920
.option("-e, --extension <path>", "Path to the extension bundle directory")
2021
.option("-d, --debug", "Enable debug output (includes detailed debug information)", false)
2122
.option("-a, --require-approval", "Require manual approval for actions", false)

apps/cli/src/types/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export type FlagOptions = {
2222
promptFile?: string
2323
workspace?: string
2424
print: boolean
25+
stdinPromptStream: boolean
2526
extension?: string
2627
debug: boolean
2728
requireApproval: boolean

0 commit comments

Comments
 (0)