claude-code-capabilities 0.1.2__py3-none-any.whl

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 (47) hide show
  1. capdisc/__init__.py +69 -0
  2. capdisc/base.py +56 -0
  3. capdisc/catalog/__init__.py +71 -0
  4. capdisc/catalog/entries.py +104 -0
  5. capdisc/catalog/types.py +227 -0
  6. capdisc/discovery.py +278 -0
  7. capdisc/frontmatter.py +49 -0
  8. capdisc/hooks/__init__.py +61 -0
  9. capdisc/hooks/schema.py +216 -0
  10. capdisc/hooks/types.py +156 -0
  11. capdisc/html.py +75 -0
  12. capdisc/mcp_catalog.py +87 -0
  13. capdisc/mcp_harvest/__init__.py +17 -0
  14. capdisc/mcp_harvest/auth.py +120 -0
  15. capdisc/mcp_harvest/cache.py +144 -0
  16. capdisc/mcp_harvest/config.py +325 -0
  17. capdisc/mcp_harvest/connect.py +187 -0
  18. capdisc/mcp_harvest/types.py +29 -0
  19. capdisc/plugin_catalog.py +308 -0
  20. capdisc/py.typed +0 -0
  21. capdisc/report/__init__.py +36 -0
  22. capdisc/report/__main__.py +6 -0
  23. capdisc/report/assets.py +138 -0
  24. capdisc/report/cards.py +109 -0
  25. capdisc/report/cli.py +35 -0
  26. capdisc/report/components.py +144 -0
  27. capdisc/report/harvest.py +150 -0
  28. capdisc/report/html.py +79 -0
  29. capdisc/report/models.py +82 -0
  30. capdisc/report/page.py +129 -0
  31. capdisc/report/sections.py +154 -0
  32. capdisc/report/types.py +43 -0
  33. capdisc/scope/__init__.py +83 -0
  34. capdisc/scope/inventory/__init__.py +20 -0
  35. capdisc/scope/inventory/assets.py +54 -0
  36. capdisc/scope/inventory/capture.py +219 -0
  37. capdisc/scope/inventory/render.py +149 -0
  38. capdisc/scope/inventory/roots.py +175 -0
  39. capdisc/scope/locations.py +330 -0
  40. capdisc/scope/types.py +154 -0
  41. capdisc/settings.py +230 -0
  42. capdisc/tokens.py +48 -0
  43. claude_code_capabilities-0.1.2.dist-info/METADATA +136 -0
  44. claude_code_capabilities-0.1.2.dist-info/RECORD +47 -0
  45. claude_code_capabilities-0.1.2.dist-info/WHEEL +4 -0
  46. claude_code_capabilities-0.1.2.dist-info/entry_points.txt +2 -0
  47. claude_code_capabilities-0.1.2.dist-info/licenses/LICENSE +21 -0
