argsbarg 5.1.15 → 6.0.0

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 (63) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/README.md +32 -24
  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/configure.md +2 -2
  8. package/docs/developing.md +1 -1
  9. package/docs/mcp.md +5 -5
  10. package/docs/output-schema.md +3 -3
  11. package/examples/full-example/README.md +8 -0
  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/scripts/dev-formula.ts +1 -1
  21. package/examples/full-example/src/commands/echo/command.ts +6 -1
  22. package/examples/full-example/src/program.ts +3 -0
  23. package/examples/mcp-test.ts +13 -2
  24. package/examples/nested.ts +12 -3
  25. package/examples/servers.ts +72 -0
  26. package/index.d.ts +66 -7
  27. package/package.json +1 -1
  28. package/src/api/openapi.ts +115 -0
  29. package/src/api/result.ts +89 -0
  30. package/src/api/server.ts +120 -0
  31. package/src/api.integration.test.ts +358 -0
  32. package/src/builtins/api.ts +38 -0
  33. package/src/builtins/dispatch.ts +26 -0
  34. package/src/builtins/registry.ts +4 -0
  35. package/src/capabilities.ts +12 -1
  36. package/src/cli-tool/full-example-capabilities.test.ts +3 -0
  37. package/src/cli.ts +60 -8
  38. package/src/config/bootstrap.test.ts +46 -0
  39. package/src/config/bootstrap.ts +22 -5
  40. package/src/config.integration.test.ts +22 -4
  41. package/src/configure/index.ts +1 -1
  42. package/src/context.ts +29 -1
  43. package/src/docs/api-guide.ts +2 -2
  44. package/src/docs/builtin.ts +11 -1
  45. package/src/docs/docs.test.ts +70 -12
  46. package/src/docs/http-guide.ts +132 -0
  47. package/src/docs/mcp-guide.ts +3 -3
  48. package/src/docs/resolve.ts +26 -2
  49. package/src/docs/save.ts +5 -2
  50. package/src/headless/tool-call.ts +147 -0
  51. package/src/headless.test.ts +4 -2
  52. package/src/headless.ts +10 -5
  53. package/src/index.ts +5 -0
  54. package/src/mcp/result.ts +39 -34
  55. package/src/mcp/server.ts +18 -36
  56. package/src/mcp/tools.ts +14 -3
  57. package/src/mcp.integration.test.ts +46 -39
  58. package/src/parse.test.ts +16 -6
  59. package/src/respond.ts +48 -0
  60. package/src/schema.ts +1 -1
  61. package/src/skill/generate.ts +1 -1
  62. package/src/types.ts +46 -4
  63. package/src/validate.ts +7 -0
@@ -0,0 +1,38 @@
1
+ import { resolveApiListenAddress } from "../api/server.ts";
2
+ import { resolveCapabilities } from "../capabilities.ts";
3
+ import { docsEnabled } from "../docs/resolve.ts";
4
+ import { CliFallbackMode, type CliLeaf, type CliProgram, type CliRouter } from "../types.ts";
5
+
6
+ /** Built-in `api` router: bare `myapp api` runs the HTTP server (via hidden `serve` fallback). */
7
+ export function cliBuiltinApiCommand(program: CliProgram): CliRouter {
8
+ const caps = resolveCapabilities(program);
9
+ const { hostname, port } = resolveApiListenAddress(program);
10
+ const lines = [
11
+ `HTTP tool server on http://${hostname}:${port}.`,
12
+ "",
13
+ "Endpoints: GET /health, GET /openapi.json, GET /openapi-browser, POST /tools/:name",
14
+ "",
15
+ ];
16
+ if (caps.configure) {
17
+ lines.push("Configure app settings:", "", " {argsbarg:program} configure", "");
18
+ }
19
+ if (docsEnabled(program)) {
20
+ lines.push("Full setup guide: {argsbarg:program} docs http");
21
+ }
22
+
23
+ const serve: CliLeaf = {
24
+ key: "serve",
25
+ hidden: true,
26
+ description: "Run as an HTTP API server for tools.",
27
+ handler: () => {},
28
+ };
29
+
30
+ return {
31
+ key: "api",
32
+ description: "HTTP API server for tools.",
33
+ notes: lines.join("\n"),
34
+ fallbackCommand: "serve",
35
+ fallbackMode: CliFallbackMode.MissingOnly,
36
+ commands: [serve],
37
+ };
38
+ }
@@ -7,6 +7,7 @@ import type { ParseResult } from "../parse.ts";
7
7
  import { ParseKind } from "../parse.ts";
