apcore-cli 0.1.0__tar.gz → 0.2.1__tar.gz

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 (68) hide show
  1. apcore_cli-0.2.1/CHANGELOG.md +92 -0
  2. apcore_cli-0.2.1/CLAUDE.md +30 -0
  3. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/PKG-INFO +15 -46
  4. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/README.md +13 -44
  5. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/examples/extensions/math/add.py +3 -3
  6. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/examples/extensions/math/multiply.py +3 -3
  7. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/examples/extensions/sysutil/disk.py +2 -2
  8. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/examples/extensions/sysutil/env.py +3 -3
  9. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/examples/extensions/text/reverse.py +2 -2
  10. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/examples/extensions/text/upper.py +2 -2
  11. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/examples/extensions/text/wordcount.py +2 -2
  12. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/planning/schema-parser.md +1 -1
  13. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/pyproject.toml +2 -2
  14. apcore_cli-0.2.1/src/apcore_cli/__init__.py +9 -0
  15. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/src/apcore_cli/__main__.py +55 -11
  16. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/src/apcore_cli/approval.py +14 -12
  17. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/src/apcore_cli/cli.py +32 -9
  18. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/src/apcore_cli/config.py +2 -1
  19. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/src/apcore_cli/discovery.py +2 -0
  20. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/src/apcore_cli/output.py +16 -2
  21. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/src/apcore_cli/schema_parser.py +12 -8
  22. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/src/apcore_cli/security/audit.py +16 -2
  23. apcore_cli-0.2.1/src/apcore_cli/shell.py +290 -0
  24. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/test_approval.py +15 -30
  25. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/test_cli.py +74 -2
  26. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/test_config.py +1 -0
  27. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/test_e2e.py +3 -1
  28. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/test_integration.py +58 -33
  29. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/test_schema_parser.py +18 -3
  30. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/test_security/test_audit.py +27 -8
  31. apcore_cli-0.2.1/tests/test_shell.py +186 -0
  32. apcore_cli-0.1.0/CHANGELOG.md +0 -37
  33. apcore_cli-0.1.0/src/apcore_cli/__init__.py +0 -3
  34. apcore_cli-0.1.0/src/apcore_cli/shell.py +0 -185
  35. apcore_cli-0.1.0/tests/test_shell.py +0 -126
  36. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/.github/CODEOWNERS +0 -0
  37. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/.github/copilot-ignore +0 -0
  38. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/.github/workflows/ci.yml +0 -0
  39. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/.gitignore +0 -0
  40. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/.gitmessage +0 -0
  41. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/.pre-commit-config.yaml +0 -0
  42. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/examples/extensions/sysutil/info.py +0 -0
  43. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/examples/run_examples.sh +0 -0
  44. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/planning/approval-gate.md +0 -0
  45. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/planning/config-resolver.md +0 -0
  46. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/planning/core-dispatcher.md +0 -0
  47. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/planning/discovery.md +0 -0
  48. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/planning/output-formatter.md +0 -0
  49. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/planning/overview.md +0 -0
  50. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/planning/security-manager.md +0 -0
  51. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/planning/shell-integration.md +0 -0
  52. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/planning/state.json +0 -0
  53. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/src/apcore_cli/_sandbox_runner.py +0 -0
  54. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/src/apcore_cli/ref_resolver.py +0 -0
  55. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/src/apcore_cli/security/__init__.py +0 -0
  56. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/src/apcore_cli/security/auth.py +0 -0
  57. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/src/apcore_cli/security/config_encryptor.py +0 -0
  58. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/src/apcore_cli/security/sandbox.py +0 -0
  59. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/__init__.py +0 -0
  60. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/conftest.py +0 -0
  61. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/test_bugfixes.py +0 -0
  62. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/test_discovery.py +0 -0
  63. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/test_output.py +0 -0
  64. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/test_ref_resolver.py +0 -0
  65. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/test_security/__init__.py +0 -0
  66. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/test_security/test_auth.py +0 -0
  67. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/test_security/test_config_encryptor.py +0 -0
  68. {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/test_security/test_sandbox.py +0 -0
@@ -0,0 +1,92 @@
1
+ # Changelog
2
+
3
+ All notable changes to apcore-cli (Python SDK) will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
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
+
27
+ ## [0.2.0] - 2026-03-16
28
+
29
+ ### Added
30
+ - `APCORE_CLI_LOGGING_LEVEL` env var — CLI-specific log level that takes priority over `APCORE_LOGGING_LEVEL`; 3-tier precedence: `--log-level` flag > `APCORE_CLI_LOGGING_LEVEL` > `APCORE_LOGGING_LEVEL` > `WARNING` (`__main__.py`)
31
+ - `test_cli_logging_level_takes_priority_over_global` — verifies `APCORE_CLI_LOGGING_LEVEL=DEBUG` wins over `APCORE_LOGGING_LEVEL=ERROR`
32
+ - `test_cli_logging_level_fallback_to_global` — verifies fallback when CLI-specific var is unset
33
+ - `test_builtin_name_collision_exits_2` — schema property named `format` (or other reserved names) causes `build_module_command` to exit 2
34
+ - `test_exec_result_table_format` — `--format table` renders Rich Key/Value table to stdout
35
+ - `test_bash_completion_quotes_prog_name_in_directive` — verifies `shlex.quote()` applied to `complete -F` directive, not just embedded subshell
36
+ - `test_zsh_completion_quotes_prog_name_in_directives` — verifies `compdef` line uses quoted prog_name
37
+ - `test_fish_completion_quotes_prog_name_in_directives` — verifies `complete -c` lines use quoted prog_name
38
+ - 17 new tests (244 → 261 total)
39
+
40
+ ### Changed
41
+ - `--log-level` accepted choices: `WARN` → `WARNING` (`__main__.py`)
42
+ - `schema_to_click_options`: schema-derived options now always have `required=False`; required fields marked `[required]` in help text instead of Click enforcement — allows `--input -` STDIN to supply required values without Click rejecting first (`schema_parser.py`)
43
+ - `format_exec_result`: now routes through `resolve_format()` and renders Rich table when `--format table` is specified; previously ignored its `format` parameter (`output.py`)
44
+ - `_generate_bash_completion`, `_generate_zsh_completion`, `_generate_fish_completion`: `shlex.quote()` applied to ALL prog_name positions in generated scripts (complete directives, compdef, complete -c), not only embedded subshell commands (`shell.py`)
45
+ - `check_approval`: removed unused `ctx: click.Context` parameter (`approval.py`)
46
+ - `set_audit_logger`: broadened type annotation from `AuditLogger` to `AuditLogger | None` (`cli.py`)
47
+ - `collect_input`: simplified redundant condition `if not raw or raw_size == 0:` → `if not raw:` (`cli.py`)
48
+ - Example `Input` models: all 7 modules updated with `Field(description=...)` on every field so CLI `--help` shows descriptive text for each flag
49
+
50
+ ### Fixed
51
+ - **`--input -` STDIN blocked by Click required enforcement**: `schema_to_click_options` was generating `required=True` Click options; Click validated before the callback ran, rejecting STDIN-only invocations. Resolved by always using `required=False` and delegating required validation to `jsonschema.validate()` after input collection. Fixes all 6 `TestRealStdinPiping` failures.
52
+ - **`--log-level` had no effect**: `logging.basicConfig()` is a no-op after the first call; subsequent `create_cli()` calls in tests retained the prior handler's level. Fixed by calling `logging.getLogger().setLevel()` explicitly after `basicConfig()`.
53
+ - **`test_log_level_flag_takes_effect` false pass**: `--help` is an eager flag that exits before the group callback, so `--log-level DEBUG --help` never applied the log level. Test updated to use `completion bash` subcommand instead.
54
+ - **Shell completion directives not shell-safe**: prog names with spaces or special characters were unquoted in `complete -F`, `compdef`, and `complete -c` lines. Fixed by assigning `quoted = shlex.quote(prog_name)` and using it in all directive positions.
55
+ - **Audit `set_audit_logger(None)` type error**: type annotation rejected `None`; broadened to `AuditLogger | None`.
56
+ - **Test logger level leakage**: tests modifying root logger level affected subsequent tests; fixed with `try/finally` that restores the original level.
57
+
58
+ ### Security
59
+ - `AuditLogger._hash_input`: now uses `secrets.token_bytes(16)` per-invocation salt before hashing, preventing cross-invocation input correlation via SHA-256 rainbow tables
60
+ - `build_module_command`: added reserved-name collision guard — exits 2 if a schema property (`input`, `yes`, `large_input`, `format`, `sandbox`) conflicts with a built-in CLI option name
61
+ - `_prompt_with_timeout` (SIGALRM path): wrapped in `try/finally` to guarantee signal handler restoration regardless of exit path
62
+
63
+ ## [0.1.0] - 2026-03-15
64
+
65
+ ### Added
66
+ - `--sandbox` flag for subprocess-isolated module execution (FE-05)
67
+ - `ModuleExecutionError` exception class for sandbox failures
68
+ - Windows approval timeout support via `threading.Timer` + `ctypes` (FE-03)
69
+ - Approval timeout clamping to 1..3600 seconds range (FE-03)
70
+ - Tag format validation (`^[a-z][a-z0-9_-]*$`) in `list --tag` (FE-04)
71
+ - `cli.auto_approve` config key with `False` default (FE-07)
72
+ - Extensions directory readability check with exit code 47 (FE-01)
73
+ - Missing required property warning in schema parser (FE-02)
74
+ - DEBUG log `"Loading extensions from {path}"` before registry discovery (FE-01)
75
+ - `TYPE_CHECKING` imports for proper type annotations (`Registry`, `Executor`, `ModuleDescriptor`, `ConfigResolver`, `AuditLogger`)
76
+ - `_get_module_id()` helper for `canonical_id`/`module_id` resolution
77
+ - `APCORE_AUTH_API_KEY` and `APCORE_CLI_SANDBOX` to README environment variables table
78
+ - `--sandbox` to README module execution options table
79
+ - CHANGELOG.md
80
+ - Core Dispatcher (FE-01): `LazyModuleGroup`, `build_module_command`, `collect_input`, `validate_module_id`
81
+ - Schema Parser (FE-02): `schema_to_click_options`, `_map_type`, `_extract_help`, `reconvert_enum_values`
82
+ - Ref Resolver (FE-02): `resolve_refs`, `_resolve_node` with `$ref`, `allOf`, `anyOf`, `oneOf` support
83
+ - Config Resolver (FE-07): `ConfigResolver` with 4-tier precedence (CLI > Env > File > Default)
84
+ - Approval Gate (FE-03): `check_approval`, `_prompt_with_timeout` with TTY detection and Unix SIGALRM
85
+ - Discovery (FE-04): `list` and `describe` commands with tag filtering and TTY-adaptive output
86
+ - Output Formatter (FE-08): `format_module_list`, `format_module_detail`, `format_exec_result` with Rich rendering
87
+ - Security Manager (FE-05): `AuthProvider`, `ConfigEncryptor` (keyring + AES-256-GCM), `AuditLogger` (JSON Lines), `Sandbox` (subprocess isolation)
88
+ - Shell Integration (FE-06): bash/zsh/fish completion generators, roff man page generator
89
+ - 8 example modules: `math.add`, `math.multiply`, `text.upper`, `text.reverse`, `text.wordcount`, `sysutil.info`, `sysutil.env`, `sysutil.disk`
90
+ - 244 tests (unit, integration, end-to-end)
91
+ - CI workflow with pytest and coverage
92
+ - Pre-commit hooks configuration
@@ -0,0 +1,30 @@
1
+ # CLAUDE.md — apcore-cli-python
2
+
3
+ ## Build & Test
4
+
5
+ - `pytest` — run all tests. **Must pass before considering any task complete.**
6
+ - `pytest --cov` — run with coverage report.
7
+ - `ruff check .` — lint check.
8
+ - `ruff format .` — format all code. **Run after every code change.**
9
+
10
+ ## Code Style
11
+
12
+ - Python 3.11+ with `from __future__ import annotations`.
13
+ - All code must pass `ruff check` and `ruff format --check`.
14
+ - Type annotations on all public function signatures.
15
+ - Use `click.echo()` for user-facing output, `logger.*` for debug/diagnostic output.
16
+ - Prefer `sys.exit(code)` with exit code constants over raising exceptions for CLI errors.
17
+
18
+ ## Project Conventions
19
+
20
+ - Spec repo (single source of truth): `../apcore-cli/docs/`
21
+ - Package structure: `src/apcore_cli/` with `__init__.py` exporting `__version__` only.
22
+ - Entry point: `apcore_cli.__main__:main`.
23
+ - Security modules live in `src/apcore_cli/security/` sub-package.
24
+ - ConfigResolver.DEFAULTS values are Python-typed (str, int, bool).
25
+ - Tests organized by module: `tests/test_<module>.py`, security tests in `tests/test_security/`.
26
+
27
+ ## Environment
28
+
29
+ - Python >= 3.11
30
+ - Key dependencies: click >= 8.0, rich >= 13.0, jsonschema >= 4.0, pyyaml >= 6.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apcore-cli
3
- Version: 0.1.0
3
+ Version: 0.2.1
4
4
  Summary: Terminal adapter for apcore — execute AI-Perceivable modules from the command line
5
5
  Project-URL: Homepage, https://aipartnerup.com
6
6
  Project-URL: Repository, https://github.com/aipartnerup/apcore-cli-python
@@ -20,7 +20,7 @@ Classifier: Programming Language :: Python :: 3.13
20
20
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
21
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
22
  Requires-Python: >=3.11
23
- Requires-Dist: apcore>=0.13.0
23
+ Requires-Dist: apcore>=0.13.1
24
24
  Requires-Dist: click>=8.1
25
25
  Requires-Dist: cryptography>=41.0
26
26
  Requires-Dist: jsonschema>=4.20
@@ -45,7 +45,7 @@ Terminal adapter for apcore. Execute AI-Perceivable modules from the command lin
45
45
 
46
46
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
47
47
  [![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://python.org)
48
- [![Tests](https://img.shields.io/badge/tests-244%20passed-brightgreen.svg)]()
48
+ [![Tests](https://img.shields.io/badge/tests-263%20passed-brightgreen.svg)]()
49
49
 
50
50
  | | |
51
51
  |---|---|
@@ -207,7 +207,7 @@ apcore-cli [OPTIONS] COMMAND [ARGS]
207
207
  | Option | Default | Description |
208
208
  |--------|---------|-------------|
209
209
  | `--extensions-dir` | `./extensions` | Path to apcore extensions directory |
210
- | `--log-level` | `INFO` | Logging: `DEBUG`, `INFO`, `WARN`, `ERROR` |
210
+ | `--log-level` | `WARNING` | Logging: `DEBUG`, `INFO`, `WARNING`, `ERROR` |
211
211
  | `--version` | | Show version and exit |
212
212
  | `--help` | | Show help and exit |
213
213
 
@@ -264,9 +264,11 @@ apcore-cli uses a 4-tier configuration precedence:
264
264
  |----------|-------------|---------|
265
265
  | `APCORE_EXTENSIONS_ROOT` | Path to extensions directory | `./extensions` |
266
266
  | `APCORE_CLI_AUTO_APPROVE` | Set to `1` to bypass all approval prompts | *(unset)* |
267
- | `APCORE_LOGGING_LEVEL` | Log level | `INFO` |
267
+ | `APCORE_CLI_LOGGING_LEVEL` | CLI-specific log level (takes priority over `APCORE_LOGGING_LEVEL`) | `WARNING` |
268
+ | `APCORE_LOGGING_LEVEL` | Global apcore log level (fallback when `APCORE_CLI_LOGGING_LEVEL` is unset) | `WARNING` |
268
269
  | `APCORE_AUTH_API_KEY` | API key for remote registry authentication | *(unset)* |
269
270
  | `APCORE_CLI_SANDBOX` | Set to `1` to enable subprocess sandboxing | *(unset)* |
271
+ | `APCORE_CLI_HELP_TEXT_MAX_LENGTH` | Maximum characters for CLI option help text before truncation | `1000` |
270
272
 
271
273
  ### Config File (`apcore.yaml`)
272
274
 
@@ -277,6 +279,8 @@ logging:
277
279
  level: DEBUG
278
280
  sandbox:
279
281
  enabled: false
282
+ cli:
283
+ help_text_max_length: 1000
280
284
  ```
281
285
 
282
286
  ## Features
@@ -303,7 +307,7 @@ sandbox:
303
307
  | `module_id` (`math.add`) | Command name (`apcore-cli math.add`) |
304
308
  | `description` | `--help` text |
305
309
  | `input_schema.properties` | CLI flags (`--a`, `--b`) |
306
- | `input_schema.required` | Required flag enforcement |
310
+ | `input_schema.required` | Validated post-collection via `jsonschema.validate()` (required fields shown as `[required]` in `--help`) |
307
311
  | `annotations.requires_approval` | HITL approval prompt |
308
312
 
309
313
  ### Architecture
@@ -316,10 +320,10 @@ apcore-cli (the adapter)
316
320
  |
317
321
  +-- ConfigResolver 4-tier config precedence
318
322
  +-- LazyModuleGroup Dynamic Click command generation
319
- +-- SchemaParser JSON Schema -> Click options
320
- +-- RefResolver $ref / allOf / anyOf / oneOf
321
- +-- ApprovalGate TTY-aware HITL approval
322
- +-- OutputFormatter TTY-adaptive JSON/table output
323
+ +-- schema_parser JSON Schema -> Click options
324
+ +-- ref_resolver $ref / allOf / anyOf / oneOf
325
+ +-- approval TTY-aware HITL approval
326
+ +-- output TTY-adaptive JSON/table output
323
327
  +-- AuditLogger JSON Lines execution logging
324
328
  +-- Sandbox Subprocess isolation
325
329
  |
@@ -414,46 +418,11 @@ apcore-cli --extensions-dir ./extensions greet.hello --name Alice --greeting Hi
414
418
  git clone https://github.com/aipartnerup/apcore-cli-python.git
415
419
  cd apcore-cli-python
416
420
  pip install -e ".[dev]"
417
- pytest # 244 tests
421
+ pytest # 263 tests
418
422
  pytest --cov # with coverage report
419
423
  bash examples/run_examples.sh # run all examples
420
424
  ```
421
425
 
422
- ### Project Structure
423
-
424
- ```
425
- src/apcore_cli/
426
- ├── __init__.py # Package version
427
- ├── __main__.py # CLI entry point, wiring
428
- ├── cli.py # LazyModuleGroup, build_module_command, collect_input
429
- ├── config.py # ConfigResolver (4-tier precedence)
430
- ├── schema_parser.py # JSON Schema -> Click options
431
- ├── ref_resolver.py # $ref / allOf / anyOf / oneOf resolution
432
- ├── output.py # TTY-adaptive output formatting (rich)
433
- ├── discovery.py # list / describe commands
434
- ├── approval.py # HITL approval gate with timeout
435
- ├── shell.py # bash/zsh/fish completion + man pages
436
- ├── _sandbox_runner.py # Subprocess entry point for sandboxed execution
437
- └── security/
438
- ├── __init__.py # Exports
439
- ├── auth.py # API key authentication
440
- ├── config_encryptor.py # Keyring + AES-256-GCM encrypted config
441
- ├── audit.py # JSON Lines audit logging
442
- └── sandbox.py # Subprocess-based execution isolation
443
-
444
- examples/
445
- ├── run_examples.sh # Run all examples end-to-end
446
- └── extensions/
447
- ├── math/ # math.add, math.multiply
448
- ├── text/ # text.upper, text.reverse, text.wordcount
449
- └── sysutil/ # sysutil.info, sysutil.env, sysutil.disk
450
-
451
- planning/ # Implementation plans (TDD task breakdowns)
452
- ├── overview.md
453
- ├── state.json
454
- └── *.md # Per-feature plans
455
- ```
456
-
457
426
  ## License
458
427
 
459
428
  Apache-2.0
@@ -8,7 +8,7 @@ Terminal adapter for apcore. Execute AI-Perceivable modules from the command lin
8
8
 
9
9
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
10
10
  [![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://python.org)
11
- [![Tests](https://img.shields.io/badge/tests-244%20passed-brightgreen.svg)]()
11
+ [![Tests](https://img.shields.io/badge/tests-263%20passed-brightgreen.svg)]()
12
12
 
13
13
  | | |
14
14
  |---|---|
@@ -170,7 +170,7 @@ apcore-cli [OPTIONS] COMMAND [ARGS]
170
170
  | Option | Default | Description |
171
171
  |--------|---------|-------------|
172
172
  | `--extensions-dir` | `./extensions` | Path to apcore extensions directory |
173
- | `--log-level` | `INFO` | Logging: `DEBUG`, `INFO`, `WARN`, `ERROR` |
173
+ | `--log-level` | `WARNING` | Logging: `DEBUG`, `INFO`, `WARNING`, `ERROR` |
174
174
  | `--version` | | Show version and exit |
175
175
  | `--help` | | Show help and exit |
176
176
 
@@ -227,9 +227,11 @@ apcore-cli uses a 4-tier configuration precedence:
227
227
  |----------|-------------|---------|
228
228
  | `APCORE_EXTENSIONS_ROOT` | Path to extensions directory | `./extensions` |
229
229
  | `APCORE_CLI_AUTO_APPROVE` | Set to `1` to bypass all approval prompts | *(unset)* |
230
- | `APCORE_LOGGING_LEVEL` | Log level | `INFO` |
230
+ | `APCORE_CLI_LOGGING_LEVEL` | CLI-specific log level (takes priority over `APCORE_LOGGING_LEVEL`) | `WARNING` |
231
+ | `APCORE_LOGGING_LEVEL` | Global apcore log level (fallback when `APCORE_CLI_LOGGING_LEVEL` is unset) | `WARNING` |
231
232
  | `APCORE_AUTH_API_KEY` | API key for remote registry authentication | *(unset)* |
232
233
  | `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` |
233
235
 
234
236
  ### Config File (`apcore.yaml`)
235
237
 
@@ -240,6 +242,8 @@ logging:
240
242
  level: DEBUG
241
243
  sandbox:
242
244
  enabled: false
245
+ cli:
246
+ help_text_max_length: 1000
243
247
  ```
244
248
 
245
249
  ## Features
@@ -266,7 +270,7 @@ sandbox:
266
270
  | `module_id` (`math.add`) | Command name (`apcore-cli math.add`) |
267
271
  | `description` | `--help` text |
268
272
  | `input_schema.properties` | CLI flags (`--a`, `--b`) |
269
- | `input_schema.required` | Required flag enforcement |
273
+ | `input_schema.required` | Validated post-collection via `jsonschema.validate()` (required fields shown as `[required]` in `--help`) |
270
274
  | `annotations.requires_approval` | HITL approval prompt |
271
275
 
272
276
  ### Architecture
@@ -279,10 +283,10 @@ apcore-cli (the adapter)
279
283
  |
280
284
  +-- ConfigResolver 4-tier config precedence
281
285
  +-- LazyModuleGroup Dynamic Click command generation
282
- +-- SchemaParser JSON Schema -> Click options
283
- +-- RefResolver $ref / allOf / anyOf / oneOf
284
- +-- ApprovalGate TTY-aware HITL approval
285
- +-- OutputFormatter TTY-adaptive JSON/table output
286
+ +-- schema_parser JSON Schema -> Click options
287
+ +-- ref_resolver $ref / allOf / anyOf / oneOf
288
+ +-- approval TTY-aware HITL approval
289
+ +-- output TTY-adaptive JSON/table output
286
290
  +-- AuditLogger JSON Lines execution logging
287
291
  +-- Sandbox Subprocess isolation
288
292
  |
@@ -377,46 +381,11 @@ apcore-cli --extensions-dir ./extensions greet.hello --name Alice --greeting Hi
377
381
  git clone https://github.com/aipartnerup/apcore-cli-python.git
378
382
  cd apcore-cli-python
379
383
  pip install -e ".[dev]"
380
- pytest # 244 tests
384
+ pytest # 263 tests
381
385
  pytest --cov # with coverage report
382
386
  bash examples/run_examples.sh # run all examples
383
387
  ```
384
388
 
385
- ### Project Structure
386
-
387
- ```
388
- src/apcore_cli/
389
- ├── __init__.py # Package version
390
- ├── __main__.py # CLI entry point, wiring
391
- ├── cli.py # LazyModuleGroup, build_module_command, collect_input
392
- ├── config.py # ConfigResolver (4-tier precedence)
393
- ├── schema_parser.py # JSON Schema -> Click options
394
- ├── ref_resolver.py # $ref / allOf / anyOf / oneOf resolution
395
- ├── output.py # TTY-adaptive output formatting (rich)
396
- ├── discovery.py # list / describe commands
397
- ├── approval.py # HITL approval gate with timeout
398
- ├── shell.py # bash/zsh/fish completion + man pages
399
- ├── _sandbox_runner.py # Subprocess entry point for sandboxed execution
400
- └── security/
401
- ├── __init__.py # Exports
402
- ├── auth.py # API key authentication
403
- ├── config_encryptor.py # Keyring + AES-256-GCM encrypted config
404
- ├── audit.py # JSON Lines audit logging
405
- └── sandbox.py # Subprocess-based execution isolation
406
-
407
- examples/
408
- ├── run_examples.sh # Run all examples end-to-end
409
- └── extensions/
410
- ├── math/ # math.add, math.multiply
411
- ├── text/ # text.upper, text.reverse, text.wordcount
412
- └── sysutil/ # sysutil.info, sysutil.env, sysutil.disk
413
-
414
- planning/ # Implementation plans (TDD task breakdowns)
415
- ├── overview.md
416
- ├── state.json
417
- └── *.md # Per-feature plans
418
- ```
419
-
420
389
  ## License
421
390
 
422
391
  Apache-2.0
@@ -1,11 +1,11 @@
1
1
  """math.add — Add two numbers."""
2
2
 
3
- from pydantic import BaseModel
3
+ from pydantic import BaseModel, Field
4
4
 
5
5
 
6
6
  class Input(BaseModel):
7
- a: int
8
- b: int
7
+ a: int = Field(..., description="First operand")
8
+ b: int = Field(..., description="Second operand")
9
9
 
10
10
 
11
11
  class Output(BaseModel):
@@ -1,11 +1,11 @@
1
1
  """math.multiply — Multiply two numbers."""
2
2
 
3
- from pydantic import BaseModel
3
+ from pydantic import BaseModel, Field
4
4
 
5
5
 
6
6
  class Input(BaseModel):
7
- a: int
8
- b: int
7
+ a: int = Field(..., description="First operand")
8
+ b: int = Field(..., description="Second operand")
9
9
 
10
10
 
11
11
  class Output(BaseModel):
@@ -2,11 +2,11 @@
2
2
 
3
3
  import os
4
4
 
5
- from pydantic import BaseModel
5
+ from pydantic import BaseModel, Field
6
6
 
7
7
 
8
8
  class Input(BaseModel):
9
- path: str = "/"
9
+ path: str = Field("/", description="Filesystem path to check (default: /)")
10
10
 
11
11
 
12
12
  class Output(BaseModel):
@@ -2,12 +2,12 @@
2
2
 
3
3
  import os
4
4
 
5
- from pydantic import BaseModel
5
+ from pydantic import BaseModel, Field
6
6
 
7
7
 
8
8
  class Input(BaseModel):
9
- name: str
10
- default: str = ""
9
+ name: str = Field(..., description="Environment variable name to read")
10
+ default: str = Field("", description="Value to return if the variable is not set")
11
11
 
12
12
 
13
13
  class Output(BaseModel):
@@ -1,10 +1,10 @@
1
1
  """text.reverse — Reverse a string."""
2
2
 
3
- from pydantic import BaseModel
3
+ from pydantic import BaseModel, Field
4
4
 
5
5
 
6
6
  class Input(BaseModel):
7
- text: str
7
+ text: str = Field(..., description="Input string to reverse")
8
8
 
9
9
 
10
10
  class Output(BaseModel):
@@ -1,10 +1,10 @@
1
1
  """text.upper — Convert text to uppercase."""
2
2
 
3
- from pydantic import BaseModel
3
+ from pydantic import BaseModel, Field
4
4
 
5
5
 
6
6
  class Input(BaseModel):
7
- text: str
7
+ text: str = Field(..., description="Input string to convert")
8
8
 
9
9
 
10
10
  class Output(BaseModel):
@@ -1,10 +1,10 @@
1
1
  """text.wordcount — Count words, characters, and lines."""
2
2
 
3
- from pydantic import BaseModel
3
+ from pydantic import BaseModel, Field
4
4
 
5
5
 
6
6
  class Input(BaseModel):
7
- text: str
7
+ text: str = Field(..., description="Input text to analyse")
8
8
 
9
9
 
10
10
  class Output(BaseModel):
@@ -83,7 +83,7 @@
83
83
  - `tests/test_schema_parser.py`:
84
84
  - `test_help_from_x_llm_description`: `x-llm-description` present → used over `description`.
85
85
  - `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 + "...".
87
87
  - `test_help_none`: No description fields → `None`.
88
88
  - `test_flag_collision_detection`: Properties `foo_bar` and `foo-bar` both map to `--foo-bar` → exit 48.
89
89
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "apcore-cli"
7
- version = "0.1.0"
7
+ version = "0.2.1"
8
8
  description = "Terminal adapter for apcore — execute AI-Perceivable modules from the command line"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -26,7 +26,7 @@ classifiers = [
26
26
  "Environment :: Console",
27
27
  ]
28
28
  dependencies = [
29
- "apcore>=0.13.0",
29
+ "apcore>=0.13.1",
30
30
  "click>=8.1",
31
31
  "jsonschema>=4.20",
32
32
  "rich>=13.0",
@@ -0,0 +1,9 @@
1
+ """apcore-cli: CLI adapter for the apcore module ecosystem."""
2
+
3
+ from importlib.metadata import PackageNotFoundError
4
+ from importlib.metadata import version as _get_version
5
+
6
+ try:
7
+ __version__ = _get_version("apcore-cli")
8
+ except PackageNotFoundError:
9
+ __version__ = "unknown"
@@ -36,23 +36,55 @@ def _extract_extensions_dir(argv: list[str] | None = None) -> str | None:
36
36
  return None
37
37
 
38
38
 
39
- def create_cli(extensions_dir: str | None = None) -> click.Group:
39
+ def create_cli(extensions_dir: str | None = None, prog_name: str | None = None) -> click.Group:
40
40
  """Create the CLI application.
41
41
 
42
42
  Args:
43
43
  extensions_dir: Override for extensions directory.
44
44
  When None, resolves via ConfigResolver (env/file/default).
45
+ prog_name: Name shown in help text and version output.
46
+ Defaults to the basename of sys.argv[0], so downstream projects
47
+ that install their own entry-point script get the correct name
48
+ automatically (e.g. ``mycli`` instead of ``apcore-cli``).
45
49
  """
50
+ if prog_name is None:
51
+ prog_name = os.path.basename(sys.argv[0]) or "apcore-cli"
52
+
53
+ # Resolve CLI log level (3-tier precedence, evaluated before Click runs):
54
+ # APCORE_CLI_LOGGING_LEVEL (CLI-specific) > APCORE_LOGGING_LEVEL (global) > WARNING
55
+ # The --log-level flag (parsed later) can further override at runtime.
56
+ _cli_level_str = os.environ.get("APCORE_CLI_LOGGING_LEVEL", "").upper()
57
+ _global_level_str = os.environ.get("APCORE_LOGGING_LEVEL", "").upper()
58
+ _active_level_str = _cli_level_str or _global_level_str
59
+ _default_level = getattr(logging, _active_level_str, logging.WARNING) if _active_level_str else logging.WARNING
60
+ logging.basicConfig(level=_default_level, format="%(levelname)s: %(message)s")
61
+ # basicConfig is a no-op if handlers already exist; always set the root level explicitly.
62
+ logging.getLogger().setLevel(_default_level)
63
+ # Silence noisy upstream apcore loggers unless the user requests verbose output.
64
+ # Always set explicitly so the level is deterministic regardless of prior state.
65
+ apcore_level = _default_level if _default_level <= logging.INFO else logging.ERROR
66
+ logging.getLogger("apcore").setLevel(apcore_level)
67
+
68
+ config = ConfigResolver()
69
+
46
70
  if extensions_dir is not None:
47
71
  ext_dir = extensions_dir
48
72
  else:
49
- config = ConfigResolver()
50
73
  ext_dir = config.resolve(
51
74
  "extensions.root",
52
75
  cli_flag="--extensions-dir",
53
76
  env_var="APCORE_EXTENSIONS_ROOT",
54
77
  )
55
78
 
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
+
56
88
  ext_dir_missing = not os.path.exists(ext_dir)
57
89
  ext_dir_unreadable = not ext_dir_missing and not os.access(ext_dir, os.R_OK)
58
90
 
@@ -97,12 +129,13 @@ def create_cli(extensions_dir: str | None = None) -> click.Group:
97
129
  cls=LazyModuleGroup,
98
130
  registry=registry,
99
131
  executor=executor,
100
- name="apcore-cli",
132
+ help_text_max_length=help_text_max_length,
133
+ name=prog_name,
101
134
  help="CLI adapter for the apcore module ecosystem.",
102
135
  )
103
136
  @click.version_option(
104
137
  version=__version__,
105
- prog_name="apcore-cli",
138
+ prog_name=prog_name,
106
139
  )
107
140
  @click.option(
108
141
  "--extensions-dir",
@@ -113,11 +146,18 @@ def create_cli(extensions_dir: str | None = None) -> click.Group:
113
146
  @click.option(
114
147
  "--log-level",
115
148
  default=None,
116
- type=click.Choice(["DEBUG", "INFO", "WARN", "ERROR"], case_sensitive=False),
117
- help="Log level.",
149
+ type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR"], case_sensitive=False),
150
+ help="Log verbosity. Overrides APCORE_CLI_LOGGING_LEVEL and APCORE_LOGGING_LEVEL env vars.",
118
151
  )
119
152
  @click.pass_context
120
153
  def cli(ctx: click.Context, extensions_dir_opt: str | None = None, log_level: str | None = None) -> None:
154
+ if log_level is not None:
155
+ # basicConfig() is a no-op once handlers exist; set level on the root logger directly.
156
+ level = getattr(logging, log_level.upper(), logging.WARNING)
157
+ logging.getLogger().setLevel(level)
158
+ # Keep apcore logger in sync: verbose when user asks for it, quiet otherwise.
159
+ apcore_level = level if level <= logging.INFO else logging.ERROR
160
+ logging.getLogger("apcore").setLevel(apcore_level)
121
161
  ctx.ensure_object(dict)
122
162
  ctx.obj["extensions_dir"] = ext_dir
123
163
 
@@ -125,16 +165,20 @@ def create_cli(extensions_dir: str | None = None) -> click.Group:
125
165
  register_discovery_commands(cli, registry)
126
166
 
127
167
  # Register shell integration commands
128
- register_shell_commands(cli)
168
+ register_shell_commands(cli, prog_name=prog_name)
129
169
 
130
170
  return cli
131
171
 
132
172
 
133
- def main() -> None:
134
- """Main entry point for apcore-cli."""
135
- # Extract --extensions-dir from argv before Click parses
173
+ def main(prog_name: str | None = None) -> None:
174
+ """Main entry point for apcore-cli.
175
+
176
+ Args:
177
+ prog_name: Override the program name shown in help/version output.
178
+ When None, inferred from sys.argv[0] automatically.
179
+ """
136
180
  ext_dir = _extract_extensions_dir()
137
- cli = create_cli(extensions_dir=ext_dir)
181
+ cli = create_cli(extensions_dir=ext_dir, prog_name=prog_name)
138
182
  cli(standalone_mode=True)
139
183
 
140
184