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.
- package/dist/Cli.d.ts +69 -19
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +419 -73
- package/dist/Cli.js.map +1 -1
- package/dist/Completions.d.ts +2 -1
- package/dist/Completions.d.ts.map +1 -1
- package/dist/Completions.js +52 -13
- package/dist/Completions.js.map +1 -1
- package/dist/Formatter.d.ts.map +1 -1
- package/dist/Formatter.js +6 -5
- package/dist/Formatter.js.map +1 -1
- package/dist/Help.d.ts +5 -0
- package/dist/Help.d.ts.map +1 -1
- package/dist/Help.js +19 -5
- package/dist/Help.js.map +1 -1
- package/dist/Mcp.d.ts +21 -0
- package/dist/Mcp.d.ts.map +1 -1
- package/dist/Mcp.js +44 -13
- package/dist/Mcp.js.map +1 -1
- package/dist/Parser.d.ts +14 -0
- package/dist/Parser.d.ts.map +1 -1
- package/dist/Parser.js +127 -1
- package/dist/Parser.js.map +1 -1
- package/dist/Skill.d.ts.map +1 -1
- package/dist/Skill.js +5 -2
- package/dist/Skill.js.map +1 -1
- package/dist/Skillgen.d.ts.map +1 -1
- package/dist/Skillgen.js +4 -38
- package/dist/Skillgen.js.map +1 -1
- package/dist/SyncMcp.d.ts.map +1 -1
- package/dist/SyncMcp.js +75 -8
- package/dist/SyncMcp.js.map +1 -1
- package/dist/SyncSkills.d.ts +9 -0
- package/dist/SyncSkills.d.ts.map +1 -1
- package/dist/SyncSkills.js +13 -74
- package/dist/SyncSkills.js.map +1 -1
- package/dist/bin.d.ts +1 -1
- package/dist/bin.d.ts.map +1 -1
- package/dist/internal/command.d.ts +31 -19
- package/dist/internal/command.d.ts.map +1 -1
- package/dist/internal/command.js +10 -5
- package/dist/internal/command.js.map +1 -1
- package/dist/internal/cta.d.ts +22 -0
- package/dist/internal/cta.d.ts.map +1 -0
- package/dist/internal/cta.js +25 -0
- package/dist/internal/cta.js.map +1 -0
- package/dist/internal/json.d.ts +5 -0
- package/dist/internal/json.d.ts.map +1 -0
- package/dist/internal/json.js +13 -0
- package/dist/internal/json.js.map +1 -0
- package/dist/internal/yaml.d.ts +12 -0
- package/dist/internal/yaml.d.ts.map +1 -0
- package/dist/internal/yaml.js +20 -0
- package/dist/internal/yaml.js.map +1 -0
- package/dist/middleware.d.ts +8 -4
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +1 -1
- package/dist/middleware.js.map +1 -1
- package/package.json +1 -1
- package/src/Cli.test-d.ts +73 -0
- package/src/Cli.test.ts +1536 -48
- package/src/Cli.ts +606 -108
- package/src/Completions.test.ts +85 -0
- package/src/Completions.ts +48 -9
- package/src/Formatter.test.ts +17 -0
- package/src/Formatter.ts +7 -5
- package/src/Help.test.ts +32 -1
- package/src/Help.ts +42 -4
- package/src/Mcp.test.ts +289 -0
- package/src/Mcp.ts +68 -13
- package/src/Parser.test.ts +173 -0
- package/src/Parser.ts +132 -1
- package/src/Skill.test.ts +6 -0
- package/src/Skill.ts +8 -5
- package/src/Skillgen.test.ts +55 -6
- package/src/Skillgen.ts +9 -35
- package/src/SyncMcp.test.ts +89 -0
- package/src/SyncMcp.ts +72 -8
- package/src/SyncSkills.test.ts +78 -1
- package/src/SyncSkills.ts +20 -78
- package/src/bun-compile.test.ts +5 -0
- package/src/e2e.test.ts +166 -12
- package/src/internal/command.ts +12 -4
- package/src/internal/cta.ts +58 -0
- package/src/internal/json.ts +16 -0
- package/src/internal/yaml.ts +23 -0
- package/src/lazy-imports.test.ts +94 -0
- package/src/middleware.ts +12 -3
package/dist/Cli.js
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
|
import * as Completions from './Completions.js';
|
|
8
8
|
import { IncurError, ParseError, ValidationError } from './Errors.js';
|
|
@@ -12,8 +12,11 @@ import * as Formatter from './Formatter.js';
|
|
|
12
12
|
import * as Help from './Help.js';
|
|
13
13
|
import { builtinCommands, findBuiltin, findBuiltinSubcommand, shells, } from './internal/command.js';
|
|
14
14
|
import * as Command from './internal/command.js';
|
|
15
|
+
import { formatCtaBlock } from './internal/cta.js';
|
|
15
16
|
import { isRecord, suggest, toKebab } from './internal/helpers.js';
|
|
17
|
+
import * as Json from './internal/json.js';
|
|
16
18
|
import { detectRunner } from './internal/pm.js';
|
|
19
|
+
import * as Yaml from './internal/yaml.js';
|
|
17
20
|
import * as Mcp from './Mcp.js';
|
|
18
21
|
import * as Openapi from './Openapi.js';
|
|
19
22
|
import * as Parser from './Parser.js';
|
|
@@ -21,6 +24,7 @@ import * as Schema from './Schema.js';
|
|
|
21
24
|
import * as Skill from './Skill.js';
|
|
22
25
|
import * as SyncMcp from './SyncMcp.js';
|
|
23
26
|
import * as SyncSkills from './SyncSkills.js';
|
|
27
|
+
const destructiveCommandHint = 'Confirm with the user before executing this destructive command.';
|
|
24
28
|
export function create(nameOrDefinition, definition) {
|
|
25
29
|
const name = typeof nameOrDefinition === 'string' ? nameOrDefinition : nameOrDefinition.name;
|
|
26
30
|
const def = typeof nameOrDefinition === 'string' ? (definition ?? {}) : nameOrDefinition;
|
|
@@ -31,7 +35,9 @@ export function create(nameOrDefinition, definition) {
|
|
|
31
35
|
const commands = new Map();
|
|
32
36
|
const middlewares = [];
|
|
33
37
|
const pending = [];
|
|
34
|
-
const mcpHandler = createMcpHttpHandler(name, def.version ?? '0.0.0'
|
|
38
|
+
const mcpHandler = createMcpHttpHandler(name, def.version ?? '0.0.0', {
|
|
39
|
+
stateless: def.mcp?.stateless,
|
|
40
|
+
});
|
|
35
41
|
if (def.openapi && rootFetch) {
|
|
36
42
|
pending.push((async () => {
|
|
37
43
|
const spec = await Openapi.resolve(def.openapi, { baseUrl: rootFetchBaseUrl });
|
|
@@ -61,12 +67,14 @@ export function create(nameOrDefinition, definition) {
|
|
|
61
67
|
basePath: def.basePath,
|
|
62
68
|
config: def.openapiConfig,
|
|
63
69
|
});
|
|
64
|
-
|
|
70
|
+
const entry = {
|
|
65
71
|
_group: true,
|
|
66
72
|
description: def.description,
|
|
67
73
|
commands: generated,
|
|
68
74
|
...(def.outputPolicy ? { outputPolicy: def.outputPolicy } : undefined),
|
|
69
|
-
}
|
|
75
|
+
};
|
|
76
|
+
assertNoGlobalOptionConflicts(nameOrCli, entry, toGlobals.get(cli));
|
|
77
|
+
commands.set(nameOrCli, entry);
|
|
70
78
|
})());
|
|
71
79
|
return cli;
|
|
72
80
|
}
|
|
@@ -79,6 +87,7 @@ export function create(nameOrDefinition, definition) {
|
|
|
79
87
|
});
|
|
80
88
|
return cli;
|
|
81
89
|
}
|
|
90
|
+
assertNoGlobalOptionConflicts(nameOrCli, def, toGlobals.get(cli));
|
|
82
91
|
commands.set(nameOrCli, def);
|
|
83
92
|
if (def.aliases)
|
|
84
93
|
for (const a of def.aliases)
|
|
@@ -87,6 +96,7 @@ export function create(nameOrDefinition, definition) {
|
|
|
87
96
|
}
|
|
88
97
|
const mountedRootDef = toRootDefinition.get(nameOrCli);
|
|
89
98
|
if (mountedRootDef) {
|
|
99
|
+
assertNoGlobalOptionConflicts(nameOrCli.name, mountedRootDef, toGlobals.get(cli));
|
|
90
100
|
commands.set(nameOrCli.name, mountedRootDef);
|
|
91
101
|
const rootAliases = toRootAliases.get(nameOrCli);
|
|
92
102
|
if (rootAliases)
|
|
@@ -98,21 +108,25 @@ export function create(nameOrDefinition, definition) {
|
|
|
98
108
|
const subCommands = toCommands.get(sub);
|
|
99
109
|
const subOutputPolicy = toOutputPolicy.get(sub);
|
|
100
110
|
const subMiddlewares = toMiddlewares.get(sub);
|
|
101
|
-
|
|
111
|
+
const entry = {
|
|
102
112
|
_group: true,
|
|
103
113
|
description: sub.description,
|
|
104
114
|
commands: subCommands,
|
|
105
115
|
...(subOutputPolicy ? { outputPolicy: subOutputPolicy } : undefined),
|
|
106
116
|
...(subMiddlewares?.length ? { middlewares: subMiddlewares } : undefined),
|
|
107
|
-
}
|
|
117
|
+
};
|
|
118
|
+
assertNoGlobalOptionConflicts(sub.name, entry, toGlobals.get(cli));
|
|
119
|
+
commands.set(sub.name, entry);
|
|
108
120
|
return cli;
|
|
109
121
|
},
|
|
110
122
|
async fetch(req) {
|
|
111
123
|
if (pending.length > 0)
|
|
112
124
|
await Promise.all(pending);
|
|
125
|
+
const globalsDesc = toGlobals.get(cli);
|
|
113
126
|
return fetchImpl(name, commands, req, {
|
|
114
127
|
description: def.description,
|
|
115
128
|
envSchema: def.env,
|
|
129
|
+
globals: globalsDesc,
|
|
116
130
|
mcpHandler,
|
|
117
131
|
middlewares,
|
|
118
132
|
name,
|
|
@@ -124,13 +138,16 @@ export function create(nameOrDefinition, definition) {
|
|
|
124
138
|
async serve(argv = process.argv.slice(2), serveOptions = {}) {
|
|
125
139
|
if (pending.length > 0)
|
|
126
140
|
await Promise.all(pending);
|
|
141
|
+
const globalsDesc = toGlobals.get(cli);
|
|
127
142
|
return serveImpl(name, commands, argv, {
|
|
128
143
|
...serveOptions,
|
|
129
144
|
aliases: def.aliases,
|
|
145
|
+
banner: def.banner,
|
|
130
146
|
config: def.config,
|
|
131
147
|
description: def.description,
|
|
132
148
|
envSchema: def.env,
|
|
133
149
|
format: def.format,
|
|
150
|
+
globals: globalsDesc,
|
|
134
151
|
mcp: def.mcp,
|
|
135
152
|
middlewares,
|
|
136
153
|
outputPolicy: def.outputPolicy,
|
|
@@ -156,6 +173,40 @@ export function create(nameOrDefinition, definition) {
|
|
|
156
173
|
toConfigEnabled.set(cli, true);
|
|
157
174
|
if (def.outputPolicy)
|
|
158
175
|
toOutputPolicy.set(cli, def.outputPolicy);
|
|
176
|
+
if (def.globals) {
|
|
177
|
+
toGlobals.set(cli, { schema: def.globals, alias: def.globalAlias });
|
|
178
|
+
const builtinNames = [
|
|
179
|
+
'verbose',
|
|
180
|
+
'format',
|
|
181
|
+
'json',
|
|
182
|
+
'llms',
|
|
183
|
+
'llmsFull',
|
|
184
|
+
'mcp',
|
|
185
|
+
'help',
|
|
186
|
+
'version',
|
|
187
|
+
'schema',
|
|
188
|
+
'filterOutput',
|
|
189
|
+
'tokenLimit',
|
|
190
|
+
'tokenOffset',
|
|
191
|
+
'tokenCount',
|
|
192
|
+
...(def.config?.flag
|
|
193
|
+
? [def.config.flag, `no${def.config.flag[0].toUpperCase()}${def.config.flag.slice(1)}`]
|
|
194
|
+
: []),
|
|
195
|
+
];
|
|
196
|
+
const globalKeys = Object.keys(def.globals.shape);
|
|
197
|
+
for (const key of globalKeys) {
|
|
198
|
+
if (builtinNames.includes(key))
|
|
199
|
+
throw new Error(`Global option '${key}' conflicts with a built-in flag. Choose a different name.`);
|
|
200
|
+
}
|
|
201
|
+
// Check globalAlias values against reserved short aliases
|
|
202
|
+
const reservedShorts = new Set(['h']);
|
|
203
|
+
if (def.globalAlias) {
|
|
204
|
+
for (const [name, short] of Object.entries(def.globalAlias)) {
|
|
205
|
+
if (reservedShorts.has(short))
|
|
206
|
+
throw new Error(`Global alias '-${short}' for '${name}' conflicts with a built-in short flag. Choose a different alias.`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
159
210
|
toMiddlewares.set(cli, middlewares);
|
|
160
211
|
toCommands.set(cli, commands);
|
|
161
212
|
return cli;
|
|
@@ -172,6 +223,22 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
172
223
|
function writeln(s) {
|
|
173
224
|
stdout(s.endsWith('\n') ? s : `${s}\n`);
|
|
174
225
|
}
|
|
226
|
+
async function writeBanner() {
|
|
227
|
+
if (!options.banner || help)
|
|
228
|
+
return;
|
|
229
|
+
const banner = typeof options.banner === 'function'
|
|
230
|
+
? { render: options.banner, mode: 'all' }
|
|
231
|
+
: options.banner;
|
|
232
|
+
const mode = banner.mode ?? 'all';
|
|
233
|
+
if (mode !== 'all' && mode !== (human ? 'human' : 'agent'))
|
|
234
|
+
return;
|
|
235
|
+
try {
|
|
236
|
+
const text = await banner.render();
|
|
237
|
+
if (text)
|
|
238
|
+
writeln(text);
|
|
239
|
+
}
|
|
240
|
+
catch { }
|
|
241
|
+
}
|
|
175
242
|
let builtinFlags;
|
|
176
243
|
try {
|
|
177
244
|
builtinFlags = extractBuiltinFlags(argv, { configFlag });
|
|
@@ -185,7 +252,36 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
185
252
|
exit(1);
|
|
186
253
|
return;
|
|
187
254
|
}
|
|
188
|
-
const { fullOutput, format: formatFlag, formatExplicit, filterOutput, tokenLimit, tokenOffset, tokenCount, llms, llmsFull, mcp: mcpFlag, help, version, schema, configPath, configDisabled, rest
|
|
255
|
+
const { fullOutput, format: formatFlag, formatExplicit, filterOutput, tokenLimit, tokenOffset, tokenCount, llms, llmsFull, mcp: mcpFlag, help, version, schema, configPath, configDisabled, rest, } = builtinFlags;
|
|
256
|
+
let globals = {};
|
|
257
|
+
let filtered = rest;
|
|
258
|
+
function parseGlobalOptions(validate) {
|
|
259
|
+
if (!options.globals)
|
|
260
|
+
return true;
|
|
261
|
+
try {
|
|
262
|
+
const result = Parser.parseGlobals(rest, options.globals.schema, options.globals.alias, {
|
|
263
|
+
validate,
|
|
264
|
+
});
|
|
265
|
+
if (validate)
|
|
266
|
+
globals = result.parsed;
|
|
267
|
+
filtered = result.rest;
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
272
|
+
if (human)
|
|
273
|
+
writeln(formatHumanError({ code: 'UNKNOWN', message }));
|
|
274
|
+
else
|
|
275
|
+
writeln(Formatter.format({ code: 'UNKNOWN', message }, 'toon'));
|
|
276
|
+
exit(1);
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (!parseGlobalOptions(false))
|
|
281
|
+
return;
|
|
282
|
+
// Pre-load yaml for the sync formatting paths below (yaml is loaded lazily -- see internal/yaml.ts).
|
|
283
|
+
if (formatFlag === 'yaml')
|
|
284
|
+
await Yaml.load();
|
|
189
285
|
// --mcp: start as MCP stdio server
|
|
190
286
|
if (mcpFlag) {
|
|
191
287
|
await Mcp.serve(name, options.version ?? '0.0.0', commands, {
|
|
@@ -193,6 +289,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
193
289
|
env: options.envSchema,
|
|
194
290
|
vars: options.vars,
|
|
195
291
|
version: options.version,
|
|
292
|
+
...(options.mcp?.instructions ? { instructions: options.mcp.instructions } : undefined),
|
|
196
293
|
});
|
|
197
294
|
return;
|
|
198
295
|
}
|
|
@@ -209,7 +306,9 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
209
306
|
}
|
|
210
307
|
else {
|
|
211
308
|
const index = Number(process.env._COMPLETE_INDEX ?? words.length - 1);
|
|
212
|
-
const candidates = Completions.complete(commands, options.rootCommand, words, index
|
|
309
|
+
const candidates = Completions.complete(commands, options.rootCommand, words, index, options.globals
|
|
310
|
+
? { schema: options.globals.schema, alias: options.globals.alias }
|
|
311
|
+
: undefined);
|
|
213
312
|
// Add built-in commands (completions, mcp, skills) to completions
|
|
214
313
|
const current = words[index] ?? '';
|
|
215
314
|
const nonFlags = words.slice(0, index).filter((w) => !w.startsWith('-'));
|
|
@@ -282,25 +381,29 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
282
381
|
}
|
|
283
382
|
}
|
|
284
383
|
const scopedRoot = prefix.length === 0 ? options.rootCommand : undefined;
|
|
384
|
+
// Markdown skill output renders scopedName separately. Passing prefix again
|
|
385
|
+
// to those collect helpers would double the group segment in command names
|
|
386
|
+
// (e.g. "cli auth auth login" instead of "cli auth login").
|
|
387
|
+
const collectPrefix = prefix.length > 0 ? [] : prefix;
|
|
285
388
|
if (llmsFull) {
|
|
286
389
|
if (!formatExplicit || formatFlag === 'md') {
|
|
287
390
|
const groups = new Map();
|
|
288
|
-
const cmds = collectSkillCommands(scopedCommands,
|
|
391
|
+
const cmds = collectSkillCommands(scopedCommands, collectPrefix, groups, scopedRoot);
|
|
289
392
|
const scopedName = prefix.length > 0 ? `${name} ${prefix.join(' ')}` : name;
|
|
290
393
|
writeln(Skill.generate(scopedName, cmds, groups));
|
|
291
394
|
return;
|
|
292
395
|
}
|
|
293
|
-
writeln(Formatter.format(buildManifest(scopedCommands, prefix), formatFlag));
|
|
396
|
+
writeln(Formatter.format(buildManifest(scopedCommands, prefix, options.globals?.schema), formatFlag));
|
|
294
397
|
return;
|
|
295
398
|
}
|
|
296
399
|
if (!formatExplicit || formatFlag === 'md') {
|
|
297
400
|
const groups = new Map();
|
|
298
|
-
const cmds = collectSkillCommands(scopedCommands,
|
|
401
|
+
const cmds = collectSkillCommands(scopedCommands, collectPrefix, groups, scopedRoot);
|
|
299
402
|
const scopedName = prefix.length > 0 ? `${name} ${prefix.join(' ')}` : name;
|
|
300
403
|
writeln(Skill.index(scopedName, cmds, scopedDescription));
|
|
301
404
|
return;
|
|
302
405
|
}
|
|
303
|
-
writeln(Formatter.format(buildIndexManifest(scopedCommands, prefix), formatFlag));
|
|
406
|
+
writeln(Formatter.format(buildIndexManifest(scopedCommands, prefix, options.globals?.schema), formatFlag));
|
|
304
407
|
return;
|
|
305
408
|
}
|
|
306
409
|
// completions <shell>: print shell hook script to stdout
|
|
@@ -465,12 +568,15 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
465
568
|
}
|
|
466
569
|
return;
|
|
467
570
|
}
|
|
468
|
-
// mcp add: register CLI
|
|
571
|
+
// mcp add/doctor: register or smoke-test CLI MCP server integration.
|
|
469
572
|
const mcpIdx = builtinIdx(filtered, name, 'mcp');
|
|
470
573
|
if (mcpIdx !== -1) {
|
|
574
|
+
const builtin = findBuiltin('mcp');
|
|
471
575
|
const mcpSub = filtered[mcpIdx + 1];
|
|
472
|
-
|
|
473
|
-
|
|
576
|
+
const sub = mcpSub ? findBuiltinSubcommand(builtin, mcpSub) : undefined;
|
|
577
|
+
if (mcpSub && !sub) {
|
|
578
|
+
const candidates = builtin.subcommands?.flatMap((sub) => [sub.name, ...(sub.aliases ?? [])]) ?? [];
|
|
579
|
+
const suggestion = suggest(mcpSub, candidates);
|
|
474
580
|
const didYouMean = suggestion ? ` Did you mean '${suggestion}'?` : '';
|
|
475
581
|
const message = `'${mcpSub}' is not a command for '${name} mcp'.${didYouMean}`;
|
|
476
582
|
const ctaCommands = [];
|
|
@@ -493,13 +599,18 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
493
599
|
return;
|
|
494
600
|
}
|
|
495
601
|
if (!mcpSub) {
|
|
496
|
-
|
|
497
|
-
writeln(formatBuiltinHelp(name, b));
|
|
602
|
+
writeln(formatBuiltinHelp(name, builtin));
|
|
498
603
|
return;
|
|
499
604
|
}
|
|
500
605
|
if (help) {
|
|
501
|
-
|
|
502
|
-
|
|
606
|
+
writeln(formatBuiltinSubcommandHelp(name, builtin, sub.name));
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (sub.name === 'doctor') {
|
|
610
|
+
const result = await runMcpDoctor(name, commands, options);
|
|
611
|
+
writeln(Formatter.format(result, formatExplicit ? formatFlag : 'toon'));
|
|
612
|
+
if (!result.ok)
|
|
613
|
+
exit(1);
|
|
503
614
|
return;
|
|
504
615
|
}
|
|
505
616
|
const rest = filtered.slice(mcpIdx + 2);
|
|
@@ -556,11 +667,13 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
556
667
|
hasRequiredArgs(options.rootCommand.args)) {
|
|
557
668
|
// Root command with args but none provided (human mode) — show help
|
|
558
669
|
const cmd = options.rootCommand;
|
|
670
|
+
await writeBanner();
|
|
559
671
|
writeln(Help.formatCommand(name, {
|
|
560
672
|
alias: cmd.alias,
|
|
561
673
|
aliases: options.aliases,
|
|
562
674
|
configFlag,
|
|
563
675
|
description: cmd.description ?? options.description,
|
|
676
|
+
globals: options.globals,
|
|
564
677
|
version: options.version,
|
|
565
678
|
args: cmd.args,
|
|
566
679
|
env: cmd.env,
|
|
@@ -578,10 +691,12 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
578
691
|
// Root command/fetch with no args — treat as root invocation
|
|
579
692
|
}
|
|
580
693
|
else {
|
|
694
|
+
await writeBanner();
|
|
581
695
|
writeln(Help.formatRoot(name, {
|
|
582
696
|
aliases: options.aliases,
|
|
583
697
|
configFlag,
|
|
584
698
|
description: options.description,
|
|
699
|
+
globals: options.globals,
|
|
585
700
|
version: options.version,
|
|
586
701
|
commands: collectHelpCommands(commands),
|
|
587
702
|
root: true,
|
|
@@ -635,6 +750,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
635
750
|
aliases: options.aliases,
|
|
636
751
|
configFlag,
|
|
637
752
|
description: cmd.description ?? options.description,
|
|
753
|
+
globals: options.globals,
|
|
638
754
|
version: options.version,
|
|
639
755
|
args: cmd.args,
|
|
640
756
|
env: cmd.env,
|
|
@@ -652,6 +768,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
652
768
|
aliases: isRoot ? options.aliases : undefined,
|
|
653
769
|
configFlag,
|
|
654
770
|
description: helpDesc,
|
|
771
|
+
globals: options.globals,
|
|
655
772
|
version: isRoot ? options.version : undefined,
|
|
656
773
|
commands: collectHelpCommands(helpCmds),
|
|
657
774
|
root: isRoot,
|
|
@@ -670,6 +787,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
670
787
|
aliases: isRootCmd ? options.aliases : cmd.aliases,
|
|
671
788
|
configFlag,
|
|
672
789
|
description: cmd.description,
|
|
790
|
+
globals: options.globals,
|
|
673
791
|
version: isRootCmd ? options.version : undefined,
|
|
674
792
|
args: cmd.args,
|
|
675
793
|
env: cmd.env,
|
|
@@ -690,6 +808,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
690
808
|
writeln(Help.formatRoot(`${name} ${resolved.path}`, {
|
|
691
809
|
configFlag,
|
|
692
810
|
description: resolved.description,
|
|
811
|
+
globals: options.globals,
|
|
693
812
|
commands: collectHelpCommands(resolved.commands),
|
|
694
813
|
}));
|
|
695
814
|
return;
|
|
@@ -718,6 +837,8 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
718
837
|
result.options = Schema.toJsonSchema(cmd.options);
|
|
719
838
|
if (cmd.output)
|
|
720
839
|
result.output = Schema.toJsonSchema(cmd.output);
|
|
840
|
+
if (options.globals?.schema)
|
|
841
|
+
result.globals = Schema.toJsonSchema(options.globals.schema);
|
|
721
842
|
writeln(Formatter.format(result, format));
|
|
722
843
|
return;
|
|
723
844
|
}
|
|
@@ -725,6 +846,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
725
846
|
writeln(Help.formatRoot(`${name} ${resolved.path}`, {
|
|
726
847
|
configFlag,
|
|
727
848
|
description: resolved.description,
|
|
849
|
+
globals: options.globals,
|
|
728
850
|
commands: collectHelpCommands(resolved.commands),
|
|
729
851
|
}));
|
|
730
852
|
return;
|
|
@@ -733,6 +855,8 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
733
855
|
// Resolve effective format: explicit --format/--json → command default → CLI default → toon
|
|
734
856
|
const resolvedFormat = 'command' in resolved && resolved.command.format;
|
|
735
857
|
const format = formatExplicit ? formatFlag : resolvedFormat || options.format || 'toon';
|
|
858
|
+
if (format === 'yaml')
|
|
859
|
+
await Yaml.load();
|
|
736
860
|
// Fall back to root fetch/command when no subcommand matches,
|
|
737
861
|
// but only if the token doesn't look like a typo of a known command.
|
|
738
862
|
const rootFallbackBlocked = 'error' in resolved &&
|
|
@@ -888,6 +1012,8 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
888
1012
|
}
|
|
889
1013
|
// Fetch gateway execution path
|
|
890
1014
|
if ('fetchGateway' in effective) {
|
|
1015
|
+
if (!parseGlobalOptions(true))
|
|
1016
|
+
return;
|
|
891
1017
|
const { fetchGateway, path, rest: fetchRest } = effective;
|
|
892
1018
|
const fetchMiddleware = [
|
|
893
1019
|
...(options.middlewares ?? []),
|
|
@@ -963,6 +1089,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
963
1089
|
error: errorFn,
|
|
964
1090
|
format,
|
|
965
1091
|
formatExplicit,
|
|
1092
|
+
globals,
|
|
966
1093
|
name,
|
|
967
1094
|
set(key, value) {
|
|
968
1095
|
varsMap[key] = value;
|
|
@@ -1012,7 +1139,9 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
1012
1139
|
}
|
|
1013
1140
|
return;
|
|
1014
1141
|
}
|
|
1015
|
-
const { command, path, rest } = effective;
|
|
1142
|
+
const { command, path, rest: commandRest } = effective;
|
|
1143
|
+
if (!parseGlobalOptions(true))
|
|
1144
|
+
return;
|
|
1016
1145
|
// Collect middleware: root CLI + groups traversed + per-command
|
|
1017
1146
|
const allMiddleware = [
|
|
1018
1147
|
...(options.middlewares ?? []),
|
|
@@ -1022,7 +1151,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
1022
1151
|
...(command.middleware ?? []),
|
|
1023
1152
|
];
|
|
1024
1153
|
if (human)
|
|
1025
|
-
emitDeprecationWarnings(
|
|
1154
|
+
emitDeprecationWarnings(commandRest, command.options, command.alias);
|
|
1026
1155
|
let defaults;
|
|
1027
1156
|
if (configEnabled) {
|
|
1028
1157
|
try {
|
|
@@ -1048,13 +1177,14 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
1048
1177
|
}
|
|
1049
1178
|
const result = await Command.execute(command, {
|
|
1050
1179
|
agent: !human,
|
|
1051
|
-
argv:
|
|
1180
|
+
argv: commandRest,
|
|
1052
1181
|
defaults,
|
|
1053
1182
|
displayName,
|
|
1054
1183
|
env: options.envSchema,
|
|
1055
1184
|
envSource: options.env,
|
|
1056
1185
|
format,
|
|
1057
1186
|
formatExplicit,
|
|
1187
|
+
globals,
|
|
1058
1188
|
inputOptions: {},
|
|
1059
1189
|
middlewares: allMiddleware,
|
|
1060
1190
|
name,
|
|
@@ -1123,11 +1253,14 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
1123
1253
|
}
|
|
1124
1254
|
}
|
|
1125
1255
|
/** @internal Creates a lazy MCP HTTP handler scoped to a CLI instance. */
|
|
1126
|
-
function createMcpHttpHandler(name, version) {
|
|
1256
|
+
function createMcpHttpHandler(name, version, options = {}) {
|
|
1127
1257
|
let transport;
|
|
1128
1258
|
return async (req, commands, mcpOptions) => {
|
|
1259
|
+
const stateless = options.stateless ?? true;
|
|
1260
|
+
if (stateless && req.method !== 'POST')
|
|
1261
|
+
return new Response(null, { status: 405, headers: { Allow: 'POST' } });
|
|
1129
1262
|
if (!transport) {
|
|
1130
|
-
const { McpServer, WebStandardStreamableHTTPServerTransport } = await import('@modelcontextprotocol/server');
|
|
1263
|
+
const { fromJsonSchema, McpServer, WebStandardStreamableHTTPServerTransport } = await import('@modelcontextprotocol/server');
|
|
1131
1264
|
const server = new McpServer({ name, version });
|
|
1132
1265
|
for (const tool of Mcp.collectTools(commands, [])) {
|
|
1133
1266
|
const mergedShape = {
|
|
@@ -1138,6 +1271,9 @@ function createMcpHttpHandler(name, version) {
|
|
|
1138
1271
|
server.registerTool(tool.name, {
|
|
1139
1272
|
...(tool.description ? { description: tool.description } : undefined),
|
|
1140
1273
|
...(hasInput ? { inputSchema: z.object(mergedShape) } : undefined),
|
|
1274
|
+
...(tool.outputSchema
|
|
1275
|
+
? { outputSchema: fromJsonSchema(tool.outputSchema) }
|
|
1276
|
+
: undefined),
|
|
1141
1277
|
}, async (...callArgs) => {
|
|
1142
1278
|
const params = hasInput ? callArgs[0] : {};
|
|
1143
1279
|
return Mcp.callTool(tool, params, {
|
|
@@ -1149,10 +1285,13 @@ function createMcpHttpHandler(name, version) {
|
|
|
1149
1285
|
});
|
|
1150
1286
|
});
|
|
1151
1287
|
}
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1288
|
+
const transportOptions = stateless
|
|
1289
|
+
? { enableJsonResponse: true }
|
|
1290
|
+
: {
|
|
1291
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
1292
|
+
enableJsonResponse: true,
|
|
1293
|
+
};
|
|
1294
|
+
transport = new WebStandardStreamableHTTPServerTransport(transportOptions);
|
|
1156
1295
|
await server.connect(transport);
|
|
1157
1296
|
}
|
|
1158
1297
|
return transport.handleRequest(req);
|
|
@@ -1185,7 +1324,7 @@ async function fetchImpl(name, commands, req, options = {}) {
|
|
|
1185
1324
|
if (req.method === 'GET' && isOpenapiRoute(segments)) {
|
|
1186
1325
|
const spec = generatedOpenapi(name, commands, options);
|
|
1187
1326
|
const yaml = segments[0] === 'openapi.yml' || segments[0] === 'openapi.yaml';
|
|
1188
|
-
return new Response(yaml ?
|
|
1327
|
+
return new Response(yaml ? (await Yaml.load()).stringify(spec) : JSON.stringify(spec), {
|
|
1189
1328
|
status: 200,
|
|
1190
1329
|
headers: {
|
|
1191
1330
|
'content-type': yaml ? 'application/yaml' : 'application/json',
|
|
@@ -1205,6 +1344,8 @@ async function fetchImpl(name, commands, req, options = {}) {
|
|
|
1205
1344
|
segments[1] === 'skills' &&
|
|
1206
1345
|
segments.length >= 3 &&
|
|
1207
1346
|
req.method === 'GET') {
|
|
1347
|
+
// Pre-load yaml for the sync call paths below (`Skill.split`, frontmatter parsing).
|
|
1348
|
+
await Yaml.load();
|
|
1208
1349
|
const groups = new Map();
|
|
1209
1350
|
const cmds = collectSkillCommands(commands, [], groups, options.rootCommand);
|
|
1210
1351
|
// GET /.well-known/skills/index.json
|
|
@@ -1212,7 +1353,7 @@ async function fetchImpl(name, commands, req, options = {}) {
|
|
|
1212
1353
|
const files = Skill.split(name, cmds, 1, groups);
|
|
1213
1354
|
const skills = files.map((f) => {
|
|
1214
1355
|
const fmMatch = f.content.match(/^---\n([\s\S]*?)\n---/);
|
|
1215
|
-
const meta = fmMatch ?
|
|
1356
|
+
const meta = fmMatch ? Yaml.loadSync().parse(fmMatch[1]) : {};
|
|
1216
1357
|
return {
|
|
1217
1358
|
name: f.dir || name,
|
|
1218
1359
|
description: meta.description ?? '',
|
|
@@ -1252,7 +1393,7 @@ async function fetchImpl(name, commands, req, options = {}) {
|
|
|
1252
1393
|
catch { }
|
|
1253
1394
|
}
|
|
1254
1395
|
function jsonResponse(body, status) {
|
|
1255
|
-
return new Response(
|
|
1396
|
+
return new Response(Json.stringify(body), {
|
|
1256
1397
|
status,
|
|
1257
1398
|
headers: { 'content-type': 'application/json' },
|
|
1258
1399
|
});
|
|
@@ -1303,7 +1444,7 @@ async function fetchImpl(name, commands, req, options = {}) {
|
|
|
1303
1444
|
/** @internal Executes a resolved command for the fetch handler and returns a JSON Response. */
|
|
1304
1445
|
async function executeCommand(path, command, rest, inputOptions, start, options) {
|
|
1305
1446
|
function jsonResponse(body, status) {
|
|
1306
|
-
return new Response(
|
|
1447
|
+
return new Response(Json.stringify(body), {
|
|
1307
1448
|
status,
|
|
1308
1449
|
headers: { 'content-type': 'application/json' },
|
|
1309
1450
|
});
|
|
@@ -1313,13 +1454,39 @@ async function executeCommand(path, command, rest, inputOptions, start, options)
|
|
|
1313
1454
|
...(options.groupMiddlewares ?? []),
|
|
1314
1455
|
...(command.middleware ?? []),
|
|
1315
1456
|
];
|
|
1457
|
+
let globals = {};
|
|
1458
|
+
let commandInputOptions = inputOptions;
|
|
1459
|
+
if (options.globals) {
|
|
1460
|
+
const globalKeys = new Set(Object.keys(options.globals.schema.shape));
|
|
1461
|
+
const rawGlobals = {};
|
|
1462
|
+
commandInputOptions = {};
|
|
1463
|
+
for (const [key, value] of Object.entries(inputOptions)) {
|
|
1464
|
+
if (globalKeys.has(key))
|
|
1465
|
+
rawGlobals[key] = value;
|
|
1466
|
+
else
|
|
1467
|
+
commandInputOptions[key] = value;
|
|
1468
|
+
}
|
|
1469
|
+
try {
|
|
1470
|
+
globals = options.globals.schema.parse(rawGlobals);
|
|
1471
|
+
}
|
|
1472
|
+
catch (error) {
|
|
1473
|
+
const issues = error?.issues ?? error?.error?.issues ?? [];
|
|
1474
|
+
const message = issues.map((i) => i.message).join('; ') || 'Validation failed';
|
|
1475
|
+
return jsonResponse({
|
|
1476
|
+
ok: false,
|
|
1477
|
+
error: { code: 'VALIDATION_ERROR', message },
|
|
1478
|
+
meta: { command: path, duration: `${Math.round(performance.now() - start)}ms` },
|
|
1479
|
+
}, 400);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1316
1482
|
const result = await Command.execute(command, {
|
|
1317
1483
|
agent: true,
|
|
1318
1484
|
argv: rest,
|
|
1319
1485
|
env: options.envSchema,
|
|
1320
1486
|
format: 'json',
|
|
1321
1487
|
formatExplicit: true,
|
|
1322
|
-
|
|
1488
|
+
globals,
|
|
1489
|
+
inputOptions: commandInputOptions,
|
|
1323
1490
|
middlewares: allMiddleware,
|
|
1324
1491
|
name: options.name ?? path,
|
|
1325
1492
|
parseMode: 'split',
|
|
@@ -1330,30 +1497,68 @@ async function executeCommand(path, command, rest, inputOptions, start, options)
|
|
|
1330
1497
|
const duration = `${Math.round(performance.now() - start)}ms`;
|
|
1331
1498
|
// Streaming path — async generator → NDJSON response
|
|
1332
1499
|
if ('stream' in result) {
|
|
1500
|
+
const iterator = result.stream;
|
|
1501
|
+
const encoder = new TextEncoder();
|
|
1502
|
+
const meta = (cta) => ({
|
|
1503
|
+
command: path,
|
|
1504
|
+
duration: `${Math.round(performance.now() - start)}ms`,
|
|
1505
|
+
...(cta ? { cta } : undefined),
|
|
1506
|
+
});
|
|
1507
|
+
const errorRecord = (err) => ({
|
|
1508
|
+
type: 'error',
|
|
1509
|
+
ok: false,
|
|
1510
|
+
error: {
|
|
1511
|
+
code: err.code,
|
|
1512
|
+
message: err.message,
|
|
1513
|
+
...(err.retryable !== undefined ? { retryable: err.retryable } : undefined),
|
|
1514
|
+
},
|
|
1515
|
+
meta: meta(formatCtaBlock(options.name ?? path, err.cta)),
|
|
1516
|
+
});
|
|
1333
1517
|
const stream = new ReadableStream({
|
|
1334
|
-
async
|
|
1335
|
-
|
|
1518
|
+
async cancel() {
|
|
1519
|
+
await iterator.return(undefined);
|
|
1520
|
+
},
|
|
1521
|
+
async pull(controller) {
|
|
1336
1522
|
try {
|
|
1337
|
-
|
|
1338
|
-
|
|
1523
|
+
const { value, done } = await iterator.next();
|
|
1524
|
+
if (done) {
|
|
1525
|
+
if (isSentinel(value) && value[sentinel] === 'error') {
|
|
1526
|
+
controller.enqueue(encoder.encode(Json.stringify(errorRecord(value)) + '\n'));
|
|
1527
|
+
controller.close();
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
const cta = isSentinel(value) && value[sentinel] === 'ok'
|
|
1531
|
+
? formatCtaBlock(options.name ?? path, value.cta)
|
|
1532
|
+
: undefined;
|
|
1533
|
+
controller.enqueue(encoder.encode(Json.stringify({
|
|
1534
|
+
type: 'done',
|
|
1535
|
+
ok: true,
|
|
1536
|
+
meta: meta(cta),
|
|
1537
|
+
}) + '\n'));
|
|
1538
|
+
controller.close();
|
|
1539
|
+
return;
|
|
1339
1540
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1541
|
+
if (isSentinel(value) && value[sentinel] === 'error') {
|
|
1542
|
+
controller.enqueue(encoder.encode(Json.stringify(errorRecord(value)) + '\n'));
|
|
1543
|
+
await iterator.return(undefined);
|
|
1544
|
+
controller.close();
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
controller.enqueue(encoder.encode(Json.stringify({ type: 'chunk', data: value }) + '\n'));
|
|
1345
1548
|
}
|
|
1346
1549
|
catch (error) {
|
|
1347
|
-
controller.enqueue(encoder.encode(
|
|
1550
|
+
controller.enqueue(encoder.encode(Json.stringify({
|
|
1348
1551
|
type: 'error',
|
|
1349
1552
|
ok: false,
|
|
1350
1553
|
error: {
|
|
1351
|
-
code: 'UNKNOWN',
|
|
1554
|
+
code: error instanceof IncurError ? error.code : 'UNKNOWN',
|
|
1352
1555
|
message: error instanceof Error ? error.message : String(error),
|
|
1556
|
+
...(error instanceof IncurError ? { retryable: error.retryable } : undefined),
|
|
1353
1557
|
},
|
|
1558
|
+
meta: meta(),
|
|
1354
1559
|
}) + '\n'));
|
|
1560
|
+
controller.close();
|
|
1355
1561
|
}
|
|
1356
|
-
controller.close();
|
|
1357
1562
|
},
|
|
1358
1563
|
});
|
|
1359
1564
|
return new Response(stream, {
|
|
@@ -1371,6 +1576,7 @@ async function executeCommand(path, command, rest, inputOptions, start, options)
|
|
|
1371
1576
|
...(result.error.retryable !== undefined
|
|
1372
1577
|
? { retryable: result.error.retryable }
|
|
1373
1578
|
: undefined),
|
|
1579
|
+
...(result.error.fieldErrors ? { fieldErrors: result.error.fieldErrors } : undefined),
|
|
1374
1580
|
},
|
|
1375
1581
|
meta: {
|
|
1376
1582
|
command: path,
|
|
@@ -1765,6 +1971,115 @@ function formatBuiltinSubcommandHelp(cli, builtin, subName) {
|
|
|
1765
1971
|
options: sub?.options,
|
|
1766
1972
|
});
|
|
1767
1973
|
}
|
|
1974
|
+
async function runMcpDoctor(name, commands, options) {
|
|
1975
|
+
const warnings = [];
|
|
1976
|
+
const errors = [];
|
|
1977
|
+
const input = new PassThrough();
|
|
1978
|
+
const output = new PassThrough();
|
|
1979
|
+
const chunks = [];
|
|
1980
|
+
output.on('data', (chunk) => chunks.push(chunk.toString()));
|
|
1981
|
+
let serveError;
|
|
1982
|
+
const done = Mcp.serve(name, options.version ?? '0.0.0', commands, {
|
|
1983
|
+
input,
|
|
1984
|
+
output,
|
|
1985
|
+
middlewares: options.middlewares,
|
|
1986
|
+
env: options.envSchema,
|
|
1987
|
+
vars: options.vars,
|
|
1988
|
+
version: options.version,
|
|
1989
|
+
...(options.mcp?.instructions ? { instructions: options.mcp.instructions } : undefined),
|
|
1990
|
+
}).catch((error) => {
|
|
1991
|
+
serveError = error;
|
|
1992
|
+
});
|
|
1993
|
+
input.write(`${Json.stringify({
|
|
1994
|
+
jsonrpc: '2.0',
|
|
1995
|
+
id: 1,
|
|
1996
|
+
method: 'initialize',
|
|
1997
|
+
params: {
|
|
1998
|
+
protocolVersion: '2024-11-05',
|
|
1999
|
+
capabilities: {},
|
|
2000
|
+
clientInfo: { name: 'incur-doctor', version: '1.0.0' },
|
|
2001
|
+
},
|
|
2002
|
+
})}\n`);
|
|
2003
|
+
input.write(`${Json.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} })}\n`);
|
|
2004
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
2005
|
+
input.end();
|
|
2006
|
+
await done;
|
|
2007
|
+
if (serveError)
|
|
2008
|
+
return {
|
|
2009
|
+
ok: false,
|
|
2010
|
+
toolCount: 0,
|
|
2011
|
+
tools: [],
|
|
2012
|
+
warnings,
|
|
2013
|
+
errors: [{ code: 'MCP_SERVER_FAILED', message: errorMessage(serveError) }],
|
|
2014
|
+
};
|
|
2015
|
+
let responses;
|
|
2016
|
+
try {
|
|
2017
|
+
responses = parseMcpDoctorResponses(chunks);
|
|
2018
|
+
}
|
|
2019
|
+
catch (error) {
|
|
2020
|
+
return {
|
|
2021
|
+
ok: false,
|
|
2022
|
+
toolCount: 0,
|
|
2023
|
+
tools: [],
|
|
2024
|
+
warnings,
|
|
2025
|
+
errors: [{ code: 'MCP_RESPONSE_PARSE_FAILED', message: errorMessage(error) }],
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
const initialize = responses.find((response) => response.id === 1);
|
|
2029
|
+
if (!initialize)
|
|
2030
|
+
errors.push({ code: 'MCP_INITIALIZE_MISSING', message: 'Missing initialize response.' });
|
|
2031
|
+
else if (initialize.error)
|
|
2032
|
+
errors.push({ code: 'MCP_INITIALIZE_FAILED', message: mcpErrorMessage(initialize.error) });
|
|
2033
|
+
const toolsList = responses.find((response) => response.id === 2);
|
|
2034
|
+
let tools = [];
|
|
2035
|
+
if (!toolsList)
|
|
2036
|
+
errors.push({ code: 'MCP_TOOLS_LIST_MISSING', message: 'Missing tools/list response.' });
|
|
2037
|
+
else if (toolsList.error)
|
|
2038
|
+
errors.push({ code: 'MCP_TOOLS_LIST_FAILED', message: mcpErrorMessage(toolsList.error) });
|
|
2039
|
+
else if (!isRecord(toolsList.result) || !Array.isArray(toolsList.result.tools))
|
|
2040
|
+
errors.push({
|
|
2041
|
+
code: 'MCP_TOOLS_LIST_INVALID',
|
|
2042
|
+
message: 'tools/list did not return a tools array.',
|
|
2043
|
+
});
|
|
2044
|
+
else
|
|
2045
|
+
tools = toolsList.result.tools
|
|
2046
|
+
.filter(isRecord)
|
|
2047
|
+
.map((tool) => ({
|
|
2048
|
+
name: typeof tool.name === 'string' ? tool.name : '',
|
|
2049
|
+
...(typeof tool.description === 'string' ? { description: tool.description } : undefined),
|
|
2050
|
+
}))
|
|
2051
|
+
.filter((tool) => tool.name);
|
|
2052
|
+
if (errors.length === 0 && tools.length === 0)
|
|
2053
|
+
warnings.push('No MCP tools exposed.');
|
|
2054
|
+
return {
|
|
2055
|
+
ok: errors.length === 0,
|
|
2056
|
+
toolCount: tools.length,
|
|
2057
|
+
tools,
|
|
2058
|
+
warnings,
|
|
2059
|
+
errors,
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
function parseMcpDoctorResponses(chunks) {
|
|
2063
|
+
const responses = [];
|
|
2064
|
+
for (const line of chunks.join('').split(/\r?\n/)) {
|
|
2065
|
+
const trimmed = line.trim();
|
|
2066
|
+
if (!trimmed)
|
|
2067
|
+
continue;
|
|
2068
|
+
const parsed = JSON.parse(trimmed);
|
|
2069
|
+
if (!isRecord(parsed))
|
|
2070
|
+
throw new Error('Expected JSON-RPC response object.');
|
|
2071
|
+
responses.push(parsed);
|
|
2072
|
+
}
|
|
2073
|
+
return responses;
|
|
2074
|
+
}
|
|
2075
|
+
function errorMessage(error) {
|
|
2076
|
+
return error instanceof Error ? error.message : String(error);
|
|
2077
|
+
}
|
|
2078
|
+
function mcpErrorMessage(error) {
|
|
2079
|
+
if (isRecord(error) && typeof error.message === 'string')
|
|
2080
|
+
return error.message;
|
|
2081
|
+
return Json.stringify(error);
|
|
2082
|
+
}
|
|
1768
2083
|
/** @internal Formats help text for a fetch gateway command. */
|
|
1769
2084
|
function formatFetchHelp(name, description) {
|
|
1770
2085
|
const lines = [];
|
|
@@ -1819,6 +2134,31 @@ function resolveAlias(commands, entry) {
|
|
|
1819
2134
|
return commands.get(entry.target);
|
|
1820
2135
|
return entry;
|
|
1821
2136
|
}
|
|
2137
|
+
/** @internal Validates command options against CLI-level global options. */
|
|
2138
|
+
function assertNoGlobalOptionConflicts(path, entry, globals) {
|
|
2139
|
+
if (!globals || isFetchGateway(entry) || isAlias(entry))
|
|
2140
|
+
return;
|
|
2141
|
+
if (isGroup(entry)) {
|
|
2142
|
+
for (const [name, child] of entry.commands)
|
|
2143
|
+
assertNoGlobalOptionConflicts(`${path} ${name}`, child, globals);
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
if (entry.options) {
|
|
2147
|
+
const globalKeys = Object.keys(globals.schema.shape);
|
|
2148
|
+
const optionKeys = Object.keys(entry.options.shape);
|
|
2149
|
+
for (const key of optionKeys) {
|
|
2150
|
+
if (globalKeys.includes(key))
|
|
2151
|
+
throw new Error(`Command '${path}' option '${key}' conflicts with a global option. Choose a different name.`);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
if (globals.alias && entry.alias) {
|
|
2155
|
+
const globalAliasValues = new Set(Object.values(globals.alias));
|
|
2156
|
+
for (const [name, short] of Object.entries(entry.alias)) {
|
|
2157
|
+
if (short && globalAliasValues.has(short))
|
|
2158
|
+
throw new Error(`Command '${path}' alias '-${short}' for '${name}' conflicts with a global alias. Choose a different alias.`);
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
1822
2162
|
/** @internal Maps CLI instances to their command maps. */
|
|
1823
2163
|
export const toCommands = new WeakMap();
|
|
1824
2164
|
/** @internal Maps CLI instances to their middleware arrays. */
|
|
@@ -1831,6 +2171,8 @@ export const toRootOptions = new WeakMap();
|
|
|
1831
2171
|
export const toConfigEnabled = new WeakMap();
|
|
1832
2172
|
/** @internal Maps CLI instances to their output policy. */
|
|
1833
2173
|
const toOutputPolicy = new WeakMap();
|
|
2174
|
+
/** @internal Maps CLI instances to their globals schema and alias map. */
|
|
2175
|
+
const toGlobals = new WeakMap();
|
|
1834
2176
|
/** @internal Maps root CLI instances to their command aliases. */
|
|
1835
2177
|
const toRootAliases = new WeakMap();
|
|
1836
2178
|
/** @internal Sentinel symbol for `ok()` and `error()` return values. */
|
|
@@ -1901,7 +2243,7 @@ async function handleStreaming(generator, ctx) {
|
|
|
1901
2243
|
}
|
|
1902
2244
|
}
|
|
1903
2245
|
if (useJsonl)
|
|
1904
|
-
ctx.writeln(
|
|
2246
|
+
ctx.writeln(Json.stringify({ type: 'chunk', data: value }));
|
|
1905
2247
|
else if (ctx.renderOutput)
|
|
1906
2248
|
ctx.writeln(ctx.truncate(Formatter.format(value, ctx.format)).text);
|
|
1907
2249
|
}
|
|
@@ -1947,6 +2289,7 @@ async function handleStreaming(generator, ctx) {
|
|
|
1947
2289
|
error: {
|
|
1948
2290
|
code: error instanceof IncurError ? error.code : 'UNKNOWN',
|
|
1949
2291
|
message: error instanceof Error ? error.message : String(error),
|
|
2292
|
+
...(error instanceof IncurError ? { retryable: error.retryable } : undefined),
|
|
1950
2293
|
},
|
|
1951
2294
|
}));
|
|
1952
2295
|
else
|
|
@@ -2025,6 +2368,7 @@ async function handleStreaming(generator, ctx) {
|
|
|
2025
2368
|
error: {
|
|
2026
2369
|
code: error instanceof IncurError ? error.code : 'UNKNOWN',
|
|
2027
2370
|
message: error instanceof Error ? error.message : String(error),
|
|
2371
|
+
...(error instanceof IncurError ? { retryable: error.retryable } : undefined),
|
|
2028
2372
|
},
|
|
2029
2373
|
meta: {
|
|
2030
2374
|
command: ctx.path,
|
|
@@ -2035,35 +2379,12 @@ async function handleStreaming(generator, ctx) {
|
|
|
2035
2379
|
}
|
|
2036
2380
|
}
|
|
2037
2381
|
}
|
|
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
2382
|
/** @internal Builds the `--llms` index manifest (name + description only) from the command tree. */
|
|
2063
|
-
function buildIndexManifest(commands, prefix = []) {
|
|
2383
|
+
function buildIndexManifest(commands, prefix = [], globalsSchema) {
|
|
2064
2384
|
return {
|
|
2065
2385
|
version: 'incur.v1',
|
|
2066
2386
|
commands: collectIndexCommands(commands, prefix).sort((a, b) => a.name.localeCompare(b.name)),
|
|
2387
|
+
...(globalsSchema ? { globals: Schema.toJsonSchema(globalsSchema) } : undefined),
|
|
2067
2388
|
};
|
|
2068
2389
|
}
|
|
2069
2390
|
/** @internal Recursively collects leaf commands with name + description only. */
|
|
@@ -2090,10 +2411,11 @@ function collectIndexCommands(commands, prefix) {
|
|
|
2090
2411
|
return result;
|
|
2091
2412
|
}
|
|
2092
2413
|
/** @internal Builds the `--llms` manifest from the command tree. */
|
|
2093
|
-
function buildManifest(commands, prefix = []) {
|
|
2414
|
+
function buildManifest(commands, prefix = [], globalsSchema) {
|
|
2094
2415
|
return {
|
|
2095
2416
|
version: 'incur.v1',
|
|
2096
2417
|
commands: collectCommands(commands, prefix).sort((a, b) => a.name.localeCompare(b.name)),
|
|
2418
|
+
...(globalsSchema ? { globals: Schema.toJsonSchema(globalsSchema) } : undefined),
|
|
2097
2419
|
};
|
|
2098
2420
|
}
|
|
2099
2421
|
/** @internal Recursively collects leaf commands with their full paths. */
|
|
@@ -2143,7 +2465,7 @@ function collectCommands(commands, prefix) {
|
|
|
2143
2465
|
return result;
|
|
2144
2466
|
}
|
|
2145
2467
|
/** @internal Recursively collects leaf commands as `Skill.CommandInfo` for `--llms --format md`. */
|
|
2146
|
-
function collectSkillCommands(commands, prefix, groups, rootCommand) {
|
|
2468
|
+
export function collectSkillCommands(commands, prefix, groups, rootCommand) {
|
|
2147
2469
|
const result = [];
|
|
2148
2470
|
if (rootCommand) {
|
|
2149
2471
|
const cmd = {};
|
|
@@ -2155,6 +2477,8 @@ function collectSkillCommands(commands, prefix, groups, rootCommand) {
|
|
|
2155
2477
|
cmd.env = rootCommand.env;
|
|
2156
2478
|
if (rootCommand.hint)
|
|
2157
2479
|
cmd.hint = rootCommand.hint;
|
|
2480
|
+
if (isDestructive(rootCommand))
|
|
2481
|
+
cmd.hint = appendDestructiveHint(cmd.hint);
|
|
2158
2482
|
if (rootCommand.options)
|
|
2159
2483
|
cmd.options = rootCommand.options;
|
|
2160
2484
|
if (rootCommand.output)
|
|
@@ -2190,6 +2514,8 @@ function collectSkillCommands(commands, prefix, groups, rootCommand) {
|
|
|
2190
2514
|
cmd.env = entry.env;
|
|
2191
2515
|
if (entry.hint)
|
|
2192
2516
|
cmd.hint = entry.hint;
|
|
2517
|
+
if (isDestructive(entry))
|
|
2518
|
+
cmd.hint = appendDestructiveHint(cmd.hint);
|
|
2193
2519
|
if (entry.options)
|
|
2194
2520
|
cmd.options = entry.options;
|
|
2195
2521
|
if (entry.output)
|
|
@@ -2207,6 +2533,16 @@ function collectSkillCommands(commands, prefix, groups, rootCommand) {
|
|
|
2207
2533
|
}
|
|
2208
2534
|
return result.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''));
|
|
2209
2535
|
}
|
|
2536
|
+
function isDestructive(command) {
|
|
2537
|
+
return command.destructive === true || command.mcp?.annotations?.destructiveHint === true;
|
|
2538
|
+
}
|
|
2539
|
+
function appendDestructiveHint(hint) {
|
|
2540
|
+
if (!hint)
|
|
2541
|
+
return destructiveCommandHint;
|
|
2542
|
+
if (hint.includes(destructiveCommandHint))
|
|
2543
|
+
return hint;
|
|
2544
|
+
return `${hint} ${destructiveCommandHint}`;
|
|
2545
|
+
}
|
|
2210
2546
|
/** @internal Formats examples into `{ command, description }` objects. `command` is the args/options suffix only. */
|
|
2211
2547
|
export function formatExamples(examples) {
|
|
2212
2548
|
if (!examples || examples.length === 0)
|
|
@@ -2225,6 +2561,16 @@ export function formatExamples(examples) {
|
|
|
2225
2561
|
return result;
|
|
2226
2562
|
});
|
|
2227
2563
|
}
|
|
2564
|
+
/** @internal Parses YAML frontmatter from generated skill Markdown. */
|
|
2565
|
+
export function parseSkillFrontmatter(content) {
|
|
2566
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
2567
|
+
if (!match)
|
|
2568
|
+
return {};
|
|
2569
|
+
const meta = Yaml.loadSync().parse(match[1]);
|
|
2570
|
+
if (!meta || typeof meta !== 'object')
|
|
2571
|
+
return {};
|
|
2572
|
+
return meta;
|
|
2573
|
+
}
|
|
2228
2574
|
/** @internal Builds separate args, env, and options JSON Schemas. */
|
|
2229
2575
|
function buildInputSchema(args, env, options) {
|
|
2230
2576
|
if (!args && !env && !options)
|