incur 0.4.8 → 0.4.9

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.
Files changed (88) hide show
  1. package/dist/Cli.d.ts +65 -19
  2. package/dist/Cli.d.ts.map +1 -1
  3. package/dist/Cli.js +294 -66
  4. package/dist/Cli.js.map +1 -1
  5. package/dist/Completions.d.ts +2 -1
  6. package/dist/Completions.d.ts.map +1 -1
  7. package/dist/Completions.js +52 -13
  8. package/dist/Completions.js.map +1 -1
  9. package/dist/Formatter.d.ts.map +1 -1
  10. package/dist/Formatter.js +6 -5
  11. package/dist/Formatter.js.map +1 -1
  12. package/dist/Help.d.ts +5 -0
  13. package/dist/Help.d.ts.map +1 -1
  14. package/dist/Help.js +19 -5
  15. package/dist/Help.js.map +1 -1
  16. package/dist/Mcp.d.ts +21 -0
  17. package/dist/Mcp.d.ts.map +1 -1
  18. package/dist/Mcp.js +25 -8
  19. package/dist/Mcp.js.map +1 -1
  20. package/dist/Parser.d.ts +14 -0
  21. package/dist/Parser.d.ts.map +1 -1
  22. package/dist/Parser.js +127 -1
  23. package/dist/Parser.js.map +1 -1
  24. package/dist/Skill.d.ts.map +1 -1
  25. package/dist/Skill.js +5 -2
  26. package/dist/Skill.js.map +1 -1
  27. package/dist/Skillgen.d.ts.map +1 -1
  28. package/dist/Skillgen.js +4 -38
  29. package/dist/Skillgen.js.map +1 -1
  30. package/dist/SyncMcp.d.ts.map +1 -1
  31. package/dist/SyncMcp.js +75 -8
  32. package/dist/SyncMcp.js.map +1 -1
  33. package/dist/SyncSkills.d.ts +9 -0
  34. package/dist/SyncSkills.d.ts.map +1 -1
  35. package/dist/SyncSkills.js +13 -74
  36. package/dist/SyncSkills.js.map +1 -1
  37. package/dist/bin.d.ts +1 -1
  38. package/dist/bin.d.ts.map +1 -1
  39. package/dist/internal/command.d.ts +2 -0
  40. package/dist/internal/command.d.ts.map +1 -1
  41. package/dist/internal/command.js +6 -5
  42. package/dist/internal/command.js.map +1 -1
  43. package/dist/internal/cta.d.ts +22 -0
  44. package/dist/internal/cta.d.ts.map +1 -0
  45. package/dist/internal/cta.js +25 -0
  46. package/dist/internal/cta.js.map +1 -0
  47. package/dist/internal/json.d.ts +5 -0
  48. package/dist/internal/json.d.ts.map +1 -0
  49. package/dist/internal/json.js +13 -0
  50. package/dist/internal/json.js.map +1 -0
  51. package/dist/internal/yaml.d.ts +12 -0
  52. package/dist/internal/yaml.d.ts.map +1 -0
  53. package/dist/internal/yaml.js +20 -0
  54. package/dist/internal/yaml.js.map +1 -0
  55. package/dist/middleware.d.ts +8 -4
  56. package/dist/middleware.d.ts.map +1 -1
  57. package/dist/middleware.js +1 -1
  58. package/dist/middleware.js.map +1 -1
  59. package/package.json +1 -1
  60. package/src/Cli.test-d.ts +71 -0
  61. package/src/Cli.test.ts +1164 -65
  62. package/src/Cli.ts +456 -101
  63. package/src/Completions.test.ts +84 -0
  64. package/src/Completions.ts +48 -9
  65. package/src/Formatter.test.ts +17 -0
  66. package/src/Formatter.ts +7 -5
  67. package/src/Help.test.ts +31 -0
  68. package/src/Help.ts +42 -4
  69. package/src/Mcp.test.ts +193 -0
  70. package/src/Mcp.ts +49 -8
  71. package/src/Parser.test.ts +173 -0
  72. package/src/Parser.ts +132 -1
  73. package/src/Skill.test.ts +6 -0
  74. package/src/Skill.ts +8 -5
  75. package/src/Skillgen.test.ts +55 -6
  76. package/src/Skillgen.ts +9 -35
  77. package/src/SyncMcp.test.ts +89 -0
  78. package/src/SyncMcp.ts +72 -8
  79. package/src/SyncSkills.test.ts +78 -1
  80. package/src/SyncSkills.ts +20 -78
  81. package/src/bun-compile.test.ts +5 -0
  82. package/src/e2e.test.ts +162 -8
  83. package/src/internal/command.ts +8 -4
  84. package/src/internal/cta.ts +58 -0
  85. package/src/internal/json.ts +16 -0
  86. package/src/internal/yaml.ts +23 -0
  87. package/src/lazy-imports.test.ts +94 -0
  88. package/src/middleware.ts +12 -3
package/src/Cli.ts CHANGED
@@ -2,7 +2,6 @@ import * as fs from 'node:fs/promises'
2
2
  import * as os from 'node:os'
3
3
  import * as path from 'node:path'
4
4
  import { estimateTokenCount, sliceByTokens } from 'tokenx'
5
- import { parse as yamlParse, stringify as yamlStringify } from 'yaml'
6
5
  import { z } from 'zod'
7
6
 
8
7
  import * as Completions from './Completions.js'
@@ -21,9 +20,12 @@ import {
21
20
  shells,
22
21
  } from './internal/command.js'
23
22
  import * as Command from './internal/command.js'
23
+ import { formatCtaBlock, type FormattedCta, type FormattedCtaBlock } from './internal/cta.js'
24
24
  import { isRecord, suggest, toKebab } from './internal/helpers.js'
25
+ import * as Json from './internal/json.js'
25
26
  import { detectRunner } from './internal/pm.js'
26
27
  import type { OneOf } from './internal/types.js'
28
+ import * as Yaml from './internal/yaml.js'
27
29
  import * as Mcp from './Mcp.js'
28
30
  import type { Context as MiddlewareContext, Handler as MiddlewareHandler } from './middleware.js'
29
31
  import * as Openapi from './Openapi.js'
@@ -35,11 +37,14 @@ import * as Skill from './Skill.js'
35
37
  import * as SyncMcp from './SyncMcp.js'
36
38
  import * as SyncSkills from './SyncSkills.js'
37
39
 
40
+ const destructiveCommandHint = 'Confirm with the user before executing this destructive command.'
41
+
38
42
  /** A CLI application instance. Also used as a command group when mounted on a parent CLI. */
39
43
  export type Cli<
40
44
  commands extends CommandsMap = {},
41
45
  vars extends z.ZodObject<any> | undefined = undefined,
42
46
  env extends z.ZodObject<any> | undefined = undefined,
47
+ globals extends z.ZodObject<any> | undefined = undefined,
43
48
  > = {
44
49
  /** Registers a root command or mounts a sub-CLI as a command group. */
45
50
  command: {
@@ -56,12 +61,18 @@ export type Cli<
56
61
  ): Cli<
57
62
  commands & { [key in name]: { args: InferOutput<args>; options: InferOutput<options> } },
58
63
  vars,
59
- env
64
+ env,
65
+ globals
60
66
  >
61
67
  /** Mounts a sub-CLI as a command group. */
62
68
  <const name extends string, const sub extends CommandsMap>(
63
- cli: Cli<sub, any, any> & { name: name },
64
- ): Cli<commands & { [key in keyof sub & string as `${name} ${key}`]: sub[key] }, vars, env>
69
+ cli: Cli<sub, any, any, any> & { name: name },
70
+ ): Cli<
71
+ commands & { [key in keyof sub & string as `${name} ${key}`]: sub[key] },
72
+ vars,
73
+ env,
74
+ globals
75
+ >
65
76
  /** Mounts a root CLI as a single command. */
