incur 0.2.0 → 0.2.2
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/README.md +158 -9
- package/SKILL.md +149 -0
- package/dist/Cli.d.ts +16 -4
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +384 -31
- package/dist/Cli.js.map +1 -1
- package/dist/Errors.d.ts +4 -0
- package/dist/Errors.d.ts.map +1 -1
- package/dist/Errors.js +3 -0
- package/dist/Errors.js.map +1 -1
- package/dist/Filter.d.ts +14 -0
- package/dist/Filter.d.ts.map +1 -0
- package/dist/Filter.js +134 -0
- package/dist/Filter.js.map +1 -0
- package/dist/Help.js +5 -0
- package/dist/Help.js.map +1 -1
- package/dist/Mcp.d.ts +26 -0
- package/dist/Mcp.d.ts.map +1 -1
- package/dist/Mcp.js +2 -2
- package/dist/Mcp.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts +8 -2
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js.map +1 -1
- package/package.json +2 -1
- package/src/Cli.test-d.ts +25 -0
- package/src/Cli.test.ts +829 -0
- package/src/Cli.ts +492 -37
- package/src/Errors.ts +5 -0
- package/src/Filter.test.ts +237 -0
- package/src/Filter.ts +139 -0
- package/src/Help.test.ts +35 -0
- package/src/Help.ts +5 -0
- package/src/Mcp.ts +3 -3
- package/src/e2e.test.ts +715 -0
- package/src/index.ts +1 -0
- package/src/middleware.ts +9 -2
package/src/Errors.ts
CHANGED
|
@@ -41,12 +41,15 @@ export class IncurError extends BaseError {
|
|
|
41
41
|
hint: string | undefined
|
|
42
42
|
/** Whether the operation can be retried. */
|
|
43
43
|
retryable: boolean
|
|
44
|
+
/** Process exit code. When set, `serve()` uses this instead of `1`. */
|
|
45
|
+
exitCode: number | undefined
|
|
44
46
|
|
|
45
47
|
constructor(options: IncurError.Options) {
|
|
46
48
|
super(options.message, options.cause ? { cause: options.cause } : undefined)
|
|
47
49
|
this.code = options.code
|
|
48
50
|
this.hint = options.hint
|
|
49
51
|
this.retryable = options.retryable ?? false
|
|
52
|
+
this.exitCode = options.exitCode
|
|
50
53
|
}
|
|
51
54
|
}
|
|
52
55
|
|
|
@@ -61,6 +64,8 @@ export declare namespace IncurError {
|
|
|
61
64
|
hint?: string | undefined
|
|
62
65
|
/** Whether the operation can be retried. Defaults to `false`. */
|
|
63
66
|
retryable?: boolean | undefined
|
|
67
|
+
/** Process exit code. When set, `serve()` uses this instead of `1`. */
|
|
68
|
+
exitCode?: number | undefined
|
|
64
69
|
/** The underlying cause. */
|
|
65
70
|
cause?: Error | undefined
|
|
66
71
|
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { Filter } from 'incur'
|
|
2
|
+
|
|
3
|
+
describe('parse', () => {
|
|
4
|
+
test('single key', () => {
|
|
5
|
+
expect(Filter.parse('foo')).toMatchInlineSnapshot(`
|
|
6
|
+
[
|
|
7
|
+
[
|
|
8
|
+
{
|
|
9
|
+
"key": "foo",
|
|
10
|
+
},
|
|
11
|
+
],
|
|
12
|
+
]
|
|
13
|
+
`)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('dot notation', () => {
|
|
17
|
+
expect(Filter.parse('bar.baz')).toMatchInlineSnapshot(`
|
|
18
|
+
[
|
|
19
|
+
[
|
|
20
|
+
{
|
|
21
|
+
"key": "bar",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"key": "baz",
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
]
|
|
28
|
+
`)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('slice notation', () => {
|
|
32
|
+
expect(Filter.parse('items[0,3]')).toMatchInlineSnapshot(`
|
|
33
|
+
[
|
|
34
|
+
[
|
|
35
|
+
{
|
|
36
|
+
"key": "items",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"end": 3,
|
|
40
|
+
"start": 0,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
]
|
|
44
|
+
`)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('mixed keys and slices', () => {
|
|
48
|
+
expect(Filter.parse('a.b.c[0,10]')).toMatchInlineSnapshot(`
|
|
49
|
+
[
|
|
50
|
+
[
|
|
51
|
+
{
|
|
52
|
+
"key": "a",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"key": "b",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"key": "c",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"end": 10,
|
|
62
|
+
"start": 0,
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
]
|
|
66
|
+
`)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('multiple paths', () => {
|
|
70
|
+
expect(Filter.parse('name,age')).toMatchInlineSnapshot(`
|
|
71
|
+
[
|
|
72
|
+
[
|
|
73
|
+
{
|
|
74
|
+
"key": "name",
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
[
|
|
78
|
+
{
|
|
79
|
+
"key": "age",
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
]
|
|
83
|
+
`)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('slice followed by dot path', () => {
|
|
87
|
+
expect(Filter.parse('items[0,3].name')).toMatchInlineSnapshot(`
|
|
88
|
+
[
|
|
89
|
+
[
|
|
90
|
+
{
|
|
91
|
+
"key": "items",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"end": 3,
|
|
95
|
+
"start": 0,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"key": "name",
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
]
|
|
102
|
+
`)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('comma inside slice is not a separator', () => {
|
|
106
|
+
expect(Filter.parse('foo,items[0,3],bar')).toMatchInlineSnapshot(`
|
|
107
|
+
[
|
|
108
|
+
[
|
|
109
|
+
{
|
|
110
|
+
"key": "foo",
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
[
|
|
114
|
+
{
|
|
115
|
+
"key": "items",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"end": 3,
|
|
119
|
+
"start": 0,
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
[
|
|
123
|
+
{
|
|
124
|
+
"key": "bar",
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
]
|
|
128
|
+
`)
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
describe('apply', () => {
|
|
133
|
+
test('selects single top-level key', () => {
|
|
134
|
+
const data = { name: 'alice', age: 30, email: 'alice@example.com' }
|
|
135
|
+
expect(Filter.apply(data, Filter.parse('name'))).toMatchInlineSnapshot(`"alice"`)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('selects nested key with dot notation', () => {
|
|
139
|
+
const data = { user: { name: 'alice', email: 'alice@example.com' }, status: 'active' }
|
|
140
|
+
expect(Filter.apply(data, Filter.parse('user.name'))).toMatchInlineSnapshot(`
|
|
141
|
+
{
|
|
142
|
+
"user": {
|
|
143
|
+
"name": "alice",
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
`)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test('slices array', () => {
|
|
150
|
+
const data = { items: [1, 2, 3, 4, 5] }
|
|
151
|
+
expect(Filter.apply(data, Filter.parse('items[0,3]'))).toMatchInlineSnapshot(`
|
|
152
|
+
{
|
|
153
|
+
"items": [
|
|
154
|
+
1,
|
|
155
|
+
2,
|
|
156
|
+
3,
|
|
157
|
+
],
|
|
158
|
+
}
|
|
159
|
+
`)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
test('selects nested field after slice', () => {
|
|
163
|
+
const data = {
|
|
164
|
+
users: [
|
|
165
|
+
{ name: 'alice', age: 30 },
|
|
166
|
+
{ name: 'bob', age: 25 },
|
|
167
|
+
{ name: 'charlie', age: 35 },
|
|
168
|
+
],
|
|
169
|
+
}
|
|
170
|
+
expect(Filter.apply(data, Filter.parse('users[0,2].name'))).toMatchInlineSnapshot(`
|
|
171
|
+
{
|
|
172
|
+
"users": [
|
|
173
|
+
{
|
|
174
|
+
"name": "alice",
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"name": "bob",
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
}
|
|
181
|
+
`)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test('multiple filter paths merged', () => {
|
|
185
|
+
const data = { name: 'alice', age: 30, email: 'alice@example.com' }
|
|
186
|
+
expect(Filter.apply(data, Filter.parse('name,age'))).toMatchInlineSnapshot(`
|
|
187
|
+
{
|
|
188
|
+
"age": 30,
|
|
189
|
+
"name": "alice",
|
|
190
|
+
}
|
|
191
|
+
`)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
test('returns scalar directly for single key selection', () => {
|
|
195
|
+
const data = { message: 'hello world', status: 'ok' }
|
|
196
|
+
const result = Filter.apply(data, Filter.parse('message'))
|
|
197
|
+
expect(result).toBe('hello world')
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
test('returns object for single key with object value', () => {
|
|
201
|
+
const data = { user: { name: 'alice' }, status: 'ok' }
|
|
202
|
+
expect(Filter.apply(data, Filter.parse('user'))).toMatchInlineSnapshot(`
|
|
203
|
+
{
|
|
204
|
+
"user": {
|
|
205
|
+
"name": "alice",
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
`)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
test('returns undefined for missing key', () => {
|
|
212
|
+
const data = { name: 'alice' }
|
|
213
|
+
expect(Filter.apply(data, Filter.parse('missing'))).toMatchInlineSnapshot(`undefined`)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
test('applies filter to each element when data is array', () => {
|
|
217
|
+
const data = [
|
|
218
|
+
{ name: 'alice', age: 30 },
|
|
219
|
+
{ name: 'bob', age: 25 },
|
|
220
|
+
]
|
|
221
|
+
expect(Filter.apply(data, Filter.parse('name'))).toMatchInlineSnapshot(`
|
|
222
|
+
[
|
|
223
|
+
"alice",
|
|
224
|
+
"bob",
|
|
225
|
+
]
|
|
226
|
+
`)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
test('empty paths returns data unchanged', () => {
|
|
230
|
+
const data = { foo: 'bar' }
|
|
231
|
+
expect(Filter.apply(data, [])).toMatchInlineSnapshot(`
|
|
232
|
+
{
|
|
233
|
+
"foo": "bar",
|
|
234
|
+
}
|
|
235
|
+
`)
|
|
236
|
+
})
|
|
237
|
+
})
|
package/src/Filter.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/** A single segment in a filter path: either a string key or an array slice. */
|
|
2
|
+
export type Segment = { key: string } | { start: number; end: number }
|
|
3
|
+
|
|
4
|
+
/** A filter path is an ordered list of segments to traverse. */
|
|
5
|
+
export type FilterPath = Segment[]
|
|
6
|
+
|
|
7
|
+
/** Parses a filter expression string into structured filter paths. */
|
|
8
|
+
export function parse(expression: string): FilterPath[] {
|
|
9
|
+
const paths: FilterPath[] = []
|
|
10
|
+
const tokens: string[] = []
|
|
11
|
+
let current = ''
|
|
12
|
+
let depth = 0
|
|
13
|
+
|
|
14
|
+
// Split on commas, but commas inside [...] are part of a slice
|
|
15
|
+
for (let i = 0; i < expression.length; i++) {
|
|
16
|
+
const ch = expression[i]!
|
|
17
|
+
if (ch === '[') depth++
|
|
18
|
+
else if (ch === ']') depth--
|
|
19
|
+
|
|
20
|
+
if (ch === ',' && depth === 0) {
|
|
21
|
+
tokens.push(current)
|
|
22
|
+
current = ''
|
|
23
|
+
} else current += ch
|
|
24
|
+
}
|
|
25
|
+
if (current) tokens.push(current)
|
|
26
|
+
|
|
27
|
+
for (const token of tokens) {
|
|
28
|
+
const path: FilterPath = []
|
|
29
|
+
let remaining = token
|
|
30
|
+
|
|
31
|
+
while (remaining.length > 0) {
|
|
32
|
+
const bracketIdx = remaining.indexOf('[')
|
|
33
|
+
|
|
34
|
+
if (bracketIdx === -1) {
|
|
35
|
+
// No more slices — split remaining by dots
|
|
36
|
+
for (const part of remaining.split('.')) if (part) path.push({ key: part })
|
|
37
|
+
break
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Parse dot-separated keys before the bracket
|
|
41
|
+
const before = remaining.slice(0, bracketIdx)
|
|
42
|
+
for (const part of before.split('.')) if (part) path.push({ key: part })
|
|
43
|
+
|
|
44
|
+
// Parse the slice [start,end]
|
|
45
|
+
const closeBracket = remaining.indexOf(']', bracketIdx)
|
|
46
|
+
const inner = remaining.slice(bracketIdx + 1, closeBracket)
|
|
47
|
+
const [startStr, endStr] = inner.split(',')
|
|
48
|
+
path.push({ start: Number(startStr), end: Number(endStr) })
|
|
49
|
+
|
|
50
|
+
remaining = remaining.slice(closeBracket + 1)
|
|
51
|
+
if (remaining.startsWith('.')) remaining = remaining.slice(1)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
paths.push(path)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return paths
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Applies parsed filter paths to a data value, returning a filtered copy. */
|
|
61
|
+
export function apply(data: unknown, paths: FilterPath[]): unknown {
|
|
62
|
+
if (paths.length === 0) return data
|
|
63
|
+
|
|
64
|
+
// Single key selecting a scalar → return the scalar directly
|
|
65
|
+
if (paths.length === 1 && paths[0]!.length === 1 && 'key' in paths[0]![0]!) {
|
|
66
|
+
const key = paths[0]![0]!.key
|
|
67
|
+
if (Array.isArray(data)) return data.map((item) => apply(item, paths))
|
|
68
|
+
if (typeof data === 'object' && data !== null) {
|
|
69
|
+
const val = (data as Record<string, unknown>)[key]
|
|
70
|
+
if (typeof val !== 'object' || val === null) return val
|
|
71
|
+
return { [key]: val }
|
|
72
|
+
}
|
|
73
|
+
return undefined
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (Array.isArray(data)) return data.map((item) => apply(item, paths))
|
|
77
|
+
|
|
78
|
+
const result: Record<string, unknown> = {}
|
|
79
|
+
for (const path of paths) merge(result, data, path, 0)
|
|
80
|
+
return result
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resolve(data: unknown, segments: Segment[], index: number): unknown {
|
|
84
|
+
if (index >= segments.length) return data
|
|
85
|
+
const segment = segments[index]!
|
|
86
|
+
|
|
87
|
+
if ('key' in segment) {
|
|
88
|
+
if (typeof data !== 'object' || data === null) return undefined
|
|
89
|
+
const val = (data as Record<string, unknown>)[segment.key]
|
|
90
|
+
return resolve(val, segments, index + 1)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// slice segment
|
|
94
|
+
if (!Array.isArray(data)) return undefined
|
|
95
|
+
const sliced = data.slice(segment.start, segment.end)
|
|
96
|
+
if (index + 1 >= segments.length) return sliced
|
|
97
|
+
return sliced.map((item) => resolve(item, segments, index + 1))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function merge(target: Record<string, unknown>, data: unknown, segments: Segment[], index: number): void {
|
|
101
|
+
if (index >= segments.length || typeof data !== 'object' || data === null) return
|
|
102
|
+
const segment = segments[index]!
|
|
103
|
+
|
|
104
|
+
if ('key' in segment) {
|
|
105
|
+
const val = (data as Record<string, unknown>)[segment.key]
|
|
106
|
+
if (val === undefined) return
|
|
107
|
+
|
|
108
|
+
if (index + 1 >= segments.length) {
|
|
109
|
+
target[segment.key] = val
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const next = segments[index + 1]!
|
|
114
|
+
if ('start' in next) {
|
|
115
|
+
// Next segment is a slice
|
|
116
|
+
if (!Array.isArray(val)) return
|
|
117
|
+
const sliced = val.slice(next.start, next.end)
|
|
118
|
+
if (index + 2 >= segments.length) {
|
|
119
|
+
target[segment.key] = sliced
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
target[segment.key] = sliced.map((item) => {
|
|
123
|
+
const sub: Record<string, unknown> = {}
|
|
124
|
+
merge(sub, item, segments, index + 2)
|
|
125
|
+
return sub
|
|
126
|
+
})
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Next segment is a key — recurse into nested object
|
|
131
|
+
if (typeof val !== 'object' || val === null) return
|
|
132
|
+
if (!target[segment.key] || typeof target[segment.key] !== 'object')
|
|
133
|
+
target[segment.key] = {}
|
|
134
|
+
merge(target[segment.key] as Record<string, unknown>, val, segments, index + 1)
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// slice at root level — shouldn't happen in merge (merge starts from object keys)
|
|
139
|
+
}
|
package/src/Help.test.ts
CHANGED
|
@@ -25,9 +25,14 @@ describe('formatCommand', () => {
|
|
|
25
25
|
--limit <number> Max PRs to return (default: 30)
|
|
26
26
|
|
|
27
27
|
Global Options:
|
|
28
|
+
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
28
29
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
29
30
|
--help Show help
|
|
30
31
|
--llms Print LLM-readable manifest
|
|
32
|
+
--schema Show JSON Schema for a command
|
|
33
|
+
--token-count Print token count of output (instead of output)
|
|
34
|
+
--token-limit <n> Limit output to n tokens
|
|
35
|
+
--token-offset <n> Skip first n tokens of output
|
|
31
36
|
--verbose Show full output envelope"
|
|
32
37
|
`)
|
|
33
38
|
})
|
|
@@ -42,9 +47,14 @@ describe('formatCommand', () => {
|
|
|
42
47
|
Usage: tool ping
|
|
43
48
|
|
|
44
49
|
Global Options:
|
|
50
|
+
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
45
51
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
46
52
|
--help Show help
|
|
47
53
|
--llms Print LLM-readable manifest
|
|
54
|
+
--schema Show JSON Schema for a command
|
|
55
|
+
--token-count Print token count of output (instead of output)
|
|
56
|
+
--token-limit <n> Limit output to n tokens
|
|
57
|
+
--token-offset <n> Skip first n tokens of output
|
|
48
58
|
--verbose Show full output envelope"
|
|
49
59
|
`)
|
|
50
60
|
})
|
|
@@ -66,9 +76,14 @@ describe('formatCommand', () => {
|
|
|
66
76
|
title Title
|
|
67
77
|
|
|
68
78
|
Global Options:
|
|
79
|
+
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
69
80
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
70
81
|
--help Show help
|
|
71
82
|
--llms Print LLM-readable manifest
|
|
83
|
+
--schema Show JSON Schema for a command
|
|
84
|
+
--token-count Print token count of output (instead of output)
|
|
85
|
+
--token-limit <n> Limit output to n tokens
|
|
86
|
+
--token-offset <n> Skip first n tokens of output
|
|
72
87
|
--verbose Show full output envelope"
|
|
73
88
|
`)
|
|
74
89
|
})
|
|
@@ -107,9 +122,14 @@ describe('formatRoot', () => {
|
|
|
107
122
|
issue list List issues
|
|
108
123
|
|
|
109
124
|
Global Options:
|
|
125
|
+
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
110
126
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
111
127
|
--help Show help
|
|
112
128
|
--llms Print LLM-readable manifest
|
|
129
|
+
--schema Show JSON Schema for a command
|
|
130
|
+
--token-count Print token count of output (instead of output)
|
|
131
|
+
--token-limit <n> Limit output to n tokens
|
|
132
|
+
--token-offset <n> Skip first n tokens of output
|
|
113
133
|
--verbose Show full output envelope"
|
|
114
134
|
`)
|
|
115
135
|
})
|
|
@@ -127,9 +147,14 @@ describe('formatRoot', () => {
|
|
|
127
147
|
ping Health check
|
|
128
148
|
|
|
129
149
|
Global Options:
|
|
150
|
+
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
130
151
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
131
152
|
--help Show help
|
|
132
153
|
--llms Print LLM-readable manifest
|
|
154
|
+
--schema Show JSON Schema for a command
|
|
155
|
+
--token-count Print token count of output (instead of output)
|
|
156
|
+
--token-limit <n> Limit output to n tokens
|
|
157
|
+
--token-offset <n> Skip first n tokens of output
|
|
133
158
|
--verbose Show full output envelope"
|
|
134
159
|
`)
|
|
135
160
|
})
|
|
@@ -151,9 +176,14 @@ describe('formatRoot', () => {
|
|
|
151
176
|
fetch Fetch a URL
|
|
152
177
|
|
|
153
178
|
Global Options:
|
|
179
|
+
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
154
180
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
155
181
|
--help Show help
|
|
156
182
|
--llms Print LLM-readable manifest
|
|
183
|
+
--schema Show JSON Schema for a command
|
|
184
|
+
--token-count Print token count of output (instead of output)
|
|
185
|
+
--token-limit <n> Limit output to n tokens
|
|
186
|
+
--token-offset <n> Skip first n tokens of output
|
|
157
187
|
--verbose Show full output envelope"
|
|
158
188
|
`)
|
|
159
189
|
})
|
|
@@ -175,9 +205,14 @@ describe('formatRoot', () => {
|
|
|
175
205
|
url URL to fetch
|
|
176
206
|
|
|
177
207
|
Global Options:
|
|
208
|
+
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
178
209
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
179
210
|
--help Show help
|
|
180
211
|
--llms Print LLM-readable manifest
|
|
212
|
+
--schema Show JSON Schema for a command
|
|
213
|
+
--token-count Print token count of output (instead of output)
|
|
214
|
+
--token-limit <n> Limit output to n tokens
|
|
215
|
+
--token-offset <n> Skip first n tokens of output
|
|
181
216
|
--verbose Show full output envelope"
|
|
182
217
|
`)
|
|
183
218
|
})
|
package/src/Help.ts
CHANGED
|
@@ -326,10 +326,15 @@ function globalOptionsLines(root = false): string[] {
|
|
|
326
326
|
}
|
|
327
327
|
|
|
328
328
|
const flags = [
|
|
329
|
+
{ flag: '--filter-output <keys>', desc: 'Filter output by key paths (e.g. foo,bar.baz,a[0,3])' },
|
|
329
330
|
{ flag: '--format <toon|json|yaml|md|jsonl>', desc: 'Output format' },
|
|
330
331
|
{ flag: '--help', desc: 'Show help' },
|
|
331
332
|
{ flag: '--llms', desc: 'Print LLM-readable manifest' },
|
|
332
333
|
...(root ? [{ flag: '--mcp', desc: 'Start as MCP stdio server' }] : []),
|
|
334
|
+
{ flag: '--schema', desc: 'Show JSON Schema for a command' },
|
|
335
|
+
{ flag: '--token-count', desc: 'Print token count of output (instead of output)' },
|
|
336
|
+
{ flag: '--token-limit <n>', desc: 'Limit output to n tokens' },
|
|
337
|
+
{ flag: '--token-offset <n>', desc: 'Skip first n tokens of output' },
|
|
333
338
|
{ flag: '--verbose', desc: 'Show full output envelope' },
|
|
334
339
|
...(root ? [{ flag: '--version', desc: 'Show version' }] : []),
|
|
335
340
|
]
|
package/src/Mcp.ts
CHANGED
|
@@ -52,7 +52,7 @@ export declare namespace serve {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/** @internal Executes a tool call and returns a CallToolResult. */
|
|
55
|
-
async function callTool(
|
|
55
|
+
export async function callTool(
|
|
56
56
|
tool: ToolEntry,
|
|
57
57
|
params: Record<string, unknown>,
|
|
58
58
|
extra?: {
|
|
@@ -135,7 +135,7 @@ function isAsyncGenerator(value: unknown): value is AsyncGenerator<unknown, unkn
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
/** @internal A resolved tool entry from the command tree. */
|
|
138
|
-
type ToolEntry = {
|
|
138
|
+
export type ToolEntry = {
|
|
139
139
|
name: string
|
|
140
140
|
description?: string | undefined
|
|
141
141
|
inputSchema: { type: 'object'; properties: Record<string, unknown>; required?: string[] }
|
|
@@ -143,7 +143,7 @@ type ToolEntry = {
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
/** @internal Recursively collects leaf commands as tool entries. */
|
|
146
|
-
function collectTools(commands: Map<string, any>, prefix: string[]): ToolEntry[] {
|
|
146
|
+
export function collectTools(commands: Map<string, any>, prefix: string[]): ToolEntry[] {
|
|
147
147
|
const result: ToolEntry[] = []
|
|
148
148
|
for (const [name, entry] of commands) {
|
|
149
149
|
const path = [...prefix, name]
|