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/dist/Cli.js 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
  import * as Completions from './Completions.js';
8
7
  import { IncurError, ParseError, ValidationError } from './Errors.js';
@@ -12,8 +11,11 @@ import * as Formatter from './Formatter.js';
12
11
  import * as Help from './Help.js';
13
12
  import { builtinCommands, findBuiltin, findBuiltinSubcommand, shells, } from './internal/command.js';
14
13
  import * as Command from './internal/command.js';
14
+ import { formatCtaBlock } from './internal/cta.js';
15
15
  import { isRecord, suggest, toKebab } from './internal/helpers.js';
16
+ import * as Json from './internal/json.js';
16
17
  import { detectRunner } from './internal/pm.js';
18
+ import * as Yaml from './internal/yaml.js';
17
19
  import * as Mcp from './Mcp.js';
18
20
  import * as Openapi from './Openapi.js';
19
21
  import * as Parser from './Parser.js';
@@ -21,6 +23,7 @@ import * as Schema from './Schema.js';
21
23
  import * as Skill from './Skill.js';
22
24
  import * as SyncMcp from './SyncMcp.js';
23
25
  import * as SyncSkills from './SyncSkills.js';
26
+ const destructiveCommandHint = 'Confirm with the user before executing this destructive command.';
24
27
  export function create(nameOrDefinition, definition) {
25
28
  const name = typeof nameOrDefinition === 'string' ? nameOrDefinition : nameOrDefinition.name;
26
29
  const def = typeof nameOrDefinition === 'string' ? (definition ?? {}) : nameOrDefinition;
@@ -31,7 +34,9 @@ export function create(nameOrDefinition, definition) {
31
34
  const commands = new Map();
32
35
  const middlewares = [];
33
36
  const pending = [];
34
- const mcpHandler = createMcpHttpHandler(name, def.version ?? '0.0.0');
37
+ const mcpHandler = createMcpHttpHandler(name, def.version ?? '0.0.0', {
38
+ stateless: def.mcp?.stateless,
39
+ });
35
40
  if (def.openapi && rootFetch) {
36
41
  pending.push((async () => {
37
42
  const spec = await Openapi.resolve(def.openapi, { baseUrl: rootFetchBaseUrl });
@@ -61,12 +66,14 @@ export function create(nameOrDefinition, definition) {
61
66
  basePath: def.basePath,
62
67
  config: def.openapiConfig,
63
68
  });
64
- commands.set(nameOrCli, {
69
+ const entry = {
65
70
  _group: true,
66
71
  description: def.description,
67
72
  commands: generated,
68
73
  ...(def.outputPolicy ? { outputPolicy: def.outputPolicy } : undefined),
69
- });
74
+ };
75
+ assertNoGlobalOptionConflicts(nameOrCli, entry, toGlobals.get(cli));
76
+ commands.set(nameOrCli, entry);
70
77
  })());
71
78
  return cli;
72
79
  }
@@ -79,6 +86,7 @@ export function create(nameOrDefinition, definition) {
79
86
  });
80
87
  return cli;
81
88
  }
89
+ assertNoGlobalOptionConflicts(nameOrCli, def, toGlobals.get(cli));
82
90
  commands.set(nameOrCli, def);
83
91
  if (def.aliases)
84
92
  for (const a of def.aliases)
@@ -87,6 +95,7 @@ export function create(nameOrDefinition, definition) {
87
95
  }
88
96
  const mountedRootDef = toRootDefinition.get(nameOrCli);
