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.
- package/dist/Cli.d.ts +65 -19
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +294 -66
- 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 +25 -8
- 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 +2 -0
- package/dist/internal/command.d.ts.map +1 -1
- package/dist/internal/command.js +6 -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 +71 -0
- package/src/Cli.test.ts +1164 -65
- package/src/Cli.ts +456 -101
- package/src/Completions.test.ts +84 -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 +31 -0
- package/src/Help.ts +42 -4
- package/src/Mcp.test.ts +193 -0
- package/src/Mcp.ts +49 -8
- 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 +162 -8
- package/src/internal/command.ts +8 -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/src/Completions.test.ts
CHANGED
|
@@ -226,6 +226,90 @@ describe('complete', () => {
|
|
|
226
226
|
})
|
|
227
227
|
})
|
|
228
228
|
|
|
229
|
+
describe('complete with globals', () => {
|
|
230
|
+
const globalSchema = z.object({
|
|
231
|
+
rpcUrl: z.string().optional().describe('RPC endpoint URL'),
|
|
232
|
+
chain: z.enum(['mainnet', 'sepolia', 'goerli']).default('mainnet').describe('Target chain'),
|
|
233
|
+
dryRun: z.boolean().default(false).describe('Dry run mode'),
|
|
234
|
+
})
|
|
235
|
+
const globalAlias = { rpcUrl: 'r', dryRun: 'd' }
|
|
236
|
+
const globals = { schema: globalSchema, alias: globalAlias }
|
|
237
|
+
|
|
238
|
+
test('global flags appear as completion candidates when typing --', () => {
|
|
239
|
+
const cli = makeCli()
|
|
240
|
+
const commands = Cli.toCommands.get(cli)!
|
|
241
|
+
const candidates = Completions.complete(commands, undefined, ['mycli', 'build', '--'], 2, globals)
|
|
242
|
+
const values = candidates.map((c) => c.value)
|
|
243
|
+
expect(values).toContain('--rpc-url')
|
|
244
|
+
expect(values).toContain('--chain')
|
|
245
|
+
expect(values).toContain('--dry-run')
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
test('global short aliases appear as candidates when typing -', () => {
|
|
249
|
+
const cli = makeCli()
|
|
250
|
+
const commands = Cli.toCommands.get(cli)!
|
|
251
|
+
const candidates = Completions.complete(commands, undefined, ['mycli', 'build', '-'], 2, globals)
|
|
252
|
+
const values = candidates.map((c) => c.value)
|
|
253
|
+
expect(values).toContain('-r')
|
|
254
|
+
expect(values).toContain('-d')
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
test('global flags do not duplicate command-specific flags', () => {
|
|
258
|
+
// Create a CLI where a command has --chain already
|
|
259
|
+
const cli = Cli.create('mycli', { version: '1.0.0' })
|
|
260
|
+
cli.command('deploy', {
|
|
261
|
+
description: 'Deploy',
|
|
262
|
+
options: z.object({
|
|
263
|
+
chain: z.string().optional().describe('Chain override'),
|
|
264
|
+
}),
|
|
265
|
+
run: () => ({}),
|
|
266
|
+
})
|
|
267
|
+
const commands = Cli.toCommands.get(cli)!
|
|
268
|
+
const rootCmd = { options: z.object({ chain: z.string().optional() }) }
|
|
269
|
+
const dupeGlobals = { schema: z.object({ chain: z.string().optional().describe('Global chain') }) }
|
|
270
|
+
const candidates = Completions.complete(
|
|
271
|
+
commands,
|
|
272
|
+
rootCmd,
|
|
273
|
+
['mycli', 'deploy', '--'],
|
|
274
|
+
2,
|
|
275
|
+
dupeGlobals,
|
|
276
|
+
)
|
|
277
|
+
const chainCount = candidates.filter((c) => c.value === '--chain').length
|
|
278
|
+
expect(chainCount).toBe(1)
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
test('global option value completion for enum', () => {
|
|
282
|
+
const cli = makeCli()
|
|
283
|
+
const commands = Cli.toCommands.get(cli)!
|
|
284
|
+
const candidates = Completions.complete(
|
|
285
|
+
commands,
|
|
286
|
+
undefined,
|
|
287
|
+
['mycli', 'build', '--chain', ''],
|
|
288
|
+
3,
|
|
289
|
+
globals,
|
|
290
|
+
)
|
|
291
|
+
const values = candidates.map((c) => c.value)
|
|
292
|
+
expect(values).toEqual(['mainnet', 'sepolia', 'goerli'])
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
test('global boolean flag does not consume next token as value in completions', () => {
|
|
296
|
+
const cli = makeCli()
|
|
297
|
+
const commands = Cli.toCommands.get(cli)!
|
|
298
|
+
// After --dry-run (boolean), the next token should suggest subcommands, not be consumed as value
|
|
299
|
+
const candidates = Completions.complete(
|
|
300
|
+
commands,
|
|
301
|
+
undefined,
|
|
302
|
+
['mycli', '--dry-run', ''],
|
|
303
|
+
2,
|
|
304
|
+
globals,
|
|
305
|
+
)
|
|
306
|
+
const values = candidates.map((c) => c.value)
|
|
307
|
+
// Should suggest subcommands since --dry-run is boolean and doesn't consume next token
|
|
308
|
+
expect(values).toContain('build')
|
|
309
|
+
expect(values).toContain('test')
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
|
|
229
313
|
describe('format', () => {
|
|
230
314
|
const candidates: Completions.Candidate[] = [
|
|
231
315
|
{ value: '--target', description: 'Build target' },
|
package/src/Completions.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { z } from 'zod'
|
|
2
2
|
|
|
3
|
+
import type { GlobalsDescriptor } from './Cli.js'
|
|
3
4
|
import type { Shell } from './internal/command.js'
|
|
5
|
+
import { toKebab } from './internal/helpers.js'
|
|
4
6
|
|
|
5
7
|
/** A completion candidate with an optional description. */
|
|
6
8
|
export type Candidate = {
|
|
@@ -41,6 +43,7 @@ export function register(shell: Shell, name: string): string {
|
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
|
|
44
47
|
/**
|
|
45
48
|
* Computes completion candidates for the given argv words and cursor index.
|
|
46
49
|
* Walks the command tree to resolve the active command, then suggests
|
|
@@ -51,6 +54,7 @@ export function complete(
|
|
|
51
54
|
rootCommand: CommandEntry | undefined,
|
|
52
55
|
argv: string[],
|
|
53
56
|
index: number,
|
|
57
|
+
globals?: GlobalsDescriptor | undefined,
|
|
54
58
|
): Candidate[] {
|
|
55
59
|
const current = argv[index] ?? ''
|
|
56
60
|
|
|
@@ -84,7 +88,7 @@ export function complete(
|
|
|
84
88
|
if (leaf?.options) {
|
|
85
89
|
const shape = leaf.options.shape as Record<string, any>
|
|
86
90
|
for (const key of Object.keys(shape)) {
|
|
87
|
-
const kebab = key
|
|
91
|
+
const kebab = toKebab(key)
|
|
88
92
|
const flag = `--${kebab}`
|
|
89
93
|
if (flag.startsWith(current))
|
|
90
94
|
candidates.push({ value: flag, description: descriptionOf(shape[key]) })
|
|
@@ -99,6 +103,25 @@ export function complete(
|
|
|
99
103
|
}
|
|
100
104
|
}
|
|
101
105
|
}
|
|
106
|
+
// Global options
|
|
107
|
+
if (globals) {
|
|
108
|
+
const globalShape = globals.schema.shape as Record<string, any>
|
|
109
|
+
for (const key of Object.keys(globalShape)) {
|
|
110
|
+
const kebab = toKebab(key)
|
|
111
|
+
const flag = `--${kebab}`
|
|
112
|
+
if (flag.startsWith(current) && !candidates.some((c) => c.value === flag))
|
|
113
|
+
candidates.push({ value: flag, description: descriptionOf(globalShape[key]) })
|
|
114
|
+
}
|
|
115
|
+
// Global short aliases
|
|
116
|
+
if (globals.alias)
|
|
117
|
+
for (const [name, short] of Object.entries(globals.alias)) {
|
|
118
|
+
const flag = `-${short}`
|
|
119
|
+
if (flag.startsWith(current) && !candidates.some((c) => c.value === flag)) {
|
|
120
|
+
const desc = descriptionOf(globalShape[name])
|
|
121
|
+
candidates.push({ value: flag, description: desc })
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
102
125
|
return candidates
|
|
103
126
|
}
|
|
104
127
|
|
|
@@ -106,15 +129,31 @@ export function complete(
|
|
|
106
129
|
if (index > 0) {
|
|
107
130
|
const prev = argv[index - 1]!
|
|
108
131
|
const leaf = scope.leaf
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
if (
|
|
112
|
-
const
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
|
|
132
|
+
if (prev.startsWith('-')) {
|
|
133
|
+
// Try command-specific options first
|
|
134
|
+
if (leaf?.options) {
|
|
135
|
+
const name = resolveOptionName(prev, leaf)
|
|
136
|
+
if (name) {
|
|
137
|
+
const values = possibleValues(name, leaf.options)
|
|
138
|
+
if (values) {
|
|
139
|
+
for (const v of values) if (v.startsWith(current)) candidates.push({ value: v })
|
|
140
|
+
return candidates
|
|
141
|
+
}
|
|
142
|
+
if (!isBooleanOption(name, leaf.options)) return candidates
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Try global options
|
|
146
|
+
if (globals) {
|
|
147
|
+
const globalEntry: CommandEntry = { options: globals.schema, alias: globals.alias }
|
|
148
|
+
const name = resolveOptionName(prev, globalEntry)
|
|
149
|
+
if (name) {
|
|
150
|
+
const values = possibleValues(name, globals.schema)
|
|
151
|
+
if (values) {
|
|
152
|
+
for (const v of values) if (v.startsWith(current)) candidates.push({ value: v })
|
|
153
|
+
return candidates
|
|
154
|
+
}
|
|
155
|
+
if (!isBooleanOption(name, globals.schema)) return candidates
|
|
116
156
|
}
|
|
117
|
-
if (!isBooleanOption(name, leaf.options)) return candidates
|
|
118
157
|
}
|
|
119
158
|
}
|
|
120
159
|
}
|
package/src/Formatter.test.ts
CHANGED
|
@@ -62,6 +62,15 @@ describe('format', () => {
|
|
|
62
62
|
`)
|
|
63
63
|
})
|
|
64
64
|
|
|
65
|
+
test('formats bigint values as strings in JSON', () => {
|
|
66
|
+
const result = Formatter.format({ expiry: 2461152330n }, 'json')
|
|
67
|
+
expect(result).toMatchInlineSnapshot(`
|
|
68
|
+
"{
|
|
69
|
+
"expiry": "2461152330"
|
|
70
|
+
}"
|
|
71
|
+
`)
|
|
72
|
+
})
|
|
73
|
+
|
|
65
74
|
test('formats as YAML', () => {
|
|
66
75
|
const result = Formatter.format({ message: 'hello' }, 'yaml')
|
|
67
76
|
expect(result).toMatchInlineSnapshot(`
|
|
@@ -319,6 +328,14 @@ describe('format jsonl', () => {
|
|
|
319
328
|
`)
|
|
320
329
|
})
|
|
321
330
|
|
|
331
|
+
test('array outputs bigint values as strings', () => {
|
|
332
|
+
const result = Formatter.format([{ expiry: 2461152330n }, { expiry: 2461152331n }], 'jsonl')
|
|
333
|
+
expect(result).toMatchInlineSnapshot(`
|
|
334
|
+
"{"expiry":"2461152330"}
|
|
335
|
+
{"expiry":"2461152331"}"
|
|
336
|
+
`)
|
|
337
|
+
})
|
|
338
|
+
|
|
322
339
|
test('scalar value outputs JSON', () => {
|
|
323
340
|
expect(Formatter.format(42, 'jsonl')).toMatchInlineSnapshot(`"42"`)
|
|
324
341
|
expect(Formatter.format('hi', 'jsonl')).toMatchInlineSnapshot(`""hi""`)
|
package/src/Formatter.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { encode } from '@toon-format/toon'
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
import * as Json from './internal/json.js'
|
|
4
|
+
import * as Yaml from './internal/yaml.js'
|
|
3
5
|
|
|
4
6
|
/** Supported output formats. */
|
|
5
7
|
export type Format = 'toon' | 'json' | 'yaml' | 'md' | 'jsonl'
|
|
@@ -16,13 +18,13 @@ export function format(value: unknown, fmt: Format = 'toon'): string {
|
|
|
16
18
|
} catch {}
|
|
17
19
|
}
|
|
18
20
|
}
|
|
19
|
-
return
|
|
21
|
+
return Json.stringify(value, 2)
|
|
20
22
|
}
|
|
21
|
-
if (fmt === 'yaml') return
|
|
23
|
+
if (fmt === 'yaml') return Yaml.loadSync().stringify(value)
|
|
22
24
|
if (fmt === 'md') return formatMarkdown(value)
|
|
23
25
|
if (fmt === 'jsonl') {
|
|
24
|
-
if (Array.isArray(value)) return value.map((v) =>
|
|
25
|
-
return
|
|
26
|
+
if (Array.isArray(value)) return value.map((v) => Json.stringify(v)).join('\n')
|
|
27
|
+
return Json.stringify(value)
|
|
26
28
|
}
|
|
27
29
|
// toon (default)
|
|
28
30
|
if (isScalar(value)) return String(value)
|
package/src/Help.test.ts
CHANGED
|
@@ -411,4 +411,35 @@ describe('formatRoot', () => {
|
|
|
411
411
|
--version Show version"
|
|
412
412
|
`)
|
|
413
413
|
})
|
|
414
|
+
|
|
415
|
+
test('formatCommand shows custom global options with deprecated flag', () => {
|
|
416
|
+
const result = Help.formatCommand('tool deploy', {
|
|
417
|
+
description: 'Deploy app',
|
|
418
|
+
globals: {
|
|
419
|
+
schema: z.object({
|
|
420
|
+
rpcUrl: z.string().optional().describe('RPC endpoint URL'),
|
|
421
|
+
oldRpc: z.string().optional().describe('Old RPC endpoint').meta({ deprecated: true }),
|
|
422
|
+
}),
|
|
423
|
+
alias: { rpcUrl: 'r' },
|
|
424
|
+
},
|
|
425
|
+
})
|
|
426
|
+
expect(result).toContain('Custom Global Options:')
|
|
427
|
+
expect(result).toContain('--rpc-url, -r <string>')
|
|
428
|
+
expect(result).toContain('RPC endpoint URL')
|
|
429
|
+
expect(result).toContain('[deprecated] Old RPC endpoint')
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
test('formatRoot shows custom global options', () => {
|
|
433
|
+
const result = Help.formatRoot('tool', {
|
|
434
|
+
globals: {
|
|
435
|
+
schema: z.object({
|
|
436
|
+
chain: z.string().default('mainnet').describe('Target chain'),
|
|
437
|
+
}),
|
|
438
|
+
},
|
|
439
|
+
commands: [{ name: 'deploy', description: 'Deploy' }],
|
|
440
|
+
})
|
|
441
|
+
expect(result).toContain('Custom Global Options:')
|
|
442
|
+
expect(result).toContain('--chain <string>')
|
|
443
|
+
expect(result).toContain('Target chain (default: mainnet)')
|
|
444
|
+
})
|
|
414
445
|
})
|
package/src/Help.ts
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
|
|
3
|
+
import type { GlobalsDescriptor } from './Cli.js'
|
|
3
4
|
import { builtinCommands } from './internal/command.js'
|
|
4
5
|
import { toKebab } from './internal/helpers.js'
|
|
5
6
|
import { defaultEnvSource } from './Parser.js'
|
|
6
7
|
|
|
7
8
|
/** Formats help text for a router CLI or command group. */
|
|
8
9
|
export function formatRoot(name: string, options: formatRoot.Options = {}): string {
|
|
9
|
-
const {
|
|
10
|
+
const {
|
|
11
|
+
aliases,
|
|
12
|
+
configFlag,
|
|
13
|
+
description,
|
|
14
|
+
globals,
|
|
15
|
+
version,
|
|
16
|
+
commands = [],
|
|
17
|
+
root = false,
|
|
18
|
+
} = options
|
|
10
19
|
const lines: string[] = []
|
|
11
20
|
|
|
12
21
|
// Header
|
|
@@ -31,7 +40,7 @@ export function formatRoot(name: string, options: formatRoot.Options = {}): stri
|
|
|
31
40
|
}
|
|
32
41
|
}
|
|
33
42
|
|
|
34
|
-
lines.push(...globalOptionsLines(root, configFlag))
|
|
43
|
+
lines.push(...globalOptionsLines(root, configFlag, globals))
|
|
35
44
|
|
|
36
45
|
return lines.join('\n')
|
|
37
46
|
}
|
|
@@ -46,6 +55,8 @@ export declare namespace formatRoot {
|
|
|
46
55
|
commands?: { name: string; description?: string | undefined }[] | undefined
|
|
47
56
|
/** A short description of the CLI or group. */
|
|
48
57
|
description?: string | undefined
|
|
58
|
+
/** Custom global options schema and alias map. */
|
|
59
|
+
globals?: GlobalsDescriptor | undefined
|
|
49
60
|
/** Show root-level built-in commands and flags. */
|
|
50
61
|
root?: boolean | undefined
|
|
51
62
|
/** CLI version string. */
|
|
@@ -71,6 +82,8 @@ export declare namespace formatCommand {
|
|
|
71
82
|
env?: z.ZodObject<any> | undefined
|
|
72
83
|
/** Override environment variable source for "set:" display. Defaults to `process.env`. */
|
|
73
84
|
envSource?: Record<string, string | undefined> | undefined
|
|
85
|
+
/** Custom global options schema and alias map. */
|
|
86
|
+
globals?: GlobalsDescriptor | undefined
|
|
74
87
|
/** Formatted usage examples. */
|
|
75
88
|
examples?: { command: string; description?: string }[] | undefined
|
|
76
89
|
/** Plain text hint displayed after examples and before global options. */
|
|
@@ -102,6 +115,7 @@ export function formatCommand(name: string, options: formatCommand.Options = {})
|
|
|
102
115
|
aliases,
|
|
103
116
|
configFlag,
|
|
104
117
|
description,
|
|
118
|
+
globals,
|
|
105
119
|
version,
|
|
106
120
|
args,
|
|
107
121
|
env,
|
|
@@ -206,7 +220,7 @@ export function formatCommand(name: string, options: formatCommand.Options = {})
|
|
|
206
220
|
}
|
|
207
221
|
}
|
|
208
222
|
|
|
209
|
-
if (!options.hideGlobalOptions) lines.push(...globalOptionsLines(root, configFlag))
|
|
223
|
+
if (!options.hideGlobalOptions) lines.push(...globalOptionsLines(root, configFlag, globals))
|
|
210
224
|
|
|
211
225
|
// Environment Variables
|
|
212
226
|
if (env) {
|
|
@@ -340,7 +354,11 @@ function extractDeprecated(schema: unknown): boolean | undefined {
|
|
|
340
354
|
}
|
|
341
355
|
|
|
342
356
|
/** Renders the built-in commands and global options block. Root-only items are hidden for subcommands. */
|
|
343
|
-
function globalOptionsLines(
|
|
357
|
+
function globalOptionsLines(
|
|
358
|
+
root = false,
|
|
359
|
+
configFlag?: string,
|
|
360
|
+
globals?: GlobalsDescriptor,
|
|
361
|
+
): string[] {
|
|
344
362
|
const lines: string[] = []
|
|
345
363
|
|
|
346
364
|
if (root) {
|
|
@@ -361,6 +379,26 @@ function globalOptionsLines(root = false, configFlag?: string): string[] {
|
|
|
361
379
|
)
|
|
362
380
|
}
|
|
363
381
|
|
|
382
|
+
if (globals) {
|
|
383
|
+
const entries = optionEntries(globals.schema, globals.alias)
|
|
384
|
+
if (entries.length > 0) {
|
|
385
|
+
const maxLen = Math.max(...entries.map((e) => e.flag.length))
|
|
386
|
+
lines.push(
|
|
387
|
+
'',
|
|
388
|
+
'Custom Global Options:',
|
|
389
|
+
...entries.map((entry) => {
|
|
390
|
+
const padding = ' '.repeat(maxLen - entry.flag.length)
|
|
391
|
+
const prefix = entry.deprecated ? '[deprecated] ' : ''
|
|
392
|
+
const desc =
|
|
393
|
+
entry.defaultValue !== undefined
|
|
394
|
+
? `${prefix}${entry.description} (default: ${entry.defaultValue})`
|
|
395
|
+
: `${prefix}${entry.description}`
|
|
396
|
+
return ` ${entry.flag}${padding} ${desc}`
|
|
397
|
+
}),
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
364
402
|
const flags = [
|
|
365
403
|
...(configFlag
|
|
366
404
|
? [{ flag: `--${configFlag} <path>`, desc: 'Load JSON option defaults from a file' }]
|
package/src/Mcp.test.ts
CHANGED
|
@@ -165,6 +165,18 @@ describe('Mcp', () => {
|
|
|
165
165
|
expect(res.result.content).toEqual([{ type: 'text', text: '{"result":"HELLO"}' }])
|
|
166
166
|
})
|
|
167
167
|
|
|
168
|
+
test('tools/call validation error includes fieldErrors', async () => {
|
|
169
|
+
const tool = Mcp.collectTools(createTestCommands(), []).find((tool) => tool.name === 'echo')!
|
|
170
|
+
const result = await Mcp.callTool(tool, { message: 123 })
|
|
171
|
+
expect(result.isError).toBe(true)
|
|
172
|
+
const [content] = result.content
|
|
173
|
+
expect(content).toBeDefined()
|
|
174
|
+
expect(JSON.parse(content!.text)).toMatchObject({
|
|
175
|
+
code: 'VALIDATION_ERROR',
|
|
176
|
+
fieldErrors: [{ code: 'invalid_type', missing: false, path: 'message' }],
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
|
|
168
180
|
test('tools/call with nested group command', async () => {
|
|
169
181
|
const [, res] = await mcpSession(createTestCommands(), [
|
|
170
182
|
{ id: 1, method: 'initialize', params: initParams },
|
|
@@ -177,6 +189,106 @@ describe('Mcp', () => {
|
|
|
177
189
|
expect(res.result.content).toEqual([{ type: 'text', text: '{"greeting":"hello world"}' }])
|
|
178
190
|
})
|
|
179
191
|
|
|
192
|
+
test('tools/call serializes bigint values as strings', async () => {
|
|
193
|
+
const commands = new Map<string, any>()
|
|
194
|
+
commands.set('whois', {
|
|
195
|
+
description: 'Return ENS data',
|
|
196
|
+
output: z.object({ expiry: z.bigint() }),
|
|
197
|
+
run() {
|
|
198
|
+
return { expiry: 2461152330n }
|
|
199
|
+
},
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
const [, res] = await mcpSession(commands, [
|
|
203
|
+
{ id: 1, method: 'initialize', params: initParams },
|
|
204
|
+
{ id: 2, method: 'tools/call', params: { name: 'whois', arguments: {} } },
|
|
205
|
+
])
|
|
206
|
+
expect(res.result.isError).toBeUndefined()
|
|
207
|
+
expect(res.result.content).toEqual([{ type: 'text', text: '{"expiry":"2461152330"}' }])
|
|
208
|
+
expect(res.result.structuredContent).toEqual({ expiry: '2461152330' })
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
test('tools/call surfaces cta metadata without changing structured content', async () => {
|
|
212
|
+
const commands = new Map<string, any>()
|
|
213
|
+
commands.set('show', {
|
|
214
|
+
description: 'Show a record',
|
|
215
|
+
output: z.object({ id: z.string() }),
|
|
216
|
+
run(c: any) {
|
|
217
|
+
return c.ok(
|
|
218
|
+
{ id: 'foo' },
|
|
219
|
+
{
|
|
220
|
+
cta: {
|
|
221
|
+
description: 'Next:',
|
|
222
|
+
commands: [{ command: 'list', description: 'List all' }],
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
)
|
|
226
|
+
},
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
const [, res] = await mcpSession(commands, [
|
|
230
|
+
{ id: 1, method: 'initialize', params: initParams },
|
|
231
|
+
{ id: 2, method: 'tools/call', params: { name: 'show', arguments: {} } },
|
|
232
|
+
])
|
|
233
|
+
|
|
234
|
+
expect(res.result.content).toEqual([{ type: 'text', text: '{"id":"foo"}' }])
|
|
235
|
+
expect(res.result.structuredContent).toEqual({ id: 'foo' })
|
|
236
|
+
expect(res.result._meta?.cta).toEqual({
|
|
237
|
+
description: 'Next:',
|
|
238
|
+
commands: [{ command: 'test-cli list', description: 'List all' }],
|
|
239
|
+
})
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
test('callTool serializes bigint values as strings', async () => {
|
|
243
|
+
const result = await Mcp.callTool(
|
|
244
|
+
{
|
|
245
|
+
name: 'whois',
|
|
246
|
+
inputSchema: { type: 'object', properties: {} },
|
|
247
|
+
outputSchema: {
|
|
248
|
+
type: 'object',
|
|
249
|
+
properties: { expiry: { type: 'string' } },
|
|
250
|
+
required: ['expiry'],
|
|
251
|
+
},
|
|
252
|
+
command: {
|
|
253
|
+
run() {
|
|
254
|
+
return { expiry: 2461152330n }
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
{},
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
expect(result.isError).toBeUndefined()
|
|
262
|
+
expect(result.content).toEqual([{ type: 'text', text: '{"expiry":"2461152330"}' }])
|
|
263
|
+
expect(result.structuredContent).toEqual({ expiry: '2461152330' })
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
test('callTool serializes streamed bigint chunks as strings', async () => {
|
|
267
|
+
const notifications: any[] = []
|
|
268
|
+
const result = await Mcp.callTool(
|
|
269
|
+
{
|
|
270
|
+
name: 'whois',
|
|
271
|
+
inputSchema: { type: 'object', properties: {} },
|
|
272
|
+
command: {
|
|
273
|
+
async *run() {
|
|
274
|
+
yield { expiry: 2461152330n }
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
{},
|
|
279
|
+
{
|
|
280
|
+
extra: { mcpReq: { _meta: { progressToken: 'tok-1' } } },
|
|
281
|
+
sendNotification: async (notification) => {
|
|
282
|
+
notifications.push(notification)
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
expect(result.isError).toBeUndefined()
|
|
288
|
+
expect(result.content).toEqual([{ type: 'text', text: '[{"expiry":"2461152330"}]' }])
|
|
289
|
+
expect(notifications[0].params.message).toBe('{"expiry":"2461152330"}')
|
|
290
|
+
})
|
|
291
|
+
|
|
180
292
|
test('tools/call unknown tool returns error', async () => {
|
|
181
293
|
const [, res] = await mcpSession(createTestCommands(), [
|
|
182
294
|
{ id: 1, method: 'initialize', params: initParams },
|
|
@@ -410,4 +522,85 @@ describe('Mcp', () => {
|
|
|
410
522
|
expect(progress[0].params.progress).toBe(1)
|
|
411
523
|
expect(progress[1].params.progress).toBe(2)
|
|
412
524
|
})
|
|
525
|
+
|
|
526
|
+
test('serve options.instructions appears in initialize response', async () => {
|
|
527
|
+
const input = new PassThrough()
|
|
528
|
+
const output = new PassThrough()
|
|
529
|
+
const chunks: string[] = []
|
|
530
|
+
output.on('data', (chunk) => chunks.push(chunk.toString()))
|
|
531
|
+
|
|
532
|
+
const done = Mcp.serve('test-cli', '1.0.0', createTestCommands(), {
|
|
533
|
+
input,
|
|
534
|
+
output,
|
|
535
|
+
instructions: 'Use this CLI to run test commands.',
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
input.write(
|
|
539
|
+
`${JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: initParams })}\n`,
|
|
540
|
+
)
|
|
541
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
542
|
+
input.end()
|
|
543
|
+
await done
|
|
544
|
+
|
|
545
|
+
const [res] = chunks.map((c) => JSON.parse(c.trim()))
|
|
546
|
+
expect(res.result.instructions).toBe('Use this CLI to run test commands.')
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
test('command mcp.annotations appear in tools/list', async () => {
|
|
550
|
+
const commands = new Map<string, any>()
|
|
551
|
+
commands.set('read-data', {
|
|
552
|
+
description: 'Read some data',
|
|
553
|
+
mcp: { annotations: { readOnlyHint: true, idempotentHint: true } },
|
|
554
|
+
run: () => ({ data: 42 }),
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
const [, res] = await mcpSession(commands, [
|
|
558
|
+
{ id: 1, method: 'initialize', params: initParams },
|
|
559
|
+
{ id: 2, method: 'tools/list', params: {} },
|
|
560
|
+
])
|
|
561
|
+
const tool = res.result.tools.find((t: any) => t.name === 'read-data')
|
|
562
|
+
expect(tool.annotations).toEqual({ readOnlyHint: true, idempotentHint: true })
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
test('command mcp.instructions appear in tools/list as _meta.instructions', async () => {
|
|
566
|
+
const commands = new Map<string, any>()
|
|
567
|
+
commands.set('guided', {
|
|
568
|
+
description: 'A guided command',
|
|
569
|
+
mcp: { instructions: 'Pass a valid JSON payload.' },
|
|
570
|
+
run: () => ({ ok: true }),
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
const [, res] = await mcpSession(commands, [
|
|
574
|
+
{ id: 1, method: 'initialize', params: initParams },
|
|
575
|
+
{ id: 2, method: 'tools/list', params: {} },
|
|
576
|
+
])
|
|
577
|
+
const tool = res.result.tools.find((t: any) => t.name === 'guided')
|
|
578
|
+
expect(tool._meta?.instructions).toBe('Pass a valid JSON payload.')
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
test('collectTools extracts annotations and instructions from entry.mcp', () => {
|
|
582
|
+
const commands = new Map<string, any>()
|
|
583
|
+
commands.set('destroy', {
|
|
584
|
+
description: 'Destructive op',
|
|
585
|
+
mcp: {
|
|
586
|
+
annotations: { destructiveHint: true, openWorldHint: false },
|
|
587
|
+
instructions: 'Only call this in dry-run mode.',
|
|
588
|
+
},
|
|
589
|
+
run: () => null,
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
const tools = Mcp.collectTools(commands, [])
|
|
593
|
+
expect(tools).toHaveLength(1)
|
|
594
|
+
expect(tools[0]?.annotations).toEqual({ destructiveHint: true, openWorldHint: false })
|
|
595
|
+
expect(tools[0]?.instructions).toBe('Only call this in dry-run mode.')
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
test('collectTools omits annotations/instructions when not set', () => {
|
|
599
|
+
const commands = new Map<string, any>()
|
|
600
|
+
commands.set('plain', { description: 'No mcp opts', run: () => null })
|
|
601
|
+
|
|
602
|
+
const tools = Mcp.collectTools(commands, [])
|
|
603
|
+
expect(tools[0]?.annotations).toBeUndefined()
|
|
604
|
+
expect(tools[0]?.instructions).toBeUndefined()
|
|
605
|
+
})
|
|
413
606
|
})
|