mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
fix(cli): require input source for --quiet and drop empty final-only JSON on failure
- `--quiet` with neither `--prompt` nor `--input-format` now fails validation
("Quiet mode requires --prompt or --input-format.") instead of entering print
mode and crashing on an undefined prompt.
- `PromptFinalJsonWriter.finish()` skips emitting when there is no final text,
matching the text writer — a failed `--final-message-only` turn no longer
prints a spurious `{"role":"assistant","content":""}` before its error line.
Adds a changeset for the print-mode stream-json work.
This commit is contained in:
parent
d61fc2d628
commit
3bea6f9fa2
5 changed files with 60 additions and 5 deletions
5
.changeset/print-mode-stream-json.md
Normal file
5
.changeset/print-mode-stream-json.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Expand `kimi -p` print mode to cover (and slightly exceed) kimi-cli's non-interactive `stream-json` capabilities: emit model thinking as its own JSONL line; add `--input-format text|stream-json` (multi-turn prompts over stdin), `--final-message-only` and the `--quiet` shorthand; surface background-task/cron events as `notification` lines; report turn failures as `{"type":"error",...}` JSON with retryable provider errors mapped to exit code 75; wait for background tasks before exit (gated by `background.keepAliveOnExit`); and emit the TUI activity layer (tool progress, subagent lifecycle, warnings, skill/MCP/compaction/goal/agent-status/tool-list updates) on stdout so the stream-json output stays both complete and entirely JSON.
|
||||
|
|
@ -53,6 +53,11 @@ export function validateOptions(opts: CLIOptions): ValidatedOptions {
|
|||
if (opts.quiet === true && opts.outputFormat !== undefined && opts.outputFormat !== 'text') {
|
||||
throw new OptionConflictError('Quiet mode implies --output-format text.');
|
||||
}
|
||||
// Prompt mode needs an input source. `--prompt` and `--input-format` provide
|
||||
// one; `--quiet` on its own does not, so it must be paired with either.
|
||||
if (promptMode && !hasPrompt && opts.inputFormat === undefined) {
|
||||
throw new OptionConflictError('Quiet mode requires --prompt or --input-format.');
|
||||
}
|
||||
if (hasPrompt && opts.inputFormat !== undefined) {
|
||||
throw new OptionConflictError(
|
||||
'Cannot combine --prompt with --input-format; the prompt is read from stdin.',
|
||||
|
|
|
|||
|
|
@ -950,7 +950,12 @@ class PromptFinalJsonWriter implements PromptTurnWriter {
|
|||
}
|
||||
|
||||
finish(): void {
|
||||
this.stdout.write(`${JSON.stringify({ role: 'assistant', content: this.assistantText })}\n`);
|
||||
// Only emit when there is a final message: a failed turn settles through
|
||||
// finish() before its error line, so an empty emit would read as a spurious
|
||||
// successful empty answer. Mirrors PromptFinalTextWriter.
|
||||
if (this.assistantText.length > 0) {
|
||||
this.stdout.write(`${JSON.stringify({ role: 'assistant', content: this.assistantText })}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -362,19 +362,31 @@ describe('CLI options parsing', () => {
|
|||
expect(parse([]).quiet).toBe(false);
|
||||
});
|
||||
|
||||
it('enters print mode on its own', () => {
|
||||
const opts = parse(['--quiet']);
|
||||
it('enters print mode when paired with --prompt', () => {
|
||||
const opts = parse(['--quiet', '-p', 'hi']);
|
||||
expect(opts.quiet).toBe(true);
|
||||
expect(validateOptions(opts).uiMode).toBe('print');
|
||||
});
|
||||
|
||||
it('enters print mode when paired with --input-format', () => {
|
||||
const opts = parse(['--quiet', '--input-format', 'text']);
|
||||
expect(opts.quiet).toBe(true);
|
||||
expect(validateOptions(opts).uiMode).toBe('print');
|
||||
});
|
||||
|
||||
it('rejects --quiet on its own (no input source)', () => {
|
||||
const opts = parse(['--quiet']);
|
||||
expect(() => validateOptions(opts)).toThrow(OptionConflictError);
|
||||
expect(() => validateOptions(opts)).toThrow('Quiet mode requires --prompt or --input-format.');
|
||||
});
|
||||
|
||||
it('is allowed alongside an explicit --output-format text', () => {
|
||||
const opts = parse(['--quiet', '--output-format', 'text']);
|
||||
const opts = parse(['--quiet', '-p', 'hi', '--output-format', 'text']);
|
||||
expect(() => validateOptions(opts)).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects --quiet with --output-format stream-json', () => {
|
||||
const opts = parse(['--quiet', '--output-format', 'stream-json']);
|
||||
const opts = parse(['--quiet', '-p', 'hi', '--output-format', 'stream-json']);
|
||||
expect(() => validateOptions(opts)).toThrow(OptionConflictError);
|
||||
expect(() => validateOptions(opts)).toThrow('Quiet mode implies --output-format text.');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -811,6 +811,34 @@ describe('runPrompt', () => {
|
|||
expect(stderr.text()).toBe('');
|
||||
});
|
||||
|
||||
it('does not emit an empty final assistant message when a final-only turn fails', async () => {
|
||||
mocks.session.prompt.mockImplementationOnce(async () => {
|
||||
for (const handler of mocks.eventHandlers) {
|
||||
handler(mocks.mainEvent({ type: 'turn.started', turnId: 41, origin: { kind: 'user' } }));
|
||||
handler(
|
||||
mocks.mainEvent({
|
||||
type: 'turn.ended',
|
||||
turnId: 41,
|
||||
reason: 'failed',
|
||||
error: { code: 'provider.api_error', message: 'boom', retryable: false },
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
const stdout = writer();
|
||||
|
||||
await runPrompt(opts({ outputFormat: 'stream-json', finalMessageOnly: true }), '1.2.3-test', {
|
||||
stdout,
|
||||
stderr: writer(),
|
||||
});
|
||||
|
||||
// Only the error line — no spurious `{"role":"assistant","content":""}` first.
|
||||
expect(stdout.text()).toBe(
|
||||
'{"type":"error","code":"provider.api_error","message":"boom","retryable":false}\n',
|
||||
);
|
||||
expect(process.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it('emits only the final text in text final-message-only mode and skips the resume hint (output B)', async () => {
|
||||
const stdout = writer();
|
||||
const stderr = writer();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue