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.
- apcore_cli-0.2.1/CHANGELOG.md +92 -0
- apcore_cli-0.2.1/CLAUDE.md +30 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/PKG-INFO +15 -46
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/README.md +13 -44
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/examples/extensions/math/add.py +3 -3
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/examples/extensions/math/multiply.py +3 -3
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/examples/extensions/sysutil/disk.py +2 -2
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/examples/extensions/sysutil/env.py +3 -3
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/examples/extensions/text/reverse.py +2 -2
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/examples/extensions/text/upper.py +2 -2
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/examples/extensions/text/wordcount.py +2 -2
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/planning/schema-parser.md +1 -1
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/pyproject.toml +2 -2
- apcore_cli-0.2.1/src/apcore_cli/__init__.py +9 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/src/apcore_cli/__main__.py +55 -11
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/src/apcore_cli/approval.py +14 -12
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/src/apcore_cli/cli.py +32 -9
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/src/apcore_cli/config.py +2 -1
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/src/apcore_cli/discovery.py +2 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/src/apcore_cli/output.py +16 -2
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/src/apcore_cli/schema_parser.py +12 -8
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/src/apcore_cli/security/audit.py +16 -2
- apcore_cli-0.2.1/src/apcore_cli/shell.py +290 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/test_approval.py +15 -30
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/test_cli.py +74 -2
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/test_config.py +1 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/test_e2e.py +3 -1
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/test_integration.py +58 -33
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/test_schema_parser.py +18 -3
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/test_security/test_audit.py +27 -8
- apcore_cli-0.2.1/tests/test_shell.py +186 -0
- apcore_cli-0.1.0/CHANGELOG.md +0 -37
- apcore_cli-0.1.0/src/apcore_cli/__init__.py +0 -3
- apcore_cli-0.1.0/src/apcore_cli/shell.py +0 -185
- apcore_cli-0.1.0/tests/test_shell.py +0 -126
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/.github/CODEOWNERS +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/.github/copilot-ignore +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/.github/workflows/ci.yml +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/.gitignore +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/.gitmessage +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/.pre-commit-config.yaml +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/examples/extensions/sysutil/info.py +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/examples/run_examples.sh +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/planning/approval-gate.md +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/planning/config-resolver.md +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/planning/core-dispatcher.md +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/planning/discovery.md +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/planning/output-formatter.md +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/planning/overview.md +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/planning/security-manager.md +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/planning/shell-integration.md +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/planning/state.json +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/src/apcore_cli/_sandbox_runner.py +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/src/apcore_cli/ref_resolver.py +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/src/apcore_cli/security/__init__.py +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/src/apcore_cli/security/auth.py +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/src/apcore_cli/security/config_encryptor.py +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/src/apcore_cli/security/sandbox.py +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/__init__.py +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/conftest.py +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/test_bugfixes.py +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/test_discovery.py +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/test_output.py +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/test_ref_resolver.py +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/test_security/__init__.py +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/test_security/test_auth.py +0 -0
- {apcore_cli-0.1.0 → apcore_cli-0.2.1}/tests/test_security/test_config_encryptor.py +0 -0
- {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
|
|
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.
|
|
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)
|
|
47
47
|
[](https://python.org)
|
|
48
|
-
[]()
|
|
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` | `
|
|
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
|
-
| `
|
|
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` |
|
|
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
|
-
+--
|
|
320
|
-
+--
|
|
321
|
-
+--
|
|
322
|
-
+--
|
|
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 #
|
|
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)
|
|
10
10
|
[](https://python.org)
|
|
11
|
-
[]()
|
|
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` | `
|
|
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
|
-
| `
|
|
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` |
|
|
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
|
-
+--
|
|
283
|
-
+--
|
|
284
|
-
+--
|
|
285
|
-
+--
|
|
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 #
|
|
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,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.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
|
-
- `
|
|
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
|
|
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.
|
|
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
|
-
|
|
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=
|
|
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", "
|
|
117
|
-
help="Log
|
|
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
|
-
|
|
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
|
|