argsbarg 5.1.16 → 6.0.1
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 +46 -1
- package/README.md +33 -25
- package/docs/README.md +2 -1
- package/docs/api-server.md +141 -0
- package/docs/bundled-docs.md +24 -10
- package/docs/cli-program.md +17 -2
- package/docs/config-schema.md +18 -8
- package/docs/developing.md +1 -1
- package/docs/mcp.md +5 -5
- package/docs/output-schema.md +78 -47
- package/examples/full-example/README.md +15 -6
- package/examples/full-example/docs/README.md +27 -0
- package/examples/full-example/docs/api.md +511 -0
- package/examples/full-example/docs/cli-schema.json +453 -0
- package/examples/full-example/docs/http.md +81 -0
- package/examples/full-example/docs/mcp.md +159 -0
- package/examples/full-example/docs/openapi.json +222 -0
- package/examples/full-example/docs/skill.md +46 -0
- package/examples/full-example/justfile +6 -2
- package/examples/full-example/schemas/generated/app-config.json +1 -1
- package/examples/full-example/schemas/generated/status.json +1 -1
- package/examples/full-example/scripts/dev-formula.ts +1 -1
- package/examples/full-example/scripts/schemagen/discover-schema-roots.test.ts +3 -3
- package/examples/full-example/scripts/schemagen/discover-schema-roots.ts +90 -35
- package/examples/full-example/scripts/schemagen/naming.ts +17 -0
- package/examples/full-example/scripts/schemagen.ts +14 -3
- package/examples/full-example/src/commands/echo/command.ts +6 -1
- package/examples/full-example/src/commands/status/schema-types.ts +14 -0
- package/examples/full-example/src/commands/status/types.ts +1 -11
- package/examples/full-example/src/{types.ts → config/schema-types.ts} +3 -2
- package/examples/full-example/src/program.ts +3 -0
- package/examples/mcp-test.ts +13 -2
- package/examples/nested.ts +12 -3
- package/examples/servers.ts +72 -0
- package/index.d.ts +66 -7
- package/package.json +17 -17
- package/src/api/openapi.ts +117 -0
- package/src/api/result.ts +111 -0
- package/src/api/schema-deref.test.ts +99 -0
- package/src/api/schema-deref.ts +76 -0
- package/src/api/server.ts +120 -0
- package/src/api.integration.test.ts +441 -0
- package/src/builtins/api.ts +38 -0
- package/src/builtins/dispatch.ts +26 -0
- package/src/builtins/registry.ts +4 -0
- package/src/capabilities.ts +12 -1
- package/src/cli-errors.ts +3 -0
- package/src/cli-tool/full-example-capabilities.test.ts +3 -0
- package/src/cli.ts +60 -8
- package/src/config.integration.test.ts +22 -4
- package/src/context.ts +29 -1
- package/src/docs/api-guide.ts +2 -2
- package/src/docs/builtin.ts +11 -1
- package/src/docs/docs.test.ts +70 -12
- package/src/docs/http-guide.ts +132 -0
- package/src/docs/mcp-guide.ts +3 -3
- package/src/docs/mcp-resources.ts +5 -2
- package/src/docs/resolve.ts +26 -2
- package/src/docs/save.ts +5 -2
- package/src/headless/tool-call.ts +157 -0
- package/src/headless.test.ts +4 -2
- package/src/headless.ts +10 -5
- package/src/index.ts +5 -0
- package/src/mcp/result.ts +39 -34
- package/src/mcp/server.ts +18 -36
- package/src/mcp/tools.ts +14 -3
- package/src/mcp.integration.test.ts +46 -39
- package/src/parse.test.ts +16 -6
- package/src/respond.ts +48 -0
- package/src/schema.ts +1 -1
- package/src/skill/generate.ts +1 -1
- package/src/types.ts +46 -4
- package/src/validate.ts +7 -0
package/examples/mcp-test.ts
CHANGED
|
@@ -49,7 +49,12 @@ const program = {
|
|
|
49
49
|
],
|
|
50
50
|
handler: (ctx) => {
|
|
51
51
|
const name = ctx.stringOpt("name") ?? "";
|
|
52
|
-
|
|
52
|
+
const value = process.env[name] ?? "";
|
|
53
|
+
if (ctx.invocation === "cli") {
|
|
54
|
+
console.log(value);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
53
58
|
},
|
|
54
59
|
},
|
|
55
60
|
{
|
|
@@ -65,7 +70,13 @@ const program = {
|
|
|
65
70
|
},
|
|
66
71
|
],
|
|
67
72
|
handler: (ctx) => {
|
|
68
|
-
|
|
73
|
+
const mode = ctx.stringOpt("mode") ?? "";
|
|
74
|
+
const text = `mode=${mode}`;
|
|
75
|
+
if (ctx.invocation === "cli") {
|
|
76
|
+
console.log(text);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
return text;
|
|
69
80
|
},
|
|
70
81
|
},
|
|
71
82
|
],
|
package/examples/nested.ts
CHANGED
|
@@ -64,10 +64,19 @@ const program = {
|
|
|
64
64
|
process.exit(1);
|
|
65
65
|
}
|
|
66
66
|
if (ctx.hasFlag("json")) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
const payload = { user, path };
|
|
68
|
+
if (ctx.invocation === "cli") {
|
|
69
|
+
console.log(JSON.stringify(payload));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
return payload;
|
|
70
73
|
}
|
|
74
|
+
const text = `lookup user=${user} path=${path}`;
|
|
75
|
+
if (ctx.invocation === "cli") {
|
|
76
|
+
console.log(text);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
return text;
|
|
71
80
|
},
|
|
72
81
|
},
|
|
73
82
|
],
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/*
|
|
3
|
+
This example shows the smallest end-to-end CLI+MCP+API setup.
|
|
4
|
+
It includes one command, a couple of options, and a direct call to the runtime so
|
|
5
|
+
readers can copy the pattern into their own scripts quickly.
|
|
6
|
+
|
|
7
|
+
Demonstrates: `servers.ts hello`, MCP tool `hello`, and `POST /tools/hello`.
|
|
8
|
+
|
|
9
|
+
Ex API Call:
|
|
10
|
+
curl -s -X POST http://127.0.0.1:3000/tools/hello \
|
|
11
|
+
-H 'content-type: application/json' \
|
|
12
|
+
-d '{"name":"alice"}'
|
|
13
|
+
Ex API Response:
|
|
14
|
+
{ "greeting": "hello alice" }
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import pkg from "../package.json" with { type: "json" };
|
|
20
|
+
import { Cli, CliOptionKind, type CliProgram } from "../src/index.ts";
|
|
21
|
+
|
|
22
|
+
const program = {
|
|
23
|
+
key: "servers.ts",
|
|
24
|
+
version: pkg.version,
|
|
25
|
+
description: "Tiny demo.",
|
|
26
|
+
mcpServer: { enabled: true },
|
|
27
|
+
apiServer: { enabled: true },
|
|
28
|
+
docs: {
|
|
29
|
+
enabled: true,
|
|
30
|
+
topics: {
|
|
31
|
+
readme: { text: "# servers.ts\n\nServers demo.\n" },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
commands: [
|
|
35
|
+
{
|
|
36
|
+
key: "hello",
|
|
37
|
+
description: "Say hello.",
|
|
38
|
+
positionals: [
|
|
39
|
+
{
|
|
40
|
+
name: "name",
|
|
41
|
+
description: "Who to greet.",
|
|
42
|
+
kind: CliOptionKind.String,
|
|
43
|
+
argMin: 0,
|
|
44
|
+
argMax: 1,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
options: [
|
|
48
|
+
{
|
|
49
|
+
name: "verbose",
|
|
50
|
+
description: "Enable extra logging.",
|
|
51
|
+
kind: CliOptionKind.Presence,
|
|
52
|
+
shortName: "v",
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
handler: (ctx) => {
|
|
56
|
+
const name = ctx.args[0] ?? "world";
|
|
57
|
+
if (ctx.hasFlag("verbose") && ctx.invocation === "cli") {
|
|
58
|
+
console.log("verbose mode");
|
|
59
|
+
}
|
|
60
|
+
const greeting = `hello ${name}`;
|
|
61
|
+
if (ctx.invocation === "cli") {
|
|
62
|
+
console.log(greeting);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
return { greeting, verbose: ctx.hasFlag("verbose") };
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
} satisfies CliProgram;
|
|
70
|
+
|
|
71
|
+
const cli = new Cli(program);
|
|
72
|
+
await cli.run();
|
package/index.d.ts
CHANGED
|
@@ -57,8 +57,18 @@ export declare class CliContext {
|
|
|
57
57
|
readonly opts: Record<string, string>;
|
|
58
58
|
readonly invocation: CliInvocation;
|
|
59
59
|
readonly appConfig: AnyAppConfigSnapshot;
|
|
60
|
+
/** Original flat tool arguments for API/MCP invocations (when provided). */
|
|
61
|
+
readonly toolArgs?: Record<string, unknown>;
|
|
62
|
+
private response?;
|
|
60
63
|
/** Captures the program root, routed path, positional words, and option map for a leaf handler. */
|
|
61
|
-
constructor(appName: string, commandPath: string[], args: string[], opts: Record<string, string>, program: CliProgram, invocation?: CliInvocation, appConfig?: AnyAppConfigSnapshot);
|
|
64
|
+
constructor(appName: string, commandPath: string[], args: string[], opts: Record<string, string>, program: CliProgram, invocation?: CliInvocation, appConfig?: AnyAppConfigSnapshot, toolArgs?: Record<string, unknown>);
|
|
65
|
+
/**
|
|
66
|
+
* Sets the machine-readable response for API/MCP invocations, or writes to stdout in CLI mode.
|
|
67
|
+
* May only be called once per invocation.
|
|
68
|
+
*/
|
|
69
|
+
respond(opts: CliRespondOptions): void;
|
|
70
|
+
/** Returns the respond payload set by {@link respond}, if any. */
|
|
71
|
+
getResponse(): CliRespondOptions | undefined;
|
|
62
72
|
/** Returns whether a presence flag was set (including implicit "1" for boolean options). */
|
|
63
73
|
hasFlag(name: string): boolean;
|
|
64
74
|
/** Returns the string value for a string-valued option, if present. */
|
|
@@ -90,7 +100,7 @@ export declare class CliContext {
|
|
|
90
100
|
/**
|
|
91
101
|
* How a leaf handler was dispatched.
|
|
92
102
|
*/
|
|
93
|
-
export type CliInvocation = "cli" | "mcp";
|
|
103
|
+
export type CliInvocation = "cli" | "mcp" | "api";
|
|
94
104
|
/**
|
|
95
105
|
* Option kinds: presence (boolean flag), string (free-form text), number (strict double), or enum (fixed choices).
|
|
96
106
|
*/
|
|
@@ -227,6 +237,38 @@ export interface CliMcpServerConfig {
|
|
|
227
237
|
/** Optional MCP Bundle (`.mcpb`) metadata for `mcp bundle`. */
|
|
228
238
|
bundle?: CliMcpBundleConfig;
|
|
229
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Enables `myapp api` and the HTTP tool server (program root only).
|
|
242
|
+
* Must include `enabled: true`; omit `apiServer` entirely to disable HTTP.
|
|
243
|
+
*/
|
|
244
|
+
export interface CliApiServerConfig {
|
|
245
|
+
/** When `true`, enables the `api` built-in and HTTP tool server. */
|
|
246
|
+
enabled: boolean;
|
|
247
|
+
/** Listen host (default: `127.0.0.1`). */
|
|
248
|
+
host?: string;
|
|
249
|
+
/** Listen port (default: `3000`). */
|
|
250
|
+
port?: number;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Declarative HTTP response hints for a leaf (used by OpenAPI and default headers).
|
|
254
|
+
*/
|
|
255
|
+
export interface CliApiResponseConfig {
|
|
256
|
+
/** Default success Content-Type (default: `application/json`). */
|
|
257
|
+
contentType?: string;
|
|
258
|
+
/** Optional Content-Disposition (e.g. `attachment; filename="invoice.pdf"`). */
|
|
259
|
+
contentDisposition?: string;
|
|
260
|
+
}
|
|
261
|
+
/** Body types accepted by {@link CliContext.respond}. */
|
|
262
|
+
export type CliRespondBody = string | Uint8Array | Record<string, unknown> | unknown[];
|
|
263
|
+
/** Options for {@link CliContext.respond} and headless invoke results. */
|
|
264
|
+
export interface CliRespondOptions {
|
|
265
|
+
body: CliRespondBody;
|
|
266
|
+
/** Default: `application/json` for objects/arrays, `text/plain` for strings; binary requires explicit type. */
|
|
267
|
+
contentType?: string;
|
|
268
|
+
/** HTTP status (default: 200). */
|
|
269
|
+
status?: number;
|
|
270
|
+
headers?: Record<string, string>;
|
|
271
|
+
}
|
|
230
272
|
/**
|
|
231
273
|
* A custom MCP resource exposed under resources/list and resources/read.
|
|
232
274
|
*/
|
|
@@ -429,9 +471,13 @@ export type CliLeaf = CliNodeBase & {
|
|
|
429
471
|
positionals?: CliPositional[];
|
|
430
472
|
/**
|
|
431
473
|
* JSON Schema for structured stdout (e.g. with `--json` or MCP when the handler emits JSON).
|
|
432
|
-
* Exported in `docs schema`, `docs api`, and MCP `tools/list`; not validated at runtime yet.
|
|
474
|
+
* Exported in `docs cli-schema`, `docs api`, and MCP `tools/list`; not validated at runtime yet.
|
|
433
475
|
*/
|
|
434
476
|
outputSchema?: Record<string, unknown>;
|
|
477
|
+
/** JSON Schema for MCP/HTTP tool arguments (flat object). */
|
|
478
|
+
inputSchema?: Record<string, unknown>;
|
|
479
|
+
/** Declarative HTTP response metadata (Content-Type, Content-Disposition). */
|
|
480
|
+
apiResponse?: CliApiResponseConfig;
|
|
435
481
|
/** Per-tool MCP exposure and metadata. */
|
|
436
482
|
mcpTool?: CliMcpToolConfig;
|
|
437
483
|
};
|
|
@@ -461,6 +507,8 @@ export type CliProgram = CliNode & {
|
|
|
461
507
|
appConfig?: CliAppConfig;
|
|
462
508
|
/** When set with `enabled: true`, enables the `mcp` built-in subcommand. */
|
|
463
509
|
mcpServer?: CliMcpServerConfig;
|
|
510
|
+
/** When set with `enabled: true`, enables the `api` built-in HTTP server. */
|
|
511
|
+
apiServer?: CliApiServerConfig;
|
|
464
512
|
/** Opt-out and defaults for `configure`. */
|
|
465
513
|
configure?: CliConfigureConfig;
|
|
466
514
|
/** Opt-out for shell completion generation (`completion bash|zsh|fish`). */
|
|
@@ -470,9 +518,9 @@ export type CliProgram = CliNode & {
|
|
|
470
518
|
};
|
|
471
519
|
/**
|
|
472
520
|
* Handler closure type for leaf commands.
|
|
473
|
-
* Supports
|
|
521
|
+
* Supports sync and async handlers; non-undefined return values become implicit JSON responses for headless invocations.
|
|
474
522
|
*/
|
|
475
|
-
export type CliHandler = (ctx: CliContext) =>
|
|
523
|
+
export type CliHandler = (ctx: CliContext) => unknown | Promise<unknown>;
|
|
476
524
|
/**
|
|
477
525
|
* Error thrown when the static CLI tree violates ArgsBarg rules.
|
|
478
526
|
*/
|
|
@@ -480,8 +528,13 @@ export declare class CliSchemaValidationError extends Error {
|
|
|
480
528
|
/** Creates a schema validation error with a human-readable rule violation. */
|
|
481
529
|
constructor(message: string);
|
|
482
530
|
}
|
|
531
|
+
/** Generates an OpenAPI 3.1 document for the program's exposed tools. */
|
|
532
|
+
export declare function generateOpenApi(program: CliProgram): Record<string, unknown>;
|
|
533
|
+
/** Pretty-printed OpenAPI JSON (same document as `GET /openapi.json`). */
|
|
534
|
+
export declare function openApiJson(program: CliProgram): string;
|
|
483
535
|
/** Platform builtins derived from program config and runtime. */
|
|
484
536
|
export interface CliCapabilities {
|
|
537
|
+
api: boolean;
|
|
485
538
|
completion: boolean;
|
|
486
539
|
mcp: boolean;
|
|
487
540
|
configure: boolean;
|
|
@@ -510,6 +563,8 @@ export interface CliInvokeResult {
|
|
|
510
563
|
stdout: string;
|
|
511
564
|
stderr: string;
|
|
512
565
|
errorMsg?: string;
|
|
566
|
+
/** Headless response payload when invocation is `api` or `mcp` and the handler succeeded. */
|
|
567
|
+
response?: CliRespondOptions;
|
|
513
568
|
}
|
|
514
569
|
/** Argsbarg runtime for a validated, frozen {@link CliProgram}. */
|
|
515
570
|
export declare class Cli {
|
|
@@ -523,8 +578,12 @@ export declare class Cli {
|
|
|
523
578
|
exportCommandSchema(): CliSchemaExport;
|
|
524
579
|
exportAppConfigSchema(): Record<string, unknown> | undefined;
|
|
525
580
|
run(argv?: string[]): Promise<never>;
|
|
526
|
-
invoke(argv: string[]
|
|
581
|
+
invoke(argv: string[], opts?: {
|
|
582
|
+
invocation?: CliInvocation;
|
|
583
|
+
toolArgs?: Record<string, unknown>;
|
|
584
|
+
}): Promise<CliInvokeResult>;
|
|
527
585
|
serveMcp(): Promise<never>;
|
|
586
|
+
serveApi(): Promise<never>;
|
|
528
587
|
private prepareDispatch;
|
|
529
588
|
private buildAppConfigSnapshot;
|
|
530
589
|
}
|
|
@@ -539,7 +598,7 @@ export declare function parseDate(s: string): string;
|
|
|
539
598
|
export declare function parseDateTime(s: string): string;
|
|
540
599
|
/** Minimal context for headless routing helpers. */
|
|
541
600
|
export type HeadlessContext = Pick<CliContext, "invocation">;
|
|
542
|
-
/** True when `--json` was passed or the handler was invoked
|
|
601
|
+
/** True when `--json` was passed or the handler was invoked headlessly over MCP/HTTP. */
|
|
543
602
|
export declare function wantsExplicitJson(ctx: HeadlessContext, hasJsonFlag: boolean): boolean;
|
|
544
603
|
/**
|
|
545
604
|
* Headless when MCP, `--json`, `--dry-run`, or stdin is not a TTY.
|
package/package.json
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "argsbarg",
|
|
3
|
-
"version": "
|
|
4
|
-
"type": "module",
|
|
5
|
-
"engines": {
|
|
6
|
-
"bun": ">=1.3"
|
|
7
|
-
},
|
|
8
|
-
"scripts": {
|
|
9
|
-
"//just": "echo this app uses justfile for development tasks"
|
|
10
|
-
},
|
|
3
|
+
"version": "6.0.1",
|
|
11
4
|
"main": "./src/index.ts",
|
|
12
5
|
"module": "./src/index.ts",
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
"
|
|
6
|
+
"devDependencies": {
|
|
7
|
+
"@biomejs/biome": "^2.5.0",
|
|
8
|
+
"@types/bun": "^1.3.12",
|
|
9
|
+
"dts-bundle-generator": "^9.5.1",
|
|
10
|
+
"typescript": "^5.9.3"
|
|
16
11
|
},
|
|
17
12
|
"exports": {
|
|
18
13
|
".": {
|
|
@@ -20,6 +15,12 @@
|
|
|
20
15
|
"default": "./src/index.ts"
|
|
21
16
|
}
|
|
22
17
|
},
|
|
18
|
+
"bin": {
|
|
19
|
+
"argsbarg": "src/cli-tool/main.ts"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"bun": ">=1.3"
|
|
23
|
+
},
|
|
23
24
|
"files": [
|
|
24
25
|
"src",
|
|
25
26
|
"index.d.ts",
|
|
@@ -29,10 +30,9 @@
|
|
|
29
30
|
"LICENSE",
|
|
30
31
|
"CHANGELOG.md"
|
|
31
32
|
],
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
33
|
+
"scripts": {
|
|
34
|
+
"//just": "echo this app uses justfile for development tasks"
|
|
35
|
+
},
|
|
36
|
+
"type": "module",
|
|
37
|
+
"types": "./index.d.ts"
|
|
38
38
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Hand-built OpenAPI 3.1 document from exposed MCP tools.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { collectMcpTools } from "../mcp/tools.ts";
|
|
6
|
+
import type { CliProgram } from "../types.ts";
|
|
7
|
+
import { dereferenceJsonSchema } from "./schema-deref.ts";
|
|
8
|
+
|
|
9
|
+
const JSON_CONTENT_TYPE = "application/json; charset=utf-8";
|
|
10
|
+
|
|
11
|
+
/** Resolves the effective API content type for OpenAPI response mapping. */
|
|
12
|
+
function effectiveApiContentType(tool: ReturnType<typeof collectMcpTools>[number]): string {
|
|
13
|
+
return tool.leaf.apiResponse?.contentType ?? "application/json";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Builds an OpenAPI 3.1 response schema for a tool's success payload. */
|
|
17
|
+
function buildSuccessResponse(tool: ReturnType<typeof collectMcpTools>[number]): Record<string, unknown> {
|
|
18
|
+
const contentType = effectiveApiContentType(tool);
|
|
19
|
+
const media: Record<string, unknown> = {};
|
|
20
|
+
|
|
21
|
+
if (contentType.includes("application/json")) {
|
|
22
|
+
const outputSchema = tool.outputSchema ?? { type: "object" };
|
|
23
|
+
media[contentType] = {
|
|
24
|
+
schema: dereferenceJsonSchema(outputSchema),
|
|
25
|
+
};
|
|
26
|
+
} else if (contentType.includes("text/html")) {
|
|
27
|
+
media[contentType] = { schema: { type: "string" } };
|
|
28
|
+
} else {
|
|
29
|
+
media[contentType] = { schema: { type: "string", format: "binary" } };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
description: "Successful tool invocation",
|
|
34
|
+
content: media,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Generates an OpenAPI 3.1 document for the program's exposed tools. */
|
|
39
|
+
export function generateOpenApi(program: CliProgram): Record<string, unknown> {
|
|
40
|
+
const tools = collectMcpTools(program);
|
|
41
|
+
const paths: Record<string, unknown> = {};
|
|
42
|
+
|
|
43
|
+
for (const tool of tools) {
|
|
44
|
+
const pathKey = `/tools/${tool.apiName}`;
|
|
45
|
+
paths[pathKey] = {
|
|
46
|
+
post: {
|
|
47
|
+
operationId: tool.apiName,
|
|
48
|
+
summary: tool.description,
|
|
49
|
+
requestBody: {
|
|
50
|
+
required: false,
|
|
51
|
+
content: {
|
|
52
|
+
[JSON_CONTENT_TYPE]: {
|
|
53
|
+
schema: dereferenceJsonSchema(tool.inputSchema),
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
responses: {
|
|
58
|
+
"200": buildSuccessResponse(tool),
|
|
59
|
+
"400": {
|
|
60
|
+
description: "Invalid arguments or help requested",
|
|
61
|
+
content: {
|
|
62
|
+
[JSON_CONTENT_TYPE]: {
|
|
63
|
+
schema: {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {
|
|
66
|
+
error: { type: "string" },
|
|
67
|
+
exitCode: { type: "integer" },
|
|
68
|
+
},
|
|
69
|
+
required: ["error"],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
"404": {
|
|
75
|
+
description: "Unknown tool",
|
|
76
|
+
content: {
|
|
77
|
+
[JSON_CONTENT_TYPE]: {
|
|
78
|
+
schema: {
|
|
79
|
+
type: "object",
|
|
80
|
+
properties: { error: { type: "string" } },
|
|
81
|
+
required: ["error"],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
"500": {
|
|
87
|
+
description: "Handler error",
|
|
88
|
+
content: {
|
|
89
|
+
[JSON_CONTENT_TYPE]: {
|
|
90
|
+
schema: {
|
|
91
|
+
type: "object",
|
|
92
|
+
properties: { error: { type: "string" } },
|
|
93
|
+
required: ["error"],
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
openapi: "3.1.0",
|
|
105
|
+
info: {
|
|
106
|
+
title: program.key,
|
|
107
|
+
version: program.version,
|
|
108
|
+
description: program.description,
|
|
109
|
+
},
|
|
110
|
+
paths,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Pretty-printed OpenAPI JSON (same document as `GET /openapi.json`). */
|
|
115
|
+
export function openApiJson(program: CliProgram): string {
|
|
116
|
+
return `${JSON.stringify(generateOpenApi(program), null, 2)}\n`;
|
|
117
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Maps headless respond payloads to native HTTP Response objects.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { CliApiResponseConfig, CliRespondOptions } from "../types.ts";
|
|
6
|
+
|
|
7
|
+
/** JSON body for a failed HTTP tool invocation. */
|
|
8
|
+
export interface ApiToolCallErrorBody {
|
|
9
|
+
error: string;
|
|
10
|
+
exitCode?: number;
|
|
11
|
+
stdout?: string;
|
|
12
|
+
stderr?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Strips ANSI escape sequences from CLI-formatted text. */
|
|
16
|
+
export function stripAnsi(text: string): string {
|
|
17
|
+
const ansiEscape = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
18
|
+
return text.replace(ansiEscape, "");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Returns the first non-empty line of text with ANSI escapes removed. */
|
|
22
|
+
export function firstErrorLine(text: string): string {
|
|
23
|
+
const line = stripAnsi(text)
|
|
24
|
+
.split("\n")
|
|
25
|
+
.map((part) => part.trim())
|
|
26
|
+
.find((part) => part.length > 0);
|
|
27
|
+
return line ?? stripAnsi(text).trim();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Wide-open CORS headers applied to all API responses. */
|
|
31
|
+
export const API_CORS_HEADERS: Readonly<Record<string, string>> = {
|
|
32
|
+
"access-control-allow-origin": "*",
|
|
33
|
+
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
34
|
+
"access-control-allow-headers": "Content-Type, Authorization",
|
|
35
|
+
"access-control-max-age": "86400",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Builds a 204 OPTIONS preflight response with CORS headers. */
|
|
39
|
+
export function apiOptionsResponse(): Response {
|
|
40
|
+
return new Response(null, { status: 204, headers: { ...API_CORS_HEADERS } });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Resolves effective Content-Type for a respond payload. */
|
|
44
|
+
export function resolveRespondContentType(response: CliRespondOptions, leafApiResponse?: CliApiResponseConfig): string {
|
|
45
|
+
return (
|
|
46
|
+
response.contentType ??
|
|
47
|
+
leafApiResponse?.contentType ??
|
|
48
|
+
(typeof response.body === "string" ? "text/plain; charset=utf-8" : "application/json; charset=utf-8")
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Builds a native HTTP Response from a successful headless respond payload. */
|
|
53
|
+
export function apiSuccessResponse(response: CliRespondOptions, leafApiResponse?: CliApiResponseConfig): Response {
|
|
54
|
+
const contentType = resolveRespondContentType(response, leafApiResponse);
|
|
55
|
+
const headers: Record<string, string> = {
|
|
56
|
+
...API_CORS_HEADERS,
|
|
57
|
+
"content-type": contentType,
|
|
58
|
+
...(response.headers ?? {}),
|
|
59
|
+
};
|
|
60
|
+
if (leafApiResponse?.contentDisposition && !headers["content-disposition"]) {
|
|
61
|
+
headers["content-disposition"] = leafApiResponse.contentDisposition;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const status = response.status ?? 200;
|
|
65
|
+
const { body } = response;
|
|
66
|
+
|
|
67
|
+
if (body instanceof Uint8Array) {
|
|
68
|
+
return new Response(body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength) as ArrayBuffer, {
|
|
69
|
+
status,
|
|
70
|
+
headers,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (typeof body === "string") {
|
|
74
|
+
return new Response(body, { status, headers });
|
|
75
|
+
}
|
|
76
|
+
return new Response(JSON.stringify(body), { status, headers });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Builds a JSON error HTTP Response with CORS headers. */
|
|
80
|
+
export function apiErrorResponse(status: number, body: ApiToolCallErrorBody): Response {
|
|
81
|
+
return new Response(JSON.stringify(body), {
|
|
82
|
+
status,
|
|
83
|
+
headers: {
|
|
84
|
+
...API_CORS_HEADERS,
|
|
85
|
+
"content-type": "application/json; charset=utf-8",
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Scalar API reference HTML served at GET /openapi-browser. */
|
|
91
|
+
export function apiDocsHtml(): string {
|
|
92
|
+
return `<!doctype html>
|
|
93
|
+
<html lang="en">
|
|
94
|
+
<head>
|
|
95
|
+
<meta charset="utf-8" />
|
|
96
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
97
|
+
<title>API Reference</title>
|
|
98
|
+
</head>
|
|
99
|
+
<body>
|
|
100
|
+
<div id="api-reference"></div>
|
|
101
|
+
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
|
|
102
|
+
<script>
|
|
103
|
+
Scalar.createApiReference("#api-reference", {
|
|
104
|
+
url: "/openapi.json",
|
|
105
|
+
orderSchemaPropertiesBy: "preserve",
|
|
106
|
+
orderRequiredPropertiesFirst: false,
|
|
107
|
+
});
|
|
108
|
+
</script>
|
|
109
|
+
</body>
|
|
110
|
+
</html>`;
|
|
111
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { dereferenceJsonSchema } from "./schema-deref.ts";
|
|
3
|
+
|
|
4
|
+
test("dereferenceJsonSchema inlines nested definitions", () => {
|
|
5
|
+
const schema = {
|
|
6
|
+
type: "object",
|
|
7
|
+
properties: {
|
|
8
|
+
invoice: { $ref: "#/definitions/InvoiceData" },
|
|
9
|
+
},
|
|
10
|
+
definitions: {
|
|
11
|
+
InvoiceData: {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: { id: { type: "string" } },
|
|
14
|
+
required: ["id"],
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
const out = dereferenceJsonSchema(schema);
|
|
19
|
+
expect(out.properties).toEqual({
|
|
20
|
+
invoice: {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: { id: { type: "string" } },
|
|
23
|
+
required: ["id"],
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
expect(out.definitions).toBeUndefined();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("dereferenceJsonSchema supports $defs", () => {
|
|
30
|
+
const schema = {
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: {
|
|
33
|
+
item: { $ref: "#/$defs/Item" },
|
|
34
|
+
},
|
|
35
|
+
$defs: {
|
|
36
|
+
Item: { type: "string" },
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
const out = dereferenceJsonSchema(schema);
|
|
40
|
+
expect(out.properties).toEqual({ item: { type: "string" } });
|
|
41
|
+
expect(out.$defs).toBeUndefined();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("dereferenceJsonSchema merges $ref siblings", () => {
|
|
45
|
+
const schema = {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
invoice: {
|
|
49
|
+
$ref: "#/definitions/InvoiceData",
|
|
50
|
+
description: "Invoice payload",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
definitions: {
|
|
54
|
+
InvoiceData: { type: "object" },
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
const out = dereferenceJsonSchema(schema) as {
|
|
58
|
+
properties: { invoice: { type: string; description: string } };
|
|
59
|
+
};
|
|
60
|
+
expect(out.properties.invoice).toEqual({
|
|
61
|
+
type: "object",
|
|
62
|
+
description: "Invoice payload",
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("dereferenceJsonSchema ignores circular refs", () => {
|
|
67
|
+
const schema = {
|
|
68
|
+
type: "object",
|
|
69
|
+
properties: {
|
|
70
|
+
self: { $ref: "#/definitions/Node" },
|
|
71
|
+
},
|
|
72
|
+
definitions: {
|
|
73
|
+
Node: {
|
|
74
|
+
type: "object",
|
|
75
|
+
properties: {
|
|
76
|
+
again: { $ref: "#/definitions/Node" },
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
const out = dereferenceJsonSchema(schema) as {
|
|
82
|
+
properties: { self: { type: string; properties: { again: { $ref: string } } } };
|
|
83
|
+
};
|
|
84
|
+
expect(out.properties.self.type).toBe("object");
|
|
85
|
+
expect(out.properties.self.properties.again).toEqual({ $ref: "#/definitions/Node" });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("dereferenceJsonSchema leaves external refs unchanged", () => {
|
|
89
|
+
const schema = {
|
|
90
|
+
type: "object",
|
|
91
|
+
properties: {
|
|
92
|
+
remote: { $ref: "https://example.com/schema.json" },
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
const out = dereferenceJsonSchema(schema);
|
|
96
|
+
expect(out.properties).toEqual({
|
|
97
|
+
remote: { $ref: "https://example.com/schema.json" },
|
|
98
|
+
});
|
|
99
|
+
});
|