lazyopencode 0.1.1__tar.gz → 0.2.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.
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/PKG-INFO +2 -1
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/pyproject.toml +2 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/__init__.py +12 -1
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/_version.py +2 -2
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/app.py +132 -2
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/bindings.py +2 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/mixins/help.py +2 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/models/customization.py +35 -4
- lazyopencode-0.2.0/src/lazyopencode/services/claude_code/__init__.py +9 -0
- lazyopencode-0.2.0/src/lazyopencode/services/claude_code/discovery.py +158 -0
- lazyopencode-0.2.0/src/lazyopencode/services/claude_code/parsers/__init__.py +7 -0
- lazyopencode-0.2.0/src/lazyopencode/services/claude_code/parsers/agent.py +58 -0
- lazyopencode-0.2.0/src/lazyopencode/services/claude_code/parsers/command.py +75 -0
- lazyopencode-0.2.0/src/lazyopencode/services/claude_code/parsers/skill.py +130 -0
- lazyopencode-0.2.0/src/lazyopencode/services/claude_code/plugin_loader.py +164 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/services/discovery.py +25 -4
- lazyopencode-0.2.0/src/lazyopencode/services/writer.py +119 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/widgets/app_footer.py +1 -1
- lazyopencode-0.2.0/src/lazyopencode/widgets/level_selector.py +130 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/uv.lock +22 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/.github/release-drafter.yml +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/.github/workflows/ci.yml +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/.github/workflows/publish.yml +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/.github/workflows/release-drafter.yml +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/.gitignore +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/.opencode/command/run-quality-gates.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/.opencode/skill/quality-gates/SKILL.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/.pre-commit-config.yaml +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/AGENTS.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/LICENSE +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/README.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/artifacts/demo.png +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/artifacts/lazyclaude-reference.png +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/opencode.json +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/scripts/check_quality.sh +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/__main__.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/mixins/filtering.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/mixins/navigation.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/models/__init__.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/services/__init__.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/services/gitignore_filter.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/services/parsers/__init__.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/services/parsers/agent.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/services/parsers/command.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/services/parsers/mcp.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/services/parsers/plugin.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/services/parsers/rules.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/services/parsers/skill.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/services/parsers/tool.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/styles/app.tcss +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/themes.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/widgets/__init__.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/widgets/combined_panel.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/widgets/detail_pane.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/widgets/filter_input.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/widgets/helpers/__init__.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/widgets/helpers/rendering.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/widgets/status_panel.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/src/lazyopencode/widgets/type_panel.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/AGENTS.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/conftest.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/integration/__init__.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/integration/discovery/__init__.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/integration/discovery/test_agents.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/integration/discovery/test_commands.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/integration/discovery/test_full_discovery.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/integration/discovery/test_mcps.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/integration/discovery/test_rules.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/integration/discovery/test_skills.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/integration/fixtures/agent/explorer.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/integration/fixtures/command/greet.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/integration/fixtures/mcp/project-opencode.json +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/integration/fixtures/mcp/user-opencode.json +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/integration/fixtures/memory/AGENTS.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/integration/fixtures/project/AGENTS.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/integration/fixtures/project/agent/reviewer.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/integration/fixtures/project/command/project-cmd.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/integration/fixtures/project/docs/guidelines.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/integration/fixtures/project/skill/project-skill/SKILL.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/integration/fixtures/project/skill/project-skill/src/helper.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/integration/fixtures/skill/task-tracker/SKILL.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/integration/fixtures/skill/task-tracker/reference.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/integration/fixtures/skill/task-tracker/scripts/run.sh +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/SPEC.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/__init__.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/conftest.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/complex/.opencode/agent/reviewer.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/complex/.opencode/command/verify.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/complex/.opencode/plugin/metrics.ts +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/complex/.opencode/skill/deploy-helper/SKILL.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/complex/.opencode/skill/deploy-helper/scripts/deploy.sh +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/complex/.opencode/tool/search.ts +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/complex/AGENTS.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/complex/README.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/complex/docs/guidelines.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/complex/opencode.json +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/complex/prompts/auditor.txt +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/file_commands/.opencode/command/deploy.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/file_commands/.opencode/command/greet.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/file_commands/README.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/file_references/README.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/file_references/opencode.json +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/file_references/prompts/agent.txt +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/file_references/templates/cmd.txt +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/inline_commands/README.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/inline_commands/opencode.json +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/minimal/AGENTS.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/minimal/README.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/mixed_config/.opencode/agent/file-agent.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/mixed_config/.opencode/command/file-cmd.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/mixed_config/AGENTS.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/mixed_config/README.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/mixed_config/opencode.json +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/rich_opencode_json/README.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/rich_opencode_json/docs/api-standards.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/rich_opencode_json/opencode.json +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/rich_opencode_json/prompts/security-audit.txt +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/rich_opencode_json/scripts/fake-lint.sh +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/skills_with_tree/.opencode/skill/my-skill/SKILL.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/skills_with_tree/.opencode/skill/my-skill/docs/guide.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/skills_with_tree/.opencode/skill/my-skill/scripts/run.sh +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/scenarios/skills_with_tree/README.md +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/spec/test_scenarios.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/test_inline.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/test_version.py +0 -0
- {lazyopencode-0.1.1 → lazyopencode-0.2.0}/tests/unit/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lazyopencode
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A lazygit-style TUI for visualizing OpenCode customizations
|
|
5
5
|
Project-URL: Homepage, https://github.com/nikiforovall/lazyopencode
|
|
6
6
|
Project-URL: Repository, https://github.com/nikiforovall/lazyopencode
|
|
@@ -19,6 +19,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
19
19
|
Classifier: Topic :: Software Development :: User Interfaces
|
|
20
20
|
Requires-Python: >=3.11
|
|
21
21
|
Requires-Dist: pathspec>=0.12.0
|
|
22
|
+
Requires-Dist: pyperclip>=1.9.0
|
|
22
23
|
Requires-Dist: pyyaml>=6.0
|
|
23
24
|
Requires-Dist: rich>=13.0.0
|
|
24
25
|
Requires-Dist: textual>=0.89.0
|
|
@@ -26,6 +26,7 @@ dependencies = [
|
|
|
26
26
|
"rich>=13.0.0",
|
|
27
27
|
"pyyaml>=6.0",
|
|
28
28
|
"pathspec>=0.12.0",
|
|
29
|
+
"pyperclip>=1.9.0",
|
|
29
30
|
]
|
|
30
31
|
|
|
31
32
|
[project.urls]
|
|
@@ -108,5 +109,6 @@ dev = [
|
|
|
108
109
|
"pytest>=9.0.1",
|
|
109
110
|
"pytest-asyncio>=1.3.0",
|
|
110
111
|
"ruff>=0.14.8",
|
|
112
|
+
"types-pyperclip>=1.9.0",
|
|
111
113
|
"types-pyyaml>=6.0.12.20250915",
|
|
112
114
|
]
|
|
@@ -38,11 +38,22 @@ def main() -> None:
|
|
|
38
38
|
help="Override user config path (default: ~/.config/opencode)",
|
|
39
39
|
)
|
|
40
40
|
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"--claude-code",
|
|
43
|
+
action="store_true",
|
|
44
|
+
default=False,
|
|
45
|
+
help="Enable Claude Code customizations discovery (from ~/.claude/)",
|
|
46
|
+
)
|
|
47
|
+
|
|
41
48
|
args = parser.parse_args()
|
|
42
49
|
|
|
43
50
|
# Handle directory argument - resolve to absolute path
|
|
44
51
|
project_root = args.directory.resolve() if args.directory else None
|
|
45
52
|
user_config = args.user_config.resolve() if args.user_config else None
|
|
46
53
|
|
|
47
|
-
app = create_app(
|
|
54
|
+
app = create_app(
|
|
55
|
+
project_root=project_root,
|
|
56
|
+
global_config_path=user_config,
|
|
57
|
+
enable_claude_code=args.claude_code,
|
|
58
|
+
)
|
|
48
59
|
app.run()
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.
|
|
32
|
-
__version_tuple__ = version_tuple = (0,
|
|
31
|
+
__version__ = version = '0.2.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 2, 0)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -16,6 +16,7 @@ from lazyopencode.mixins.help import HelpMixin
|
|
|
16
16
|
from lazyopencode.mixins.navigation import NavigationMixin
|
|
17
17
|
from lazyopencode.models.customization import (
|
|
18
18
|
ConfigLevel,
|
|
19
|
+
ConfigSource,
|
|
19
20
|
Customization,
|
|
20
21
|
CustomizationType,
|
|
21
22
|
)
|
|
@@ -25,6 +26,7 @@ from lazyopencode.widgets.app_footer import AppFooter
|
|
|
25
26
|
from lazyopencode.widgets.combined_panel import CombinedPanel
|
|
26
27
|
from lazyopencode.widgets.detail_pane import MainPane
|
|
27
28
|
from lazyopencode.widgets.filter_input import FilterInput
|
|
29
|
+
from lazyopencode.widgets.level_selector import LevelSelector
|
|
28
30
|
from lazyopencode.widgets.status_panel import StatusPanel
|
|
29
31
|
from lazyopencode.widgets.type_panel import TypePanel
|
|
30
32
|
|
|
@@ -44,6 +46,7 @@ class LazyOpenCode(App, NavigationMixin, FilteringMixin, HelpMixin):
|
|
|
44
46
|
discovery_service: ConfigDiscoveryService | None = None,
|
|
45
47
|
project_root: Path | None = None,
|
|
46
48
|
global_config_path: Path | None = None,
|
|
49
|
+
enable_claude_code: bool = False,
|
|
47
50
|
) -> None:
|
|
48
51
|
"""Initialize LazyOpenCode application."""
|
|
49
52
|
super().__init__()
|
|
@@ -51,6 +54,7 @@ class LazyOpenCode(App, NavigationMixin, FilteringMixin, HelpMixin):
|
|
|
51
54
|
self._discovery_service = discovery_service or ConfigDiscoveryService(
|
|
52
55
|
project_root=project_root,
|
|
53
56
|
global_config_path=global_config_path,
|
|
57
|
+
enable_claude_code=enable_claude_code,
|
|
54
58
|
)
|
|
55
59
|
self._customizations: list[Customization] = []
|
|
56
60
|
self._level_filter: ConfigLevel | None = None
|
|
@@ -60,7 +64,9 @@ class LazyOpenCode(App, NavigationMixin, FilteringMixin, HelpMixin):
|
|
|
60
64
|
self._main_pane: MainPane | None = None
|
|
61
65
|
self._filter_input: FilterInput | None = None
|
|
62
66
|
self._app_footer: AppFooter | None = None
|
|
67
|
+
self._level_selector: LevelSelector | None = None
|
|
63
68
|
self._last_focused_panel: TypePanel | CombinedPanel | None = None
|
|
69
|
+
self._pending_customization: Customization | None = None
|
|
64
70
|
|
|
65
71
|
def compose(self) -> ComposeResult:
|
|
66
72
|
"""Compose the application layout."""
|
|
@@ -105,6 +111,9 @@ class LazyOpenCode(App, NavigationMixin, FilteringMixin, HelpMixin):
|
|
|
105
111
|
self._filter_input = FilterInput(id="filter-input")
|
|
106
112
|
yield self._filter_input
|
|
107
113
|
|
|
114
|
+
self._level_selector = LevelSelector(id="level-selector")
|
|
115
|
+
yield self._level_selector
|
|
116
|
+
|
|
108
117
|
self._app_footer = AppFooter(id="app-footer")
|
|
109
118
|
yield self._app_footer
|
|
110
119
|
|
|
@@ -153,7 +162,12 @@ class LazyOpenCode(App, NavigationMixin, FilteringMixin, HelpMixin):
|
|
|
153
162
|
"""Get customizations filtered by current level and search query."""
|
|
154
163
|
result = self._customizations
|
|
155
164
|
if self._level_filter:
|
|
156
|
-
|
|
165
|
+
# When filtering by level, only show OpenCode items (exclude Claude Code)
|
|
166
|
+
result = [
|
|
167
|
+
c
|
|
168
|
+
for c in result
|
|
169
|
+
if c.level == self._level_filter and c.source == ConfigSource.OPENCODE
|
|
170
|
+
]
|
|
157
171
|
if self._search_query:
|
|
158
172
|
query = self._search_query.lower()
|
|
159
173
|
result = [c for c in result if query in c.name.lower()]
|
|
@@ -296,14 +310,130 @@ class LazyOpenCode(App, NavigationMixin, FilteringMixin, HelpMixin):
|
|
|
296
310
|
except Exception as e:
|
|
297
311
|
self.notify(f"Error opening editor: {e}", severity="error")
|
|
298
312
|
|
|
313
|
+
# Copy actions
|
|
314
|
+
|
|
315
|
+
def action_copy_customization(self) -> None:
|
|
316
|
+
"""Copy selected customization to another level."""
|
|
317
|
+
panel = self._get_focused_panel()
|
|
318
|
+
customization = None
|
|
319
|
+
|
|
320
|
+
if panel:
|
|
321
|
+
customization = panel.selected_customization
|
|
322
|
+
|
|
323
|
+
if not customization:
|
|
324
|
+
self.notify("No customization selected", severity="warning")
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
# Only allow copying commands, agents, and skills
|
|
328
|
+
copyable_types = (
|
|
329
|
+
CustomizationType.COMMAND,
|
|
330
|
+
CustomizationType.AGENT,
|
|
331
|
+
CustomizationType.SKILL,
|
|
332
|
+
)
|
|
333
|
+
if customization.type not in copyable_types:
|
|
334
|
+
self.notify(
|
|
335
|
+
f"Cannot copy {customization.type_label} customizations",
|
|
336
|
+
severity="warning",
|
|
337
|
+
)
|
|
338
|
+
return
|
|
339
|
+
|
|
340
|
+
available = customization.get_copy_targets()
|
|
341
|
+
if not available:
|
|
342
|
+
self.notify("No available target levels", severity="warning")
|
|
343
|
+
return
|
|
344
|
+
|
|
345
|
+
self._pending_customization = customization
|
|
346
|
+
self._last_focused_panel = panel
|
|
347
|
+
if self._level_selector:
|
|
348
|
+
self._level_selector.show(available, customization.name)
|
|
349
|
+
|
|
350
|
+
def action_copy_path_to_clipboard(self) -> None:
|
|
351
|
+
"""Copy path of selected customization to clipboard."""
|
|
352
|
+
panel = self._get_focused_panel()
|
|
353
|
+
customization = None
|
|
354
|
+
|
|
355
|
+
if panel:
|
|
356
|
+
customization = panel.selected_customization
|
|
357
|
+
|
|
358
|
+
if not customization:
|
|
359
|
+
self.notify("No customization selected", severity="warning")
|
|
360
|
+
return
|
|
361
|
+
|
|
362
|
+
file_path = customization.path
|
|
363
|
+
if customization.type == CustomizationType.SKILL:
|
|
364
|
+
file_path = customization.path.parent
|
|
365
|
+
|
|
366
|
+
try:
|
|
367
|
+
import pyperclip
|
|
368
|
+
|
|
369
|
+
pyperclip.copy(str(file_path))
|
|
370
|
+
self.notify(f"Copied: {file_path}", severity="information")
|
|
371
|
+
except ImportError:
|
|
372
|
+
self.notify(
|
|
373
|
+
"pyperclip not installed. Run: pip install pyperclip",
|
|
374
|
+
severity="error",
|
|
375
|
+
)
|
|
376
|
+
except Exception as e:
|
|
377
|
+
self.notify(f"Failed to copy to clipboard: {e}", severity="error")
|
|
378
|
+
|
|
379
|
+
# Level selector message handlers
|
|
380
|
+
|
|
381
|
+
def on_level_selector_level_selected(
|
|
382
|
+
self, message: LevelSelector.LevelSelected
|
|
383
|
+
) -> None:
|
|
384
|
+
"""Handle level selection from the level selector."""
|
|
385
|
+
if self._pending_customization:
|
|
386
|
+
self._handle_copy(self._pending_customization, message.level)
|
|
387
|
+
self._pending_customization = None
|
|
388
|
+
self._restore_focus_after_selector()
|
|
389
|
+
|
|
390
|
+
def on_level_selector_selection_cancelled(
|
|
391
|
+
self,
|
|
392
|
+
message: LevelSelector.SelectionCancelled, # noqa: ARG002
|
|
393
|
+
) -> None:
|
|
394
|
+
"""Handle level selector cancellation."""
|
|
395
|
+
self._pending_customization = None
|
|
396
|
+
self._restore_focus_after_selector()
|
|
397
|
+
|
|
398
|
+
def _handle_copy(
|
|
399
|
+
self, customization: Customization, target_level: ConfigLevel
|
|
400
|
+
) -> None:
|
|
401
|
+
"""Handle copy operation."""
|
|
402
|
+
from lazyopencode.services.writer import CustomizationWriter
|
|
403
|
+
|
|
404
|
+
writer = CustomizationWriter(
|
|
405
|
+
global_config_path=self._discovery_service.global_config_path,
|
|
406
|
+
project_config_path=self._discovery_service.project_config_path,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
success, msg = writer.copy_customization(customization, target_level)
|
|
410
|
+
|
|
411
|
+
if success:
|
|
412
|
+
self.notify(msg, severity="information")
|
|
413
|
+
self.action_refresh()
|
|
414
|
+
else:
|
|
415
|
+
self.notify(msg, severity="error")
|
|
416
|
+
|
|
417
|
+
def _restore_focus_after_selector(self) -> None:
|
|
418
|
+
"""Restore focus to the previously focused panel."""
|
|
419
|
+
if self._last_focused_panel:
|
|
420
|
+
self._last_focused_panel.focus()
|
|
421
|
+
elif self._panels:
|
|
422
|
+
self._panels[0].focus()
|
|
423
|
+
|
|
299
424
|
|
|
300
425
|
def create_app(
|
|
301
426
|
project_root: Path | None = None,
|
|
302
427
|
global_config_path: Path | None = None,
|
|
428
|
+
enable_claude_code: bool = False,
|
|
303
429
|
) -> LazyOpenCode:
|
|
304
430
|
"""Create application with all dependencies wired."""
|
|
305
431
|
discovery_service = ConfigDiscoveryService(
|
|
306
432
|
project_root=project_root,
|
|
307
433
|
global_config_path=global_config_path,
|
|
434
|
+
enable_claude_code=enable_claude_code,
|
|
435
|
+
)
|
|
436
|
+
return LazyOpenCode(
|
|
437
|
+
discovery_service=discovery_service,
|
|
438
|
+
enable_claude_code=enable_claude_code,
|
|
308
439
|
)
|
|
309
|
-
return LazyOpenCode(discovery_service=discovery_service)
|
|
@@ -7,6 +7,8 @@ APP_BINDINGS: list[BindingType] = [
|
|
|
7
7
|
Binding("?", "toggle_help", "Help"),
|
|
8
8
|
Binding("r", "refresh", "Refresh"),
|
|
9
9
|
Binding("e", "open_in_editor", "Edit"),
|
|
10
|
+
Binding("c", "copy_customization", "Copy"),
|
|
11
|
+
Binding("C", "copy_path_to_clipboard", "Copy Path", key_display="shift+c"),
|
|
10
12
|
Binding("ctrl+u", "open_user_config", "User Config"),
|
|
11
13
|
Binding("tab", "focus_next_panel", "Next Panel", show=False),
|
|
12
14
|
Binding("shift+tab", "focus_previous_panel", "Prev Panel", show=False),
|
|
@@ -43,6 +43,13 @@ class ConfigLevel(Enum):
|
|
|
43
43
|
return "G" if self == ConfigLevel.GLOBAL else "P"
|
|
44
44
|
|
|
45
45
|
|
|
46
|
+
class ConfigSource(Enum):
|
|
47
|
+
"""Source of customization configuration."""
|
|
48
|
+
|
|
49
|
+
OPENCODE = "opencode" # Native OpenCode customizations
|
|
50
|
+
CLAUDE_CODE = "claude" # Imported from Claude Code paths
|
|
51
|
+
|
|
52
|
+
|
|
46
53
|
class CustomizationType(Enum):
|
|
47
54
|
"""Type of OpenCode customization."""
|
|
48
55
|
|
|
@@ -93,6 +100,8 @@ class Customization:
|
|
|
93
100
|
content: str | None = None
|
|
94
101
|
metadata: dict[str, Any] = field(default_factory=dict)
|
|
95
102
|
error: str | None = None
|
|
103
|
+
source: ConfigSource = ConfigSource.OPENCODE
|
|
104
|
+
source_level: str | None = None # "user", "project", "plugin" for Claude Code
|
|
96
105
|
|
|
97
106
|
@property
|
|
98
107
|
def type_label(self) -> str:
|
|
@@ -105,16 +114,38 @@ class Customization:
|
|
|
105
114
|
return self.level.label
|
|
106
115
|
|
|
107
116
|
@property
|
|
108
|
-
def level_icon(self) -> str:
|
|
109
|
-
"""Level icon for compact display."""
|
|
110
|
-
|
|
117
|
+
def level_icon(self) -> str | None:
|
|
118
|
+
"""Level icon for compact display. Returns None for OpenCode items."""
|
|
119
|
+
if self.source == ConfigSource.CLAUDE_CODE:
|
|
120
|
+
return "👾"
|
|
121
|
+
return None
|
|
111
122
|
|
|
112
123
|
@property
|
|
113
124
|
def display_name(self) -> str:
|
|
114
125
|
"""Formatted name for display with level indicator."""
|
|
115
|
-
|
|
126
|
+
icon = self.level_icon
|
|
127
|
+
parts = []
|
|
128
|
+
if icon:
|
|
129
|
+
parts.append(f"[{icon}]")
|
|
130
|
+
parts.append(self.name)
|
|
131
|
+
|
|
132
|
+
# Add dimmed plugin name for plugin-sourced items
|
|
133
|
+
if self.source_level and self.source_level.startswith("plugin:"):
|
|
134
|
+
plugin_name = self.source_level.replace("plugin:", "")
|
|
135
|
+
parts.append(f"[dim]{plugin_name}[/dim]")
|
|
136
|
+
|
|
137
|
+
return " ".join(parts)
|
|
116
138
|
|
|
117
139
|
@property
|
|
118
140
|
def has_error(self) -> bool:
|
|
119
141
|
"""Check if this customization has a parse error."""
|
|
120
142
|
return self.error is not None
|
|
143
|
+
|
|
144
|
+
def get_copy_targets(self) -> list[ConfigLevel]:
|
|
145
|
+
"""Get valid copy target levels for this item."""
|
|
146
|
+
if self.source == ConfigSource.CLAUDE_CODE:
|
|
147
|
+
return [ConfigLevel.GLOBAL, ConfigLevel.PROJECT]
|
|
148
|
+
elif self.level == ConfigLevel.GLOBAL:
|
|
149
|
+
return [ConfigLevel.PROJECT]
|
|
150
|
+
else:
|
|
151
|
+
return [ConfigLevel.GLOBAL]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Claude Code integration layer for LazyOpenCode.
|
|
2
|
+
|
|
3
|
+
This module provides discovery of Claude Code customizations from standard
|
|
4
|
+
Claude Code paths (~/.claude/, ./.claude/, plugins).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from lazyopencode.services.claude_code.discovery import ClaudeCodeDiscoveryService
|
|
8
|
+
|
|
9
|
+
__all__ = ["ClaudeCodeDiscoveryService"]
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Service for discovering Claude Code customizations."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from lazyopencode.models.customization import (
|
|
6
|
+
ConfigLevel,
|
|
7
|
+
Customization,
|
|
8
|
+
)
|
|
9
|
+
from lazyopencode.services.claude_code.parsers.agent import AgentParser
|
|
10
|
+
from lazyopencode.services.claude_code.parsers.command import CommandParser
|
|
11
|
+
from lazyopencode.services.claude_code.parsers.skill import SkillParser
|
|
12
|
+
from lazyopencode.services.claude_code.plugin_loader import PluginLoader
|
|
13
|
+
from lazyopencode.services.gitignore_filter import GitignoreFilter
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ClaudeCodeDiscoveryService:
|
|
17
|
+
"""Discovers Claude Code customizations from standard Claude Code paths.
|
|
18
|
+
|
|
19
|
+
This is a separate layer responsible for Claude Code integration.
|
|
20
|
+
|
|
21
|
+
Discovery paths:
|
|
22
|
+
- User: ~/.claude/
|
|
23
|
+
- Project: ./.claude/
|
|
24
|
+
- Plugins: ~/.claude/plugins/ (via plugin registry)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, project_root: Path) -> None:
|
|
28
|
+
self.project_root = project_root
|
|
29
|
+
self.user_path = Path.home() / ".claude"
|
|
30
|
+
self.plugins_path = self.user_path / "plugins"
|
|
31
|
+
self._gitignore_filter = GitignoreFilter(project_root=project_root)
|
|
32
|
+
self._plugin_loader = PluginLoader(
|
|
33
|
+
user_config_path=self.user_path,
|
|
34
|
+
project_root=project_root,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def discover_all(self) -> list[Customization]:
|
|
38
|
+
"""Discover all Claude Code customizations."""
|
|
39
|
+
customizations: list[Customization] = []
|
|
40
|
+
|
|
41
|
+
# User-level
|
|
42
|
+
customizations.extend(self._discover_from_path(self.user_path, "user"))
|
|
43
|
+
|
|
44
|
+
# Project-level
|
|
45
|
+
project_claude = self.project_root / ".claude"
|
|
46
|
+
customizations.extend(self._discover_from_path(project_claude, "project"))
|
|
47
|
+
|
|
48
|
+
# Plugin-level (using plugin registry)
|
|
49
|
+
customizations.extend(self._discover_from_plugins())
|
|
50
|
+
|
|
51
|
+
return customizations
|
|
52
|
+
|
|
53
|
+
def _discover_from_path(self, base: Path, source_level: str) -> list[Customization]:
|
|
54
|
+
"""Discover commands, agents, skills from a Claude Code path."""
|
|
55
|
+
if not base.exists():
|
|
56
|
+
return []
|
|
57
|
+
|
|
58
|
+
customizations: list[Customization] = []
|
|
59
|
+
level = ConfigLevel.GLOBAL if source_level == "user" else ConfigLevel.PROJECT
|
|
60
|
+
|
|
61
|
+
# Commands: base/commands/**/*.md
|
|
62
|
+
commands_path = base / "commands"
|
|
63
|
+
customizations.extend(
|
|
64
|
+
self._discover_commands(commands_path, level, source_level)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Agents: base/agents/*.md
|
|
68
|
+
agents_path = base / "agents"
|
|
69
|
+
customizations.extend(self._discover_agents(agents_path, level, source_level))
|
|
70
|
+
|
|
71
|
+
# Skills: base/skills/*/SKILL.md
|
|
72
|
+
skills_path = base / "skills"
|
|
73
|
+
customizations.extend(self._discover_skills(skills_path, level, source_level))
|
|
74
|
+
|
|
75
|
+
return customizations
|
|
76
|
+
|
|
77
|
+
def _discover_commands(
|
|
78
|
+
self, commands_path: Path, level: ConfigLevel, source_level: str
|
|
79
|
+
) -> list[Customization]:
|
|
80
|
+
"""Discover command customizations from commands directory."""
|
|
81
|
+
if not commands_path.exists():
|
|
82
|
+
return []
|
|
83
|
+
|
|
84
|
+
customizations: list[Customization] = []
|
|
85
|
+
parser = CommandParser(commands_path)
|
|
86
|
+
|
|
87
|
+
# Use rglob for nested commands (commands/**/*.md)
|
|
88
|
+
for md_file in commands_path.rglob("*.md"):
|
|
89
|
+
if parser.can_parse(md_file):
|
|
90
|
+
customizations.append(parser.parse(md_file, level, source_level))
|
|
91
|
+
|
|
92
|
+
return customizations
|
|
93
|
+
|
|
94
|
+
def _discover_agents(
|
|
95
|
+
self, agents_path: Path, level: ConfigLevel, source_level: str
|
|
96
|
+
) -> list[Customization]:
|
|
97
|
+
"""Discover agent customizations from agents directory."""
|
|
98
|
+
if not agents_path.exists():
|
|
99
|
+
return []
|
|
100
|
+
|
|
101
|
+
customizations: list[Customization] = []
|
|
102
|
+
parser = AgentParser(agents_path)
|
|
103
|
+
|
|
104
|
+
for md_file in agents_path.glob("*.md"):
|
|
105
|
+
if parser.can_parse(md_file):
|
|
106
|
+
customizations.append(parser.parse(md_file, level, source_level))
|
|
107
|
+
|
|
108
|
+
return customizations
|
|
109
|
+
|
|
110
|
+
def _discover_skills(
|
|
111
|
+
self, skills_path: Path, level: ConfigLevel, source_level: str
|
|
112
|
+
) -> list[Customization]:
|
|
113
|
+
"""Discover skill customizations from skills directory."""
|
|
114
|
+
if not skills_path.exists():
|
|
115
|
+
return []
|
|
116
|
+
|
|
117
|
+
customizations: list[Customization] = []
|
|
118
|
+
parser = SkillParser(skills_path, gitignore_filter=self._gitignore_filter)
|
|
119
|
+
|
|
120
|
+
for skill_dir in skills_path.iterdir():
|
|
121
|
+
if skill_dir.is_dir():
|
|
122
|
+
skill_file = skill_dir / "SKILL.md"
|
|
123
|
+
if skill_file.exists() and parser.can_parse(skill_file):
|
|
124
|
+
customizations.append(parser.parse(skill_file, level, source_level))
|
|
125
|
+
|
|
126
|
+
return customizations
|
|
127
|
+
|
|
128
|
+
def _discover_from_plugins(self) -> list[Customization]:
|
|
129
|
+
"""Discover customizations from installed Claude Code plugins."""
|
|
130
|
+
customizations: list[Customization] = []
|
|
131
|
+
|
|
132
|
+
for plugin_info in self._plugin_loader.get_all_plugins():
|
|
133
|
+
install_path = plugin_info.install_path
|
|
134
|
+
source_level = f"plugin:{plugin_info.short_name}"
|
|
135
|
+
|
|
136
|
+
# Commands
|
|
137
|
+
commands_path = install_path / "commands"
|
|
138
|
+
customizations.extend(
|
|
139
|
+
self._discover_commands(commands_path, ConfigLevel.GLOBAL, source_level)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Agents
|
|
143
|
+
agents_path = install_path / "agents"
|
|
144
|
+
customizations.extend(
|
|
145
|
+
self._discover_agents(agents_path, ConfigLevel.GLOBAL, source_level)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Skills
|
|
149
|
+
skills_path = install_path / "skills"
|
|
150
|
+
customizations.extend(
|
|
151
|
+
self._discover_skills(skills_path, ConfigLevel.GLOBAL, source_level)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
return customizations
|
|
155
|
+
|
|
156
|
+
def refresh(self) -> None:
|
|
157
|
+
"""Clear cached plugin registry to force reload."""
|
|
158
|
+
self._plugin_loader.refresh()
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Parsers for Claude Code customizations."""
|
|
2
|
+
|
|
3
|
+
from lazyopencode.services.claude_code.parsers.agent import AgentParser
|
|
4
|
+
from lazyopencode.services.claude_code.parsers.command import CommandParser
|
|
5
|
+
from lazyopencode.services.claude_code.parsers.skill import SkillParser
|
|
6
|
+
|
|
7
|
+
__all__ = ["CommandParser", "AgentParser", "SkillParser"]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Parser for Claude Code subagent customizations."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from lazyopencode.models.customization import (
|
|
6
|
+
ConfigLevel,
|
|
7
|
+
ConfigSource,
|
|
8
|
+
Customization,
|
|
9
|
+
CustomizationType,
|
|
10
|
+
)
|
|
11
|
+
from lazyopencode.services.parsers import parse_frontmatter
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AgentParser:
|
|
15
|
+
"""Parser for subagent markdown files.
|
|
16
|
+
|
|
17
|
+
File pattern: agents/*.md
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, agents_dir: Path) -> None:
|
|
21
|
+
"""Initialize with the agents directory path."""
|
|
22
|
+
self.agents_dir = agents_dir
|
|
23
|
+
|
|
24
|
+
def can_parse(self, path: Path) -> bool:
|
|
25
|
+
"""Check if path is a markdown file in agents directory."""
|
|
26
|
+
return path.suffix == ".md" and path.parent == self.agents_dir
|
|
27
|
+
|
|
28
|
+
def parse(self, path: Path, level: ConfigLevel, source_level: str) -> Customization:
|
|
29
|
+
"""Parse a subagent markdown file."""
|
|
30
|
+
try:
|
|
31
|
+
content = path.read_text(encoding="utf-8")
|
|
32
|
+
except OSError as e:
|
|
33
|
+
return Customization(
|
|
34
|
+
name=path.stem,
|
|
35
|
+
type=CustomizationType.AGENT,
|
|
36
|
+
level=level,
|
|
37
|
+
path=path,
|
|
38
|
+
error=f"Failed to read file: {e}",
|
|
39
|
+
source=ConfigSource.CLAUDE_CODE,
|
|
40
|
+
source_level=source_level,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
frontmatter, _ = parse_frontmatter(content)
|
|
44
|
+
|
|
45
|
+
name = frontmatter.get("name", path.stem)
|
|
46
|
+
description = frontmatter.get("description")
|
|
47
|
+
|
|
48
|
+
return Customization(
|
|
49
|
+
name=name,
|
|
50
|
+
type=CustomizationType.AGENT,
|
|
51
|
+
level=level,
|
|
52
|
+
path=path,
|
|
53
|
+
description=description,
|
|
54
|
+
content=content,
|
|
55
|
+
metadata=frontmatter,
|
|
56
|
+
source=ConfigSource.CLAUDE_CODE,
|
|
57
|
+
source_level=source_level,
|
|
58
|
+
)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Parser for Claude Code slash command customizations."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from lazyopencode.models.customization import (
|
|
6
|
+
ConfigLevel,
|
|
7
|
+
ConfigSource,
|
|
8
|
+
Customization,
|
|
9
|
+
CustomizationType,
|
|
10
|
+
)
|
|
11
|
+
from lazyopencode.services.parsers import parse_frontmatter
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CommandParser:
|
|
15
|
+
"""Parser for slash command markdown files.
|
|
16
|
+
|
|
17
|
+
File pattern: commands/**/*.md
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, commands_dir: Path) -> None:
|
|
21
|
+
"""Initialize with the commands directory path."""
|
|
22
|
+
self.commands_dir = commands_dir
|
|
23
|
+
|
|
24
|
+
def can_parse(self, path: Path) -> bool:
|
|
25
|
+
"""Check if path is a markdown file in commands directory."""
|
|
26
|
+
return path.suffix == ".md" and self.commands_dir in path.parents
|
|
27
|
+
|
|
28
|
+
def parse(self, path: Path, level: ConfigLevel, source_level: str) -> Customization:
|
|
29
|
+
"""Parse a slash command markdown file."""
|
|
30
|
+
try:
|
|
31
|
+
content = path.read_text(encoding="utf-8")
|
|
32
|
+
except OSError as e:
|
|
33
|
+
return Customization(
|
|
34
|
+
name=self._derive_name(path),
|
|
35
|
+
type=CustomizationType.COMMAND,
|
|
36
|
+
level=level,
|
|
37
|
+
path=path,
|
|
38
|
+
error=f"Failed to read file: {e}",
|
|
39
|
+
source=ConfigSource.CLAUDE_CODE,
|
|
40
|
+
source_level=source_level,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
frontmatter, body = parse_frontmatter(content)
|
|
44
|
+
|
|
45
|
+
description = frontmatter.get("description")
|
|
46
|
+
if not description and body.strip():
|
|
47
|
+
first_line = body.strip().split("\n")[0]
|
|
48
|
+
if not first_line.startswith("#"):
|
|
49
|
+
description = first_line[:100]
|
|
50
|
+
|
|
51
|
+
return Customization(
|
|
52
|
+
name=self._derive_name(path),
|
|
53
|
+
type=CustomizationType.COMMAND,
|
|
54
|
+
level=level,
|
|
55
|
+
path=path,
|
|
56
|
+
description=description,
|
|
57
|
+
content=content,
|
|
58
|
+
metadata=frontmatter,
|
|
59
|
+
source=ConfigSource.CLAUDE_CODE,
|
|
60
|
+
source_level=source_level,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def _derive_name(self, path: Path) -> str:
|
|
64
|
+
"""Derive command name from file path.
|
|
65
|
+
|
|
66
|
+
For nested paths: dir/file.md -> dir:file
|
|
67
|
+
For simple paths: file.md -> file
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
relative = path.relative_to(self.commands_dir)
|
|
71
|
+
parts = list(relative.parts)
|
|
72
|
+
parts[-1] = parts[-1].removesuffix(".md")
|
|
73
|
+
return ":".join(parts)
|
|
74
|
+
except ValueError:
|
|
75
|
+
return path.stem
|