Skip to content

Commit 992980e

Browse files
committed
feat: Make CLI help text truncation limit configurable via cli.help_text_max_length and increase its default from 200 to 1000 characters.
1 parent c2ab45e commit 992980e

File tree

10 files changed

+73
-16
lines changed

10 files changed

+73
-16
lines changed

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,25 @@ All notable changes to apcore-cli (Python SDK) will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.2.1] - 2026-03-19
9+
10+
### Changed
11+
- Help text truncation limit increased from 200 to 1000 characters (configurable via `cli.help_text_max_length` config key)
12+
- `_extract_help`: added `max_length: int = 1000` parameter (`schema_parser.py`)
13+
- `schema_to_click_options`: added `max_help_length: int = 1000` parameter (`schema_parser.py`)
14+
- `build_module_command`: added `help_text_max_length: int = 1000` parameter, threaded through to schema parser (`cli.py`)
15+
- `LazyModuleGroup`: constructor accepts `help_text_max_length: int = 1000`, passes to `build_module_command` (`cli.py`)
16+
- `create_cli`: resolves `cli.help_text_max_length` from `ConfigResolver` and passes to `LazyModuleGroup` (`__main__.py`)
17+
- `format_exec_result`: nested dict/list values in table mode now rendered with `json.dumps` instead of `str()` (`output.py`)
18+
19+
### Added
20+
- `cli.help_text_max_length` config key (default: 1000) in `ConfigResolver.DEFAULTS` (`config.py`)
21+
- `APCORE_CLI_HELP_TEXT_MAX_LENGTH` environment variable support for configuring help text max length
22+
- `test_help_truncation_default`: tests default 1000-char truncation
23+
- `test_help_no_truncation_within_limit`: tests no truncation at 999 chars
24+
- `test_help_truncation_custom_max`: tests custom max_length parameter
25+
- 263 tests (up from 261)
26+
827
## [0.2.0] - 2026-03-16
928

1029
### Added

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Terminal adapter for apcore. Execute AI-Perceivable modules from the command lin
88

