mirror of
https://github.com/morluto/rea.git
synced 2026-10-22 05:42:31 -05:00
* fix(cli): accept --flag=value for global output flags `rea <command> --format=json` failed with "Unknown flag: --format". `--filter-output=...`, `--token-limit=...` and `--token-offset=...` failed the same way. Incur reads these global flags only in spaced form, so the equals form fell through to the command parser. Command options already accept both forms, and `rea mcp doctor` parses `--format=` itself. A valued output flag with no value (`--format` last, or `--format=`) also reported the flag as unknown with code UNKNOWN. normalizeCliOutputArguments, in the module that owns output-argument interpretation, now splits the equals forms of the four valued output flags. The entry point passes the normalized argv to rea's output validation and to Incur, so both read the same effective values. A missing value now fails with MISSING_OUTPUT_VALUE and names the flag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cli): detect missing output values while parsing output flags The missing-value check scanned argv again after parsing, so it also checked tokens the parser had already consumed as values. For example, `--filter-output --format` selects the field `--format`, yet the second scan reported `--format` as missing a value. parseCliOutputArguments now records the first valued flag it skips for lack of a value, in the same pass that consumes values. Cover normalizeCliOutputArguments directly: values containing `=`, empty values, and command options that only share a prefix. Export it in the dispatcher test's runtime stub as well. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
205 lines
7.0 KiB
TypeScript
205 lines
7.0 KiB
TypeScript
const UNSUPPORTED_OUTPUT_COMBINATION_MESSAGE =
|
|
"Token windows cannot preserve structured output. Remove --token-limit/--token-offset and use --filter-output or command pagination.";
|
|
|
|
type StructuredOutputFormat = "json" | "jsonl" | "yaml";
|
|
const INCUR_OUTPUT_FORMATS = new Set(["toon", "json", "yaml", "md", "jsonl"]);
|
|
/** Global output flags that take a value. */
|
|
const VALUED_OUTPUT_FLAGS = new Set([
|
|
"--format",
|
|
"--filter-output",
|
|
"--token-limit",
|
|
"--token-offset",
|
|
]);
|
|
|
|
/**
|
|
* Split `--flag=value` forms of the valued output flags. Incur reads these
|
|
* flags only in spaced form, so `--format=json` otherwise reaches the command
|
|
* parser and fails as an unknown flag. Command options accept both forms.
|
|
*/
|
|
export const normalizeCliOutputArguments = (
|
|
arguments_: readonly string[],
|
|
): string[] =>
|
|
arguments_.flatMap((argument) => {
|
|
const separator = argument.indexOf("=");
|
|
if (separator < 0) return [argument];
|
|
const flag = argument.slice(0, separator);
|
|
return VALUED_OUTPUT_FLAGS.has(flag)
|
|
? [flag, argument.slice(separator + 1)]
|
|
: [argument];
|
|
});
|
|
|
|
export type CliOutputArgumentValidation =
|
|
| { readonly ok: true }
|
|
| {
|
|
readonly ok: false;
|
|
readonly format: StructuredOutputFormat;
|
|
readonly code:
|
|
| "UNSUPPORTED_OUTPUT_COMBINATION"
|
|
| "INVALID_OUTPUT_FILTER"
|
|
| "MISSING_OUTPUT_VALUE";
|
|
readonly message: string;
|
|
};
|
|
|
|
interface ParsedCliOutputArguments {
|
|
readonly filterOutput: string | undefined;
|
|
readonly format: string;
|
|
readonly fullOutput: boolean;
|
|
/** First valued output flag that Incur skips because it has no value. */
|
|
readonly missingValue: string | undefined;
|
|
readonly parseError: boolean;
|
|
readonly tokenCount: boolean;
|
|
readonly tokenWindow: boolean;
|
|
readonly tokenLimit: number | undefined;
|
|
readonly tokenOffset: number | undefined;
|
|
}
|
|
|
|
/** Read effective Incur output controls at the executable boundary. */
|
|
export const parseCliOutputArguments = (
|
|
arguments_: readonly string[],
|
|
): ParsedCliOutputArguments => {
|
|
let format: string = "toon";
|
|
let filterOutput: string | undefined;
|
|
let fullOutput = false;
|
|
let missingValue: string | undefined;
|
|
let tokenCount = false;
|
|
let tokenLimit: number | undefined;
|
|
let tokenOffset: number | undefined;
|
|
const parsed = (parseError: boolean): ParsedCliOutputArguments => ({
|
|
filterOutput,
|
|
format,
|
|
fullOutput,
|
|
missingValue,
|
|
parseError,
|
|
tokenCount,
|
|
tokenWindow:
|
|
!parseError && (tokenLimit !== undefined || tokenOffset !== undefined),
|
|
tokenLimit: parseError ? undefined : tokenLimit,
|
|
tokenOffset: parseError ? undefined : tokenOffset,
|
|
});
|
|
for (let index = 0; index < arguments_.length; index += 1) {
|
|
const argument = arguments_[index];
|
|
// Incur scans builtins across `--` and reads spaced value forms, which
|
|
// normalizeCliOutputArguments has produced. Mirror those effective values.
|
|
if (argument === "--json") {
|
|
format = "json";
|
|
continue;
|
|
}
|
|
if (argument === "--format") {
|
|
const value = arguments_[index + 1];
|
|
if (value) {
|
|
// Incur stops parsing at an invalid format before applying any later
|
|
// output controls. Let it report that malformed flag itself.
|
|
if (!INCUR_OUTPUT_FORMATS.has(value)) return parsed(true);
|
|
format = value;
|
|
index += 1;
|
|
} else missingValue ??= argument;
|
|
continue;
|
|
}
|
|
if (argument === "--filter-output") {
|
|
if (arguments_[index + 1]) {
|
|
filterOutput = arguments_[index + 1];
|
|
index += 1;
|
|
} else missingValue ??= argument;
|
|
continue;
|
|
}
|
|
if (argument === "--token-limit" || argument === "--token-offset") {
|
|
const value = arguments_[index + 1];
|
|
if (value) {
|
|
const numericValue = Number(value);
|
|
if (!Number.isFinite(numericValue) || value.trim() === "")
|
|
return parsed(true);
|
|
if (argument === "--token-limit") tokenLimit = numericValue;
|
|
else tokenOffset = numericValue;
|
|
index += 1;
|
|
} else missingValue ??= argument;
|
|
continue;
|
|
}
|
|
if (argument === "--full-output") fullOutput = true;
|
|
if (argument === "--token-count") tokenCount = true;
|
|
}
|
|
return parsed(false);
|
|
};
|
|
|
|
/** Reject slices that cannot advance within Incur's comma-delimited paths. */
|
|
const hasUnclosedFilterSlice = (expression: string): boolean => {
|
|
let depth = 0;
|
|
let unclosed = false;
|
|
for (const character of expression) {
|
|
if (character === "[") {
|
|
depth += 1;
|
|
unclosed = true;
|
|
} else if (character === "]") {
|
|
depth -= 1;
|
|
// Incur consumes everything through the next closing bracket, including
|
|
// nested opening brackets. Do not impose a different filter grammar.
|
|
unclosed = false;
|
|
} else if (character === "," && depth === 0 && unclosed) return true;
|
|
}
|
|
return unclosed;
|
|
};
|
|
|
|
/** Reject output controls that cannot safely produce a complete result. */
|
|
export const validateCliOutputArguments = (
|
|
arguments_: readonly string[],
|
|
): CliOutputArgumentValidation => {
|
|
const { filterOutput, format, missingValue, parseError, tokenWindow } =
|
|
parseCliOutputArguments(arguments_);
|
|
if (parseError) return { ok: true };
|
|
if (filterOutput !== undefined && hasUnclosedFilterSlice(filterOutput))
|
|
return {
|
|
ok: false,
|
|
format: format === "yaml" || format === "jsonl" ? format : "json",
|
|
code: "INVALID_OUTPUT_FILTER",
|
|
message:
|
|
"The --filter-output expression has a slice without a closing ]. Close the slice or remove --filter-output.",
|
|
};
|
|
if (
|
|
tokenWindow &&
|
|
(format === "json" || format === "jsonl" || format === "yaml")
|
|
)
|
|
return {
|
|
ok: false,
|
|
format,
|
|
code: "UNSUPPORTED_OUTPUT_COMBINATION",
|
|
message: UNSUPPORTED_OUTPUT_COMBINATION_MESSAGE,
|
|
};
|
|
// Incur skips a valued flag without a value and then reports the flag
|
|
// itself as unknown. Name the missing value instead.
|
|
if (missingValue !== undefined)
|
|
return {
|
|
ok: false,
|
|
format: format === "yaml" || format === "jsonl" ? format : "json",
|
|
code: "MISSING_OUTPUT_VALUE",
|
|
message: `${missingValue} requires a value, as in ${missingValue} <value> or ${missingValue}=<value>.`,
|
|
};
|
|
return { ok: true };
|
|
};
|
|
|
|
/** Preserve a complete document when Incur filters the entire result away. */
|
|
export const renderEmptyFilteredCliOutput = (
|
|
arguments_: readonly string[],
|
|
): string | undefined => {
|
|
const { filterOutput, format, parseError } =
|
|
parseCliOutputArguments(arguments_);
|
|
if (parseError) return undefined;
|
|
if (
|
|
filterOutput &&
|
|
(format === "json" || format === "jsonl" || format === "yaml")
|
|
)
|
|
return "{}\n";
|
|
return undefined;
|
|
};
|
|
|
|
/** Render one complete structured error before command execution. */
|
|
export const renderCliOutputArgumentError = (
|
|
error: Exclude<CliOutputArgumentValidation, { readonly ok: true }>,
|
|
): string => {
|
|
const value = {
|
|
ok: false,
|
|
error: { code: error.code, message: error.message },
|
|
};
|
|
if (error.format === "yaml")
|
|
return `ok: false\nerror:\n code: ${error.code}\n message: ${JSON.stringify(error.message)}\n`;
|
|
return `${JSON.stringify(value)}\n`;
|
|
};
|