capdisc/scope/types.py ADDED
@@ -0,0 +1,154 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import StrEnum
4
+ from pathlib import Path
5
+ from typing import Annotated
6
+
7
+ from pydantic import Field
8
+
9
+ ARTIFACT_CONTENT_MAX_CHARS = 200_000 # length cap of ArtifactContents; _safe_read enforces it
10
+
11
+
12
+ class ScopeKind(StrEnum):
13
+ """Who sees a definition. Not every artifact supports every scope — session is subagents
14
+ via --agents, component is a hook living in a skill/agent's frontmatter."""
15
+
16
+ managed = "managed"
17
+ session = "session"
18
+ project = "project"
19
+ user = "user"
20
+ plugin = "plugin"
21
+ component = "component"
22
+
23
+
24
+ class Shareability(StrEnum):
25
+ """How a definition is (or is not) shared — the mechanism behind the docs' Shareable column."""
26
+
27
+ committed = "committed" # in-repo, e.g. .claude/settings.json, .claude/agents/
28
+ gitignored = "gitignored" # .claude/settings.local.json
29
+ machine_local = "machine_local" # ~/.claude/*, this machine only
30
+ admin = "admin" # managed policy, admin-controlled
31
+ bundled = "bundled" # shipped inside a plugin
32
+ in_component = "in_component" # embedded in a host component's frontmatter
33
+ ephemeral = "ephemeral" # in-memory only, e.g. --agents JSON
34
+
35
+
36
+ class ArtifactKind(StrEnum):
37
+ """A kind of scoped artifact Claude Code loads. Storage differs by kind: agents/skills/
38
+ commands are standalone files, hooks are settings entries or frontmatter."""
39
+
40
+ agent = "agent"
41
+ skill = "skill"
42
+ command = "command"
43
+ hook = "hook"
44
+
45
+
46
+ class ResolutionMode(StrEnum):
47
+ """How same-named definitions across scopes combine. Collision: the highest-precedence one
48
+ wins (agents, skills, commands). Additive: every matching one fires (hooks)."""
49
+
50
+ collision = "collision"
51
+ additive = "additive"
52
+
53
+
54
+ ScopeRoot = Annotated[
55
+ str,
56
+ Field(
57
+ min_length=1,
58
+ max_length=120,
59
+ title="Scope root",
60
+ description="Filesystem root a scope's artifacts live under.",
61
+ examples=[".claude", "~/.claude"],
62
+ ),
63
+ ]
64
+ ArtifactSubdir = Annotated[
65
+ str,
66
+ Field(
67
+ pattern=r"^[a-z]+$",
68
+ title="Artifact subdir",
69
+ description="Subdirectory under a scope root that holds one artifact kind.",
70
+ examples=["agents", "skills"],
71
+ ),
72
+ ]
73
+ GlobPattern = Annotated[
74
+ str,
75
+ Field(
76
+ min_length=1,
77
+ max_length=64,
78
+ title="Glob pattern",
79
+ description="Glob, relative to an artifact's subdir, that matches its entry files.",
80
+ examples=["**/*.md", "**/SKILL.md"],
81
+ ),
82
+ ]
83
+ SettingsFile = Annotated[
84
+ str,
85
+ Field(
86
+ min_length=1,
87
+ max_length=64,
88
+ title="Settings file",
89
+ description="Name of the JSON file a settings-entry artifact lives inside.",
90
+ examples=["settings.json", "settings.local.json", "hooks/hooks.json"],
91
+ ),
92
+ ]
93
+ JsonKey = Annotated[
94
+ str,
95
+ Field(
96
+ pattern=r"^[a-zA-Z][a-zA-Z0-9_]*$",
97
+ title="JSON key",
98
+ description="Top-level key, inside a settings file or frontmatter, holding the entries.",
99
+ examples=["hooks", "mcpServers"],
100
+ ),
101
+ ]
102
+ CliFlag = Annotated[
103
+ str,
104
+ Field(
105
+ pattern=r"^--[a-z][a-z-]*$",
106
+ title="CLI flag",
107
+ description="Launch flag that supplies an in-memory definition.",
108
+ examples=["--agents"],
109
+ ),
110
+ ]
111
+ ArtifactName = Annotated[
112
+ str,
113
+ Field(
114
+ min_length=1,
115
+ max_length=128,
116
+ title="Artifact name",
117
+ description="Path-derived label for a captured artifact; its true identity is the "
118
+ "name frontmatter inside contents.",
119
+ examples=["code-reviewer"],
120
+ ),
121
+ ]
122
+ ArtifactPath = Annotated[
123
+ Path,
124
+ Field(
125
+ title="Artifact path",
126
+ description="Exact filesystem path of a captured artifact (None when supplied in-memory).",
127
+ examples=["~/.claude/agents/code-reviewer.md"],
128
+ ),
129
+ ]
130
+ ArtifactContents = Annotated[
131
+ str,
132
+ Field(
133
+ max_length=ARTIFACT_CONTENT_MAX_CHARS,
134
+ title="Artifact contents",
135
+ description="Raw text of a captured artifact file.",
136
+ ),
137
+ ]
138
+ ResolutionRank = Annotated[
139
+ int,
140
+ Field(
141
+ ge=0,
142
+ title="Resolution rank",
143
+ description="Precedence position when names collide — 0 is highest, larger loses. "
144
+ "Encodes both cross-scope order and nearest-first depth within project scope.",
145
+ ),
146
+ ]
147
+ ScanBasePath = Annotated[
148
+ Path,
149
+ Field(
150
+ title="Scan base path",
151
+ description="Concrete filesystem directory scanned for a scope's artifacts.",
152
+ examples=["~/.claude", "/repo/.claude"],
153
+ ),
154
+ ]
capdisc/settings.py ADDED
@@ -0,0 +1,230 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import cache
4
+ from pathlib import Path
5
+ from typing import Annotated
6
+
7
+ from pydantic import Field
8
+ from pydantic_settings import (
9
+ BaseSettings,
10
+ JsonConfigSettingsSource,
11
+ PydanticBaseSettingsSource,
12
+ SettingsConfigDict,
13
+ YamlConfigSettingsSource,
14
+ )
15
+
16
+ from .base import InputModel
17
+ from .hooks import EnvVarName
18
+
19
+ ExtraSourceDir = Annotated[
20
+ Path,
21
+ Field(
22
+ title="Extra source directory",
23
+ description=(
24
+ "A filesystem directory contributed to discovery as a cataloged source. "
25
+ "These dirs are cataloged as discovery data only — never honored as active "
26
+ "hooks or MCP servers; that boundary is enforced upstream."
27
+ ),
28
+ examples=["/home/user/my-org-skills", "/opt/shared-agents"],
29
+ ),
30
+ ]
31
+ ExactHostname = Annotated[
32
+ str,
33
+ Field(
34
+ title="Exact hostname",
35
+ description="Hostname a server's url must resolve to before a credential is attached.",
36
+ examples=["api.githubcopilot.com"],
37
+ ),
38
+ ]
39
+ OAuthClientId = Annotated[
40
+ str,
41
+ Field(
42
+ title="OAuth client id",
43
+ description="Pre-registered OAuth client id for an authorization server without dynamic "
44
+ "client registration.",
45
+ examples=["abc123"],
46
+ ),
47
+ ]
48
+ OAuthScope = Annotated[
49
+ str,
50
+ Field(
51
+ title="OAuth scope",
52
+ description="One OAuth scope requested for a pre-registered client.",
53
+ examples=["read:user"],
54
+ ),
55
+ ]
56
+ CallbackPort = Annotated[
57
+ int,
58
+ Field(
59
+ title="OAuth callback port",
60
+ description="Fixed localhost callback port for an OAuth app registered with an exact "
61
+ "redirect URL.",
62
+ ge=1,
63
+ le=65535,
64
+ examples=[8765],
65
+ ),
66
+ ]
67
+ PluginsRootPath = Annotated[
68
+ Path,
69
+ Field(title="Plugins root", description="Root holding installed_plugins.json."),
70
+ ]
71
+ ClaudeJsonPath = Annotated[
72
+ Path,
73
+ Field(
74
+ title="Claude JSON path",
75
+ description="User/project MCP server config (~/.claude.json).",
76
+ ),
77
+ ]
78
+ UserSettingsPath = Annotated[
79
+ Path,
80
+ Field(
81
+ title="User settings path",
82
+ description="User settings.json, read for plugin user_config options.",
83
+ ),
84
+ ]
85
+ McpCachePath = Annotated[
86
+ Path,
87
+ Field(title="MCP cache path", description="Tool-enriched MCP server cache."),
88
+ ]
89
+ ReportDirPath = Annotated[
90
+ Path,
91
+ Field(
92
+ title="Report directory",
93
+ description="Directory the discovery report (json + html) is written to.",
94
+ ),
95
+ ]
96
+ OAuthTokenDirPath = Annotated[
97
+ Path,
98
+ Field(
99
+ title="OAuth token directory",
100
+ description="OAuth token cache for `--oauth` harvests. Plaintext files, dir mode 0700.",
101
+ ),
102
+ ]
103
+
104
+ DEFAULT_CONFIG_PATH: Path = Path.home() / ".claude" / "capdisc" / "config.json"
105
+ DEFAULT_YAML_CONFIG_PATH: Path = Path.home() / ".claude" / "capdisc" / "config.yaml"
106
+
107
+
108
+ class McpBearerAuth(InputModel):
109
+ """A bearer credential for one HTTP MCP server, bound to the exact host it may be sent to.
110
+
111
+ Keyed by bare server name (matched case-insensitively), but the name alone never grants the
112
+ credential — a plugin cannot receive it by simply declaring a same-named server, since the
113
+ server's own `url` must resolve to `host` before the token is attached."""
114
+
115
+ env: EnvVarName
116
+ host: ExactHostname
117
+
118
+
119
+ class McpOAuthClient(InputModel):
120
+ """A pre-registered OAuth client for one HTTP MCP server, used by the opt-in `--oauth`
121
+ harvest. Required for authorization servers without dynamic client registration (GitHub).
122
+ The secret itself is never stored here — only the name of the env var that holds it.
123
+
124
+ Bound to `host` for the same reason as `McpBearerAuth`: the bare-name key alone must never
125
+ be enough for a same-named server to receive the client credentials."""
126
+
127
+ client_id: OAuthClientId
128
+ host: ExactHostname
129
+ scopes: list[OAuthScope] = Field(default_factory=list)
130
+ secret_env: EnvVarName | None = Field(
131
+ default=None,
132
+ description="Name of the env var holding the client secret; omit for PKCE-only clients.",
133
+ )
134
+ callback_port: CallbackPort | None = Field(
135
+ default=None,
136
+ description="Omit to use a random free port.",
137
+ )
138
+
139
+
140
+ class DiscoverySettings(BaseSettings):
141
+ """Paths and source dirs the discovery layer reads from and writes to.
142
+
143
+ Precedence (highest to lowest): init args > env vars > .env file > JSON config file >
144
+ YAML config file > defaults. Env vars use the ``CAPDISC_`` prefix; the
145
+ .env, JSON, and YAML files are all read from ``~/.claude/capdisc/`` (any
146
+ or all may be absent = empty, never an error) — never from the process's current working
147
+ directory, so running this tool from inside an untrusted checkout can't override
148
+ ``claude_json``/``plugins_root`` via a repo-committed ``.env`` and defeat the harvest's own
149
+ exclusion of untrusted project config (see ``scope_server_configs``). For a scalar or list
150
+ field set in both JSON and YAML, the JSON value wins outright. For a dict-typed field
151
+ (``mcp_bearer_env``, ``mcp_oauth_clients``), the two files are deep-merged instead — keys
152
+ present in both compose recursively, and only an actual leaf conflict picks JSON's value.
153
+ Subclasses may override ``model_config`` to change the prefix or config file paths.
154
+
155
+ ``extra_scan_dirs`` and ``extra_plugin_dirs`` are cataloged as discovery data only —
156
+ never honored as active hooks or MCP servers; that boundary is enforced upstream.
157
+ """
158
+
159
+ model_config = SettingsConfigDict(
160
+ env_prefix="CAPDISC_",
161
+ env_file=DEFAULT_CONFIG_PATH.parent / ".env",
162
+ extra="ignore",
163
+ json_file=DEFAULT_CONFIG_PATH,
164
+ yaml_file=DEFAULT_YAML_CONFIG_PATH,
165
+ )
166
+
167
+ extra_scan_dirs: list[ExtraSourceDir] = Field(default_factory=list[ExtraSourceDir])
168
+ extra_plugin_dirs: list[ExtraSourceDir] = Field(default_factory=list[ExtraSourceDir])
169
+
170
+ # Locations discovery reads from / writes to; each independently overridable.
171
+ plugins_root: PluginsRootPath = Field(
172
+ default_factory=lambda: Path.home() / ".claude" / "plugins"
173
+ )
174
+ claude_json: ClaudeJsonPath = Field(default_factory=lambda: Path.home() / ".claude.json")
175
+ user_settings: UserSettingsPath = Field(
176
+ default_factory=lambda: Path.home() / ".claude" / "settings.json"
177
+ )
178
+ mcp_cache: McpCachePath = Field(
179
+ default_factory=lambda: (
180
+ Path.home() / ".claude" / "capdisc" / "mcp-tools.json"
181
+ )
182
+ )
183
+ report_dir: ReportDirPath = Field(
184
+ default_factory=lambda: Path.home() / ".claude" / "capdisc"
185
+ )
186
+
187
+ # Auth for HTTP MCP servers the anonymous probe can't reach. Keyed by bare server name
188
+ # (the last segment of the ref, e.g. "github" for plugin:github:github). The name is only
189
+ # a lookup key, not a trust boundary — `host` is what actually gates the credential.
190
+ mcp_bearer_env: dict[str, McpBearerAuth] = Field(
191
+ default_factory=dict,
192
+ description="Server name → {env, host} for a bearer token, e.g. "
193
+ '{"github": {"env": "GH_TOKEN", "host": "api.githubcopilot.com"}}. Non-interactive; '
194
+ "used whenever the var is set and the server's url matches host.",
195
+ )
196
+ mcp_oauth_clients: dict[str, McpOAuthClient] = Field(
197
+ default_factory=dict,
198
+ description="Server name → pre-registered OAuth client, used only under `--oauth` "
199
+ "(interactive browser flow; never in background paths).",
200
+ )
201
+ oauth_token_dir: OAuthTokenDirPath = Field(
202
+ default_factory=lambda: Path.home() / ".claude" / "capdisc" / "oauth-tokens"
203
+ )
204
+
205
+ @classmethod
206
+ def settings_customise_sources(
207
+ cls,
208
+ settings_cls: type[BaseSettings],
209
+ init_settings: PydanticBaseSettingsSource,
210
+ env_settings: PydanticBaseSettingsSource,
211
+ dotenv_settings: PydanticBaseSettingsSource,
212
+ file_secret_settings: PydanticBaseSettingsSource, # noqa: ARG003
213
+ ) -> tuple[PydanticBaseSettingsSource, ...]:
214
+ return (
215
+ init_settings,
216
+ env_settings,
217
+ dotenv_settings,
218
+ JsonConfigSettingsSource(settings_cls),
219
+ YamlConfigSettingsSource(settings_cls),
220
+ )
221
+
222
+
223
+ @cache
224
+ def get_settings() -> DiscoverySettings:
225
+ """The process-wide settings, resolved lazily on first use from env/.env/JSON/YAML config.
226
+
227
+ Lazy so importing the package never reads config files or raises on a malformed
228
+ environment, and so configuration applied before first use is honored. The default
229
+ source for discovery/report/mcp path defaults."""
230
+ return DiscoverySettings()
capdisc/tokens.py ADDED
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Annotated
4
+
5
+ from pydantic import Field
6
+ from pydantic.functional_validators import AfterValidator
7
+
8
+ # Defined here, not imported from catalog/types.py: catalog/types.py itself imports
9
+ # token_bounds from this module, so importing back from it would be circular.
10
+ TokenCount = Annotated[
11
+ int,
12
+ Field(ge=0, title="Token count", description="Estimated token count of a text."),
13
+ ]
14
+
15
+
16
+ def estimate_tokens(text: str) -> TokenCount:
17
+ """Estimate a string's token count, cheaply and deterministically.
18
+
19
+ ~4 chars/token with a word-count floor. A guard rail, not a billing figure — swap in
20
+ tiktoken for a tighter number.
21
+
22
+ Args:
23
+ text: The text to estimate.
24
+
25
+ Returns:
26
+ The estimated token count.
27
+ """
28
+ return max(len(text) // 4, len(text.split()))
29
+
30
+
31
+ def token_bounds(max_tokens: TokenCount) -> AfterValidator:
32
+ """Build an `AfterValidator` that rejects strings over a token estimate.
33
+
34
+ Args:
35
+ max_tokens: The inclusive upper bound on `estimate_tokens(value)`.
36
+
37
+ Returns:
38
+ A validator that returns the value unchanged, or raises `ValueError` when the estimate
39
+ exceeds `max_tokens`.
40
+ """
41
+
42
+ def _check(value: str) -> str:
43
+ used = estimate_tokens(value)
44
+ if used > max_tokens:
45
+ raise ValueError(f"~{used} tokens exceeds {max_tokens}-token maximum")
46
+ return value
47
+
48
+ return AfterValidator(_check)
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: claude-code-capabilities
3
+ Version: 0.1.2
4
+ Summary: Discovers Claude Code capabilities — skills, agents, plugins, MCP servers, hooks — into typed catalogs and an environment report.
5
+ Project-URL: Homepage, https://github.com/Magic-Man-us/capability-discovery
6
+ Project-URL: Repository, https://github.com/Magic-Man-us/capability-discovery
7
+ Author: Magic-Man-us
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: MacOS
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.12
20
+ Requires-Dist: fastmcp>=3.4.4
21
+ Requires-Dist: py-key-value-aio[filetree]>=0.4.5
22
+ Requires-Dist: pydantic-settings>=2.14.2
23
+ Requires-Dist: pydantic>=2.13.4
24
+ Requires-Dist: pyyaml>=6.0.3
25
+ Description-Content-Type: text/markdown
26
+
27
+ # capdisc
28
+
29
+ [![PyPI](https://img.shields.io/pypi/v/claude-code-capabilities.svg)](https://pypi.org/project/claude-code-capabilities/)
30
+ [![Python versions](https://img.shields.io/pypi/pyversions/claude-code-capabilities.svg)](https://pypi.org/project/claude-code-capabilities/)
31
+ [![CI](https://github.com/Magic-Man-us/capability-discovery/actions/workflows/ci.yml/badge.svg)](https://github.com/Magic-Man-us/capability-discovery/actions/workflows/ci.yml)
32
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/Magic-Man-us/capability-discovery/blob/main/LICENSE)
33
+
34
+ Discovers Claude Code capabilities — skills, agents, plugins, MCP servers, hooks — into typed
35
+ Pydantic catalogs and an environment report.
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ uv add claude-code-capabilities
41
+ # or
42
+ pip install claude-code-capabilities
43
+ ```
44
+
45
+ ## CLI
46
+
47
+ ```bash
48
+ capdisc # scan this machine, write discovery-report.json + .html
49
+ capdisc --oauth # also allow the interactive OAuth flow for HTTP MCP servers
50
+ # with a pre-registered client (forces a fresh MCP harvest)
51
+ ```
52
+
53
+ Both files are written to `~/.claude/capdisc/` by default. Configure paths and MCP auth via env
54
+ vars (`CAPDISC_` prefix), a `.env` file, or `~/.claude/capdisc/config.json`/`config.yaml` — see
55
+ `DiscoverySettings` in `settings.py` for every field.
56
+
57
+ ## Library
58
+
59
+ Scan the machine into a typed catalog:
60
+
61
+ ```python
62
+ from pathlib import Path
63
+ from capdisc.discovery import scan_environment
64
+ from capdisc.scope import ScopeRoots
65
+
66
+ roots = ScopeRoots.discover(start=Path.cwd(), home_dir=Path.home())
67
+ catalog = scan_environment(roots)
68
+ for entry in catalog.entries:
69
+ ... # CatalogSkill | CatalogTool | CatalogMcpServer | CatalogPlugin
70
+ ```
71
+
72
+ Build and persist the full environment report (what the CLI does):
73
+
74
+ ```python
75
+ from capdisc.report import build_report, write_report
76
+
77
+ report = build_report(oauth=False)
78
+ write_report(report) # -> ~/.claude/capdisc/discovery-report.{json,html}
79
+ ```
80
+
81
+ Inspect the on-disk scope inventory directly — every skill/agent/command/hook found, its
82
+ precedence, and which ones actually win a collision:
83
+
84
+ ```python
85
+ from capdisc.scope import ScopeInventory, ScopeRoots
86
+
87
+ inventory = ScopeInventory.scan(ScopeRoots.discover(start=Path.cwd(), home_dir=Path.home()))
88
+ print(len(inventory.artifacts), "captured,", len(inventory.effective), "in effect")
89
+ for hooks in inventory.hook_configs:
90
+ ... # HookConfig, unified from settings.json and component frontmatter
91
+ ```
92
+
93
+ ### MCP servers
94
+
95
+ Read the last harvested tool inventory (fast, no network — served from a 12h cache):
96
+
97
+ ```python
98
+ from capdisc.mcp_harvest import read_mcp_cache, cache_is_stale
99
+
100
+ servers = read_mcp_cache() # [] if there's no cache yet
101
+ if cache_is_stale():
102
+ ... # trigger a refresh (below) before trusting this
103
+ ```
104
+
105
+ Force a fresh harvest — connects to every configured server concurrently (bounded) and lists
106
+ their real tool schemas:
107
+
108
+ ```python
109
+ from capdisc.mcp_harvest import refresh_mcp_cache
110
+
111
+ servers = refresh_mcp_cache(oauth=False) # also (re)writes the cache
112
+ for server in servers:
113
+ print(server.ref, [t.name for t in server.tools])
114
+ ```
115
+
116
+ Bearer/OAuth auth for HTTP servers is opt-in via settings, bound to an exact hostname so a
117
+ same-named server elsewhere can never receive a credential meant for another:
118
+
119
+ ```bash
120
+ export CAPDISC_MCP_BEARER_ENV='{"github": {"env": "GH_TOKEN", "host": "api.githubcopilot.com"}}'
121
+ ```
122
+
123
+ `report.EnvironmentReport` captures the full discovery harvest (scan roots, on-disk inventory, skills,
124
+ builtin tools, plugins with per-component token cost, MCP servers). `mcp_harvest` and `mcp_catalog`
125
+ enumerate connected MCP servers; `plugin_catalog` reads installed plugins; `scope` resolves which
126
+ roots to scan.
127
+
128
+ The package ships `py.typed`.
129
+
130
+ ## Tests
131
+
132
+ ```bash
133
+ uv run pytest
134
+ uv run mypy src
135
+ uv run ruff check .
136
+ ```
@@ -0,0 +1,47 @@
1
+ capdisc/__init__.py,sha256=xRifNB3tWg3Ax67e9TkMjb7SflAFnXY_ZkUkEKKpYnU,1804
2
+ capdisc/base.py,sha256=7124V1bHbVMkeCtnDQUOFFZ-u1Y7Un8EVUOOQYB6-mc,2193
3
+ capdisc/discovery.py,sha256=XbKelU4VjYgo4TFTXJZzglcRltVWdJiW2ObfIrg0HUw,10235
4
+ capdisc/frontmatter.py,sha256=XAwmLgqyHacO5iKYHHjIXilvJIJ5bB4PEc-J533hIiQ,1745
5
+ capdisc/html.py,sha256=qWRpXTcDS06dRJTtR71GAJ12Ms_ofr47neWW1HMMv3A,2990
6
+ capdisc/mcp_catalog.py,sha256=Rwv2ReEJfa6dncBLH29VlLdlR6CmY923s4AshySY9YU,3009
7
+ capdisc/plugin_catalog.py,sha256=k-0pmQyHMWSTY0fw53OCuJeLliovGvnbQoHJYZxWBQw,10306
8
+ capdisc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ capdisc/settings.py,sha256=La5Wb8_PaqnbN0MKDRWz6laivmlvGKuA3owPsd9Qg8U,8622
10
+ capdisc/tokens.py,sha256=3u_ib8hAItJc3WzzHy1EiK6EYtGEitjmC32v5wEkisE,1450
11
+ capdisc/catalog/__init__.py,sha256=rDTI88ikm1r3DYNTwruLFdKpf7HLuJOZWmv-8Rq1n-8,1365
12
+ capdisc/catalog/entries.py,sha256=apmhUEmCnrjt5QbvS2FgRG1N88sxcapoVNdkN2BYCS4,2966
13
+ capdisc/catalog/types.py,sha256=YKMOCtxB1aZYQg818Em2MgI1XtCBr-UVFDGiXsTm_Rc,7090
14
+ capdisc/hooks/__init__.py,sha256=BMlHREAhXS5mIVSBuQ5I9zdWHttuYNfMNf2VqzS-9As,1047
15
+ capdisc/hooks/schema.py,sha256=ZVi58WclKJCM99IR47Gc1YILa07B7FV8hhiVqOcOm2E,7161
16
+ capdisc/hooks/types.py,sha256=MnMhCNoseSq1eGix0IiyFlyuBqi145LG72anSxViOMs,4339
17
+ capdisc/mcp_harvest/__init__.py,sha256=XEkZoT2IhZvs5kMzteAEq332LSgE29RMB41vqDDkU38,306
18
+ capdisc/mcp_harvest/auth.py,sha256=IOke9rMfgN_zJSwcUMnq1lym6o5DA57fMK2eiW73aLc,4608
19
+ capdisc/mcp_harvest/cache.py,sha256=HHFY69iDXcVmnl0BM3olVstHWG4nULfxNpyR3jLLcdM,5590
20
+ capdisc/mcp_harvest/config.py,sha256=BeGmoakOXm4Ws25NaFZCOMYK7WM_1LnKRPD6xFbFXug,11892
21
+ capdisc/mcp_harvest/connect.py,sha256=Bjs0oh6D-nVsEFL1cHsoLeVItBTCFfh9gCO2qLtQj7M,7115
22
+ capdisc/mcp_harvest/types.py,sha256=QMklnjXH-b-9r0AgAX878bWEHAq3vin4zvdUNMlAw7A,884
23
+ capdisc/report/__init__.py,sha256=axlk_daMqLeKi3VdMj6wsoDTz8kcnmWXDK-3pMsIkXA,1415
24
+ capdisc/report/__main__.py,sha256=u80yf2rO_w6_MuyOfo2SES79u_F6tQUiwFwMexKpGFQ,97
25
+ capdisc/report/assets.py,sha256=P1-yIv3RbkizRSkiDxsSgaCwsaRTzMPwSHom-MQrG9Q,7531
26
+ capdisc/report/cards.py,sha256=NuGS-Fp-K4XvC4pbE31GcdxKNeKKlMF2Vog7OoVydnA,3944
27
+ capdisc/report/cli.py,sha256=uU19sFd_zQ6S_r8RSBOR_fJr0I7haY_lT3272S0mhV8,1167
28
+ capdisc/report/components.py,sha256=EUgAhfwUJhjFRYy9AXSTxfXHe5apt3tvaRRRq7zaPe0,5772
29
+ capdisc/report/harvest.py,sha256=HHuFnQvl18_IX12tXobsje1ko7oUUFRtOrE0Yz271lI,5990
30
+ capdisc/report/html.py,sha256=v3oJnRlm0aWMT0RdI7IOOYcL0VaPPQpe8Y0Ts8r8aDI,2077
31
+ capdisc/report/models.py,sha256=KW078sZH0KFDDmONj5b4TBnNKUVk4Q7AZVz7QYAvUDM,2562
32
+ capdisc/report/page.py,sha256=zy2RZ3aGpqBgwKCWPdg9SsmDNh9E-8ccExTXT5d6z30,4410
33
+ capdisc/report/sections.py,sha256=rFEe03NTSgn9FDg-p4Ke11NcvFcXvUPP5aSfdr-npZY,5632
34
+ capdisc/report/types.py,sha256=h8XWPhz3HQGPjARc4vI8MZidY-bwvXmOdmBG6RYCRm4,961
35
+ capdisc/scope/__init__.py,sha256=CCddcTSUmooThC9sax6GFFQE3YG5ZfwUigDjAHzWXOQ,1480
36
+ capdisc/scope/locations.py,sha256=UMqfadVjRkZQfh2Ib2S_EF993bf8RY4CilpyzfARd8I,11922
37
+ capdisc/scope/types.py,sha256=XdA6UwqgnsYUX1_VvR5o9GVmOAgqlF4RzmAnjyu6kPY,4523
38
+ capdisc/scope/inventory/__init__.py,sha256=CQ3j0N0pqh6a3ttlf_BGWf9YKHY3MHjUQmkcK9SSy88,609
39
+ capdisc/scope/inventory/assets.py,sha256=A9odfbo4UK7kJA9B-UgMeHkiBl5y_DS-GHvMAhTZ0eg,2781
40
+ capdisc/scope/inventory/capture.py,sha256=WVoYEmiWQO01LdKfH1cQ0mWHRmjuOb704wsf-YxLKQg,8654
41
+ capdisc/scope/inventory/render.py,sha256=bbDvt7LXkk3EJSP6MSauoVHW1kNIdNb5WrnavCXDE_Q,6304
42
+ capdisc/scope/inventory/roots.py,sha256=GYZhMeFqw7I5U1bRAWTUvyJyXlMdqwhu83q2r2DnNV4,7219
43
+ claude_code_capabilities-0.1.2.dist-info/METADATA,sha256=icXWOGkscpD8SeUyfSH4WYPo1fMdt7bBR1hwVTGB5kk,4912
44
+ claude_code_capabilities-0.1.2.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
45
+ claude_code_capabilities-0.1.2.dist-info/entry_points.txt,sha256=_Bpcif5HawFaBeHswPeJ_7Ep_F37KsTOaOV-VDcUdm0,48
46
+ claude_code_capabilities-0.1.2.dist-info/licenses/LICENSE,sha256=U8Kmm9RvqogXmfGNlmVcAHpi72693fiOfsJsnnA4J1s,1069
47
+ claude_code_capabilities-0.1.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ capdisc = capdisc.report:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Magic-Man-us
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.