99
[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
1010
[![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://python.org)
11-
[![Tests](https://img.shields.io/badge/tests-261%20passed-brightgreen.svg)]()
11+
[![Tests](https://img.shields.io/badge/tests-263%20passed-brightgreen.svg)]()
1212

1313
| | |
1414
|---|---|
@@ -231,6 +231,7 @@ apcore-cli uses a 4-tier configuration precedence:
231231
| `APCORE_LOGGING_LEVEL` | Global apcore log level (fallback when `APCORE_CLI_LOGGING_LEVEL` is unset) | `WARNING` |
232232
| `APCORE_AUTH_API_KEY` | API key for remote registry authentication | *(unset)* |
233233
| `APCORE_CLI_SANDBOX` | Set to `1` to enable subprocess sandboxing | *(unset)* |
234+
| `APCORE_CLI_HELP_TEXT_MAX_LENGTH` | Maximum characters for CLI option help text before truncation | `1000` |
234235

235236
### Config File (`apcore.yaml`)
236237

@@ -241,6 +242,8 @@ logging:
241242
level: DEBUG
242243
sandbox:
243244
enabled: false
245+
cli:
246+
help_text_max_length: 1000
244247
```
245248
246249
## Features
@@ -378,7 +381,7 @@ apcore-cli --extensions-dir ./extensions greet.hello --name Alice --greeting Hi
378381
git clone https://github.com/aipartnerup/apcore-cli-python.git
379382
cd apcore-cli-python
380383
pip install -e ".[dev]"
381-
pytest # 261 tests
384+
pytest # 263 tests
382385
pytest --cov # with coverage report
383386
bash examples/run_examples.sh # run all examples
384387
```

planning/schema-parser.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@
8383
- `tests/test_schema_parser.py`:
8484
- `test_help_from_x_llm_description`: `x-llm-description` present → used over `description`.
8585
- `test_help_from_description`: Only `description` → used.
86-
- `test_help_truncation`: Description > 200 chars → truncated to 197 + "...".
86+
- `test_help_truncation_default`: Description > 1000 chars (default) → truncated to 997 + "...".
8787
- `test_help_none`: No description fields → `None`.
8888
- `test_flag_collision_detection`: Properties `foo_bar` and `foo-bar` both map to `--foo-bar` → exit 48.
8989

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "apcore-cli"
7-
version = "0.2.0"
7+
version = "0.2.1"
88
description = "Terminal adapter for apcore — execute AI-Perceivable modules from the command line"
99
readme = "README.md"
1010
license = "Apache-2.0"

src/apcore_cli/__main__.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,26 @@ def create_cli(extensions_dir: str | None = None, prog_name: str | None = None)
6565
apcore_level = _default_level if _default_level <= logging.INFO else logging.ERROR
6666
logging.getLogger("apcore").setLevel(apcore_level)
6767

68+
config = ConfigResolver()
69+
6870
if extensions_dir is not None:
6971
ext_dir = extensions_dir
7072
else:
71-
config = ConfigResolver()
7273
ext_dir = config.resolve(
7374
"extensions.root",
7475
cli_flag="--extensions-dir",
7576
env_var="APCORE_EXTENSIONS_ROOT",
7677
)
7778

79+
help_text_max_length = config.resolve(
80+
"cli.help_text_max_length",
81+
env_var="APCORE_CLI_HELP_TEXT_MAX_LENGTH",
82+
)
83+
try:
84+
help_text_max_length = int(help_text_max_length)
85+
except (TypeError, ValueError):
86+
help_text_max_length = 1000
87+
7888
ext_dir_missing = not os.path.exists(ext_dir)
7989
ext_dir_unreadable = not ext_dir_missing and not os.access(ext_dir, os.R_OK)
8090

@@ -119,6 +129,7 @@ def create_cli(extensions_dir: str | None = None, prog_name: str | None = None)
119129
cls=LazyModuleGroup,
120130
registry=registry,
121131
executor=executor,
132+
help_text_max_length=help_text_max_length,
122133
name=prog_name,
123134
help="CLI adapter for the apcore module ecosystem.",
124135
)

src/apcore_cli/cli.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,17 @@ def set_audit_logger(audit_logger: AuditLogger | None) -> None:
4141
class LazyModuleGroup(click.Group):
4242
"""Custom Click Group that lazily loads apcore modules as subcommands."""
4343

44-
def __init__(self, registry: Registry, executor: Executor, **kwargs: Any) -> None:
44+
def __init__(
45+
self,
46+
registry: Registry,
47+
executor: Executor,
48+
help_text_max_length: int = 1000,
49+
**kwargs: Any,
50+
) -> None:
4551
super().__init__(**kwargs)
4652
self._registry = registry
4753
self._executor = executor
54+
self._help_text_max_length = help_text_max_length
4855
self._module_cache: dict[str, click.Command] = {}
4956

5057
def list_commands(self, ctx: click.Context) -> list[str]:
@@ -70,7 +77,7 @@ def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None
7077
if module_def is None:
7178
return None
7279

73-
cmd = build_module_command(module_def, self._executor)
80+
cmd = build_module_command(module_def, self._executor, help_text_max_length=self._help_text_max_length)
7481
self._module_cache[cmd_name] = cmd
7582
return cmd
7683

@@ -101,7 +108,11 @@ def _get_module_id(module_def: ModuleDescriptor) -> str:
101108
return module_def.module_id
102109

103110

104-
def build_module_command(module_def: ModuleDescriptor, executor: Executor) -> click.Command:
111+
def build_module_command(
112+
module_def: ModuleDescriptor,
113+
executor: Executor,
114+
help_text_max_length: int = 1000,
115+
) -> click.Command:
105116
"""Build a Click command from an apcore module definition.
106117
107118
Generates Click options from the module's input_schema, wires up
@@ -137,7 +148,7 @@ def build_module_command(module_def: ModuleDescriptor, executor: Executor) -> cl
137148
else:
138149
resolved_schema = input_schema
139150

140-
schema_options = schema_to_click_options(resolved_schema)
151+
schema_options = schema_to_click_options(resolved_schema, max_help_length=help_text_max_length)
141152

142153
def callback(**kwargs: Any) -> None:
143154
# Separate built-in options from schema-generated kwargs

src/apcore_cli/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class ConfigResolver:
2222
"sandbox.enabled": False,
2323
"cli.stdin_buffer_limit": 10_485_760, # 10 MB
2424
"cli.auto_approve": False,
25+
"cli.help_text_max_length": 1000,
2526
}
2627

2728
def __init__(

src/apcore_cli/schema_parser.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,19 +50,19 @@ def _map_type(prop_name: str, prop_schema: dict) -> Any:
5050
return result
5151

5252

53-
def _extract_help(prop_schema: dict) -> str | None:
53+
def _extract_help(prop_schema: dict, max_length: int = 1000) -> str | None:
5454
"""Extract help text from schema property, preferring x-llm-description."""
5555
text = prop_schema.get("x-llm-description")
5656
if not text:
5757
text = prop_schema.get("description")
5858
if not text:
5959
return None
60-
if len(text) > 200:
61-
return text[:197] + "..."
60+
if max_length > 0 and len(text) > max_length:
61+
return text[: max_length - 3] + "..."
6262
return text
6363

6464

65-
def schema_to_click_options(schema: dict) -> list[click.Option]:
65+
def schema_to_click_options(schema: dict, max_help_length: int = 1000) -> list[click.Option]:
6666
"""Convert JSON Schema properties to a list of Click options."""
6767
properties = schema.get("properties", {})
6868
required_list = schema.get("required", [])
@@ -93,7 +93,7 @@ def schema_to_click_options(schema: dict) -> list[click.Option]:
9393

9494
click_type = _map_type(prop_name, prop_schema)
9595
is_required = prop_name in required_list
96-
_help_base = _extract_help(prop_schema)
96+
_help_base = _extract_help(prop_schema, max_length=max_help_length)
9797
# Append [required] to help text for user clarity; do NOT set required=True
9898
# at the Click level because that would block --input - (STDIN) from working.
9999
# Schema-level required validation happens in the callback via jsonschema.validate().

tests/test_config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ def test_defaults_contains_expected_keys(self):
2626
"logging.level",
2727
"sandbox.enabled",
2828
"cli.stdin_buffer_limit",
29+
"cli.help_text_max_length",
2930
]
3031
for key in expected_keys:
3132
assert key in resolver.DEFAULTS, f"Missing default key: {key}"

tests/test_schema_parser.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,20 @@ def test_help_from_description(self):
205205
result = _extract_help({"description": "Regular help"})
206206
assert result == "Regular help"
207207

208-
def test_help_truncation(self):
209-
long_text = "x" * 250
208+
def test_help_truncation_default(self):
209+
long_text = "x" * 1100
210210
result = _extract_help({"description": long_text})
211+
assert len(result) == 1000
212+
assert result.endswith("...")
213+
214+
def test_help_no_truncation_within_limit(self):
215+
text = "x" * 999
216+
result = _extract_help({"description": text})
217+
assert result == text
218+
219+
def test_help_truncation_custom_max(self):
220+
long_text = "x" * 300
221+
result = _extract_help({"description": long_text}, max_length=200)
211222
assert len(result) == 200
212223
assert result.endswith("...")
213224

0 commit comments

Comments
 (0)