argsbarg 7.0.8 → 7.0.10
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 +22 -3
- package/docs/cli-program.md +22 -7
- package/examples/full-example/docs/cli-schema.json +20 -24
- package/examples/full-example/docs/cli.md +4 -26
- package/examples/full-example/docs/openapi.json +3 -1
- package/examples/full-example-json/docs/cli-schema.json +102 -108
- package/examples/full-example-json/docs/cli.md +18 -117
- package/examples/full-example-json/docs/openapi.json +3 -1
- package/index.d.ts +36 -6
- package/package.json +1 -1
- package/src/builtins/export.ts +2 -0
- package/src/core/document-leaf.test.ts +208 -0
- package/src/core/json-leaf.test.ts +26 -0
- package/src/core/leaf-inputs.ts +70 -15
- package/src/core/parse.ts +7 -6
- package/src/core/schema.ts +2 -0
- package/src/core/types.ts +19 -7
- package/src/core/validate.ts +6 -5
- package/src/core/wire-schema.test.ts +256 -0
- package/src/core/wire-schema.ts +131 -0
- package/src/docs/cli-guide.test.ts +53 -0
- package/src/exports/cli.ts +2 -0
- package/src/help.test.ts +40 -0
- package/src/help.ts +19 -15
- package/src/http/openapi.ts +4 -40
- package/src/http/routes.ts +2 -2
- package/src/http/server.ts +9 -1
- package/src/index.ts +3 -0
- package/src/mcp/tools.ts +17 -106
- package/src/test/integration/http.test.ts +36 -0
package/src/http/server.ts
CHANGED
|
@@ -175,7 +175,15 @@ export async function handleApiRequest(
|
|
|
175
175
|
}
|
|
176
176
|
body = parsed as Record<string, unknown>;
|
|
177
177
|
} catch {
|
|
178
|
-
|
|
178
|
+
try {
|
|
179
|
+
const parsed = Bun.YAML.parse(rawBody);
|
|
180
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
181
|
+
return finish(apiErrorResponse(400, { error: "Request body must be a JSON object" }));
|
|
182
|
+
}
|
|
183
|
+
body = parsed as Record<string, unknown>;
|
|
184
|
+
} catch {
|
|
185
|
+
return finish(apiErrorResponse(400, { error: "Invalid JSON body" }));
|
|
186
|
+
}
|
|
179
187
|
}
|
|
180
188
|
}
|
|
181
189
|
}
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ export {
|
|
|
18
18
|
} from "./core/formats.ts";
|
|
19
19
|
export {
|
|
20
20
|
LeafInputError,
|
|
21
|
+
parseDocumentText,
|
|
21
22
|
preloadPipableJson,
|
|
22
23
|
readJsonOptionValue,
|
|
23
24
|
} from "./core/leaf-inputs.ts";
|
|
@@ -68,8 +69,10 @@ export {
|
|
|
68
69
|
CliOptionKind,
|
|
69
70
|
CliSchemaValidationError,
|
|
70
71
|
CliValueFormat,
|
|
72
|
+
isDocumentLeaf,
|
|
71
73
|
isJsonLeaf,
|
|
72
74
|
} from "./core/types.ts";
|
|
75
|
+
export { buildLeafInputSchema, leafWireOptions } from "./core/wire-schema.ts";
|
|
73
76
|
export type { HeadlessContext } from "./headless/routing.ts";
|
|
74
77
|
export {
|
|
75
78
|
formatDryRunMessage,
|
package/src/mcp/tools.ts
CHANGED
|
@@ -9,29 +9,25 @@ import {
|
|
|
9
9
|
type CliNode,
|
|
10
10
|
type CliOption,
|
|
11
11
|
CliOptionKind,
|
|
12
|
-
type CliPositional,
|
|
13
12
|
type CliProgram,
|
|
14
13
|
CliValueFormat,
|
|
15
14
|
isCliLeaf,
|
|
16
|
-
|
|
15
|
+
isDocumentLeaf,
|
|
17
16
|
leafOutputSchema,
|
|
18
17
|
} from "../core/types.ts";
|
|
18
|
+
import { buildLeafInputSchema, leafWireOptions } from "../core/wire-schema.ts";
|
|
19
19
|
import { docsMcpResources } from "../docs/mcp-resources.ts";
|
|
20
20
|
import { cliResolveNotes } from "../help.ts";
|
|
21
21
|
import { isMcpHidden, visibleOptions } from "../runtime/exposure.ts";
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
/** Presence flags omitted from MCP wire schemas (handled by the framework on invoke). */
|
|
26
|
-
const MCP_WIRE_OMIT_PRESENCE = new Set(["json", "yes", "verbose"]);
|
|
23
|
+
export { buildLeafInputSchema, leafWireOptions } from "../core/wire-schema.ts";
|
|
24
|
+
export { defaultDocsTopicResourceUri, resolveDocsTopicResourceUri } from "../docs/mcp-resources.ts";
|
|
27
25
|
|
|
28
26
|
/** Default URI pattern for the CLI schema MCP resource (`<mcpId>://schema`). */
|
|
29
27
|
export function defaultMcpSchemaUri(mcpId: string): string {
|
|
30
28
|
return `${mcpId}://schema`;
|
|
31
29
|
}
|
|
32
30
|
|
|
33
|
-
export { defaultDocsTopicResourceUri, resolveDocsTopicResourceUri } from "../docs/mcp-resources.ts";
|
|
34
|
-
|
|
35
31
|
/** Sanitizes a command key segment for MCP tool names and server identity. */
|
|
36
32
|
export function sanitizeToolSegment(key: string): string {
|
|
37
33
|
return key.replace(/[^a-zA-Z0-9]/g, "_");
|
|
@@ -72,61 +68,21 @@ export function mcpToolName(root: CliProgram, path: string[]): string {
|
|
|
72
68
|
return path.map(sanitizeToolSegment).join("_");
|
|
73
69
|
}
|
|
74
70
|
|
|
75
|
-
/** Leaf options exposed on MCP/HTTP wire schemas (omits framework-handled presence flags). */
|
|
76
|
-
export function leafWireOptions(leaf: CliLeaf): CliOption[] {
|
|
77
|
-
return visibleOptions(leaf.options).filter(
|
|
78
|
-
(opt) => !(opt.kind === CliOptionKind.Presence && MCP_WIRE_OMIT_PRESENCE.has(opt.name)),
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
71
|
/** True when the leaf declares a `yes` presence option (auto-injected on MCP invoke). */
|
|
83
|
-
export function leafHasYesOption(
|
|
72
|
+
export function leafHasYesOption(
|
|
73
|
+
/** Leaf command node to inspect. */
|
|
74
|
+
leaf: CliLeaf,
|
|
75
|
+
): boolean {
|
|
84
76
|
return visibleOptions(leaf.options).some((opt) => opt.name === "yes" && opt.kind === CliOptionKind.Presence);
|
|
85
77
|
}
|
|
86
78
|
|
|
87
|
-
/**
|
|
88
|
-
function
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
case CliOptionKind.Presence:
|
|
95
|
-
return { type: "boolean", ...base };
|
|
96
|
-
case CliOptionKind.String: {
|
|
97
|
-
if (opt.format === CliValueFormat.CommaList) {
|
|
98
|
-
return {
|
|
99
|
-
oneOf: [
|
|
100
|
-
{ type: "string", ...base },
|
|
101
|
-
{ type: "array", items: { type: "string" }, ...base },
|
|
102
|
-
],
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
const stringBase = { type: "string", ...base };
|
|
106
|
-
if (opt.format === CliValueFormat.Duration) {
|
|
107
|
-
return { ...stringBase, pattern: DURATION_PATTERN };
|
|
108
|
-
}
|
|
109
|
-
if (opt.format === CliValueFormat.Date) {
|
|
110
|
-
return { ...stringBase, format: "date" };
|
|
111
|
-
}
|
|
112
|
-
if (opt.format === CliValueFormat.DateTime) {
|
|
113
|
-
return { ...stringBase, format: "date-time" };
|
|
114
|
-
}
|
|
115
|
-
if (opt.pattern !== undefined) {
|
|
116
|
-
return { ...stringBase, pattern: opt.pattern };
|
|
117
|
-
}
|
|
118
|
-
return stringBase;
|
|
119
|
-
}
|
|
120
|
-
case CliOptionKind.Number:
|
|
121
|
-
return { type: "number", ...base };
|
|
122
|
-
case CliOptionKind.Enum:
|
|
123
|
-
return { type: "string", enum: opt.choices, ...base };
|
|
124
|
-
case CliOptionKind.Json:
|
|
125
|
-
return { type: "object", ...base };
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export function formatMcpOptionValue(opt: CliOption, val: unknown): string | { error: string } {
|
|
79
|
+
/** Formats an incoming MCP option value to an argv string. */
|
|
80
|
+
export function formatMcpOptionValue(
|
|
81
|
+
/** Option definition to format the value for. */
|
|
82
|
+
opt: CliOption,
|
|
83
|
+
/** Incoming raw value from the MCP tool call. */
|
|
84
|
+
val: unknown,
|
|
85
|
+
): string | { error: string } {
|
|
130
86
|
if (opt.format === CliValueFormat.CommaList) {
|
|
131
87
|
if (Array.isArray(val)) {
|
|
132
88
|
const items = val.map(String).filter(Boolean);
|
|
@@ -143,51 +99,6 @@ export function formatMcpOptionValue(opt: CliOption, val: unknown): string | { e
|
|
|
143
99
|
return String(val);
|
|
144
100
|
}
|
|
145
101
|
|
|
146
|
-
/** JSON Schema property for one positional slot. */
|
|
147
|
-
function positionalProperty(p: CliPositional): Record<string, unknown> {
|
|
148
|
-
const base = { description: p.description };
|
|
149
|
-
const { argMax = 1 } = p;
|
|
150
|
-
if (argMax === 0) {
|
|
151
|
-
return { type: "array", items: { type: "string" }, ...base };
|
|
152
|
-
}
|
|
153
|
-
return { type: "string", ...base };
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** Builds inputSchema for a leaf command. */
|
|
157
|
-
function buildInputSchema(leaf: CliLeaf): Record<string, unknown> {
|
|
158
|
-
if (isJsonLeaf(leaf) && leaf.inputSchema !== undefined) {
|
|
159
|
-
return leaf.inputSchema;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const properties: Record<string, unknown> = {};
|
|
163
|
-
const required: string[] = [];
|
|
164
|
-
|
|
165
|
-
for (const opt of leafWireOptions(leaf)) {
|
|
166
|
-
properties[opt.name] = optionProperty(opt);
|
|
167
|
-
if (opt.required) {
|
|
168
|
-
required.push(opt.name);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
for (const p of leaf.positionals ?? []) {
|
|
173
|
-
properties[p.name] = positionalProperty(p);
|
|
174
|
-
const { argMin = 1, argMax = 1 } = p;
|
|
175
|
-
if (argMax === 1 && argMin >= 1) {
|
|
176
|
-
required.push(p.name);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const schema: Record<string, unknown> = {
|
|
181
|
-
type: "object",
|
|
182
|
-
properties,
|
|
183
|
-
additionalProperties: false,
|
|
184
|
-
};
|
|
185
|
-
if (required.length > 0) {
|
|
186
|
-
schema.required = required;
|
|
187
|
-
}
|
|
188
|
-
return schema;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
102
|
/** Resolves MCP tool description with optional override and leaf notes. */
|
|
192
103
|
function resolveToolDescription(root: CliProgram, path: string[], leaf: CliLeaf): string {
|
|
193
104
|
let desc: string;
|
|
@@ -251,7 +162,7 @@ export function collectMcpTools(root: CliProgram): McpToolDef[] {
|
|
|
251
162
|
description: resolveToolDescription(root, path, cmd),
|
|
252
163
|
path,
|
|
253
164
|
leaf: cmd,
|
|
254
|
-
inputSchema:
|
|
165
|
+
inputSchema: buildLeafInputSchema(cmd),
|
|
255
166
|
...(outputSchema === undefined ? {} : { outputSchema }),
|
|
256
167
|
});
|
|
257
168
|
return;
|
|
@@ -294,7 +205,7 @@ export function mcpToolCallToArgv(
|
|
|
294
205
|
tool: McpToolDef,
|
|
295
206
|
args: Record<string, unknown>,
|
|
296
207
|
): string[] | { error: string } {
|
|
297
|
-
if (
|
|
208
|
+
if (isDocumentLeaf(tool.leaf)) {
|
|
298
209
|
return [...tool.path];
|
|
299
210
|
}
|
|
300
211
|
|
|
@@ -448,6 +448,42 @@ describe("HTTP API routes", () => {
|
|
|
448
448
|
expect(body).toEqual({ error: "bad input" });
|
|
449
449
|
});
|
|
450
450
|
|
|
451
|
+
/** Tests that POST endpoints accept YAML request bodies. */
|
|
452
|
+
test("POST accepts YAML request body", async () => {
|
|
453
|
+
const yamlProgram = testProgram({
|
|
454
|
+
key: "app",
|
|
455
|
+
description: "Test app",
|
|
456
|
+
httpServer: { enabled: true },
|
|
457
|
+
commands: [
|
|
458
|
+
{
|
|
459
|
+
key: "create",
|
|
460
|
+
kind: "document",
|
|
461
|
+
description: "Create resource",
|
|
462
|
+
inputSchema: {
|
|
463
|
+
type: "object",
|
|
464
|
+
properties: { name: { type: "string" } },
|
|
465
|
+
required: ["name"],
|
|
466
|
+
},
|
|
467
|
+
handler: (ctx: CliContextType) => {
|
|
468
|
+
return { created: ctx.inputsAs<{ name: string }>().name };
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
],
|
|
472
|
+
});
|
|
473
|
+
cliValidateProgram(yamlProgram);
|
|
474
|
+
const res = await apiRequest(
|
|
475
|
+
yamlProgram,
|
|
476
|
+
new Request("http://127.0.0.1/create", {
|
|
477
|
+
method: "POST",
|
|
478
|
+
headers: { "content-type": "application/yaml" },
|
|
479
|
+
body: "name: test-resource",
|
|
480
|
+
}),
|
|
481
|
+
);
|
|
482
|
+
expect(res.status).toBe(201);
|
|
483
|
+
const body = (await res.json()) as Record<string, unknown>;
|
|
484
|
+
expect(body).toEqual({ created: "test-resource" });
|
|
485
|
+
});
|
|
486
|
+
|
|
451
487
|
test("GET /openapi.json lists REST paths", async () => {
|
|
452
488
|
const res = await apiRequest(program, new Request("http://127.0.0.1/openapi.json"));
|
|
453
489
|
expect(res.status).toBe(200);
|