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.
Files changed (73) hide show
  1. package/CHANGELOG.md +46 -1
  2. package/README.md +33 -25
  3. package/docs/README.md +2 -1
  4. package/docs/api-server.md +141 -0
  5. package/docs/bundled-docs.md +24 -10
  6. package/docs/cli-program.md +17 -2
  7. package/docs/config-schema.md +18 -8
  8. package/docs/developing.md +1 -1
  9. package/docs/mcp.md +5 -5
  10. package/docs/output-schema.md +78 -47
  11. package/examples/full-example/README.md +15 -6
  12. package/examples/full-example/docs/README.md +27 -0
  13. package/examples/full-example/docs/api.md +511 -0
  14. package/examples/full-example/docs/cli-schema.json +453 -0
  15. package/examples/full-example/docs/http.md +81 -0
  16. package/examples/full-example/docs/mcp.md +159 -0
  17. package/examples/full-example/docs/openapi.json +222 -0
  18. package/examples/full-example/docs/skill.md +46 -0
  19. package/examples/full-example/justfile +6 -2
  20. package/examples/full-example/schemas/generated/app-config.json +1 -1
  21. package/examples/full-example/schemas/generated/status.json +1 -1
  22. package/examples/full-example/scripts/dev-formula.ts +1 -1
  23. package/examples/full-example/scripts/schemagen/discover-schema-roots.test.ts +3 -3
  24. package/examples/full-example/scripts/schemagen/discover-schema-roots.ts +90 -35
  25. package/examples/full-example/scripts/schemagen/naming.ts +17 -0
  26. package/examples/full-example/scripts/schemagen.ts +14 -3
  27. package/examples/full-example/src/commands/echo/command.ts +6 -1
  28. package/examples/full-example/src/commands/status/schema-types.ts +14 -0
  29. package/examples/full-example/src/commands/status/types.ts +1 -11
  30. package/examples/full-example/src/{types.ts → config/schema-types.ts} +3 -2
  31. package/examples/full-example/src/program.ts +3 -0
  32. package/examples/mcp-test.ts +13 -2
  33. package/examples/nested.ts +12 -3
  34. package/examples/servers.ts +72 -0
  35. package/index.d.ts +66 -7
  36. package/package.json +17 -17
  37. package/src/api/openapi.ts +117 -0
  38. package/src/api/result.ts +111 -0
  39. package/src/api/schema-deref.test.ts +99 -0
  40. package/src/api/schema-deref.ts +76 -0
  41. package/src/api/server.ts +120 -0
  42. package/src/api.integration.test.ts +441 -0
  43. package/src/builtins/api.ts +38 -0
  44. package/src/builtins/dispatch.ts +26 -0
  45. package/src/builtins/registry.ts +4 -0
  46. package/src/capabilities.ts +12 -1
  47. package/src/cli-errors.ts +3 -0
  48. package/src/cli-tool/full-example-capabilities.test.ts +3 -0
  49. package/src/cli.ts +60 -8
  50. package/src/config.integration.test.ts +22 -4
  51. package/src/context.ts +29 -1
  52. package/src/docs/api-guide.ts +2 -2
  53. package/src/docs/builtin.ts +11 -1
  54. package/src/docs/docs.test.ts +70 -12
  55. package/src/docs/http-guide.ts +132 -0
  56. package/src/docs/mcp-guide.ts +3 -3
  57. package/src/docs/mcp-resources.ts +5 -2
  58. package/src/docs/resolve.ts +26 -2
  59. package/src/docs/save.ts +5 -2
  60. package/src/headless/tool-call.ts +157 -0
  61. package/src/headless.test.ts +4 -2
  62. package/src/headless.ts +10 -5
  63. package/src/index.ts +5 -0
  64. package/src/mcp/result.ts +39 -34
  65. package/src/mcp/server.ts +18 -36
  66. package/src/mcp/tools.ts +14 -3
  67. package/src/mcp.integration.test.ts +46 -39
  68. package/src/parse.test.ts +16 -6
  69. package/src/respond.ts +48 -0
  70. package/src/schema.ts +1 -1
  71. package/src/skill/generate.ts +1 -1
  72. package/src/types.ts +46 -4
  73. package/src/validate.ts +7 -0
@@ -49,7 +49,12 @@ const program = {
49
49
  ],
50
50
  handler: (ctx) => {
51
51
  const name = ctx.stringOpt("name") ?? "";
52
- console.log(process.env[name] ?? "");
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
- console.log(`mode=${ctx.stringOpt("mode")}`);
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
  ],
@@ -64,10 +64,19 @@ const program = {
64
64
  process.exit(1);
65
65
  }
66
66
  if (ctx.hasFlag("json")) {
67
- console.log(JSON.stringify({ user, path }));
68
- } else {
69
- console.log(`lookup user=${user} path=${path}`);
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 both sync and async handlers.
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) => void | Promise<void>;
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[]): Promise<CliInvokeResult>;
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 via MCP. */
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": "5.1.16",
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
- "types": "./index.d.ts",
14
- "bin": {
15
- "argsbarg": "src/cli-tool/main.ts"
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
- "devDependencies": {
33
- "@biomejs/biome": "^2.5.0",
34
- "@types/bun": "^1.3.12",
35
- "dts-bundle-generator": "^9.5.1",
36
- "typescript": "^5.9.3"
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
+ });