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.
- capdisc/__init__.py +69 -0
- capdisc/base.py +56 -0
- capdisc/catalog/__init__.py +71 -0
- capdisc/catalog/entries.py +104 -0
- capdisc/catalog/types.py +227 -0
- capdisc/discovery.py +278 -0
- capdisc/frontmatter.py +49 -0
- capdisc/hooks/__init__.py +61 -0
- capdisc/hooks/schema.py +216 -0
- capdisc/hooks/types.py +156 -0
- capdisc/html.py +75 -0
- capdisc/mcp_catalog.py +87 -0
- capdisc/mcp_harvest/__init__.py +17 -0
- capdisc/mcp_harvest/auth.py +120 -0
- capdisc/mcp_harvest/cache.py +144 -0
- capdisc/mcp_harvest/config.py +325 -0
- capdisc/mcp_harvest/connect.py +187 -0
- capdisc/mcp_harvest/types.py +29 -0
- capdisc/plugin_catalog.py +308 -0
- capdisc/py.typed +0 -0
- capdisc/report/__init__.py +36 -0
- capdisc/report/__main__.py +6 -0
- capdisc/report/assets.py +138 -0
- capdisc/report/cards.py +109 -0
- capdisc/report/cli.py +35 -0
- capdisc/report/components.py +144 -0
- capdisc/report/harvest.py +150 -0
- capdisc/report/html.py +79 -0
- capdisc/report/models.py +82 -0
- capdisc/report/page.py +129 -0
- capdisc/report/sections.py +154 -0
- capdisc/report/types.py +43 -0
- capdisc/scope/__init__.py +83 -0
- capdisc/scope/inventory/__init__.py +20 -0
- capdisc/scope/inventory/assets.py +54 -0
- capdisc/scope/inventory/capture.py +219 -0
- capdisc/scope/inventory/render.py +149 -0
- capdisc/scope/inventory/roots.py +175 -0
- capdisc/scope/locations.py +330 -0
- capdisc/scope/types.py +154 -0
- capdisc/settings.py +230 -0
- capdisc/tokens.py +48 -0
- claude_code_capabilities-0.1.2.dist-info/METADATA +136 -0
- claude_code_capabilities-0.1.2.dist-info/RECORD +47 -0
- claude_code_capabilities-0.1.2.dist-info/WHEEL +4 -0
- claude_code_capabilities-0.1.2.dist-info/entry_points.txt +2 -0
- 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
|
+
[](https://pypi.org/project/claude-code-capabilities/)
|
|
30
|
+
[](https://pypi.org/project/claude-code-capabilities/)
|
|
31
|
+
[](https://github.com/Magic-Man-us/capability-discovery/actions/workflows/ci.yml)
|
|
32
|
+
[](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,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.
|