66
77
  <
67
78
  const name extends string,
@@ -72,7 +83,8 @@ export type Cli<
72
83
  ): Cli<
73
84
  commands & { [key in name]: { args: InferOutput<args>; options: InferOutput<opts> } },
74
85
  vars,
75
- env
86
+ env,
87
+ globals
76
88
  >
77
89
  /** Mounts a fetch handler as a command, optionally with OpenAPI spec for typed subcommands. */
78
90
  <const name extends string>(
@@ -85,7 +97,7 @@ export type Cli<
85
97
  openapiConfig?: Openapi.Config | undefined
86
98
  outputPolicy?: OutputPolicy | undefined
87
99
  },
88
- ): Cli<commands, vars, env>
100
+ ): Cli<commands, vars, env, globals>
89
101
  }
90
102
  /** A short description of the CLI. */
91
103
  description?: string | undefined
@@ -98,7 +110,7 @@ export type Cli<
98
110
  /** Parses argv, runs the matched command, and writes the output envelope to stdout. */
99
111
  serve(argv?: string[], options?: serve.Options): Promise<void>
100
112
  /** Registers middleware that runs around every command. */
101
- use(handler: MiddlewareHandler<vars, env>): Cli<commands, vars, env>
113
+ use(handler: MiddlewareHandler<vars, env, globals>): Cli<commands, vars, env, globals>
102
114
  /** The vars schema, if declared. Use `typeof cli.vars` with `middleware<vars, env>()` for typed middleware. */
103
115
  vars: vars
104
116
  }
@@ -163,10 +175,16 @@ export function create<
163
175
  const opts extends z.ZodObject<any> | undefined = undefined,
164
176
  const output extends z.ZodType | undefined = undefined,
165
177
  const vars extends z.ZodObject<any> | undefined = undefined,
178
+ const globals extends z.ZodObject<any> | undefined = undefined,
166
179
  >(
167
180
  name: string,
168
- definition: create.Options<args, env, opts, output, vars> & { run: Function },
169
- ): Cli<{ [key in typeof name]: { args: InferOutput<args>; options: InferOutput<opts> } }, vars, env>
181
+ definition: create.Options<args, env, opts, output, vars, globals> & { run: Function },
182
+ ): Cli<
183
+ { [key in typeof name]: { args: InferOutput<args>; options: InferOutput<opts> } },
184
+ vars,
185
+ env,
186
+ globals
187
+ >
170
188
  /** Creates a router CLI that registers subcommands. */
171
189
  export function create<
172
190
  const args extends z.ZodObject<any> | undefined = undefined,
@@ -174,7 +192,11 @@ export function create<
174
192
  const opts extends z.ZodObject<any> | undefined = undefined,
175
193
  const output extends z.ZodType | undefined = undefined,
176
194
  const vars extends z.ZodObject<any> | undefined = undefined,
177
- >(name: string, definition?: create.Options<args, env, opts, output, vars>): Cli<{}, vars, env>
195
+ const globals extends z.ZodObject<any> | undefined = undefined,
196
+ >(
197
+ name: string,
198
+ definition?: create.Options<args, env, opts, output, vars, globals>,
199
+ ): Cli<{}, vars, env, globals>
178
200
  /** Creates a CLI with a root handler from a single options object. Can still register subcommands. */
179
201
  export function create<
180
202
  const args extends z.ZodObject<any> | undefined = undefined,
@@ -182,14 +204,19 @@ export function create<
182
204
  const opts extends z.ZodObject<any> | undefined = undefined,
183
205
  const output extends z.ZodType | undefined = undefined,
184
206
  const vars extends z.ZodObject<any> | undefined = undefined,
207
+ const globals extends z.ZodObject<any> | undefined = undefined,
185
208
  >(
186
- definition: create.Options<args, env, opts, output, vars> & { name: string; run: Function },
209
+ definition: create.Options<args, env, opts, output, vars, globals> & {
210
+ name: string
211
+ run: Function
212
+ },
187
213
  ): Cli<
188
214
  {
189
215
  [key in (typeof definition)['name']]: { args: InferOutput<args>; options: InferOutput<opts> }
190
216
  },
191
217
  vars,
192
- env
218
+ env,
219
+ globals
193
220
  >
194
221
  /** Creates a router CLI from a single options object (e.g. package.json). */
195
222
  export function create<
@@ -198,7 +225,10 @@ export function create<
198
225
  const opts extends z.ZodObject<any> | undefined = undefined,
199
226
  const output extends z.ZodType | undefined = undefined,
200
227
  const vars extends z.ZodObject<any> | undefined = undefined,
201
- >(definition: create.Options<args, env, opts, output, vars> & { name: string }): Cli<{}, vars, env>
228
+ const globals extends z.ZodObject<any> | undefined = undefined,
229
+ >(
230
+ definition: create.Options<args, env, opts, output, vars, globals> & { name: string },
231
+ ): Cli<{}, vars, env, globals>
202
232
  export function create(
203
233
  nameOrDefinition: string | (any & { name: string }),
204
234
  definition?: any,
@@ -214,7 +244,9 @@ export function create(
214
244
  const commands = new Map<string, CommandEntry>()
215
245
  const middlewares: MiddlewareHandler[] = []
216
246
  const pending: Promise<void>[] = []
217
- const mcpHandler = createMcpHttpHandler(name, def.version ?? '0.0.0')
247
+ const mcpHandler = createMcpHttpHandler(name, def.version ?? '0.0.0', {
248
+ stateless: def.mcp?.stateless,
249
+ })
218
250
 
219
251
  if (def.openapi && rootFetch) {
220
252
  pending.push(
@@ -249,12 +281,14 @@ export function create(
249
281
  basePath: def.basePath,
250
282
  config: def.openapiConfig,
251
283
  })
252
- commands.set(nameOrCli, {
284
+ const entry = {
253
285
  _group: true,
254
286
  description: def.description,
255
287
  commands: generated as Map<string, CommandEntry>,
256
288
  ...(def.outputPolicy ? { outputPolicy: def.outputPolicy } : undefined),
257
- } as InternalGroup)
289
+ } as InternalGroup
290
+ assertNoGlobalOptionConflicts(nameOrCli, entry, toGlobals.get(cli))
291
+ commands.set(nameOrCli, entry)
258
292
  })(),
259
293
  )
260
294
  return cli
@@ -268,6 +302,7 @@ export function create(
268
302
  } as InternalFetchGateway)
269
303
  return cli
270
304
  }
305
+ assertNoGlobalOptionConflicts(nameOrCli, def, toGlobals.get(cli))
271
306
  commands.set(nameOrCli, def)
272
307
  if (def.aliases)
273
308
  for (const a of def.aliases) commands.set(a, { _alias: true, target: nameOrCli })
@@ -275,6 +310,7 @@ export function create(
275
310
  }
276
311
  const mountedRootDef = toRootDefinition.get(nameOrCli)
