incur 0.4.8 → 0.4.10

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 +69 -19
  2. package/dist/Cli.d.ts.map +1 -1
  3. package/dist/Cli.js +419 -73
  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 +44 -13
  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 +31 -19
  40. package/dist/internal/command.d.ts.map +1 -1
  41. package/dist/internal/command.js +10 -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 +73 -0
  61. package/src/Cli.test.ts +1536 -48
  62. package/src/Cli.ts +606 -108
  63. package/src/Completions.test.ts +85 -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 +32 -1
  68. package/src/Help.ts +42 -4
  69. package/src/Mcp.test.ts +289 -0
  70. package/src/Mcp.ts +68 -13
  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 +166 -12
  83. package/src/internal/command.ts +12 -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
@@ -1,8 +1,8 @@
1
1
  import * as fs from 'node:fs/promises'
2
2
  import * as os from 'node:os'
3
3
  import * as path from 'node:path'
4
+ import { PassThrough } from 'node:stream'
4
5
  import { estimateTokenCount, sliceByTokens } from 'tokenx'
5
- import { parse as yamlParse, stringify as yamlStringify } from 'yaml'
6
6
  import { z } from 'zod'
7
7
 
8
8
  import * as Completions from './Completions.js'
@@ -21,9 +21,12 @@ import {
21
21
  shells,
22
22
  } from './internal/command.js'
23
23
  import * as Command from './internal/command.js'
24
+ import { formatCtaBlock, type FormattedCta, type FormattedCtaBlock } from './internal/cta.js'
24
25
  import { isRecord, suggest, toKebab } from './internal/helpers.js'
26
+ import * as Json from './internal/json.js'
25
27
  import { detectRunner } from './internal/pm.js'
26
28
  import type { OneOf } from './internal/types.js'
29
+ import * as Yaml from './internal/yaml.js'
27
30
  import * as Mcp from './Mcp.js'
28
31
  import type { Context as MiddlewareContext, Handler as MiddlewareHandler } from './middleware.js'
29
32
  import * as Openapi from './Openapi.js'
@@ -35,11 +38,14 @@ import * as Skill from './Skill.js'
35
38
  import * as SyncMcp from './SyncMcp.js'
36
39
  import * as SyncSkills from './SyncSkills.js'
37
40
 
41
+ const destructiveCommandHint = 'Confirm with the user before executing this destructive command.'
42
+
38
43
  /** A CLI application instance. Also used as a command group when mounted on a parent CLI. */
39
44
  export type Cli<
40
45
  commands extends CommandsMap = {},
41
46
  vars extends z.ZodObject<any> | undefined = undefined,
42
47
  env extends z.ZodObject<any> | undefined = undefined,
48
+ globals extends z.ZodObject<any> | undefined = undefined,
43
49
  > = {
44
50
  /** Registers a root command or mounts a sub-CLI as a command group. */
45
51
  command: {
@@ -56,12 +62,18 @@ export type Cli<
56
62
  ): Cli<
57
63
  commands & { [key in name]: { args: InferOutput<args>; options: InferOutput<options> } },
58
64
  vars,
59
- env
65
+ env,
66
+ globals
60
67
  >
61
68
  /** Mounts a sub-CLI as a command group. */
62
69
  <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>
70
+ cli: Cli<sub, any, any, any> & { name: name },
71
+ ): Cli<
72
+ commands & { [key in keyof sub & string as `${name} ${key}`]: sub[key] },
73
+ vars,
74
+ env,
75
+ globals
76
+ >
65
77
  /** Mounts a root CLI as a single command. */
66
78
  <
67
79
  const name extends string,
@@ -72,7 +84,8 @@ export type Cli<
72
84
  ): Cli<
73
85
  commands & { [key in name]: { args: InferOutput<args>; options: InferOutput<opts> } },
74
86
  vars,
75
- env
87
+ env,
88
+ globals
76
89
  >
77
90
  /** Mounts a fetch handler as a command, optionally with OpenAPI spec for typed subcommands. */
78
91
  <const name extends string>(
@@ -85,7 +98,7 @@ export type Cli<
85
98
  openapiConfig?: Openapi.Config | undefined
86
99
  outputPolicy?: OutputPolicy | undefined
87
100
  },
88
- ): Cli<commands, vars, env>
101
+ ): Cli<commands, vars, env, globals>
89
102
  }
90
103
  /** A short description of the CLI. */
91
104
  description?: string | undefined
@@ -98,7 +111,7 @@ export type Cli<
98
111
  /** Parses argv, runs the matched command, and writes the output envelope to stdout. */
99
112
  serve(argv?: string[], options?: serve.Options): Promise<void>
100
113
  /** Registers middleware that runs around every command. */
101
- use(handler: MiddlewareHandler<vars, env>): Cli<commands, vars, env>
114
+ use(handler: MiddlewareHandler<vars, env, globals>): Cli<commands, vars, env, globals>
102
115
  /** The vars schema, if declared. Use `typeof cli.vars` with `middleware<vars, env>()` for typed middleware. */
103
116
  vars: vars
104
117
  }
@@ -163,10 +176,16 @@ export function create<
163
176
  const opts extends z.ZodObject<any> | undefined = undefined,
164
177
  const output extends z.ZodType | undefined = undefined,
165
178
  const vars extends z.ZodObject<any> | undefined = undefined,
179
+ const globals extends z.ZodObject<any> | undefined = undefined,
166
180
  >(
167
181
  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>
182
+ definition: create.Options<args, env, opts, output, vars, globals> & { run: Function },
183
+ ): Cli<
184
+ { [key in typeof name]: { args: InferOutput<args>; options: InferOutput<opts> } },
185
+ vars,
186
+ env,
187
+ globals
188
+ >
170
189
  /** Creates a router CLI that registers subcommands. */
171
190
  export function create<
172
191
  const args extends z.ZodObject<any> | undefined = undefined,
@@ -174,7 +193,11 @@ export function create<
174
193
  const opts extends z.ZodObject<any> | undefined = undefined,
175
194
  const output extends z.ZodType | undefined = undefined,
176
195
  const vars extends z.ZodObject<any> | undefined = undefined,
177
- >(name: string, definition?: create.Options<args, env, opts, output, vars>): Cli<{}, vars, env>
196
+ const globals extends z.ZodObject<any> | undefined = undefined,
197
+ >(
198
+ name: string,
199
+ definition?: create.Options<args, env, opts, output, vars, globals>,
200
+ ): Cli<{}, vars, env, globals>
178
201
  /** Creates a CLI with a root handler from a single options object. Can still register subcommands. */
179
202
  export function create<
180
203
  const args extends z.ZodObject<any> | undefined = undefined,
@@ -182,14 +205,19 @@ export function create<
182
205
  const opts extends z.ZodObject<any> | undefined = undefined,
183
206
  const output extends z.ZodType | undefined = undefined,
184
207
  const vars extends z.ZodObject<any> | undefined = undefined,
208
+ const globals extends z.ZodObject<any> | undefined = undefined,
185
209
  >(
186
- definition: create.Options<args, env, opts, output, vars> & { name: string; run: Function },
210
+ definition: create.Options<args, env, opts, output, vars, globals> & {
211
+ name: string
212
+ run: Function
213
+ },
187
214
  ): Cli<
188
215
  {
189
216
  [key in (typeof definition)['name']]: { args: InferOutput<args>; options: InferOutput<opts> }
190
217
  },
191
218
  vars,
192
- env
219
+ env,
220
+ globals
193
221
  >
194
222
  /** Creates a router CLI from a single options object (e.g. package.json). */
195
223
  export function create<
@@ -198,7 +226,10 @@ export function create<
198
226
  const opts extends z.ZodObject<any> | undefined = undefined,
199
227
  const output extends z.ZodType | undefined = undefined,
200
228
  const vars extends z.ZodObject<any> | undefined = undefined,
