Skip to content

Commit 3f8220a

Browse files
committed
fix: use ValidationError, eliminate double config load, move header parsing inside telemetry wrapper
- Use ValidationError for validation and prompt resolution failures - Pass pre-loaded InvokeContext into handleInvokeCLI to avoid reading config files twice on the happy path - Move parseHeaderFlags inside the telemetry wrapper so invalid -H values are recorded as failures
1 parent f26ffa2 commit 3f8220a

1 file changed

Lines changed: 52 additions & 38 deletions

File tree

src/cli/commands/invoke/command.tsx

Lines changed: 52 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { type Result, serializeResult } from '../../../lib';
1+
import { type Result, ValidationError, serializeResult } from '../../../lib';
22
import { getErrorMessage } from '../../errors';
33
import { withCommandRunTelemetry } from '../../telemetry/cli-command-run.js';
44
import { AuthType, Protocol, standardize } from '../../telemetry/schemas/common-shapes.js';
55
import { COMMAND_DESCRIPTIONS } from '../../tui/copy';
66
import { requireProject, requireTTY } from '../../tui/guards';
77
import { InvokeScreen } from '../../tui/screens/invoke';
88
import { parseHeaderFlags } from '../shared/header-utils';
9-
import { handleInvoke, loadInvokeConfig } from './action';
9+
import { type InvokeContext, handleInvoke, loadInvokeConfig } from './action';
1010
import { resolvePrompt } from './resolve-prompt';
1111
import type { InvokeOptions, InvokeResult } from './types';
1212
import { validateInvokeOptions } from './validate';
@@ -36,16 +36,16 @@ function resolveProtocol(options: InvokeOptions, projectProtocol?: string): stri
3636
return 'http';
3737
}
3838

39-
async function handleInvokeCLI(options: InvokeOptions): Promise<InvokeResult> {
39+
async function handleInvokeCLI(options: InvokeOptions, preloadedContext?: InvokeContext): Promise<InvokeResult> {
4040
const validation = validateInvokeOptions(options);
4141
if (!validation.valid) {
42-
return { success: false, error: new Error(validation.error) };
42+
return { success: false, error: new ValidationError(validation.error ?? 'Validation failed') };
4343
}
4444

4545
let spinner: NodeJS.Timeout | undefined;
4646

4747
try {
48-
const context = await loadInvokeConfig();
48+
const context = preloadedContext ?? (await loadInvokeConfig());
4949

5050
// Show spinner for non-streaming, non-json, non-exec invocations
5151
if (!options.stream && !options.json && !options.exec) {
@@ -150,19 +150,14 @@ export const registerInvoke = (program: Command) => {
150150
try {
151151
requireProject();
152152

153-
// Parse custom headers
154-
let headers: Record<string, string> | undefined;
155-
if (cliOptions.header && cliOptions.header.length > 0) {
156-
headers = parseHeaderFlags(cliOptions.header);
157-
}
158-
159-
// Determine protocol from project config (best-effort for telemetry)
153+
// Load config once for protocol resolution and to pass into handleInvokeCLI
154+
let invokeContext: InvokeContext | undefined;
160155
let agentProtocol: string | undefined;
161156
try {
162-
const { project } = await loadInvokeConfig();
157+
invokeContext = await loadInvokeConfig();
163158
const agent = cliOptions.runtime
164-
? project.runtimes.find(a => a.name === cliOptions.runtime)
165-
: project.runtimes[0];
159+
? invokeContext.project.runtimes.find(a => a.name === cliOptions.runtime)
160+
: invokeContext.project.runtimes[0];
166161
agentProtocol = agent?.protocol;
167162
} catch {
168163
// Config load failure will be caught again inside handleInvokeCLI
@@ -189,44 +184,63 @@ export const registerInvoke = (program: Command) => {
189184
cliOptions.exec ||
190185
cliOptions.bearerToken
191186
) {
192-
const options: InvokeOptions = {
193-
prompt: resolved.prompt,
194-
agentName: cliOptions.runtime,
195-
targetName: cliOptions.target ?? 'default',
196-
sessionId: cliOptions.sessionId,
197-
userId: cliOptions.userId,
198-
json: cliOptions.json,
199-
stream: cliOptions.stream,
200-
tool: cliOptions.tool,
201-
input: cliOptions.input,
202-
exec: cliOptions.exec,
203-
timeout: cliOptions.timeout,
204-
headers,
205-
bearerToken: cliOptions.bearerToken,
206-
};
207-
208187
const result = await withCommandRunTelemetry(
209188
'invoke',
210189
{
211-
has_stream: options.stream ?? false,
212-
has_session_id: !!options.sessionId,
213-
auth_type: standardize(AuthType, options.bearerToken ? 'bearer_token' : 'sigv4'),
214-
protocol: standardize(Protocol, resolveProtocol(options, agentProtocol)),
190+
has_stream: cliOptions.stream ?? false,
191+
has_session_id: !!cliOptions.sessionId,
192+
auth_type: standardize(AuthType, cliOptions.bearerToken ? 'bearer_token' : 'sigv4'),
193+
protocol: standardize(
194+
Protocol,
195+
resolveProtocol({ tool: cliOptions.tool } as InvokeOptions, agentProtocol)
196+
),
215197
},
216198
async (): Promise<InvokeResult> => {
217199
if (!resolved.success) {
218-
return { success: false, error: new Error(resolved.error ?? 'Prompt resolution failed') };
200+
return { success: false, error: new ValidationError(resolved.error ?? 'Prompt resolution failed') };
219201
}
220-
return handleInvokeCLI(options);
202+
203+
// Parse custom headers inside wrapper so failures are recorded
204+
let headers: Record<string, string> | undefined;
205+
if (cliOptions.header && cliOptions.header.length > 0) {
206+
headers = parseHeaderFlags(cliOptions.header);
207+
}
208+
209+
const options: InvokeOptions = {
210+
prompt: resolved.prompt,
211+
agentName: cliOptions.runtime,
212+
targetName: cliOptions.target ?? 'default',
213+
sessionId: cliOptions.sessionId,
214+
userId: cliOptions.userId,
215+
json: cliOptions.json,
216+
stream: cliOptions.stream,
217+
tool: cliOptions.tool,
218+
input: cliOptions.input,
219+
exec: cliOptions.exec,
220+
timeout: cliOptions.timeout,
221+
headers,
222+
bearerToken: cliOptions.bearerToken,
223+
};
224+
225+
return handleInvokeCLI(options, invokeContext);
221226
}
222227
);
223228

224-
printInvokeResult(result, options);
229+
printInvokeResult(result, {
230+
json: cliOptions.json,
231+
stream: cliOptions.stream,
232+
});
225233
process.exit(result.success ? 0 : 1);
226234
} else {
227235
// No CLI options - interactive TUI mode (headers still passed if provided)
228236
requireTTY();
229237

238+
// Parse custom headers for TUI mode
239+
let headers: Record<string, string> | undefined;
240+
if (cliOptions.header && cliOptions.header.length > 0) {
241+
headers = parseHeaderFlags(cliOptions.header);
242+
}
243+
230244
const tuiResult = await withCommandRunTelemetry(
231245
'invoke',
232246
{

0 commit comments

Comments
 (0)