89
97
  if (mountedRootDef) {
98
+ assertNoGlobalOptionConflicts(nameOrCli.name, mountedRootDef, toGlobals.get(cli));
90
99
  commands.set(nameOrCli.name, mountedRootDef);
91
100
  const rootAliases = toRootAliases.get(nameOrCli);
92
101
  if (rootAliases)
@@ -98,21 +107,25 @@ export function create(nameOrDefinition, definition) {
98
107
  const subCommands = toCommands.get(sub);
99
108
  const subOutputPolicy = toOutputPolicy.get(sub);
100
109
  const subMiddlewares = toMiddlewares.get(sub);
101
- commands.set(sub.name, {
110
+ const entry = {
102
111
  _group: true,
103
112
  description: sub.description,
104
113
  commands: subCommands,
105
114
  ...(subOutputPolicy ? { outputPolicy: subOutputPolicy } : undefined),
106
115
  ...(subMiddlewares?.length ? { middlewares: subMiddlewares } : undefined),
107
- });
116
+ };
117
+ assertNoGlobalOptionConflicts(sub.name, entry, toGlobals.get(cli));
118
+ commands.set(sub.name, entry);
108
119
  return cli;
109
120
  },
110
121
  async fetch(req) {
111
122
  if (pending.length > 0)
112
123
  await Promise.all(pending);
124
+ const globalsDesc = toGlobals.get(cli);
113
125
  return fetchImpl(name, commands, req, {
114
126
  description: def.description,
115
127
  envSchema: def.env,
128
+ globals: globalsDesc,
116
129
  mcpHandler,
117
130
  middlewares,
118
131
  name,
@@ -124,13 +137,16 @@ export function create(nameOrDefinition, definition) {
124
137
  async serve(argv = process.argv.slice(2), serveOptions = {}) {
125
138
  if (pending.length > 0)
126
139
  await Promise.all(pending);
140
+ const globalsDesc = toGlobals.get(cli);
127
141
  return serveImpl(name, commands, argv, {
128
142
  ...serveOptions,
129
143
  aliases: def.aliases,
144
+ banner: def.banner,
130
145
  config: def.config,
131
146
  description: def.description,
132
147
  envSchema: def.env,
133
148
  format: def.format,
149
+ globals: globalsDesc,
134
150
  mcp: def.mcp,
135
151
  middlewares,
136
152
  outputPolicy: def.outputPolicy,
@@ -156,6 +172,40 @@ export function create(nameOrDefinition, definition) {
156
172
  toConfigEnabled.set(cli, true);
157
173
  if (def.outputPolicy)
158
174
  toOutputPolicy.set(cli, def.outputPolicy);
175
+ if (def.globals) {
176
+ toGlobals.set(cli, { schema: def.globals, alias: def.globalAlias });
177
+ const builtinNames = [
178
+ 'verbose',
179
+ 'format',
180
+ 'json',
181
+ 'llms',
182
+ 'llmsFull',
183
+ 'mcp',
184
+ 'help',
185
+ 'version',
186
+ 'schema',
187
+ 'filterOutput',
188
+ 'tokenLimit',
189
+ 'tokenOffset',
190
+ 'tokenCount',
191
+ ...(def.config?.flag
192
+ ? [def.config.flag, `no${def.config.flag[0].toUpperCase()}${def.config.flag.slice(1)}`]
193
+ : []),
194
+ ];
195
+ const globalKeys = Object.keys(def.globals.shape);
196
+ for (const key of globalKeys) {
197
+ if (builtinNames.includes(key))
198
+ throw new Error(`Global option '${key}' conflicts with a built-in flag. Choose a different name.`);
199
+ }
200
+ // Check globalAlias values against reserved short aliases
201
+ const reservedShorts = new Set(['h']);
202
+ if (def.globalAlias) {
203
+ for (const [name, short] of Object.entries(def.globalAlias)) {
204
+ if (reservedShorts.has(short))
205
+ throw new Error(`Global alias '-${short}' for '${name}' conflicts with a built-in short flag. Choose a different alias.`);
206
+ }
207
+ }
208
+ }
159
209
  toMiddlewares.set(cli, middlewares);
160
210
  toCommands.set(cli, commands);
161
211
  return cli;
@@ -172,6 +222,22 @@ async function serveImpl(name, commands, argv, options = {}) {
172
222
  function writeln(s) {
173
223
  stdout(s.endsWith('\n') ? s : `${s}\n`);
174
224
  }
225
+ async function writeBanner() {
226
+ if (!options.banner || help)
227
+ return;
228
+ const banner = typeof options.banner === 'function'
229
+ ? { render: options.banner, mode: 'all' }
230
+ : options.banner;
231
+ const mode = banner.mode ?? 'all';
232
+ if (mode !== 'all' && mode !== (human ? 'human' : 'agent'))
233
+ return;
234
+ try {
235
+ const text = await banner.render();
236
+ if (text)
237
+ writeln(text);
238
+ }
239
+ catch { }
240
+ }
175
241
  let builtinFlags;
176
242
  try {
177
243
  builtinFlags = extractBuiltinFlags(argv, { configFlag });
@@ -185,7 +251,36 @@ async function serveImpl(name, commands, argv, options = {}) {
185
251
  exit(1);
186
252
  return;
187
253
  }
188
- const { fullOutput, format: formatFlag, formatExplicit, filterOutput, tokenLimit, tokenOffset, tokenCount, llms, llmsFull, mcp: mcpFlag, help, version, schema, configPath, configDisabled, rest: filtered, } = builtinFlags;
254
+ const { fullOutput, format: formatFlag, formatExplicit, filterOutput, tokenLimit, tokenOffset, tokenCount, llms, llmsFull, mcp: mcpFlag, help, version, schema, configPath, configDisabled, rest, } = builtinFlags;
255
+ let globals = {};
256
+ let filtered = rest;
257
+ function parseGlobalOptions(validate) {
258
+ if (!options.globals)
259
+ return true;
260
+ try {
261
+ const result = Parser.parseGlobals(rest, options.globals.schema, options.globals.alias, {
262
+ validate,
263
+ });
264
+ if (validate)
265
+ globals = result.parsed;
266
+ filtered = result.rest;
267
+ return true;
268
+ }
269
+ catch (error) {
270
+ const message = error instanceof Error ? error.message : String(error);
271
+ if (human)
272
+ writeln(formatHumanError({ code: 'UNKNOWN', message }));
273
+ else
274
+ writeln(Formatter.format({ code: 'UNKNOWN', message }, 'toon'));
275
+ exit(1);
276
+ return false;
277
+ }
278
+ }
279
+ if (!parseGlobalOptions(false))
280
+ return;
281
+ // Pre-load yaml for the sync formatting paths below (yaml is loaded lazily -- see internal/yaml.ts).
282
+ if (formatFlag === 'yaml')
283
+ await Yaml.load();
189
284
  // --mcp: start as MCP stdio server
190
285
  if (mcpFlag) {
191
286
  await Mcp.serve(name, options.version ?? '0.0.0', commands, {
@@ -193,6 +288,7 @@ async function serveImpl(name, commands, argv, options = {}) {
193
288
  env: options.envSchema,
194
289
  vars: options.vars,
195
290
  version: options.version,
291
+ ...(options.mcp?.instructions ? { instructions: options.mcp.instructions } : undefined),
196
292
  });
197
293
  return;
198
294
  }
@@ -209,7 +305,9 @@ async function serveImpl(name, commands, argv, options = {}) {
209
305
  }
210
306
  else {
211
307
  const index = Number(process.env._COMPLETE_INDEX ?? words.length - 1);
212
- const candidates = Completions.complete(commands, options.rootCommand, words, index);
308
+ const candidates = Completions.complete(commands, options.rootCommand, words, index, options.globals
309
+ ? { schema: options.globals.schema, alias: options.globals.alias }
310
+ : undefined);
213
311
  // Add built-in commands (completions, mcp, skills) to completions
214
312
  const current = words[index] ?? '';
215
313
  const nonFlags = words.slice(0, index).filter((w) => !w.startsWith('-'));
@@ -282,25 +380,29 @@ async function serveImpl(name, commands, argv, options = {}) {
282
380
  }
283
381
  }
284
382
  const scopedRoot = prefix.length === 0 ? options.rootCommand : undefined;
383
+ // Markdown skill output renders scopedName separately. Passing prefix again
384
+ // to those collect helpers would double the group segment in command names
385
+ // (e.g. "cli auth auth login" instead of "cli auth login").
386
+ const collectPrefix = prefix.length > 0 ? [] : prefix;
285
387
  if (llmsFull) {
286
388
  if (!formatExplicit || formatFlag === 'md') {
287
389
  const groups = new Map();
288
- const cmds = collectSkillCommands(scopedCommands, prefix, groups, scopedRoot);
390
+ const cmds = collectSkillCommands(scopedCommands, collectPrefix, groups, scopedRoot);
289
391
  const scopedName = prefix.length > 0 ? `${name} ${prefix.join(' ')}` : name;
290
392
  writeln(Skill.generate(scopedName, cmds, groups));
291
393
  return;
292
394
  }
293
- writeln(Formatter.format(buildManifest(scopedCommands, prefix), formatFlag));
395
+ writeln(Formatter.format(buildManifest(scopedCommands, prefix, options.globals?.schema), formatFlag));
294
396
  return;
295
397
  }
296
398
  if (!formatExplicit || formatFlag === 'md') {
297
399
  const groups = new Map();
298
- const cmds = collectSkillCommands(scopedCommands, prefix, groups, scopedRoot);
400
+ const cmds = collectSkillCommands(scopedCommands, collectPrefix, groups, scopedRoot);
299
401
  const scopedName = prefix.length > 0 ? `${name} ${prefix.join(' ')}` : name;
300
402
  writeln(Skill.index(scopedName, cmds, scopedDescription));
301
403
  return;
302
404
  }
303
- writeln(Formatter.format(buildIndexManifest(scopedCommands, prefix), formatFlag));
405
+ writeln(Formatter.format(buildIndexManifest(scopedCommands, prefix, options.globals?.schema), formatFlag));
304
406
  return;
305
407
  }
306
408
  // completions <shell>: print shell hook script to stdout
@@ -556,11 +658,13 @@ async function serveImpl(name, commands, argv, options = {}) {
556
658
  hasRequiredArgs(options.rootCommand.args)) {
557
659
  // Root command with args but none provided (human mode) — show help
558
660
  const cmd = options.rootCommand;
661
+ await writeBanner();
559
662
  writeln(Help.formatCommand(name, {
560
663
  alias: cmd.alias,
561
664
  aliases: options.aliases,
562
665
  configFlag,
563
666
  description: cmd.description ?? options.description,
667
+ globals: options.globals,
564
668
  version: options.version,
565
669
  args: cmd.args,
566
670
  env: cmd.env,
@@ -578,10 +682,12 @@ async function serveImpl(name, commands, argv, options = {}) {
578
682
  // Root command/fetch with no args — treat as root invocation
579
683
  }
580
684
  else {
685
+ await writeBanner();
581
686
  writeln(Help.formatRoot(name, {
582
687
  aliases: options.aliases,
583
688
  configFlag,
584
689
  description: options.description,
690
+ globals: options.globals,
585
691
  version: options.version,
586
692
  commands: collectHelpCommands(commands),
587
693
  root: true,
@@ -635,6 +741,7 @@ async function serveImpl(name, commands, argv, options = {}) {
635
741
  aliases: options.aliases,
636
742
  configFlag,
637
743
  description: cmd.description ?? options.description,
744
+ globals: options.globals,
638
745
  version: options.version,
639
746
  args: cmd.args,
640
747
  env: cmd.env,
@@ -652,6 +759,7 @@ async function serveImpl(name, commands, argv, options = {}) {
652
759
  aliases: isRoot ? options.aliases : undefined,
653
760
  configFlag,
654
761
  description: helpDesc,
762
+ globals: options.globals,
655
763
  version: isRoot ? options.version : undefined,
656
764
  commands: collectHelpCommands(helpCmds),
657
765
  root: isRoot,
@@ -670,6 +778,7 @@ async function serveImpl(name, commands, argv, options = {}) {
670
778
  aliases: isRootCmd ? options.aliases : cmd.aliases,
671
779
  configFlag,
672
780
  description: cmd.description,
781
+ globals: options.globals,
673
782
  version: isRootCmd ? options.version : undefined,
674
783
  args: cmd.args,
675
784
  env: cmd.env,
@@ -690,6 +799,7 @@ async function serveImpl(name, commands, argv, options = {}) {
690
799
  writeln(Help.formatRoot(`${name} ${resolved.path}`, {
691
800
  configFlag,
692
801
  description: resolved.description,
802
+ globals: options.globals,
693
803
  commands: collectHelpCommands(resolved.commands),
694
804
  }));
695
805
  return;
@@ -718,6 +828,8 @@ async function serveImpl(name, commands, argv, options = {}) {
718
828
  result.options = Schema.toJsonSchema(cmd.options);
719
829
  if (cmd.output)
720
830
  result.output = Schema.toJsonSchema(cmd.output);
831
+ if (options.globals?.schema)
832
+ result.globals = Schema.toJsonSchema(options.globals.schema);
721
833
  writeln(Formatter.format(result, format));
722
834
  return;
723
835
  }
@@ -725,6 +837,7 @@ async function serveImpl(name, commands, argv, options = {}) {
725
837
  writeln(Help.formatRoot(`${name} ${resolved.path}`, {
726
838
  configFlag,
727
839
  description: resolved.description,
840
+ globals: options.globals,
728
841
  commands: collectHelpCommands(resolved.commands),
729
842
  }));
730
843
  return;
@@ -733,6 +846,8 @@ async function serveImpl(name, commands, argv, options = {}) {
733
846
  // Resolve effective format: explicit --format/--json → command default → CLI default → toon
734
847
  const resolvedFormat = 'command' in resolved && resolved.command.format;
735
848
  const format = formatExplicit ? formatFlag : resolvedFormat || options.format || 'toon';
849
+ if (format === 'yaml')
850
+ await Yaml.load();
736
851
  // Fall back to root fetch/command when no subcommand matches,
737
852
  // but only if the token doesn't look like a typo of a known command.
738
853
  const rootFallbackBlocked = 'error' in resolved &&
@@ -888,6 +1003,8 @@ async function serveImpl(name, commands, argv, options = {}) {
888
1003
  }
889
1004
  // Fetch gateway execution path
890
1005
  if ('fetchGateway' in effective) {
1006
+ if (!parseGlobalOptions(true))
1007
+ return;
891
1008
  const { fetchGateway, path, rest: fetchRest } = effective;
892
1009
  const fetchMiddleware = [
893
1010
  ...(options.middlewares ?? []),
@@ -963,6 +1080,7 @@ async function serveImpl(name, commands, argv, options = {}) {
963
1080
  error: errorFn,
964
1081
  format,
965
1082
  formatExplicit,
1083
+ globals,
966
1084
  name,
967
1085
  set(key, value) {
968
1086
  varsMap[key] = value;
@@ -1012,7 +1130,9 @@ async function serveImpl(name, commands, argv, options = {}) {
1012
1130
  }
1013
1131
  return;
1014
1132
  }
1015
- const { command, path, rest } = effective;
1133
+ const { command, path, rest: commandRest } = effective;
1134
+ if (!parseGlobalOptions(true))
1135
+ return;
1016
1136
  // Collect middleware: root CLI + groups traversed + per-command
1017
1137
  const allMiddleware = [
1018
1138
  ...(options.middlewares ?? []),
@@ -1022,7 +1142,7 @@ async function serveImpl(name, commands, argv, options = {}) {
1022
1142
  ...(command.middleware ?? []),
1023
1143
  ];
1024
1144
  if (human)
1025
- emitDeprecationWarnings(rest, command.options, command.alias);
1145
+ emitDeprecationWarnings(commandRest, command.options, command.alias);
1026
1146
  let defaults;
1027
1147
  if (configEnabled) {
1028
1148
  try {
@@ -1048,13 +1168,14 @@ async function serveImpl(name, commands, argv, options = {}) {
1048
1168
  }
1049
1169
  const result = await Command.execute(command, {
1050
1170
  agent: !human,
1051
- argv: rest,
1171
+ argv: commandRest,
1052
1172
  defaults,
1053
1173
  displayName,
1054
1174
  env: options.envSchema,
1055
1175
  envSource: options.env,
1056
1176
  format,
1057
1177
  formatExplicit,
1178
+ globals,
1058
1179
  inputOptions: {},
1059
1180
  middlewares: allMiddleware,
1060
1181
  name,
@@ -1123,11 +1244,14 @@ async function serveImpl(name, commands, argv, options = {}) {
1123
1244
  }
1124
1245
  }
1125
1246
  /** @internal Creates a lazy MCP HTTP handler scoped to a CLI instance. */
1126
- function createMcpHttpHandler(name, version) {
1247
+ function createMcpHttpHandler(name, version, options = {}) {
1127
1248
  let transport;
1128
1249
  return async (req, commands, mcpOptions) => {
1250
+ const stateless = options.stateless ?? true;
1251
+ if (stateless && req.method !== 'POST')
1252
+ return new Response(null, { status: 405, headers: { Allow: 'POST' } });
1129
1253
  if (!transport) {
1130
- const { McpServer, WebStandardStreamableHTTPServerTransport } = await import('@modelcontextprotocol/server');
1254
+ const { fromJsonSchema, McpServer, WebStandardStreamableHTTPServerTransport } = await import('@modelcontextprotocol/server');
1131
1255
  const server = new McpServer({ name, version });
1132
1256
  for (const tool of Mcp.collectTools(commands, [])) {
1133
1257
  const mergedShape = {
@@ -1138,6 +1262,9 @@ function createMcpHttpHandler(name, version) {
1138
1262
  server.registerTool(tool.name, {
1139
1263
  ...(tool.description ? { description: tool.description } : undefined),
1140
1264
  ...(hasInput ? { inputSchema: z.object(mergedShape) } : undefined),
1265
+ ...(tool.outputSchema
1266
+ ? { outputSchema: fromJsonSchema(tool.outputSchema) }
1267
+ : undefined),
1141
1268
  }, async (...callArgs) => {
1142
1269
  const params = hasInput ? callArgs[0] : {};
1143
1270
  return Mcp.callTool(tool, params, {
@@ -1149,10 +1276,13 @@ function createMcpHttpHandler(name, version) {
1149
1276
  });
1150
1277
  });
1151
1278
  }
1152
- transport = new WebStandardStreamableHTTPServerTransport({
1153
- sessionIdGenerator: () => crypto.randomUUID(),
1154
- enableJsonResponse: true,
1155
- });
1279
+ const transportOptions = stateless
1280
+ ? { enableJsonResponse: true }
1281
+ : {
1282
+ sessionIdGenerator: () => crypto.randomUUID(),
1283
+ enableJsonResponse: true,
1284
+ };
1285
+ transport = new WebStandardStreamableHTTPServerTransport(transportOptions);
1156
1286
  await server.connect(transport);
1157
1287
  }
1158
1288
  return transport.handleRequest(req);
@@ -1185,7 +1315,7 @@ async function fetchImpl(name, commands, req, options = {}) {
1185
1315
  if (req.method === 'GET' && isOpenapiRoute(segments)) {
1186
1316
  const spec = generatedOpenapi(name, commands, options);
1187
1317
  const yaml = segments[0] === 'openapi.yml' || segments[0] === 'openapi.yaml';
1188
- return new Response(yaml ? yamlStringify(spec) : JSON.stringify(spec), {
1318
+ return new Response(yaml ? (await Yaml.load()).stringify(spec) : JSON.stringify(spec), {
1189
1319
  status: 200,
1190
1320
  headers: {
1191
1321
  'content-type': yaml ? 'application/yaml' : 'application/json',
@@ -1205,6 +1335,8 @@ async function fetchImpl(name, commands, req, options = {}) {
1205
1335
  segments[1] === 'skills' &&
1206
1336
  segments.length >= 3 &&
1207
1337
  req.method === 'GET') {
1338
+ // Pre-load yaml for the sync call paths below (`Skill.split`, frontmatter parsing).
1339
+ await Yaml.load();
1208
1340
  const groups = new Map();
1209
1341
  const cmds = collectSkillCommands(commands, [], groups, options.rootCommand);
1210
1342
  // GET /.well-known/skills/index.json
@@ -1212,7 +1344,7 @@ async function fetchImpl(name, commands, req, options = {}) {
1212
1344
  const files = Skill.split(name, cmds, 1, groups);
1213
1345
  const skills = files.map((f) => {
1214
1346
  const fmMatch = f.content.match(/^---\n([\s\S]*?)\n---/);
1215
- const meta = fmMatch ? yamlParse(fmMatch[1]) : {};
1347
+ const meta = fmMatch ? Yaml.loadSync().parse(fmMatch[1]) : {};
1216
1348
  return {
1217
1349
  name: f.dir || name,
1218
1350
  description: meta.description ?? '',
@@ -1252,7 +1384,7 @@ async function fetchImpl(name, commands, req, options = {}) {
1252
1384
  catch { }
1253
1385
  }
1254
1386
  function jsonResponse(body, status) {
1255
- return new Response(JSON.stringify(body), {
1387
+ return new Response(Json.stringify(body), {
1256
1388
  status,
1257
1389
  headers: { 'content-type': 'application/json' },
1258
1390
  });
@@ -1303,7 +1435,7 @@ async function fetchImpl(name, commands, req, options = {}) {
1303
1435
  /** @internal Executes a resolved command for the fetch handler and returns a JSON Response. */
1304
1436
  async function executeCommand(path, command, rest, inputOptions, start, options) {
1305
1437
  function jsonResponse(body, status) {
1306
- return new Response(JSON.stringify(body), {
1438
+ return new Response(Json.stringify(body), {
1307
1439
  status,
1308
1440
  headers: { 'content-type': 'application/json' },
1309
1441
  });
@@ -1313,13 +1445,39 @@ async function executeCommand(path, command, rest, inputOptions, start, options)
1313
1445
  ...(options.groupMiddlewares ?? []),
1314
1446
  ...(command.middleware ?? []),
1315
1447
  ];
1448
+ let globals = {};
1449
+ let commandInputOptions = inputOptions;
1450
+ if (options.globals) {
1451
+ const globalKeys = new Set(Object.keys(options.globals.schema.shape));
1452
+ const rawGlobals = {};
1453
+ commandInputOptions = {};
1454
+ for (const [key, value] of Object.entries(inputOptions)) {
1455
+ if (globalKeys.has(key))
1456
+ rawGlobals[key] = value;
1457
+ else
1458
+ commandInputOptions[key] = value;
1459
+ }
1460
+ try {
1461
+ globals = options.globals.schema.parse(rawGlobals);
1462
+ }
1463
+ catch (error) {
1464
+ const issues = error?.issues ?? error?.error?.issues ?? [];
1465
+ const message = issues.map((i) => i.message).join('; ') || 'Validation failed';
1466
+ return jsonResponse({
1467
+ ok: false,
1468
+ error: { code: 'VALIDATION_ERROR', message },
1469
+ meta: { command: path, duration: `${Math.round(performance.now() - start)}ms` },
1470
+ }, 400);
1471
+ }
1472
+ }
1316
1473
  const result = await Command.execute(command, {
1317
1474
  agent: true,
1318
1475
  argv: rest,
1319
1476
  env: options.envSchema,
1320
1477
  format: 'json',
1321
1478
  formatExplicit: true,
1322
- inputOptions,
1479
+ globals,
1480
+ inputOptions: commandInputOptions,
1323
1481
  middlewares: allMiddleware,
1324
1482
  name: options.name ?? path,
1325
1483
  parseMode: 'split',
@@ -1330,30 +1488,68 @@ async function executeCommand(path, command, rest, inputOptions, start, options)
1330
1488
  const duration = `${Math.round(performance.now() - start)}ms`;
1331
1489
  // Streaming path — async generator → NDJSON response
1332
1490
  if ('stream' in result) {
1491
+ const iterator = result.stream;
1492
+ const encoder = new TextEncoder();
1493
+ const meta = (cta) => ({
1494
+ command: path,
1495
+ duration: `${Math.round(performance.now() - start)}ms`,
1496
+ ...(cta ? { cta } : undefined),
1497
+ });
1498
+ const errorRecord = (err) => ({
1499
+ type: 'error',
1500
+ ok: false,
1501
+ error: {
1502
+ code: err.code,
1503
+ message: err.message,
1504
+ ...(err.retryable !== undefined ? { retryable: err.retryable } : undefined),
1505
+ },
1506
+ meta: meta(formatCtaBlock(options.name ?? path, err.cta)),
1507
+ });
1333
1508
  const stream = new ReadableStream({
1334
- async start(controller) {
1335
- const encoder = new TextEncoder();
1509
+ async cancel() {
1510
+ await iterator.return(undefined);
1511
+ },
1512
+ async pull(controller) {
1336
1513
  try {
1337
- for await (const value of result.stream) {
1338
- controller.enqueue(encoder.encode(JSON.stringify({ type: 'chunk', data: value }) + '\n'));
1514
+ const { value, done } = await iterator.next();
1515
+ if (done) {
1516
+ if (isSentinel(value) && value[sentinel] === 'error') {
1517
+ controller.enqueue(encoder.encode(Json.stringify(errorRecord(value)) + '\n'));
1518
+ controller.close();
1519
+ return;
1520
+ }
1521
+ const cta = isSentinel(value) && value[sentinel] === 'ok'
1522
+ ? formatCtaBlock(options.name ?? path, value.cta)
1523
+ : undefined;
1524
+ controller.enqueue(encoder.encode(Json.stringify({
1525
+ type: 'done',
1526
+ ok: true,
1527
+ meta: meta(cta),
1528
+ }) + '\n'));
1529
+ controller.close();
1530
+ return;
1339
1531
  }
1340
- controller.enqueue(encoder.encode(JSON.stringify({
1341
- type: 'done',
1342
- ok: true,
1343
- meta: { command: path },
1344
- }) + '\n'));
1532
+ if (isSentinel(value) && value[sentinel] === 'error') {
1533
+ controller.enqueue(encoder.encode(Json.stringify(errorRecord(value)) + '\n'));
1534
+ await iterator.return(undefined);
1535
+ controller.close();
1536
+ return;
1537
+ }
1538
+ controller.enqueue(encoder.encode(Json.stringify({ type: 'chunk', data: value }) + '\n'));
1345
1539
  }
1346
1540
  catch (error) {
1347
- controller.enqueue(encoder.encode(JSON.stringify({
1541
+ controller.enqueue(encoder.encode(Json.stringify({
1348
1542
  type: 'error',
1349
1543
  ok: false,
1350
1544
  error: {
1351
- code: 'UNKNOWN',
1545
+ code: error instanceof IncurError ? error.code : 'UNKNOWN',
1352
1546
  message: error instanceof Error ? error.message : String(error),
1547
+ ...(error instanceof IncurError ? { retryable: error.retryable } : undefined),
1353
1548
  },
1549
+ meta: meta(),
1354
1550
  }) + '\n'));
1551
+ controller.close();
1355
1552
  }
1356
- controller.close();
1357
1553
  },
1358
1554
  });
1359
1555
  return new Response(stream, {
@@ -1371,6 +1567,7 @@ async function executeCommand(path, command, rest, inputOptions, start, options)
1371
1567
  ...(result.error.retryable !== undefined
1372
1568
  ? { retryable: result.error.retryable }
1373
1569
  : undefined),
1570
+ ...(result.error.fieldErrors ? { fieldErrors: result.error.fieldErrors } : undefined),
1374
1571
  },
1375
1572
  meta: {
1376
1573
  command: path,
@@ -1819,6 +2016,31 @@ function resolveAlias(commands, entry) {
1819
2016
  return commands.get(entry.target);
1820
2017
  return entry;
1821
2018
  }
2019
+ /** @internal Validates command options against CLI-level global options. */
2020
+ function assertNoGlobalOptionConflicts(path, entry, globals) {
2021
+ if (!globals || isFetchGateway(entry) || isAlias(entry))
2022
+ return;
2023
+ if (isGroup(entry)) {
2024
+ for (const [name, child] of entry.commands)
2025
+ assertNoGlobalOptionConflicts(`${path} ${name}`, child, globals);
2026
+ return;
2027
+ }
2028
+ if (entry.options) {
2029
+ const globalKeys = Object.keys(globals.schema.shape);
2030
+ const optionKeys = Object.keys(entry.options.shape);
2031
+ for (const key of optionKeys) {
2032
+ if (globalKeys.includes(key))
2033
+ throw new Error(`Command '${path}' option '${key}' conflicts with a global option. Choose a different name.`);
2034
+ }
2035
+ }
2036
+ if (globals.alias && entry.alias) {
2037
+ const globalAliasValues = new Set(Object.values(globals.alias));
2038
+ for (const [name, short] of Object.entries(entry.alias)) {
2039
+ if (short && globalAliasValues.has(short))
2040
+ throw new Error(`Command '${path}' alias '-${short}' for '${name}' conflicts with a global alias. Choose a different alias.`);
2041
+ }
2042
+ }
2043
+ }
1822
2044
  /** @internal Maps CLI instances to their command maps. */
1823
2045
  export const toCommands = new WeakMap();
1824
2046
  /** @internal Maps CLI instances to their middleware arrays. */
@@ -1831,6 +2053,8 @@ export const toRootOptions = new WeakMap();
1831
2053
  export const toConfigEnabled = new WeakMap();
1832
2054
  /** @internal Maps CLI instances to their output policy. */
1833
2055
  const toOutputPolicy = new WeakMap();
2056
+ /** @internal Maps CLI instances to their globals schema and alias map. */
2057
+ const toGlobals = new WeakMap();
1834
2058
  /** @internal Maps root CLI instances to their command aliases. */
1835
2059
  const toRootAliases = new WeakMap();
1836
2060
  /** @internal Sentinel symbol for `ok()` and `error()` return values. */
@@ -1901,7 +2125,7 @@ async function handleStreaming(generator, ctx) {
1901
2125
  }
1902
2126
  }
1903
2127
  if (useJsonl)
1904
- ctx.writeln(JSON.stringify({ type: 'chunk', data: value }));
2128
+ ctx.writeln(Json.stringify({ type: 'chunk', data: value }));
1905
2129
  else if (ctx.renderOutput)
1906
2130
  ctx.writeln(ctx.truncate(Formatter.format(value, ctx.format)).text);
1907
2131
  }
@@ -1947,6 +2171,7 @@ async function handleStreaming(generator, ctx) {
1947
2171
  error: {
1948
2172
  code: error instanceof IncurError ? error.code : 'UNKNOWN',
1949
2173
  message: error instanceof Error ? error.message : String(error),
2174
+ ...(error instanceof IncurError ? { retryable: error.retryable } : undefined),
1950
2175
  },
1951
2176
  }));
1952
2177
  else
@@ -2025,6 +2250,7 @@ async function handleStreaming(generator, ctx) {
2025
2250
  error: {
2026
2251
  code: error instanceof IncurError ? error.code : 'UNKNOWN',
2027
2252
  message: error instanceof Error ? error.message : String(error),
2253
+ ...(error instanceof IncurError ? { retryable: error.retryable } : undefined),
2028
2254
  },
2029
2255
  meta: {
2030
2256
  command: ctx.path,
@@ -2035,35 +2261,12 @@ async function handleStreaming(generator, ctx) {
2035
2261
  }
2036
2262
  }
2037
2263
  }
2038
- /** @internal Formats a CTA block into the output envelope shape. */
2039
- function formatCtaBlock(name, block) {
2040
- if (!block || block.commands.length === 0)
2041
- return undefined;
2042
- return {
2043
- description: block.description ??
2044
- (block.commands.length === 1 ? 'Suggested command:' : 'Suggested commands:'),
2045
- commands: block.commands.map((c) => formatCta(name, c)),
2046
- };
2047
- }
2048
- /** @internal Formats a CTA by prefixing the CLI name. Handles string and object forms. */
2049
- function formatCta(name, cta) {
2050
- if (typeof cta === 'string')
2051
- return { command: `${name} ${cta}` };
2052
- const prefix = cta.command === name || cta.command.startsWith(`${name} `) ? '' : `${name} `;
2053
- let cmd = `${prefix}${cta.command}`;
2054
- if (cta.args)
2055
- for (const [key, value] of Object.entries(cta.args))
2056
- cmd += value === true ? ` <${key}>` : ` ${value}`;
2057
- if (cta.options)
2058
- for (const [key, value] of Object.entries(cta.options))
2059
- cmd += value === true ? ` --${key} <${key}>` : ` --${key} ${value}`;
2060
- return { command: cmd, ...(cta.description ? { description: cta.description } : undefined) };
2061
- }
2062
2264
  /** @internal Builds the `--llms` index manifest (name + description only) from the command tree. */
2063
- function buildIndexManifest(commands, prefix = []) {
2265
+ function buildIndexManifest(commands, prefix = [], globalsSchema) {
2064
2266
  return {
2065
2267
  version: 'incur.v1',
2066
2268
  commands: collectIndexCommands(commands, prefix).sort((a, b) => a.name.localeCompare(b.name)),
2269
+ ...(globalsSchema ? { globals: Schema.toJsonSchema(globalsSchema) } : undefined),
2067
2270
  };
2068
2271
  }
2069
2272
  /** @internal Recursively collects leaf commands with name + description only. */
@@ -2090,10 +2293,11 @@ function collectIndexCommands(commands, prefix) {
2090
2293
  return result;
2091
2294
  }
2092
2295
  /** @internal Builds the `--llms` manifest from the command tree. */
2093
- function buildManifest(commands, prefix = []) {
2296
+ function buildManifest(commands, prefix = [], globalsSchema) {
2094
2297
  return {
2095
2298
  version: 'incur.v1',
2096
2299
  commands: collectCommands(commands, prefix).sort((a, b) => a.name.localeCompare(b.name)),
2300
+ ...(globalsSchema ? { globals: Schema.toJsonSchema(globalsSchema) } : undefined),
2097
2301
  };
2098
2302
  }
2099
2303
  /** @internal Recursively collects leaf commands with their full paths. */
@@ -2143,7 +2347,7 @@ function collectCommands(commands, prefix) {
2143
2347
  return result;
2144
2348
  }
2145
2349
  /** @internal Recursively collects leaf commands as `Skill.CommandInfo` for `--llms --format md`. */
2146
- function collectSkillCommands(commands, prefix, groups, rootCommand) {
2350
+ export function collectSkillCommands(commands, prefix, groups, rootCommand) {
2147
2351
  const result = [];
2148
2352
  if (rootCommand) {
2149
2353
  const cmd = {};
@@ -2155,6 +2359,8 @@ function collectSkillCommands(commands, prefix, groups, rootCommand) {
2155
2359
  cmd.env = rootCommand.env;
2156
2360
  if (rootCommand.hint)
2157
2361
  cmd.hint = rootCommand.hint;
2362
+ if (isDestructive(rootCommand))
2363
+ cmd.hint = appendDestructiveHint(cmd.hint);
2158
2364
  if (rootCommand.options)
2159
2365
  cmd.options = rootCommand.options;
2160
2366
  if (rootCommand.output)
@@ -2190,6 +2396,8 @@ function collectSkillCommands(commands, prefix, groups, rootCommand) {
2190
2396
  cmd.env = entry.env;
2191
2397
  if (entry.hint)
2192
2398
  cmd.hint = entry.hint;
2399
+ if (isDestructive(entry))
2400
+ cmd.hint = appendDestructiveHint(cmd.hint);
2193
2401
  if (entry.options)
2194
2402
  cmd.options = entry.options;
2195
2403
  if (entry.output)
@@ -2207,6 +2415,16 @@ function collectSkillCommands(commands, prefix, groups, rootCommand) {
2207
2415
  }
2208
2416
  return result.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''));
2209
2417
  }
2418
+ function isDestructive(command) {
2419
+ return command.destructive === true || command.mcp?.annotations?.destructiveHint === true;
2420
+ }
2421
+ function appendDestructiveHint(hint) {
2422
+ if (!hint)
2423
+ return destructiveCommandHint;
2424
+ if (hint.includes(destructiveCommandHint))
2425
+ return hint;
2426
+ return `${hint} ${destructiveCommandHint}`;
2427
+ }
2210
2428
  /** @internal Formats examples into `{ command, description }` objects. `command` is the args/options suffix only. */
2211
2429
  export function formatExamples(examples) {
2212
2430
  if (!examples || examples.length === 0)
@@ -2225,6 +2443,16 @@ export function formatExamples(examples) {
2225
2443
  return result;
2226
2444
  });
2227
2445
  }
2446
+ /** @internal Parses YAML frontmatter from generated skill Markdown. */
2447
+ export function parseSkillFrontmatter(content) {
2448
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
2449
+ if (!match)
2450
+ return {};
2451
+ const meta = Yaml.loadSync().parse(match[1]);
2452
+ if (!meta || typeof meta !== 'object')
2453
+ return {};
2454
+ return meta;
2455
+ }
2228
2456
  /** @internal Builds separate args, env, and options JSON Schemas. */
2229
2457
  function buildInputSchema(args, env, options) {
2230
2458
  if (!args && !env && !options)