201
- >(definition: create.Options<args, env, opts, output, vars> & { name: string }): Cli<{}, vars, env>
229
+ const globals extends z.ZodObject<any> | undefined = undefined,
230
+ >(
231
+ definition: create.Options<args, env, opts, output, vars, globals> & { name: string },
232
+ ): Cli<{}, vars, env, globals>
202
233
  export function create(
203
234
  nameOrDefinition: string | (any & { name: string }),
204
235
  definition?: any,
@@ -214,7 +245,9 @@ export function create(
214
245
  const commands = new Map<string, CommandEntry>()
215
246
  const middlewares: MiddlewareHandler[] = []
216
247
  const pending: Promise<void>[] = []
217
- const mcpHandler = createMcpHttpHandler(name, def.version ?? '0.0.0')
248
+ const mcpHandler = createMcpHttpHandler(name, def.version ?? '0.0.0', {
249
+ stateless: def.mcp?.stateless,
250
+ })
218
251
 
219
252
  if (def.openapi && rootFetch) {
220
253
  pending.push(
@@ -249,12 +282,14 @@ export function create(
249
282
  basePath: def.basePath,
250
283
  config: def.openapiConfig,
251
284
  })
252
- commands.set(nameOrCli, {
285
+ const entry = {
253
286
  _group: true,
254
287
  description: def.description,
255
288
  commands: generated as Map<string, CommandEntry>,
256
289
  ...(def.outputPolicy ? { outputPolicy: def.outputPolicy } : undefined),
257
- } as InternalGroup)
290
+ } as InternalGroup
291
+ assertNoGlobalOptionConflicts(nameOrCli, entry, toGlobals.get(cli))
292
+ commands.set(nameOrCli, entry)
258
293
  })(),
259
294
  )
260
295
  return cli
@@ -268,6 +303,7 @@ export function create(
268
303
  } as InternalFetchGateway)
269
304
  return cli
270
305
  }
306
+ assertNoGlobalOptionConflicts(nameOrCli, def, toGlobals.get(cli))
271
307
  commands.set(nameOrCli, def)
272
308
  if (def.aliases)
273
309
  for (const a of def.aliases) commands.set(a, { _alias: true, target: nameOrCli })
@@ -275,6 +311,7 @@ export function create(
275
311
  }
276
312
  const mountedRootDef = toRootDefinition.get(nameOrCli)
