Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 52 additions & 6 deletions scripts/extract-cli-model.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import { execFileSync } from 'node:child_process';
import { writeFileSync } from 'node:fs';
import { argv } from 'node:process';
import { pathToFileURL } from 'node:url';

// Command grouping (arranged properly, per the request). Mirrors the sections
// in agentcore-cli/docs/commands.md. Any command discovered from the binary but
Expand Down Expand Up @@ -84,41 +85,79 @@ function help(args) {
}
}

const PARAMETER_DESCRIPTION_OVERRIDES = new Map([
[
'bedrock, open_ai, or gemini',
'The model provider. Valid values: `bedrock`, `open_ai`, or `gemini`.',
],
[
'Override LiteLLM API base URL (harness only, lite_llm) [non-interactive]',
'The LiteLLM API base URL override for harness invocations. ' +
'Available only with `lite_llm` in non-interactive mode.',
],
[
'Override LiteLLM additional params as a JSON object (harness only, lite_llm) [non-interactive]',
'The additional LiteLLM parameters, as a JSON object, for harness invocations. ' +
'Available only with `lite_llm` in non-interactive mode.',
],
]);

export function normalizeParameterDescription(description) {
return PARAMETER_DESCRIPTION_OVERRIDES.get(description) || description;
}

// Parse a Commander.js help blob into { summary, signature, params, options }.
function parseHelp(text, name) {
export function parseHelp(text) {
const lines = text.split('\n');
const out = { summary: '', signature: '', args: [], options: [] };
let section = 'head';
let currentItem = null;
const descLines = [];

for (const raw of lines) {
const line = raw.replace(/\s+$/, '');
if (/^Usage:/.test(line)) {
out.signature = line.replace(/^Usage:\s*/, '').trim();
section = 'desc';
currentItem = null;
continue;
}
if (/^Arguments:/.test(line)) {
section = 'args';
currentItem = null;
continue;
}
if (/^Options:/.test(line)) {
section = 'options';
currentItem = null;
continue;
}
if (/^Commands:/.test(line)) {
section = 'commands';
currentItem = null;
continue;
}

if (section === 'desc') {
if (line.trim()) descLines.push(line.trim());
} else if (section === 'args') {
const m = line.match(/^\s+(\S+)\s{2,}(.*)$/);
if (m) out.args.push({ name: m[1], type: null, required: true, description: m[2].trim() });
if (m) {
currentItem = { name: m[1], type: null, required: true, description: m[2].trim() };
out.args.push(currentItem);
} else if (currentItem && /^\s+\S/.test(line)) {
currentItem.description += ` ${line.trim()}`;
}
} else if (section === 'options') {
const m = line.match(/^\s+(-[^\s].*?)\s{2,}(.*)$/);
if (m) out.options.push({ name: m[1].trim(), type: null, required: false, description: m[2].trim() });
if (m) {
currentItem = { name: m[1].trim(), type: null, required: false, description: m[2].trim() };
out.options.push(currentItem);
} else if (currentItem && /^\s+\S/.test(line)) {
currentItem.description += ` ${line.trim()}`;
} else if (line.trim()) {
currentItem = null;
}
}
}
out.summary = descLines.join(' ');
Expand All @@ -138,7 +177,7 @@ function entryForCommand(name) {
`not a real CLI command (phantom/renamed). Remove it from GROUPS or fix the name.`
);
}
const parsed = parseHelp(raw, name);
const parsed = parseHelp(raw);
// subcommands (e.g. `add agent`, `remove tool`) show under "Commands:"—
// we surface the top-level command; nested ones can be expanded later.
return {
Expand All @@ -150,7 +189,12 @@ function entryForCommand(name) {
// reuse params for both positional args and flags, flagged by required.
// The renderer already wraps param names in backticks, so don't add our
// own (that produced double-backticks). Drop the ubiquitous help flag.
params: [...parsed.args, ...parsed.options.filter(o => !/^-h,?\s|--help\b/.test(o.name))],
params: [...parsed.args, ...parsed.options.filter(o => !/^-h,?\s|--help\b/.test(o.name))].map(
param => ({
...param,
description: normalizeParameterDescription(param.description),
})
),
returns: null,
raises: [],
examples: [],
Expand Down Expand Up @@ -225,4 +269,6 @@ function main() {
process.stderr.write(`Wrote doc-model: ${groups.length} groups, version ${version}\n`);
}

main();
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main();
}
46 changes: 46 additions & 0 deletions scripts/extract-cli-model.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import assert from "node:assert/strict";
import test from "node:test";

import { normalizeParameterDescription, parseHelp } from "./extract-cli-model.mjs";

test("parseHelp preserves wrapped argument and option descriptions", () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The regression test passes with node --test, but no repository test target runs it: the unit project only discovers src/**/*.test.ts(x), and the package test scripts invoke Vitest. I confirmed Vitest reports this file as “No test files found.” Please wire it into a CI-invoked script/workflow or convert/move it into the discovered suite so this regression remains enforced.

const parsed = parseHelp(`Usage: agentcore invoke [options] [prompt]

Invoke an agent.

Arguments:
prompt Prompt to send to the agent. Also accepts piped
stdin when no prompt is provided

Options:
--prompt-file <path> Read the prompt from a file (for long or
structured payloads that exceed shell limits)

Output
--json Output as JSON
`);

assert.equal(
parsed.args[0].description,
"Prompt to send to the agent. Also accepts piped stdin when no prompt is provided",
);
assert.equal(
parsed.options[0].description,
"Read the prompt from a file (for long or structured payloads that exceed shell limits)",
);
assert.equal(parsed.options[1].description, "Output as JSON");
});

test("normalizeParameterDescription expands model provider and LiteLLM fragments", () => {
assert.equal(
normalizeParameterDescription("bedrock, open_ai, or gemini"),
"The model provider. Valid values: `bedrock`, `open_ai`, or `gemini`.",
);
assert.equal(
normalizeParameterDescription(
"Override LiteLLM API base URL (harness only, lite_llm) [non-interactive]",
),
"The LiteLLM API base URL override for harness invocations. " +
"Available only with `lite_llm` in non-interactive mode.",
);
});
66 changes: 61 additions & 5 deletions scripts/render_adoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,71 @@
SCHEMA_VERSION = 1


def normalize_style(text):
"""Apply style-safe substitutions to generated prose."""
if not text:
return ""
text = re.sub(r"\be\.g\.(?:,)?", "for example,", text, flags=re.IGNORECASE)
text = re.sub(
r"\bAWS Bedrock(?: AgentCore)? Code\s*Interpreter\b",
"AgentCore Code Interpreter",
text,
flags=re.IGNORECASE,
)
text = re.sub(r"\bAWS Bedrock AgentCore\b", "Amazon Bedrock AgentCore", text)
text = re.sub(r"\bAWS Bedrock\b", "Amazon Bedrock", text)
text = re.sub(r"\bAgentCore Memory\b", "AgentCore memory", text)
text = re.sub(r"\bAgentCore Runtime\b", "AgentCore runtime", text)
text = text.replace(
"This feature is in preview and may change in future releases.",
"This feature is in preview and might change in future releases.",
)
text = text.replace("validation will ensure", "validation ensures")
return re.sub(r"\bAWS\b", "{aws}", text)


def normalize_param_description(text):
"""Normalize recurring parameter-description style issues."""
text = normalize_style(text).strip()
exact_substitutions = {
"bedrock, open_ai, or gemini": (
"The model provider. Valid values: `bedrock`, `open_ai`, or `gemini`."
),
"Override LiteLLM API base URL (harness only, lite_llm) [non-interactive]": (
"The LiteLLM API base URL override for harness invocations. "
"Available only with `lite_llm` in non-interactive mode."
),
"Override LiteLLM additional params as a JSON object (harness only, lite_llm) "
"[non-interactive]": (
"The additional LiteLLM parameters, as a JSON object, for harness invocations. "
"Available only with `lite_llm` in non-interactive mode."
),
}
if text in exact_substitutions:
return exact_substitutions[text]
substitutions = (
(r"^Optional\b", "An optional"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 This global rewrite breaks valid plural and mass-noun descriptions from the renderer's other supported sources. I reproduced Optional tags.An optional tags. and Optional additional task metadataAn optional additional task metadata; every parameter reaches this rule via render_params(). Could we preserve Optional, or make the rewrite noun-aware, and add an exact renderer test for those cases plus the intended {aws} output?

(r"^(?:\{aws\}|AWS)\s+region\b", "The {aws} Region"),
(r"^id of\b", "The ID of"),
(r"^Behaviour\b", "The behavior"),
(r"^Behavior\b", "The behavior"),
(r"^Memory resource ID\b", "The memory resource ID"),
(r"^Strategy name\b", "The name of the memory strategy"),
(r"^Strategy ID\b", "The ID of the memory strategy"),
)
for pattern, replacement in substitutions:
text = re.sub(pattern, replacement, text, count=1, flags=re.IGNORECASE)
return text


def esc(text):
"""Escape AsciiDoc-significant characters in inline text."""
if not text:
return ""
# Guard the couple of chars that start AsciiDoc markup in running prose.
return (
text.replace("|", "\\|")
.replace("{", "\\{")
)
marker = "\0AWS_ENTITY\0"
text = normalize_style(text).replace("{aws}", marker)
return text.replace("|", "\\|").replace("{", "\\{").replace(marker, "{aws}")


# Match markdown code fences that may be indented (reST/Google docstrings often
Expand Down Expand Up @@ -116,7 +172,7 @@ def render_params(params, out):
req = "" if p.get("required") else " _(optional)_"
typ = f"`{p['type']}`" if p.get("type") else ""
out.append(f"`{p['name']}`{req} {typ}::")
out.append(esc(p.get("description", "")) or "_No description._")
out.append(esc(normalize_param_description(p.get("description", ""))) or "_No description._")
out.append("")


Expand Down
Loading