Skip to content
Open
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
13 changes: 13 additions & 0 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,19 @@ Deep Code 会在当前目录中启动交互式界面。你可以直接输入任
deepcode -p "总结这个项目"
```

### 非交互模式(Headless)

如果你需要在脚本、CI/CD 流水线或 Docker 容器中自动执行任务,可以使用 `--headless` 参数。该模式下 deepcode 不会启动交互界面,执行完 prompt 后自动退出并将结果输出到 stdout:

```bash
deepcode --headless -p "总结这个项目"

# 可与管道组合
deepcode --headless -p "列出所有 API 接口" | grep "auth"
```

`--headless` 必须与 `-p` 搭配使用,且在该模式下所有权限操作均自动批准。

## 第一次可以这样问

可以先从只读任务开始:
Expand Down
13 changes: 13 additions & 0 deletions docs/quickstart_en.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,19 @@ To start with an initial prompt:
deepcode -p "Summarize this project"
```

### Non-Interactive Mode (Headless)

For scripts, CI/CD pipelines, or Docker containers where you need automatic execution, use the `--headless` flag. In this mode, deepcode skips the interactive TUI, runs the prompt, prints the result to stdout, and exits:

```bash
deepcode --headless -p "Summarize this project"

# Pipe with other tools
deepcode --headless -p "List all API endpoints" | grep "auth"
```

`--headless` requires `-p`, and all permission operations are auto-approved in this mode.

## Try These First

Start with a read-only task:
Expand Down
13 changes: 12 additions & 1 deletion packages/cli/src/cli-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export interface ParsedCliArgs {
version: boolean;
/** True when --help / -h was passed */
help: boolean;
/** True when --headless was passed (non-interactive, run-and-exit mode) */
headless: boolean;
}

const EPILOG = [
Expand Down Expand Up @@ -73,7 +75,7 @@ async function configureYargs(argv?: string[]) {
.locale("en")
.scriptName("deepcode")
.usage(
"Usage: $0 [options] [command]\n\nDeep Code - Launch an interactive CLI, use -p/--prompt for non-interactive mode"
"Usage: $0 [options] [command]\n\nDeep Code - Launch an interactive CLI, use -p/--prompt with --headless for non-interactive mode"
)
.command("$0 [query..]", "Launch Deep Code CLI", (yargsInstance: Argv) =>
yargsInstance
Expand All @@ -82,6 +84,10 @@ async function configureYargs(argv?: string[]) {
type: "string",
describe: "Submit a prompt on launch",
})
.option("headless", {
type: "boolean",
describe: "Run in non-interactive (headless) mode. Use with --prompt to execute and exit automatically.",
})
.option("resume", {
alias: "r",
type: "string",
Expand All @@ -106,6 +112,10 @@ async function configureYargs(argv?: string[]) {
if (argv["prompt"] === "") {
return "--prompt / -p requires a non-empty value.";
}
// headless mode requires --prompt
if (argv["headless"] && !argv["prompt"]) {
return "--headless mode requires --prompt / -p with a non-empty value.";
}
return true;
})
)
Expand Down Expand Up @@ -156,5 +166,6 @@ export async function parseArguments(argv?: string[]): Promise<ParsedCliArgs> {
resume,
version: parsed.version === true,
help: parsed.help === true,
headless: parsed.headless === true,
};
}
76 changes: 75 additions & 1 deletion packages/cli/src/cli.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@ import { render } from "ink";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
import { setShellIfWindows, getProjectCode } from "@vegamo/deepcode-core";
import {
setShellIfWindows,
getProjectCode,
SessionManager,
createOpenAIClient,
resolveCurrentSettings,
} from "@vegamo/deepcode-core";
import type { SessionMessage, LlmStreamProgress } from "@vegamo/deepcode-core";
import { checkForNpmUpdate, promptForPendingUpdate } from "./common/update-check";
import { AppContainer } from "./ui";
import { parseArguments } from "./cli-args";
Expand Down Expand Up @@ -32,6 +39,11 @@ async function main(): Promise<void> {
let resumeSessionId = parsed.resume;
const projectRoot = process.cwd();

if (parsed.headless && parsed.prompt) {
await runHeadless(parsed.prompt, projectRoot);
return;
}

if (!process.stdin.isTTY) {
writeStderrLine("deepcode requires an interactive terminal (TTY). Re-run from a real terminal session.\n");
process.exit(1);
Expand Down Expand Up @@ -115,3 +127,65 @@ function configureWindowsShell(): void {
process.exit(1);
}
}

/**
* Run in headless (non-interactive) mode.
* Creates a SessionManager, submits the prompt, outputs the final response, and exits.
*/
async function runHeadless(promptText: string, projectRoot: string): Promise<void> {
process.stderr.write("[headless] Starting non-interactive mode...\n");

const assistantMessages: SessionMessage[] = [];

const sessionManager = new SessionManager({
projectRoot,
createOpenAIClient: () => createOpenAIClient(projectRoot),
getResolvedSettings: () => {
const settings = resolveCurrentSettings(projectRoot);
// Headless = no user to ask → auto-approve all permissions
return {
...settings,
permissions: {
allow: [],
deny: [],
ask: [],
defaultMode: "allowAll" as const,
},
};
},
renderMarkdown: (text: string) => text,
onAssistantMessage: (message: SessionMessage) => {
assistantMessages.push(message);
// Stream content to stdout as it arrives
if (message.content) {
process.stdout.write(message.content as string);
}
},
onLlmStreamProgress: (progress: LlmStreamProgress) => {
if (progress.phase === "start") {
process.stderr.write("[headless] Model is thinking...\n");
} else if (progress.phase === "end") {
process.stderr.write("[headless] Response complete.\n");
}
},
});

try {
// Initialize MCP servers from settings
const settings = resolveCurrentSettings(projectRoot);
await sessionManager.initMcpServers(settings.mcpServers);

// Submit the prompt
await sessionManager.handleUserPrompt({ text: promptText });

process.stdout.write("\n");
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(`[headless] Error: ${message}\n`);
process.exit(1);
} finally {
sessionManager.dispose();
}

process.exit(0);
}