277
313
  if (mountedRootDef) {
314
+ assertNoGlobalOptionConflicts(nameOrCli.name, mountedRootDef, toGlobals.get(cli))
278
315
  commands.set(nameOrCli.name, mountedRootDef)
279
316
  const rootAliases = toRootAliases.get(nameOrCli)
280
317
  if (rootAliases)
@@ -285,21 +322,25 @@ export function create(
285
322
  const subCommands = toCommands.get(sub)!
286
323
  const subOutputPolicy = toOutputPolicy.get(sub)
287
324
  const subMiddlewares = toMiddlewares.get(sub)
288
- commands.set(sub.name, {
325
+ const entry = {
289
326
  _group: true,
290
327
  description: sub.description,
291
328
  commands: subCommands,
292
329
  ...(subOutputPolicy ? { outputPolicy: subOutputPolicy } : undefined),
293
330
  ...(subMiddlewares?.length ? { middlewares: subMiddlewares } : undefined),
294
- })
331
+ } as InternalGroup
332
+ assertNoGlobalOptionConflicts(sub.name, entry, toGlobals.get(cli))
333
+ commands.set(sub.name, entry)
295
334
  return cli
296
335
  },
297
336
 
298
337
  async fetch(req: Request) {
299
338
  if (pending.length > 0) await Promise.all(pending)
339
+ const globalsDesc = toGlobals.get(cli)
300
340
  return fetchImpl(name, commands, req, {
301
341
  description: def.description,
302
342
  envSchema: def.env,
343
+ globals: globalsDesc,
303
344
  mcpHandler,
304
345
  middlewares,
305
346
  name,
@@ -311,13 +352,16 @@ export function create(
311
352
 
312
353
  async serve(argv = process.argv.slice(2), serveOptions: serve.Options = {}) {
313
354
  if (pending.length > 0) await Promise.all(pending)
355
+ const globalsDesc = toGlobals.get(cli)
314
356
  return serveImpl(name, commands, argv, {
315
357
  ...serveOptions,
316
358
  aliases: def.aliases,
359
+ banner: def.banner,
317
360
  config: def.config,
318
361
  description: def.description,
319
362
  envSchema: def.env,
320
363
  format: def.format,
364
+ globals: globalsDesc,
321
365
  mcp: def.mcp,
322
366
  middlewares,
323
367
  outputPolicy: def.outputPolicy,
@@ -340,6 +384,44 @@ export function create(
340
384
  if (def.options) toRootOptions.set(cli, def.options)
341
385
  if (def.config !== undefined) toConfigEnabled.set(cli, true)
342
386
  if (def.outputPolicy) toOutputPolicy.set(cli, def.outputPolicy)
387
+ if (def.globals) {
388
+ toGlobals.set(cli, { schema: def.globals, alias: def.globalAlias as any })
389
+ const builtinNames = [
390
+ 'verbose',
391
+ 'format',
392
+ 'json',
393
+ 'llms',
394
+ 'llmsFull',
395
+ 'mcp',
396
+ 'help',
397
+ 'version',
398
+ 'schema',
399
+ 'filterOutput',
400
+ 'tokenLimit',
401
+ 'tokenOffset',
402
+ 'tokenCount',
403
+ ...(def.config?.flag
404
+ ? [def.config.flag, `no${def.config.flag[0].toUpperCase()}${def.config.flag.slice(1)}`]
405
+ : []),
406
+ ]
407
+ const globalKeys = Object.keys(def.globals.shape)
408
+ for (const key of globalKeys) {
409
+ if (builtinNames.includes(key))
410
+ throw new Error(
411
+ `Global option '${key}' conflicts with a built-in flag. Choose a different name.`,
412
+ )
413
+ }
414
+ // Check globalAlias values against reserved short aliases
415
+ const reservedShorts = new Set(['h'])
416
+ if (def.globalAlias) {
417
+ for (const [name, short] of Object.entries(def.globalAlias as Record<string, string>)) {
418
+ if (reservedShorts.has(short))
419
+ throw new Error(
420
+ `Global alias '-${short}' for '${name}' conflicts with a built-in short flag. Choose a different alias.`,
421
+ )
422
+ }
423
+ }
424
+ }
343
425
  toMiddlewares.set(cli, middlewares)
344
426
  toCommands.set(cli, commands)
345
427
  return cli
@@ -353,6 +435,7 @@ export declare namespace create {
353
435
  options extends z.ZodObject<any> | undefined = undefined,
354
436
  output extends z.ZodType | undefined = undefined,
355
437
  vars extends z.ZodObject<any> | undefined = undefined,
438
+ globals extends z.ZodObject<any> | undefined = undefined,
356
439
  > = {
357
440
  /** Map of option names to single-char aliases. */
358
441
  alias?: options extends z.ZodObject<any>
@@ -360,6 +443,19 @@ export declare namespace create {
360
443
  : Record<string, string> | undefined
361
444
  /** Alternative binary names for this CLI (e.g. shorter aliases in package.json `bin`). Shell completions are registered for all names. */
362
445
  aliases?: string[] | undefined
446
+ /**
447
+ * 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.
448
+ *
449
+ * Pass a function for all consumers, or an object with `mode` to target `'human'`, `'agent'`, or `'all'` (default).
450
+ */
451
+ banner?:
452
+ | (() => string | undefined | Promise<string | undefined>)
453
+ | {
454
+ render: () => string | undefined | Promise<string | undefined>
455
+ /** @default 'all' */
456
+ mode?: 'all' | 'human' | 'agent' | undefined
457
+ }
458
+ | undefined
363
459
  /** Zod schema for positional arguments. */
364
460
  args?: args | undefined
365
461
  /** Enable config-file defaults for command options. */
@@ -382,6 +478,8 @@ export declare namespace create {
382
478
  | undefined
383
479
  /** A short description of what the CLI does. */
384
480
  description?: string | undefined
481
+ /** Marks the root command as destructive when generating agent skills. */
482
+ destructive?: boolean | undefined
385
483
  /** Zod schema for environment variables. Keys are the variable names (e.g. `NPM_TOKEN`). */
386
484
  env?: env | undefined
387
485
  /** Usage examples for this command. */
@@ -394,6 +492,14 @@ export declare namespace create {
394
492
  openapiConfig?: Openapi.Config | undefined
395
493
  /** Default output format. Overridden by `--format` or `--json`. */
396
494
  format?: Formatter.Format | undefined
495
+ /** Plain text hint displayed after examples and before global options. */
496
+ hint?: string | undefined
497
+ /** Map of global option names to single-char aliases. */
498
+ globalAlias?: globals extends z.ZodObject<any>
499
+ ? Partial<Record<keyof z.output<globals>, string>>
500
+ : Record<string, string> | undefined
501
+ /** Zod schema for global options available to all commands. Parsed before command resolution and passed to middleware and command handlers. */
502
+ globals?: globals | undefined
397
503
  /** Zod schema for named options/flags. */
398
504
  options?: options | undefined
399
505
  /** Zod schema for the return value. */
@@ -446,13 +552,17 @@ export declare namespace create {
446
552
  | Promise<InferReturn<output>>
447
553
  | AsyncGenerator<InferReturn<output>, unknown, unknown>)
448
554
  | undefined
449
- /** Options for the built-in `mcp add` command. */
555
+ /** Options for MCP integration. */
450
556
  mcp?:
451
557
  | {
452
558
  /** Target specific agents by default (e.g. `['claude-code', 'cursor']`). */
453
559
  agents?: string[] | undefined
454
560
  /** Override the command agents will run to start the MCP server. Auto-detected if omitted. */
455
561
  command?: string | undefined
562
+ /** Instructions describing how to use the server and its features. */
563
+ instructions?: string | undefined
564
+ /** Disable HTTP MCP session management. Defaults to `true`. */
565
+ stateless?: boolean | undefined
456
566
  }
457
567
  | undefined
458
568
  /** Options for the built-in `skills add` command. */
@@ -504,6 +614,20 @@ async function serveImpl(
504
614
  stdout(s.endsWith('\n') ? s : `${s}\n`)
505
615
  }
506
616
 
617
+ async function writeBanner() {
618
+ if (!options.banner || help) return
619
+ const banner =
620
+ typeof options.banner === 'function'
621
+ ? { render: options.banner, mode: 'all' as const }
622
+ : options.banner
623
+ const mode = banner.mode ?? 'all'
624
+ if (mode !== 'all' && mode !== (human ? 'human' : 'agent')) return
625
+ try {
626
+ const text = await banner.render()
627
+ if (text) writeln(text)
628
+ } catch {}
629
+ }
630
+
507
631
  let builtinFlags: ReturnType<typeof extractBuiltinFlags>
508
632
  try {
509
633
  builtinFlags = extractBuiltinFlags(argv, { configFlag })
@@ -531,9 +655,35 @@ async function serveImpl(
531
655
  schema,
532
656
  configPath,
533
657
  configDisabled,
534
- rest: filtered,
658
+ rest,
535
659
  } = builtinFlags
536
660
 
661
+ let globals: Record<string, unknown> = {}
662
+ let filtered = rest
663
+
664
+ function parseGlobalOptions(validate: boolean) {
665
+ if (!options.globals) return true
666
+ try {
667
+ const result = Parser.parseGlobals(rest, options.globals.schema, options.globals.alias, {
668
+ validate,
669
+ })
670
+ if (validate) globals = result.parsed
671
+ filtered = result.rest
672
+ return true
673
+ } catch (error) {
674
+ const message = error instanceof Error ? error.message : String(error)
675
+ if (human) writeln(formatHumanError({ code: 'UNKNOWN', message }))
676
+ else writeln(Formatter.format({ code: 'UNKNOWN', message }, 'toon'))
677
+ exit(1)
678
+ return false
679
+ }
680
+ }
681
+
682
+ if (!parseGlobalOptions(false)) return
683
+
684
+ // Pre-load yaml for the sync formatting paths below (yaml is loaded lazily -- see internal/yaml.ts).
685
+ if (formatFlag === 'yaml') await Yaml.load()
686
+
537
687
  // --mcp: start as MCP stdio server
538
688
  if (mcpFlag) {
539
689
  await Mcp.serve(name, options.version ?? '0.0.0', commands, {
@@ -541,6 +691,7 @@ async function serveImpl(
541
691
  env: options.envSchema,
542
692
  vars: options.vars,
543
693
  version: options.version,
694
+ ...(options.mcp?.instructions ? { instructions: options.mcp.instructions } : undefined),
544
695
  })
545
696
  return
546
697
  }
@@ -557,7 +708,15 @@ async function serveImpl(
557
708
  stdout(names.map((n) => Completions.register(completeShell, n)).join('\n'))
558
709
  } else {
559
710
  const index = Number(process.env._COMPLETE_INDEX ?? words.length - 1)
560
- const candidates = Completions.complete(commands, options.rootCommand, words, index)
711
+ const candidates = Completions.complete(
712
+ commands,
713
+ options.rootCommand,
714
+ words,
715
+ index,
716
+ options.globals
717
+ ? { schema: options.globals.schema, alias: options.globals.alias }
718
+ : undefined,
719
+ )
561
720
  // Add built-in commands (completions, mcp, skills) to completions
562
721
  const current = words[index] ?? ''
563
722
  const nonFlags = words.slice(0, index).filter((w) => !w.startsWith('-'))
@@ -630,27 +789,41 @@ async function serveImpl(
630
789
  }
631
790
 
632
791
  const scopedRoot = prefix.length === 0 ? options.rootCommand : undefined
792
+ // Markdown skill output renders scopedName separately. Passing prefix again
793
+ // to those collect helpers would double the group segment in command names
794
+ // (e.g. "cli auth auth login" instead of "cli auth login").
795
+ const collectPrefix = prefix.length > 0 ? ([] as string[]) : prefix
633
796
 
634
797
  if (llmsFull) {
635
798
  if (!formatExplicit || formatFlag === 'md') {
636
799
  const groups = new Map<string, string>()
637
- const cmds = collectSkillCommands(scopedCommands, prefix, groups, scopedRoot)
800
+ const cmds = collectSkillCommands(scopedCommands, collectPrefix, groups, scopedRoot)
638
801
  const scopedName = prefix.length > 0 ? `${name} ${prefix.join(' ')}` : name
639
802
  writeln(Skill.generate(scopedName, cmds, groups))
640
803
  return
641
804
  }
642
- writeln(Formatter.format(buildManifest(scopedCommands, prefix), formatFlag))
805
+ writeln(
806
+ Formatter.format(
807
+ buildManifest(scopedCommands, prefix, options.globals?.schema),
808
+ formatFlag,
809
+ ),
810
+ )
643
811
  return
644
812
  }
645
813
 
646
814
  if (!formatExplicit || formatFlag === 'md') {
647
815
  const groups = new Map<string, string>()
648
- const cmds = collectSkillCommands(scopedCommands, prefix, groups, scopedRoot)
816
+ const cmds = collectSkillCommands(scopedCommands, collectPrefix, groups, scopedRoot)
649
817
  const scopedName = prefix.length > 0 ? `${name} ${prefix.join(' ')}` : name
650
818
  writeln(Skill.index(scopedName, cmds, scopedDescription))
651
819
  return
652
820
  }
653
- writeln(Formatter.format(buildIndexManifest(scopedCommands, prefix), formatFlag))
821
+ writeln(
822
+ Formatter.format(
823
+ buildIndexManifest(scopedCommands, prefix, options.globals?.schema),
824
+ formatFlag,
825
+ ),
826
+ )
654
827
  return
655
828
  }
656
829
 
@@ -830,12 +1003,16 @@ async function serveImpl(
830
1003
  return
831
1004
  }
832
1005
 
833
- // mcp add: register CLI as MCP server via `npx add-mcp`
1006
+ // mcp add/doctor: register or smoke-test CLI MCP server integration.
834
1007
  const mcpIdx = builtinIdx(filtered, name, 'mcp')
835
1008
  if (mcpIdx !== -1) {
1009
+ const builtin = findBuiltin('mcp')!
836
1010
  const mcpSub = filtered[mcpIdx + 1]
837
- if (mcpSub && mcpSub !== 'add') {
838
- const suggestion = suggest(mcpSub, ['add'])
1011
+ const sub = mcpSub ? findBuiltinSubcommand(builtin, mcpSub) : undefined
1012
+ if (mcpSub && !sub) {
1013
+ const candidates =
1014
+ builtin.subcommands?.flatMap((sub) => [sub.name, ...(sub.aliases ?? [])]) ?? []
1015
+ const suggestion = suggest(mcpSub, candidates)
839
1016
  const didYouMean = suggestion ? ` Did you mean '${suggestion}'?` : ''
840
1017
  const message = `'${mcpSub}' is not a command for '${name} mcp'.${didYouMean}`
841
1018
  const ctaCommands: FormattedCta[] = []
@@ -856,13 +1033,17 @@ async function serveImpl(
856
1033
  return
857
1034
  }
858
1035
  if (!mcpSub) {
859
- const b = findBuiltin('mcp')!
860
- writeln(formatBuiltinHelp(name, b))
1036
+ writeln(formatBuiltinHelp(name, builtin))
861
1037
  return
862
1038
  }
863
1039
  if (help) {
864
- const b = findBuiltin('mcp')!
865
- writeln(formatBuiltinSubcommandHelp(name, b, 'add'))
1040
+ writeln(formatBuiltinSubcommandHelp(name, builtin, sub!.name))
1041
+ return
1042
+ }
1043
+ if (sub!.name === 'doctor') {
1044
+ const result = await runMcpDoctor(name, commands, options)
1045
+ writeln(Formatter.format(result, formatExplicit ? formatFlag : 'toon'))
1046
+ if (!result.ok) exit(1)
866
1047
  return
867
1048
  }
868
1049
  const rest = filtered.slice(mcpIdx + 2)
@@ -930,12 +1111,14 @@ async function serveImpl(
930
1111
  ) {
931
1112
  // Root command with args but none provided (human mode) — show help
932
1113
  const cmd = options.rootCommand
1114
+ await writeBanner()
933
1115
  writeln(
934
1116
  Help.formatCommand(name, {
935
1117
  alias: cmd.alias as Record<string, string> | undefined,
936
1118
  aliases: options.aliases,
937
1119
  configFlag,
938
1120
  description: cmd.description ?? options.description,
1121
+ globals: options.globals,
939
1122
  version: options.version,
940
1123
  args: cmd.args,
941
1124
  env: cmd.env,
@@ -953,11 +1136,13 @@ async function serveImpl(
953
1136
  if (options.rootCommand || options.rootFetch) {
954
1137
  // Root command/fetch with no args — treat as root invocation
955
1138
  } else {
1139
+ await writeBanner()
956
1140
  writeln(
957
1141
  Help.formatRoot(name, {
958
1142
  aliases: options.aliases,
959
1143
  configFlag,
960
1144
  description: options.description,
1145
+ globals: options.globals,
961
1146
  version: options.version,
962
1147
  commands: collectHelpCommands(commands),
963
1148
  root: true,
@@ -1018,6 +1203,7 @@ async function serveImpl(
1018
1203
  aliases: options.aliases,
1019
1204
  configFlag,
1020
1205
  description: cmd.description ?? options.description,
1206
+ globals: options.globals,
1021
1207
  version: options.version,
1022
1208
  args: cmd.args,
1023
1209
  env: cmd.env,
@@ -1036,6 +1222,7 @@ async function serveImpl(
1036
1222
  aliases: isRoot ? options.aliases : undefined,
1037
1223
  configFlag,
1038
1224
  description: helpDesc,
1225
+ globals: options.globals,
1039
1226
  version: isRoot ? options.version : undefined,
1040
1227
  commands: collectHelpCommands(helpCmds),
1041
1228
  root: isRoot,
@@ -1056,6 +1243,7 @@ async function serveImpl(
1056
1243
  aliases: isRootCmd ? options.aliases : cmd.aliases,
1057
1244
  configFlag,
1058
1245
  description: cmd.description,
1246
+ globals: options.globals,
1059
1247
  version: isRootCmd ? options.version : undefined,
1060
1248
  args: cmd.args,
1061
1249
  env: cmd.env,
@@ -1079,6 +1267,7 @@ async function serveImpl(
1079
1267
  Help.formatRoot(`${name} ${resolved.path}`, {
1080
1268
  configFlag,
1081
1269
  description: resolved.description,
1270
+ globals: options.globals,
1082
1271
  commands: collectHelpCommands(resolved.commands),
1083
1272
  }),
1084
1273
  )
@@ -1104,6 +1293,7 @@ async function serveImpl(
1104
1293
  if (cmd.env) result.env = Schema.toJsonSchema(cmd.env)
1105
1294
  if (cmd.options) result.options = Schema.toJsonSchema(cmd.options)
1106
1295
  if (cmd.output) result.output = Schema.toJsonSchema(cmd.output)
1296
+ if (options.globals?.schema) result.globals = Schema.toJsonSchema(options.globals.schema)
1107
1297
  writeln(Formatter.format(result, format))
1108
1298
  return
1109
1299
  }
@@ -1113,6 +1303,7 @@ async function serveImpl(
1113
1303
  Help.formatRoot(`${name} ${resolved.path}`, {
1114
1304
  configFlag,
1115
1305
  description: resolved.description,
1306
+ globals: options.globals,
1116
1307
  commands: collectHelpCommands(resolved.commands),
1117
1308
  }),
1118
1309
  )
@@ -1124,6 +1315,7 @@ async function serveImpl(
1124
1315
  // Resolve effective format: explicit --format/--json → command default → CLI default → toon
1125
1316
  const resolvedFormat = 'command' in resolved && (resolved as any).command.format
1126
1317
  const format = formatExplicit ? formatFlag : resolvedFormat || options.format || 'toon'
1318
+ if (format === 'yaml') await Yaml.load()
1127
1319
 
1128
1320
  // Fall back to root fetch/command when no subcommand matches,
1129
1321
  // but only if the token doesn't look like a typo of a known command.
@@ -1285,6 +1477,7 @@ async function serveImpl(
1285
1477
 
1286
1478
  // Fetch gateway execution path
1287
1479
  if ('fetchGateway' in effective) {
1480
+ if (!parseGlobalOptions(true)) return
1288
1481
  const { fetchGateway, path, rest: fetchRest } = effective
1289
1482
  const fetchMiddleware = [
1290
1483
  ...(options.middlewares ?? []),
@@ -1370,6 +1563,7 @@ async function serveImpl(
1370
1563
  error: errorFn,
1371
1564
  format,
1372
1565
  formatExplicit,
1566
+ globals,
1373
1567
  name,
1374
1568
  set(key: string, value: unknown) {
1375
1569
  varsMap[key] = value
@@ -1420,7 +1614,9 @@ async function serveImpl(
1420
1614
  return
1421
1615
  }
1422
1616
 
1423
- const { command, path, rest } = effective
1617
+ const { command, path, rest: commandRest } = effective
1618
+
1619
+ if (!parseGlobalOptions(true)) return
1424
1620
 
1425
1621
  // Collect middleware: root CLI + groups traversed + per-command
1426
1622
  const allMiddleware = [
@@ -1433,7 +1629,7 @@ async function serveImpl(
1433
1629
 
1434
1630
  if (human)
1435
1631
  emitDeprecationWarnings(
1436
- rest,
1632
+ commandRest,
1437
1633
  command.options,
1438
1634
  command.alias as Record<string, string> | undefined,
1439
1635
  )
@@ -1463,13 +1659,14 @@ async function serveImpl(
1463
1659
 
1464
1660
  const result = await Command.execute(command, {
1465
1661
  agent: !human,
1466
- argv: rest,
1662
+ argv: commandRest,
1467
1663
  defaults,
1468
1664
  displayName,
1469
1665
  env: options.envSchema,
1470
1666
  envSource: options.env,
1471
1667
  format,
1472
1668
  formatExplicit,
1669
+ globals,
1473
1670
  inputOptions: {},
1474
1671
  middlewares: allMiddleware,
1475
1672
  name,
@@ -1558,6 +1755,8 @@ declare namespace fetchImpl {
1558
1755
  description?: string | undefined
1559
1756
  /** CLI-level env schema. */
1560
1757
  envSchema?: z.ZodObject<any> | undefined
1758
+ /** Global options schema and alias map. */
1759
+ globals?: GlobalsDescriptor | undefined
1561
1760
  /** Group-level middleware collected during command resolution. */
1562
1761
  groupMiddlewares?: MiddlewareHandler[] | undefined
1563
1762
  mcpHandler?:
@@ -1582,7 +1781,11 @@ declare namespace fetchImpl {
1582
1781
  }
1583
1782
 
1584
1783
  /** @internal Creates a lazy MCP HTTP handler scoped to a CLI instance. */
1585
- function createMcpHttpHandler(name: string, version: string) {
1784
+ function createMcpHttpHandler(
1785
+ name: string,
1786
+ version: string,
1787
+ options: createMcpHttpHandler.Options = {},
1788
+ ) {
1586
1789
  let transport: any
1587
1790
 
1588
1791
  return async (
@@ -1594,8 +1797,12 @@ function createMcpHttpHandler(name: string, version: string) {
1594
1797
  vars?: z.ZodObject<any> | undefined
1595
1798
  },
1596
1799
  ): Promise<Response> => {
1800
+ const stateless = options.stateless ?? true
1801
+ if (stateless && req.method !== 'POST')
1802
+ return new Response(null, { status: 405, headers: { Allow: 'POST' } })
1803
+
1597
1804
  if (!transport) {
1598
- const { McpServer, WebStandardStreamableHTTPServerTransport } =
1805
+ const { fromJsonSchema, McpServer, WebStandardStreamableHTTPServerTransport } =
1599
1806
  await import('@modelcontextprotocol/server')
1600
1807
 
1601
1808
  const server = new McpServer({ name, version })
@@ -1612,6 +1819,9 @@ function createMcpHttpHandler(name: string, version: string) {
1612
1819
  {
1613
1820
  ...(tool.description ? { description: tool.description } : undefined),
1614
1821
  ...(hasInput ? { inputSchema: z.object(mergedShape) } : undefined),
1822
+ ...(tool.outputSchema
1823
+ ? { outputSchema: fromJsonSchema(tool.outputSchema) }
1824
+ : undefined),
1615
1825
  },
1616
1826
  async (...callArgs: any[]) => {
1617
1827
  const params = hasInput ? (callArgs[0] as Record<string, unknown>) : {}
@@ -1626,16 +1836,26 @@ function createMcpHttpHandler(name: string, version: string) {
1626
1836
  )
1627
1837
  }
1628
1838
 
1629
- transport = new WebStandardStreamableHTTPServerTransport({
1630
- sessionIdGenerator: () => crypto.randomUUID(),
1631
- enableJsonResponse: true,
1632
- })
1839
+ const transportOptions = stateless
1840
+ ? { enableJsonResponse: true }
1841
+ : {
1842
+ sessionIdGenerator: () => crypto.randomUUID(),
1843
+ enableJsonResponse: true,
1844
+ }
1845
+ transport = new WebStandardStreamableHTTPServerTransport(transportOptions)
1633
1846
  await server.connect(transport)
1634
1847
  }
1635
1848
  return transport.handleRequest(req)
1636
1849
  }
1637
1850
  }
1638
1851
 
1852
+ declare namespace createMcpHttpHandler {
1853
+ type Options = {
1854
+ /** Disable HTTP MCP session management. Defaults to `true`. */
1855
+ stateless?: boolean | undefined
1856
+ }
1857
+ }
1858
+
1639
1859
  function isOpenapiRoute(segments: string[]) {
1640
1860
  if (segments.length === 1)
1641
1861
  return (
@@ -1677,7 +1897,7 @@ async function fetchImpl(
1677
1897
  if (req.method === 'GET' && isOpenapiRoute(segments)) {
1678
1898
  const spec = generatedOpenapi(name, commands, options)
1679
1899
  const yaml = segments[0] === 'openapi.yml' || segments[0] === 'openapi.yaml'
1680
- return new Response(yaml ? yamlStringify(spec) : JSON.stringify(spec), {
1900
+ return new Response(yaml ? (await Yaml.load()).stringify(spec) : JSON.stringify(spec), {
1681
1901
  status: 200,
1682
1902
  headers: {
1683
1903
  'content-type': yaml ? 'application/yaml' : 'application/json',
@@ -1701,6 +1921,8 @@ async function fetchImpl(
1701
1921
  segments.length >= 3 &&
1702
1922
  req.method === 'GET'
1703
1923
  ) {
1924
+ // Pre-load yaml for the sync call paths below (`Skill.split`, frontmatter parsing).
1925
+ await Yaml.load()
1704
1926
  const groups = new Map<string, string>()
1705
1927
  const cmds = collectSkillCommands(commands, [], groups, options.rootCommand)
1706
1928
 
@@ -1709,7 +1931,7 @@ async function fetchImpl(
1709
1931
  const files = Skill.split(name, cmds, 1, groups)
1710
1932
  const skills = files.map((f) => {
1711
1933
  const fmMatch = f.content.match(/^---\n([\s\S]*?)\n---/)
1712
- const meta = fmMatch ? (yamlParse(fmMatch[1]!) as Record<string, string>) : {}
1934
+ const meta = fmMatch ? (Yaml.loadSync().parse(fmMatch[1]!) as Record<string, string>) : {}
1713
1935
  return {
1714
1936
  name: f.dir || name,
1715
1937
  description: meta.description ?? '',
@@ -1750,7 +1972,7 @@ async function fetchImpl(
1750
1972
  }
1751
1973
 
1752
1974
  function jsonResponse(body: unknown, status: number) {
1753
- return new Response(JSON.stringify(body), {
1975
+ return new Response(Json.stringify(body), {
1754
1976
  status,
1755
1977
  headers: { 'content-type': 'application/json' },
1756
1978
  })
@@ -1823,7 +2045,7 @@ async function executeCommand(
1823
2045
  options: fetchImpl.Options,
1824
2046
  ): Promise<Response> {
1825
2047
  function jsonResponse(body: unknown, status: number) {
1826
- return new Response(JSON.stringify(body), {
2048
+ return new Response(Json.stringify(body), {
1827
2049
  status,
1828
2050
  headers: { 'content-type': 'application/json' },
1829
2051
  })
@@ -1835,13 +2057,40 @@ async function executeCommand(
1835
2057
  ...((command.middleware as MiddlewareHandler[] | undefined) ?? []),
1836
2058
  ]
1837
2059
 
2060
+ let globals: Record<string, unknown> = {}
2061
+ let commandInputOptions = inputOptions
2062
+ if (options.globals) {
2063
+ const globalKeys = new Set(Object.keys(options.globals.schema.shape))
2064
+ const rawGlobals: Record<string, unknown> = {}
2065
+ commandInputOptions = {}
2066
+ for (const [key, value] of Object.entries(inputOptions)) {
2067
+ if (globalKeys.has(key)) rawGlobals[key] = value
2068
+ else commandInputOptions[key] = value
2069
+ }
2070
+ try {
2071
+ globals = options.globals.schema.parse(rawGlobals)
2072
+ } catch (error: any) {
2073
+ const issues: any[] = error?.issues ?? error?.error?.issues ?? []
2074
+ const message = issues.map((i: any) => i.message).join('; ') || 'Validation failed'
2075
+ return jsonResponse(
2076
+ {
2077
+ ok: false,
2078
+ error: { code: 'VALIDATION_ERROR', message },
2079
+ meta: { command: path, duration: `${Math.round(performance.now() - start)}ms` },
2080
+ },
2081
+ 400,
2082
+ )
2083
+ }
2084
+ }
2085
+
1838
2086
  const result = await Command.execute(command, {
1839
2087
  agent: true,
1840
2088
  argv: rest,
1841
2089
  env: options.envSchema,
1842
2090
  format: 'json',
1843
2091
  formatExplicit: true,
1844
- inputOptions,
2092
+ globals,
2093
+ inputOptions: commandInputOptions,
1845
2094
  middlewares: allMiddleware,
1846
2095
  name: options.name ?? path,
1847
2096
  parseMode: 'split',
@@ -1854,39 +2103,78 @@ async function executeCommand(
1854
2103
 
1855
2104
  // Streaming path — async generator → NDJSON response
1856
2105
  if ('stream' in result) {
2106
+ const iterator = result.stream
2107
+ const encoder = new TextEncoder()
2108
+ const meta = (cta?: FormattedCtaBlock | undefined) => ({
2109
+ command: path,
2110
+ duration: `${Math.round(performance.now() - start)}ms`,
2111
+ ...(cta ? { cta } : undefined),
2112
+ })
2113
+ const errorRecord = (err: ErrorResult) => ({
2114
+ type: 'error',
2115
+ ok: false,
2116
+ error: {
2117
+ code: err.code,
2118
+ message: err.message,
2119
+ ...(err.retryable !== undefined ? { retryable: err.retryable } : undefined),
2120
+ },
2121
+ meta: meta(formatCtaBlock(options.name ?? path, err.cta)),
2122
+ })
1857
2123
  const stream = new ReadableStream({
1858
- async start(controller) {
1859
- const encoder = new TextEncoder()
2124
+ async cancel() {
2125
+ await iterator.return(undefined)
2126
+ },
2127
+ async pull(controller) {
1860
2128
  try {
1861
- for await (const value of result.stream) {
2129
+ const { value, done } = await iterator.next()
2130
+ if (done) {
2131
+ if (isSentinel(value) && value[sentinel] === 'error') {
2132
+ controller.enqueue(encoder.encode(Json.stringify(errorRecord(value)) + '\n'))
2133
+ controller.close()
2134
+ return
2135
+ }
2136
+ const cta =
2137
+ isSentinel(value) && value[sentinel] === 'ok'
2138
+ ? formatCtaBlock(options.name ?? path, value.cta)
2139
+ : undefined
1862
2140
  controller.enqueue(
1863
- encoder.encode(JSON.stringify({ type: 'chunk', data: value }) + '\n'),
2141
+ encoder.encode(
2142
+ Json.stringify({
2143
+ type: 'done',
2144
+ ok: true,
2145
+ meta: meta(cta),
2146
+ }) + '\n',
2147
+ ),
1864
2148
  )
2149
+ controller.close()
2150
+ return
1865
2151
  }
1866
- controller.enqueue(
1867
- encoder.encode(
1868
- JSON.stringify({
1869
- type: 'done',
1870
- ok: true,
1871
- meta: { command: path },
1872
- }) + '\n',
1873
- ),
1874
- )
2152
+
2153
+ if (isSentinel(value) && value[sentinel] === 'error') {
2154
+ controller.enqueue(encoder.encode(Json.stringify(errorRecord(value)) + '\n'))
2155
+ await iterator.return(undefined)
2156
+ controller.close()
2157
+ return
2158
+ }
2159
+
2160
+ controller.enqueue(encoder.encode(Json.stringify({ type: 'chunk', data: value }) + '\n'))
1875
2161
  } catch (error) {
1876
2162
  controller.enqueue(
1877
2163
  encoder.encode(
1878
- JSON.stringify({
2164
+ Json.stringify({
1879
2165
  type: 'error',
1880
2166
  ok: false,
1881
2167
  error: {
1882
- code: 'UNKNOWN',
2168
+ code: error instanceof IncurError ? error.code : 'UNKNOWN',
1883
2169
  message: error instanceof Error ? error.message : String(error),
2170
+ ...(error instanceof IncurError ? { retryable: error.retryable } : undefined),
1884
2171
  },
2172
+ meta: meta(),
1885
2173
  }) + '\n',
1886
2174
  ),
1887
2175
  )
2176
+ controller.close()
1888
2177
  }
1889
- controller.close()
1890
2178
  },
1891
2179
  })
1892
2180
  return new Response(stream, {
@@ -1906,6 +2194,7 @@ async function executeCommand(
1906
2194
  ...(result.error.retryable !== undefined
1907
2195
  ? { retryable: result.error.retryable }
1908
2196
  : undefined),
2197
+ ...(result.error.fieldErrors ? { fieldErrors: result.error.fieldErrors } : undefined),
1909
2198
  },
1910
2199
  meta: {
1911
2200
  command: path,
@@ -2107,6 +2396,8 @@ declare namespace serveImpl {
2107
2396
  envSchema?: z.ZodObject<any> | undefined
2108
2397
  /** CLI-level default output format. */
2109
2398
  format?: Formatter.Format | undefined
2399
+ /** Global options schema and alias map. */
2400
+ globals?: GlobalsDescriptor | undefined
2110
2401
  /** Middleware handlers registered on the root CLI. */
2111
2402
  middlewares?: MiddlewareHandler[] | undefined
2112
2403
  /** CLI-level default output policy. */
@@ -2115,6 +2406,16 @@ declare namespace serveImpl {
2115
2406
  | {
2116
2407
  agents?: string[] | undefined
2117
2408
  command?: string | undefined
2409
+ instructions?: string | undefined
2410
+ stateless?: boolean | undefined
2411
+ }
2412
+ | undefined
2413
+ /** Banner config, called before root help. */
2414
+ banner?:
2415
+ | (() => string | undefined | Promise<string | undefined>)
2416
+ | {
2417
+ render: () => string | undefined | Promise<string | undefined>
2418
+ mode?: 'all' | 'human' | 'agent' | undefined
2118
2419
  }
2119
2420
  | undefined
2120
2421
  /** Root command handler, invoked when no subcommand matches. */
@@ -2421,6 +2722,136 @@ function formatBuiltinSubcommandHelp(
2421
2722
  })
2422
2723
  }
2423
2724
 
2725
+ type McpDoctorResult = {
2726
+ ok: boolean
2727
+ toolCount: number
2728
+ tools: { name: string; description?: string | undefined }[]
2729
+ warnings: string[]
2730
+ errors: { code: string; message: string }[]
2731
+ }
2732
+
2733
+ async function runMcpDoctor(
2734
+ name: string,
2735
+ commands: Map<string, CommandEntry>,
2736
+ options: serveImpl.Options,
2737
+ ): Promise<McpDoctorResult> {
2738
+ const warnings: string[] = []
2739
+ const errors: McpDoctorResult['errors'] = []
2740
+ const input = new PassThrough()
2741
+ const output = new PassThrough()
2742
+ const chunks: string[] = []
2743
+ output.on('data', (chunk) => chunks.push(chunk.toString()))
2744
+
2745
+ let serveError: unknown
2746
+ const done = Mcp.serve(name, options.version ?? '0.0.0', commands, {
2747
+ input,
2748
+ output,
2749
+ middlewares: options.middlewares,
2750
+ env: options.envSchema,
2751
+ vars: options.vars,
2752
+ version: options.version,
2753
+ ...(options.mcp?.instructions ? { instructions: options.mcp.instructions } : undefined),
2754
+ }).catch((error) => {
2755
+ serveError = error
2756
+ })
2757
+
2758
+ input.write(
2759
+ `${Json.stringify({
2760
+ jsonrpc: '2.0',
2761
+ id: 1,
2762
+ method: 'initialize',
2763
+ params: {
2764
+ protocolVersion: '2024-11-05',
2765
+ capabilities: {},
2766
+ clientInfo: { name: 'incur-doctor', version: '1.0.0' },
2767
+ },
2768
+ })}\n`,
2769
+ )
2770
+ input.write(`${Json.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} })}\n`)
2771
+ await new Promise((resolve) => setTimeout(resolve, 20))
2772
+ input.end()
2773
+ await done
2774
+
2775
+ if (serveError)
2776
+ return {
2777
+ ok: false,
2778
+ toolCount: 0,
2779
+ tools: [],
2780
+ warnings,
2781
+ errors: [{ code: 'MCP_SERVER_FAILED', message: errorMessage(serveError) }],
2782
+ }
2783
+
2784
+ let responses: Record<string, unknown>[]
2785
+ try {
2786
+ responses = parseMcpDoctorResponses(chunks)
2787
+ } catch (error) {
2788
+ return {
2789
+ ok: false,
2790
+ toolCount: 0,
2791
+ tools: [],
2792
+ warnings,
2793
+ errors: [{ code: 'MCP_RESPONSE_PARSE_FAILED', message: errorMessage(error) }],
2794
+ }
2795
+ }
2796
+
2797
+ const initialize = responses.find((response) => response.id === 1)
2798
+ if (!initialize)
2799
+ errors.push({ code: 'MCP_INITIALIZE_MISSING', message: 'Missing initialize response.' })
2800
+ else if (initialize.error)
2801
+ errors.push({ code: 'MCP_INITIALIZE_FAILED', message: mcpErrorMessage(initialize.error) })
2802
+
2803
+ const toolsList = responses.find((response) => response.id === 2)
2804
+ let tools: McpDoctorResult['tools'] = []
2805
+ if (!toolsList)
2806
+ errors.push({ code: 'MCP_TOOLS_LIST_MISSING', message: 'Missing tools/list response.' })
2807
+ else if (toolsList.error)
2808
+ errors.push({ code: 'MCP_TOOLS_LIST_FAILED', message: mcpErrorMessage(toolsList.error) })
2809
+ else if (!isRecord(toolsList.result) || !Array.isArray(toolsList.result.tools))
2810
+ errors.push({
2811
+ code: 'MCP_TOOLS_LIST_INVALID',
2812
+ message: 'tools/list did not return a tools array.',
2813
+ })
2814
+ else
2815
+ tools = toolsList.result.tools
2816
+ .filter(isRecord)
2817
+ .map((tool) => ({
2818
+ name: typeof tool.name === 'string' ? tool.name : '',
2819
+ ...(typeof tool.description === 'string' ? { description: tool.description } : undefined),
2820
+ }))
2821
+ .filter((tool) => tool.name)
2822
+
2823
+ if (errors.length === 0 && tools.length === 0) warnings.push('No MCP tools exposed.')
2824
+
2825
+ return {
2826
+ ok: errors.length === 0,
2827
+ toolCount: tools.length,
2828
+ tools,
2829
+ warnings,
2830
+ errors,
2831
+ }
2832
+ }
2833
+
2834
+ function parseMcpDoctorResponses(chunks: string[]): Record<string, unknown>[] {
2835
+ const responses: Record<string, unknown>[] = []
2836
+ for (const line of chunks.join('').split(/\r?\n/)) {
2837
+ const trimmed = line.trim()
2838
+ if (!trimmed) continue
2839
+ const parsed = JSON.parse(trimmed)
2840
+ if (!isRecord(parsed)) throw new Error('Expected JSON-RPC response object.')
2841
+ responses.push(parsed)
2842
+ }
2843
+ return responses
2844
+ }
2845
+
2846
+ function errorMessage(error: unknown): string {
2847
+ return error instanceof Error ? error.message : String(error)
2848
+ }
2849
+
2850
+ function mcpErrorMessage(error: unknown): string {
2851
+ if (isRecord(error) && typeof error.message === 'string') return error.message
2852
+ return Json.stringify(error)
2853
+ }
2854
+
2424
2855
  /** @internal Formats help text for a fetch gateway command. */
2425
2856
  function formatFetchHelp(name: string, description?: string): string {
2426
2857
  const lines: string[] = []
@@ -2528,6 +2959,41 @@ function resolveAlias(
2528
2959
  return entry
2529
2960
  }
2530
2961
 
2962
+ /** @internal Validates command options against CLI-level global options. */
2963
+ function assertNoGlobalOptionConflicts(
2964
+ path: string,
2965
+ entry: CommandEntry,
2966
+ globals: GlobalsDescriptor | undefined,
2967
+ ) {
2968
+ if (!globals || isFetchGateway(entry) || isAlias(entry)) return
2969
+ if (isGroup(entry)) {
2970
+ for (const [name, child] of entry.commands)
2971
+ assertNoGlobalOptionConflicts(`${path} ${name}`, child, globals)
2972
+ return
2973
+ }
2974
+
2975
+ if (entry.options) {
2976
+ const globalKeys = Object.keys(globals.schema.shape)
2977
+ const optionKeys = Object.keys(entry.options.shape)
2978
+ for (const key of optionKeys) {
2979
+ if (globalKeys.includes(key))
2980
+ throw new Error(
2981
+ `Command '${path}' option '${key}' conflicts with a global option. Choose a different name.`,
2982
+ )
2983
+ }
2984
+ }
2985
+
2986
+ if (globals.alias && entry.alias) {
2987
+ const globalAliasValues = new Set(Object.values(globals.alias))
2988
+ for (const [name, short] of Object.entries(entry.alias)) {
2989
+ if (short && globalAliasValues.has(short))
2990
+ throw new Error(
2991
+ `Command '${path}' alias '-${short}' for '${name}' conflicts with a global alias. Choose a different alias.`,
2992
+ )
2993
+ }
2994
+ }
2995
+ }
2996
+
2531
2997
  /** @internal Maps CLI instances to their command maps. */
2532
2998
  export const toCommands = new WeakMap<Cli, Map<string, CommandEntry>>()
2533
2999
 
@@ -2546,6 +3012,15 @@ export const toConfigEnabled = new WeakMap<Cli, boolean>()
2546
3012
  /** @internal Maps CLI instances to their output policy. */
2547
3013
  const toOutputPolicy = new WeakMap<Cli, OutputPolicy>()
2548
3014
 
3015
+ /** Descriptor for a CLI's custom global options schema and aliases. */
3016
+ export type GlobalsDescriptor = {
3017
+ schema: z.ZodObject<any>
3018
+ alias?: Record<string, string> | undefined
3019
+ }
3020
+
3021
+ /** @internal Maps CLI instances to their globals schema and alias map. */
3022
+ const toGlobals = new WeakMap<Cli, GlobalsDescriptor>()
3023
+
2549
3024
  /** @internal Maps root CLI instances to their command aliases. */
2550
3025
  const toRootAliases = new WeakMap<Root, string[]>()
2551
3026
 
@@ -2667,7 +3142,7 @@ async function handleStreaming(
2667
3142
  return
2668
3143
  }
2669
3144
  }
2670
- if (useJsonl) ctx.writeln(JSON.stringify({ type: 'chunk', data: value }))
3145
+ if (useJsonl) ctx.writeln(Json.stringify({ type: 'chunk', data: value }))
2671
3146
  else if (ctx.renderOutput)
2672
3147
  ctx.writeln(ctx.truncate(Formatter.format(value, ctx.format)).text)
2673
3148
  }
@@ -2719,6 +3194,7 @@ async function handleStreaming(
2719
3194
  error: {
2720
3195
  code: error instanceof IncurError ? error.code : 'UNKNOWN',
2721
3196
  message: error instanceof Error ? error.message : String(error),
3197
+ ...(error instanceof IncurError ? { retryable: error.retryable } : undefined),
2722
3198
  },
2723
3199
  }),
2724
3200
  )
@@ -2802,6 +3278,7 @@ async function handleStreaming(
2802
3278
  error: {
2803
3279
  code: error instanceof IncurError ? error.code : 'UNKNOWN',
2804
3280
  message: error instanceof Error ? error.message : String(error),
3281
+ ...(error instanceof IncurError ? { retryable: error.retryable } : undefined),
2805
3282
  },
2806
3283
  meta: {
2807
3284
  command: ctx.path,
@@ -2813,36 +3290,16 @@ async function handleStreaming(
2813
3290
  }
2814
3291
  }
2815
3292
 
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
3293
  /** @internal Builds the `--llms` index manifest (name + description only) from the command tree. */
2842
- function buildIndexManifest(commands: Map<string, CommandEntry>, prefix: string[] = []) {
3294
+ function buildIndexManifest(
3295
+ commands: Map<string, CommandEntry>,
3296
+ prefix: string[] = [],
3297
+ globalsSchema?: z.ZodObject<any>,
3298
+ ) {
2843
3299
  return {
2844
3300
  version: 'incur.v1',
2845
3301
  commands: collectIndexCommands(commands, prefix).sort((a, b) => a.name.localeCompare(b.name)),
3302
+ ...(globalsSchema ? { globals: Schema.toJsonSchema(globalsSchema) } : undefined),
2846
3303
  }
2847
3304
  }
2848
3305
 
@@ -2869,10 +3326,15 @@ function collectIndexCommands(
2869
3326
  }
2870
3327
 
2871
3328
  /** @internal Builds the `--llms` manifest from the command tree. */
2872
- function buildManifest(commands: Map<string, CommandEntry>, prefix: string[] = []) {
3329
+ function buildManifest(
3330
+ commands: Map<string, CommandEntry>,
3331
+ prefix: string[] = [],
3332
+ globalsSchema?: z.ZodObject<any>,
3333
+ ) {
2873
3334
  return {
2874
3335
  version: 'incur.v1',
2875
3336
  commands: collectCommands(commands, prefix).sort((a, b) => a.name.localeCompare(b.name)),
3337
+ ...(globalsSchema ? { globals: Schema.toJsonSchema(globalsSchema) } : undefined),
2876
3338
  }
2877
3339
  }
2878
3340
 
@@ -2925,11 +3387,11 @@ function collectCommands(
2925
3387
  }
2926
3388
 
2927
3389
  /** @internal Recursively collects leaf commands as `Skill.CommandInfo` for `--llms --format md`. */
2928
- function collectSkillCommands(
3390
+ export function collectSkillCommands(
2929
3391
  commands: Map<string, CommandEntry>,
2930
3392
  prefix: string[],
2931
3393
  groups: Map<string, string>,
2932
- rootCommand?: CommandDefinition<any, any, any> | undefined,
3394
+ rootCommand?: SkillCommandSource | undefined,
2933
3395
  ): Skill.CommandInfo[] {
2934
3396
  const result: Skill.CommandInfo[] = []
2935
3397
  if (rootCommand) {
@@ -2938,6 +3400,7 @@ function collectSkillCommands(
2938
3400
  if (rootCommand.args) cmd.args = rootCommand.args
2939
3401
  if (rootCommand.env) cmd.env = rootCommand.env
2940
3402
  if (rootCommand.hint) cmd.hint = rootCommand.hint
3403
+ if (isDestructive(rootCommand)) cmd.hint = appendDestructiveHint(cmd.hint)
2941
3404
  if (rootCommand.options) cmd.options = rootCommand.options
2942
3405
  if (rootCommand.output) cmd.output = rootCommand.output
2943
3406
  const examples = formatExamples(rootCommand.examples)
@@ -2961,6 +3424,7 @@ function collectSkillCommands(
2961
3424
  if (entry.args) cmd.args = entry.args
2962
3425
  if (entry.env) cmd.env = entry.env
2963
3426
  if (entry.hint) cmd.hint = entry.hint
3427
+ if (isDestructive(entry)) cmd.hint = appendDestructiveHint(cmd.hint)
2964
3428
  if (entry.options) cmd.options = entry.options
2965
3429
  if (entry.output) cmd.output = entry.output
2966
3430
  const examples = formatExamples(entry.examples)
@@ -2977,6 +3441,29 @@ function collectSkillCommands(
2977
3441
  return result.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''))
2978
3442
  }
2979
3443
 
3444
+ type SkillCommandSource = Pick<
3445
+ CommandDefinition<any, any, any, any, any, any>,
3446
+ | 'args'
3447
+ | 'description'
3448
+ | 'destructive'
3449
+ | 'env'
3450
+ | 'examples'
3451
+ | 'hint'
3452
+ | 'mcp'
3453
+ | 'options'
3454
+ | 'output'
3455
+ >
3456
+
3457
+ function isDestructive(command: SkillCommandSource): boolean {
3458
+ return command.destructive === true || command.mcp?.annotations?.destructiveHint === true
3459
+ }
3460
+
3461
+ function appendDestructiveHint(hint: string | undefined): string {
3462
+ if (!hint) return destructiveCommandHint
3463
+ if (hint.includes(destructiveCommandHint)) return hint
3464
+ return `${hint} ${destructiveCommandHint}`
3465
+ }
3466
+
2980
3467
  /** @internal Formats examples into `{ command, description }` objects. `command` is the args/options suffix only. */
2981
3468
  export function formatExamples(
2982
3469
  examples: Example<any, any>[] | undefined,
@@ -2993,6 +3480,18 @@ export function formatExamples(
2993
3480
  })
2994
3481
  }
2995
3482
 
3483
+ /** @internal Parses YAML frontmatter from generated skill Markdown. */
3484
+ export function parseSkillFrontmatter(content: string): {
3485
+ description?: string | undefined
3486
+ name?: string | undefined
3487
+ } {
3488
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
3489
+ if (!match) return {}
3490
+ const meta = Yaml.loadSync().parse(match[1]!)
3491
+ if (!meta || typeof meta !== 'object') return {}
3492
+ return meta as { description?: string | undefined; name?: string | undefined }
3493
+ }
3494
+
2996
3495
  /** @internal Builds separate args, env, and options JSON Schemas. */
2997
3496
  function buildInputSchema(
2998
3497
  args: z.ZodObject<any> | undefined,
@@ -3121,12 +3620,27 @@ type CommandDefinition<
3121
3620
  args?: args | undefined
3122
3621
  /** Zod schema for environment variables. Keys are the variable names (e.g. `NPM_TOKEN`). */
3123
3622
  env?: env | undefined
3623
+ /** Marks this command as destructive when generating agent skills. */
3624
+ destructive?: boolean | undefined
3124
3625
  /** Usage examples for this command. */
3125
3626
  examples?: Example<args, options>[] | undefined
3126
3627
  /** Default output format. Overridden by `--format` or `--json`. */
3127
3628
  format?: Formatter.Format | undefined
3128
3629
  /** Plain text hint displayed after examples and before global options. */
3129
3630
  hint?: string | undefined
3631
+ /** MCP-specific metadata exposed when this command is served as a tool. */
3632
+ mcp?:
3633
+ | {
3634
+ /** Override the command name exposed to MCP clients. */
3635
+ name?: string | undefined
3636
+ /** Override the command description exposed to MCP clients. */
3637
+ description?: string | undefined
3638
+ /** MCP tool annotations that describe tool behavior to clients. */
3639
+ annotations?: Mcp.ToolAnnotations | undefined
3640
+ /** Tool-specific instructions surfaced to MCP clients. */
3641
+ instructions?: string | undefined
3642
+ }
3643
+ | undefined
3130
3644
  /** Zod schema for the command's return value. */
3131
3645
  output?: output | undefined
3132
3646
  /**
@@ -3179,22 +3693,6 @@ type CommandDefinition<
3179
3693
  | AsyncGenerator<InferReturn<output>, unknown, unknown>
3180
3694
  }
3181
3695
 
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
3696
  /** @internal Scans argv for deprecated flags and writes warnings to stderr. */
3199
3697
  function emitDeprecationWarnings(
3200
3698
  argv: string[],