incur 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Cli.ts CHANGED
@@ -1,18 +1,18 @@
1
+ import { estimateTokenCount, sliceByTokens } from 'tokenx'
1
2
  import type { z } from 'zod'
2
3
 
3
- import { estimateTokenCount, sliceByTokens } from 'tokenx'
4
4
  import * as Completions from './Completions.js'
5
- import * as Filter from './Filter.js'
6
5
  import type { FieldError } from './Errors.js'
7
6
  import { IncurError, ValidationError } from './Errors.js'
8
7
  import * as Fetch from './Fetch.js'
9
- import * as Openapi from './Openapi.js'
8
+ import * as Filter from './Filter.js'
10
9
  import * as Formatter from './Formatter.js'
11
10
  import * as Help from './Help.js'
12
11
  import { detectRunner } from './internal/pm.js'
13
12
  import type { OneOf } from './internal/types.js'
14
13
  import * as Mcp from './Mcp.js'
15
14
  import type { Context as MiddlewareContext, Handler as MiddlewareHandler } from './middleware.js'
15
+ import * as Openapi from './Openapi.js'
16
16
  export type { MiddlewareHandler }
17
17
  import * as Parser from './Parser.js'
18
18
  import type { Register } from './Register.js'
@@ -26,6 +26,7 @@ export type Cli<
26
26
  commands extends CommandsMap = {},
27
27
  vars extends z.ZodObject<any> | undefined = undefined,
28
28
  env extends z.ZodObject<any> | undefined = undefined,
29
+ opts extends z.ZodObject<any> | undefined = undefined,
29
30
  > = {
30
31
  /** Registers a root command or mounts a sub-CLI as a command group. */
31
32
  command: {
@@ -42,29 +43,42 @@ export type Cli<
42
43
  ): Cli<
43
44
  commands & { [key in name]: { args: InferOutput<args>; options: InferOutput<options> } },
44
45
  vars,
45
- env
46
+ env,
47
+ opts
46
48
  >
47
49
  /** Mounts a sub-CLI as a command group. */
48
50
  <const name extends string, const sub extends CommandsMap>(
49
- cli: Cli<sub, any, any> & { name: name },
50
- ): Cli<commands & { [key in keyof sub & string as `${name} ${key}`]: sub[key] }, vars, env>
51
+ cli: Cli<sub, any, any, any> & { name: name },
52
+ ): Cli<
53
+ commands & { [key in keyof sub & string as `${name} ${key}`]: sub[key] },
54
+ vars,
55
+ env,
56
+ opts
57
+ >
51
58
  /** Mounts a root CLI as a single command. */
52
59
  <
53
60
  const name extends string,
54
61
  const args extends z.ZodObject<any> | undefined,
55
- const opts extends z.ZodObject<any> | undefined,
62
+ const cmdOpts extends z.ZodObject<any> | undefined,
56
63
  >(
57
- cli: Root<args, opts> & { name: name },
64
+ cli: Root<args, cmdOpts> & { name: name },
58
65
  ): Cli<
59
- commands & { [key in name]: { args: InferOutput<args>; options: InferOutput<opts> } },
66
+ commands & { [key in name]: { args: InferOutput<args>; options: InferOutput<cmdOpts> } },
60
67
  vars,
61
- env
68
+ env,
69
+ opts
62
70
  >
63
71
  /** Mounts a fetch handler as a command, optionally with OpenAPI spec for typed subcommands. */
64
72
  <const name extends string>(
65
73
  name: name,
66
- definition: { basePath?: string | undefined; description?: string | undefined; fetch: FetchHandler; openapi?: Openapi.OpenAPISpec | undefined; outputPolicy?: OutputPolicy | undefined },
67
- ): Cli<commands, vars, env>
74
+ definition: {
75
+ basePath?: string | undefined
76
+ description?: string | undefined
77
+ fetch: FetchHandler
78
+ openapi?: Openapi.OpenAPISpec | undefined
79
+ outputPolicy?: OutputPolicy | undefined
80
+ },
81
+ ): Cli<commands, vars, env, opts>
68
82
  }
69
83
  /** A short description of the CLI. */
70
84
  description?: string | undefined
@@ -76,8 +90,10 @@ export type Cli<
76
90
  fetch(req: Request): Promise<Response>
