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.
Files changed (45) hide show
  1. {libcontext-0.4.0 → libcontext-0.6.0}/CHANGELOG.md +10 -1
  2. {libcontext-0.4.0 → libcontext-0.6.0}/PKG-INFO +24 -14
  3. {libcontext-0.4.0 → libcontext-0.6.0}/README.md +23 -13
  4. {libcontext-0.4.0 → libcontext-0.6.0}/pyproject.toml +1 -1
  5. {libcontext-0.4.0 → libcontext-0.6.0}/src/libcontext/cache.py +68 -0
  6. {libcontext-0.4.0 → libcontext-0.6.0}/src/libcontext/cli.py +76 -5
  7. {libcontext-0.4.0 → libcontext-0.6.0}/tests/test_cache.py +162 -0
  8. {libcontext-0.4.0 → libcontext-0.6.0}/tests/test_cli.py +146 -0
  9. {libcontext-0.4.0 → libcontext-0.6.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  10. {libcontext-0.4.0 → libcontext-0.6.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  11. {libcontext-0.4.0 → libcontext-0.6.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  12. {libcontext-0.4.0 → libcontext-0.6.0}/.github/workflows/ci.yml +0 -0
  13. {libcontext-0.4.0 → libcontext-0.6.0}/.github/workflows/release.yml +0 -0
  14. {libcontext-0.4.0 → libcontext-0.6.0}/.gitignore +0 -0
  15. {libcontext-0.4.0 → libcontext-0.6.0}/CONTRIBUTING.md +0 -0
  16. {libcontext-0.4.0 → libcontext-0.6.0}/DEPENDENCIES.md +0 -0
  17. {libcontext-0.4.0 → libcontext-0.6.0}/LICENSE +0 -0
  18. {libcontext-0.4.0 → libcontext-0.6.0}/SECURITY.md +0 -0
  19. {libcontext-0.4.0 → libcontext-0.6.0}/docs/adr/001-progressive-disclosure-over-always-on-context.md +0 -0
  20. {libcontext-0.4.0 → libcontext-0.6.0}/docs/adr/002-skill-plus-cli-as-primary-integration.md +0 -0
  21. {libcontext-0.4.0 → libcontext-0.6.0}/docs/adr/004-ast-only-inspection.md +0 -0
  22. {libcontext-0.4.0 → libcontext-0.6.0}/docs/adr/README.md +0 -0
  23. {libcontext-0.4.0 → libcontext-0.6.0}/src/libcontext/__init__.py +0 -0
  24. {libcontext-0.4.0 → libcontext-0.6.0}/src/libcontext/_envsetup.py +0 -0
  25. {libcontext-0.4.0 → libcontext-0.6.0}/src/libcontext/_security.py +0 -0
  26. {libcontext-0.4.0 → libcontext-0.6.0}/src/libcontext/collector.py +0 -0
  27. {libcontext-0.4.0 → libcontext-0.6.0}/src/libcontext/config.py +0 -0
  28. {libcontext-0.4.0 → libcontext-0.6.0}/src/libcontext/diff.py +0 -0
  29. {libcontext-0.4.0 → libcontext-0.6.0}/src/libcontext/exceptions.py +0 -0
  30. {libcontext-0.4.0 → libcontext-0.6.0}/src/libcontext/inspector.py +0 -0
  31. {libcontext-0.4.0 → libcontext-0.6.0}/src/libcontext/mcp_server.py +0 -0
  32. {libcontext-0.4.0 → libcontext-0.6.0}/src/libcontext/models.py +0 -0
  33. {libcontext-0.4.0 → libcontext-0.6.0}/src/libcontext/py.typed +0 -0
  34. {libcontext-0.4.0 → libcontext-0.6.0}/src/libcontext/renderer.py +0 -0
  35. {libcontext-0.4.0 → libcontext-0.6.0}/tests/__init__.py +0 -0
  36. {libcontext-0.4.0 → libcontext-0.6.0}/tests/test_cli_mcp_parity.py +0 -0
  37. {libcontext-0.4.0 → libcontext-0.6.0}/tests/test_collector.py +0 -0
  38. {libcontext-0.4.0 → libcontext-0.6.0}/tests/test_config.py +0 -0
  39. {libcontext-0.4.0 → libcontext-0.6.0}/tests/test_diff.py +0 -0
  40. {libcontext-0.4.0 → libcontext-0.6.0}/tests/test_envsetup.py +0 -0
  41. {libcontext-0.4.0 → libcontext-0.6.0}/tests/test_inspector.py +0 -0
  42. {libcontext-0.4.0 → libcontext-0.6.0}/tests/test_mcp_server.py +0 -0
  43. {libcontext-0.4.0 → libcontext-0.6.0}/tests/test_models.py +0 -0
  44. {libcontext-0.4.0 → libcontext-0.6.0}/tests/test_renderer.py +0 -0
  45. {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.4.0...HEAD
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.4.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
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
38
38
  [![Typed](https://img.shields.io/badge/typed-mypy-blue.svg)](https://mypy-lang.org/)
39
39
 
40
- > Make your AI coding assistant aware of any Python library's API — on demand, not always-on.
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 Copilot (via an MCP server) to provide **progressive disclosure** — only loading API context when you actually need it, avoiding context window pollution.
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
- When you ask an AI assistant how to use a library, the quality of the output depends entirely on what the model knows about that library's API. For many real-world scenarios, the model is working blind:
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
- Dumping entire API references into always-on instruction files (like `copilot-instructions.md` or `CLAUDE.md`) wastes context window on every interaction even when you're not using that library. Research ([ReadMe.LLM, UC Berkeley 2025](https://arxiv.org/abs/2504.15870)) confirms that excessive context triggers hallucinations and degrades output quality.
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
- libcontext solves this with **progressive disclosure**: overview first, then drill into specific modules only when needed.
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 exists for proprietary code |
61
- | **Niche open-source packages** | High | Sparse training data leads to hallucinated methods |
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 / Cursor) automate this workflow — the AI assistant decides what to inspect based on the task at hand.
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
- # Clear all cached API data
142
- libctx cache clear
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
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
9
9
  [![Typed](https://img.shields.io/badge/typed-mypy-blue.svg)](https://mypy-lang.org/)
10
10
 
11
- > Make your AI coding assistant aware of any Python library's API — on demand, not always-on.
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 Copilot (via an MCP server) to provide **progressive disclosure** — only loading API context when you actually need it, avoiding context window pollution.
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
- When you ask an AI assistant how to use a library, the quality of the output depends entirely on what the model knows about that library's API. For many real-world scenarios, the model is working blind:
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
- Dumping entire API references into always-on instruction files (like `copilot-instructions.md` or `CLAUDE.md`) wastes context window on every interaction even when you're not using that library. Research ([ReadMe.LLM, UC Berkeley 2025](https://arxiv.org/abs/2504.15870)) confirms that excessive context triggers hallucinations and degrades output quality.
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
- libcontext solves this with **progressive disclosure**: overview first, then drill into specific modules only when needed.
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 exists for proprietary code |
32
- | **Niche open-source packages** | High | Sparse training data leads to hallucinated methods |
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 / Cursor) automate this workflow — the AI assistant decides what to inspect based on the task at hand.
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
- # Clear all cached API data
113
- libctx cache clear
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
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "libcontext"
7
- version = "0.4.0"
7
+ version = "0.6.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"
@@ -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 two subcommands:
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
- def clear() -> None:
762
- """Remove all cached API snapshots."""
763
- count = _cache.clear_all()
764
- click.echo(f"Cleared {count} cache entries.")
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