277
312
  if (mountedRootDef) {
313
+ assertNoGlobalOptionConflicts(nameOrCli.name, mountedRootDef, toGlobals.get(cli))
278
314
  commands.set(nameOrCli.name, mountedRootDef)
279
315
  const rootAliases = toRootAliases.get(nameOrCli)
280
316
  if (rootAliases)
@@ -285,21 +321,25 @@ export function create(
285
321
  const subCommands = toCommands.get(sub)!
286
322
  const subOutputPolicy = toOutputPolicy.get(sub)
287
323
  const subMiddlewares = toMiddlewares.get(sub)
288
- commands.set(sub.name, {
324
+ const entry = {
289
325
  _group: true,
290
326
  description: sub.description,
291
327
  commands: subCommands,
292
328
  ...(subOutputPolicy ? { outputPolicy: subOutputPolicy } : undefined),
293
329
  ...(subMiddlewares?.length ? { middlewares: subMiddlewares } : undefined),
294
- })
330
+ } as InternalGroup
331
+ assertNoGlobalOptionConflicts(sub.name, entry, toGlobals.get(cli))
332
+ commands.set(sub.name, entry)
295
333
  return cli
296
334
  },
297
335
 
298
336
  async fetch(req: Request) {
299
337
  if (pending.length > 0) await Promise.all(pending)
338
+ const globalsDesc = toGlobals.get(cli)
300
339
  return fetchImpl(name, commands, req, {
301
340
  description: def.description,
302
341
  envSchema: def.env,
342
+ globals: globalsDesc,
303
343
  mcpHandler,
304
344
  middlewares,
305
345
  name,
@@ -311,13 +351,16 @@ export function create(
311
351
 
312
352
  async serve(argv = process.argv.slice(2), serveOptions: serve.Options = {}) {
313
353
  if (pending.length > 0) await Promise.all(pending)
354
+ const globalsDesc = toGlobals.get(cli)
314
355
  return serveImpl(name, commands, argv, {
315
356
  ...serveOptions,
316
357
  aliases: def.aliases,
358
+ banner: def.banner,
317
359
  config: def.config,
318
360
  description: def.description,
319
361
  envSchema: def.env,
320
362
  format: def.format,
363
+ globals: globalsDesc,
321
364
  mcp: def.mcp,
322
365
  middlewares,
323
366
  outputPolicy: def.outputPolicy,
@@ -340,6 +383,44 @@ export function create(
340
383
  if (def.options) toRootOptions.set(cli, def.options)
341
384
  if (def.config !== undefined) toConfigEnabled.set(cli, true)
342
385
  if (def.outputPolicy) toOutputPolicy.set(cli, def.outputPolicy)
386
+ if (def.globals) {
387
+ toGlobals.set(cli, { schema: def.globals, alias: def.globalAlias as any })
388
+ const builtinNames = [
389
+ 'verbose',
390
+ 'format',
391
+ 'json',
392
+ 'llms',
393
+ 'llmsFull',
394
+ 'mcp',
395
+ 'help',
396
+ 'version',
397
+ 'schema',
398
+ 'filterOutput',
399
+ 'tokenLimit',
400
+ 'tokenOffset',
401
+ 'tokenCount',
402
+ ...(def.config?.flag
403
+ ? [def.config.flag, `no${def.config.flag[0].toUpperCase()}${def.config.flag.slice(1)}`]
404
+ : []),
405
+ ]
406
+ const globalKeys = Object.keys(def.globals.shape)
407
+ for (const key of globalKeys) {
408
+ if (builtinNames.includes(key))
409
+ throw new Error(
410
+ `Global option '${key}' conflicts with a built-in flag. Choose a different name.`,
411
+ )
412
+ }
413
+ // Check globalAlias values against reserved short aliases
414
+ const reservedShorts = new Set(['h'])
415
+ if (def.globalAlias) {
416
+ for (const [name, short] of Object.entries(def.globalAlias as Record<string, string>)) {
417
+ if (reservedShorts.has(short))
418
+ throw new Error(
419
+ `Global alias '-${short}' for '${name}' conflicts with a built-in short flag. Choose a different alias.`,
420
+ )
421
+ }
422
+ }
423
+ }
343
424
  toMiddlewares.set(cli, middlewares)
344
425
  toCommands.set(cli, commands)
345
426
  return cli
@@ -353,6 +434,7 @@ export declare namespace create {
353
434
  options extends z.ZodObject<any> | undefined = undefined,
354
435
  output extends z.ZodType | undefined = undefined,
355
436
  vars extends z.ZodObject<any> | undefined = undefined,
437
+ globals extends z.ZodObject<any> | undefined = undefined,
356
438
  > = {
357
439
  /** Map of option names to single-char aliases. */
358
440
  alias?: options extends z.ZodObject<any>
@@ -360,6 +442,19 @@ export declare namespace create {
360
442
  : Record<string, string> | undefined
361
443
  /** Alternative binary names for this CLI (e.g. shorter aliases in package.json `bin`). Shell completions are registered for all names. */
362
444
  aliases?: string[] | undefined
445
+ /**
446
+ * Text to display above root help output (e.g. branding, live status). Only called when the CLI is invoked with no subcommand. Errors are silently swallowed.
447
+ *
448
+ * Pass a function for all consumers, or an object with `mode` to target `'human'`, `'agent'`, or `'all'` (default).
449
+ */
450
+ banner?:
451
+ | (() => string | undefined | Promise<string | undefined>)
452
+ | {
453
+ render: () => string | undefined | Promise<string | undefined>
454
+ /** @default 'all' */
455
+ mode?: 'all' | 'human' | 'agent' | undefined
456
+ }
457
+ | undefined
363
458
  /** Zod schema for positional arguments. */
364
459
  args?: args | undefined
365
460
  /** Enable config-file defaults for command options. */
@@ -382,6 +477,8 @@ export declare namespace create {
382
477
  | undefined
383
478
  /** A short description of what the CLI does. */
384
479
  description?: string | undefined
480
+ /** Marks the root command as destructive when generating agent skills. */
481
+ destructive?: boolean | undefined
385
482
  /** Zod schema for environment variables. Keys are the variable names (e.g. `NPM_TOKEN`). */
386
483
  env?: env | undefined
387
484
  /** Usage examples for this command. */
@@ -394,6 +491,14 @@ export declare namespace create {
394
491
  openapiConfig?: Openapi.Config | undefined
395
492
  /** Default output format. Overridden by `--format` or `--json`. */
396
493
  format?: Formatter.Format | undefined
494
+ /** Plain text hint displayed after examples and before global options. */
495
+ hint?: string | undefined
496
+ /** Map of global option names to single-char aliases. */
497
+ globalAlias?: globals extends z.ZodObject<any>
498
+ ? Partial<Record<keyof z.output<globals>, string>>
499
+ : Record<string, string> | undefined
500
+ /** Zod schema for global options available to all commands. Parsed before command resolution and passed to middleware and command handlers. */
501
+ globals?: globals | undefined
397
502
  /** Zod schema for named options/flags. */
398
503
  options?: options | undefined
399
504
  /** Zod schema for the return value. */
@@ -446,13 +551,17 @@ export declare namespace create {
446
551
  | Promise<InferReturn<output>>
447
552
  | AsyncGenerator<InferReturn<output>, unknown, unknown>)
448
553
  | undefined
449
- /** Options for the built-in `mcp add` command. */
554
+ /** Options for MCP integration. */
450
555
  mcp?:
451
556
  | {
452
557
  /** Target specific agents by default (e.g. `['claude-code', 'cursor']`). */
453
558
  agents?: string[] | undefined
454
559
  /** Override the command agents will run to start the MCP server. Auto-detected if omitted. */
455
560
  command?: string | undefined
561
+ /** Instructions describing how to use the server and its features. */
562
+ instructions?: string | undefined
563
+ /** Disable HTTP MCP session management. Defaults to `true`. */
564
+ stateless?: boolean | undefined
456
565
  }
457
566
  | undefined
458
567
  /** Options for the built-in `skills add` command. */
@@ -504,6 +613,20 @@ async function serveImpl(
504
613
  stdout(s.endsWith('\n') ? s : `${s}\n`)
505
614
  }
506
615
 
616
+ async function writeBanner() {
617
+ if (!options.banner || help) return
618
+ const banner =
619
+ typeof options.banner === 'function'
620
+ ? { render: options.banner, mode: 'all' as const }
621
+ : options.banner
622
+ const mode = banner.mode ?? 'all'
623
+ if (mode !== 'all' && mode !== (human ? 'human' : 'agent')) return
624
+ try {
625
+ const text = await banner.render()
626
+ if (text) writeln(text)
627
+ } catch {}
628
+ }
629
+
507
630
  let builtinFlags: ReturnType<typeof extractBuiltinFlags>
508
631
  try {
509
632
  builtinFlags = extractBuiltinFlags(argv, { configFlag })
@@ -531,9 +654,35 @@ async function serveImpl(
531
654
  schema,
532
655
  configPath,
533
656
  configDisabled,
534
- rest: filtered,
657
+ rest,
535
658
  } = builtinFlags
536
659
 
660
+ let globals: Record<string, unknown> = {}
661
+ let filtered = rest
662
+
663
+ function parseGlobalOptions(validate: boolean) {
664
+ if (!options.globals) return true
665
+ try {
666
+ const result = Parser.parseGlobals(rest, options.globals.schema, options.globals.alias, {
667
+ validate,
668
+ })
669
+ if (validate) globals = result.parsed
670
+ filtered = result.rest
671
+ return true
672
+ } catch (error) {
673
+ const message = error instanceof Error ? error.message : String(error)
674
+ if (human) writeln(formatHumanError({ code: 'UNKNOWN', message }))
675
+ else writeln(Formatter.format({ code: 'UNKNOWN', message }, 'toon'))
676
+ exit(1)
677
+ return false
678
+ }
679
+ }
680
+
681
+ if (!parseGlobalOptions(false)) return
682
+
683
+ // Pre-load yaml for the sync formatting paths below (yaml is loaded lazily -- see internal/yaml.ts).
684
+ if (formatFlag === 'yaml') await Yaml.load()
685
+
537
686
  // --mcp: start as MCP stdio server
538
687
  if (mcpFlag) {
539
688
  await Mcp.serve(name, options.version ?? '0.0.0', commands, {
@@ -541,6 +690,7 @@ async function serveImpl(
541
690
  env: options.envSchema,
542
691
  vars: options.vars,
543
692
  version: options.version,
693
+ ...(options.mcp?.instructions ? { instructions: options.mcp.instructions } : undefined),
544
694
  })
545
695
  return
546
696
  }
@@ -557,7 +707,15 @@ async function serveImpl(
557
707
  stdout(names.map((n) => Completions.register(completeShell, n)).join('\n'))
558
708
  } else {
559
709
  const index = Number(process.env._COMPLETE_INDEX ?? words.length - 1)
560
- const candidates = Completions.complete(commands, options.rootCommand, words, index)
710
+ const candidates = Completions.complete(
711
+ commands,
712
+ options.rootCommand,
713
+ words,
714
+ index,
715
+ options.globals
716
+ ? { schema: options.globals.schema, alias: options.globals.alias }
717
+ : undefined,
718
+ )
561
719
  // Add built-in commands (completions, mcp, skills) to completions
562
720
  const current = words[index] ?? ''
563
721
  const nonFlags = words.slice(0, index).filter((w) => !w.startsWith('-'))
@@ -630,27 +788,41 @@ async function serveImpl(
630
788
  }
631
789
 
632
790
  const scopedRoot = prefix.length === 0 ? options.rootCommand : undefined
791
+ // Markdown skill output renders scopedName separately. Passing prefix again
792
+ // to those collect helpers would double the group segment in command names
793
+ // (e.g. "cli auth auth login" instead of "cli auth login").
794
+ const collectPrefix = prefix.length > 0 ? ([] as string[]) : prefix
633
795
 
634
796
  if (llmsFull) {
635
797
  if (!formatExplicit || formatFlag === 'md') {
636
798
  const groups = new Map<string, string>()
637
- const cmds = collectSkillCommands(scopedCommands, prefix, groups, scopedRoot)
799
+ const cmds = collectSkillCommands(scopedCommands, collectPrefix, groups, scopedRoot)
638
800
  const scopedName = prefix.length > 0 ? `${name} ${prefix.join(' ')}` : name
639
801
  writeln(Skill.generate(scopedName, cmds, groups))
640
802
  return
641
803
  }
642
- writeln(Formatter.format(buildManifest(scopedCommands, prefix), formatFlag))
804
+ writeln(
805
+ Formatter.format(
806
+ buildManifest(scopedCommands, prefix, options.globals?.schema),
807
+ formatFlag,
808
+ ),
809
+ )
643
810
  return
644
811
  }
645
812
 
646
813
  if (!formatExplicit || formatFlag === 'md') {
647
814
  const groups = new Map<string, string>()
648
- const cmds = collectSkillCommands(scopedCommands, prefix, groups, scopedRoot)
815
+ const cmds = collectSkillCommands(scopedCommands, collectPrefix, groups, scopedRoot)
649
816
  const scopedName = prefix.length > 0 ? `${name} ${prefix.join(' ')}` : name
650
817
  writeln(Skill.index(scopedName, cmds, scopedDescription))
651
818
  return
652
819
  }
653
- writeln(Formatter.format(buildIndexManifest(scopedCommands, prefix), formatFlag))
820
+ writeln(
821
+ Formatter.format(
822
+ buildIndexManifest(scopedCommands, prefix, options.globals?.schema),
823
+ formatFlag,
824
+ ),
825
+ )
654
826
  return
655
827
  }
656
828
 
@@ -930,12 +1102,14 @@ async function serveImpl(
930
1102
  ) {
931
1103
  // Root command with args but none provided (human mode) — show help
932
1104
  const cmd = options.rootCommand
1105
+ await writeBanner()
933
1106
  writeln(
934
1107
  Help.formatCommand(name, {
935
1108
  alias: cmd.alias as Record<string, string> | undefined,
936
1109
  aliases: options.aliases,
937
1110
  configFlag,
938
1111
  description: cmd.description ?? options.description,
1112
+ globals: options.globals,
939
1113
  version: options.version,
940
1114
  args: cmd.args,
941
1115
  env: cmd.env,
@@ -953,11 +1127,13 @@ async function serveImpl(
953
1127
  if (options.rootCommand || options.rootFetch) {
954
1128
  // Root command/fetch with no args — treat as root invocation
955
1129
  } else {
1130
+ await writeBanner()
956
1131
  writeln(
957
1132
  Help.formatRoot(name, {
958
1133
  aliases: options.aliases,
959
1134
  configFlag,
960
1135
  description: options.description,
1136
+ globals: options.globals,
961
1137
  version: options.version,
962
1138
  commands: collectHelpCommands(commands),
963
1139
  root: true,
@@ -1018,6 +1194,7 @@ async function serveImpl(
1018
1194
  aliases: options.aliases,
1019
1195
  configFlag,
1020
1196
  description: cmd.description ?? options.description,
1197
+ globals: options.globals,
1021
1198
  version: options.version,
1022
1199
  args: cmd.args,
1023
1200
  env: cmd.env,
@@ -1036,6 +1213,7 @@ async function serveImpl(
1036
1213
  aliases: isRoot ? options.aliases : undefined,
1037
1214
  configFlag,
1038
1215
  description: helpDesc,
1216
+ globals: options.globals,
1039
1217
  version: isRoot ? options.version : undefined,
1040
1218
  commands: collectHelpCommands(helpCmds),
1041
1219
  root: isRoot,
@@ -1056,6 +1234,7 @@ async function serveImpl(
1056
1234
  aliases: isRootCmd ? options.aliases : cmd.aliases,
1057
1235
  configFlag,
1058
1236
  description: cmd.description,
1237
+ globals: options.globals,
1059
1238
  version: isRootCmd ? options.version : undefined,
1060
1239
  args: cmd.args,
1061
1240
  env: cmd.env,
@@ -1079,6 +1258,7 @@ async function serveImpl(
1079
1258
  Help.formatRoot(`${name} ${resolved.path}`, {
1080
1259
  configFlag,
1081
1260
  description: resolved.description,
1261
+ globals: options.globals,
1082
1262
  commands: collectHelpCommands(resolved.commands),
1083
1263
  }),
1084
1264
  )
@@ -1104,6 +1284,7 @@ async function serveImpl(
1104
1284
  if (cmd.env) result.env = Schema.toJsonSchema(cmd.env)
1105
1285
  if (cmd.options) result.options = Schema.toJsonSchema(cmd.options)
1106
1286
  if (cmd.output) result.output = Schema.toJsonSchema(cmd.output)
1287
+ if (options.globals?.schema) result.globals = Schema.toJsonSchema(options.globals.schema)
1107
1288
  writeln(Formatter.format(result, format))
1108
1289
  return
1109
1290
  }
@@ -1113,6 +1294,7 @@ async function serveImpl(
1113
1294
  Help.formatRoot(`${name} ${resolved.path}`, {
1114
1295
  configFlag,
1115
1296
  description: resolved.description,
1297
+ globals: options.globals,
1116
1298
  commands: collectHelpCommands(resolved.commands),
1117
1299
  }),
1118
1300
  )
@@ -1124,6 +1306,7 @@ async function serveImpl(
1124
1306
  // Resolve effective format: explicit --format/--json → command default → CLI default → toon
1125
1307
  const resolvedFormat = 'command' in resolved && (resolved as any).command.format
1126
1308
  const format = formatExplicit ? formatFlag : resolvedFormat || options.format || 'toon'
1309
+ if (format === 'yaml') await Yaml.load()
1127
1310
 
1128
1311
  // Fall back to root fetch/command when no subcommand matches,
1129
1312
  // but only if the token doesn't look like a typo of a known command.
@@ -1285,6 +1468,7 @@ async function serveImpl(
1285
1468
 
1286
1469
  // Fetch gateway execution path
1287
1470
  if ('fetchGateway' in effective) {
1471
+ if (!parseGlobalOptions(true)) return
1288
1472
  const { fetchGateway, path, rest: fetchRest } = effective
1289
1473
  const fetchMiddleware = [
1290
1474
  ...(options.middlewares ?? []),
@@ -1370,6 +1554,7 @@ async function serveImpl(
1370
1554
  error: errorFn,
1371
1555
  format,
1372
1556
  formatExplicit,
1557
+ globals,
1373
1558
  name,
1374
1559
  set(key: string, value: unknown) {
1375
1560
  varsMap[key] = value
@@ -1420,7 +1605,9 @@ async function serveImpl(
1420
1605
  return
1421
1606
  }
1422
1607
 
1423
- const { command, path, rest } = effective
1608
+ const { command, path, rest: commandRest } = effective
1609
+
1610
+ if (!parseGlobalOptions(true)) return
1424
1611
 
1425
1612
  // Collect middleware: root CLI + groups traversed + per-command
1426
1613
  const allMiddleware = [
@@ -1433,7 +1620,7 @@ async function serveImpl(
1433
1620
 
1434
1621
  if (human)
1435
1622
  emitDeprecationWarnings(
1436
- rest,
1623
+ commandRest,
1437
1624
  command.options,
1438
1625
  command.alias as Record<string, string> | undefined,
1439
1626
  )
@@ -1463,13 +1650,14 @@ async function serveImpl(
1463
1650
 
1464
1651
  const result = await Command.execute(command, {
1465
1652
  agent: !human,
1466
- argv: rest,
1653
+ argv: commandRest,
1467
1654
  defaults,
1468
1655
  displayName,
1469
1656
  env: options.envSchema,
1470
1657
  envSource: options.env,
1471
1658
  format,
1472
1659
  formatExplicit,
1660
+ globals,
1473
1661
  inputOptions: {},
1474
1662
  middlewares: allMiddleware,
1475
1663
  name,
@@ -1558,6 +1746,8 @@ declare namespace fetchImpl {
1558
1746
  description?: string | undefined
1559
1747
  /** CLI-level env schema. */
1560
1748
  envSchema?: z.ZodObject<any> | undefined
1749
+ /** Global options schema and alias map. */
1750
+ globals?: GlobalsDescriptor | undefined
1561
1751
  /** Group-level middleware collected during command resolution. */
1562
1752
  groupMiddlewares?: MiddlewareHandler[] | undefined
1563
1753
  mcpHandler?:
@@ -1582,7 +1772,11 @@ declare namespace fetchImpl {
1582
1772
  }
1583
1773
 
1584
1774
  /** @internal Creates a lazy MCP HTTP handler scoped to a CLI instance. */
1585
- function createMcpHttpHandler(name: string, version: string) {
1775
+ function createMcpHttpHandler(
1776
+ name: string,
1777
+ version: string,
1778
+ options: createMcpHttpHandler.Options = {},
1779
+ ) {
1586
1780
  let transport: any
1587
1781
 
1588
1782
  return async (
@@ -1594,8 +1788,12 @@ function createMcpHttpHandler(name: string, version: string) {
1594
1788
  vars?: z.ZodObject<any> | undefined
1595
1789
  },
1596
1790
  ): Promise<Response> => {
1791
+ const stateless = options.stateless ?? true
1792
+ if (stateless && req.method !== 'POST')
1793
+ return new Response(null, { status: 405, headers: { Allow: 'POST' } })
1794
+
1597
1795
  if (!transport) {
1598
- const { McpServer, WebStandardStreamableHTTPServerTransport } =
1796
+ const { fromJsonSchema, McpServer, WebStandardStreamableHTTPServerTransport } =
1599
1797
  await import('@modelcontextprotocol/server')
1600
1798
 
1601
1799
  const server = new McpServer({ name, version })
@@ -1612,6 +1810,9 @@ function createMcpHttpHandler(name: string, version: string) {
1612
1810
  {
1613
1811
  ...(tool.description ? { description: tool.description } : undefined),
1614
1812
  ...(hasInput ? { inputSchema: z.object(mergedShape) } : undefined),
1813
+ ...(tool.outputSchema
1814
+ ? { outputSchema: fromJsonSchema(tool.outputSchema) }
1815
+ : undefined),
1615
1816
  },
1616
1817
  async (...callArgs: any[]) => {
1617
1818
  const params = hasInput ? (callArgs[0] as Record<string, unknown>) : {}
@@ -1626,16 +1827,26 @@ function createMcpHttpHandler(name: string, version: string) {
1626
1827
  )
1627
1828
  }
1628
1829
 
1629
- transport = new WebStandardStreamableHTTPServerTransport({
1630
- sessionIdGenerator: () => crypto.randomUUID(),
1631
- enableJsonResponse: true,
1632
- })
1830
+ const transportOptions = stateless
1831
+ ? { enableJsonResponse: true }
1832
+ : {
1833
+ sessionIdGenerator: () => crypto.randomUUID(),
1834
+ enableJsonResponse: true,
1835
+ }
1836
+ transport = new WebStandardStreamableHTTPServerTransport(transportOptions)
1633
1837
  await server.connect(transport)
1634
1838
  }
1635
1839
  return transport.handleRequest(req)
1636
1840
  }
1637
1841
  }
1638
1842
 
1843
+ declare namespace createMcpHttpHandler {
1844
+ type Options = {
1845
+ /** Disable HTTP MCP session management. Defaults to `true`. */
1846
+ stateless?: boolean | undefined
1847
+ }
1848
+ }
1849
+
1639
1850
  function isOpenapiRoute(segments: string[]) {
1640
1851
  if (segments.length === 1)
1641
1852
  return (
@@ -1677,7 +1888,7 @@ async function fetchImpl(
1677
1888
  if (req.method === 'GET' && isOpenapiRoute(segments)) {
1678
1889
  const spec = generatedOpenapi(name, commands, options)
1679
1890
  const yaml = segments[0] === 'openapi.yml' || segments[0] === 'openapi.yaml'
1680
- return new Response(yaml ? yamlStringify(spec) : JSON.stringify(spec), {
1891
+ return new Response(yaml ? (await Yaml.load()).stringify(spec) : JSON.stringify(spec), {
1681
1892
  status: 200,
1682
1893
  headers: {
1683
1894
  'content-type': yaml ? 'application/yaml' : 'application/json',
@@ -1701,6 +1912,8 @@ async function fetchImpl(
1701
1912
  segments.length >= 3 &&
1702
1913
  req.method === 'GET'
1703
1914
  ) {
1915
+ // Pre-load yaml for the sync call paths below (`Skill.split`, frontmatter parsing).
1916
+ await Yaml.load()
1704
1917
  const groups = new Map<string, string>()
1705
1918
  const cmds = collectSkillCommands(commands, [], groups, options.rootCommand)
1706
1919
 
@@ -1709,7 +1922,7 @@ async function fetchImpl(
1709
1922
  const files = Skill.split(name, cmds, 1, groups)
1710
1923
  const skills = files.map((f) => {
1711
1924
  const fmMatch = f.content.match(/^---\n([\s\S]*?)\n---/)
1712
- const meta = fmMatch ? (yamlParse(fmMatch[1]!) as Record<string, string>) : {}
1925
+ const meta = fmMatch ? (Yaml.loadSync().parse(fmMatch[1]!) as Record<string, string>) : {}
1713
1926
  return {
1714
1927
  name: f.dir || name,
1715
1928
  description: meta.description ?? '',
@@ -1750,7 +1963,7 @@ async function fetchImpl(
1750
1963
  }
1751
1964
 
1752
1965
  function jsonResponse(body: unknown, status: number) {
1753
- return new Response(JSON.stringify(body), {
1966
+ return new Response(Json.stringify(body), {
1754
1967
  status,
1755
1968
  headers: { 'content-type': 'application/json' },
1756
1969
  })
@@ -1823,7 +2036,7 @@ async function executeCommand(
1823
2036
  options: fetchImpl.Options,
1824
2037
  ): Promise<Response> {
1825
2038
  function jsonResponse(body: unknown, status: number) {
1826
- return new Response(JSON.stringify(body), {
2039
+ return new Response(Json.stringify(body), {
1827
2040
  status,
1828
2041
  headers: { 'content-type': 'application/json' },
1829
2042
  })
@@ -1835,13 +2048,40 @@ async function executeCommand(
1835
2048
  ...((command.middleware as MiddlewareHandler[] | undefined) ?? []),
1836
2049
  ]
1837
2050
 
2051
+ let globals: Record<string, unknown> = {}
2052
+ let commandInputOptions = inputOptions
2053
+ if (options.globals) {
2054
+ const globalKeys = new Set(Object.keys(options.globals.schema.shape))
2055
+ const rawGlobals: Record<string, unknown> = {}
2056
+ commandInputOptions = {}
2057
+ for (const [key, value] of Object.entries(inputOptions)) {
2058
+ if (globalKeys.has(key)) rawGlobals[key] = value
2059
+ else commandInputOptions[key] = value
2060
+ }
2061
+ try {
2062
+ globals = options.globals.schema.parse(rawGlobals)
2063
+ } catch (error: any) {
2064
+ const issues: any[] = error?.issues ?? error?.error?.issues ?? []
2065
+ const message = issues.map((i: any) => i.message).join('; ') || 'Validation failed'
2066
+ return jsonResponse(
2067
+ {
2068
+ ok: false,
2069
+ error: { code: 'VALIDATION_ERROR', message },
2070
+ meta: { command: path, duration: `${Math.round(performance.now() - start)}ms` },
2071
+ },
2072
+ 400,
2073
+ )
2074
+ }
2075
+ }
2076
+
1838
2077
  const result = await Command.execute(command, {
1839
2078
  agent: true,
1840
2079
  argv: rest,
1841
2080
  env: options.envSchema,
1842
2081
  format: 'json',
1843
2082
  formatExplicit: true,
1844
- inputOptions,
2083
+ globals,
2084
+ inputOptions: commandInputOptions,
1845
2085
  middlewares: allMiddleware,
1846
2086
  name: options.name ?? path,
1847
2087
  parseMode: 'split',
@@ -1854,39 +2094,78 @@ async function executeCommand(
1854
2094
 
1855
2095
  // Streaming path — async generator → NDJSON response
1856
2096
  if ('stream' in result) {
2097
+ const iterator = result.stream
2098
+ const encoder = new TextEncoder()
2099
+ const meta = (cta?: FormattedCtaBlock | undefined) => ({
2100
+ command: path,
2101
+ duration: `${Math.round(performance.now() - start)}ms`,
2102
+ ...(cta ? { cta } : undefined),
2103
+ })
2104
+ const errorRecord = (err: ErrorResult) => ({
2105
+ type: 'error',
2106
+ ok: false,
2107
+ error: {
2108
+ code: err.code,
2109
+ message: err.message,
2110
+ ...(err.retryable !== undefined ? { retryable: err.retryable } : undefined),
2111
+ },
2112
+ meta: meta(formatCtaBlock(options.name ?? path, err.cta)),
2113
+ })
1857
2114
  const stream = new ReadableStream({
1858
- async start(controller) {
1859
- const encoder = new TextEncoder()
2115
+ async cancel() {
2116
+ await iterator.return(undefined)
2117
+ },
2118
+ async pull(controller) {
1860
2119
  try {
1861
- for await (const value of result.stream) {
2120
+ const { value, done } = await iterator.next()
2121
+ if (done) {
2122
+ if (isSentinel(value) && value[sentinel] === 'error') {
2123
+ controller.enqueue(encoder.encode(Json.stringify(errorRecord(value)) + '\n'))
2124
+ controller.close()
2125
+ return
2126
+ }
2127
+ const cta =
2128
+ isSentinel(value) && value[sentinel] === 'ok'
2129
+ ? formatCtaBlock(options.name ?? path, value.cta)
2130
+ : undefined
1862
2131
  controller.enqueue(
1863
- encoder.encode(JSON.stringify({ type: 'chunk', data: value }) + '\n'),
2132
+ encoder.encode(
2133
+ Json.stringify({
2134
+ type: 'done',
2135
+ ok: true,
2136
+ meta: meta(cta),
2137
+ }) + '\n',
2138
+ ),
1864
2139
  )
2140
+ controller.close()
2141
+ return
1865
2142
  }
1866
- controller.enqueue(
1867
- encoder.encode(
1868
- JSON.stringify({
1869
- type: 'done',
1870
- ok: true,
1871
- meta: { command: path },
1872
- }) + '\n',
1873
- ),
1874
- )
2143
+
2144
+ if (isSentinel(value) && value[sentinel] === 'error') {
2145
+ controller.enqueue(encoder.encode(Json.stringify(errorRecord(value)) + '\n'))
2146
+ await iterator.return(undefined)
2147
+ controller.close()
2148
+ return
2149
+ }
2150
+
2151
+ controller.enqueue(encoder.encode(Json.stringify({ type: 'chunk', data: value }) + '\n'))
1875
2152
  } catch (error) {
1876
2153
  controller.enqueue(
1877
2154
  encoder.encode(
1878
- JSON.stringify({
2155
+ Json.stringify({
1879
2156
  type: 'error',
1880
2157
  ok: false,
1881
2158
  error: {
1882
- code: 'UNKNOWN',
2159
+ code: error instanceof IncurError ? error.code : 'UNKNOWN',
1883
2160
  message: error instanceof Error ? error.message : String(error),
2161
+ ...(error instanceof IncurError ? { retryable: error.retryable } : undefined),
1884
2162
  },
2163
+ meta: meta(),
1885
2164
  }) + '\n',
1886
2165
  ),
1887
2166
  )
2167
+ controller.close()
1888
2168
  }
1889
- controller.close()
1890
2169
  },
1891
2170
  })
1892
2171
  return new Response(stream, {
@@ -1906,6 +2185,7 @@ async function executeCommand(
1906
2185
  ...(result.error.retryable !== undefined
1907
2186
  ? { retryable: result.error.retryable }
1908
2187
  : undefined),
2188
+ ...(result.error.fieldErrors ? { fieldErrors: result.error.fieldErrors } : undefined),
1909
2189
  },
1910
2190
  meta: {
1911
2191
  command: path,
@@ -2107,6 +2387,8 @@ declare namespace serveImpl {
2107
2387
  envSchema?: z.ZodObject<any> | undefined
2108
2388
  /** CLI-level default output format. */
2109
2389
  format?: Formatter.Format | undefined
2390
+ /** Global options schema and alias map. */
2391
+ globals?: GlobalsDescriptor | undefined
2110
2392
  /** Middleware handlers registered on the root CLI. */
2111
2393
  middlewares?: MiddlewareHandler[] | undefined
2112
2394
  /** CLI-level default output policy. */
@@ -2115,6 +2397,16 @@ declare namespace serveImpl {
2115
2397
  | {
2116
2398
  agents?: string[] | undefined
2117
2399
  command?: string | undefined
2400
+ instructions?: string | undefined
2401
+ stateless?: boolean | undefined
2402
+ }
2403
+ | undefined
2404
+ /** Banner config, called before root help. */
2405
+ banner?:
2406
+ | (() => string | undefined | Promise<string | undefined>)
2407
+ | {
2408
+ render: () => string | undefined | Promise<string | undefined>
2409
+ mode?: 'all' | 'human' | 'agent' | undefined
2118
2410
  }
2119
2411
  | undefined
2120
2412
  /** Root command handler, invoked when no subcommand matches. */
@@ -2528,6 +2820,41 @@ function resolveAlias(
2528
2820
  return entry
2529
2821
  }
2530
2822
 
2823
+ /** @internal Validates command options against CLI-level global options. */
2824
+ function assertNoGlobalOptionConflicts(
2825
+ path: string,
2826
+ entry: CommandEntry,
2827
+ globals: GlobalsDescriptor | undefined,
2828
+ ) {
2829
+ if (!globals || isFetchGateway(entry) || isAlias(entry)) return
2830
+ if (isGroup(entry)) {
2831
+ for (const [name, child] of entry.commands)
2832
+ assertNoGlobalOptionConflicts(`${path} ${name}`, child, globals)
2833
+ return
2834
+ }
2835
+
2836
+ if (entry.options) {
2837
+ const globalKeys = Object.keys(globals.schema.shape)
2838
+ const optionKeys = Object.keys(entry.options.shape)
2839
+ for (const key of optionKeys) {
2840
+ if (globalKeys.includes(key))
2841
+ throw new Error(
2842
+ `Command '${path}' option '${key}' conflicts with a global option. Choose a different name.`,
2843
+ )
2844
+ }
2845
+ }
2846
+
2847
+ if (globals.alias && entry.alias) {
2848
+ const globalAliasValues = new Set(Object.values(globals.alias))
2849
+ for (const [name, short] of Object.entries(entry.alias)) {
2850
+ if (short && globalAliasValues.has(short))
2851
+ throw new Error(
2852
+ `Command '${path}' alias '-${short}' for '${name}' conflicts with a global alias. Choose a different alias.`,
2853
+ )
2854
+ }
2855
+ }
2856
+ }
2857
+
2531
2858
  /** @internal Maps CLI instances to their command maps. */
2532
2859
  export const toCommands = new WeakMap<Cli, Map<string, CommandEntry>>()
2533
2860
 
@@ -2546,6 +2873,15 @@ export const toConfigEnabled = new WeakMap<Cli, boolean>()
2546
2873
  /** @internal Maps CLI instances to their output policy. */
2547
2874
  const toOutputPolicy = new WeakMap<Cli, OutputPolicy>()
2548
2875
 
2876
+ /** Descriptor for a CLI's custom global options schema and aliases. */
2877
+ export type GlobalsDescriptor = {
2878
+ schema: z.ZodObject<any>
2879
+ alias?: Record<string, string> | undefined
2880
+ }
2881
+
2882
+ /** @internal Maps CLI instances to their globals schema and alias map. */
2883
+ const toGlobals = new WeakMap<Cli, GlobalsDescriptor>()
2884
+
2549
2885
  /** @internal Maps root CLI instances to their command aliases. */
2550
2886
  const toRootAliases = new WeakMap<Root, string[]>()
2551
2887
 
@@ -2667,7 +3003,7 @@ async function handleStreaming(
2667
3003
  return
2668
3004
  }
2669
3005
  }
2670
- if (useJsonl) ctx.writeln(JSON.stringify({ type: 'chunk', data: value }))
3006
+ if (useJsonl) ctx.writeln(Json.stringify({ type: 'chunk', data: value }))
2671
3007
  else if (ctx.renderOutput)
2672
3008
  ctx.writeln(ctx.truncate(Formatter.format(value, ctx.format)).text)
2673
3009
  }
@@ -2719,6 +3055,7 @@ async function handleStreaming(
2719
3055
  error: {
2720
3056
  code: error instanceof IncurError ? error.code : 'UNKNOWN',
2721
3057
  message: error instanceof Error ? error.message : String(error),
3058
+ ...(error instanceof IncurError ? { retryable: error.retryable } : undefined),
2722
3059
  },
2723
3060
  }),
2724
3061
  )
@@ -2802,6 +3139,7 @@ async function handleStreaming(
2802
3139
  error: {
2803
3140
  code: error instanceof IncurError ? error.code : 'UNKNOWN',
2804
3141
  message: error instanceof Error ? error.message : String(error),
3142
+ ...(error instanceof IncurError ? { retryable: error.retryable } : undefined),
2805
3143
  },
2806
3144
  meta: {
2807
3145
  command: ctx.path,
@@ -2813,36 +3151,16 @@ async function handleStreaming(
2813
3151
  }
2814
3152
  }
2815
3153
 
2816
- /** @internal Formats a CTA block into the output envelope shape. */
2817
- function formatCtaBlock(name: string, block: CtaBlock | undefined): FormattedCtaBlock | undefined {
2818
- if (!block || block.commands.length === 0) return undefined
2819
- return {
2820
- description:
2821
- block.description ??
2822
- (block.commands.length === 1 ? 'Suggested command:' : 'Suggested commands:'),
2823
- commands: block.commands.map((c) => formatCta(name, c)),
2824
- }
2825
- }
2826
-
2827
- /** @internal Formats a CTA by prefixing the CLI name. Handles string and object forms. */
2828
- function formatCta(name: string, cta: Cta): FormattedCta {
2829
- if (typeof cta === 'string') return { command: `${name} ${cta}` }
2830
- const prefix = cta.command === name || cta.command.startsWith(`${name} `) ? '' : `${name} `
2831
- let cmd = `${prefix}${cta.command}`
2832
- if (cta.args)
2833
- for (const [key, value] of Object.entries(cta.args))
2834
- cmd += value === true ? ` <${key}>` : ` ${value}`
2835
- if (cta.options)
2836
- for (const [key, value] of Object.entries(cta.options))
2837
- cmd += value === true ? ` --${key} <${key}>` : ` --${key} ${value}`
2838
- return { command: cmd, ...(cta.description ? { description: cta.description } : undefined) }
2839
- }
2840
-
2841
3154
  /** @internal Builds the `--llms` index manifest (name + description only) from the command tree. */
2842
- function buildIndexManifest(commands: Map<string, CommandEntry>, prefix: string[] = []) {
3155
+ function buildIndexManifest(
3156
+ commands: Map<string, CommandEntry>,
3157
+ prefix: string[] = [],
3158
+ globalsSchema?: z.ZodObject<any>,
3159
+ ) {
2843
3160
  return {
2844
3161
  version: 'incur.v1',
2845
3162
  commands: collectIndexCommands(commands, prefix).sort((a, b) => a.name.localeCompare(b.name)),
3163
+ ...(globalsSchema ? { globals: Schema.toJsonSchema(globalsSchema) } : undefined),
2846
3164
  }
2847
3165
  }
2848
3166
 
@@ -2869,10 +3187,15 @@ function collectIndexCommands(
2869
3187
  }
2870
3188
 
2871
3189
  /** @internal Builds the `--llms` manifest from the command tree. */
2872
- function buildManifest(commands: Map<string, CommandEntry>, prefix: string[] = []) {
3190
+ function buildManifest(
3191
+ commands: Map<string, CommandEntry>,
3192
+ prefix: string[] = [],
3193
+ globalsSchema?: z.ZodObject<any>,
3194
+ ) {
2873
3195
  return {
2874
3196
  version: 'incur.v1',
2875
3197
  commands: collectCommands(commands, prefix).sort((a, b) => a.name.localeCompare(b.name)),
3198
+ ...(globalsSchema ? { globals: Schema.toJsonSchema(globalsSchema) } : undefined),
2876
3199
  }
2877
3200
  }
2878
3201
 
@@ -2925,11 +3248,11 @@ function collectCommands(
2925
3248
  }
2926
3249
 
2927
3250
  /** @internal Recursively collects leaf commands as `Skill.CommandInfo` for `--llms --format md`. */
2928
- function collectSkillCommands(
3251
+ export function collectSkillCommands(
2929
3252
  commands: Map<string, CommandEntry>,
2930
3253
  prefix: string[],
2931
3254
  groups: Map<string, string>,
2932
- rootCommand?: CommandDefinition<any, any, any> | undefined,
3255
+ rootCommand?: SkillCommandSource | undefined,
2933
3256
  ): Skill.CommandInfo[] {
2934
3257
  const result: Skill.CommandInfo[] = []
2935
3258
  if (rootCommand) {
@@ -2938,6 +3261,7 @@ function collectSkillCommands(
2938
3261
  if (rootCommand.args) cmd.args = rootCommand.args
2939
3262
  if (rootCommand.env) cmd.env = rootCommand.env
2940
3263
  if (rootCommand.hint) cmd.hint = rootCommand.hint
3264
+ if (isDestructive(rootCommand)) cmd.hint = appendDestructiveHint(cmd.hint)
2941
3265
  if (rootCommand.options) cmd.options = rootCommand.options
2942
3266
  if (rootCommand.output) cmd.output = rootCommand.output
2943
3267
  const examples = formatExamples(rootCommand.examples)
@@ -2961,6 +3285,7 @@ function collectSkillCommands(
2961
3285
  if (entry.args) cmd.args = entry.args
2962
3286
  if (entry.env) cmd.env = entry.env
2963
3287
  if (entry.hint) cmd.hint = entry.hint
3288
+ if (isDestructive(entry)) cmd.hint = appendDestructiveHint(cmd.hint)
2964
3289
  if (entry.options) cmd.options = entry.options
2965
3290
  if (entry.output) cmd.output = entry.output
2966
3291
  const examples = formatExamples(entry.examples)
@@ -2977,6 +3302,29 @@ function collectSkillCommands(
2977
3302
  return result.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''))
2978
3303
  }
2979
3304
 
3305
+ type SkillCommandSource = Pick<
3306
+ CommandDefinition<any, any, any, any, any, any>,
3307
+ | 'args'
3308
+ | 'description'
3309
+ | 'destructive'
3310
+ | 'env'
3311
+ | 'examples'
3312
+ | 'hint'
3313
+ | 'mcp'
3314
+ | 'options'
3315
+ | 'output'
3316
+ >
3317
+
3318
+ function isDestructive(command: SkillCommandSource): boolean {
3319
+ return command.destructive === true || command.mcp?.annotations?.destructiveHint === true
3320
+ }
3321
+
3322
+ function appendDestructiveHint(hint: string | undefined): string {
3323
+ if (!hint) return destructiveCommandHint
3324
+ if (hint.includes(destructiveCommandHint)) return hint
3325
+ return `${hint} ${destructiveCommandHint}`
3326
+ }
3327
+
2980
3328
  /** @internal Formats examples into `{ command, description }` objects. `command` is the args/options suffix only. */
2981
3329
  export function formatExamples(
2982
3330
  examples: Example<any, any>[] | undefined,
@@ -2993,6 +3341,18 @@ export function formatExamples(
2993
3341
  })
2994
3342
  }
2995
3343
 
3344
+ /** @internal Parses YAML frontmatter from generated skill Markdown. */
3345
+ export function parseSkillFrontmatter(content: string): {
3346
+ description?: string | undefined
3347
+ name?: string | undefined
3348
+ } {
3349
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
3350
+ if (!match) return {}
3351
+ const meta = Yaml.loadSync().parse(match[1]!)
3352
+ if (!meta || typeof meta !== 'object') return {}
3353
+ return meta as { description?: string | undefined; name?: string | undefined }
3354
+ }
3355
+
2996
3356
  /** @internal Builds separate args, env, and options JSON Schemas. */
2997
3357
  function buildInputSchema(
2998
3358
  args: z.ZodObject<any> | undefined,
@@ -3121,12 +3481,23 @@ type CommandDefinition<
3121
3481
  args?: args | undefined
3122
3482
  /** Zod schema for environment variables. Keys are the variable names (e.g. `NPM_TOKEN`). */
3123
3483
  env?: env | undefined
3484
+ /** Marks this command as destructive when generating agent skills. */
3485
+ destructive?: boolean | undefined
3124
3486
  /** Usage examples for this command. */
3125
3487
  examples?: Example<args, options>[] | undefined
3126
3488
  /** Default output format. Overridden by `--format` or `--json`. */
3127
3489
  format?: Formatter.Format | undefined
3128
3490
  /** Plain text hint displayed after examples and before global options. */
3129
3491
  hint?: string | undefined
3492
+ /** MCP-specific metadata exposed when this command is served as a tool. */
3493
+ mcp?:
3494
+ | {
3495
+ /** MCP tool annotations that describe tool behavior to clients. */
3496
+ annotations?: Mcp.ToolAnnotations | undefined
3497
+ /** Tool-specific instructions surfaced to MCP clients. */
3498
+ instructions?: string | undefined
3499
+ }
3500
+ | undefined
3130
3501
  /** Zod schema for the command's return value. */
3131
3502
  output?: output | undefined
3132
3503
  /**
@@ -3179,22 +3550,6 @@ type CommandDefinition<
3179
3550
  | AsyncGenerator<InferReturn<output>, unknown, unknown>
3180
3551
  }
3181
3552
 
3182
- /** @internal A formatted CTA block as it appears in the output envelope. */
3183
- type FormattedCtaBlock = {
3184
- /** Formatted command suggestions. */
3185
- commands: FormattedCta[]
3186
- /** Human-readable label for the CTA block. */
3187
- description: string
3188
- }
3189
-
3190
- /** @internal A formatted CTA as it appears in the output envelope. */
3191
- type FormattedCta = {
3192
- /** The full command string with args and options folded in. */
3193
- command: string
3194
- /** A short description of what the command does. */
3195
- description?: string | undefined
3196
- }
3197
-
3198
3553
  /** @internal Scans argv for deprecated flags and writes warnings to stderr. */
3199
3554
  function emitDeprecationWarnings(
3200
3555
  argv: string[],