Skip to content

Commit a2daa82

Browse files
committed
fix: strip ANSI cursor escapes from non-TTY stdout/stderr
ink (via cli-cursor) writes \x1b[?25h to its render stream on App unmount. The cli-cursor isTTY check was passing because ink's stdout wrapper inherits isTTY from the underlying stream object, even when stdout is piped or redirected. The trailing escape corrupted machine-parseable output for every CLI invocation that captured stdout — most visibly --json (broke JSON parsing) but also any pipe/redirect workflow. Found by the round-2 bug bash (E6) when an agent tried to `json.load()` the output of `agentcore add interceptor --json` and got "Extra data" — the bytes after the JSON were the cursor-show escape. Fix: at the CLI entry, wrap process.stdout and process.stderr writes to strip \x1b[?25[hl] (cursor show / hide) when the stream is NOT a TTY. Real TTYs are untouched so interactive terminal behavior — cursor hiding during spinners, restoring on exit — keeps working. Verified live with the bundled CLI: - agentcore --version: ends with newline only, no escape - agentcore validate: ends with newline only - agentcore add gateway --json: produces valid JSON, json.load() succeeds
1 parent 23877fd commit a2daa82

1 file changed

Lines changed: 25 additions & 0 deletions

File tree

src/cli/index.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,31 @@
22
import { main } from './cli.js';
33
import { getErrorMessage } from './errors.js';
44

5+
// Strip cursor show/hide ANSI escapes from non-TTY stdout/stderr.
6+
// ink (via cli-cursor) writes `\x1b[?25h` on exit, even when stdout is piped or
7+
// redirected. That trailing escape corrupts machine-parseable output (e.g. the
8+
// `--json` flag). Filter the two known cursor escapes while leaving real text
9+
// alone. Real TTYs keep the escapes so interactive terminal behavior is
10+
// unchanged.
11+
// eslint-disable-next-line no-control-regex -- \x1b is the ANSI escape we are explicitly filtering
12+
const CURSOR_ESCAPE_PATTERN = /\x1b\[\?25[hl]/g;
13+
function patchStream(stream: NodeJS.WriteStream): void {
14+
if (stream.isTTY) return;
15+
const originalWrite = stream.write.bind(stream);
16+
stream.write = ((chunk: unknown, ...rest: unknown[]) => {
17+
if (typeof chunk === 'string') {
18+
return originalWrite(chunk.replace(CURSOR_ESCAPE_PATTERN, ''), ...(rest as [never]));
19+
}
20+
if (chunk instanceof Buffer) {
21+
const filtered = chunk.toString('utf8').replace(CURSOR_ESCAPE_PATTERN, '');
22+
return originalWrite(filtered, ...(rest as [never]));
23+
}
24+
return originalWrite(chunk as never, ...(rest as [never]));
25+
}) as typeof stream.write;
26+
}
27+
patchStream(process.stdout);
28+
patchStream(process.stderr);
29+
530
// Global safety net — prevent raw stack traces from reaching the user
631
process.on('uncaughtException', err => {
732
console.error(`Error: ${getErrorMessage(err)}`);

0 commit comments

Comments
 (0)