incur 0.3.2 → 0.3.4
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 +32 -36
- package/SKILL.md +17 -20
- package/dist/Cli.d.ts +1 -1
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +136 -27
- package/dist/Cli.js.map +1 -1
- package/dist/Fetch.d.ts.map +1 -1
- package/dist/Fetch.js.map +1 -1
- package/dist/Filter.js.map +1 -1
- package/dist/Help.js +7 -7
- package/dist/Help.js.map +1 -1
- package/dist/Openapi.d.ts.map +1 -1
- package/dist/Openapi.js +10 -2
- package/dist/Openapi.js.map +1 -1
- package/dist/Skill.d.ts.map +1 -1
- package/dist/Skill.js.map +1 -1
- package/package.json +1 -1
- package/src/Cli.test.ts +28 -39
- package/src/Cli.ts +215 -66
- package/src/Fetch.test.ts +3 -13
- package/src/Fetch.ts +1 -2
- package/src/Filter.ts +7 -3
- package/src/Help.test.ts +9 -9
- package/src/Help.ts +5 -5
- package/src/Openapi.test.ts +89 -32
- package/src/Openapi.ts +15 -6
- package/src/Skill.test.ts +1 -3
- package/src/Skill.ts +9 -2
- package/src/e2e.test.ts +85 -45
package/src/Cli.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
+
import { estimateTokenCount, sliceByTokens } from 'tokenx'
|
|
1
2
|
import type { z } from 'zod'
|
|
2
3
|
|
|
3
|
-
import { estimateTokenCount, sliceByTokens } from 'tokenx'
|
|
4
4
|
import * as Completions from './Completions.js'
|
|
5
|
-
import * as Filter from './Filter.js'
|
|
6
5
|
import type { FieldError } from './Errors.js'
|
|
7
6
|
import { IncurError, ValidationError } from './Errors.js'
|
|
8
7
|
import * as Fetch from './Fetch.js'
|
|
9
|
-
import * as
|
|
8
|
+
import * as Filter from './Filter.js'
|
|
10
9
|
import * as Formatter from './Formatter.js'
|
|
11
10
|
import * as Help from './Help.js'
|
|
12
11
|
import { detectRunner } from './internal/pm.js'
|
|
13
12
|
import type { OneOf } from './internal/types.js'
|
|
14
13
|
import * as Mcp from './Mcp.js'
|
|
15
14
|
import type { Context as MiddlewareContext, Handler as MiddlewareHandler } from './middleware.js'
|
|
15
|
+
import * as Openapi from './Openapi.js'
|
|
16
16
|
export type { MiddlewareHandler }
|
|
17
17
|
import * as Parser from './Parser.js'
|
|
18
18
|
import type { Register } from './Register.js'
|
|
@@ -63,7 +63,13 @@ export type Cli<
|
|
|
63
63
|
/** Mounts a fetch handler as a command, optionally with OpenAPI spec for typed subcommands. */
|
|
64
64
|
<const name extends string>(
|
|
65
65
|
name: name,
|
|
66
|
-
definition: {
|
|
66
|
+
definition: {
|
|
67
|
+
basePath?: string | undefined
|
|
68
|
+
description?: string | undefined
|
|
69
|
+
fetch: FetchHandler
|
|
70
|
+
openapi?: Openapi.OpenAPISpec | undefined
|
|
71
|
+
outputPolicy?: OutputPolicy | undefined
|
|
72
|
+
},
|
|
67
73
|
): Cli<commands, vars, env>
|
|
68
74
|
}
|
|
69
75
|
/** A short description of the CLI. */
|
|
@@ -204,14 +210,16 @@ export function create(
|
|
|
204
210
|
// OpenAPI + fetch → generate typed command group (async, resolved before serve)
|
|
205
211
|
if (def.openapi) {
|
|
206
212
|
pending.push(
|
|
207
|
-
Openapi.generateCommands(def.openapi, def.fetch, { basePath: def.basePath }).then(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
213
|
+
Openapi.generateCommands(def.openapi, def.fetch, { basePath: def.basePath }).then(
|
|
214
|
+
(generated) => {
|
|
215
|
+
commands.set(nameOrCli, {
|
|
216
|
+
_group: true,
|
|
217
|
+
description: def.description,
|
|
218
|
+
commands: generated as Map<string, CommandEntry>,
|
|
219
|
+
...(def.outputPolicy ? { outputPolicy: def.outputPolicy } : undefined),
|
|
220
|
+
} as InternalGroup)
|
|
221
|
+
},
|
|
222
|
+
),
|
|
215
223
|
)
|
|
216
224
|
return cli
|
|
217
225
|
}
|
|
@@ -594,7 +602,7 @@ async function serveImpl(
|
|
|
594
602
|
if (help) {
|
|
595
603
|
writeln(
|
|
596
604
|
[
|
|
597
|
-
`${name} skills add — Sync skill files to
|
|
605
|
+
`${name} skills add — Sync skill files to agents`,
|
|
598
606
|
'',
|
|
599
607
|
`Usage: ${name} skills add [options]`,
|
|
600
608
|
'',
|
|
@@ -670,7 +678,7 @@ async function serveImpl(
|
|
|
670
678
|
if (help) {
|
|
671
679
|
writeln(
|
|
672
680
|
[
|
|
673
|
-
`${name} mcp add — Register as
|
|
681
|
+
`${name} mcp add — Register as MCP server for your agent`,
|
|
674
682
|
'',
|
|
675
683
|
`Usage: ${name} mcp add [options]`,
|
|
676
684
|
'',
|
|
@@ -786,7 +794,16 @@ async function serveImpl(
|
|
|
786
794
|
filtered.length === 0 && options.rootCommand
|
|
787
795
|
? { command: options.rootCommand, path: name, rest: [] as string[] }
|
|
788
796
|
: filtered.length === 0 && options.rootFetch
|
|
789
|
-
? {
|
|
797
|
+
? {
|
|
798
|
+
fetchGateway: {
|
|
799
|
+
_fetch: true as const,
|
|
800
|
+
fetch: options.rootFetch,
|
|
801
|
+
description: options.description,
|
|
802
|
+
},
|
|
803
|
+
middlewares: [] as MiddlewareHandler[],
|
|
804
|
+
path: name,
|
|
805
|
+
rest: [] as string[],
|
|
806
|
+
}
|
|
790
807
|
: resolveCommand(commands, filtered)
|
|
791
808
|
|
|
792
809
|
// --help on a fetch gateway → show fetch-specific help
|
|
@@ -916,7 +933,16 @@ async function serveImpl(
|
|
|
916
933
|
// Fall back to root fetch when no subcommand matches
|
|
917
934
|
const effective =
|
|
918
935
|
'error' in resolved && options.rootFetch && !resolved.path
|
|
919
|
-
? {
|
|
936
|
+
? {
|
|
937
|
+
fetchGateway: {
|
|
938
|
+
_fetch: true as const,
|
|
939
|
+
fetch: options.rootFetch,
|
|
940
|
+
description: options.description,
|
|
941
|
+
},
|
|
942
|
+
middlewares: [] as MiddlewareHandler[],
|
|
943
|
+
path: name,
|
|
944
|
+
rest: filtered,
|
|
945
|
+
}
|
|
920
946
|
: 'error' in resolved && options.rootCommand && !resolved.path
|
|
921
947
|
? { command: options.rootCommand, path: name, rest: filtered }
|
|
922
948
|
: resolved
|
|
@@ -928,7 +954,11 @@ async function serveImpl(
|
|
|
928
954
|
|
|
929
955
|
const filterPaths = filterOutput ? Filter.parse(filterOutput) : undefined
|
|
930
956
|
|
|
931
|
-
function truncate(s: string): {
|
|
957
|
+
function truncate(s: string): {
|
|
958
|
+
text: string
|
|
959
|
+
truncated: boolean
|
|
960
|
+
nextOffset?: number | undefined
|
|
961
|
+
} {
|
|
932
962
|
if (tokenLimit == null && tokenOffset == null) return { text: s, truncated: false }
|
|
933
963
|
const total = estimateTokenCount(s)
|
|
934
964
|
const offset = tokenOffset ?? 0
|
|
@@ -964,11 +994,12 @@ async function serveImpl(
|
|
|
964
994
|
if (verbose) {
|
|
965
995
|
if (tokenLimit != null || tokenOffset != null) {
|
|
966
996
|
// Truncate data separately so meta (including nextOffset) is always visible
|
|
967
|
-
const dataFormatted =
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
997
|
+
const dataFormatted =
|
|
998
|
+
output.ok && output.data != null
|
|
999
|
+
? Formatter.format(output.data, format)
|
|
1000
|
+
: !output.ok
|
|
1001
|
+
? Formatter.format(output.error, format)
|
|
1002
|
+
: ''
|
|
972
1003
|
const t = truncate(dataFormatted)
|
|
973
1004
|
if (t.truncated) {
|
|
974
1005
|
const envelope: Record<string, unknown> = output.ok
|
|
@@ -1070,9 +1101,12 @@ async function serveImpl(
|
|
|
1070
1101
|
ok: false,
|
|
1071
1102
|
error: {
|
|
1072
1103
|
code: `HTTP_${output.status}`,
|
|
1073
|
-
message:
|
|
1074
|
-
|
|
1075
|
-
|
|
1104
|
+
message:
|
|
1105
|
+
typeof output.data === 'object' && output.data !== null && 'message' in output.data
|
|
1106
|
+
? String((output.data as any).message)
|
|
1107
|
+
: typeof output.data === 'string'
|
|
1108
|
+
? output.data
|
|
1109
|
+
: `HTTP ${output.status}`,
|
|
1076
1110
|
},
|
|
1077
1111
|
meta: {
|
|
1078
1112
|
command: path,
|
|
@@ -1084,11 +1118,18 @@ async function serveImpl(
|
|
|
1084
1118
|
}
|
|
1085
1119
|
|
|
1086
1120
|
try {
|
|
1087
|
-
const cliEnv = options.envSchema
|
|
1121
|
+
const cliEnv = options.envSchema
|
|
1122
|
+
? Parser.parseEnv(options.envSchema, options.env ?? process.env)
|
|
1123
|
+
: {}
|
|
1088
1124
|
if (fetchMiddleware.length > 0) {
|
|
1089
1125
|
const varsMap: Record<string, unknown> = options.vars ? options.vars.parse({}) : {}
|
|
1090
|
-
const errorFn = (opts: {
|
|
1091
|
-
|
|
1126
|
+
const errorFn = (opts: {
|
|
1127
|
+
code: string
|
|
1128
|
+
exitCode?: number | undefined
|
|
1129
|
+
message: string
|
|
1130
|
+
retryable?: boolean | undefined
|
|
1131
|
+
cta?: CtaBlock | undefined
|
|
1132
|
+
}): never => ({ [sentinel]: 'error', ...opts }) as never
|
|
1092
1133
|
const mwCtx: MiddlewareContext = {
|
|
1093
1134
|
agent: !human,
|
|
1094
1135
|
command: path,
|
|
@@ -1097,7 +1138,9 @@ async function serveImpl(
|
|
|
1097
1138
|
format,
|
|
1098
1139
|
formatExplicit,
|
|
1099
1140
|
name,
|
|
1100
|
-
set(key: string, value: unknown) {
|
|
1141
|
+
set(key: string, value: unknown) {
|
|
1142
|
+
varsMap[key] = value
|
|
1143
|
+
},
|
|
1101
1144
|
var: varsMap,
|
|
1102
1145
|
version: options.version,
|
|
1103
1146
|
}
|
|
@@ -1107,13 +1150,23 @@ async function serveImpl(
|
|
|
1107
1150
|
const cta = formatCtaBlock(name, err.cta)
|
|
1108
1151
|
write({
|
|
1109
1152
|
ok: false,
|
|
1110
|
-
error: {
|
|
1111
|
-
|
|
1153
|
+
error: {
|
|
1154
|
+
code: err.code,
|
|
1155
|
+
message: err.message,
|
|
1156
|
+
...(err.retryable !== undefined ? { retryable: err.retryable } : undefined),
|
|
1157
|
+
},
|
|
1158
|
+
meta: {
|
|
1159
|
+
command: path,
|
|
1160
|
+
duration: `${Math.round(performance.now() - start)}ms`,
|
|
1161
|
+
...(cta ? { cta } : undefined),
|
|
1162
|
+
},
|
|
1112
1163
|
})
|
|
1113
1164
|
exit(err.exitCode ?? 1)
|
|
1114
1165
|
}
|
|
1115
1166
|
const composed = fetchMiddleware.reduceRight(
|
|
1116
|
-
(next: () => Promise<void>, mw) => async () => {
|
|
1167
|
+
(next: () => Promise<void>, mw) => async () => {
|
|
1168
|
+
handleMwSentinel(await mw(mwCtx, next))
|
|
1169
|
+
},
|
|
1117
1170
|
runFetch,
|
|
1118
1171
|
)
|
|
1119
1172
|
await composed()
|
|
@@ -1344,7 +1397,9 @@ async function serveImpl(
|
|
|
1344
1397
|
/** @internal Options for fetchImpl. */
|
|
1345
1398
|
declare namespace fetchImpl {
|
|
1346
1399
|
type Options = {
|
|
1347
|
-
mcpHandler?:
|
|
1400
|
+
mcpHandler?:
|
|
1401
|
+
| ((req: Request, commands: Map<string, CommandEntry>) => Promise<Response>)
|
|
1402
|
+
| undefined
|
|
1348
1403
|
middlewares?: MiddlewareHandler[] | undefined
|
|
1349
1404
|
rootCommand?: CommandDefinition<any, any, any> | undefined
|
|
1350
1405
|
vars?: z.ZodObject<any> | undefined
|
|
@@ -1358,9 +1413,8 @@ function createMcpHttpHandler(name: string, version: string) {
|
|
|
1358
1413
|
return async (req: Request, commands: Map<string, CommandEntry>): Promise<Response> => {
|
|
1359
1414
|
if (!transport) {
|
|
1360
1415
|
const { McpServer } = await import('@modelcontextprotocol/sdk/server/mcp.js')
|
|
1361
|
-
const { WebStandardStreamableHTTPServerTransport } =
|
|
1362
|
-
'@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js'
|
|
1363
|
-
)
|
|
1416
|
+
const { WebStandardStreamableHTTPServerTransport } =
|
|
1417
|
+
await import('@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js')
|
|
1364
1418
|
|
|
1365
1419
|
const server = new McpServer({ name, version })
|
|
1366
1420
|
|
|
@@ -1455,12 +1509,12 @@ async function fetchImpl(
|
|
|
1455
1509
|
|
|
1456
1510
|
// Parse options from search params (GET) or body (non-GET)
|
|
1457
1511
|
let inputOptions: Record<string, unknown> = {}
|
|
1458
|
-
if (req.method === 'GET')
|
|
1459
|
-
for (const [key, value] of url.searchParams) inputOptions[key] = value
|
|
1512
|
+
if (req.method === 'GET') for (const [key, value] of url.searchParams) inputOptions[key] = value
|
|
1460
1513
|
else {
|
|
1461
1514
|
try {
|
|
1462
1515
|
const contentType = req.headers.get('content-type') ?? ''
|
|
1463
|
-
if (contentType.includes('application/json'))
|
|
1516
|
+
if (contentType.includes('application/json'))
|
|
1517
|
+
inputOptions = (await req.json()) as Record<string, unknown>
|
|
1464
1518
|
} catch {}
|
|
1465
1519
|
}
|
|
1466
1520
|
|
|
@@ -1477,7 +1531,11 @@ async function fetchImpl(
|
|
|
1477
1531
|
if (options.rootCommand)
|
|
1478
1532
|
return executeCommand(name, options.rootCommand, [], inputOptions, start, options)
|
|
1479
1533
|
return jsonResponse(
|
|
1480
|
-
{
|
|
1534
|
+
{
|
|
1535
|
+
ok: false,
|
|
1536
|
+
error: { code: 'COMMAND_NOT_FOUND', message: 'No root command defined.' },
|
|
1537
|
+
meta: { command: '/', duration: `${Math.round(performance.now() - start)}ms` },
|
|
1538
|
+
},
|
|
1481
1539
|
404,
|
|
1482
1540
|
)
|
|
1483
1541
|
}
|
|
@@ -1486,18 +1544,31 @@ async function fetchImpl(
|
|
|
1486
1544
|
|
|
1487
1545
|
if ('error' in resolved)
|
|
1488
1546
|
return jsonResponse(
|
|
1489
|
-
{
|
|
1547
|
+
{
|
|
1548
|
+
ok: false,
|
|
1549
|
+
error: {
|
|
1550
|
+
code: 'COMMAND_NOT_FOUND',
|
|
1551
|
+
message: `'${resolved.error}' is not a command for '${resolved.path ? `${name} ${resolved.path}` : name}'.`,
|
|
1552
|
+
},
|
|
1553
|
+
meta: { command: resolved.error, duration: `${Math.round(performance.now() - start)}ms` },
|
|
1554
|
+
},
|
|
1490
1555
|
404,
|
|
1491
1556
|
)
|
|
1492
1557
|
|
|
1493
1558
|
if ('help' in resolved)
|
|
1494
1559
|
return jsonResponse(
|
|
1495
|
-
{
|
|
1560
|
+
{
|
|
1561
|
+
ok: false,
|
|
1562
|
+
error: {
|
|
1563
|
+
code: 'COMMAND_NOT_FOUND',
|
|
1564
|
+
message: `'${resolved.path}' is a command group. Specify a subcommand.`,
|
|
1565
|
+
},
|
|
1566
|
+
meta: { command: resolved.path, duration: `${Math.round(performance.now() - start)}ms` },
|
|
1567
|
+
},
|
|
1496
1568
|
404,
|
|
1497
1569
|
)
|
|
1498
1570
|
|
|
1499
|
-
if ('fetchGateway' in resolved)
|
|
1500
|
-
return resolved.fetchGateway.fetch(req)
|
|
1571
|
+
if ('fetchGateway' in resolved) return resolved.fetchGateway.fetch(req)
|
|
1501
1572
|
|
|
1502
1573
|
const { command, path, rest } = resolved
|
|
1503
1574
|
return executeCommand(path, command, rest, inputOptions, start, options)
|
|
@@ -1528,8 +1599,11 @@ async function executeCommand(
|
|
|
1528
1599
|
const parsedOptions = command.options ? command.options.parse(inputOptions) : {}
|
|
1529
1600
|
|
|
1530
1601
|
const okFn = (data: unknown): never => ({ [sentinel_]: 'ok', data }) as never
|
|
1531
|
-
const errorFn = (opts: {
|
|
1532
|
-
|
|
1602
|
+
const errorFn = (opts: {
|
|
1603
|
+
code: string
|
|
1604
|
+
message: string
|
|
1605
|
+
exitCode?: number | undefined
|
|
1606
|
+
}): never => ({ [sentinel_]: 'error', ...opts }) as never
|
|
1533
1607
|
|
|
1534
1608
|
const result = command.run({
|
|
1535
1609
|
agent: true,
|
|
@@ -1560,26 +1634,60 @@ async function executeCommand(
|
|
|
1560
1634
|
}
|
|
1561
1635
|
if (isSentinel(value) && (value as any)[sentinel] === 'error') {
|
|
1562
1636
|
const tagged = value as any
|
|
1563
|
-
controller.enqueue(
|
|
1637
|
+
controller.enqueue(
|
|
1638
|
+
encoder.encode(
|
|
1639
|
+
JSON.stringify({
|
|
1640
|
+
type: 'error',
|
|
1641
|
+
ok: false,
|
|
1642
|
+
error: { code: tagged.code, message: tagged.message },
|
|
1643
|
+
}) + '\n',
|
|
1644
|
+
),
|
|
1645
|
+
)
|
|
1564
1646
|
controller.close()
|
|
1565
1647
|
return
|
|
1566
1648
|
}
|
|
1567
|
-
controller.enqueue(
|
|
1649
|
+
controller.enqueue(
|
|
1650
|
+
encoder.encode(JSON.stringify({ type: 'chunk', data: value }) + '\n'),
|
|
1651
|
+
)
|
|
1568
1652
|
}
|
|
1569
1653
|
const meta: Record<string, unknown> = { command: path }
|
|
1570
1654
|
if (isSentinel(returnValue) && (returnValue as any)[sentinel] === 'error') {
|
|
1571
1655
|
const tagged = returnValue as any
|
|
1572
|
-
controller.enqueue(
|
|
1656
|
+
controller.enqueue(
|
|
1657
|
+
encoder.encode(
|
|
1658
|
+
JSON.stringify({
|
|
1659
|
+
type: 'error',
|
|
1660
|
+
ok: false,
|
|
1661
|
+
error: { code: tagged.code, message: tagged.message },
|
|
1662
|
+
}) + '\n',
|
|
1663
|
+
),
|
|
1664
|
+
)
|
|
1573
1665
|
} else {
|
|
1574
|
-
controller.enqueue(
|
|
1666
|
+
controller.enqueue(
|
|
1667
|
+
encoder.encode(JSON.stringify({ type: 'done', ok: true, meta }) + '\n'),
|
|
1668
|
+
)
|
|
1575
1669
|
}
|
|
1576
1670
|
} catch (error) {
|
|
1577
|
-
controller.enqueue(
|
|
1671
|
+
controller.enqueue(
|
|
1672
|
+
encoder.encode(
|
|
1673
|
+
JSON.stringify({
|
|
1674
|
+
type: 'error',
|
|
1675
|
+
ok: false,
|
|
1676
|
+
error: {
|
|
1677
|
+
code: 'UNKNOWN',
|
|
1678
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1679
|
+
},
|
|
1680
|
+
}) + '\n',
|
|
1681
|
+
),
|
|
1682
|
+
)
|
|
1578
1683
|
}
|
|
1579
1684
|
controller.close()
|
|
1580
1685
|
},
|
|
1581
1686
|
})
|
|
1582
|
-
response = new Response(stream, {
|
|
1687
|
+
response = new Response(stream, {
|
|
1688
|
+
status: 200,
|
|
1689
|
+
headers: { 'content-type': 'application/x-ndjson' },
|
|
1690
|
+
})
|
|
1583
1691
|
return
|
|
1584
1692
|
}
|
|
1585
1693
|
|
|
@@ -1590,7 +1698,11 @@ async function executeCommand(
|
|
|
1590
1698
|
const tagged = awaited as any
|
|
1591
1699
|
if (tagged[sentinel_] === 'error')
|
|
1592
1700
|
response = jsonResponse(
|
|
1593
|
-
{
|
|
1701
|
+
{
|
|
1702
|
+
ok: false,
|
|
1703
|
+
error: { code: tagged.code, message: tagged.message },
|
|
1704
|
+
meta: { command: path, duration },
|
|
1705
|
+
},
|
|
1594
1706
|
500,
|
|
1595
1707
|
)
|
|
1596
1708
|
else
|
|
@@ -1601,19 +1713,24 @@ async function executeCommand(
|
|
|
1601
1713
|
return
|
|
1602
1714
|
}
|
|
1603
1715
|
|
|
1604
|
-
response = jsonResponse(
|
|
1605
|
-
{ ok: true, data: awaited, meta: { command: path, duration } },
|
|
1606
|
-
200,
|
|
1607
|
-
)
|
|
1716
|
+
response = jsonResponse({ ok: true, data: awaited, meta: { command: path, duration } }, 200)
|
|
1608
1717
|
}
|
|
1609
1718
|
|
|
1610
1719
|
try {
|
|
1611
1720
|
const allMiddleware = options.middlewares ?? []
|
|
1612
1721
|
if (allMiddleware.length > 0) {
|
|
1613
|
-
const errorFn = (opts: {
|
|
1722
|
+
const errorFn = (opts: {
|
|
1723
|
+
code: string
|
|
1724
|
+
message: string
|
|
1725
|
+
exitCode?: number | undefined
|
|
1726
|
+
}): never => {
|
|
1614
1727
|
const duration = `${Math.round(performance.now() - start)}ms`
|
|
1615
1728
|
response = jsonResponse(
|
|
1616
|
-
{
|
|
1729
|
+
{
|
|
1730
|
+
ok: false,
|
|
1731
|
+
error: { code: opts.code, message: opts.message },
|
|
1732
|
+
meta: { command: path, duration },
|
|
1733
|
+
},
|
|
1617
1734
|
500,
|
|
1618
1735
|
)
|
|
1619
1736
|
return undefined as never
|
|
@@ -1626,12 +1743,16 @@ async function executeCommand(
|
|
|
1626
1743
|
format: 'json',
|
|
1627
1744
|
formatExplicit: true,
|
|
1628
1745
|
name: path,
|
|
1629
|
-
set(key: string, value: unknown) {
|
|
1746
|
+
set(key: string, value: unknown) {
|
|
1747
|
+
varsMap[key] = value
|
|
1748
|
+
},
|
|
1630
1749
|
var: varsMap,
|
|
1631
1750
|
version: undefined,
|
|
1632
1751
|
}
|
|
1633
1752
|
const composed = allMiddleware.reduceRight(
|
|
1634
|
-
(next: () => Promise<void>, mw) => async () => {
|
|
1753
|
+
(next: () => Promise<void>, mw) => async () => {
|
|
1754
|
+
await mw(mwCtx, next)
|
|
1755
|
+
},
|
|
1635
1756
|
runCommand,
|
|
1636
1757
|
)
|
|
1637
1758
|
await composed()
|
|
@@ -1642,11 +1763,22 @@ async function executeCommand(
|
|
|
1642
1763
|
const duration = `${Math.round(performance.now() - start)}ms`
|
|
1643
1764
|
if (error instanceof ValidationError)
|
|
1644
1765
|
return jsonResponse(
|
|
1645
|
-
{
|
|
1766
|
+
{
|
|
1767
|
+
ok: false,
|
|
1768
|
+
error: { code: 'VALIDATION_ERROR', message: error.message },
|
|
1769
|
+
meta: { command: path, duration },
|
|
1770
|
+
},
|
|
1646
1771
|
400,
|
|
1647
1772
|
)
|
|
1648
1773
|
return jsonResponse(
|
|
1649
|
-
{
|
|
1774
|
+
{
|
|
1775
|
+
ok: false,
|
|
1776
|
+
error: {
|
|
1777
|
+
code: error instanceof IncurError ? error.code : 'UNKNOWN',
|
|
1778
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1779
|
+
},
|
|
1780
|
+
meta: { command: path, duration },
|
|
1781
|
+
},
|
|
1650
1782
|
500,
|
|
1651
1783
|
)
|
|
1652
1784
|
}
|
|
@@ -1857,7 +1989,22 @@ function extractBuiltinFlags(argv: string[]) {
|
|
|
1857
1989
|
else rest.push(token)
|
|
1858
1990
|
}
|
|
1859
1991
|
|
|
1860
|
-
return {
|
|
1992
|
+
return {
|
|
1993
|
+
verbose,
|
|
1994
|
+
format,
|
|
1995
|
+
formatExplicit,
|
|
1996
|
+
filterOutput,
|
|
1997
|
+
tokenLimit,
|
|
1998
|
+
tokenOffset,
|
|
1999
|
+
tokenCount,
|
|
2000
|
+
llms,
|
|
2001
|
+
llmsFull,
|
|
2002
|
+
mcp,
|
|
2003
|
+
help,
|
|
2004
|
+
version,
|
|
2005
|
+
schema,
|
|
2006
|
+
rest,
|
|
2007
|
+
}
|
|
1861
2008
|
}
|
|
1862
2009
|
|
|
1863
2010
|
/** @internal Collects immediate child commands/groups for help output. */
|
|
@@ -1991,8 +2138,9 @@ function formatHumanError(error: {
|
|
|
1991
2138
|
/** @internal Formats a CTA block for human-readable TTY output. */
|
|
1992
2139
|
function formatHumanCta(cta: FormattedCtaBlock): string {
|
|
1993
2140
|
const lines: string[] = ['', cta.description]
|
|
2141
|
+
const maxLen = Math.max(...cta.commands.map((c) => c.command.length))
|
|
1994
2142
|
for (const c of cta.commands) {
|
|
1995
|
-
const desc = c.description ? ` # ${c.description}` : ''
|
|
2143
|
+
const desc = c.description ? ` ${''.padEnd(maxLen - c.command.length)}# ${c.description}` : ''
|
|
1996
2144
|
lines.push(` ${c.command}${desc}`)
|
|
1997
2145
|
}
|
|
1998
2146
|
return lines.join('\n')
|
|
@@ -2073,7 +2221,8 @@ async function handleStreaming(
|
|
|
2073
2221
|
}
|
|
2074
2222
|
}
|
|
2075
2223
|
if (useJsonl) ctx.writeln(JSON.stringify({ type: 'chunk', data: value }))
|
|
2076
|
-
else if (ctx.renderOutput)
|
|
2224
|
+
else if (ctx.renderOutput)
|
|
2225
|
+
ctx.writeln(ctx.truncate(Formatter.format(value, ctx.format)).text)
|
|
2077
2226
|
}
|
|
2078
2227
|
|
|
2079
2228
|
// Handle return value — error() or ok() sentinel
|
package/src/Fetch.test.ts
CHANGED
|
@@ -66,13 +66,7 @@ describe('parseArgv', () => {
|
|
|
66
66
|
})
|
|
67
67
|
|
|
68
68
|
test('multiple headers', () => {
|
|
69
|
-
const input = Fetch.parseArgv([
|
|
70
|
-
'users',
|
|
71
|
-
'-H',
|
|
72
|
-
'X-A: 1',
|
|
73
|
-
'-H',
|
|
74
|
-
'X-B: 2',
|
|
75
|
-
])
|
|
69
|
+
const input = Fetch.parseArgv(['users', '-H', 'X-A: 1', '-H', 'X-B: 2'])
|
|
76
70
|
expect(input.headers.get('X-A')).toBe('1')
|
|
77
71
|
expect(input.headers.get('X-B')).toBe('2')
|
|
78
72
|
})
|
|
@@ -123,16 +117,12 @@ describe('buildRequest', () => {
|
|
|
123
117
|
})
|
|
124
118
|
|
|
125
119
|
test('builds POST request with body', () => {
|
|
126
|
-
const req = Fetch.buildRequest(
|
|
127
|
-
Fetch.parseArgv(['users', '-X', 'POST', '-d', '{"name":"Bob"}']),
|
|
128
|
-
)
|
|
120
|
+
const req = Fetch.buildRequest(Fetch.parseArgv(['users', '-X', 'POST', '-d', '{"name":"Bob"}']))
|
|
129
121
|
expect(req.method).toBe('POST')
|
|
130
122
|
})
|
|
131
123
|
|
|
132
124
|
test('builds request with headers', () => {
|
|
133
|
-
const req = Fetch.buildRequest(
|
|
134
|
-
Fetch.parseArgv(['users', '-H', 'X-Api-Key: secret']),
|
|
135
|
-
)
|
|
125
|
+
const req = Fetch.buildRequest(Fetch.parseArgv(['users', '-H', 'X-Api-Key: secret']))
|
|
136
126
|
expect(req.headers.get('X-Api-Key')).toBe('secret')
|
|
137
127
|
})
|
|
138
128
|
})
|
package/src/Fetch.ts
CHANGED
|
@@ -102,8 +102,7 @@ export function buildRequest(input: FetchInput): Request {
|
|
|
102
102
|
|
|
103
103
|
if (input.body !== undefined) {
|
|
104
104
|
init.body = input.body
|
|
105
|
-
if (!input.headers.has('content-type'))
|
|
106
|
-
input.headers.set('content-type', 'application/json')
|
|
105
|
+
if (!input.headers.has('content-type')) input.headers.set('content-type', 'application/json')
|
|
107
106
|
}
|
|
108
107
|
|
|
109
108
|
return new Request(url.toString(), init)
|
package/src/Filter.ts
CHANGED
|
@@ -97,7 +97,12 @@ function resolve(data: unknown, segments: Segment[], index: number): unknown {
|
|
|
97
97
|
return sliced.map((item) => resolve(item, segments, index + 1))
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
function merge(
|
|
100
|
+
function merge(
|
|
101
|
+
target: Record<string, unknown>,
|
|
102
|
+
data: unknown,
|
|
103
|
+
segments: Segment[],
|
|
104
|
+
index: number,
|
|
105
|
+
): void {
|
|
101
106
|
if (index >= segments.length || typeof data !== 'object' || data === null) return
|
|
102
107
|
const segment = segments[index]!
|
|
103
108
|
|
|
@@ -129,8 +134,7 @@ function merge(target: Record<string, unknown>, data: unknown, segments: Segment
|
|
|
129
134
|
|
|
130
135
|
// Next segment is a key — recurse into nested object
|
|
131
136
|
if (typeof val !== 'object' || val === null) return
|
|
132
|
-
if (!target[segment.key] || typeof target[segment.key] !== 'object')
|
|
133
|
-
target[segment.key] = {}
|
|
137
|
+
if (!target[segment.key] || typeof target[segment.key] !== 'object') target[segment.key] = {}
|
|
134
138
|
merge(target[segment.key] as Record<string, unknown>, val, segments, index + 1)
|
|
135
139
|
return
|
|
136
140
|
}
|
package/src/Help.test.ts
CHANGED
|
@@ -29,7 +29,7 @@ describe('formatCommand', () => {
|
|
|
29
29
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
30
30
|
--help Show help
|
|
31
31
|
--llms, --llms-full Print LLM-readable manifest
|
|
32
|
-
--schema Show JSON Schema for
|
|
32
|
+
--schema Show JSON Schema for command
|
|
33
33
|
--token-count Print token count of output (instead of output)
|
|
34
34
|
--token-limit <n> Limit output to n tokens
|
|
35
35
|
--token-offset <n> Skip first n tokens of output
|
|
@@ -51,7 +51,7 @@ describe('formatCommand', () => {
|
|
|
51
51
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
52
52
|
--help Show help
|
|
53
53
|
--llms, --llms-full Print LLM-readable manifest
|
|
54
|
-
--schema Show JSON Schema for
|
|
54
|
+
--schema Show JSON Schema for command
|
|
55
55
|
--token-count Print token count of output (instead of output)
|
|
56
56
|
--token-limit <n> Limit output to n tokens
|
|
57
57
|
--token-offset <n> Skip first n tokens of output
|
|
@@ -80,7 +80,7 @@ describe('formatCommand', () => {
|
|
|
80
80
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
81
81
|
--help Show help
|
|
82
82
|
--llms, --llms-full Print LLM-readable manifest
|
|
83
|
-
--schema Show JSON Schema for
|
|
83
|
+
--schema Show JSON Schema for command
|
|
84
84
|
--token-count Print token count of output (instead of output)
|
|
85
85
|
--token-limit <n> Limit output to n tokens
|
|
86
86
|
--token-offset <n> Skip first n tokens of output
|
|
@@ -157,7 +157,7 @@ describe('formatRoot', () => {
|
|
|
157
157
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
158
158
|
--help Show help
|
|
159
159
|
--llms, --llms-full Print LLM-readable manifest
|
|
160
|
-
--schema Show JSON Schema for
|
|
160
|
+
--schema Show JSON Schema for command
|
|
161
161
|
--token-count Print token count of output (instead of output)
|
|
162
162
|
--token-limit <n> Limit output to n tokens
|
|
163
163
|
--token-offset <n> Skip first n tokens of output
|
|
@@ -182,7 +182,7 @@ describe('formatRoot', () => {
|
|
|
182
182
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
183
183
|
--help Show help
|
|
184
184
|
--llms, --llms-full Print LLM-readable manifest
|
|
185
|
-
--schema Show JSON Schema for
|
|
185
|
+
--schema Show JSON Schema for command
|
|
186
186
|
--token-count Print token count of output (instead of output)
|
|
187
187
|
--token-limit <n> Limit output to n tokens
|
|
188
188
|
--token-offset <n> Skip first n tokens of output
|
|
@@ -199,9 +199,9 @@ describe('formatRoot', () => {
|
|
|
199
199
|
})
|
|
200
200
|
expect(result).toMatchInlineSnapshot(`
|
|
201
201
|
"my-tool@1.0.0 — A test CLI
|
|
202
|
-
Aliases: mt, myt
|
|
203
202
|
|
|
204
203
|
Usage: my-tool <command>
|
|
204
|
+
Aliases: mt, myt
|
|
205
205
|
|
|
206
206
|
Commands:
|
|
207
207
|
fetch Fetch a URL
|
|
@@ -211,7 +211,7 @@ describe('formatRoot', () => {
|
|
|
211
211
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
212
212
|
--help Show help
|
|
213
213
|
--llms, --llms-full Print LLM-readable manifest
|
|
214
|
-
--schema Show JSON Schema for
|
|
214
|
+
--schema Show JSON Schema for command
|
|
215
215
|
--token-count Print token count of output (instead of output)
|
|
216
216
|
--token-limit <n> Limit output to n tokens
|
|
217
217
|
--token-offset <n> Skip first n tokens of output
|
|
@@ -228,9 +228,9 @@ describe('formatRoot', () => {
|
|
|
228
228
|
})
|
|
229
229
|
expect(result).toMatchInlineSnapshot(`
|
|
230
230
|
"my-tool@1.0.0 — A test CLI
|
|
231
|
-
Aliases: mt, myt
|
|
232
231
|
|
|
233
232
|
Usage: my-tool <url>
|
|
233
|
+
Aliases: mt, myt
|
|
234
234
|
|
|
235
235
|
Arguments:
|
|
236
236
|
url URL to fetch
|
|
@@ -240,7 +240,7 @@ describe('formatRoot', () => {
|
|
|
240
240
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
241
241
|
--help Show help
|
|
242
242
|
--llms, --llms-full Print LLM-readable manifest
|
|
243
|
-
--schema Show JSON Schema for
|
|
243
|
+
--schema Show JSON Schema for command
|
|
244
244
|
--token-count Print token count of output (instead of output)
|
|
245
245
|
--token-limit <n> Limit output to n tokens
|
|
246
246
|
--token-offset <n> Skip first n tokens of output
|