libcontext 0.7.4__tar.gz → 0.8.0__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.
- {libcontext-0.7.4 → libcontext-0.8.0}/.github/workflows/ci.yml +2 -2
- {libcontext-0.7.4 → libcontext-0.8.0}/CHANGELOG.md +28 -1
- {libcontext-0.7.4 → libcontext-0.8.0}/CONTRIBUTING.md +1 -1
- {libcontext-0.7.4 → libcontext-0.8.0}/PKG-INFO +3 -4
- {libcontext-0.7.4 → libcontext-0.8.0}/docs/adr/002-skill-plus-cli-as-primary-integration.md +3 -3
- {libcontext-0.7.4 → libcontext-0.8.0}/pyproject.toml +9 -7
- {libcontext-0.7.4 → libcontext-0.8.0}/src/libcontext/__init__.py +0 -2
- {libcontext-0.7.4 → libcontext-0.8.0}/src/libcontext/_envsetup.py +16 -7
- {libcontext-0.7.4 → libcontext-0.8.0}/src/libcontext/_security.py +0 -2
- {libcontext-0.7.4 → libcontext-0.8.0}/src/libcontext/cache.py +0 -2
- {libcontext-0.7.4 → libcontext-0.8.0}/src/libcontext/cli.py +44 -37
- {libcontext-0.7.4 → libcontext-0.8.0}/src/libcontext/collector.py +25 -13
- {libcontext-0.7.4 → libcontext-0.8.0}/src/libcontext/config.py +38 -5
- {libcontext-0.7.4 → libcontext-0.8.0}/src/libcontext/diff.py +0 -2
- {libcontext-0.7.4 → libcontext-0.8.0}/src/libcontext/exceptions.py +0 -2
- {libcontext-0.7.4 → libcontext-0.8.0}/src/libcontext/inspector.py +0 -2
- {libcontext-0.7.4 → libcontext-0.8.0}/src/libcontext/mcp_server.py +22 -12
- {libcontext-0.7.4 → libcontext-0.8.0}/src/libcontext/models.py +12 -14
- {libcontext-0.7.4 → libcontext-0.8.0}/src/libcontext/renderer.py +0 -2
- {libcontext-0.7.4 → libcontext-0.8.0}/tests/test_cache.py +69 -2
- {libcontext-0.7.4 → libcontext-0.8.0}/tests/test_cli.py +0 -2
- libcontext-0.8.0/tests/test_cli_mcp_parity.py +394 -0
- {libcontext-0.7.4 → libcontext-0.8.0}/tests/test_collector.py +483 -2
- {libcontext-0.7.4 → libcontext-0.8.0}/tests/test_config.py +134 -2
- {libcontext-0.7.4 → libcontext-0.8.0}/tests/test_diff.py +0 -2
- {libcontext-0.7.4 → libcontext-0.8.0}/tests/test_envsetup.py +197 -5
- {libcontext-0.7.4 → libcontext-0.8.0}/tests/test_inspector.py +0 -2
- {libcontext-0.7.4 → libcontext-0.8.0}/tests/test_mcp_server.py +0 -2
- {libcontext-0.7.4 → libcontext-0.8.0}/tests/test_models.py +0 -2
- {libcontext-0.7.4 → libcontext-0.8.0}/tests/test_renderer.py +0 -2
- {libcontext-0.7.4 → libcontext-0.8.0}/tests/test_security.py +0 -2
- libcontext-0.7.4/tests/test_cli_mcp_parity.py +0 -185
- {libcontext-0.7.4 → libcontext-0.8.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {libcontext-0.7.4 → libcontext-0.8.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {libcontext-0.7.4 → libcontext-0.8.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {libcontext-0.7.4 → libcontext-0.8.0}/.github/workflows/release.yml +0 -0
- {libcontext-0.7.4 → libcontext-0.8.0}/.gitignore +0 -0
- {libcontext-0.7.4 → libcontext-0.8.0}/DEPENDENCIES.md +0 -0
- {libcontext-0.7.4 → libcontext-0.8.0}/LICENSE +0 -0
- {libcontext-0.7.4 → libcontext-0.8.0}/README.md +0 -0
- {libcontext-0.7.4 → libcontext-0.8.0}/SECURITY.md +0 -0
- {libcontext-0.7.4 → libcontext-0.8.0}/docs/adr/001-progressive-disclosure-over-always-on-context.md +0 -0
- {libcontext-0.7.4 → libcontext-0.8.0}/docs/adr/004-ast-only-inspection.md +0 -0
- {libcontext-0.7.4 → libcontext-0.8.0}/docs/adr/README.md +0 -0
- {libcontext-0.7.4 → libcontext-0.8.0}/src/libcontext/py.typed +0 -0
- {libcontext-0.7.4 → libcontext-0.8.0}/tests/__init__.py +0 -0
|
@@ -18,7 +18,7 @@ jobs:
|
|
|
18
18
|
fail-fast: false
|
|
19
19
|
matrix:
|
|
20
20
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
21
|
-
python-version: ["3.
|
|
21
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
22
22
|
|
|
23
23
|
steps:
|
|
24
24
|
- uses: actions/checkout@v4
|
|
@@ -33,7 +33,7 @@ jobs:
|
|
|
33
33
|
run: uv sync --group dev
|
|
34
34
|
|
|
35
35
|
- name: Run tests
|
|
36
|
-
run: uv run pytest -v --cov=libcontext --cov-report=xml
|
|
36
|
+
run: uv run pytest -v --cov=libcontext --cov-report=xml --cov-fail-under=90
|
|
37
37
|
|
|
38
38
|
- name: Upload coverage
|
|
39
39
|
if: matrix.python-version == '3.12' && matrix.os == 'ubuntu-latest'
|
|
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.8.0] - 2026-03-26
|
|
11
|
+
|
|
12
|
+
### Breaking Changes
|
|
13
|
+
|
|
14
|
+
- Dropped Python 3.9 support. Minimum required version is now 3.10.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Configurable `file_size_limit`, `output_char_limit`, and `subprocess_timeout`
|
|
19
|
+
in `[tool.libcontext]` configuration section.
|
|
20
|
+
- CLI output now respects `output_char_limit` for consistency with MCP server.
|
|
21
|
+
- `LIBCONTEXT_OUTPUT_CHAR_LIMIT` environment variable for MCP server truncation
|
|
22
|
+
limit override.
|
|
23
|
+
- Usage examples in `libctx inspect --help` and `libctx diff --help`.
|
|
24
|
+
- CI enforces minimum 90% code coverage via `--cov-fail-under`.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Development status upgraded from Alpha to Beta.
|
|
29
|
+
- CI matrix updated to test Python 3.10-3.13 only.
|
|
30
|
+
- Enabled `warn_unused_ignores` in mypy configuration; removed stale
|
|
31
|
+
`type: ignore` comments.
|
|
32
|
+
- Removed `from __future__ import annotations` from all files (no longer
|
|
33
|
+
needed with Python >=3.10 floor).
|
|
34
|
+
- Test coverage raised from 93% to 96% (63 new tests, 549 total).
|
|
35
|
+
|
|
10
36
|
## [0.7.0] - 2026-03-25
|
|
11
37
|
|
|
12
38
|
### Added
|
|
@@ -143,7 +169,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
143
169
|
- Free-form `extra_context` field for library authors.
|
|
144
170
|
- Python API for programmatic usage (`collect_package`, `render_package`).
|
|
145
171
|
|
|
146
|
-
[Unreleased]: https://github.com/Syclaw/libcontext/compare/v0.
|
|
172
|
+
[Unreleased]: https://github.com/Syclaw/libcontext/compare/v0.8.0...HEAD
|
|
173
|
+
[0.8.0]: https://github.com/Syclaw/libcontext/compare/v0.7.0...v0.8.0
|
|
147
174
|
[0.7.0]: https://github.com/Syclaw/libcontext/compare/v0.6.1...v0.7.0
|
|
148
175
|
[0.6.1]: https://github.com/Syclaw/libcontext/compare/v0.6.0...v0.6.1
|
|
149
176
|
[0.6.0]: https://github.com/Syclaw/libcontext/compare/v0.5.0...v0.6.0
|
|
@@ -11,7 +11,7 @@ By participating in this project, you agree to maintain a respectful and inclusi
|
|
|
11
11
|
### Prerequisites
|
|
12
12
|
|
|
13
13
|
- [uv](https://docs.astral.sh/uv/) (recommended installer: `pip install uv` or see [installation docs](https://docs.astral.sh/uv/getting-started/installation/))
|
|
14
|
-
- Python 3.
|
|
14
|
+
- Python 3.10 or later
|
|
15
15
|
- Git
|
|
16
16
|
|
|
17
17
|
### Setting Up Your Development Environment
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: libcontext
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Generate optimized LLM context from Python library APIs — CLI, skill, and MCP server
|
|
5
5
|
Project-URL: Homepage, https://github.com/Syclaw/libcontext
|
|
6
6
|
Project-URL: Repository, https://github.com/Syclaw/libcontext
|
|
@@ -9,18 +9,17 @@ Author: Jonathan VARELA
|
|
|
9
9
|
License-Expression: MIT
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Keywords: ast,context,copilot,documentation,introspection,llm,mcp
|
|
12
|
-
Classifier: Development Status ::
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
14
|
Classifier: License :: OSI Approved :: MIT License
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
20
|
Classifier: Topic :: Software Development :: Documentation
|
|
22
21
|
Classifier: Topic :: Software Development :: Libraries
|
|
23
|
-
Requires-Python: >=3.
|
|
22
|
+
Requires-Python: >=3.10
|
|
24
23
|
Requires-Dist: click>=8.0
|
|
25
24
|
Requires-Dist: tomli>=1.0; python_version < '3.11'
|
|
26
25
|
Provides-Extra: mcp
|
|
@@ -30,7 +30,7 @@ A skill (`.claude/skills/lib/SKILL.md` or `.github/skills/lib/SKILL.md`) instruc
|
|
|
30
30
|
- Zero external dependencies beyond the CLI itself.
|
|
31
31
|
- Works in any environment that allows shell commands — no MCP infrastructure needed.
|
|
32
32
|
- Skills are static Markdown files checked into the repository. No running process, no attack surface.
|
|
33
|
-
- Compatible with Python 3.
|
|
33
|
+
- Compatible with Python 3.10+.
|
|
34
34
|
|
|
35
35
|
**Disadvantages:**
|
|
36
36
|
- Bash tool calls have no typed interface — the LLM constructs command strings.
|
|
@@ -70,7 +70,7 @@ MCP is offered as an optional extra (`pip install libcontext[mcp]`) for environm
|
|
|
70
70
|
|
|
71
71
|
## MCP as Optional Dependency
|
|
72
72
|
|
|
73
|
-
The MCP server (`libctx-mcp`) depends on `mcp[cli]`, which requires Python 3.10+ and pulls in transitive dependencies (HTTP server, JSON-RPC, Pydantic). Since the core library targets Python 3.
|
|
73
|
+
The MCP server (`libctx-mcp`) depends on `mcp[cli]`, which requires Python 3.10+ and pulls in transitive dependencies (HTTP server, JSON-RPC, Pydantic). Since the core library targets Python 3.10+, MCP is an optional extra:
|
|
74
74
|
|
|
75
75
|
- Installed via `pip install libcontext[mcp]`.
|
|
76
76
|
- `pyproject.toml` declares: `mcp = ["mcp[cli]>=1.0; python_version >= '3.10'"]`.
|
|
@@ -97,7 +97,7 @@ The `/lib` skill template is embedded as a string in `cli.py` (`_get_skill_conte
|
|
|
97
97
|
## Consequences
|
|
98
98
|
|
|
99
99
|
- **Positive.** Works in enterprise environments that block MCP.
|
|
100
|
-
- **Positive.** No additional runtime dependencies for the primary path. Core functionality works on Python 3.
|
|
100
|
+
- **Positive.** No additional runtime dependencies for the primary path. Core functionality works on Python 3.10+.
|
|
101
101
|
- **Positive.** The skill file is a single Markdown file — easy to audit, version, and customize.
|
|
102
102
|
- **Positive.** MCP users are not excluded — they install the optional extra.
|
|
103
103
|
- **Negative.** The skill-based path relies on the LLM correctly constructing CLI commands. Mitigated by providing explicit command templates in the skill body.
|
|
@@ -4,22 +4,21 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "libcontext"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.8.0"
|
|
8
8
|
description = "Generate optimized LLM context from Python library APIs — CLI, skill, and MCP server"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
11
|
-
requires-python = ">=3.
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
12
|
authors = [
|
|
13
13
|
{ name = "Jonathan VARELA" },
|
|
14
14
|
]
|
|
15
15
|
urls = { Homepage = "https://github.com/Syclaw/libcontext", Repository = "https://github.com/Syclaw/libcontext", Issues = "https://github.com/Syclaw/libcontext/issues" }
|
|
16
16
|
keywords = ["copilot", "context", "documentation", "ast", "introspection", "mcp", "llm"]
|
|
17
17
|
classifiers = [
|
|
18
|
-
"Development Status ::
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
19
|
"Intended Audience :: Developers",
|
|
20
20
|
"License :: OSI Approved :: MIT License",
|
|
21
21
|
"Programming Language :: Python :: 3",
|
|
22
|
-
"Programming Language :: Python :: 3.9",
|
|
23
22
|
"Programming Language :: Python :: 3.10",
|
|
24
23
|
"Programming Language :: Python :: 3.11",
|
|
25
24
|
"Programming Language :: Python :: 3.12",
|
|
@@ -49,6 +48,9 @@ packages = ["src/libcontext"]
|
|
|
49
48
|
include_modules = []
|
|
50
49
|
exclude_modules = []
|
|
51
50
|
include_private = false
|
|
51
|
+
# file_size_limit = 10485760 # 10 MiB — skip source files larger than this
|
|
52
|
+
# output_char_limit = 0 # 0 = unlimited; set to e.g. 120000 for ~30k tokens
|
|
53
|
+
# subprocess_timeout = 10 # seconds for target interpreter queries
|
|
52
54
|
|
|
53
55
|
[tool.pytest.ini_options]
|
|
54
56
|
testpaths = ["tests"]
|
|
@@ -58,7 +60,7 @@ pythonpath = ["src"]
|
|
|
58
60
|
# Ruff — linter & formatter
|
|
59
61
|
# ---------------------------------------------------------------------------
|
|
60
62
|
[tool.ruff]
|
|
61
|
-
target-version = "
|
|
63
|
+
target-version = "py310"
|
|
62
64
|
line-length = 88
|
|
63
65
|
src = ["src", "tests"]
|
|
64
66
|
|
|
@@ -103,7 +105,7 @@ convention = "google"
|
|
|
103
105
|
# Mypy — static type checking
|
|
104
106
|
# ---------------------------------------------------------------------------
|
|
105
107
|
[tool.mypy]
|
|
106
|
-
python_version = "3.
|
|
108
|
+
python_version = "3.10"
|
|
107
109
|
warn_return_any = true
|
|
108
110
|
warn_unused_configs = true
|
|
109
111
|
disallow_untyped_defs = true
|
|
@@ -112,7 +114,7 @@ check_untyped_defs = true
|
|
|
112
114
|
strict_equality = true
|
|
113
115
|
extra_checks = true
|
|
114
116
|
warn_redundant_casts = true
|
|
115
|
-
warn_unused_ignores =
|
|
117
|
+
warn_unused_ignores = true
|
|
116
118
|
show_error_codes = true
|
|
117
119
|
mypy_path = "src"
|
|
118
120
|
|
|
@@ -19,8 +19,6 @@ Detection priority:
|
|
|
19
19
|
7. Neither → use the current process's environment (no delegation).
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
|
-
from __future__ import annotations
|
|
23
|
-
|
|
24
22
|
import json
|
|
25
23
|
import logging
|
|
26
24
|
import os
|
|
@@ -254,6 +252,8 @@ def setup_environment(
|
|
|
254
252
|
def query_target_package(
|
|
255
253
|
python_exe: Path,
|
|
256
254
|
package_name: str,
|
|
255
|
+
*,
|
|
256
|
+
timeout: int | None = None,
|
|
257
257
|
) -> dict[str, object]:
|
|
258
258
|
"""Discover a package by running the target interpreter.
|
|
259
259
|
|
|
@@ -265,6 +265,8 @@ def query_target_package(
|
|
|
265
265
|
Args:
|
|
266
266
|
python_exe: Absolute path to the target Python executable.
|
|
267
267
|
package_name: Package import name (e.g. ``openai``).
|
|
268
|
+
timeout: Subprocess timeout in seconds. Defaults to
|
|
269
|
+
``_SUBPROCESS_TIMEOUT_SECONDS`` (10s).
|
|
268
270
|
|
|
269
271
|
Returns:
|
|
270
272
|
Dict with keys ``path`` (str | None), ``version`` (str | None),
|
|
@@ -273,12 +275,13 @@ def query_target_package(
|
|
|
273
275
|
Raises:
|
|
274
276
|
EnvironmentSetupError: If the subprocess fails or times out.
|
|
275
277
|
"""
|
|
278
|
+
effective_timeout = timeout or _SUBPROCESS_TIMEOUT_SECONDS
|
|
276
279
|
try:
|
|
277
280
|
result = subprocess.run(
|
|
278
281
|
[str(python_exe), "-c", _PACKAGE_DISCOVERY_SCRIPT, package_name],
|
|
279
282
|
capture_output=True,
|
|
280
283
|
text=True,
|
|
281
|
-
timeout=
|
|
284
|
+
timeout=effective_timeout,
|
|
282
285
|
)
|
|
283
286
|
except (FileNotFoundError, OSError) as exc:
|
|
284
287
|
raise EnvironmentSetupError(
|
|
@@ -288,7 +291,7 @@ def query_target_package(
|
|
|
288
291
|
except subprocess.TimeoutExpired as exc:
|
|
289
292
|
raise EnvironmentSetupError(
|
|
290
293
|
str(python_exe),
|
|
291
|
-
f"interpreter timed out after {
|
|
294
|
+
f"interpreter timed out after {effective_timeout}s",
|
|
292
295
|
) from exc
|
|
293
296
|
|
|
294
297
|
if result.returncode != 0:
|
|
@@ -317,11 +320,17 @@ def resolve_python_executable(python_arg: str) -> Path:
|
|
|
317
320
|
in the standard locations (``Scripts/python.exe`` on Windows,
|
|
318
321
|
``bin/python`` on Unix).
|
|
319
322
|
|
|
323
|
+
Returns an absolute path **without following symlinks**. This is
|
|
324
|
+
critical for venv interpreters: Python discovers ``pyvenv.cfg``
|
|
325
|
+
relative to the symlink location, not the symlink target. Following
|
|
326
|
+
the symlink would resolve to a global interpreter that lacks the
|
|
327
|
+
venv's ``site-packages``.
|
|
328
|
+
|
|
320
329
|
Args:
|
|
321
330
|
python_arg: Path to a Python interpreter or venv directory.
|
|
322
331
|
|
|
323
332
|
Returns:
|
|
324
|
-
|
|
333
|
+
Absolute path to the Python executable (symlinks preserved).
|
|
325
334
|
|
|
326
335
|
Raises:
|
|
327
336
|
EnvironmentSetupError: If the path does not exist or no
|
|
@@ -337,7 +346,7 @@ def resolve_python_executable(python_arg: str) -> Path:
|
|
|
337
346
|
|
|
338
347
|
# Direct path to an executable
|
|
339
348
|
if path.is_file():
|
|
340
|
-
return path.
|
|
349
|
+
return path.absolute()
|
|
341
350
|
|
|
342
351
|
# Directory — probe for interpreter
|
|
343
352
|
if path.is_dir():
|
|
@@ -348,7 +357,7 @@ def resolve_python_executable(python_arg: str) -> Path:
|
|
|
348
357
|
python_arg,
|
|
349
358
|
candidate,
|
|
350
359
|
)
|
|
351
|
-
return candidate.
|
|
360
|
+
return candidate.absolute()
|
|
352
361
|
|
|
353
362
|
raise EnvironmentSetupError(
|
|
354
363
|
python_arg,
|
|
@@ -32,8 +32,6 @@ Usage examples::
|
|
|
32
32
|
libctx cache clear requests
|
|
33
33
|
"""
|
|
34
34
|
|
|
35
|
-
from __future__ import annotations
|
|
36
|
-
|
|
37
35
|
import dataclasses
|
|
38
36
|
import json
|
|
39
37
|
import logging
|
|
@@ -218,7 +216,16 @@ def inspect(
|
|
|
218
216
|
quiet: bool,
|
|
219
217
|
verbose: bool,
|
|
220
218
|
) -> None:
|
|
221
|
-
"""Generate LLM context for one or more Python packages.
|
|
219
|
+
r"""Generate LLM context for one or more Python packages.
|
|
220
|
+
|
|
221
|
+
\b
|
|
222
|
+
Examples:
|
|
223
|
+
libctx inspect requests # full API reference
|
|
224
|
+
libctx inspect requests --overview -q # structural map
|
|
225
|
+
libctx inspect requests -m requests.api -q # single module
|
|
226
|
+
libctx inspect requests --search Session # search by name
|
|
227
|
+
libctx inspect flask --format json -o api.json
|
|
228
|
+
"""
|
|
222
229
|
# Configure logging
|
|
223
230
|
if verbose:
|
|
224
231
|
logging.basicConfig(
|
|
@@ -362,11 +369,13 @@ def inspect(
|
|
|
362
369
|
if readme_lines is None:
|
|
363
370
|
readme_lines = 100
|
|
364
371
|
|
|
372
|
+
output_limit = config.output_char_limit if config else 0
|
|
365
373
|
rendered = render_package(
|
|
366
374
|
pkg_info,
|
|
367
375
|
include_readme=not no_readme,
|
|
368
376
|
max_readme_lines=readme_lines,
|
|
369
377
|
extra_context=config.extra_context if config else None,
|
|
378
|
+
max_output_chars=output_limit,
|
|
370
379
|
)
|
|
371
380
|
|
|
372
381
|
all_blocks.append((pkg_info.name, rendered))
|
|
@@ -470,9 +479,12 @@ def _get_skill_content() -> str:
|
|
|
470
479
|
---
|
|
471
480
|
name: lib
|
|
472
481
|
description: >-
|
|
473
|
-
|
|
474
|
-
Use when
|
|
475
|
-
|
|
482
|
+
Inspect the API of an installed Python package with libcontext/libctx.
|
|
483
|
+
Use when you need to understand how to use a library, dependency, SDK,
|
|
484
|
+
client, framework, or package that is unfamiliar, niche, recently
|
|
485
|
+
updated, poorly documented, or not reliable in model memory. Trigger
|
|
486
|
+
for requests like: "check the package API", "inspect this dependency",
|
|
487
|
+
"find the right class/function", or "look up how this library works".
|
|
476
488
|
argument-hint: "<package> [module] [--search query]"
|
|
477
489
|
---
|
|
478
490
|
|
|
@@ -487,28 +499,32 @@ def _get_skill_content() -> str:
|
|
|
487
499
|
or any non-Python library. If the requested package is not a Python
|
|
488
500
|
package, inform the user and stop — do not attempt inspection.
|
|
489
501
|
|
|
502
|
+
## Prerequisites
|
|
503
|
+
|
|
504
|
+
If the project uses uv, prefix all commands with `uv run`.
|
|
505
|
+
The examples below omit the prefix for brevity.
|
|
506
|
+
|
|
490
507
|
## Workflow
|
|
491
508
|
|
|
492
509
|
### Step 1 — Verify installation
|
|
493
510
|
|
|
494
|
-
Run `pip show
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
If not installed, inform the user and stop.
|
|
511
|
+
Run `pip show <package>` to confirm the package is installed and note
|
|
512
|
+
its version. If not installed, inform the user and stop.
|
|
498
513
|
|
|
499
514
|
### Step 2 — Get structural overview
|
|
500
515
|
|
|
501
|
-
Run:
|
|
502
516
|
```
|
|
503
|
-
libctx inspect
|
|
517
|
+
libctx inspect <package> --overview -q
|
|
504
518
|
```
|
|
505
519
|
|
|
506
520
|
This returns module names with class/function names (no signatures).
|
|
507
521
|
Present this overview to understand the package shape.
|
|
508
522
|
|
|
523
|
+
If libctx cannot find the package (e.g. non-standard venv location),
|
|
524
|
+
pass `--python /path/to/.venv` to point it at the right environment.
|
|
525
|
+
|
|
509
526
|
### Step 3 — Drill into relevant modules
|
|
510
527
|
|
|
511
|
-
Based on the task at hand, request detailed API for specific modules:
|
|
512
528
|
```
|
|
513
529
|
libctx inspect <package> --module <module_name> -q
|
|
514
530
|
```
|
|
@@ -518,37 +534,23 @@ def _get_skill_content() -> str:
|
|
|
518
534
|
|
|
519
535
|
### Step 4 — Search when needed
|
|
520
536
|
|
|
521
|
-
If looking for a specific class, function, or method:
|
|
522
537
|
```
|
|
523
538
|
libctx inspect <package> --search <query> -q
|
|
524
539
|
```
|
|
525
540
|
|
|
526
|
-
|
|
541
|
+
Searches across all modules by name and docstring (case-insensitive).
|
|
527
542
|
|
|
528
543
|
To narrow results by type, add `--type`:
|
|
529
544
|
```
|
|
530
545
|
libctx inspect <package> --search <query> --type class -q
|
|
531
|
-
libctx inspect <package> --search <query> --type function -q
|
|
532
546
|
```
|
|
533
547
|
|
|
534
548
|
Valid types: `class`, `function`, `variable`, `alias`.
|
|
535
549
|
|
|
536
|
-
### Step 5 — JSON
|
|
550
|
+
### Step 5 — JSON output (advanced)
|
|
537
551
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
libctx inspect <package> --format json -q
|
|
541
|
-
libctx inspect <package> --module <module_name> --format json -q
|
|
542
|
-
libctx inspect <package> --search <query> --format json -q
|
|
543
|
-
```
|
|
544
|
-
|
|
545
|
-
To compare API versions after a package upgrade:
|
|
546
|
-
```
|
|
547
|
-
libctx inspect <package> --format json -q > old.json
|
|
548
|
-
# ... upgrade the package ...
|
|
549
|
-
libctx inspect <package> --format json -q > new.json
|
|
550
|
-
libctx diff old.json new.json
|
|
551
|
-
```
|
|
552
|
+
Add `--format json` to any inspect command for structured output.
|
|
553
|
+
Use `libctx diff old.json new.json` to compare API snapshots.
|
|
552
554
|
|
|
553
555
|
## Rules
|
|
554
556
|
|
|
@@ -556,14 +558,13 @@ def _get_skill_content() -> str:
|
|
|
556
558
|
as the full output may be very large and saturate context.
|
|
557
559
|
- Request at most 2-3 modules per invocation cycle. If more are needed,
|
|
558
560
|
summarize what was learned so far, then request the next batch.
|
|
559
|
-
-
|
|
561
|
+
- Always use `-q` to suppress stderr noise.
|
|
560
562
|
- If the user specifies a module directly (e.g., `/lib requests requests.auth`),
|
|
561
563
|
skip the overview and go straight to `--module`.
|
|
562
|
-
- If a signature
|
|
563
|
-
|
|
564
|
-
`libctx inspect <package> --module <module_name> --no-cache -q`
|
|
564
|
+
- If a signature doesn't match what the code expects, add `--no-cache`
|
|
565
|
+
to force a fresh scan.
|
|
565
566
|
- Use `--type` with `--search` to reduce noise when you know what kind of
|
|
566
|
-
symbol you need
|
|
567
|
+
symbol you need.
|
|
567
568
|
|
|
568
569
|
## Safety limits
|
|
569
570
|
|
|
@@ -852,7 +853,13 @@ def _format_age(iso_timestamp: str) -> str:
|
|
|
852
853
|
help="Output format.",
|
|
853
854
|
)
|
|
854
855
|
def diff(old_file: Path, new_file: Path, output_format: str) -> None:
|
|
855
|
-
"""Compare two API snapshots and show what changed.
|
|
856
|
+
r"""Compare two API snapshots and show what changed.
|
|
857
|
+
|
|
858
|
+
\b
|
|
859
|
+
Examples:
|
|
860
|
+
libctx diff old.json new.json # markdown diff
|
|
861
|
+
libctx diff old.json new.json --format json
|
|
862
|
+
"""
|
|
856
863
|
from ._security import MAX_JSON_INPUT_BYTES
|
|
857
864
|
|
|
858
865
|
for label, path in (("old_file", old_file), ("new_file", new_file)):
|
|
@@ -5,8 +5,6 @@ rules from the optional ``[tool.libcontext]`` configuration, and returns a
|
|
|
5
5
|
complete :class:`~libcontext.models.PackageInfo` data structure.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
8
|
import copy
|
|
11
9
|
import difflib
|
|
12
10
|
import importlib.metadata
|
|
@@ -164,11 +162,11 @@ def _find_stub_package(package_name: str) -> Path | None:
|
|
|
164
162
|
if dist.files:
|
|
165
163
|
for f in dist.files:
|
|
166
164
|
if str(f).endswith(".pyi"):
|
|
167
|
-
stub_root = Path(dist.locate_file(f.parts[0]))
|
|
165
|
+
stub_root = Path(str(dist.locate_file(f.parts[0])))
|
|
168
166
|
if stub_root.is_dir():
|
|
169
167
|
return stub_root
|
|
170
168
|
# Fallback: convention-based path
|
|
171
|
-
site_packages = Path(dist.locate_file(""))
|
|
169
|
+
site_packages = Path(str(dist.locate_file("")))
|
|
172
170
|
for suffix in (f"{package_name}-stubs", f"{norm_name}-stubs"):
|
|
173
171
|
candidate = site_packages / suffix
|
|
174
172
|
if candidate.is_dir():
|
|
@@ -357,8 +355,8 @@ def _get_package_metadata(package_name: str) -> dict[str, str | None]:
|
|
|
357
355
|
try:
|
|
358
356
|
meta = importlib.metadata.metadata(package_name)
|
|
359
357
|
return {
|
|
360
|
-
"version": meta.get("Version"),
|
|
361
|
-
"summary": meta.get("Summary"),
|
|
358
|
+
"version": meta.get("Version"), # type: ignore[attr-defined]
|
|
359
|
+
"summary": meta.get("Summary"), # type: ignore[attr-defined]
|
|
362
360
|
}
|
|
363
361
|
except importlib.metadata.PackageNotFoundError:
|
|
364
362
|
logger.debug("No installed metadata for '%s'", package_name)
|
|
@@ -380,7 +378,7 @@ def _find_readme(package_name: str, package_path: Path | None) -> str | None:
|
|
|
380
378
|
# 1. Metadata long description
|
|
381
379
|
try:
|
|
382
380
|
meta = importlib.metadata.metadata(package_name)
|
|
383
|
-
body = meta.get_payload() # type: ignore[
|
|
381
|
+
body = meta.get_payload() # type: ignore[attr-defined]
|
|
384
382
|
if isinstance(body, str) and body.strip():
|
|
385
383
|
logger.debug("README found via metadata for '%s'", package_name)
|
|
386
384
|
return body.strip()
|
|
@@ -431,13 +429,15 @@ def _safe_rglob(root: Path, pattern: str) -> list[Path]:
|
|
|
431
429
|
return results
|
|
432
430
|
|
|
433
431
|
|
|
434
|
-
def _is_safe_source_file(
|
|
432
|
+
def _is_safe_source_file(
|
|
433
|
+
file_path: Path, root: Path, *, file_size_limit: int = 0
|
|
434
|
+
) -> bool:
|
|
435
435
|
"""Check that a source file is safe to read.
|
|
436
436
|
|
|
437
437
|
Rejects files that escape the package boundary via symlinks and
|
|
438
438
|
files larger than the configured limit (likely generated data, not API).
|
|
439
439
|
"""
|
|
440
|
-
from ._security import check_file_size, is_within_boundary
|
|
440
|
+
from ._security import MAX_SOURCE_FILE_BYTES, check_file_size, is_within_boundary
|
|
441
441
|
|
|
442
442
|
if not is_within_boundary(file_path, root):
|
|
443
443
|
logger.warning(
|
|
@@ -446,7 +446,8 @@ def _is_safe_source_file(file_path: Path, root: Path) -> bool:
|
|
|
446
446
|
root,
|
|
447
447
|
)
|
|
448
448
|
return False
|
|
449
|
-
if
|
|
449
|
+
effective_limit = file_size_limit if file_size_limit > 0 else MAX_SOURCE_FILE_BYTES
|
|
450
|
+
if not check_file_size(file_path, limit=effective_limit):
|
|
450
451
|
logger.warning("Skipped %s: exceeds source file size limit", file_path)
|
|
451
452
|
return False
|
|
452
453
|
return True
|
|
@@ -546,7 +547,9 @@ def _walk_package(
|
|
|
546
547
|
for source_file in _safe_rglob(package_path, "*.py*"):
|
|
547
548
|
if source_file.suffix not in (".py", ".pyi"):
|
|
548
549
|
continue
|
|
549
|
-
if not _is_safe_source_file(
|
|
550
|
+
if not _is_safe_source_file(
|
|
551
|
+
source_file, package_path, file_size_limit=config.file_size_limit
|
|
552
|
+
):
|
|
550
553
|
continue
|
|
551
554
|
relative = source_file.relative_to(package_path)
|
|
552
555
|
parts = relative.parts
|
|
@@ -562,7 +565,9 @@ def _walk_package(
|
|
|
562
565
|
# Standalone stub .pyi files
|
|
563
566
|
if stub_path is not None:
|
|
564
567
|
for pyi_file in _safe_rglob(stub_path, "*.pyi"):
|
|
565
|
-
if not _is_safe_source_file(
|
|
568
|
+
if not _is_safe_source_file(
|
|
569
|
+
pyi_file, stub_path, file_size_limit=config.file_size_limit
|
|
570
|
+
):
|
|
566
571
|
continue
|
|
567
572
|
relative = pyi_file.relative_to(stub_path)
|
|
568
573
|
parts = relative.parts
|
|
@@ -672,6 +677,8 @@ def _find_stub_package_fs(package_name: str, pkg_path: Path) -> Path | None:
|
|
|
672
677
|
def _resolve_via_target(
|
|
673
678
|
package_name: str,
|
|
674
679
|
python_exe: Path,
|
|
680
|
+
*,
|
|
681
|
+
subprocess_timeout: int = 0,
|
|
675
682
|
) -> tuple[Path, dict[str, str | None], Path | None]:
|
|
676
683
|
"""Discover a package by querying the target interpreter.
|
|
677
684
|
|
|
@@ -681,6 +688,7 @@ def _resolve_via_target(
|
|
|
681
688
|
Args:
|
|
682
689
|
package_name: The importable package name.
|
|
683
690
|
python_exe: Path to the target Python interpreter.
|
|
691
|
+
subprocess_timeout: Timeout in seconds (0 = use module default).
|
|
684
692
|
|
|
685
693
|
Returns:
|
|
686
694
|
A ``(pkg_path, metadata, stub_path)`` tuple.
|
|
@@ -690,7 +698,8 @@ def _resolve_via_target(
|
|
|
690
698
|
"""
|
|
691
699
|
from ._envsetup import query_target_package
|
|
692
700
|
|
|
693
|
-
|
|
701
|
+
timeout_kw = {"timeout": subprocess_timeout} if subprocess_timeout > 0 else {}
|
|
702
|
+
data = query_target_package(python_exe, package_name, **timeout_kw)
|
|
694
703
|
|
|
695
704
|
pkg_path_str: str | None = data.get("path") # type: ignore[assignment]
|
|
696
705
|
installed: list[str] = data.get("installed", []) # type: ignore[assignment]
|
|
@@ -790,6 +799,9 @@ def collect_package(
|
|
|
790
799
|
pkg_path, metadata, stub_path = _resolve_via_target(
|
|
791
800
|
package_name,
|
|
792
801
|
target_python,
|
|
802
|
+
subprocess_timeout=config_override.subprocess_timeout
|
|
803
|
+
if config_override
|
|
804
|
+
else 0,
|
|
793
805
|
)
|
|
794
806
|
pkg_name = package_name
|
|
795
807
|
logger.debug("Resolved '%s' via target interpreter: %s", package_name, pkg_path)
|