77
91
  /** Parses argv, runs the matched command, and writes the output envelope to stdout. */
78
92
  serve(argv?: string[], options?: serve.Options): Promise<void>
93
+ /** The options schema, if declared. Use `typeof cli.options` with `middleware<vars, env, options>()` for typed middleware. */
94
+ options: opts
79
95
  /** Registers middleware that runs around every command. */
80
- use(handler: MiddlewareHandler<vars, env>): Cli<commands, vars, env>
96
+ use(handler: MiddlewareHandler<vars, env, opts>): Cli<commands, vars, env, opts>
81
97
  /** The vars schema, if declared. Use `typeof cli.vars` with `middleware<vars, env>()` for typed middleware. */
82
98
  vars: vars
83
99
  }
@@ -145,7 +161,12 @@ export function create<
145
161
  >(
146
162
  name: string,
147
163
  definition: create.Options<args, env, opts, output, vars> & { run: Function },
148
- ): Cli<{ [key in typeof name]: { args: InferOutput<args>; options: InferOutput<opts> } }, vars, env>
164
+ ): Cli<
165
+ { [key in typeof name]: { args: InferOutput<args>; options: InferOutput<opts> } },
166
+ vars,
167
+ env,
168
+ opts
169
+ >
149
170
  /** Creates a router CLI that registers subcommands. */
150
171
  export function create<
151
172
  const args extends z.ZodObject<any> | undefined = undefined,
@@ -153,7 +174,10 @@ export function create<
153
174
  const opts extends z.ZodObject<any> | undefined = undefined,
154
175
  const output extends z.ZodType | undefined = undefined,
155
176
  const vars extends z.ZodObject<any> | undefined = undefined,
156
- >(name: string, definition?: create.Options<args, env, opts, output, vars>): Cli<{}, vars, env>
177
+ >(
178
+ name: string,
179
+ definition?: create.Options<args, env, opts, output, vars>,
180
+ ): Cli<{}, vars, env, opts>
157
181
  /** Creates a CLI with a root handler from a single options object. Can still register subcommands. */
158
182
  export function create<
159
183
  const args extends z.ZodObject<any> | undefined = undefined,
@@ -168,7 +192,8 @@ export function create<
168
192
  [key in (typeof definition)['name']]: { args: InferOutput<args>; options: InferOutput<opts> }
169
193
  },
170
194
  vars,
171
- env
195
+ env,
196
+ opts
172
197
  >
173
198
  /** Creates a router CLI from a single options object (e.g. package.json). */
174
199
  export function create<
@@ -177,7 +202,9 @@ export function create<
177
202
  const opts extends z.ZodObject<any> | undefined = undefined,
178
203
  const output extends z.ZodType | undefined = undefined,
179
204
  const vars extends z.ZodObject<any> | undefined = undefined,
