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/src/index.ts CHANGED
@@ -28,7 +28,14 @@ export {
28
28
  shouldRunHeadlessWithYes,
29
29
  wantsExplicitJson,
30
30
  } from "./headless.ts";
31
- export { LeafInputError, readLeafInputsAsync } from "./leaf-inputs.ts";
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
+ });
@@ -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, readLeafInputsAsync } from "./leaf-inputs.ts";
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: async (ctx) => {
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("readLeafInputsAsync", () => {
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("readLeafInputsAsync throws LeafInputError when required Json is missing", async () => {
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
- await expect(readLeafInputsAsync(ctx)).rejects.toThrow(LeafInputError);
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: async (ctx) => ctx.readLeafInputsAsync(),
267
+ handler: (ctx) => ctx.readLeafInputs(),
159
268
  },
160
269
  ],
161
270
  } satisfies CliProgram;
@@ -1,15 +1,15 @@
1
1
  /*
2
- Async leaf input reads: Json options (flag, piped stdin, or toolArgs), optional inputSchema validation.
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 {@link CliContext.readLeafInputsAsync} cannot resolve or validate inputs. */
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
- function leafNode(ctx: CliContext): CliLeaf | undefined {
21
- let node: CliNode = ctx.program;
22
- for (const seg of ctx.commandPath) {
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 readSyncOptionValue(
32
- ctx: CliContext,
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
- function parseJsonText(raw: string, label: string): unknown {
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 coerced leaf inputs, resolving Json options from flags, piped stdin, or toolArgs,
112
- * and validates against `leaf.inputSchema` when set.
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 readLeafInputsAsync(ctx: CliContext): Promise<CliLeafInputs> {
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)) {