libcontext 0.4.0__tar.gz → 0.6.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.4.0 → libcontext-0.6.0}/CHANGELOG.md +10 -1
- {libcontext-0.4.0 → libcontext-0.6.0}/PKG-INFO +24 -14
- {libcontext-0.4.0 → libcontext-0.6.0}/README.md +23 -13
- {libcontext-0.4.0 → libcontext-0.6.0}/pyproject.toml +1 -1
- {libcontext-0.4.0 → libcontext-0.6.0}/src/libcontext/cache.py +68 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/src/libcontext/cli.py +76 -5
- {libcontext-0.4.0 → libcontext-0.6.0}/tests/test_cache.py +162 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/tests/test_cli.py +146 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/.github/workflows/ci.yml +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/.github/workflows/release.yml +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/.gitignore +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/CONTRIBUTING.md +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/DEPENDENCIES.md +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/LICENSE +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/SECURITY.md +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/docs/adr/001-progressive-disclosure-over-always-on-context.md +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/docs/adr/002-skill-plus-cli-as-primary-integration.md +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/docs/adr/004-ast-only-inspection.md +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/docs/adr/README.md +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/src/libcontext/__init__.py +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/src/libcontext/_envsetup.py +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/src/libcontext/_security.py +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/src/libcontext/collector.py +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/src/libcontext/config.py +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/src/libcontext/diff.py +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/src/libcontext/exceptions.py +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/src/libcontext/inspector.py +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/src/libcontext/mcp_server.py +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/src/libcontext/models.py +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/src/libcontext/py.typed +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/src/libcontext/renderer.py +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/tests/__init__.py +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/tests/test_cli_mcp_parity.py +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/tests/test_collector.py +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/tests/test_config.py +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/tests/test_diff.py +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/tests/test_envsetup.py +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/tests/test_inspector.py +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/tests/test_mcp_server.py +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/tests/test_models.py +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/tests/test_renderer.py +0 -0
- {libcontext-0.4.0 → libcontext-0.6.0}/tests/test_security.py +0 -0
|
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0] - 2026-03-23
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`--version` flag**: `libctx --version` displays the installed version, read from package metadata.
|
|
15
|
+
- **`cache list`**: new subcommand showing cached packages with version, size, and relative age.
|
|
16
|
+
- **`cache clear <package>`**: selective cache clearing by package name (normalises hyphens and case). Without argument, `cache clear` still removes all entries as before.
|
|
17
|
+
|
|
10
18
|
## [0.4.0] - 2026-03-23
|
|
11
19
|
|
|
12
20
|
### Added
|
|
@@ -93,7 +101,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
93
101
|
- Free-form `extra_context` field for library authors.
|
|
94
102
|
- Python API for programmatic usage (`collect_package`, `render_package`).
|
|
95
103
|
|
|
96
|
-
[Unreleased]: https://github.com/Syclaw/libcontext/compare/v0.
|
|
104
|
+
[Unreleased]: https://github.com/Syclaw/libcontext/compare/v0.5.0...HEAD
|
|
105
|
+
[0.5.0]: https://github.com/Syclaw/libcontext/compare/v0.4.0...v0.5.0
|
|
97
106
|
[0.4.0]: https://github.com/Syclaw/libcontext/compare/v0.3.0...v0.4.0
|
|
98
107
|
[0.3.0]: https://github.com/Syclaw/libcontext/compare/v0.2.0...v0.3.0
|
|
99
108
|
[0.2.0]: https://github.com/Syclaw/libcontext/compare/v0.1.0...v0.2.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: libcontext
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.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
|
|
@@ -37,30 +37,38 @@ Description-Content-Type: text/markdown
|
|
|
37
37
|
[](https://github.com/astral-sh/ruff)
|
|
38
38
|
[](https://mypy-lang.org/)
|
|
39
39
|
|
|
40
|
-
>
|
|
40
|
+
> Context-efficient API references for LLM toolchains — structured, on-demand, not always-on.
|
|
41
41
|
|
|
42
|
-
**libcontext** inspects any installed Python package via static AST analysis (no code execution) and generates compact Markdown API references. It integrates with Claude Code (via a `/lib` skill) and VS Code
|
|
42
|
+
**libcontext** inspects any installed Python package via static AST analysis (no code execution) and generates compact Markdown API references. It integrates with Claude Code, GitHub Copilot (via a `/lib` skill), and VS Code / Cursor (via an MCP server) to provide **progressive disclosure** — only loading API context when you actually need it, avoiding context window pollution.
|
|
43
43
|
|
|
44
44
|
## Why This Exists
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
LLMs can often use popular libraries correctly from training data alone. For well-known packages like `requests` or `flask`, libcontext adds little value. The real problems arise in specific scenarios:
|
|
47
47
|
|
|
48
48
|
- **Internal / private libraries** — Zero training data exists. The model has never seen the API.
|
|
49
|
-
- **Niche open-source packages** — Sparse or outdated training data leads to hallucinated methods and wrong signatures.
|
|
49
|
+
- **Niche open-source packages** — Sparse or outdated training data leads to hallucinated methods and wrong signatures. GPT-4o achieves only 38% valid invocations on low-frequency APIs ([Amazon Science, ICSE 2025](https://www.amazon.science/publications/on-mitigating-code-llm-hallucinations-with-api-documentation)).
|
|
50
50
|
- **New versions of any library** — Training data has a cutoff. The model knows v2, you're using v3.
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
Even when an LLM could read source files directly, structured API summaries are more context-efficient: providing API documentation via retrieval improves pass rates by 83–220% compared to no documentation, while consuming far fewer tokens than raw source code ([arXiv 2503.15231, March 2025](https://arxiv.org/abs/2503.15231)).
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
Dumping entire API references into always-on instruction files wastes context window on every interaction. Selective retrieval outperforms always-on injection — always-on docs actually hurt performance on well-known APIs ([Amazon Science, ICSE 2025](https://www.amazon.science/publications/on-mitigating-code-llm-hallucinations-with-api-documentation)).
|
|
55
|
+
|
|
56
|
+
libcontext addresses this with **progressive disclosure**: overview first, then drill into specific modules only when needed.
|
|
55
57
|
|
|
56
58
|
## When libcontext makes the biggest difference
|
|
57
59
|
|
|
58
60
|
| Scenario | Impact | Why |
|
|
59
61
|
|---|---|---|
|
|
60
|
-
| **Internal / private libraries** | Critical | Zero training data
|
|
61
|
-
| **Niche open-source packages** | High | Sparse training data
|
|
62
|
+
| **Internal / private libraries** | Critical | Zero training data — the model has never seen the API |
|
|
63
|
+
| **Niche open-source packages** | High | Sparse training data; 19.7% of LLM package suggestions are hallucinated ([USENIX Security 2025](https://www.usenix.org/publications/loginonline/we-have-package-you-comprehensive-analysis-package-hallucinations-code)) |
|
|
62
64
|
| **New versions of any library** | High | Training cutoff — the LLM knows v2, you're using v3 |
|
|
63
|
-
| **Popular, stable libraries** | Low | The LLM already has good knowledge from training data |
|
|
65
|
+
| **Popular, stable libraries** | Low | The LLM already has good knowledge from training data — libcontext adds little here |
|
|
66
|
+
|
|
67
|
+
### What libcontext does NOT do
|
|
68
|
+
|
|
69
|
+
- **Replace reading source code** — LLMs with tool access (Claude Code, Cursor) can read files directly. For popular libraries, that's often sufficient.
|
|
70
|
+
- **Guarantee correctness** — Even with perfect API docs, LLMs still make errors. Research shows pass rates of 74–91% with target documentation, not 100% ([arXiv 2503.15231](https://arxiv.org/abs/2503.15231)).
|
|
71
|
+
- **Provide usage examples** — libcontext extracts signatures and docstrings, not example code. Research indicates examples have the highest impact on code generation quality.
|
|
64
72
|
|
|
65
73
|
## Quick Start
|
|
66
74
|
|
|
@@ -99,7 +107,7 @@ libctx inspect requests libctx inspect requests libctx inspect requests
|
|
|
99
107
|
(no signatures) for one module across all modules
|
|
100
108
|
```
|
|
101
109
|
|
|
102
|
-
The `/lib` skill (Claude Code) and MCP server (VS Code
|
|
110
|
+
The `/lib` skill (Claude Code, GitHub Copilot) and MCP server (Claude Code, VS Code, Cursor) automate this workflow — the AI assistant decides what to inspect based on the task at hand.
|
|
103
111
|
|
|
104
112
|
### Direct CLI Usage
|
|
105
113
|
|
|
@@ -138,8 +146,10 @@ libctx diff old.json new.json
|
|
|
138
146
|
# Bypass disk cache
|
|
139
147
|
libctx inspect requests --no-cache
|
|
140
148
|
|
|
141
|
-
#
|
|
142
|
-
libctx cache
|
|
149
|
+
# Cache management
|
|
150
|
+
libctx cache list # show cached packages with size and age
|
|
151
|
+
libctx cache clear # clear all cached API data
|
|
152
|
+
libctx cache clear requests # clear only the entries for one package
|
|
143
153
|
```
|
|
144
154
|
|
|
145
155
|
### AST Analysis
|
|
@@ -322,7 +332,7 @@ All async operations use httpx internally.
|
|
|
322
332
|
| `diff.py` | API diff between two package versions with breaking change detection |
|
|
323
333
|
| `cache.py` | Persistent disk cache with mtime/file-count invalidation and LRU eviction |
|
|
324
334
|
| `cli.py` | CLI entry point — `inspect`, `install`, `diff`, and `cache` subcommands |
|
|
325
|
-
| `mcp_server.py` | MCP server for VS Code / Cursor integration (optional) |
|
|
335
|
+
| `mcp_server.py` | MCP server for Claude Code / VS Code / Cursor integration (optional) |
|
|
326
336
|
|
|
327
337
|
## Development
|
|
328
338
|
|
|
@@ -8,30 +8,38 @@
|
|
|
8
8
|
[](https://github.com/astral-sh/ruff)
|
|
9
9
|
[](https://mypy-lang.org/)
|
|
10
10
|
|
|
11
|
-
>
|
|
11
|
+
> Context-efficient API references for LLM toolchains — structured, on-demand, not always-on.
|
|
12
12
|
|
|
13
|
-
**libcontext** inspects any installed Python package via static AST analysis (no code execution) and generates compact Markdown API references. It integrates with Claude Code (via a `/lib` skill) and VS Code
|
|
13
|
+
**libcontext** inspects any installed Python package via static AST analysis (no code execution) and generates compact Markdown API references. It integrates with Claude Code, GitHub Copilot (via a `/lib` skill), and VS Code / Cursor (via an MCP server) to provide **progressive disclosure** — only loading API context when you actually need it, avoiding context window pollution.
|
|
14
14
|
|
|
15
15
|
## Why This Exists
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
LLMs can often use popular libraries correctly from training data alone. For well-known packages like `requests` or `flask`, libcontext adds little value. The real problems arise in specific scenarios:
|
|
18
18
|
|
|
19
19
|
- **Internal / private libraries** — Zero training data exists. The model has never seen the API.
|
|
20
|
-
- **Niche open-source packages** — Sparse or outdated training data leads to hallucinated methods and wrong signatures.
|
|
20
|
+
- **Niche open-source packages** — Sparse or outdated training data leads to hallucinated methods and wrong signatures. GPT-4o achieves only 38% valid invocations on low-frequency APIs ([Amazon Science, ICSE 2025](https://www.amazon.science/publications/on-mitigating-code-llm-hallucinations-with-api-documentation)).
|
|
21
21
|
- **New versions of any library** — Training data has a cutoff. The model knows v2, you're using v3.
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Even when an LLM could read source files directly, structured API summaries are more context-efficient: providing API documentation via retrieval improves pass rates by 83–220% compared to no documentation, while consuming far fewer tokens than raw source code ([arXiv 2503.15231, March 2025](https://arxiv.org/abs/2503.15231)).
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Dumping entire API references into always-on instruction files wastes context window on every interaction. Selective retrieval outperforms always-on injection — always-on docs actually hurt performance on well-known APIs ([Amazon Science, ICSE 2025](https://www.amazon.science/publications/on-mitigating-code-llm-hallucinations-with-api-documentation)).
|
|
26
|
+
|
|
27
|
+
libcontext addresses this with **progressive disclosure**: overview first, then drill into specific modules only when needed.
|
|
26
28
|
|
|
27
29
|
## When libcontext makes the biggest difference
|
|
28
30
|
|
|
29
31
|
| Scenario | Impact | Why |
|
|
30
32
|
|---|---|---|
|
|
31
|
-
| **Internal / private libraries** | Critical | Zero training data
|
|
32
|
-
| **Niche open-source packages** | High | Sparse training data
|
|
33
|
+
| **Internal / private libraries** | Critical | Zero training data — the model has never seen the API |
|
|
34
|
+
| **Niche open-source packages** | High | Sparse training data; 19.7% of LLM package suggestions are hallucinated ([USENIX Security 2025](https://www.usenix.org/publications/loginonline/we-have-package-you-comprehensive-analysis-package-hallucinations-code)) |
|
|
33
35
|
| **New versions of any library** | High | Training cutoff — the LLM knows v2, you're using v3 |
|
|
34
|
-
| **Popular, stable libraries** | Low | The LLM already has good knowledge from training data |
|
|
36
|
+
| **Popular, stable libraries** | Low | The LLM already has good knowledge from training data — libcontext adds little here |
|
|
37
|
+
|
|
38
|
+
### What libcontext does NOT do
|
|
39
|
+
|
|
40
|
+
- **Replace reading source code** — LLMs with tool access (Claude Code, Cursor) can read files directly. For popular libraries, that's often sufficient.
|
|
41
|
+
- **Guarantee correctness** — Even with perfect API docs, LLMs still make errors. Research shows pass rates of 74–91% with target documentation, not 100% ([arXiv 2503.15231](https://arxiv.org/abs/2503.15231)).
|
|
42
|
+
- **Provide usage examples** — libcontext extracts signatures and docstrings, not example code. Research indicates examples have the highest impact on code generation quality.
|
|
35
43
|
|
|
36
44
|
## Quick Start
|
|
37
45
|
|
|
@@ -70,7 +78,7 @@ libctx inspect requests libctx inspect requests libctx inspect requests
|
|
|
70
78
|
(no signatures) for one module across all modules
|
|
71
79
|
```
|
|
72
80
|
|
|
73
|
-
The `/lib` skill (Claude Code) and MCP server (VS Code
|
|
81
|
+
The `/lib` skill (Claude Code, GitHub Copilot) and MCP server (Claude Code, VS Code, Cursor) automate this workflow — the AI assistant decides what to inspect based on the task at hand.
|
|
74
82
|
|
|
75
83
|
### Direct CLI Usage
|
|
76
84
|
|
|
@@ -109,8 +117,10 @@ libctx diff old.json new.json
|
|
|
109
117
|
# Bypass disk cache
|
|
110
118
|
libctx inspect requests --no-cache
|
|
111
119
|
|
|
112
|
-
#
|
|
113
|
-
libctx cache
|
|
120
|
+
# Cache management
|
|
121
|
+
libctx cache list # show cached packages with size and age
|
|
122
|
+
libctx cache clear # clear all cached API data
|
|
123
|
+
libctx cache clear requests # clear only the entries for one package
|
|
114
124
|
```
|
|
115
125
|
|
|
116
126
|
### AST Analysis
|
|
@@ -293,7 +303,7 @@ All async operations use httpx internally.
|
|
|
293
303
|
| `diff.py` | API diff between two package versions with breaking change detection |
|
|
294
304
|
| `cache.py` | Persistent disk cache with mtime/file-count invalidation and LRU eviction |
|
|
295
305
|
| `cli.py` | CLI entry point — `inspect`, `install`, `diff`, and `cache` subcommands |
|
|
296
|
-
| `mcp_server.py` | MCP server for VS Code / Cursor integration (optional) |
|
|
306
|
+
| `mcp_server.py` | MCP server for Claude Code / VS Code / Cursor integration (optional) |
|
|
297
307
|
|
|
298
308
|
## Development
|
|
299
309
|
|
|
@@ -258,6 +258,74 @@ def clear_all() -> int:
|
|
|
258
258
|
return count
|
|
259
259
|
|
|
260
260
|
|
|
261
|
+
def clear_package(package_name: str) -> int:
|
|
262
|
+
"""Remove cached entries matching a package name.
|
|
263
|
+
|
|
264
|
+
Matches entries whose embedded ``name`` field equals *package_name*
|
|
265
|
+
(case-insensitive, normalised with hyphens → underscores).
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
package_name: Package name to clear.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Number of entries removed.
|
|
272
|
+
"""
|
|
273
|
+
target = package_name.lower().replace("-", "_")
|
|
274
|
+
cache_dir = _get_cache_dir()
|
|
275
|
+
count = 0
|
|
276
|
+
for f in cache_dir.glob("*.json"):
|
|
277
|
+
try:
|
|
278
|
+
raw = json.loads(f.read_text(encoding="utf-8"))
|
|
279
|
+
data = _deserialize_envelope(raw)
|
|
280
|
+
except (json.JSONDecodeError, ValueError, OSError):
|
|
281
|
+
continue
|
|
282
|
+
name = data.get("name", "")
|
|
283
|
+
if isinstance(name, str) and name.lower().replace("-", "_") == target:
|
|
284
|
+
_safe_delete(f)
|
|
285
|
+
count += 1
|
|
286
|
+
return count
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@dataclass
|
|
290
|
+
class CacheEntry:
|
|
291
|
+
"""Summary of a single cache entry for display."""
|
|
292
|
+
|
|
293
|
+
package: str
|
|
294
|
+
version: str
|
|
295
|
+
cached_at: str
|
|
296
|
+
size_bytes: int
|
|
297
|
+
file_path: Path
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def list_entries() -> list[CacheEntry]:
|
|
301
|
+
"""List all cached API snapshots with metadata.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
List of :class:`CacheEntry` sorted by package name then version.
|
|
305
|
+
"""
|
|
306
|
+
cache_dir = _get_cache_dir()
|
|
307
|
+
entries: list[CacheEntry] = []
|
|
308
|
+
for f in cache_dir.glob("*.json"):
|
|
309
|
+
try:
|
|
310
|
+
size = f.stat().st_size
|
|
311
|
+
raw = json.loads(f.read_text(encoding="utf-8"))
|
|
312
|
+
data = _deserialize_envelope(raw)
|
|
313
|
+
except (json.JSONDecodeError, ValueError, OSError):
|
|
314
|
+
continue
|
|
315
|
+
meta = data.get("_cache_meta", {})
|
|
316
|
+
entries.append(
|
|
317
|
+
CacheEntry(
|
|
318
|
+
package=data.get("name", "unknown"),
|
|
319
|
+
version=data.get("version", "unknown"),
|
|
320
|
+
cached_at=meta.get("cached_at", "unknown"),
|
|
321
|
+
size_bytes=size,
|
|
322
|
+
file_path=f,
|
|
323
|
+
)
|
|
324
|
+
)
|
|
325
|
+
entries.sort(key=lambda e: (e.package.lower(), e.version))
|
|
326
|
+
return entries
|
|
327
|
+
|
|
328
|
+
|
|
261
329
|
def _cache_filename(
|
|
262
330
|
package_name: str,
|
|
263
331
|
version: str | None,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""CLI entry point for libcontext.
|
|
2
2
|
|
|
3
|
-
Provides the ``libctx`` command with
|
|
3
|
+
Provides the ``libctx`` command with subcommands:
|
|
4
4
|
|
|
5
5
|
``inspect``
|
|
6
6
|
Generate LLM-optimised Markdown context from installed Python packages.
|
|
@@ -8,6 +8,12 @@ Provides the ``libctx`` command with two subcommands:
|
|
|
8
8
|
``install``
|
|
9
9
|
Install libcontext integration files (skills, MCP) into the current project.
|
|
10
10
|
|
|
11
|
+
``diff``
|
|
12
|
+
Compare two API snapshots and show what changed.
|
|
13
|
+
|
|
14
|
+
``cache``
|
|
15
|
+
Manage the disk cache (``list``, ``clear``).
|
|
16
|
+
|
|
11
17
|
Usage examples::
|
|
12
18
|
|
|
13
19
|
libctx inspect requests
|
|
@@ -18,6 +24,12 @@ Usage examples::
|
|
|
18
24
|
libctx install --skills
|
|
19
25
|
libctx install --mcp --target vscode
|
|
20
26
|
libctx install --all --target all
|
|
27
|
+
|
|
28
|
+
libctx diff old.json new.json
|
|
29
|
+
|
|
30
|
+
libctx cache list
|
|
31
|
+
libctx cache clear
|
|
32
|
+
libctx cache clear requests
|
|
21
33
|
"""
|
|
22
34
|
|
|
23
35
|
from __future__ import annotations
|
|
@@ -54,6 +66,7 @@ from .renderer import (
|
|
|
54
66
|
|
|
55
67
|
|
|
56
68
|
@click.group()
|
|
69
|
+
@click.version_option(package_name="libcontext")
|
|
57
70
|
def main() -> None:
|
|
58
71
|
"""Generate LLM-optimised context from Python library APIs."""
|
|
59
72
|
|
|
@@ -758,10 +771,68 @@ def cache() -> None:
|
|
|
758
771
|
|
|
759
772
|
|
|
760
773
|
@cache.command()
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
774
|
+
@click.argument("package", required=False, default=None)
|
|
775
|
+
def clear(package: str | None) -> None:
|
|
776
|
+
"""Remove cached API snapshots.
|
|
777
|
+
|
|
778
|
+
When PACKAGE is given, only entries for that package are removed.
|
|
779
|
+
Without arguments, all entries are cleared.
|
|
780
|
+
"""
|
|
781
|
+
if package is not None:
|
|
782
|
+
count = _cache.clear_package(package)
|
|
783
|
+
label = f"for {package!r}" if count else f"no entries found for {package!r}"
|
|
784
|
+
else:
|
|
785
|
+
count = _cache.clear_all()
|
|
786
|
+
label = "cache entries" if count else "cache entries (already empty)"
|
|
787
|
+
click.echo(f"Cleared {count} {label}.")
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
@cache.command(name="list")
|
|
791
|
+
def list_() -> None:
|
|
792
|
+
"""Show cached API snapshots."""
|
|
793
|
+
entries = _cache.list_entries()
|
|
794
|
+
if not entries:
|
|
795
|
+
click.echo("Cache is empty.")
|
|
796
|
+
return
|
|
797
|
+
total_bytes = 0
|
|
798
|
+
for entry in entries:
|
|
799
|
+
age = _format_age(entry.cached_at)
|
|
800
|
+
size = _format_size(entry.size_bytes)
|
|
801
|
+
click.echo(f" {entry.package} {entry.version} ({size}, {age})")
|
|
802
|
+
total_bytes += entry.size_bytes
|
|
803
|
+
click.echo(f"\n{len(entries)} entries, {_format_size(total_bytes)} total.")
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def _format_size(size_bytes: int) -> str:
|
|
807
|
+
"""Format byte count as a human-readable string."""
|
|
808
|
+
if size_bytes < 1024:
|
|
809
|
+
return f"{size_bytes} B"
|
|
810
|
+
if size_bytes < 1024 * 1024:
|
|
811
|
+
return f"{size_bytes / 1024:.1f} kB"
|
|
812
|
+
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def _format_age(iso_timestamp: str) -> str:
|
|
816
|
+
"""Format an ISO timestamp as a relative age string."""
|
|
817
|
+
import datetime
|
|
818
|
+
|
|
819
|
+
try:
|
|
820
|
+
cached = datetime.datetime.fromisoformat(iso_timestamp)
|
|
821
|
+
now = datetime.datetime.now(tz=datetime.timezone.utc)
|
|
822
|
+
delta = now - cached
|
|
823
|
+
seconds = int(delta.total_seconds())
|
|
824
|
+
if seconds < 60:
|
|
825
|
+
return "just now"
|
|
826
|
+
if seconds < 3600:
|
|
827
|
+
m = seconds // 60
|
|
828
|
+
return f"{m}m ago"
|
|
829
|
+
if seconds < 86400:
|
|
830
|
+
h = seconds // 3600
|
|
831
|
+
return f"{h}h ago"
|
|
832
|
+
d = seconds // 86400
|
|
833
|
+
return f"{d}d ago"
|
|
834
|
+
except (ValueError, TypeError):
|
|
835
|
+
return "unknown age"
|
|
765
836
|
|
|
766
837
|
|
|
767
838
|
# ---------------------------------------------------------------------------
|
|
@@ -12,6 +12,8 @@ from libcontext.cache import (
|
|
|
12
12
|
_evict_oldest,
|
|
13
13
|
_get_cache_dir,
|
|
14
14
|
clear_all,
|
|
15
|
+
clear_package,
|
|
16
|
+
list_entries,
|
|
15
17
|
load,
|
|
16
18
|
save,
|
|
17
19
|
)
|
|
@@ -286,6 +288,166 @@ def test_clear_all_empty(tmp_path, monkeypatch):
|
|
|
286
288
|
assert count == 0
|
|
287
289
|
|
|
288
290
|
|
|
291
|
+
# ---------------------------------------------------------------------------
|
|
292
|
+
# clear_package
|
|
293
|
+
# ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def test_clear_package_removes_matching(tmp_path, monkeypatch):
|
|
297
|
+
"""Only entries for the given package are removed."""
|
|
298
|
+
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
|
|
299
|
+
monkeypatch.setattr("libcontext.cache.sys.platform", "linux")
|
|
300
|
+
|
|
301
|
+
src_dir = tmp_path / "src"
|
|
302
|
+
src_dir.mkdir()
|
|
303
|
+
(src_dir / "mod.py").write_text("# code")
|
|
304
|
+
|
|
305
|
+
pkg_a = PackageInfo(
|
|
306
|
+
name="alpha",
|
|
307
|
+
version="1.0.0",
|
|
308
|
+
modules=[ModuleInfo(name="alpha.core", functions=[FunctionInfo(name="f")])],
|
|
309
|
+
)
|
|
310
|
+
pkg_b = PackageInfo(
|
|
311
|
+
name="beta",
|
|
312
|
+
version="2.0.0",
|
|
313
|
+
modules=[ModuleInfo(name="beta.core", functions=[FunctionInfo(name="g")])],
|
|
314
|
+
)
|
|
315
|
+
save(pkg_a, src_dir)
|
|
316
|
+
save(pkg_b, src_dir)
|
|
317
|
+
|
|
318
|
+
count = clear_package("alpha")
|
|
319
|
+
assert count == 1
|
|
320
|
+
|
|
321
|
+
remaining = list_entries()
|
|
322
|
+
assert len(remaining) == 1
|
|
323
|
+
assert remaining[0].package == "beta"
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def test_clear_package_normalises_name(tmp_path, monkeypatch):
|
|
327
|
+
"""Hyphens and case differences are normalised for matching."""
|
|
328
|
+
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
|
|
329
|
+
monkeypatch.setattr("libcontext.cache.sys.platform", "linux")
|
|
330
|
+
|
|
331
|
+
src_dir = tmp_path / "src"
|
|
332
|
+
src_dir.mkdir()
|
|
333
|
+
(src_dir / "mod.py").write_text("# code")
|
|
334
|
+
|
|
335
|
+
pkg = PackageInfo(
|
|
336
|
+
name="my_package",
|
|
337
|
+
version="1.0.0",
|
|
338
|
+
modules=[ModuleInfo(name="my_package.core", functions=[])],
|
|
339
|
+
)
|
|
340
|
+
save(pkg, src_dir)
|
|
341
|
+
|
|
342
|
+
count = clear_package("My-Package")
|
|
343
|
+
assert count == 1
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def test_clear_package_no_match(tmp_path, monkeypatch):
|
|
347
|
+
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
|
|
348
|
+
monkeypatch.setattr("libcontext.cache.sys.platform", "linux")
|
|
349
|
+
_get_cache_dir()
|
|
350
|
+
|
|
351
|
+
count = clear_package("nonexistent")
|
|
352
|
+
assert count == 0
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def test_clear_package_skips_corrupted(tmp_path, monkeypatch):
|
|
356
|
+
"""Corrupted JSON files are skipped without crashing."""
|
|
357
|
+
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
|
|
358
|
+
monkeypatch.setattr("libcontext.cache.sys.platform", "linux")
|
|
359
|
+
|
|
360
|
+
cache_dir = _get_cache_dir()
|
|
361
|
+
(cache_dir / "bad.json").write_text("not json", encoding="utf-8")
|
|
362
|
+
|
|
363
|
+
src_dir = tmp_path / "src"
|
|
364
|
+
src_dir.mkdir()
|
|
365
|
+
(src_dir / "mod.py").write_text("# code")
|
|
366
|
+
|
|
367
|
+
pkg = PackageInfo(
|
|
368
|
+
name="goodpkg",
|
|
369
|
+
version="1.0.0",
|
|
370
|
+
modules=[ModuleInfo(name="goodpkg.core", functions=[])],
|
|
371
|
+
)
|
|
372
|
+
save(pkg, src_dir)
|
|
373
|
+
|
|
374
|
+
count = clear_package("goodpkg")
|
|
375
|
+
assert count == 1
|
|
376
|
+
# Corrupted file still exists (not matched, not deleted)
|
|
377
|
+
assert (cache_dir / "bad.json").exists()
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
# ---------------------------------------------------------------------------
|
|
381
|
+
# list_entries
|
|
382
|
+
# ---------------------------------------------------------------------------
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def test_list_entries_returns_saved_packages(tmp_path, monkeypatch):
|
|
386
|
+
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
|
|
387
|
+
monkeypatch.setattr("libcontext.cache.sys.platform", "linux")
|
|
388
|
+
|
|
389
|
+
src_dir = tmp_path / "src"
|
|
390
|
+
src_dir.mkdir()
|
|
391
|
+
(src_dir / "mod.py").write_text("# code")
|
|
392
|
+
|
|
393
|
+
save(_make_pkg("1.0.0"), src_dir)
|
|
394
|
+
|
|
395
|
+
entries = list_entries()
|
|
396
|
+
assert len(entries) == 1
|
|
397
|
+
assert entries[0].package == "testpkg"
|
|
398
|
+
assert entries[0].version == "1.0.0"
|
|
399
|
+
assert entries[0].size_bytes > 0
|
|
400
|
+
assert entries[0].cached_at != "unknown"
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def test_list_entries_sorted_by_name(tmp_path, monkeypatch):
|
|
404
|
+
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
|
|
405
|
+
monkeypatch.setattr("libcontext.cache.sys.platform", "linux")
|
|
406
|
+
|
|
407
|
+
src_dir = tmp_path / "src"
|
|
408
|
+
src_dir.mkdir()
|
|
409
|
+
(src_dir / "mod.py").write_text("# code")
|
|
410
|
+
|
|
411
|
+
for name in ("zeta", "alpha", "mid"):
|
|
412
|
+
pkg = PackageInfo(
|
|
413
|
+
name=name,
|
|
414
|
+
version="1.0.0",
|
|
415
|
+
modules=[ModuleInfo(name=f"{name}.core", functions=[])],
|
|
416
|
+
)
|
|
417
|
+
save(pkg, src_dir)
|
|
418
|
+
|
|
419
|
+
entries = list_entries()
|
|
420
|
+
names = [e.package for e in entries]
|
|
421
|
+
assert names == ["alpha", "mid", "zeta"]
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def test_list_entries_empty(tmp_path, monkeypatch):
|
|
425
|
+
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
|
|
426
|
+
monkeypatch.setattr("libcontext.cache.sys.platform", "linux")
|
|
427
|
+
_get_cache_dir()
|
|
428
|
+
|
|
429
|
+
entries = list_entries()
|
|
430
|
+
assert entries == []
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def test_list_entries_skips_corrupted(tmp_path, monkeypatch):
|
|
434
|
+
"""Corrupted files are silently skipped."""
|
|
435
|
+
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
|
|
436
|
+
monkeypatch.setattr("libcontext.cache.sys.platform", "linux")
|
|
437
|
+
|
|
438
|
+
cache_dir = _get_cache_dir()
|
|
439
|
+
(cache_dir / "bad.json").write_text("not json", encoding="utf-8")
|
|
440
|
+
|
|
441
|
+
src_dir = tmp_path / "src"
|
|
442
|
+
src_dir.mkdir()
|
|
443
|
+
(src_dir / "mod.py").write_text("# code")
|
|
444
|
+
save(_make_pkg(), src_dir)
|
|
445
|
+
|
|
446
|
+
entries = list_entries()
|
|
447
|
+
assert len(entries) == 1
|
|
448
|
+
assert entries[0].package == "testpkg"
|
|
449
|
+
|
|
450
|
+
|
|
289
451
|
# ---------------------------------------------------------------------------
|
|
290
452
|
# _cache_filename
|
|
291
453
|
# ---------------------------------------------------------------------------
|
|
@@ -860,6 +860,152 @@ def test_cache_clear(tmp_path: Path, monkeypatch) -> None:
|
|
|
860
860
|
assert "Cleared" in result.output
|
|
861
861
|
|
|
862
862
|
|
|
863
|
+
def test_cache_clear_specific_package(tmp_path: Path, monkeypatch) -> None:
|
|
864
|
+
"""``libctx cache clear <package>`` removes only that package."""
|
|
865
|
+
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
|
|
866
|
+
monkeypatch.setattr("libcontext.cache.sys.platform", "linux")
|
|
867
|
+
|
|
868
|
+
from libcontext import cache as _c
|
|
869
|
+
from libcontext.models import FunctionInfo, ModuleInfo, PackageInfo
|
|
870
|
+
|
|
871
|
+
src = tmp_path / "src"
|
|
872
|
+
src.mkdir()
|
|
873
|
+
(src / "mod.py").write_text("# code")
|
|
874
|
+
|
|
875
|
+
for name in ("alpha", "beta"):
|
|
876
|
+
pkg = PackageInfo(
|
|
877
|
+
name=name,
|
|
878
|
+
version="1.0.0",
|
|
879
|
+
modules=[
|
|
880
|
+
ModuleInfo(name=f"{name}.core", functions=[FunctionInfo(name="f")])
|
|
881
|
+
],
|
|
882
|
+
)
|
|
883
|
+
_c.save(pkg, src)
|
|
884
|
+
|
|
885
|
+
runner = CliRunner()
|
|
886
|
+
result = runner.invoke(main, ["cache", "clear", "alpha"])
|
|
887
|
+
|
|
888
|
+
assert result.exit_code == 0
|
|
889
|
+
assert "1" in result.output
|
|
890
|
+
assert "alpha" in result.output
|
|
891
|
+
|
|
892
|
+
remaining = _c.list_entries()
|
|
893
|
+
assert len(remaining) == 1
|
|
894
|
+
assert remaining[0].package == "beta"
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
def test_cache_list_empty(tmp_path: Path, monkeypatch) -> None:
|
|
898
|
+
"""``libctx cache list`` on empty cache shows message."""
|
|
899
|
+
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
|
|
900
|
+
monkeypatch.setattr("libcontext.cache.sys.platform", "linux")
|
|
901
|
+
|
|
902
|
+
runner = CliRunner()
|
|
903
|
+
result = runner.invoke(main, ["cache", "list"])
|
|
904
|
+
|
|
905
|
+
assert result.exit_code == 0
|
|
906
|
+
assert "empty" in result.output.lower()
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def test_cache_list_shows_entries(tmp_path: Path, monkeypatch) -> None:
|
|
910
|
+
"""``libctx cache list`` displays cached packages."""
|
|
911
|
+
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
|
|
912
|
+
monkeypatch.setattr("libcontext.cache.sys.platform", "linux")
|
|
913
|
+
|
|
914
|
+
from libcontext import cache as _c
|
|
915
|
+
from libcontext.models import FunctionInfo, ModuleInfo, PackageInfo
|
|
916
|
+
|
|
917
|
+
src = tmp_path / "src"
|
|
918
|
+
src.mkdir()
|
|
919
|
+
(src / "mod.py").write_text("# code")
|
|
920
|
+
|
|
921
|
+
pkg = PackageInfo(
|
|
922
|
+
name="mypkg",
|
|
923
|
+
version="3.2.1",
|
|
924
|
+
modules=[ModuleInfo(name="mypkg.core", functions=[FunctionInfo(name="f")])],
|
|
925
|
+
)
|
|
926
|
+
_c.save(pkg, src)
|
|
927
|
+
|
|
928
|
+
runner = CliRunner()
|
|
929
|
+
result = runner.invoke(main, ["cache", "list"])
|
|
930
|
+
|
|
931
|
+
assert result.exit_code == 0
|
|
932
|
+
assert "mypkg" in result.output
|
|
933
|
+
assert "3.2.1" in result.output
|
|
934
|
+
assert "1 entries" in result.output
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
# ---------------------------------------------------------------------------
|
|
938
|
+
# _format_size / _format_age helpers
|
|
939
|
+
# ---------------------------------------------------------------------------
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
def test_format_size_bytes() -> None:
|
|
943
|
+
from libcontext.cli import _format_size
|
|
944
|
+
|
|
945
|
+
assert _format_size(500) == "500 B"
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
def test_format_size_kilobytes() -> None:
|
|
949
|
+
from libcontext.cli import _format_size
|
|
950
|
+
|
|
951
|
+
assert _format_size(2048) == "2.0 kB"
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def test_format_size_megabytes() -> None:
|
|
955
|
+
from libcontext.cli import _format_size
|
|
956
|
+
|
|
957
|
+
assert _format_size(5 * 1024 * 1024) == "5.0 MB"
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
def test_format_age_just_now() -> None:
|
|
961
|
+
import datetime
|
|
962
|
+
|
|
963
|
+
from libcontext.cli import _format_age
|
|
964
|
+
|
|
965
|
+
now = datetime.datetime.now(tz=datetime.timezone.utc).isoformat()
|
|
966
|
+
assert _format_age(now) == "just now"
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
def test_format_age_minutes() -> None:
|
|
970
|
+
import datetime
|
|
971
|
+
|
|
972
|
+
from libcontext.cli import _format_age
|
|
973
|
+
|
|
974
|
+
ts = (
|
|
975
|
+
datetime.datetime.now(tz=datetime.timezone.utc) - datetime.timedelta(minutes=15)
|
|
976
|
+
).isoformat()
|
|
977
|
+
assert _format_age(ts) == "15m ago"
|
|
978
|
+
|
|
979
|
+
|
|
980
|
+
def test_format_age_hours() -> None:
|
|
981
|
+
import datetime
|
|
982
|
+
|
|
983
|
+
from libcontext.cli import _format_age
|
|
984
|
+
|
|
985
|
+
ts = (
|
|
986
|
+
datetime.datetime.now(tz=datetime.timezone.utc) - datetime.timedelta(hours=3)
|
|
987
|
+
).isoformat()
|
|
988
|
+
assert _format_age(ts) == "3h ago"
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
def test_format_age_days() -> None:
|
|
992
|
+
import datetime
|
|
993
|
+
|
|
994
|
+
from libcontext.cli import _format_age
|
|
995
|
+
|
|
996
|
+
ts = (
|
|
997
|
+
datetime.datetime.now(tz=datetime.timezone.utc) - datetime.timedelta(days=7)
|
|
998
|
+
).isoformat()
|
|
999
|
+
assert _format_age(ts) == "7d ago"
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
def test_format_age_invalid() -> None:
|
|
1003
|
+
from libcontext.cli import _format_age
|
|
1004
|
+
|
|
1005
|
+
assert _format_age("not-a-timestamp") == "unknown age"
|
|
1006
|
+
assert _format_age("") == "unknown age"
|
|
1007
|
+
|
|
1008
|
+
|
|
863
1009
|
# ---------------------------------------------------------------------------
|
|
864
1010
|
# diff subcommand
|
|
865
1011
|
# ---------------------------------------------------------------------------
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{libcontext-0.4.0 → libcontext-0.6.0}/docs/adr/001-progressive-disclosure-over-always-on-context.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|