180
- >(definition: create.Options<args, env, opts, output, vars> & { name: string }): Cli<{}, vars, env>
205
+ >(
206
+ definition: create.Options<args, env, opts, output, vars> & { name: string },
207
+ ): Cli<{}, vars, env, opts>
181
208
  export function create(
182
209
  nameOrDefinition: string | (any & { name: string }),
183
210
  definition?: any,
@@ -196,6 +223,7 @@ export function create(
196
223
  name,
197
224
  description: def.description,
198
225
  env: def.env,
226
+ options: def.options,
199
227
  vars: def.vars,
200
228
 
201
229
  command(nameOrCli: any, def?: any): any {
@@ -204,14 +232,16 @@ export function create(
204
232
  // OpenAPI + fetch → generate typed command group (async, resolved before serve)
205
233
  if (def.openapi) {
206
234
  pending.push(
207
- Openapi.generateCommands(def.openapi, def.fetch, { basePath: def.basePath }).then((generated) => {
208
- commands.set(nameOrCli, {
209
- _group: true,
210
- description: def.description,
211
- commands: generated as Map<string, CommandEntry>,
212
- ...(def.outputPolicy ? { outputPolicy: def.outputPolicy } : undefined),
213
- } as InternalGroup)
214
- }),
235
+ Openapi.generateCommands(def.openapi, def.fetch, { basePath: def.basePath }).then(
236
+ (generated) => {
237
+ commands.set(nameOrCli, {
238
+ _group: true,
239
+ description: def.description,
240
+ commands: generated as Map<string, CommandEntry>,
241
+ ...(def.outputPolicy ? { outputPolicy: def.outputPolicy } : undefined),
242
+ } as InternalGroup)
243
+ },
244
+ ),
215
245
  )
216
246
  return cli
217
247
  }
@@ -266,6 +296,7 @@ export function create(
266
296
  format: def.format,
267
297
  mcp: def.mcp,
268
298
  middlewares,
299
+ optionsSchema: def.options,
269
300
  outputPolicy: def.outputPolicy,
270
301
  rootCommand: rootDef,
271
302
  rootFetch,
@@ -786,7 +817,16 @@ async function serveImpl(
786
817
  filtered.length === 0 && options.rootCommand
787
818
  ? { command: options.rootCommand, path: name, rest: [] as string[] }
788
819
  : filtered.length === 0 && options.rootFetch
789
- ? { fetchGateway: { _fetch: true as const, fetch: options.rootFetch, description: options.description }, middlewares: [] as MiddlewareHandler[], path: name, rest: [] as string[] }
820
+ ? {
821
+ fetchGateway: {
822
+ _fetch: true as const,
823
+ fetch: options.rootFetch,
824
+ description: options.description,
825
+ },
826
+ middlewares: [] as MiddlewareHandler[],
827
+ path: name,
828
+ rest: [] as string[],
829
+ }
790
830
  : resolveCommand(commands, filtered)
791
831
 
792
832
  // --help on a fetch gateway → show fetch-specific help
@@ -909,6 +949,14 @@ async function serveImpl(
909
949
 
910
950
  const start = performance.now()
911
951
 
952
+ // Parse root CLI-level options (available to middleware via c.options)
953
+ const rootOptions = options.optionsSchema
954
+ ? Parser.parse(filtered, {
955
+ alias: options.rootCommand?.alias as Record<string, string> | undefined,
956
+ options: options.optionsSchema,
957
+ }).options
958
+ : {}
959
+
912
960
  // Resolve effective format: explicit --format/--json → command default → CLI default → toon
913
961
  const resolvedFormat = 'command' in resolved && (resolved as any).command.format
914
962
  const format = formatExplicit ? formatFlag : resolvedFormat || options.format || 'toon'
@@ -916,7 +964,16 @@ async function serveImpl(
916
964
  // Fall back to root fetch when no subcommand matches
917
965
  const effective =
918
966
  'error' in resolved && options.rootFetch && !resolved.path
919
- ? { fetchGateway: { _fetch: true as const, fetch: options.rootFetch, description: options.description }, middlewares: [] as MiddlewareHandler[], path: name, rest: filtered }
967
+ ? {
968
+ fetchGateway: {
969
+ _fetch: true as const,
970
+ fetch: options.rootFetch,
971
+ description: options.description,
972
+ },
973
+ middlewares: [] as MiddlewareHandler[],
974
+ path: name,
975
+ rest: filtered,
976
+ }
920
977
  : 'error' in resolved && options.rootCommand && !resolved.path
921
978
  ? { command: options.rootCommand, path: name, rest: filtered }
922
979
  : resolved
@@ -928,7 +985,11 @@ async function serveImpl(
928
985
 
929
986
  const filterPaths = filterOutput ? Filter.parse(filterOutput) : undefined
930
987
 
931
- function truncate(s: string): { text: string; truncated: boolean; nextOffset?: number | undefined } {
988
+ function truncate(s: string): {
989
+ text: string
990
+ truncated: boolean
991
+ nextOffset?: number | undefined
992
+ } {
932
993
  if (tokenLimit == null && tokenOffset == null) return { text: s, truncated: false }
933
994
  const total = estimateTokenCount(s)
934
995
  const offset = tokenOffset ?? 0
@@ -964,11 +1025,12 @@ async function serveImpl(
964
1025
  if (verbose) {
965
1026
  if (tokenLimit != null || tokenOffset != null) {
966
1027
  // Truncate data separately so meta (including nextOffset) is always visible
967
- const dataFormatted = output.ok && output.data != null
968
- ? Formatter.format(output.data, format)
969
- : !output.ok
970
- ? Formatter.format(output.error, format)
971
- : ''
1028
+ const dataFormatted =
1029
+ output.ok && output.data != null
1030
+ ? Formatter.format(output.data, format)
1031
+ : !output.ok
1032
+ ? Formatter.format(output.error, format)
1033
+ : ''
972
1034
  const t = truncate(dataFormatted)
973
1035
  if (t.truncated) {
974
1036
  const envelope: Record<string, unknown> = output.ok
@@ -1070,9 +1132,12 @@ async function serveImpl(
1070
1132
  ok: false,
1071
1133
  error: {
1072
1134
  code: `HTTP_${output.status}`,
1073
- message: typeof output.data === 'object' && output.data !== null && 'message' in output.data
1074
- ? String((output.data as any).message)
1075
- : typeof output.data === 'string' ? output.data : `HTTP ${output.status}`,
1135
+ message:
1136
+ typeof output.data === 'object' && output.data !== null && 'message' in output.data
1137
+ ? String((output.data as any).message)
1138
+ : typeof output.data === 'string'
1139
+ ? output.data
1140
+ : `HTTP ${output.status}`,
1076
1141
  },
1077
1142
  meta: {
1078
1143
  command: path,
@@ -1084,11 +1149,18 @@ async function serveImpl(
1084
1149
  }
1085
1150
 
1086
1151
  try {
1087
- const cliEnv = options.envSchema ? Parser.parseEnv(options.envSchema, options.env ?? process.env) : {}
1152
+ const cliEnv = options.envSchema
1153
+ ? Parser.parseEnv(options.envSchema, options.env ?? process.env)
1154
+ : {}
1088
1155
  if (fetchMiddleware.length > 0) {
1089
1156
  const varsMap: Record<string, unknown> = options.vars ? options.vars.parse({}) : {}
1090
- const errorFn = (opts: { code: string; exitCode?: number | undefined; message: string; retryable?: boolean | undefined; cta?: CtaBlock | undefined }): never =>
1091
- ({ [sentinel]: 'error', ...opts }) as never
1157
+ const errorFn = (opts: {
1158
+ code: string
1159
+ exitCode?: number | undefined
1160
+ message: string
1161
+ retryable?: boolean | undefined
1162
+ cta?: CtaBlock | undefined
1163
+ }): never => ({ [sentinel]: 'error', ...opts }) as never
1092
1164
  const mwCtx: MiddlewareContext = {
1093
1165
  agent: !human,
1094
1166
  command: path,
@@ -1097,7 +1169,10 @@ async function serveImpl(
1097
1169
  format,
1098
1170
  formatExplicit,
1099
1171
  name,
1100
- set(key: string, value: unknown) { varsMap[key] = value },
1172
+ options: rootOptions,
1173
+ set(key: string, value: unknown) {
1174
+ varsMap[key] = value
1175
+ },
1101
1176
  var: varsMap,
1102
1177
  version: options.version,
1103
1178
  }
@@ -1107,13 +1182,23 @@ async function serveImpl(
1107
1182
  const cta = formatCtaBlock(name, err.cta)
1108
1183
  write({
1109
1184
  ok: false,
1110
- error: { code: err.code, message: err.message, ...(err.retryable !== undefined ? { retryable: err.retryable } : undefined) },
1111
- meta: { command: path, duration: `${Math.round(performance.now() - start)}ms`, ...(cta ? { cta } : undefined) },
1185
+ error: {
1186
+ code: err.code,
1187
+ message: err.message,
1188
+ ...(err.retryable !== undefined ? { retryable: err.retryable } : undefined),
1189
+ },
1190
+ meta: {
1191
+ command: path,
1192
+ duration: `${Math.round(performance.now() - start)}ms`,
1193
+ ...(cta ? { cta } : undefined),
1194
+ },
1112
1195
  })
1113
1196
  exit(err.exitCode ?? 1)
1114
1197
  }
1115
1198
  const composed = fetchMiddleware.reduceRight(
1116
- (next: () => Promise<void>, mw) => async () => { handleMwSentinel(await mw(mwCtx, next)) },
1199
+ (next: () => Promise<void>, mw) => async () => {
1200
+ handleMwSentinel(await mw(mwCtx, next))
1201
+ },
1117
1202
  runFetch,
1118
1203
  )
1119
1204
  await composed()
@@ -1275,6 +1360,7 @@ async function serveImpl(
1275
1360
  format,
1276
1361
  formatExplicit,
1277
1362
  name,
1363
+ options: rootOptions,
1278
1364
  set(key: string, value: unknown) {
1279
1365
  varsMap[key] = value
1280
1366
  },
@@ -1344,7 +1430,9 @@ async function serveImpl(
1344
1430
  /** @internal Options for fetchImpl. */
1345
1431
  declare namespace fetchImpl {
1346
1432
  type Options = {
1347
- mcpHandler?: ((req: Request, commands: Map<string, CommandEntry>) => Promise<Response>) | undefined
1433
+ mcpHandler?:
1434
+ | ((req: Request, commands: Map<string, CommandEntry>) => Promise<Response>)
1435
+ | undefined
1348
1436
  middlewares?: MiddlewareHandler[] | undefined
1349
1437
  rootCommand?: CommandDefinition<any, any, any> | undefined
1350
1438
  vars?: z.ZodObject<any> | undefined
@@ -1358,9 +1446,8 @@ function createMcpHttpHandler(name: string, version: string) {
1358
1446
  return async (req: Request, commands: Map<string, CommandEntry>): Promise<Response> => {
1359
1447
  if (!transport) {
1360
1448
  const { McpServer } = await import('@modelcontextprotocol/sdk/server/mcp.js')
1361
- const { WebStandardStreamableHTTPServerTransport } = await import(
1362
- '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js'
1363
- )
1449
+ const { WebStandardStreamableHTTPServerTransport } =
1450
+ await import('@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js')
1364
1451
 
1365
1452
  const server = new McpServer({ name, version })
1366
1453
 
@@ -1455,12 +1542,12 @@ async function fetchImpl(
1455
1542
 
1456
1543
  // Parse options from search params (GET) or body (non-GET)
1457
1544
  let inputOptions: Record<string, unknown> = {}
1458
- if (req.method === 'GET')
1459
- for (const [key, value] of url.searchParams) inputOptions[key] = value
1545
+ if (req.method === 'GET') for (const [key, value] of url.searchParams) inputOptions[key] = value
1460
1546
  else {
1461
1547
  try {
1462
1548
  const contentType = req.headers.get('content-type') ?? ''
1463
- if (contentType.includes('application/json')) inputOptions = (await req.json()) as Record<string, unknown>
1549
+ if (contentType.includes('application/json'))
1550
+ inputOptions = (await req.json()) as Record<string, unknown>
1464
1551
  } catch {}
1465
1552
  }
1466
1553
 
@@ -1477,7 +1564,11 @@ async function fetchImpl(
1477
1564
  if (options.rootCommand)
1478
1565
  return executeCommand(name, options.rootCommand, [], inputOptions, start, options)
1479
1566
  return jsonResponse(
1480
- { ok: false, error: { code: 'COMMAND_NOT_FOUND', message: 'No root command defined.' }, meta: { command: '/', duration: `${Math.round(performance.now() - start)}ms` } },
1567
+ {
1568
+ ok: false,
1569
+ error: { code: 'COMMAND_NOT_FOUND', message: 'No root command defined.' },
1570
+ meta: { command: '/', duration: `${Math.round(performance.now() - start)}ms` },
1571
+ },
1481
1572
  404,
1482
1573
  )
1483
1574
  }
@@ -1486,18 +1577,31 @@ async function fetchImpl(
1486
1577
 
1487
1578
  if ('error' in resolved)
1488
1579
  return jsonResponse(
1489
- { ok: false, error: { code: 'COMMAND_NOT_FOUND', message: `'${resolved.error}' is not a command for '${resolved.path ? `${name} ${resolved.path}` : name}'.` }, meta: { command: resolved.error, duration: `${Math.round(performance.now() - start)}ms` } },
1580
+ {
1581
+ ok: false,
1582
+ error: {
1583
+ code: 'COMMAND_NOT_FOUND',
1584
+ message: `'${resolved.error}' is not a command for '${resolved.path ? `${name} ${resolved.path}` : name}'.`,
1585
+ },
1586
+ meta: { command: resolved.error, duration: `${Math.round(performance.now() - start)}ms` },
1587
+ },
1490
1588
  404,
1491
1589
  )
1492
1590
 
1493
1591
  if ('help' in resolved)
1494
1592
  return jsonResponse(
1495
- { ok: false, error: { code: 'COMMAND_NOT_FOUND', message: `'${resolved.path}' is a command group. Specify a subcommand.` }, meta: { command: resolved.path, duration: `${Math.round(performance.now() - start)}ms` } },
1593
+ {
1594
+ ok: false,
1595
+ error: {
1596
+ code: 'COMMAND_NOT_FOUND',
1597
+ message: `'${resolved.path}' is a command group. Specify a subcommand.`,
1598
+ },
1599
+ meta: { command: resolved.path, duration: `${Math.round(performance.now() - start)}ms` },
1600
+ },
1496
1601
  404,
1497
1602
  )
1498
1603
 
1499
- if ('fetchGateway' in resolved)
1500
- return resolved.fetchGateway.fetch(req)
1604
+ if ('fetchGateway' in resolved) return resolved.fetchGateway.fetch(req)
1501
1605
 
1502
1606
  const { command, path, rest } = resolved
1503
1607
  return executeCommand(path, command, rest, inputOptions, start, options)
@@ -1528,8 +1632,11 @@ async function executeCommand(
1528
1632
  const parsedOptions = command.options ? command.options.parse(inputOptions) : {}
1529
1633
 
1530
1634
  const okFn = (data: unknown): never => ({ [sentinel_]: 'ok', data }) as never
1531
- const errorFn = (opts: { code: string; message: string; exitCode?: number | undefined }): never =>
1532
- ({ [sentinel_]: 'error', ...opts }) as never
1635
+ const errorFn = (opts: {
1636
+ code: string
1637
+ message: string
1638
+ exitCode?: number | undefined
1639
+ }): never => ({ [sentinel_]: 'error', ...opts }) as never
1533
1640
 
1534
1641
  const result = command.run({
1535
1642
  agent: true,
@@ -1560,26 +1667,60 @@ async function executeCommand(
1560
1667
  }
1561
1668
  if (isSentinel(value) && (value as any)[sentinel] === 'error') {
1562
1669
  const tagged = value as any
1563
- controller.enqueue(encoder.encode(JSON.stringify({ type: 'error', ok: false, error: { code: tagged.code, message: tagged.message } }) + '\n'))
1670
+ controller.enqueue(
1671
+ encoder.encode(
1672
+ JSON.stringify({
1673
+ type: 'error',
1674
+ ok: false,
1675
+ error: { code: tagged.code, message: tagged.message },
1676
+ }) + '\n',
1677
+ ),
1678
+ )
1564
1679
  controller.close()
1565
1680
  return
1566
1681
  }
1567
- controller.enqueue(encoder.encode(JSON.stringify({ type: 'chunk', data: value }) + '\n'))
1682
+ controller.enqueue(
1683
+ encoder.encode(JSON.stringify({ type: 'chunk', data: value }) + '\n'),
1684
+ )
1568
1685
  }
1569
1686
  const meta: Record<string, unknown> = { command: path }
1570
1687
  if (isSentinel(returnValue) && (returnValue as any)[sentinel] === 'error') {
1571
1688
  const tagged = returnValue as any
1572
- controller.enqueue(encoder.encode(JSON.stringify({ type: 'error', ok: false, error: { code: tagged.code, message: tagged.message } }) + '\n'))
1689
+ controller.enqueue(
1690
+ encoder.encode(
1691
+ JSON.stringify({
1692
+ type: 'error',
1693
+ ok: false,
1694
+ error: { code: tagged.code, message: tagged.message },
1695
+ }) + '\n',
1696
+ ),
1697
+ )
1573
1698
  } else {
1574
- controller.enqueue(encoder.encode(JSON.stringify({ type: 'done', ok: true, meta }) + '\n'))
1699
+ controller.enqueue(
1700
+ encoder.encode(JSON.stringify({ type: 'done', ok: true, meta }) + '\n'),
1701
+ )
1575
1702
  }
1576
1703
  } catch (error) {
1577
- controller.enqueue(encoder.encode(JSON.stringify({ type: 'error', ok: false, error: { code: 'UNKNOWN', message: error instanceof Error ? error.message : String(error) } }) + '\n'))
1704
+ controller.enqueue(
1705
+ encoder.encode(
1706
+ JSON.stringify({
1707
+ type: 'error',
1708
+ ok: false,
1709
+ error: {
1710
+ code: 'UNKNOWN',
1711
+ message: error instanceof Error ? error.message : String(error),
1712
+ },
1713
+ }) + '\n',
1714
+ ),
1715
+ )
1578
1716
  }
1579
1717
  controller.close()
1580
1718
  },
1581
1719
  })
1582
- response = new Response(stream, { status: 200, headers: { 'content-type': 'application/x-ndjson' } })
1720
+ response = new Response(stream, {
1721
+ status: 200,
1722
+ headers: { 'content-type': 'application/x-ndjson' },
1723
+ })
1583
1724
  return
1584
1725
  }
1585
1726
 
@@ -1590,7 +1731,11 @@ async function executeCommand(
1590
1731
  const tagged = awaited as any
1591
1732
  if (tagged[sentinel_] === 'error')
1592
1733
  response = jsonResponse(
1593
- { ok: false, error: { code: tagged.code, message: tagged.message }, meta: { command: path, duration } },
1734
+ {
1735
+ ok: false,
1736
+ error: { code: tagged.code, message: tagged.message },
1737
+ meta: { command: path, duration },
1738
+ },
1594
1739
  500,
1595
1740
  )
1596
1741
  else
@@ -1601,19 +1746,24 @@ async function executeCommand(
1601
1746
  return
1602
1747
  }
1603
1748
 
1604
- response = jsonResponse(
1605
- { ok: true, data: awaited, meta: { command: path, duration } },
1606
- 200,
1607
- )
1749
+ response = jsonResponse({ ok: true, data: awaited, meta: { command: path, duration } }, 200)
1608
1750
  }
1609
1751
 
1610
1752
  try {
1611
1753
  const allMiddleware = options.middlewares ?? []
1612
1754
  if (allMiddleware.length > 0) {
1613
- const errorFn = (opts: { code: string; message: string; exitCode?: number | undefined }): never => {
1755
+ const errorFn = (opts: {
1756
+ code: string
1757
+ message: string
1758
+ exitCode?: number | undefined
1759
+ }): never => {
1614
1760
  const duration = `${Math.round(performance.now() - start)}ms`
1615
1761
  response = jsonResponse(
1616
- { ok: false, error: { code: opts.code, message: opts.message }, meta: { command: path, duration } },
1762
+ {
1763
+ ok: false,
1764
+ error: { code: opts.code, message: opts.message },
1765
+ meta: { command: path, duration },
1766
+ },
1617
1767
  500,
1618
1768
  )
1619
1769
  return undefined as never
@@ -1626,12 +1776,17 @@ async function executeCommand(
1626
1776
  format: 'json',
1627
1777
  formatExplicit: true,
1628
1778
  name: path,
1629
- set(key: string, value: unknown) { varsMap[key] = value },
1779
+ options: {},
1780
+ set(key: string, value: unknown) {
1781
+ varsMap[key] = value
1782
+ },
1630
1783
  var: varsMap,
1631
1784
  version: undefined,
1632
1785
  }
1633
1786
  const composed = allMiddleware.reduceRight(
1634
- (next: () => Promise<void>, mw) => async () => { await mw(mwCtx, next) },
1787
+ (next: () => Promise<void>, mw) => async () => {
1788
+ await mw(mwCtx, next)
1789
+ },
1635
1790
  runCommand,
1636
1791
  )
1637
1792
  await composed()
@@ -1642,11 +1797,22 @@ async function executeCommand(
1642
1797
  const duration = `${Math.round(performance.now() - start)}ms`
1643
1798
  if (error instanceof ValidationError)
1644
1799
  return jsonResponse(
1645
- { ok: false, error: { code: 'VALIDATION_ERROR', message: error.message }, meta: { command: path, duration } },
1800
+ {
1801
+ ok: false,
1802
+ error: { code: 'VALIDATION_ERROR', message: error.message },
1803
+ meta: { command: path, duration },
1804
+ },
1646
1805
  400,
1647
1806
  )
1648
1807
  return jsonResponse(
1649
- { ok: false, error: { code: error instanceof IncurError ? error.code : 'UNKNOWN', message: error instanceof Error ? error.message : String(error) }, meta: { command: path, duration } },
1808
+ {
1809
+ ok: false,
1810
+ error: {
1811
+ code: error instanceof IncurError ? error.code : 'UNKNOWN',
1812
+ message: error instanceof Error ? error.message : String(error),
1813
+ },
1814
+ meta: { command: path, duration },
1815
+ },
1650
1816
  500,
1651
1817
  )
1652
1818
  }
@@ -1785,6 +1951,8 @@ declare namespace serveImpl {
1785
1951
  format?: Formatter.Format | undefined
1786
1952
  /** Middleware handlers registered on the root CLI. */
1787
1953
  middlewares?: MiddlewareHandler[] | undefined
1954
+ /** CLI-level options schema. Parsed before middleware runs. */
1955
+ optionsSchema?: z.ZodObject<any> | undefined
1788
1956
  /** CLI-level default output policy. */
1789
1957
  outputPolicy?: OutputPolicy | undefined
1790
1958
  mcp?:
@@ -1857,7 +2025,22 @@ function extractBuiltinFlags(argv: string[]) {
1857
2025
  else rest.push(token)
1858
2026
  }
1859
2027
 
1860
- return { verbose, format, formatExplicit, filterOutput, tokenLimit, tokenOffset, tokenCount, llms, llmsFull, mcp, help, version, schema, rest }
2028
+ return {
2029
+ verbose,
2030
+ format,
2031
+ formatExplicit,
2032
+ filterOutput,
2033
+ tokenLimit,
2034
+ tokenOffset,
2035
+ tokenCount,
2036
+ llms,
2037
+ llmsFull,
2038
+ mcp,
2039
+ help,
2040
+ version,
2041
+ schema,
2042
+ rest,
2043
+ }
1861
2044
  }
1862
2045
 
1863
2046
  /** @internal Collects immediate child commands/groups for help output. */
@@ -2073,7 +2256,8 @@ async function handleStreaming(
2073
2256
  }
2074
2257
  }
2075
2258
  if (useJsonl) ctx.writeln(JSON.stringify({ type: 'chunk', data: value }))
2076
- else if (ctx.renderOutput) ctx.writeln(ctx.truncate(Formatter.format(value, ctx.format)).text)
2259
+ else if (ctx.renderOutput)
2260
+ ctx.writeln(ctx.truncate(Formatter.format(value, ctx.format)).text)
2077
2261
  }
2078
2262
 
2079
2263
  // Handle return value — error() or ok() sentinel
package/src/Fetch.test.ts CHANGED
@@ -66,13 +66,7 @@ describe('parseArgv', () => {
66
66
  })
67
67
 
68
68
  test('multiple headers', () => {
69
- const input = Fetch.parseArgv([
70
- 'users',
71
- '-H',
72
- 'X-A: 1',
73
- '-H',
74
- 'X-B: 2',
75
- ])
69
+ const input = Fetch.parseArgv(['users', '-H', 'X-A: 1', '-H', 'X-B: 2'])
76
70
  expect(input.headers.get('X-A')).toBe('1')
77
71
  expect(input.headers.get('X-B')).toBe('2')
78
72
  })
@@ -123,16 +117,12 @@ describe('buildRequest', () => {
123
117
  })
124
118
 
125
119
  test('builds POST request with body', () => {
126
- const req = Fetch.buildRequest(
127
- Fetch.parseArgv(['users', '-X', 'POST', '-d', '{"name":"Bob"}']),
128
- )
120
+ const req = Fetch.buildRequest(Fetch.parseArgv(['users', '-X', 'POST', '-d', '{"name":"Bob"}']))
129
121
  expect(req.method).toBe('POST')
130
122
  })
131
123
 
132
124
  test('builds request with headers', () => {
133
- const req = Fetch.buildRequest(
134
- Fetch.parseArgv(['users', '-H', 'X-Api-Key: secret']),
135
- )
125
+ const req = Fetch.buildRequest(Fetch.parseArgv(['users', '-H', 'X-Api-Key: secret']))
136
126
  expect(req.headers.get('X-Api-Key')).toBe('secret')
137
127
  })
138
128
  })
package/src/Fetch.ts CHANGED
@@ -102,8 +102,7 @@ export function buildRequest(input: FetchInput): Request {
102
102
 
103
103
  if (input.body !== undefined) {
104
104
  init.body = input.body
105
- if (!input.headers.has('content-type'))
106
- input.headers.set('content-type', 'application/json')
105
+ if (!input.headers.has('content-type')) input.headers.set('content-type', 'application/json')
107
106
  }
108
107
 
109
108
  return new Request(url.toString(), init)