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/__init__.py ADDED
@@ -0,0 +1,69 @@
1
+ """Discovery of Claude Code capabilities: skills, agents, plugins, MCP servers, hooks.
2
+
3
+ Scans the environment's scope roots into a typed inventory, harvests plugin and MCP
4
+ metadata, and assembles capability catalogs and the environment report.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from .catalog import (
10
+ Catalog,
11
+ CatalogEntry,
12
+ CatalogMcpServer,
13
+ CatalogPlugin,
14
+ CatalogSkill,
15
+ CatalogTool,
16
+ McpTool,
17
+ )
18
+ from .discovery import BUILTIN_TOOLS, scan_environment, scan_indexed_skills, scan_skills
19
+ from .mcp_catalog import enumerate_mcp_servers
20
+ from .mcp_harvest import (
21
+ cache_is_stale,
22
+ read_mcp_cache,
23
+ refresh_in_background,
24
+ refresh_mcp_cache,
25
+ write_mcp_cache,
26
+ )
27
+ from .plugin_catalog import (
28
+ enumerate_plugins,
29
+ enumerate_plugins_with_paths,
30
+ installed_plugin_dirs,
31
+ installed_plugins,
32
+ )
33
+ from .report import EnvironmentReport, build_report, write_report, write_report_on_start
34
+ from .scope import ScopeInventory, ScopeRoots, default_managed_dir
35
+ from .settings import DiscoverySettings, ExtraSourceDir, get_settings
36
+
37
+ __all__ = [
38
+ "BUILTIN_TOOLS",
39
+ "Catalog",
40
+ "CatalogEntry",
41
+ "CatalogMcpServer",
42
+ "CatalogPlugin",
43
+ "CatalogSkill",
44
+ "CatalogTool",
45
+ "DiscoverySettings",
46
+ "EnvironmentReport",
47
+ "ExtraSourceDir",
48
+ "McpTool",
49
+ "ScopeInventory",
50
+ "ScopeRoots",
51
+ "build_report",
52
+ "cache_is_stale",
53
+ "default_managed_dir",
54
+ "enumerate_mcp_servers",
55
+ "enumerate_plugins",
56
+ "enumerate_plugins_with_paths",
57
+ "get_settings",
58
+ "installed_plugin_dirs",
59
+ "installed_plugins",
60
+ "read_mcp_cache",
61
+ "refresh_in_background",
62
+ "refresh_mcp_cache",
63
+ "scan_environment",
64
+ "scan_indexed_skills",
65
+ "scan_skills",
66
+ "write_mcp_cache",
67
+ "write_report",
68
+ "write_report_on_start",
69
+ ]
capdisc/base.py ADDED
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from pydantic import BaseModel, ConfigDict
6
+ from pydantic.alias_generators import to_camel
7
+ from pydantic.fields import ComputedFieldInfo, FieldInfo
8
+
9
+ _CAMEL_BOUNDARY = re.compile(r"(?<=[a-z0-9])(?=[A-Z])")
10
+
11
+
12
+ def humanize_field_title(field_name: str, _info: FieldInfo | ComputedFieldInfo) -> str:
13
+ """A clean, alias-independent JSON-Schema title from a field's Python name.
14
+
15
+ Splits snake_case, kebab-case, and camelCase into words and renders them sentence-cased
16
+ (`status_message` -> `Status message`). Avoids the mangled titles Pydantic derives from a
17
+ camelCase/hyphenated alias when a schema is exported `by_alias` (`statusMessage` -> the broken
18
+ `Statusmessage`). Set as `field_title_generator`, so it drives the field-level `title` only;
19
+ a domain-primitive alias's own `title` on the inner type is untouched.
20
+ """
21
+ spaced = _CAMEL_BOUNDARY.sub(" ", field_name.replace("_", " ").replace("-", " "))
22
+ return " ".join(spaced.split()).capitalize()
23
+
24
+
25
+ class FrozenModel(BaseModel):
26
+ """Immutable, strict domain model — the single config preset for this package."""
27
+
28
+ model_config = ConfigDict(extra="forbid", frozen=True)
29
+
30
+
31
+ class FrozenWireModel(BaseModel):
32
+ """Immutable model whose snake_case fields bind to the camelCase JSON keys of a Claude Code
33
+ config file — accepts either spelling on load, emits camelCase on dump. Tolerant of unknown
34
+ keys (`extra="ignore"`): this is the ingest boundary for config written by a Claude Code that
35
+ may be newer than us, so a field we don't model yet is dropped, never a parse failure."""
36
+
37
+ model_config = ConfigDict(
38
+ extra="ignore",
39
+ frozen=True,
40
+ alias_generator=to_camel,
41
+ populate_by_name=True,
42
+ serialize_by_alias=True,
43
+ field_title_generator=humanize_field_title,
44
+ )
45
+
46
+
47
+ class MutableModel(BaseModel):
48
+ """Strict but mutable domain model — for in-place accumulators that fill across a loop."""
49
+
50
+ model_config = ConfigDict(extra="forbid")
51
+
52
+
53
+ class InputModel(BaseModel):
54
+ """Lenient boundary model — ignores unknown keys from external sources."""
55
+
56
+ model_config = ConfigDict(extra="ignore")
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ from .entries import (
4
+ Catalog,
5
+ CatalogEntry,
6
+ CatalogMcpServer,
7
+ CatalogPlugin,
8
+ CatalogSkill,
9
+ CatalogTool,
10
+ McpTool,
11
+ )
12
+ from .types import (
13
+ CATALOG_ID_MAX,
14
+ DEFAULT_RECALL_LIMIT,
15
+ DESCRIPTION_TOKEN_MAX,
16
+ MCP_INPUT_SCHEMA_MAX_BYTES,
17
+ MCP_TOOL_DESCRIPTION_MAX,
18
+ MCP_TOOL_PARAMS_MAX,
19
+ RELEVANCE_THRESHOLD,
20
+ BuiltinTool,
21
+ CatalogEntryId,
22
+ CatalogIdPrefix,
23
+ EntryDescription,
24
+ McpInputSchema,
25
+ McpServerRef,
26
+ McpToolCount,
27
+ McpToolName,
28
+ NeedsNetworkFlag,
29
+ PluginRef,
30
+ ReadOnlyFlag,
31
+ RecallLimit,
32
+ RelevanceScore,
33
+ SkillRef,
34
+ Tag,
35
+ ToolRef,
36
+ catalog_id,
37
+ )
38
+
39
+ __all__ = [
40
+ "CATALOG_ID_MAX",
41
+ "DEFAULT_RECALL_LIMIT",
42
+ "DESCRIPTION_TOKEN_MAX",
43
+ "MCP_INPUT_SCHEMA_MAX_BYTES",
44
+ "MCP_TOOL_DESCRIPTION_MAX",
45
+ "MCP_TOOL_PARAMS_MAX",
46
+ "RELEVANCE_THRESHOLD",
47
+ "BuiltinTool",
48
+ "Catalog",
49
+ "CatalogEntry",
50
+ "CatalogEntryId",
51
+ "CatalogIdPrefix",
52
+ "CatalogMcpServer",
53
+ "CatalogPlugin",
54
+ "CatalogSkill",
55
+ "CatalogTool",
56
+ "EntryDescription",
57
+ "McpInputSchema",
58
+ "McpServerRef",
59
+ "McpTool",
60
+ "McpToolCount",
61
+ "McpToolName",
62
+ "NeedsNetworkFlag",
63
+ "PluginRef",
64
+ "ReadOnlyFlag",
65
+ "RecallLimit",
66
+ "RelevanceScore",
67
+ "SkillRef",
68
+ "Tag",
69
+ "ToolRef",
70
+ "catalog_id",
71
+ ]
@@ -0,0 +1,104 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Annotated, Literal
4
+
5
+ from pydantic import Field
6
+
7
+ from ..base import FrozenModel
8
+ from .types import (
9
+ MCP_TOOL_PARAMS_MAX,
10
+ CatalogEntryId,
11
+ EntryDescription,
12
+ McpInputSchema,
13
+ McpServerRef,
14
+ McpToolDescription,
15
+ McpToolName,
16
+ NeedsNetworkFlag,
17
+ PluginRef,
18
+ ReadOnlyFlag,
19
+ SkillRef,
20
+ Tag,
21
+ ToolRef,
22
+ )
23
+
24
+
25
+ class _Card(FrozenModel):
26
+ """Fields every catalog entry shares: its id, description, and recall tags."""
27
+
28
+ id: CatalogEntryId
29
+ description: EntryDescription
30
+ tags: list[Tag] = []
31
+
32
+ @property
33
+ def search_text(self) -> str:
34
+ """The text recall ranks this entry on — its description and tags."""
35
+ return " ".join((self.description, *self.tags))
36
+
37
+
38
+ class McpTool(FrozenModel):
39
+ """One tool a connected MCP server provides, harvested from its `list_tools()`: its name,
40
+ input-parameter names, full input schema, and description — the content that lets recall
41
+ rank a server by what its tools actually do, not just its name."""
42
+
43
+ name: McpToolName
44
+ description: McpToolDescription = ""
45
+ params: list[McpToolName] = Field(default_factory=list, max_length=MCP_TOOL_PARAMS_MAX)
46
+ input_schema: McpInputSchema | None = None
47
+
48
+
49
+ class _Tool(_Card):
50
+ """Shared execution-constraint facets (`read_only`, `needs_network`) for tool-like entries."""
51
+
52
+ ref: ToolRef
53
+ read_only: ReadOnlyFlag = True
54
+ needs_network: NeedsNetworkFlag = False
55
+
56
+
57
+ class CatalogSkill(_Card):
58
+ """A skill an agent can load."""
59
+
60
+ kind: Literal["skill"] = "skill"
61
+ ref: SkillRef
62
+
63
+
64
+ class CatalogTool(_Tool):
65
+ """A built-in tool an agent can be granted, with its execution-constraint facets."""
66
+
67
+ kind: Literal["tool"] = "tool"
68
+
69
+
70
+ class CatalogMcpServer(_Card):
71
+ """A connected MCP server an agent can be wired to, with its harvested tools."""
72
+
73
+ kind: Literal["mcp_server"] = "mcp_server"
74
+ ref: McpServerRef
75
+ tools: list[McpTool] = []
76
+
77
+ @property
78
+ def search_text(self) -> str:
79
+ """The server's own description/tags plus each harvested tool's name, parameters, and
80
+ description — so a task matches what the server's tools do, fixing name-only ranking."""
81
+ tools = " ".join(f"{t.name} {' '.join(t.params)} {t.description}" for t in self.tools)
82
+ return f"{super().search_text} {tools}".strip()
83
+
84
+
85
+ class CatalogPlugin(_Card):
86
+ """An installed plugin an agent can draw from, with the skills and MCP servers it bundles."""
87
+
88
+ kind: Literal["plugin"] = "plugin"
89
+ ref: PluginRef
90
+ skills: list[SkillRef] = []
91
+ mcp_servers: list[McpServerRef] = []
92
+
93
+
94
+ CatalogEntry = Annotated[
95
+ CatalogSkill | CatalogTool | CatalogMcpServer | CatalogPlugin,
96
+ Field(discriminator="kind"),
97
+ ]
98
+
99
+
100
+ class Catalog(FrozenModel):
101
+ """The full set of capabilities — skills, tools, MCP servers, and plugins — the generator
102
+ can draw from when building an agent."""
103
+
104
+ entries: list[CatalogEntry]
@@ -0,0 +1,227 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from enum import StrEnum
5
+ from typing import Annotated, Literal
6
+
7
+ from pydantic import Field, JsonValue, TypeAdapter
8
+ from pydantic.functional_validators import AfterValidator, BeforeValidator
9
+
10
+ from ..tokens import token_bounds
11
+
12
+ DESCRIPTION_TOKEN_MAX = 512
13
+ CATALOG_ID_MAX = 64 # length cap of CatalogEntryId; catalog_id truncates to it
14
+ MCP_TOOL_DESCRIPTION_MAX = 4000 # length cap of McpToolDescription; harvest truncates to it
15
+ MCP_TOOL_PARAMS_MAX = 200 # item cap of McpTool.params
16
+ MCP_INPUT_SCHEMA_MAX_BYTES = 20_000 # serialized size cap of McpInputSchema
17
+
18
+ _INPUT_SCHEMA_ADAPTER: TypeAdapter[dict[str, JsonValue]] = TypeAdapter(dict[str, JsonValue])
19
+
20
+ _SLUG_SKILL = re.compile(r"[^a-z0-9:-]+") # skill refs keep ':' for plugin:skill
21
+ _SLUG_REF = re.compile(r"[^a-z0-9_-]+") # server and plugin refs keep '_'
22
+ _SLUG_ID = re.compile(r"[^a-z0-9._-]+") # catalog ids keep '.' for the 'kind.ref' shape
23
+
24
+
25
+ def _slugify(value: str, disallowed: re.Pattern[str], max_len: int) -> str:
26
+ """Normalize an arbitrary name into a ref: lowercase, collapse disallowed runs to '-', trim
27
+ edge punctuation so it starts and ends on a word char, and truncate."""
28
+ return disallowed.sub("-", value.strip().lower()).strip("-:_")[:max_len]
29
+
30
+
31
+ def _to_skill_ref(value: str) -> str:
32
+ """Slugify a name into a `SkillRef`, keeping `:` for the `plugin:skill` form (max 128)."""
33
+ return _slugify(value, _SLUG_SKILL, 128)
34
+
35
+
36
+ def _to_underscore_ref(value: str) -> str:
37
+ """Slugify a name into a server/plugin ref, keeping `_` (max 64)."""
38
+ return _slugify(value, _SLUG_REF, 64)
39
+
40
+
41
+ CatalogIdPrefix = Literal["plugin", "mcp", "skill"]
42
+
43
+
44
+ def catalog_id(prefix: CatalogIdPrefix, ref: str) -> CatalogEntryId:
45
+ """A valid `CatalogEntryId` for `prefix.ref` — folds ':' and overflow away so a valid ref
46
+ never yields an unrepresentable id. `CatalogEntryId` itself stays strict (it is a lookup
47
+ key); this is the one place a kind+ref is composed into an id."""
48
+ return _slugify(f"{prefix}.{ref}", _SLUG_ID, CATALOG_ID_MAX)
49
+
50
+
51
+ class BuiltinTool(StrEnum):
52
+ """The grant refs of the built-in Claude Code tools — the single source for both the tool
53
+ catalog cards and the default tool set a generated agent receives."""
54
+
55
+ read = "Read"
56
+ write = "Write"
57
+ edit = "Edit"
58
+ glob = "Glob"
59
+ grep = "Grep"
60
+ bash = "Bash"
61
+ web_fetch = "WebFetch"
62
+ web_search = "WebSearch"
63
+ task = "Task"
64
+
65
+
66
+ EntryDescription = Annotated[
67
+ str,
68
+ Field(
69
+ min_length=8,
70
+ max_length=1536,
71
+ title="Entry description",
72
+ description="What a catalog entry provides and when it applies.",
73
+ examples=["Reads a file from disk."],
74
+ ),
75
+ token_bounds(DESCRIPTION_TOKEN_MAX),
76
+ ]
77
+ ToolRef = Annotated[
78
+ str,
79
+ Field(
80
+ min_length=1,
81
+ max_length=200,
82
+ title="Tool ref",
83
+ description="A tool grant string, e.g. 'Read', 'Bash(git log:*)', or 'mcp__server__tool'.",
84
+ examples=["Read", "Bash(git log:*)"],
85
+ ),
86
+ ]
87
+ McpToolName = Annotated[
88
+ str,
89
+ Field(
90
+ pattern=r"^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$",
91
+ title="MCP tool name",
92
+ description="Name of a tool (or input parameter) provided by an MCP server.",
93
+ examples=["browser_navigate", "file_path"],
94
+ ),
95
+ ]
96
+ McpToolDescription = Annotated[
97
+ str,
98
+ Field(
99
+ max_length=MCP_TOOL_DESCRIPTION_MAX,
100
+ title="MCP tool description",
101
+ description="What an MCP tool does, as reported by the server's `list_tools()`.",
102
+ examples=["Navigate the browser to a url and wait for load."],
103
+ ),
104
+ ]
105
+
106
+
107
+ def _check_schema_size(value: dict[str, JsonValue]) -> dict[str, JsonValue]:
108
+ """Reject a schema whose JSON serialization exceeds `MCP_INPUT_SCHEMA_MAX_BYTES` — an MCP
109
+ server's `list_tools()` response is untrusted input, and one tool's input schema must not be
110
+ able to inflate the on-disk cache without limit."""
111
+ if len(_INPUT_SCHEMA_ADAPTER.dump_json(value)) > MCP_INPUT_SCHEMA_MAX_BYTES:
112
+ raise ValueError(f"input schema exceeds {MCP_INPUT_SCHEMA_MAX_BYTES} bytes serialized")
113
+ return value
114
+
115
+
116
+ McpInputSchema = Annotated[
117
+ dict[str, JsonValue],
118
+ AfterValidator(_check_schema_size),
119
+ Field(
120
+ title="MCP tool input schema",
121
+ description=(
122
+ "Full JSON Schema object for an MCP tool's input, as reported by `list_tools()`."
123
+ ),
124
+ examples=[
125
+ {
126
+ "type": "object",
127
+ "properties": {"url": {"type": "string"}},
128
+ "required": ["url"],
129
+ }
130
+ ],
131
+ ),
132
+ ]
133
+ SkillRef = Annotated[
134
+ str,
135
+ BeforeValidator(_to_skill_ref),
136
+ Field(
137
+ pattern=r"^[a-z0-9][a-z0-9:-]{0,127}$",
138
+ title="Skill ref",
139
+ description="A skill name or plugin:skill reference; a raw name is slugified to one.",
140
+ examples=["error-handling", "my-plugin:my-skill"],
141
+ ),
142
+ ]
143
+ McpServerRef = Annotated[
144
+ str,
145
+ BeforeValidator(_to_underscore_ref),
146
+ Field(
147
+ pattern=r"^[a-z0-9][a-z0-9_-]{0,63}$",
148
+ title="MCP server",
149
+ description="An MCP server name; a raw name is slugified to one.",
150
+ examples=["playwright"],
151
+ ),
152
+ ]
153
+ PluginRef = Annotated[
154
+ str,
155
+ BeforeValidator(_to_underscore_ref),
156
+ Field(
157
+ pattern=r"^[a-z0-9][a-z0-9_-]{0,63}$",
158
+ title="Plugin",
159
+ description="An installed plugin name (marketplace suffix stripped); slugified.",
160
+ examples=["agentforge"],
161
+ ),
162
+ ]
163
+ Tag = Annotated[
164
+ str,
165
+ Field(
166
+ pattern=r"^[a-z0-9][a-z0-9-]{0,31}$",
167
+ title="Tag",
168
+ description="A keyword folded into a catalog entry's text for lexical recall.",
169
+ examples=["git"],
170
+ ),
171
+ ]
172
+ CatalogEntryId = Annotated[
173
+ str,
174
+ Field(
175
+ pattern=r"^[a-z0-9][a-z0-9._-]{0,63}$",
176
+ title="Catalog id",
177
+ description=(
178
+ "Stable id of one item in the catalog of skills, tools, and MCP servers an agent "
179
+ "can be built from. Strict — it is a lookup key; compose ids via catalog_id()."
180
+ ),
181
+ examples=["tool.read"],
182
+ ),
183
+ ]
184
+ RelevanceScore = Annotated[
185
+ float,
186
+ Field(
187
+ ge=0.0,
188
+ le=1.0,
189
+ title="Relevance",
190
+ description="How well a catalog item matches the task, from 0 (no match) to 1 (best).",
191
+ ),
192
+ ]
193
+ RecallLimit = Annotated[
194
+ int,
195
+ Field(
196
+ ge=1,
197
+ le=200,
198
+ title="Recall limit",
199
+ description="Most matching catalog items the search step may return.",
200
+ ),
201
+ ]
202
+ McpToolCount = Annotated[
203
+ int,
204
+ Field(
205
+ ge=0,
206
+ title="MCP tool count",
207
+ description="Number of tools a connected MCP server provides.",
208
+ examples=[4],
209
+ ),
210
+ ]
211
+ ReadOnlyFlag = Annotated[
212
+ bool,
213
+ Field(
214
+ title="Read-only",
215
+ description="Whether the tool only reads state, never writes or executes.",
216
+ ),
217
+ ]
218
+ NeedsNetworkFlag = Annotated[
219
+ bool,
220
+ Field(
221
+ title="Needs network",
222
+ description="Whether the tool requires outbound network access to function.",
223
+ ),
224
+ ]
225
+
226
+ DEFAULT_RECALL_LIMIT = 30
227
+ RELEVANCE_THRESHOLD = 0.1