diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 11d4830cc..774fd91cf 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed redirected stdin being ignored when Bun reports a pipe with an undefined `isTTY`, so JSON/print sessions now process piped prompts and persist them under `--session-dir` or `PI_CODING_AGENT_SESSION_DIR` as requested ([#7378](https://github.com/can1357/oh-my-pi/issues/7378)). + ## [17.2.4] - 2026-08-01 ### Added diff --git a/packages/coding-agent/src/cli/args.ts b/packages/coding-agent/src/cli/args.ts index da606c156..0334039a5 100644 --- a/packages/coding-agent/src/cli/args.ts +++ b/packages/coding-agent/src/cli/args.ts @@ -1,7 +1,7 @@ /** * CLI argument parsing and help display */ -import { APP_NAME, CONFIG_DIR_NAME, logger } from "@oh-my-pi/pi-utils"; +import { $env, APP_NAME, CONFIG_DIR_NAME, logger } from "@oh-my-pi/pi-utils"; import chalk from "chalk"; import { CLI_THINKING_LEVELS, type ConfiguredThinkingLevel, parseCliThinkingLevel } from "../thinking"; import { BUILTIN_TOOL_NAMES, HIDDEN_TOOL_NAMES, normalizeToolNames } from "../tools/builtin-names"; @@ -145,6 +145,7 @@ export function parseArgs(inputArgs: string[], extensionFlags?: Map { - if (process.stdin.isTTY !== false) return undefined; +/** Reads a non-TTY stdin stream as prompt text. */ +export async function readPipedInput(): Promise { + if (process.stdin.isTTY === true) return undefined; // stdin is a pipe: a producer that never writes nor closes would block // startup forever with zero output. Say what we're blocked on after 1s. const notice = setTimeout(() => { diff --git a/packages/coding-agent/test/flag-tables.test.ts b/packages/coding-agent/test/flag-tables.test.ts index b1f6b1a6a..23e3b52d1 100644 --- a/packages/coding-agent/test/flag-tables.test.ts +++ b/packages/coding-agent/test/flag-tables.test.ts @@ -56,6 +56,23 @@ describe("OPTIONAL_VALUE_FLAGS table is honored by args.ts parseArgs", () => { } }); +describe("--session-dir", () => { + it("uses PI_CODING_AGENT_SESSION_DIR unless the CLI flag overrides it", () => { + const previous = Bun.env.PI_CODING_AGENT_SESSION_DIR; + Bun.env.PI_CODING_AGENT_SESSION_DIR = "/env/sessions"; + try { + expect(parseArgs([]).sessionDir).toBe("/env/sessions"); + expect(parseArgs(["--session-dir", "/cli/sessions"]).sessionDir).toBe("/cli/sessions"); + } finally { + if (previous === undefined) { + delete Bun.env.PI_CODING_AGENT_SESSION_DIR; + } else { + Bun.env.PI_CODING_AGENT_SESSION_DIR = previous; + } + } + }); +}); + describe("--tools legacy aliases", () => { it("maps search and find to grep and glob", () => { const result = parseArgs(["--tools", "search,find,grep"]); diff --git a/packages/coding-agent/test/main-interactive-input.test.ts b/packages/coding-agent/test/main-interactive-input.test.ts index 3f1acf42b..38fded1d6 100644 --- a/packages/coding-agent/test/main-interactive-input.test.ts +++ b/packages/coding-agent/test/main-interactive-input.test.ts @@ -2,7 +2,11 @@ import { afterEach, describe, expect, it, vi } from "bun:test"; import * as fs from "node:fs/promises"; import * as os from "node:os"; import * as path from "node:path"; -import { applyResolvedSystemPromptInputs, submitInteractiveInput } from "@oh-my-pi/pi-coding-agent/main"; +import { + applyResolvedSystemPromptInputs, + readPipedInput, + submitInteractiveInput, +} from "@oh-my-pi/pi-coding-agent/main"; import type { SubmittedUserInput } from "@oh-my-pi/pi-coding-agent/modes/types"; import type { CreateAgentSessionOptions } from "@oh-my-pi/pi-coding-agent/sdk"; import { discoverTitleSystemPromptFile } from "@oh-my-pi/pi-coding-agent/system-prompt"; @@ -12,6 +16,7 @@ const cleanupDirs: string[] = []; afterEach(async () => { await Promise.all(cleanupDirs.splice(0).map(dir => removeWithRetries(dir))); + vi.restoreAllMocks(); }); function createInput(overrides: Partial = {}): SubmittedUserInput { @@ -37,6 +42,21 @@ describe("discoverTitleSystemPromptFile", () => { }); }); +describe("readPipedInput", () => { + it("reads redirected stdin when Bun reports isTTY as undefined", async () => { + const originalIsTTY = process.stdin.isTTY; + const readText = vi.spyOn(Bun.stdin, "text").mockResolvedValue("piped prompt\n"); + Object.defineProperty(process.stdin, "isTTY", { value: undefined, configurable: true }); + + try { + expect(await readPipedInput()).toBe("piped prompt\n"); + expect(readText).toHaveBeenCalledTimes(1); + } finally { + Object.defineProperty(process.stdin, "isTTY", { value: originalIsTTY, configurable: true }); + } + }); +}); + describe("applyResolvedSystemPromptInputs", () => { it("routes SYSTEM.md content through template-aware session options", () => { const options: CreateAgentSessionOptions = {};