8
8
  import type { CliNode, CliProgram, CliRouter } from "../types.ts";
9
9
  import { isCliLeaf } from "../types.ts";
10
+ import { cliBuiltinApiCommand } from "./api.ts";
10
11
  import { completionBashScript } from "./completion-bash.ts";
11
12
  import { completionFishScript } from "./completion-fish.ts";
12
13
  import { cliBuiltinCompletionGroup as completionGroup } from "./completion-group.ts";
@@ -68,6 +69,20 @@ export async function dispatchBuiltin(program: CliProgram, pr: ParseResult, opts
68
69
  process.exit(0);
69
70
  }
70
71
 
72
+ if (pr.path[0] === "api") {
73
+ if (!caps.api) {
74
+ process.stderr.write(capabilityDeniedMessage("api"));
75
+ process.exit(1);
76
+ }
77
+ const sub = pr.path[1];
78
+ if (pr.path.length === 1 || sub === "serve") {
79
+ await new Cli(program).serveApi();
80
+ process.exit(0);
81
+ }
82
+ process.stderr.write(`Unknown subcommand: api ${pr.path.slice(1).join(" ")}\n`);
83
+ process.exit(1);
84
+ }
85
+
71
86
  if (pr.path[0] === "mcp") {
72
87
  if (!caps.mcp) {
73
88
  process.stderr.write(capabilityDeniedMessage("mcp"));
@@ -142,6 +157,17 @@ export function builtinInterceptRoot(
142
157
  };
143
158
  }
144
159
 
160
+ if (first === "api" && caps.api) {
161
+ return {
162
+ parseRoot: {
163
+ key: program.key,
164
+ description: program.description,
165
+ commands: [cliBuiltinApiCommand(program)],
166
+ },
167
+ isLeafCompletionIntercept: false,
168
+ };
169
+ }
170
+
145
171
  if (first === "mcp" && caps.mcp) {
146
172
  return {
147
173
  parseRoot: {
@@ -1,6 +1,7 @@
1
1
  import type { CliCapabilities } from "../capabilities.ts";
2
2
  import { cliBuiltinDocsGroupIfEnabled } from "../docs/builtin.ts";
3
3
  import type { CliNode, CliProgram } from "../types.ts";
4
+ import { cliBuiltinApiCommand } from "./api.ts";
4
5
  import { cliBuiltinCompletionGroup } from "./completion-group.ts";
5
6
  import { cliBuiltinConfigureCommand } from "./configure.ts";
6
7
  import { cliBuiltinMcpCommand } from "./mcp.ts";
@@ -32,5 +33,8 @@ export function resolveBuiltins(program: CliProgram, caps: CliCapabilities): Cli
32
33
  if (caps.mcp) {
33
34
  pushBuiltin(builtins, program, (p) => cliBuiltinMcpCommand(p));
34
35
  }
36
+ if (caps.api) {
37
+ pushBuiltin(builtins, program, (p) => cliBuiltinApiCommand(p));
38
+ }
35
39
  return builtins;
36
40
  }
@@ -8,6 +8,7 @@ import type { CliProgram } from "./types.ts";
8
8
 
9
9
  /** Platform builtins derived from program config and runtime. */
10
10
  export interface CliCapabilities {
11
+ api: boolean;
11
12
  completion: boolean;
12
13
  mcp: boolean;
13
14
  configure: boolean;
@@ -19,6 +20,7 @@ export interface CliCapabilities {
19
20
  export function resolveCapabilities(program: CliProgram): CliCapabilities {
20
21
  const configure = program.configure?.enabled !== false;
21
22
  return {
23
+ api: program.apiServer?.enabled === true,
22
24
  completion: program.completion?.enabled !== false,
23
25
  mcp: program.mcpServer?.enabled === true,
24
26
  configure,
@@ -42,6 +44,9 @@ export function reservedCommandNames(caps: CliCapabilities): string[] {
42
44
  if (caps.mcp) {
43
45
  names.push("mcp");
44
46
  }
47
+ if (caps.api) {
48
+ names.push("api");
49
+ }
45
50
  return names;
46
51
  }
47
52
 
@@ -60,13 +65,15 @@ export function skipsRequiredAppConfigExit(path: string[], caps: CliCapabilities
60
65
  return false;
61
66
  }
62
67
 
63
- export type CapabilityFeature = "mcp" | "configure" | "docs" | "completion";
68
+ export type CapabilityFeature = "api" | "mcp" | "configure" | "docs" | "completion";
64
69
 
65
70
  /** Stderr message when a disabled built-in is invoked from the CLI. */
66
71
  export function capabilityDeniedMessage(feature: CapabilityFeature): string {
67
72
  switch (feature) {
68
73
  case "completion":
69
74
  return "Shell completion is not available for this app.\n";
75
+ case "api":
76
+ return "HTTP API is not available for this app.\n";
70
77
  case "mcp":
71
78
  return "MCP is not available for this app.\n";
72
79
  case "configure":
@@ -90,6 +97,10 @@ export function assertBuiltinAllowed(argv: string[], caps: CliCapabilities): voi
90
97
  process.stderr.write(capabilityDeniedMessage("mcp"));
91
98
  process.exit(1);
92
99
  }
100
+ if (first === "api" && !caps.api) {
101
+ process.stderr.write(capabilityDeniedMessage("api"));
102
+ process.exit(1);
103
+ }
93
104
  if (first === "configure" && !caps.configure) {
94
105
  process.stderr.write(capabilityDeniedMessage("configure"));
95
106
  process.exit(1);
@@ -26,6 +26,7 @@ const sinkProgram = {
26
26
  topics: { readme: { text: "# readme\n" } },
27
27
  },
28
28
  mcpServer: { enabled: true },
29
+ apiServer: { enabled: true },
29
30
  configure: {},
30
31
  commands: [
31
32
  {
@@ -54,6 +55,7 @@ describe("full-example template", () => {
54
55
  /** Tests that program source enables every builtin flag. */
55
56
  test("program source enables every builtin flag", () => {
56
57
  expect(programSource).toContain("mcpServer: {");
58
+ expect(programSource).toContain("apiServer: {");
57
59
  expect(programSource).toContain("enabled: true");
58
60
  expect(programSource).toContain("docs:");
59
61
  expect(programSource).toContain("appConfig:");
@@ -66,6 +68,7 @@ describe("full-example template", () => {
66
68
 
67
69
  test("resolveCapabilities matches full sink shape", () => {
68
70
  expect(resolveCapabilities(sinkProgram)).toEqual({
71
+ api: true,
69
72
  completion: true,
70
73
  mcp: true,
71
74
  configure: true,
package/src/cli.ts CHANGED
@@ -3,6 +3,7 @@ Runtime entry point: validate program, cache derived state, run / invoke / MCP s
3
3
  */
4
4
 
5
5
  import { format } from "node:util";
6
+ import { apiServeHttp } from "./api/server.ts";
6
7
  import { builtinInterceptRoot, dispatchBuiltin } from "./builtins/dispatch.ts";
7
8
  import { cliParseRoot, cliPresentationRoot } from "./builtins/presentation.ts";
8
9
  import {
@@ -21,7 +22,7 @@ import { bootstrapMcpEnv } from "./mcp/env.ts";
21
22
  import { mcpServeStdioLoop } from "./mcp/server.ts";
22
23
  import { ParseKind, type ParseResult, parse, postParseValidate } from "./parse.ts";
23
24
  import { type CliSchemaExport, cliSchemaExport } from "./schema.ts";
24
- import type { CliHandler, CliLeaf, CliNode, CliProgram, CliRouter } from "./types.ts";
25
+ import type { CliHandler, CliInvocation, CliLeaf, CliNode, CliProgram, CliRespondOptions, CliRouter } from "./types.ts";
25
26
  import { isCliLeaf, isCliRouter } from "./types.ts";
26
27
  import { cliValidateProgram } from "./validate.ts";
27
28
 
@@ -35,6 +36,8 @@ export interface CliInvokeResult {
35
36
  stdout: string;
36
37
  stderr: string;
37
38
  errorMsg?: string;
39
+ /** Headless response payload when invocation is `api` or `mcp` and the handler succeeded. */
40
+ response?: CliRespondOptions;
38
41
  }
39
42
 
40
43
  class CliInvokeExit extends Error {
@@ -123,7 +126,10 @@ export class Cli {
123
126
 
124
127
  const ctx = new CliContext(this.program.key, pr.path, pr.args, pr.opts, this.program, "cli", snapshot);
125
128
  try {
126
- await Promise.resolve(leaf.handler(ctx));
129
+ const handlerResult = await Promise.resolve(leaf.handler(ctx));
130
+ if (handlerResult !== undefined && ctx.getResponse() === undefined) {
131
+ ctx.respond({ body: handlerResult as CliRespondOptions["body"] });
132
+ }
127
133
  process.exit(0);
128
134
  } catch (err) {
129
135
  if (err instanceof Error) {
@@ -133,7 +139,11 @@ export class Cli {
133
139
  }
134
140
  }
135
141
 
136
- async invoke(argv: string[]): Promise<CliInvokeResult> {
142
+ async invoke(
143
+ argv: string[],
144
+ opts?: { invocation?: CliInvocation; toolArgs?: Record<string, unknown> },
145
+ ): Promise<CliInvokeResult> {
146
+ const invocation = opts?.invocation ?? "mcp";
137
147
  const prep = this.prepareDispatch(argv, { presentationFallback: true });
138
148
  if ("error" in prep) {
139
149
  if (prep.error.kind === ParseKind.Help) {
@@ -142,7 +152,7 @@ export class Cli {
142
152
  exitCode: 1,
143
153
  stdout: "",
144
154
  stderr: "",
145
- errorMsg: "Help is not available via MCP tool calls.",
155
+ errorMsg: "Help is not available via tool calls.",
146
156
  };
147
157
  }
148
158
  return {
@@ -160,7 +170,16 @@ export class Cli {
160
170
  exitOnMissing: false,
161
171
  });
162
172
 
163
- const ctx = new CliContext(this.program.key, pr.path, pr.args, pr.opts, this.program, "mcp", snapshot);
173
+ const ctx = new CliContext(
174
+ this.program.key,
175
+ pr.path,
176
+ pr.args,
177
+ pr.opts,
178
+ this.program,
179
+ invocation,
180
+ snapshot,
181
+ opts?.toolArgs,
182
+ );
164
183
 
165
184
  let stdout = "";
166
185
  let stderr = "";
@@ -213,12 +232,30 @@ export class Cli {
213
232
  });
214
233
  }
215
234
 
216
- await Promise.resolve(leaf.handler(ctx));
217
- return { kind: "ok", exitCode: 0, stdout, stderr };
235
+ const handlerResult = await Promise.resolve(leaf.handler(ctx));
236
+ if (handlerResult !== undefined && ctx.getResponse() === undefined) {
237
+ ctx.respond({ body: handlerResult as CliRespondOptions["body"] });
238
+ }
239
+
240
+ const response = ctx.getResponse();
241
+ return {
242
+ kind: "ok",
243
+ exitCode: 0,
244
+ stdout,
245
+ stderr,
246
+ ...(response ? { response } : {}),
247
+ };
218
248
  } catch (err) {
219
249
  if (err instanceof CliInvokeExit) {
220
250
  if (err.code === 0) {
221
- return { kind: "ok", exitCode: 0, stdout, stderr };
251
+ const response = ctx.getResponse();
252
+ return {
253
+ kind: "ok",
254
+ exitCode: 0,
255
+ stdout,
256
+ stderr,
257
+ ...(response ? { response } : {}),
258
+ };
222
259
  }
223
260
  const msg = stderr.trim() || `Exit code ${err.code}`;
224
261
  return { kind: "error", exitCode: err.code, stdout, stderr, errorMsg: msg };
@@ -268,6 +305,21 @@ export class Cli {
268
305
  }
269
306
  }
270
307
 
308
+ async serveApi(): Promise<never> {
309
+ try {
310
+ bootstrapAppConfig(this.program, { validateFile: false });
311
+ await apiServeHttp(this);
312
+ process.exit(0);
313
+ } catch (err) {
314
+ if (err instanceof Error) {
315
+ process.stderr.write(`${err.message}\n`);
316
+ } else {
317
+ process.stderr.write("HTTP API server error.\n");
318
+ }
319
+ process.exit(1);
320
+ }
321
+ }
322
+
271
323
  private prepareDispatch(
272
324
  argv: string[],
273
325
  opts?: { presentationFallback?: boolean },
@@ -0,0 +1,46 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { CliAppConfigEntry } from "../types.ts";
3
+ import { shouldWizardPromptConfigKey } from "./bootstrap.ts";
4
+
5
+ const requiredEntry: CliAppConfigEntry = {
6
+ description: "API token.",
7
+ required: true,
8
+ };
9
+
10
+ describe("shouldWizardPromptConfigKey", () => {
11
+ test("skips addressed keys by default", () => {
12
+ expect(
13
+ shouldWizardPromptConfigKey(
14
+ "apiToken",
15
+ { apiToken: "tok", _bindings: { apiToken: "file" } },
16
+ requiredEntry,
17
+ { apiToken: "tok" },
18
+ {},
19
+ ),
20
+ ).toBe(false);
21
+ });
22
+
23
+ test("rePromptAll prompts addressed keys", () => {
24
+ expect(
25
+ shouldWizardPromptConfigKey(
26
+ "apiToken",
27
+ { apiToken: "tok", _bindings: { apiToken: "file" } },
28
+ requiredEntry,
29
+ { apiToken: "tok" },
30
+ { rePromptAll: true },
31
+ ),
32
+ ).toBe(true);
33
+ });
34
+
35
+ test("prompts when env binding is broken", () => {
36
+ expect(
37
+ shouldWizardPromptConfigKey(
38
+ "apiToken",
39
+ { _bindings: { apiToken: "env" } },
40
+ { ...requiredEntry, env: "API_TOKEN" },
41
+ {},
42
+ {},
43
+ ),
44
+ ).toBe(true);
45
+ });
46
+ });
@@ -176,6 +176,8 @@ export interface RunInstallConfigureOpts {
176
176
  context?: "standalone" | "after-install";
177
177
  /** When false, omit the "Configuration Setup" banner (e.g. inside interactive `configure`). */
178
178
  showHeading?: boolean;
179
+ /** When true, prompt every entry (Enter keeps the current value). */
180
+ rePromptAll?: boolean;
179
181
  }
180
182
 
181
183
  /** Print the "Configuration Setup" heading before the configure prompts. */
@@ -199,6 +201,24 @@ function bindingsDiffer(a: Record<string, unknown>, b: Record<string, unknown>):
199
201
  return JSON.stringify(readBindings(a)) !== JSON.stringify(readBindings(b));
200
202
  }
201
203
 
204
+ /** Whether the configure wizard should prompt for a config key. */
205
+ export function shouldWizardPromptConfigKey(
206
+ key: string,
207
+ fileData: Record<string, unknown>,
208
+ entry: CliAppConfigEntry,
209
+ resolved: Record<string, unknown>,
210
+ opts: Pick<RunInstallConfigureOpts, "rePromptAll">,
211
+ ): boolean {
212
+ const bindings = readBindings(fileData);
213
+ if (bindings[key] === "env" && !isPresent(resolved[key])) {
214
+ return true;
215
+ }
216
+ if (opts.rePromptAll) {
217
+ return true;
218
+ }
219
+ return !isKeyAddressed(key, fileData, entry);
220
+ }
221
+
202
222
  /** Ask the user for each required setting that is still empty; returns updates to save to the config file. */
203
223
  function promptMissingRequired(program: CliProgram): Record<string, unknown> {
204
224
  const appConfig = program.appConfig;
@@ -270,10 +290,7 @@ export function runConfigure(
270
290
  }
271
291
 
272
292
  for (const [key, entry] of Object.entries(program.appConfig.entries)) {
273
- const bindings = readBindings(existing);
274
- if (bindings[key] === "env" && !isPresent(resolved[key])) {
275
- // Re-prompt when env binding is broken.
276
- } else if (isKeyAddressed(key, existing, entry)) {
293
+ if (!shouldWizardPromptConfigKey(key, existing, entry, resolved, opts)) {
277
294
  continue;
278
295
  }
279
296
 
@@ -383,7 +400,7 @@ export function ensureAppConfig(program: CliProgram, opts: EnsureAppConfigOpts):
383
400
 
384
401
  if (opts.interactive && process.stdin.isTTY) {
385
402
  if (opts.configure) {
386
- runConfigure(program, { context: "standalone" });
403
+ runConfigure(program, { context: "standalone", rePromptAll: true });
387
404
  fileData = readAppConfigFileRaw(resolveAppConfigPath(program));
388
405
  resolved = resolveAppConfig(program, fileData, hostEnv);
389
406
  exportConfigToEnv(program, resolved, hostEnv);
@@ -75,9 +75,18 @@ test("MCP program.appConfig succeeds when env present", async () => {
75
75
  ],
76
76
  { script: "examples/mcp-test.ts", env: { ARGS_TEST_SECRET: "sekrit" } },
77
77
  );
78
- const res = responses.get(14) as { result: { isError: boolean; content: { text: string }[] } };
78
+ const res = responses.get(14) as {
79
+ result: {
80
+ isError: boolean;
81
+ structuredContent?: { content: string; contentType: string };
82
+ content: { text: string }[];
83
+ };
84
+ };
79
85
  expect(res.result.isError).toBe(false);
80
- expect(res.result.content[0]?.text.trim()).toBe("sekrit");
86
+ expect(res.result.structuredContent).toEqual({
87
+ content: "sekrit",
88
+ contentType: "text/plain; charset=utf-8",
89
+ });
81
90
  });
82
91
 
83
92
  /** MCP config file loads and exports vars for tool handlers. */
@@ -100,9 +109,18 @@ test("MCP config file loads and exports vars for tool handlers", async () => {
100
109
  env: { HOME: dir, ARGS_TEST_SECRET: "present" },
101
110
  },
102
111
  );
103
- const res = responses.get(15) as { result: { isError: boolean; content: { text: string }[] } };
112
+ const res = responses.get(15) as {
113
+ result: {
114
+ isError: boolean;
115
+ structuredContent?: { content: string; contentType: string };
116
+ content: { text: string }[];
117
+ };
118
+ };
104
119
  expect(res.result.isError).toBe(false);
105
- expect(res.result.content[0]?.text.trim()).toBe("present");
120
+ expect(res.result.structuredContent).toEqual({
121
+ content: "present",
122
+ contentType: "text/plain; charset=utf-8",
123
+ });
106
124
  rmSync(dir, { recursive: true, force: true });
107
125
  });
108
126
 
@@ -272,7 +272,7 @@ async function runInteractiveConfigure(root: CliProgram, opts: ConfigureOpts): P
272
272
  if (target.key === "configure") {
273
273
  if (!appConfigHasEntries(root)) continue;
274
274
 
275
- const result = runConfigure(root, { context: "standalone", showHeading: false });
275
+ const result = runConfigure(root, { context: "standalone", showHeading: false, rePromptAll: true });
276
276
  if (result.changed) {
277
277
  installOut(`Wrote config: ${displayAppConfigPath(root)}`, mutationOpts);
278
278
  recordArtifactMutation(summary, [displayAppConfigPath(root)], "configured");
package/src/context.ts CHANGED
@@ -11,7 +11,8 @@ import type { AnyAppConfigSnapshot } from "./config/context.ts";
11
11
  import { EmptyAppConfigSnapshot } from "./config/context.ts";
12
12
  import { parseCommaList, parseDate, parseDateTime, parseDurationMs } from "./formats.ts";
13
13
  import { collectOptionDefs } from "./parse.ts";
14
- import type { CliInvocation, CliLeaf, CliNode, CliOption, CliProgram } from "./types.ts";
14
+ import { normalizeRespondOptions, writeRespondBodyToStdout } from "./respond.ts";
15
+ import type { CliInvocation, CliLeaf, CliNode, CliOption, CliProgram, CliRespondOptions } from "./types.ts";
15
16
  import { CliOptionKind, CliValueFormat, isCliLeaf, isCliRouter } from "./types.ts";
16
17
  import { strictParseDouble } from "./utils.ts";
17
18
 
@@ -29,6 +30,10 @@ export class CliContext {
29
30
  readonly opts: Record<string, string>;
30
31
  readonly invocation: CliInvocation;
31
32
  readonly appConfig: AnyAppConfigSnapshot;
33
+ /** Original flat tool arguments for API/MCP invocations (when provided). */
34
+ readonly toolArgs?: Record<string, unknown>;
35
+
36
+ private response?: CliRespondOptions;
32
37
 
33
38
  /** Captures the program root, routed path, positional words, and option map for a leaf handler. */
34
39
  constructor(
@@ -39,6 +44,7 @@ export class CliContext {
39
44
  program: CliProgram,
40
45
  invocation: CliInvocation = "cli",
41
46
  appConfig: AnyAppConfigSnapshot = new EmptyAppConfigSnapshot(program),
47
+ toolArgs?: Record<string, unknown>,
42
48
  ) {
43
49
  this.appName = appName;
44
50
  this.commandPath = commandPath;
@@ -47,6 +53,28 @@ export class CliContext {
47
53
  this.program = program;
48
54
  this.invocation = invocation;
49
55
  this.appConfig = appConfig;
56
+ this.toolArgs = toolArgs;
57
+ }
58
+
59
+ /**
60
+ * Sets the machine-readable response for API/MCP invocations, or writes to stdout in CLI mode.
61
+ * May only be called once per invocation.
62
+ */
63
+ respond(opts: CliRespondOptions): void {
64
+ if (this.response !== undefined) {
65
+ throw new Error("ctx.respond() was already called for this invocation");
66
+ }
67
+ const normalized = normalizeRespondOptions(opts);
68
+ if (this.invocation === "cli") {
69
+ writeRespondBodyToStdout(normalized.body);
70
+ return;
71
+ }
72
+ this.response = normalized;
73
+ }
74
+
75
+ /** Returns the respond payload set by {@link respond}, if any. */
76
+ getResponse(): CliRespondOptions | undefined {
77
+ return this.response;
50
78
  }
51
79
 
52
80
  /** Returns whether a presence flag was set (including implicit "1" for boolean options). */
@@ -152,7 +152,7 @@ export function generateApiGuideBody(program: CliProgram): string {
152
152
  return `${lines.join("\n").trimEnd()}\n`;
153
153
  }
154
154
 
155
- /** Generates markdown API reference from the same export as `docs schema`. */
155
+ /** Generates markdown API reference from the same export as `docs cli-schema`. */
156
156
  export function generateApiGuide(program: CliProgram): string {
157
157
  const schema = cliSchemaExport(program);
158
158
  const lines: string[] = [
@@ -160,7 +160,7 @@ export function generateApiGuide(program: CliProgram): string {
160
160
  "",
161
161
  schema.description,
162
162
  "",
163
- `Machine-readable export: \`${program.key} docs schema\``,
163
+ `Machine-readable export: \`${program.key} docs cli-schema\``,
164
164
  "",
165
165
  ];
166
166
 
@@ -12,7 +12,9 @@ import {
12
12
  DOCS_ROUTER_DESCRIPTION,
13
13
  docsEffectiveDefaultTopic,
14
14
  docsEnabled,
15
+ docsIncludesHttpTopic,
15
16
  docsIncludesMcpTopic,
17
+ docsIncludesOpenApiTopic,
16
18
  docsTopicDescription,
17
19
  docsUserTopicKeys,
18
20
  printDocsTopic,
@@ -70,8 +72,16 @@ export function cliBuiltinDocsGroup(program: CliProgram): CliRouter {
70
72
  leaves.push(docsLeaf(program, "mcp", "Print MCP server setup and tool guidance."));
71
73
  }
72
74
 
75
+ if (docsIncludesHttpTopic(program)) {
76
+ leaves.push(docsLeaf(program, "http", "Print HTTP API setup and tool guidance."));
77
+ }
78
+
79
+ if (docsIncludesOpenApiTopic(program)) {
80
+ leaves.push(docsLeaf(program, "openapi", "Print the HTTP OpenAPI 3.1 document as JSON."));
81
+ }
82
+
73
83
  leaves.push(
74
- docsLeaf(program, "schema", "Print the full command tree as JSON."),
84
+ docsLeaf(program, "cli-schema", "Print the full CLI command tree as JSON."),
75
85
  docsLeaf(program, "api", "Print the full command reference as markdown."),
76
86
  docsLeaf(program, "skill", docsSkillTopicDescription(program, resolveCapabilities(program))),
77
87
  );