argsbarg 6.1.0 → 6.1.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/CHANGELOG.md +24 -2
- package/README.md +5 -2
- package/docs/api-server.md +1 -1
- package/docs/cli-program.md +35 -8
- package/docs/output-schema.md +1 -1
- package/index.d.ts +47 -8
- package/package.json +1 -1
- package/src/cli.ts +54 -1
- package/src/context.ts +40 -57
- package/src/help.ts +51 -15
- package/src/index.ts +10 -1
- package/src/json-leaf.test.ts +156 -0
- package/src/leaf-inputs.test.ts +118 -9
- package/src/leaf-inputs.ts +143 -70
- package/src/mcp/tools.ts +9 -0
- package/src/parse.ts +50 -0
- package/src/types.ts +13 -0
- package/src/validate.ts +12 -0
package/src/index.ts
CHANGED
|
@@ -28,7 +28,14 @@ export {
|
|
|
28
28
|
shouldRunHeadlessWithYes,
|
|
29
29
|
wantsExplicitJson,
|
|
30
30
|
} from "./headless.ts";
|
|
31
|
-
export {
|
|
31
|
+
export {
|
|
32
|
+
LeafInputError,
|
|
33
|
+
loadLeafInputs,
|
|
34
|
+
preloadPipableJson,
|
|
35
|
+
readJsonOptionValue,
|
|
36
|
+
readLeafInputs,
|
|
37
|
+
readLeafInputsAsync,
|
|
38
|
+
} from "./leaf-inputs.ts";
|
|
32
39
|
export type { McpBundlePaths, PackMcpBundleOpts } from "./mcp/bundle.ts";
|
|
33
40
|
export { defaultMcpBundlePaths, generateMcpManifest, packMcpBundle } from "./mcp/bundle.ts";
|
|
34
41
|
export type {
|
|
@@ -44,6 +51,7 @@ export type {
|
|
|
44
51
|
CliDocsTopic,
|
|
45
52
|
CliHandler,
|
|
46
53
|
CliInvocation,
|
|
54
|
+
CliLeafKind,
|
|
47
55
|
CliMcpBundleConfig,
|
|
48
56
|
CliMcpResource,
|
|
49
57
|
CliMcpServerConfig,
|
|
@@ -62,5 +70,6 @@ export {
|
|
|
62
70
|
CliOptionKind,
|
|
63
71
|
CliSchemaValidationError,
|
|
64
72
|
CliValueFormat,
|
|
73
|
+
isJsonLeaf,
|
|
65
74
|
} from "./types.ts";
|
|
66
75
|
export { isInteractiveTty } from "./utils.ts";
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { Cli } from "./index.ts";
|
|
3
|
+
import { ParseKind, parse } from "./parse.ts";
|
|
4
|
+
import { CliOptionKind, type CliProgram, CliSchemaValidationError } from "./types.ts";
|
|
5
|
+
import { cliValidateProgram } from "./validate.ts";
|
|
6
|
+
|
|
7
|
+
const bodySchema = {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: {
|
|
10
|
+
format: { type: "string", enum: ["pdf", "html"] },
|
|
11
|
+
invoice: {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: { id: { type: "string" } },
|
|
14
|
+
required: ["id"],
|
|
15
|
+
additionalProperties: false,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
required: ["format", "invoice"],
|
|
19
|
+
additionalProperties: false,
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
function jsonLeafProgram() {
|
|
23
|
+
return {
|
|
24
|
+
key: "json-leaf-test",
|
|
25
|
+
version: "1.0.0",
|
|
26
|
+
description: "json leaf tests",
|
|
27
|
+
commands: [
|
|
28
|
+
{
|
|
29
|
+
key: "render",
|
|
30
|
+
description: "Render from JSON body",
|
|
31
|
+
kind: "json",
|
|
32
|
+
inputSchema: bodySchema,
|
|
33
|
+
handler: (ctx) => ctx.inputsAs<{ format: string; invoice: { id: string } }>(),
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
} satisfies CliProgram;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("kind: json leaf", () => {
|
|
40
|
+
test("validate requires inputSchema and forbids options/positionals", () => {
|
|
41
|
+
expect(() =>
|
|
42
|
+
cliValidateProgram({
|
|
43
|
+
key: "bad",
|
|
44
|
+
version: "1",
|
|
45
|
+
description: "bad",
|
|
46
|
+
commands: [{ key: "x", description: "x", kind: "json", handler: () => {} }],
|
|
47
|
+
}),
|
|
48
|
+
).toThrow(CliSchemaValidationError);
|
|
49
|
+
|
|
50
|
+
expect(() =>
|
|
51
|
+
cliValidateProgram({
|
|
52
|
+
key: "bad",
|
|
53
|
+
version: "1",
|
|
54
|
+
description: "bad",
|
|
55
|
+
commands: [
|
|
56
|
+
{
|
|
57
|
+
key: "x",
|
|
58
|
+
description: "x",
|
|
59
|
+
kind: "json",
|
|
60
|
+
inputSchema: bodySchema,
|
|
61
|
+
options: [{ name: "f", description: "f", kind: CliOptionKind.String }],
|
|
62
|
+
handler: () => {},
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
}),
|
|
66
|
+
).toThrow(CliSchemaValidationError);
|
|
67
|
+
|
|
68
|
+
expect(() =>
|
|
69
|
+
cliValidateProgram({
|
|
70
|
+
key: "bad",
|
|
71
|
+
version: "1",
|
|
72
|
+
description: "bad",
|
|
73
|
+
commands: [
|
|
74
|
+
{
|
|
75
|
+
key: "x",
|
|
76
|
+
description: "x",
|
|
77
|
+
kind: "json",
|
|
78
|
+
inputSchema: bodySchema,
|
|
79
|
+
positionals: [{ name: "file", description: "file", kind: CliOptionKind.String }],
|
|
80
|
+
handler: () => {},
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
}),
|
|
84
|
+
).toThrow(CliSchemaValidationError);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("parse rejects CLI flags on json leaf", () => {
|
|
88
|
+
const root = jsonLeafProgram();
|
|
89
|
+
const pr = parse(root, ["render", "--format", "pdf"]);
|
|
90
|
+
expect(pr.kind).toBe(ParseKind.Error);
|
|
91
|
+
expect(pr.errorMsg).toContain("JSON commands do not accept options");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("parse accepts JSON positional", () => {
|
|
95
|
+
const root = jsonLeafProgram();
|
|
96
|
+
const pr = parse(root, ["render", '{"format":"pdf","invoice":{"id":"1"}}']);
|
|
97
|
+
expect(pr.kind).toBe(ParseKind.Ok);
|
|
98
|
+
expect(pr.args).toEqual(['{"format":"pdf","invoice":{"id":"1"}}']);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("invoke reads body from toolArgs", async () => {
|
|
102
|
+
const cli = new Cli(jsonLeafProgram());
|
|
103
|
+
const result = await cli.invoke(["render"], {
|
|
104
|
+
invocation: "api",
|
|
105
|
+
toolArgs: { format: "pdf", invoice: { id: "INV-1" } },
|
|
106
|
+
});
|
|
107
|
+
expect(result.kind).toBe("ok");
|
|
108
|
+
expect(result.response?.body).toEqual({ format: "pdf", invoice: { id: "INV-1" } });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("invoke reads body from JSON positional argv", async () => {
|
|
112
|
+
const cli = new Cli(jsonLeafProgram());
|
|
113
|
+
const result = await cli.invoke(["render", '{"format":"html","invoice":{"id":"2"}}'], {
|
|
114
|
+
invocation: "mcp",
|
|
115
|
+
});
|
|
116
|
+
expect(result.kind).toBe("ok");
|
|
117
|
+
expect(result.response?.body).toEqual({ format: "html", invoice: { id: "2" } });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("invoke errors when body missing", async () => {
|
|
121
|
+
const cli = new Cli(jsonLeafProgram());
|
|
122
|
+
const result = await cli.invoke(["render"], { invocation: "api" });
|
|
123
|
+
expect(result.kind).toBe("error");
|
|
124
|
+
expect(result.errorMsg).toContain("Missing JSON input");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("invoke validates inputSchema before handler", async () => {
|
|
128
|
+
let called = false;
|
|
129
|
+
const base = jsonLeafProgram();
|
|
130
|
+
const program = {
|
|
131
|
+
...base,
|
|
132
|
+
commands: [
|
|
133
|
+
{
|
|
134
|
+
...base.commands[0],
|
|
135
|
+
handler: () => {
|
|
136
|
+
called = true;
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
} satisfies CliProgram;
|
|
141
|
+
const cli = new Cli(program);
|
|
142
|
+
const result = await cli.invoke(["render"], {
|
|
143
|
+
invocation: "api",
|
|
144
|
+
toolArgs: { format: "pdf", invoice: { id: 123 } },
|
|
145
|
+
});
|
|
146
|
+
expect(result.kind).toBe("error");
|
|
147
|
+
expect(called).toBe(false);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("non-object JSON body returns error", async () => {
|
|
151
|
+
const cli = new Cli(jsonLeafProgram());
|
|
152
|
+
const result = await cli.invoke(["render", '"not-an-object"'], { invocation: "cli" });
|
|
153
|
+
expect(result.kind).toBe("error");
|
|
154
|
+
expect(result.errorMsg).toContain("JSON input must be a JSON object");
|
|
155
|
+
});
|
|
156
|
+
});
|
package/src/leaf-inputs.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { Cli, CliContext, CliOptionKind } from "./index.ts";
|
|
3
|
-
import { LeafInputError,
|
|
3
|
+
import { LeafInputError, readLeafInputs } from "./leaf-inputs.ts";
|
|
4
4
|
import type { CliProgram } from "./types.ts";
|
|
5
5
|
|
|
6
6
|
const invoiceSchema = {
|
|
@@ -46,16 +46,13 @@ function renderProgram(): CliProgram {
|
|
|
46
46
|
required: true,
|
|
47
47
|
},
|
|
48
48
|
],
|
|
49
|
-
handler:
|
|
50
|
-
const inputs = await ctx.readLeafInputsAsync();
|
|
51
|
-
return inputs;
|
|
52
|
-
},
|
|
49
|
+
handler: (ctx) => ctx.readLeafInputs(),
|
|
53
50
|
},
|
|
54
51
|
],
|
|
55
52
|
} satisfies CliProgram;
|
|
56
53
|
}
|
|
57
54
|
|
|
58
|
-
describe("
|
|
55
|
+
describe("readLeafInputs / jsonOpt", () => {
|
|
59
56
|
test("reads Json option from MCP toolArgs", async () => {
|
|
60
57
|
const cli = new Cli(renderProgram());
|
|
61
58
|
const result = await cli.invoke(["render", "--format", "pdf"], {
|
|
@@ -83,6 +80,33 @@ describe("readLeafInputsAsync", () => {
|
|
|
83
80
|
});
|
|
84
81
|
});
|
|
85
82
|
|
|
83
|
+
test("jsonOpt reads from preloadedJson", () => {
|
|
84
|
+
const program = renderProgram();
|
|
85
|
+
const ctx = new CliContext(
|
|
86
|
+
"json-pipe-test",
|
|
87
|
+
["render"],
|
|
88
|
+
[],
|
|
89
|
+
{ format: "pdf" },
|
|
90
|
+
program,
|
|
91
|
+
"cli",
|
|
92
|
+
undefined,
|
|
93
|
+
undefined,
|
|
94
|
+
{ invoice: { id: "piped" } },
|
|
95
|
+
);
|
|
96
|
+
expect(ctx.jsonOpt("invoice")).toEqual({ id: "piped" });
|
|
97
|
+
expect(ctx.inputs).toEqual({ format: "pdf", invoice: { id: "piped" } });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("inputsAs returns schemagen-shaped inputs", () => {
|
|
101
|
+
type RenderInput = { format: "pdf" | "html"; invoice: { id: string } };
|
|
102
|
+
const program = renderProgram();
|
|
103
|
+
const ctx = new CliContext("json-pipe-test", ["render"], [], { format: "pdf" }, program, "mcp", undefined, {
|
|
104
|
+
format: "pdf",
|
|
105
|
+
invoice: { id: "INV-1" },
|
|
106
|
+
});
|
|
107
|
+
expect(ctx.inputsAs<RenderInput>()).toEqual({ format: "pdf", invoice: { id: "INV-1" } });
|
|
108
|
+
});
|
|
109
|
+
|
|
86
110
|
test("rejects invalid Json flag at parse time", async () => {
|
|
87
111
|
const cli = new Cli(renderProgram());
|
|
88
112
|
const result = await cli.invoke(["render", "--format", "pdf", "--invoice", "not-json"], {
|
|
@@ -103,10 +127,95 @@ describe("readLeafInputsAsync", () => {
|
|
|
103
127
|
expect(result.stderr).toContain("invoice.id");
|
|
104
128
|
});
|
|
105
129
|
|
|
106
|
-
test("
|
|
130
|
+
test("validates inputSchema before handler runs", async () => {
|
|
131
|
+
let handlerCalled = false;
|
|
132
|
+
const program = {
|
|
133
|
+
key: "json-pipe-test",
|
|
134
|
+
version: "1.0.0",
|
|
135
|
+
description: "pre-handler validation",
|
|
136
|
+
commands: [
|
|
137
|
+
{
|
|
138
|
+
key: "render",
|
|
139
|
+
description: "Render",
|
|
140
|
+
inputSchema: invoiceSchema,
|
|
141
|
+
options: [
|
|
142
|
+
{
|
|
143
|
+
name: "format",
|
|
144
|
+
description: "Output format",
|
|
145
|
+
kind: CliOptionKind.Enum,
|
|
146
|
+
choices: ["pdf", "html"],
|
|
147
|
+
required: true,
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: "invoice",
|
|
151
|
+
description: "Invoice JSON",
|
|
152
|
+
kind: CliOptionKind.Json,
|
|
153
|
+
pipable: true,
|
|
154
|
+
required: true,
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
handler: () => {
|
|
158
|
+
handlerCalled = true;
|
|
159
|
+
return { ok: true };
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
} satisfies CliProgram;
|
|
164
|
+
const cli = new Cli(program);
|
|
165
|
+
const result = await cli.invoke(["render", "--format", "pdf"], {
|
|
166
|
+
invocation: "mcp",
|
|
167
|
+
toolArgs: { format: "pdf", invoice: { id: 123 } },
|
|
168
|
+
});
|
|
169
|
+
expect(result.kind).toBe("error");
|
|
170
|
+
expect(handlerCalled).toBe(false);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("inputs returns cached inputs after pre-handler validation", async () => {
|
|
174
|
+
const inputs: unknown[] = [];
|
|
175
|
+
const program = {
|
|
176
|
+
key: "json-pipe-test",
|
|
177
|
+
version: "1.0.0",
|
|
178
|
+
description: "cached inputs",
|
|
179
|
+
commands: [
|
|
180
|
+
{
|
|
181
|
+
key: "render",
|
|
182
|
+
description: "Render",
|
|
183
|
+
inputSchema: invoiceSchema,
|
|
184
|
+
options: [
|
|
185
|
+
{
|
|
186
|
+
name: "format",
|
|
187
|
+
description: "Output format",
|
|
188
|
+
kind: CliOptionKind.Enum,
|
|
189
|
+
choices: ["pdf", "html"],
|
|
190
|
+
required: true,
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: "invoice",
|
|
194
|
+
description: "Invoice JSON",
|
|
195
|
+
kind: CliOptionKind.Json,
|
|
196
|
+
required: true,
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
handler: (ctx) => {
|
|
200
|
+
inputs.push(ctx.inputs);
|
|
201
|
+
inputs.push(ctx.inputs);
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
} satisfies CliProgram;
|
|
206
|
+
const cli = new Cli(program);
|
|
207
|
+
await cli.invoke(["render", "--format", "pdf"], {
|
|
208
|
+
invocation: "mcp",
|
|
209
|
+
toolArgs: { format: "pdf", invoice: { id: "INV-1" } },
|
|
210
|
+
});
|
|
211
|
+
expect(inputs).toHaveLength(2);
|
|
212
|
+
expect(inputs[0]).toEqual(inputs[1]);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("readLeafInputs throws LeafInputError when required Json is missing", () => {
|
|
107
216
|
const program = renderProgram();
|
|
108
217
|
const ctx = new CliContext("json-pipe-test", ["render"], [], { format: "pdf" }, program, "mcp", undefined, {});
|
|
109
|
-
|
|
218
|
+
expect(() => readLeafInputs(ctx)).toThrow(LeafInputError);
|
|
110
219
|
});
|
|
111
220
|
|
|
112
221
|
test("omits undefined optional properties before inputSchema validation", async () => {
|
|
@@ -155,7 +264,7 @@ describe("readLeafInputsAsync", () => {
|
|
|
155
264
|
required: true,
|
|
156
265
|
},
|
|
157
266
|
],
|
|
158
|
-
handler:
|
|
267
|
+
handler: (ctx) => ctx.readLeafInputs(),
|
|
159
268
|
},
|
|
160
269
|
],
|
|
161
270
|
} satisfies CliProgram;
|
package/src/leaf-inputs.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/*
|
|
2
|
-
|
|
2
|
+
Leaf input reads: Json options (flag, preloaded stdin, or toolArgs), optional inputSchema validation.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { validateConfigDocument } from "./config/validate.ts";
|
|
6
6
|
import type { CliContext, CliLeafInputs } from "./context.ts";
|
|
7
7
|
import { collectOptionDefs } from "./parse.ts";
|
|
8
|
-
import type { CliLeaf, CliNode, CliOption } from "./types.ts";
|
|
9
|
-
import { CliOptionKind, CliValueFormat, isCliLeaf, isCliRouter } from "./types.ts";
|
|
8
|
+
import type { CliInvocation, CliLeaf, CliNode, CliOption, CliProgram } from "./types.ts";
|
|
9
|
+
import { CliOptionKind, CliValueFormat, isCliLeaf, isCliRouter, isJsonLeaf } from "./types.ts";
|
|
10
10
|
import { isInteractiveTty } from "./utils.ts";
|
|
11
11
|
|
|
12
|
-
/** Thrown when
|
|
12
|
+
/** Thrown when leaf input resolution or validation fails. */
|
|
13
13
|
export class LeafInputError extends Error {
|
|
14
14
|
constructor(message: string) {
|
|
15
15
|
super(message);
|
|
@@ -17,9 +17,12 @@ export class LeafInputError extends Error {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
/** Internal key for piped stdin on `kind: "json"` leaves. */
|
|
21
|
+
export const JSON_LEAF_BODY_KEY = "__jsonLeafBody";
|
|
22
|
+
|
|
23
|
+
function resolveLeaf(program: CliProgram, commandPath: string[]): CliLeaf | undefined {
|
|
24
|
+
let node: CliNode = program;
|
|
25
|
+
for (const seg of commandPath) {
|
|
23
26
|
if (!isCliRouter(node)) return undefined;
|
|
24
27
|
const child = node.commands.find((c) => c.key === seg);
|
|
25
28
|
if (!child) return undefined;
|
|
@@ -28,40 +31,12 @@ function leafNode(ctx: CliContext): CliLeaf | undefined {
|
|
|
28
31
|
return isCliLeaf(node) ? node : undefined;
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
function
|
|
32
|
-
ctx
|
|
33
|
-
opt: CliOption,
|
|
34
|
-
): boolean | number | string | string[] | unknown | undefined {
|
|
35
|
-
if (opt.kind === CliOptionKind.Presence) {
|
|
36
|
-
return ctx.hasFlag(opt.name);
|
|
37
|
-
}
|
|
38
|
-
if (opt.kind === CliOptionKind.Number) {
|
|
39
|
-
const n = ctx.numberOpt(opt.name);
|
|
40
|
-
return n === null ? undefined : n;
|
|
41
|
-
}
|
|
42
|
-
if (opt.kind === CliOptionKind.Json) {
|
|
43
|
-
const raw = ctx.stringOpt(opt.name);
|
|
44
|
-
if (raw === undefined) return undefined;
|
|
45
|
-
return parseJsonText(raw, `--${opt.name}`);
|
|
46
|
-
}
|
|
47
|
-
if (opt.format !== undefined) {
|
|
48
|
-
if (opt.format === CliValueFormat.Duration) {
|
|
49
|
-
return ctx.durationOpt(opt.name);
|
|
50
|
-
}
|
|
51
|
-
if (opt.format === CliValueFormat.CommaList) {
|
|
52
|
-
return ctx.commaListOpt(opt.name);
|
|
53
|
-
}
|
|
54
|
-
if (opt.format === CliValueFormat.Date) {
|
|
55
|
-
return ctx.dateOpt(opt.name);
|
|
56
|
-
}
|
|
57
|
-
if (opt.format === CliValueFormat.DateTime) {
|
|
58
|
-
return ctx.dateTimeOpt(opt.name);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return ctx.stringOpt(opt.name);
|
|
34
|
+
function leafNode(ctx: CliContext): CliLeaf | undefined {
|
|
35
|
+
return resolveLeaf(ctx.program, ctx.commandPath);
|
|
62
36
|
}
|
|
63
37
|
|
|
64
|
-
|
|
38
|
+
/** Parses a JSON string from a `--name` flag value. */
|
|
39
|
+
export function parseJsonText(raw: string, label: string): unknown {
|
|
65
40
|
const trimmed = raw.trim();
|
|
66
41
|
if (trimmed.length === 0) {
|
|
67
42
|
throw new LeafInputError(`${label}: JSON value is empty`);
|
|
@@ -86,6 +61,23 @@ async function readPipedJsonStdin(): Promise<unknown> {
|
|
|
86
61
|
}
|
|
87
62
|
}
|
|
88
63
|
|
|
64
|
+
function jsonLeafBodyHelp(): string {
|
|
65
|
+
return "Missing JSON input: pass a JSON document as an argument or pipe to stdin";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function readPipedJsonStdinForJsonLeaf(): Promise<unknown> {
|
|
69
|
+
const raw = await new Response(Bun.stdin).text();
|
|
70
|
+
const trimmed = raw.trim();
|
|
71
|
+
if (trimmed.length === 0) {
|
|
72
|
+
throw new LeafInputError(jsonLeafBodyHelp());
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(trimmed);
|
|
76
|
+
} catch {
|
|
77
|
+
throw new LeafInputError("stdin is not valid JSON");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
89
81
|
function pipableJsonHelp(opt: CliOption): string {
|
|
90
82
|
return `Missing required option --${opt.name}: pass JSON via --${opt.name} '<json>' or pipe a JSON document to stdin`;
|
|
91
83
|
}
|
|
@@ -107,46 +99,117 @@ function validateAgainstInputSchema(out: CliLeafInputs, inputSchema: Record<stri
|
|
|
107
99
|
}
|
|
108
100
|
}
|
|
109
101
|
|
|
102
|
+
/** Resolves a Json option from argv, preloaded stdin, or toolArgs (flag wins). */
|
|
103
|
+
export function readJsonOptionValue(ctx: CliContext, name: string): unknown | undefined {
|
|
104
|
+
const flagValue = ctx.stringOpt(name);
|
|
105
|
+
if (flagValue !== undefined) {
|
|
106
|
+
return parseJsonText(flagValue, `--${name}`);
|
|
107
|
+
}
|
|
108
|
+
if (name in ctx.preloadedJson) {
|
|
109
|
+
return ctx.preloadedJson[name];
|
|
110
|
+
}
|
|
111
|
+
if (ctx.toolArgs !== undefined && name in ctx.toolArgs) {
|
|
112
|
+
return ctx.toolArgs[name];
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
110
117
|
/**
|
|
111
|
-
* Reads
|
|
112
|
-
*
|
|
118
|
+
* Reads piped stdin for a pipable Json option when the flag is omitted (CLI only).
|
|
119
|
+
* Call from {@link Cli.run} before constructing the handler context.
|
|
113
120
|
*/
|
|
114
|
-
export async function
|
|
121
|
+
export async function preloadPipableJson(
|
|
122
|
+
program: CliProgram,
|
|
123
|
+
commandPath: string[],
|
|
124
|
+
opts: Record<string, string>,
|
|
125
|
+
invocation: CliInvocation,
|
|
126
|
+
args: string[] = [],
|
|
127
|
+
): Promise<Record<string, unknown>> {
|
|
128
|
+
if (invocation !== "cli" || isInteractiveTty) {
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const leaf = resolveLeaf(program, commandPath);
|
|
133
|
+
if (leaf && isJsonLeaf(leaf) && args.length === 0) {
|
|
134
|
+
return { [JSON_LEAF_BODY_KEY]: await readPipedJsonStdinForJsonLeaf() };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (const opt of collectOptionDefs(program, commandPath)) {
|
|
138
|
+
if (opt.kind === CliOptionKind.Json && opt.pipable && !(opt.name in opts)) {
|
|
139
|
+
return { [opt.name]: await readPipedJsonStdin() };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return {};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function readSyncOptionValue(
|
|
146
|
+
ctx: CliContext,
|
|
147
|
+
opt: CliOption,
|
|
148
|
+
): boolean | number | string | string[] | unknown | undefined {
|
|
149
|
+
if (opt.kind === CliOptionKind.Presence) {
|
|
150
|
+
return ctx.hasFlag(opt.name);
|
|
151
|
+
}
|
|
152
|
+
if (opt.kind === CliOptionKind.Number) {
|
|
153
|
+
const n = ctx.numberOpt(opt.name);
|
|
154
|
+
return n === null ? undefined : n;
|
|
155
|
+
}
|
|
156
|
+
if (opt.kind === CliOptionKind.Json) {
|
|
157
|
+
return readJsonOptionValue(ctx, opt.name);
|
|
158
|
+
}
|
|
159
|
+
if (opt.format !== undefined) {
|
|
160
|
+
if (opt.format === CliValueFormat.Duration) {
|
|
161
|
+
return ctx.durationOpt(opt.name);
|
|
162
|
+
}
|
|
163
|
+
if (opt.format === CliValueFormat.CommaList) {
|
|
164
|
+
return ctx.commaListOpt(opt.name);
|
|
165
|
+
}
|
|
166
|
+
if (opt.format === CliValueFormat.Date) {
|
|
167
|
+
return ctx.dateOpt(opt.name);
|
|
168
|
+
}
|
|
169
|
+
if (opt.format === CliValueFormat.DateTime) {
|
|
170
|
+
return ctx.dateTimeOpt(opt.name);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return ctx.stringOpt(opt.name);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Loads coerced leaf inputs and validates against `leaf.inputSchema` when set.
|
|
178
|
+
* Used by {@link CliContext.inputs}; prefer `ctx.inputs` or `ctx.inputsAs()` in handlers.
|
|
179
|
+
*/
|
|
180
|
+
export function loadLeafInputs(ctx: CliContext): CliLeafInputs {
|
|
115
181
|
const leaf = leafNode(ctx);
|
|
116
182
|
if (!leaf) return {};
|
|
117
183
|
|
|
184
|
+
if (isJsonLeaf(leaf)) {
|
|
185
|
+
let body: unknown;
|
|
186
|
+
if (ctx.toolArgs !== undefined) {
|
|
187
|
+
body = ctx.toolArgs;
|
|
188
|
+
} else if (ctx.args.length > 0) {
|
|
189
|
+
const [arg0] = ctx.args;
|
|
190
|
+
if (arg0 === undefined) {
|
|
191
|
+
throw new LeafInputError(jsonLeafBodyHelp());
|
|
192
|
+
}
|
|
193
|
+
body = parseJsonText(arg0, "JSON argument");
|
|
194
|
+
} else if (JSON_LEAF_BODY_KEY in ctx.preloadedJson) {
|
|
195
|
+
body = ctx.preloadedJson[JSON_LEAF_BODY_KEY];
|
|
196
|
+
} else {
|
|
197
|
+
throw new LeafInputError(jsonLeafBodyHelp());
|
|
198
|
+
}
|
|
199
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
200
|
+
throw new LeafInputError("JSON input must be a JSON object");
|
|
201
|
+
}
|
|
202
|
+
const out = body as CliLeafInputs;
|
|
203
|
+
if (leaf.inputSchema !== undefined) {
|
|
204
|
+
validateAgainstInputSchema(out, leaf.inputSchema);
|
|
205
|
+
}
|
|
206
|
+
return omitUndefinedInputs(out);
|
|
207
|
+
}
|
|
208
|
+
|
|
118
209
|
const out: CliLeafInputs = {};
|
|
119
210
|
const options = collectOptionDefs(ctx.program, ctx.commandPath);
|
|
120
|
-
let pipedJson: unknown | undefined;
|
|
121
|
-
let pipedJsonRead = false;
|
|
122
211
|
|
|
123
212
|
for (const opt of options) {
|
|
124
|
-
if (opt.kind === CliOptionKind.Json) {
|
|
125
|
-
const flagValue = ctx.stringOpt(opt.name);
|
|
126
|
-
if (flagValue !== undefined) {
|
|
127
|
-
out[opt.name] = parseJsonText(flagValue, `--${opt.name}`);
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
if (ctx.toolArgs !== undefined && opt.name in ctx.toolArgs) {
|
|
131
|
-
out[opt.name] = ctx.toolArgs[opt.name];
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
if (opt.pipable && ctx.invocation === "cli") {
|
|
135
|
-
if (isInteractiveTty) {
|
|
136
|
-
out[opt.name] = undefined;
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
if (!pipedJsonRead) {
|
|
140
|
-
pipedJson = await readPipedJsonStdin();
|
|
141
|
-
pipedJsonRead = true;
|
|
142
|
-
}
|
|
143
|
-
out[opt.name] = pipedJson;
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
out[opt.name] = undefined;
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
213
|
out[opt.name] = readSyncOptionValue(ctx, opt);
|
|
151
214
|
}
|
|
152
215
|
|
|
@@ -176,3 +239,13 @@ export async function readLeafInputsAsync(ctx: CliContext): Promise<CliLeafInput
|
|
|
176
239
|
|
|
177
240
|
return omitUndefinedInputs(out);
|
|
178
241
|
}
|
|
242
|
+
|
|
243
|
+
/** @deprecated Use {@link CliContext.inputs} or {@link loadLeafInputs} via `ctx.inputs`. */
|
|
244
|
+
export function readLeafInputs(ctx: CliContext): CliLeafInputs {
|
|
245
|
+
return ctx.inputs;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** @deprecated Use sync {@link readLeafInputs} — stdin is preloaded before the handler runs. */
|
|
249
|
+
export function readLeafInputsAsync(ctx: CliContext): Promise<CliLeafInputs> {
|
|
250
|
+
return Promise.resolve(readLeafInputs(ctx));
|
|
251
|
+
}
|
package/src/mcp/tools.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type CliProgram,
|
|
18
18
|
CliValueFormat,
|
|
19
19
|
isCliLeaf,
|
|
20
|
+
isJsonLeaf,
|
|
20
21
|
leafOutputSchema,
|
|
21
22
|
} from "../types.ts";
|
|
22
23
|
|
|
@@ -150,6 +151,10 @@ function positionalProperty(p: CliPositional): Record<string, unknown> {
|
|
|
150
151
|
|
|
151
152
|
/** Builds inputSchema for a leaf command. */
|
|
152
153
|
function buildInputSchema(root: CliProgram, path: string[], leaf: CliLeaf): Record<string, unknown> {
|
|
154
|
+
if (isJsonLeaf(leaf) && leaf.inputSchema !== undefined) {
|
|
155
|
+
return leaf.inputSchema;
|
|
156
|
+
}
|
|
157
|
+
|
|
153
158
|
const properties: Record<string, unknown> = {};
|
|
154
159
|
const required: string[] = [];
|
|
155
160
|
|
|
@@ -286,6 +291,10 @@ export function mcpToolCallToArgv(
|
|
|
286
291
|
tool: McpToolDef,
|
|
287
292
|
args: Record<string, unknown>,
|
|
288
293
|
): string[] | { error: string } {
|
|
294
|
+
if (isJsonLeaf(tool.leaf)) {
|
|
295
|
+
return [...tool.path];
|
|
296
|
+
}
|
|
297
|
+
|
|
289
298
|
const argv = [...tool.path];
|
|
290
299
|
|
|
291
300
|
for (const opt of collectOptionDefs(root, tool.path)) {
|