refactor: type parsed flag values and remove entrypoint casts (#1153)
This commit is contained in:
@@ -271,14 +271,34 @@ export const CLI_FLAG_CATALOG = {
|
||||
"max-turns": { parser: { type: "string" }, mode: "headless" },
|
||||
} as const satisfies Record<string, CliFlagDefinition>;
|
||||
|
||||
type CliFlagCatalog = typeof CLI_FLAG_CATALOG;
|
||||
|
||||
type CliCatalogOptionDescriptors = {
|
||||
[K in keyof CliFlagCatalog]: CliFlagCatalog[K]["parser"];
|
||||
};
|
||||
|
||||
type CliParsedValueForDescriptor<Descriptor extends CliFlagParserConfig> =
|
||||
Descriptor["type"] extends "boolean"
|
||||
? Descriptor["multiple"] extends true
|
||||
? boolean[]
|
||||
: boolean
|
||||
: Descriptor["multiple"] extends true
|
||||
? string[]
|
||||
: string;
|
||||
|
||||
export type CliParsedValues = {
|
||||
[K in keyof CliCatalogOptionDescriptors]?: CliParsedValueForDescriptor<
|
||||
CliCatalogOptionDescriptors[K]
|
||||
>;
|
||||
};
|
||||
|
||||
const CLI_FLAG_ENTRIES = Object.entries(CLI_FLAG_CATALOG) as Array<
|
||||
[string, CliFlagDefinition]
|
||||
[keyof CliFlagCatalog, CliFlagDefinition]
|
||||
>;
|
||||
|
||||
export const CLI_OPTIONS: Record<string, CliFlagParserConfig> =
|
||||
Object.fromEntries(
|
||||
CLI_FLAG_ENTRIES.map(([name, definition]) => [name, definition.parser]),
|
||||
);
|
||||
export const CLI_OPTIONS = Object.fromEntries(
|
||||
CLI_FLAG_ENTRIES.map(([name, definition]) => [name, definition.parser]),
|
||||
) as CliCatalogOptionDescriptors;
|
||||
// Column width for left-aligned flag labels in generated --help output.
|
||||
const HELP_LABEL_WIDTH = 24;
|
||||
|
||||
@@ -334,12 +354,16 @@ export function preprocessCliArgs(args: string[]): string[] {
|
||||
}
|
||||
|
||||
export function parseCliArgs(args: string[], strict: boolean) {
|
||||
return parseArgs({
|
||||
const parsed = parseArgs({
|
||||
args,
|
||||
options: CLI_OPTIONS,
|
||||
strict,
|
||||
allowPositionals: true,
|
||||
});
|
||||
return {
|
||||
...parsed,
|
||||
values: parsed.values as CliParsedValues,
|
||||
};
|
||||
}
|
||||
|
||||
export type ParsedCliArgs = ReturnType<typeof parseCliArgs>;
|
||||
|
||||
@@ -151,11 +151,11 @@ type ReflectionOverrides = {
|
||||
};
|
||||
|
||||
function parseReflectionOverrides(
|
||||
values: Record<string, unknown>,
|
||||
values: ParsedCliArgs["values"],
|
||||
): ReflectionOverrides {
|
||||
const triggerRaw = values["reflection-trigger"] as string | undefined;
|
||||
const behaviorRaw = values["reflection-behavior"] as string | undefined;
|
||||
const stepCountRaw = values["reflection-step-count"] as string | undefined;
|
||||
const triggerRaw = values["reflection-trigger"];
|
||||
const behaviorRaw = values["reflection-behavior"];
|
||||
const stepCountRaw = values["reflection-step-count"];
|
||||
|
||||
if (!triggerRaw && !behaviorRaw && !stepCountRaw) {
|
||||
return {};
|
||||
@@ -275,11 +275,11 @@ export async function handleHeadlessCommand(
|
||||
// Set tool filter if provided (controls which tools are loaded)
|
||||
if (values.tools !== undefined) {
|
||||
const { toolFilter } = await import("./tools/filter");
|
||||
toolFilter.setEnabledTools(values.tools as string);
|
||||
toolFilter.setEnabledTools(values.tools);
|
||||
}
|
||||
// Set permission mode if provided (or via --yolo alias)
|
||||
const permissionModeValue = values["permission-mode"] as string | undefined;
|
||||
const yoloMode = values.yolo as boolean | undefined;
|
||||
const permissionModeValue = values["permission-mode"];
|
||||
const yoloMode = values.yolo;
|
||||
if (yoloMode || permissionModeValue) {
|
||||
const { permissionMode } = await import("./permissions/mode");
|
||||
if (yoloMode) {
|
||||
@@ -307,15 +307,15 @@ export async function handleHeadlessCommand(
|
||||
if (values.allowedTools || values.disallowedTools) {
|
||||
const { cliPermissions } = await import("./permissions/cli");
|
||||
if (values.allowedTools) {
|
||||
cliPermissions.setAllowedTools(values.allowedTools as string);
|
||||
cliPermissions.setAllowedTools(values.allowedTools);
|
||||
}
|
||||
if (values.disallowedTools) {
|
||||
cliPermissions.setDisallowedTools(values.disallowedTools as string);
|
||||
cliPermissions.setDisallowedTools(values.disallowedTools);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for input-format early - if stream-json, we don't need a prompt
|
||||
const inputFormat = values["input-format"] as string | undefined;
|
||||
const inputFormat = values["input-format"];
|
||||
const isBidirectionalMode = inputFormat === "stream-json";
|
||||
|
||||
// If headless output is being piped and the downstream closes early (e.g.
|
||||
@@ -372,38 +372,35 @@ export async function handleHeadlessCommand(
|
||||
}
|
||||
|
||||
// --new: Create a new conversation (for concurrent sessions)
|
||||
let forceNewConversation = (values.new as boolean | undefined) ?? false;
|
||||
const fromAgentId = values["from-agent"] as string | undefined;
|
||||
let forceNewConversation = values.new ?? false;
|
||||
const fromAgentId = values["from-agent"];
|
||||
|
||||
// Resolve agent (same logic as interactive mode)
|
||||
let agent: AgentState | null = null;
|
||||
let specifiedAgentId = values.agent as string | undefined;
|
||||
const specifiedAgentName = values.name as string | undefined;
|
||||
let specifiedConversationId = values.conversation as string | undefined;
|
||||
const shouldContinue = values.continue as boolean | undefined;
|
||||
const forceNew = values["new-agent"] as boolean | undefined;
|
||||
const systemPromptPreset = values.system as string | undefined;
|
||||
const systemCustom = values["system-custom"] as string | undefined;
|
||||
const systemAppend = values["system-append"] as string | undefined;
|
||||
const embeddingModel = values.embedding as string | undefined;
|
||||
const memoryBlocksJson = values["memory-blocks"] as string | undefined;
|
||||
const blockValueArgs = values["block-value"] as string[] | undefined;
|
||||
const initBlocksRaw = values["init-blocks"] as string | undefined;
|
||||
const baseToolsRaw = values["base-tools"] as string | undefined;
|
||||
const skillsDirectory =
|
||||
(values.skills as string | undefined) ?? skillsDirectoryOverride;
|
||||
const noSkillsFlag = values["no-skills"] as boolean | undefined;
|
||||
const noBundledSkillsFlag = values["no-bundled-skills"] as
|
||||
| boolean
|
||||
| undefined;
|
||||
const skillSourcesRaw = values["skill-sources"] as string | undefined;
|
||||
const memfsFlag = values.memfs as boolean | undefined;
|
||||
const noMemfsFlag = values["no-memfs"] as boolean | undefined;
|
||||
let specifiedAgentId = values.agent;
|
||||
const specifiedAgentName = values.name;
|
||||
let specifiedConversationId = values.conversation;
|
||||
const shouldContinue = values.continue;
|
||||
const forceNew = values["new-agent"];
|
||||
const systemPromptPreset = values.system;
|
||||
const systemCustom = values["system-custom"];
|
||||
const systemAppend = values["system-append"];
|
||||
const embeddingModel = values.embedding;
|
||||
const memoryBlocksJson = values["memory-blocks"];
|
||||
const blockValueArgs = values["block-value"];
|
||||
const initBlocksRaw = values["init-blocks"];
|
||||
const baseToolsRaw = values["base-tools"];
|
||||
const skillsDirectory = values.skills ?? skillsDirectoryOverride;
|
||||
const noSkillsFlag = values["no-skills"];
|
||||
const noBundledSkillsFlag = values["no-bundled-skills"];
|
||||
const skillSourcesRaw = values["skill-sources"];
|
||||
const memfsFlag = values.memfs;
|
||||
const noMemfsFlag = values["no-memfs"];
|
||||
// Startup policy for the git-backed memory pull on session init.
|
||||
// "blocking" (default): await the pull before proceeding.
|
||||
// "background": fire the pull async, emit init without waiting.
|
||||
// "skip": skip the pull entirely this session.
|
||||
const memfsStartupRaw = values["memfs-startup"] as string | undefined;
|
||||
const memfsStartupRaw = values["memfs-startup"];
|
||||
const memfsStartupPolicy: "blocking" | "background" | "skip" =
|
||||
memfsStartupRaw === "background" || memfsStartupRaw === "skip"
|
||||
? memfsStartupRaw
|
||||
@@ -415,13 +412,12 @@ export async function handleHeadlessCommand(
|
||||
: undefined;
|
||||
const shouldAutoEnableMemfsForNewAgent = !memfsFlag && !noMemfsFlag;
|
||||
const fromAfFile = resolveImportFlagAlias({
|
||||
importFlagValue: values.import as string | undefined,
|
||||
fromAfFlagValue: values["from-af"] as string | undefined,
|
||||
importFlagValue: values.import,
|
||||
fromAfFlagValue: values["from-af"],
|
||||
});
|
||||
const preLoadSkillsRaw = values["pre-load-skills"] as string | undefined;
|
||||
const preLoadSkillsRaw = values["pre-load-skills"];
|
||||
const systemInfoReminderEnabled =
|
||||
systemInfoReminderEnabledOverride ??
|
||||
!(values["no-system-info-reminder"] as boolean | undefined);
|
||||
systemInfoReminderEnabledOverride ?? !values["no-system-info-reminder"];
|
||||
const reflectionOverrides = (() => {
|
||||
try {
|
||||
return parseReflectionOverrides(values);
|
||||
@@ -432,8 +428,8 @@ export async function handleHeadlessCommand(
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
const maxTurnsRaw = values["max-turns"] as string | undefined;
|
||||
const tagsRaw = values.tags as string | undefined;
|
||||
const maxTurnsRaw = values["max-turns"];
|
||||
const tagsRaw = values.tags;
|
||||
const resolvedSkillSources = (() => {
|
||||
if (skillSourcesOverride) {
|
||||
return skillSourcesOverride;
|
||||
@@ -1098,8 +1094,7 @@ export async function handleHeadlessCommand(
|
||||
setAgentContext(agent.id, skillsDirectory, resolvedSkillSources);
|
||||
|
||||
// Validate output format
|
||||
const outputFormat =
|
||||
(values["output-format"] as string | undefined) || "text";
|
||||
const outputFormat = values["output-format"] || "text";
|
||||
const includePartialMessages = Boolean(values["include-partial-messages"]);
|
||||
if (!["text", "json", "stream-json"].includes(outputFormat)) {
|
||||
console.error(
|
||||
|
||||
63
src/index.ts
63
src/index.ts
@@ -443,19 +443,18 @@ async function main(): Promise<void> {
|
||||
}
|
||||
|
||||
// --continue: Resume last session (agent + conversation) automatically
|
||||
const shouldContinue = (values.continue as boolean | undefined) ?? false;
|
||||
const shouldContinue = values.continue ?? false;
|
||||
// --resume: Open agent selector UI after loading
|
||||
const shouldResume = (values.resume as boolean | undefined) ?? false;
|
||||
let specifiedConversationId =
|
||||
(values.conversation as string | undefined) ?? null; // Specific conversation to resume
|
||||
const forceNew = (values["new-agent"] as boolean | undefined) ?? false;
|
||||
const shouldResume = values.resume ?? false;
|
||||
let specifiedConversationId = values.conversation ?? null; // Specific conversation to resume
|
||||
const forceNew = values["new-agent"] ?? false;
|
||||
|
||||
// --new: Create a new conversation (for concurrent sessions)
|
||||
const forceNewConversation = (values.new as boolean | undefined) ?? false;
|
||||
const forceNewConversation = values.new ?? false;
|
||||
|
||||
const initBlocksRaw = values["init-blocks"] as string | undefined;
|
||||
const baseToolsRaw = values["base-tools"] as string | undefined;
|
||||
let specifiedAgentId = (values.agent as string | undefined) ?? null;
|
||||
const initBlocksRaw = values["init-blocks"];
|
||||
const baseToolsRaw = values["base-tools"];
|
||||
let specifiedAgentId = values.agent ?? null;
|
||||
try {
|
||||
const normalized = normalizeConversationShorthandFlags({
|
||||
specifiedConversationId,
|
||||
@@ -486,32 +485,26 @@ async function main(): Promise<void> {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const specifiedAgentName = (values.name as string | undefined) ?? null;
|
||||
const specifiedModel = (values.model as string | undefined) ?? undefined;
|
||||
const systemPromptPreset = (values.system as string | undefined) ?? undefined;
|
||||
const systemCustom =
|
||||
(values["system-custom"] as string | undefined) ?? undefined;
|
||||
const specifiedAgentName = values.name ?? null;
|
||||
const specifiedModel = values.model ?? undefined;
|
||||
const systemPromptPreset = values.system ?? undefined;
|
||||
const systemCustom = values["system-custom"] ?? undefined;
|
||||
// Note: systemAppend is also parsed but only used in headless mode (headless.ts handles it)
|
||||
const memoryBlocksJson =
|
||||
(values["memory-blocks"] as string | undefined) ?? undefined;
|
||||
const specifiedToolset = (values.toolset as string | undefined) ?? undefined;
|
||||
const skillsDirectory = (values.skills as string | undefined) ?? undefined;
|
||||
const memfsFlag = values.memfs as boolean | undefined;
|
||||
const noMemfsFlag = values["no-memfs"] as boolean | undefined;
|
||||
const memoryBlocksJson = values["memory-blocks"] ?? undefined;
|
||||
const specifiedToolset = values.toolset ?? undefined;
|
||||
const skillsDirectory = values.skills ?? undefined;
|
||||
const memfsFlag = values.memfs;
|
||||
const noMemfsFlag = values["no-memfs"];
|
||||
const requestedMemoryPromptMode: "memfs" | "standard" | undefined = memfsFlag
|
||||
? "memfs"
|
||||
: noMemfsFlag
|
||||
? "standard"
|
||||
: undefined;
|
||||
const shouldAutoEnableMemfsForNewAgent = !memfsFlag && !noMemfsFlag;
|
||||
const noSkillsFlag = values["no-skills"] as boolean | undefined;
|
||||
const noBundledSkillsFlag = values["no-bundled-skills"] as
|
||||
| boolean
|
||||
| undefined;
|
||||
const skillSourcesRaw = values["skill-sources"] as string | undefined;
|
||||
const noSystemInfoReminderFlag = values["no-system-info-reminder"] as
|
||||
| boolean
|
||||
| undefined;
|
||||
const noSkillsFlag = values["no-skills"];
|
||||
const noBundledSkillsFlag = values["no-bundled-skills"];
|
||||
const skillSourcesRaw = values["skill-sources"];
|
||||
const noSystemInfoReminderFlag = values["no-system-info-reminder"];
|
||||
const resolvedSkillSources = (() => {
|
||||
try {
|
||||
return resolveSkillSourcesSelection({
|
||||
@@ -527,8 +520,8 @@ async function main(): Promise<void> {
|
||||
}
|
||||
})();
|
||||
const fromAfFile = resolveImportFlagAlias({
|
||||
importFlagValue: values.import as string | undefined,
|
||||
fromAfFlagValue: values["from-af"] as string | undefined,
|
||||
importFlagValue: values.import,
|
||||
fromAfFlagValue: values["from-af"],
|
||||
});
|
||||
const isHeadless = values.prompt || values.run || !process.stdin.isTTY;
|
||||
|
||||
@@ -865,23 +858,23 @@ async function main(): Promise<void> {
|
||||
// Set tool filter if provided (controls which tools are loaded)
|
||||
if (values.tools !== undefined) {
|
||||
const { toolFilter } = await import("./tools/filter");
|
||||
toolFilter.setEnabledTools(values.tools as string);
|
||||
toolFilter.setEnabledTools(values.tools);
|
||||
}
|
||||
|
||||
// Set CLI permission overrides if provided
|
||||
if (values.allowedTools || values.disallowedTools) {
|
||||
const { cliPermissions } = await import("./permissions/cli");
|
||||
if (values.allowedTools) {
|
||||
cliPermissions.setAllowedTools(values.allowedTools as string);
|
||||
cliPermissions.setAllowedTools(values.allowedTools);
|
||||
}
|
||||
if (values.disallowedTools) {
|
||||
cliPermissions.setDisallowedTools(values.disallowedTools as string);
|
||||
cliPermissions.setDisallowedTools(values.disallowedTools);
|
||||
}
|
||||
}
|
||||
|
||||
// Set permission mode if provided (or via --yolo alias)
|
||||
const permissionModeValue = values["permission-mode"] as string | undefined;
|
||||
const yoloMode = values.yolo as boolean | undefined;
|
||||
const permissionModeValue = values["permission-mode"];
|
||||
const yoloMode = values.yolo;
|
||||
|
||||
if (yoloMode || permissionModeValue) {
|
||||
if (yoloMode) {
|
||||
|
||||
@@ -16,7 +16,14 @@ describe("shared CLI arg schema", () => {
|
||||
const validModes = new Set(["interactive", "headless", "both"]);
|
||||
const validTypes = new Set(["boolean", "string"]);
|
||||
|
||||
for (const [flagName, definition] of Object.entries(CLI_FLAG_CATALOG)) {
|
||||
for (const [flagName, definition] of Object.entries(
|
||||
CLI_FLAG_CATALOG,
|
||||
) as Array<
|
||||
[
|
||||
keyof typeof CLI_FLAG_CATALOG,
|
||||
(typeof CLI_FLAG_CATALOG)[keyof typeof CLI_FLAG_CATALOG],
|
||||
]
|
||||
>) {
|
||||
expect(validModes.has(definition.mode)).toBe(true);
|
||||
expect(validTypes.has(definition.parser.type)).toBe(true);
|
||||
expect(CLI_OPTIONS[flagName]).toEqual(definition.parser);
|
||||
|
||||
Reference in New Issue
Block a user