ccdoctor 0.1.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.
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: ccdoctor
3
+ Version: 0.1.0
4
+ Summary: Inspect Claude Code visibility for the current project
5
+ Requires-Python: >=3.12
@@ -0,0 +1,418 @@
1
+ <div align="center">
2
+ <a href="https://github.com/Cookie-HOO/ccdoctor">
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/logo-dark.svg">
5
+ <source media="(prefers-color-scheme: light)" srcset="docs/assets/logo-light.svg">
6
+ <img src="docs/assets/logo-dark.svg" width="100px" alt="ccdoctor" />
7
+ </picture>
8
+ </a>
9
+ <h1 style="font-size: 28px; margin: 10px 0;">ccdoctor</h1>
10
+ <p>Inspect Claude Code visibility from your terminal, scripts, and agents.</p>
11
+ </div>
12
+
13
+ <p align="center">
14
+ <a href="https://github.com/Cookie-HOO/ccdoctor" target="_blank">
15
+ <img alt="GitHub Repository" src="https://img.shields.io/badge/GitHub-Cookie--HOO%2Fccdoctor-181717?logo=github" />
16
+ </a>
17
+ <a href="https://docs.astral.sh/uv/guides/tools/" target="_blank">
18
+ <img alt="uvx ready" src="https://img.shields.io/badge/uvx-ready-654ff0" />
19
+ </a>
20
+ <img alt="Python 3.12+" src="https://img.shields.io/badge/python-3.12%2B-3776ab?logo=python&amp;logoColor=white" />
21
+ <img alt="Output modes" src="https://img.shields.io/badge/output-JSON%20%7C%20Markdown%20%7C%20TTY-0f766e" />
22
+ <img alt="Read-only" src="https://img.shields.io/badge/safety-read--only-16a34a" />
23
+ </p>
24
+
25
+ <p align="center">
26
+ <a href="README.zh-CN.md">简体中文</a>
27
+ ·
28
+ <a href="#all-demos">View Demo</a>
29
+ ·
30
+ <a href="https://github.com/Cookie-HOO/ccdoctor/issues/new?labels=bug">Report Bug</a>
31
+ ·
32
+ <a href="https://github.com/Cookie-HOO/ccdoctor/issues/new?labels=enhancement">Request Feature</a>
33
+ ·
34
+ <a href="#agent-and-llm-usage">Agent Usage</a>
35
+ ·
36
+ <a href="#command-reference">Command Reference</a>
37
+ </p>
38
+
39
+ <br>
40
+
41
+ `ccdoctor` is a local diagnostics CLI for Claude Code projects. It reports what Claude Code can see from a project root: MCPs, skills, hooks, plugins, provider/model settings, agents, permissions, statusline configuration, diagnostics, and declared governance manifests.
42
+
43
+ It is intentionally a shell CLI instead of a global MCP. You can run it from any terminal, CI job, script, or agent without adding another always-visible MCP server to Claude Code.
44
+
45
+ > [!TIP]
46
+ > For agents and LLM pipelines, prefer narrow JSON queries: `NO_COLOR=1 ccd --json <category> [name] -p <project>`.
47
+
48
+ <details>
49
+ <summary>Table of contents (Click to show)</summary>
50
+
51
+ - [Why use ccdoctor?](#why-use-ccdoctor)
52
+ - [Quickstart](#quickstart)
53
+ - [All Demos](#all-demos)
54
+ - [Project overview](#project-overview)
55
+ - [Category view](#category-view)
56
+ - [Item detail view](#item-detail-view)
57
+ - [JSON automation](#json-automation)
58
+ - [Markdown output](#markdown-output)
59
+ - [Doctor mode](#doctor-mode)
60
+ - [Command reference](#command-reference)
61
+ - [Options](#options)
62
+ - [Categories](#categories)
63
+ - [Output field reference](#output-field-reference)
64
+ - [`scope` values](#scope-values)
65
+ - [`provider` values](#provider-values)
66
+ - [`effective` values](#effective-values)
67
+ - [Diagnostic severities](#diagnostic-severities)
68
+ - [Agent and LLM usage](#agent-and-llm-usage)
69
+ - [Safety](#safety)
70
+ - [Resources](#resources)
71
+
72
+ </details>
73
+
74
+ # Why use ccdoctor?
75
+
76
+ Claude Code configuration can come from several places at once: project files, user settings, plugins, symlinks, manifests, nested project roots, and runtime defaults. `ccdoctor` gives you one read-only report that separates what is configured, what is declared, and what is expected to be effective.
77
+
78
+ - **Visibility debugging** — See the MCPs, skills, hooks, plugins, agents, provider settings, and permissions Claude Code can discover for a project.
79
+ - **Agent-friendly inspection** — Use filtered JSON output so agents can reason over config without reading unrelated files.
80
+ - **Governance checks** — Compare project/runtime state against declared manifests and flag stale or nested config.
81
+ - **Safe diagnostics** — Runs read-only by default and redacts values whose keys look like tokens, keys, secrets, cookies, passwords, or auth fields.
82
+ - **Multiple output modes** — Human terminal output, JSON for automation, Markdown for PRs/issues, and `--doctor` exit codes for scripts.
83
+
84
+ # Quickstart
85
+
86
+ Run once without installing:
87
+
88
+ ```bash
89
+ uvx ccdoctor
90
+ uvx --from ccdoctor ccd mcp
91
+ ```
92
+
93
+ Install once, then run `ccd` anywhere:
94
+
95
+ ```bash
96
+ uv tool install ccdoctor
97
+ ccd
98
+ ccd mcp
99
+ ```
100
+
101
+ Install from GitHub instead of PyPI:
102
+
103
+ ```bash
104
+ uv tool install git+https://github.com/Cookie-HOO/ccdoctor
105
+ ccd
106
+ ```
107
+
108
+ Inspect another project:
109
+
110
+ ```bash
111
+ ccd -p ~/Projects/my_ai
112
+ ```
113
+
114
+ Inspect one category or one item:
115
+
116
+ ```bash
117
+ ccd mcp
118
+ ccd mcp playwright
119
+ ccd hook PreToolUse
120
+ ccd skill profile-project-bootstrap
121
+ ```
122
+
123
+ Get machine-readable output:
124
+
125
+ ```bash
126
+ NO_COLOR=1 ccd --json mcp playwright -p ~/Projects/my_ai
127
+ ```
128
+
129
+ # All Demos
130
+
131
+ <p align="center">
132
+ <img alt="ccdoctor terminal demo" src="docs/assets/ccdoctor-demo.svg" width="800px" />
133
+ </p>
134
+
135
+ ## Project overview
136
+
137
+ ```bash
138
+ ccd -p ~/Projects/my_ai
139
+ ```
140
+
141
+ ```text
142
+ ✨ Claude Code Doctor
143
+ 📁 Project: /Users/example/Projects/my_ai
144
+
145
+ 🧠 Provider / Model
146
+ • 🧠 model: claude-opus-4-8
147
+
148
+ 🔌 Plugins
149
+ ✅ claude-mem@thedotmack [user, installed-plugin, effective=yes]
150
+
151
+ 🧰 MCPs
152
+ ✅ playwright [project, configured, effective=yes] type=stdio command=/bin/zsh
153
+ ⚠️ fetch [declared, declared, effective=maybe, managed_by=my_ai manifest]
154
+ ```
155
+
156
+ ## Category view
157
+
158
+ ```bash
159
+ ccd mcp -p ~/Projects/my_ai
160
+ ```
161
+
162
+ ```text
163
+ ✨ Claude Code Doctor
164
+ 📁 Project: /Users/example/Projects/my_ai
165
+
166
+ 🧰 MCPs
167
+ ✅ playwright [project, configured, effective=yes] type=stdio command=/bin/zsh source=/Users/example/Projects/my_ai/.mcp.json
168
+ ✅ mcp-search [user, plugin-provided, effective=yes, managed_by=claude-mem@thedotmack] type=stdio command=node
169
+ ⚠️ fetch [declared, declared, effective=maybe, managed_by=my_ai manifest]
170
+ ```
171
+
172
+ ## Item detail view
173
+
174
+ ```bash
175
+ ccd mcp playwright -p ~/Projects/my_ai
176
+ ```
177
+
178
+ ```text
179
+ 🧰 MCPs
180
+ ✅ playwright [project, configured, effective=yes] type=stdio command=/bin/zsh source=/Users/example/Projects/my_ai/.mcp.json
181
+ kind: mcp
182
+ name: playwright
183
+ scope: project
184
+ provider: configured
185
+ effective: yes
186
+ source_file: /Users/example/Projects/my_ai/.mcp.json
187
+ metadata:
188
+ type: stdio
189
+ command: /bin/zsh
190
+ args:
191
+ [
192
+ "-lc",
193
+ "node \"$MY_AI_ROOT/mcps/playwright-mcp/node_modules/@playwright/mcp/cli.js\""
194
+ ]
195
+ ```
196
+
197
+ Hook details work the same way:
198
+
199
+ ```bash
200
+ ccd hook PreToolUse -p ~/Projects/my_ai
201
+ ```
202
+
203
+ ```text
204
+ 🪝 Hooks
205
+ ✅ PreToolUse [user, configured, effective=yes] summary=[{"hooks": [{"command": "...", "type": "command"}], "matcher": "*"}]
206
+ kind: hook
207
+ name: PreToolUse
208
+ scope: user
209
+ provider: configured
210
+ effective: yes
211
+ source_file: /Users/example/.claude/settings.json
212
+ metadata:
213
+ config:
214
+ [
215
+ {
216
+ "hooks": [{"command": "...", "type": "command"}],
217
+ "matcher": "*"
218
+ }
219
+ ]
220
+ ```
221
+
222
+ ## JSON automation
223
+
224
+ ```bash
225
+ ccd --json mcp playwright -p ~/Projects/my_ai
226
+ ```
227
+
228
+ ```json
229
+ {
230
+ "mcps": [
231
+ {
232
+ "effective": "yes",
233
+ "kind": "mcp",
234
+ "metadata": {
235
+ "args": ["-lc", "node \"$MY_AI_ROOT/mcps/playwright-mcp/node_modules/@playwright/mcp/cli.js\""],
236
+ "command": "/bin/zsh",
237
+ "type": "stdio"
238
+ },
239
+ "name": "playwright",
240
+ "provider": "configured",
241
+ "scope": "project",
242
+ "source_file": "/Users/example/Projects/my_ai/.mcp.json"
243
+ }
244
+ ],
245
+ "project_root": "/Users/example/Projects/my_ai"
246
+ }
247
+ ```
248
+
249
+ ## Markdown output
250
+
251
+ ```bash
252
+ ccd --markdown hook PreToolUse -p ~/Projects/my_ai
253
+ ```
254
+
255
+ This produces Markdown headings and JSON metadata blocks suitable for pasting into a GitHub issue or PR comment.
256
+
257
+ ## Doctor mode
258
+
259
+ ```bash
260
+ ccd --doctor -p ~/Projects/my_ai
261
+ ```
262
+
263
+ | Code | Meaning |
264
+ |---:|---|
265
+ | `0` | No warnings or errors. |
266
+ | `1` | Warning diagnostics were found. |
267
+ | `2` | Error diagnostics were found. |
268
+
269
+ # Command reference
270
+
271
+ ```text
272
+ ccd [options] [category] [name]
273
+ ccdoctor [options] [category] [name]
274
+ ```
275
+
276
+ ## Options
277
+
278
+ | Option | Meaning |
279
+ |---|---|
280
+ | `-p, --project PATH` | Inspect another project directory. Defaults to the current directory. |
281
+ | `--json` | Print stable redacted JSON for automation. |
282
+ | `--markdown` | Print Markdown tables/details for issues and PRs. |
283
+ | `--doctor` | Return non-zero when warnings/errors are found. |
284
+ | `--verbose, -v` | Include source paths, allowlists, and diagnostic hints. |
285
+ | `--include-runtime` | Run optional read-only runtime probes, currently localhost proxy `/health`. |
286
+
287
+ ## Categories
288
+
289
+ | Category | Aliases | What it shows |
290
+ |---|---|---|
291
+ | Provider/model | `provider`, `model` | Model, statusline, Claude/Anthropic environment settings, optional runtime probe. |
292
+ | Plugins | `plugin`, `plugins` | Installed/enabled Claude Code plugins and plugin metadata. |
293
+ | MCPs | `mcp`, `mcps` | Project, nested-project, plugin-provided, and manifest-declared MCP servers. |
294
+ | Skills | `skill`, `skills` | Project skills, plugin-provided skills, and runtime/manifest-declared skills. |
295
+ | Agents | `agent`, `agents` | Project agents, profile-provided agents, and plugin-provided agents. |
296
+ | Hooks | `hook`, `hooks` | User/project hooks and plugin-provided hook events. |
297
+ | Permissions | `permission`, `permissions` | Claude Code allow/deny permission settings. |
298
+ | Diagnostics | `diagnostic`, `diagnostics`, `diag` | Warnings and errors discovered while collecting status. |
299
+
300
+ Passing a `name` after a category narrows output to matching entries and prints detail metadata:
301
+
302
+ ```bash
303
+ ccd mcp playwright
304
+ ccd hook PreToolUse
305
+ ccd skill profile-project-bootstrap
306
+ ccd plugin claude-mem
307
+ ```
308
+
309
+ # Output field reference
310
+
311
+ Every collected record is a `StatusItem` with common fields:
312
+
313
+ | Field | Meaning |
314
+ |---|---|
315
+ | `kind` | Record type, such as `mcp`, `skill`, `hook`, `plugin`, `agent`, or `permission`. |
316
+ | `name` | Human-readable record name. |
317
+ | `scope` | Where the record comes from. |
318
+ | `provider` | How the record is provided or managed. |
319
+ | `effective` | Whether Claude Code should actually see or use the record. |
320
+ | `managed_by` | Optional owner/manager, such as a plugin name or `my_ai manifest`. |
321
+ | `source_file` | File or directory where the record was discovered. |
322
+ | `metadata` | Type-specific details, redacted where keys look secret-like. |
323
+ | `diagnostics` | Item-local diagnostic notes when present. |
324
+
325
+ ## `scope` values
326
+
327
+ | Value | Meaning |
328
+ |---|---|
329
+ | `project` | Directly configured in the inspected project. Usually effective when Claude is launched there. |
330
+ | `user` | Comes from the current user's Claude Code configuration or plugin installation. |
331
+ | `nested-project` | Found under the project tree but not at the inspected root. Usually not effective for this root. |
332
+ | `runtime` | Declared as available from the Claude Code runtime or generated runtime manifest. |
333
+ | `declared` | Present in a governance manifest, but not necessarily configured for runtime use. |
334
+ | `custom` | A custom local source, currently used for some agent/profile records. |
335
+
336
+ ## `provider` values
337
+
338
+ | Value | Meaning |
339
+ |---|---|
340
+ | `configured` | Found in a concrete Claude Code config file, such as `.mcp.json` or `.claude/settings.json`. |
341
+ | `custom` | Comes from this repository's custom skill/tool/profile area. |
342
+ | `installed` | Comes from this repository's installed managed assets. |
343
+ | `installed-plugin` | Comes from Claude Code's installed plugin registry. |
344
+ | `plugin-provided` | Provided by a Claude Code plugin. |
345
+ | `runtime-built-in` | Declared by the Claude Code runtime or runtime skill list. |
346
+ | `profile-provided` | Comes from a project profile definition. |
347
+ | `declared` | Listed in a manifest for tracking/governance. |
348
+
349
+ ## `effective` values
350
+
351
+ | Value | Meaning |
352
+ |---|---|
353
+ | `yes` | Expected to be visible/effective for the inspected project. |
354
+ | `no` | Found but not expected to be effective for the inspected project. |
355
+ | `maybe` | Declared or inferred, but runtime effectiveness cannot be proven from static files alone. |
356
+ | `ok` | Runtime probe succeeded. |
357
+ | `failed` | Runtime probe failed. |
358
+
359
+ ## Diagnostic severities
360
+
361
+ | Severity | Meaning |
362
+ |---|---|
363
+ | `info` | Informational note. |
364
+ | `warning` | Something may be stale, ineffective, missing, or surprising. `--doctor` exits `1`. |
365
+ | `error` | Something is malformed or broken enough to require action. `--doctor` exits `2`. |
366
+
367
+ # Agent and LLM usage
368
+
369
+ For agents and LLM pipelines, prefer narrow JSON queries. They are smaller, stable, and easier to parse than terminal output.
370
+
371
+ ```bash
372
+ NO_COLOR=1 ccd --json <category> [name] -p <project>
373
+ ```
374
+
375
+ Examples:
376
+
377
+ ```bash
378
+ NO_COLOR=1 ccd --json mcp playwright -p /repo
379
+ NO_COLOR=1 ccd --json hook PreToolUse -p /repo
380
+ NO_COLOR=1 ccd --json skill profile-project-bootstrap -p /repo
381
+ NO_COLOR=1 ccd --json diagnostics -p /repo
382
+ ```
383
+
384
+ Recommended agent flow:
385
+
386
+ 1. Start with `ccd --json diagnostics -p <project>`.
387
+ 2. If diagnostics mention MCPs, run `ccd --json mcp -p <project>`.
388
+ 3. Query a specific item with `ccd --json mcp <name> -p <project>` before recommending config changes.
389
+ 4. Quote `source_file`, `scope`, `provider`, and `effective` in your answer so the user can verify the finding.
390
+
391
+ Prompt snippet:
392
+
393
+ ```text
394
+ Run `NO_COLOR=1 ccd --json diagnostics -p <project>`. If warnings or errors exist, inspect the relevant category with `ccd --json <category> [name] -p <project>`. Do not modify files. Summarize findings with source_file, scope, provider, effective, and a recommended next action.
395
+ ```
396
+
397
+ Why JSON instead of text for agents?
398
+
399
+ - `--json` has no ANSI color codes.
400
+ - It includes redacted metadata needed for reasoning.
401
+ - It can be filtered by category and name to reduce token use.
402
+ - It avoids over-reading unrelated project configuration.
403
+
404
+ # Safety
405
+
406
+ `ccdoctor` is read-only by default. It does not modify Claude Code settings, plugin config, MCP config, or project files. It does not read `.env` files. Values whose keys look like tokens, keys, secrets, cookies, passwords, or auth fields are redacted before output.
407
+
408
+ Optional runtime probing only runs when `--include-runtime` is passed. Runtime probing is restricted to localhost-style endpoints such as `127.0.0.1` or `localhost`.
409
+
410
+ Manifest-only entries are treated as declared governance state, not proof of runtime visibility.
411
+
412
+ ---
413
+
414
+ # Resources
415
+
416
+ - [uv tools guide](https://docs.astral.sh/uv/guides/tools/) — run Python command-line tools with `uvx`.
417
+ - [Claude Code documentation](https://docs.anthropic.com/en/docs/claude-code) — Claude Code concepts and configuration.
418
+ - [GitHub repository](https://github.com/Cookie-HOO/ccdoctor) — source, issues, and releases.
@@ -0,0 +1,3 @@
1
+ """ccdoctor: inspect Claude Code project-visible configuration."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from .collectors import collect_status
8
+ from .diagnostics import summarize_exit_code
9
+ from .renderers import render_json, render_markdown, render_text
10
+
11
+
12
+ CATEGORY_ALIASES = {
13
+ "provider": "provider",
14
+ "model": "provider",
15
+ "plugin": "plugins",
16
+ "plugins": "plugins",
17
+ "mcp": "mcps",
18
+ "mcps": "mcps",
19
+ "skill": "skills",
20
+ "skills": "skills",
21
+ "agent": "agents",
22
+ "agents": "agents",
23
+ "hook": "hooks",
24
+ "hooks": "hooks",
25
+ "permission": "permissions",
26
+ "permissions": "permissions",
27
+ "diagnostic": "diagnostics",
28
+ "diagnostics": "diagnostics",
29
+ "diag": "diagnostics",
30
+ }
31
+
32
+
33
+ def build_parser() -> argparse.ArgumentParser:
34
+ parser = argparse.ArgumentParser(
35
+ description="Inspect Claude Code visible MCPs, skills, hooks, plugins, and provider settings for a project.",
36
+ )
37
+ parser.add_argument("--project", "-p", default=".", help="Project directory to inspect. Defaults to the current directory.")
38
+ parser.add_argument("--json", action="store_true", help="Print machine-readable JSON.")
39
+ parser.add_argument("--markdown", action="store_true", help="Print Markdown tables.")
40
+ parser.add_argument("--doctor", action="store_true", help="Run diagnostics and return non-zero when warnings/errors are found.")
41
+ parser.add_argument("--verbose", "-v", action="store_true", help="Include source paths, allowlists, and diagnostic hints.")
42
+ parser.add_argument("--include-runtime", action="store_true", help="Run optional read-only runtime probes, such as localhost proxy /health.")
43
+ parser.add_argument(
44
+ "category",
45
+ nargs="?",
46
+ choices=sorted(CATEGORY_ALIASES),
47
+ help="Show only one category: provider, plugin, mcp, skill, agent, hook, permission, or diagnostic.",
48
+ )
49
+ parser.add_argument("name", nargs="?", help="Show one item within the selected category, such as an MCP, hook, or skill name.")
50
+ return parser
51
+
52
+
53
+ def main(argv: list[str] | None = None) -> int:
54
+ args = build_parser().parse_args(argv)
55
+ status = collect_status(Path(args.project), include_runtime=args.include_runtime, verbose=args.verbose)
56
+ category = CATEGORY_ALIASES.get(args.category) if args.category else None
57
+
58
+ if args.name and not category:
59
+ build_parser().error("name can only be used after a category, for example: ccd mcp playwright")
60
+
61
+ if args.json:
62
+ print(render_json(status, category=category, name=args.name))
63
+ elif args.markdown:
64
+ print(render_markdown(status, category=category, name=args.name))
65
+ else:
66
+ print(render_text(status, verbose=args.verbose or args.doctor or bool(category), category=category, name=args.name))
67
+
68
+ if args.doctor:
69
+ diagnostics = status.get("diagnostics", [])
70
+ return summarize_exit_code_from_dicts(diagnostics)
71
+ return 0
72
+
73
+
74
+ def summarize_exit_code_from_dicts(diagnostics: list[dict[str, object]]) -> int:
75
+ from .models import Diagnostic
76
+
77
+ return summarize_exit_code([
78
+ Diagnostic(
79
+ severity=str(item.get("severity", "info")),
80
+ message=str(item.get("message", "")),
81
+ source_file=str(item["source_file"]) if item.get("source_file") else None,
82
+ hint=str(item["hint"]) if item.get("hint") else None,
83
+ )
84
+ for item in diagnostics
85
+ ])
86
+
87
+
88
+ if __name__ == "__main__":
89
+ sys.exit(main())
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ SECRET_MARKERS = ("TOKEN", "KEY", "SECRET", "COOKIE", "PASSWORD", "AUTH")
6
+
7
+
8
+ def is_secret_key(key: str) -> bool:
9
+ upper = key.upper()
10
+ return any(marker in upper for marker in SECRET_MARKERS)
11
+
12
+
13
+ def redact_value(key: str, value: object) -> object:
14
+ if is_secret_key(key):
15
+ if value in (None, ""):
16
+ return value
17
+ return "<redacted>"
18
+ if isinstance(value, dict):
19
+ return redact_mapping(value)
20
+ if isinstance(value, list):
21
+ return [redact_sequence_value(item) for item in value]
22
+ return value
23
+
24
+
25
+ def redact_sequence_value(value: object) -> object:
26
+ if isinstance(value, dict):
27
+ return redact_mapping(value)
28
+ if isinstance(value, list):
29
+ return [redact_sequence_value(item) for item in value]
30
+ return value
31
+
32
+
33
+ def redact_any(value: object) -> object:
34
+ if isinstance(value, dict):
35
+ return redact_mapping(value)
36
+ if isinstance(value, list):
37
+ return [redact_sequence_value(item) for item in value]
38
+ return value
39
+
40
+
41
+ def redact_mapping(mapping: dict[str, object]) -> dict[str, object]:
42
+ return {key: redact_value(key, value) for key, value in mapping.items()}
43
+
44
+
45
+ def classify_skill_target(target: Path | None, control_root: Path) -> tuple[str, str]:
46
+ if target is None:
47
+ return "configured", "unknown"
48
+
49
+ try:
50
+ resolved = target.resolve(strict=False)
51
+ except OSError:
52
+ return "configured", "unknown"
53
+
54
+ custom_root = control_root / "skills" / "custom"
55
+ installed_root = control_root / "skills" / "installed"
56
+
57
+ if _is_relative_to(resolved, custom_root):
58
+ return "custom", "my_ai"
59
+ if _is_relative_to(resolved, installed_root):
60
+ return "installed", "my_ai"
61
+ return "configured", "project"
62
+
63
+
64
+ def _is_relative_to(path: Path, parent: Path) -> bool:
65
+ try:
66
+ path.relative_to(parent.resolve(strict=False))
67
+ return True
68
+ except ValueError:
69
+ return False