code-agnostic 0.3.5__tar.gz → 0.3.7__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.
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/PKG-INFO +53 -20
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/README.md +52 -19
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/__init__.py +1 -1
- code_agnostic-0.3.7/code_agnostic/agents/claude.py +67 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/agents/compilers.py +8 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/agents/parser.py +1 -1
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/app_id.py +14 -0
- code_agnostic-0.3.7/code_agnostic/apps/claude/__init__.py +1 -0
- code_agnostic-0.3.7/code_agnostic/apps/claude/config_repository.py +59 -0
- code_agnostic-0.3.7/code_agnostic/apps/claude/mapper.py +86 -0
- code_agnostic-0.3.7/code_agnostic/apps/claude/service.py +248 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/codex/schema.json +12 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/common/compiled_planning.py +57 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/common/loader.py +1 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/constants.py +3 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/executor.py +46 -2
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/git_exclude_service.py +29 -3
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/imports/adapters.py +17 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/imports/service.py +10 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/lossiness.py +65 -7
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/models.py +11 -2
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/planner.py +117 -2
- code_agnostic-0.3.7/code_agnostic/skills/compilers.py +107 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/skills/parser.py +1 -1
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/spec/loaders.py +2 -2
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/spec/schemas/agent.v1.schema.json +2 -1
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/spec/schemas/rule.v1.schema.json +2 -1
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/spec/schemas/skill.v1.schema.json +2 -1
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/status.py +29 -1
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic.egg-info/PKG-INFO +53 -20
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic.egg-info/SOURCES.txt +5 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/pyproject.toml +1 -1
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_cli_apply_apps.py +1 -1
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_cli_explain_lossiness.py +37 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_cli_workspaces.py +6 -5
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_common_mcp_to_dto.py +16 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_git_exclude_service.py +25 -2
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_sync_plan_model.py +23 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_transactional_executor.py +102 -3
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_version.py +5 -1
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_workspace_config_sync.py +276 -1
- code_agnostic-0.3.5/code_agnostic/skills/compilers.py +0 -75
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/LICENSE +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/__main__.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/agents/__init__.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/agents/codex.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/agents/models.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/agents/opencode.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/__init__.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/apps_service.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/codex/__init__.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/codex/config_repository.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/codex/mapper.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/codex/schema_repository.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/codex/service.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/common/__init__.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/common/framework.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/common/interfaces/__init__.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/common/interfaces/mapper.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/common/interfaces/repositories.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/common/interfaces/service.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/common/models.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/common/schema.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/common/symlink_planning.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/common/utils.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/cursor/__init__.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/cursor/config_repository.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/cursor/mapper.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/cursor/schema.json +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/cursor/schema_repository.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/cursor/service.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/opencode/__init__.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/opencode/config_repository.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/opencode/mapper.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/opencode/schema.json +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/opencode/schema_repository.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/apps/opencode/service.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/cli/__init__.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/cli/aliases.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/cli/commands/__init__.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/cli/commands/agents.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/cli/commands/apply.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/cli/commands/apps.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/cli/commands/explain_lossiness.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/cli/commands/import_.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/cli/commands/mcp.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/cli/commands/plan.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/cli/commands/restore.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/cli/commands/rules.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/cli/commands/skills.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/cli/commands/status.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/cli/commands/validate.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/cli/commands/workspaces.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/cli/helpers.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/cli/options.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/core/__init__.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/core/repository.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/core/workspace_repository.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/errors.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/imports/__init__.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/imports/filesystem.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/imports/models.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/mcp_service.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/rules/__init__.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/rules/compilers.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/rules/models.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/rules/parser.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/rules/repository.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/skills/__init__.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/skills/models.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/spec/__init__.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/spec/schemas/mcp.base.schema.json +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/spec/schemas/mcp.v1.schema.json +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/tui/__init__.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/tui/enums.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/tui/import_selector.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/tui/renderers.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/tui/sections.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/tui/tables.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/utils.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/validation.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic/workspaces.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic.egg-info/dependency_links.txt +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic.egg-info/entry_points.txt +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic.egg-info/requires.txt +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/code_agnostic.egg-info/top_level.txt +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/setup.cfg +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_cli_agents.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_cli_aliases.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_cli_apply_codex.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_cli_apply_cursor.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_cli_apply_target.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_cli_apps.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_cli_flags.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_cli_git_exclude.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_cli_import.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_cli_import_interactive.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_cli_mcp.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_cli_module_organization.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_cli_plan.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_cli_restore.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_cli_rules.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_cli_skills.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_cli_status.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_cli_validate.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_cli_workspace_resolution.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_common_repository.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_compiled_planning.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_dto_to_common_mcp.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_mcp_service.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_planner_executor.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_planner_rules.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_symlink_planning.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_utils.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_workspace_repo_status.py +0 -0
- {code_agnostic-0.3.5 → code_agnostic-0.3.7}/tests/test_workspaces.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-agnostic
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.7
|
|
4
4
|
Summary: Centralized hub for LLM coding config: MCP, skills, rules, and agents.
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -53,9 +53,10 @@ AI coding tools each want config in a different place and format. When you use m
|
|
|
53
53
|
~/.config/opencode/ Compiled & synced for OpenCode
|
|
54
54
|
~/.cursor/ Compiled & synced for Cursor
|
|
55
55
|
~/.codex/ Compiled & synced for Codex
|
|
56
|
+
~/.claude.json and ~/.claude/ Compiled & synced for Claude Code
|
|
56
57
|
```
|
|
57
58
|
|
|
58
|
-
Each resource is cross-compiled to the target editor's native format. Rules become `.mdc` files for Cursor, `AGENTS.md` sections for OpenCode/Codex,
|
|
59
|
+
Each resource is cross-compiled to the target editor's native format. Rules become `.mdc` files for Cursor, `AGENTS.md` sections for OpenCode/Codex, and `CLAUDE.local.md` memory for Claude Code.
|
|
59
60
|
|
|
60
61
|
Legacy single-file rules, `skills/<name>/SKILL.md`, and markdown agents are still supported for migration, but bundle directories are the preferred source format for new config.
|
|
61
62
|
|
|
@@ -94,6 +95,7 @@ code-agnostic import apply -a codex
|
|
|
94
95
|
# Enable target editors
|
|
95
96
|
code-agnostic apps enable -a cursor
|
|
96
97
|
code-agnostic apps enable -a opencode
|
|
98
|
+
code-agnostic apps enable -a claude
|
|
97
99
|
|
|
98
100
|
# Preview and apply
|
|
99
101
|
code-agnostic validate
|
|
@@ -103,23 +105,23 @@ code-agnostic apply
|
|
|
103
105
|
|
|
104
106
|
## Editor compatibility
|
|
105
107
|
|
|
106
|
-
| Feature | OpenCode | Cursor | Codex |
|
|
107
|
-
|
|
108
|
-
| MCP sync | yes | yes | yes |
|
|
109
|
-
| Rules sync (cross-compiled) | yes | yes | yes |
|
|
110
|
-
| Skills sync | yes | yes | yes |
|
|
111
|
-
| Agents sync | yes | yes | yes |
|
|
112
|
-
| Workspace root `AGENTS.md` link | yes | yes | yes |
|
|
113
|
-
| Native repo config include for workspace `AGENTS.md` | yes | -- | -- |
|
|
114
|
-
| Repo/subdir gets shared workspace
|
|
115
|
-
| Nested `AGENTS.md` discovery | -- | yes | yes |
|
|
116
|
-
| Workspace propagation | yes | -- | yes |
|
|
117
|
-
| Import from | yes | yes | yes |
|
|
118
|
-
| Interactive import (TUI) | yes | yes | yes |
|
|
108
|
+
| Feature | OpenCode | Cursor | Codex | Claude Code |
|
|
109
|
+
|---------|:--------:|:------:|:-----:|:-----------:|
|
|
110
|
+
| MCP sync | yes | yes | yes | yes |
|
|
111
|
+
| Rules sync (cross-compiled) | yes | yes | yes | yes |
|
|
112
|
+
| Skills sync | yes | yes | yes | yes |
|
|
113
|
+
| Agents sync | yes | yes | yes | yes |
|
|
114
|
+
| Workspace root `AGENTS.md` link | yes | yes | yes | yes |
|
|
115
|
+
| Native repo config include for workspace `AGENTS.md` | yes | -- | -- | -- |
|
|
116
|
+
| Repo/subdir gets shared workspace instructions today | yes | -- | yes | yes |
|
|
117
|
+
| Nested `AGENTS.md` discovery | -- | yes | yes | -- |
|
|
118
|
+
| Workspace propagation | yes | -- | yes | yes |
|
|
119
|
+
| Import from | yes | yes | yes | yes |
|
|
120
|
+
| Interactive import (TUI) | yes | yes | yes | yes |
|
|
119
121
|
|
|
120
122
|
Cursor workspace propagation is intentionally disabled to avoid duplicate MCP initialization in multi-root workspaces: https://forum.cursor.com/t/mcp-multi-root-workspace-causes-duplicate-mcp-server-initialization-4x-createclient-actions/144003
|
|
121
123
|
|
|
122
|
-
OpenCode workspace configs include the shared workspace `AGENTS.md` natively via `instructions`, so repos under the workspace get both repo-local and shared workspace instructions. Codex repos receive workspace instructions through a generated `AGENTS.override.md`, which is added to each repo's `.git/info/exclude`.
|
|
124
|
+
OpenCode workspace configs include the shared workspace `AGENTS.md` natively via `instructions`, so repos under the workspace get both repo-local and shared workspace instructions. Codex repos receive workspace instructions through a generated `AGENTS.override.md`, which is added to each repo's `.git/info/exclude`. Claude Code receives workspace instructions through generated `CLAUDE.local.md` files, never by editing committed `CLAUDE.md`.
|
|
123
125
|
|
|
124
126
|
Cursor documents `AGENTS.md` support in project roots and subdirectories. `code-agnostic` still disables Cursor workspace propagation, so it does not copy or link the shared workspace `AGENTS.md` into child repos; Cursor will load repo-local or nested `AGENTS.md` files that already exist in the opened project. Codex documents nested `AGENTS.md` discovery, but not a native config include for an extra workspace file.
|
|
125
127
|
|
|
@@ -195,7 +197,9 @@ code-agnostic plan
|
|
|
195
197
|
code-agnostic apply
|
|
196
198
|
```
|
|
197
199
|
|
|
198
|
-
Global skills live under `~/.config/code-agnostic/skills`. Workspace-local skills live under `~/.config/code-agnostic/workspaces/<name>/skills` and can be inspected with `code-agnostic skills list -w <name>`. Codex generated skill outputs are written to `~/.agents/skills`, while Codex agents and config remain under `~/.codex`.
|
|
200
|
+
Global skills live under `~/.config/code-agnostic/skills`. Workspace-local skills live under `~/.config/code-agnostic/workspaces/<name>/skills` and can be inspected with `code-agnostic skills list -w <name>`. Codex generated skill outputs are written to `~/.agents/skills`, while Codex agents and config remain under `~/.codex`. Claude Code generated skills and agents are written under `~/.claude/skills` and `~/.claude/agents`, with workspace copies under repo-local `.claude/skills` and `.claude/agents`.
|
|
201
|
+
|
|
202
|
+
Project-local skills are not first-class source inputs in `code-agnostic` yet. If a target app discovers repo-local skill folders such as `.agents/skills`, `.opencode/skills`, or user-created `.claude/skills`, treat those as unmanaged app inputs. Workspace sync writes only the exact generated paths recorded in `.sync-state.json`.
|
|
199
203
|
|
|
200
204
|
Planned convenience command:
|
|
201
205
|
|
|
@@ -207,7 +211,7 @@ That command should copy the skill into the source of truth and then run the nor
|
|
|
207
211
|
|
|
208
212
|
### Workspaces
|
|
209
213
|
|
|
210
|
-
Register workspace directories. Workspace rules are compiled into a canonical `AGENTS.md` at the workspace root. Repos keep their own repo-specific `AGENTS.md`; Codex receives the workspace rules through generated, git-excluded `AGENTS.override.md` files, while OpenCode workspace configs reference the shared workspace file through `instructions`. Repo-local app config, skills, and agents are propagated for OpenCode and
|
|
214
|
+
Register workspace directories. Workspace rules are compiled into a canonical `AGENTS.md` at the workspace root. Repos keep their own repo-specific `AGENTS.md`; Codex receives the workspace rules through generated, git-excluded `AGENTS.override.md` files, while OpenCode workspace configs reference the shared workspace file through `instructions`. Claude receives generated `CLAUDE.local.md` files and project MCP entries in `~/.claude.json["projects"][absolute_repo_path]["mcpServers"]`. Repo-local app config, skills, and agents are propagated for OpenCode, Codex, and Claude.
|
|
211
215
|
|
|
212
216
|
`.cursor` workspace propagation is intentionally disabled to avoid duplicate MCP initialization when opening multi-root workspaces in Cursor (related issue: https://forum.cursor.com/t/mcp-multi-root-workspace-causes-duplicate-mcp-server-initialization-4x-createclient-actions/144003).
|
|
213
217
|
|
|
@@ -234,6 +238,7 @@ Migrate existing config from any supported editor into the hub.
|
|
|
234
238
|
```bash
|
|
235
239
|
code-agnostic import plan -a codex
|
|
236
240
|
code-agnostic import apply -a codex
|
|
241
|
+
code-agnostic import plan -a claude
|
|
237
242
|
code-agnostic import apply -a cursor --include mcp --on-conflict overwrite
|
|
238
243
|
code-agnostic import plan -a codex -i # interactive TUI picker
|
|
239
244
|
```
|
|
@@ -266,7 +271,8 @@ The compiler migration is documented in:
|
|
|
266
271
|
- [x] Cross-compilation for skills and agents
|
|
267
272
|
- [x] Per-workspace git-exclude customization
|
|
268
273
|
- [x] Interactive TUI for import selection
|
|
269
|
-
- [
|
|
274
|
+
- [x] Claude Code support
|
|
275
|
+
- [ ] Project-scoped skill installs and sync
|
|
270
276
|
- [ ] `rules add` / `skills add` / `agents add` commands (open `$EDITOR` with template)
|
|
271
277
|
- [ ] Planner integration for cross-compiled skills and agents
|
|
272
278
|
- [ ] Shell auto-complete
|
|
@@ -279,10 +285,37 @@ uv sync --dev
|
|
|
279
285
|
uv run pytest
|
|
280
286
|
```
|
|
281
287
|
|
|
288
|
+
Before pushing release-prep work, run the supported Python matrix locally. Tox
|
|
289
|
+
delegates each environment to `uv run --python`, so `uv` can provide the
|
|
290
|
+
requested interpreter when it is not already installed:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
uvx tox run -p auto
|
|
294
|
+
uvx tox run -e uv310 -- tests/test_version.py -q
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
For a hermetic Linux matrix that does not depend on locally installed Python
|
|
298
|
+
versions, run the Docker matrix:
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
./scripts/run-docker-matrix.sh
|
|
302
|
+
./scripts/run-docker-matrix.sh tests/test_version.py -q
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Limit the Docker matrix while iterating:
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
PYTHON_VERSIONS="3.10 3.14" ./scripts/run-docker-matrix.sh tests/test_version.py -q
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
The release gate still requires GitHub Actions to pass because the published
|
|
312
|
+
workflow is the source of truth for OS coverage across Ubuntu, macOS, and
|
|
313
|
+
Windows on every supported Python version.
|
|
314
|
+
|
|
282
315
|
Real app-ingestion E2E is gated because it requires installed target CLIs and
|
|
283
316
|
uses each tool's own introspection surface:
|
|
284
317
|
|
|
285
318
|
```bash
|
|
286
319
|
CODE_AGNOSTIC_REAL_APP_E2E=1 uv run pytest tests/e2e/test_real_app_ingestion_e2e.py -q
|
|
287
|
-
CODE_AGNOSTIC_REAL_APP_E2E=1 CODE_AGNOSTIC_REAL_APP_TARGETS=codex,opencode uv run pytest tests/e2e/test_real_app_ingestion_e2e.py -q
|
|
320
|
+
CODE_AGNOSTIC_REAL_APP_E2E=1 CODE_AGNOSTIC_REAL_APP_TARGETS=codex,opencode,claude uv run pytest tests/e2e/test_real_app_ingestion_e2e.py -q
|
|
288
321
|
```
|
|
@@ -30,9 +30,10 @@ AI coding tools each want config in a different place and format. When you use m
|
|
|
30
30
|
~/.config/opencode/ Compiled & synced for OpenCode
|
|
31
31
|
~/.cursor/ Compiled & synced for Cursor
|
|
32
32
|
~/.codex/ Compiled & synced for Codex
|
|
33
|
+
~/.claude.json and ~/.claude/ Compiled & synced for Claude Code
|
|
33
34
|
```
|
|
34
35
|
|
|
35
|
-
Each resource is cross-compiled to the target editor's native format. Rules become `.mdc` files for Cursor, `AGENTS.md` sections for OpenCode/Codex,
|
|
36
|
+
Each resource is cross-compiled to the target editor's native format. Rules become `.mdc` files for Cursor, `AGENTS.md` sections for OpenCode/Codex, and `CLAUDE.local.md` memory for Claude Code.
|
|
36
37
|
|
|
37
38
|
Legacy single-file rules, `skills/<name>/SKILL.md`, and markdown agents are still supported for migration, but bundle directories are the preferred source format for new config.
|
|
38
39
|
|
|
@@ -71,6 +72,7 @@ code-agnostic import apply -a codex
|
|
|
71
72
|
# Enable target editors
|
|
72
73
|
code-agnostic apps enable -a cursor
|
|
73
74
|
code-agnostic apps enable -a opencode
|
|
75
|
+
code-agnostic apps enable -a claude
|
|
74
76
|
|
|
75
77
|
# Preview and apply
|
|
76
78
|
code-agnostic validate
|
|
@@ -80,23 +82,23 @@ code-agnostic apply
|
|
|
80
82
|
|
|
81
83
|
## Editor compatibility
|
|
82
84
|
|
|
83
|
-
| Feature | OpenCode | Cursor | Codex |
|
|
84
|
-
|
|
85
|
-
| MCP sync | yes | yes | yes |
|
|
86
|
-
| Rules sync (cross-compiled) | yes | yes | yes |
|
|
87
|
-
| Skills sync | yes | yes | yes |
|
|
88
|
-
| Agents sync | yes | yes | yes |
|
|
89
|
-
| Workspace root `AGENTS.md` link | yes | yes | yes |
|
|
90
|
-
| Native repo config include for workspace `AGENTS.md` | yes | -- | -- |
|
|
91
|
-
| Repo/subdir gets shared workspace
|
|
92
|
-
| Nested `AGENTS.md` discovery | -- | yes | yes |
|
|
93
|
-
| Workspace propagation | yes | -- | yes |
|
|
94
|
-
| Import from | yes | yes | yes |
|
|
95
|
-
| Interactive import (TUI) | yes | yes | yes |
|
|
85
|
+
| Feature | OpenCode | Cursor | Codex | Claude Code |
|
|
86
|
+
|---------|:--------:|:------:|:-----:|:-----------:|
|
|
87
|
+
| MCP sync | yes | yes | yes | yes |
|
|
88
|
+
| Rules sync (cross-compiled) | yes | yes | yes | yes |
|
|
89
|
+
| Skills sync | yes | yes | yes | yes |
|
|
90
|
+
| Agents sync | yes | yes | yes | yes |
|
|
91
|
+
| Workspace root `AGENTS.md` link | yes | yes | yes | yes |
|
|
92
|
+
| Native repo config include for workspace `AGENTS.md` | yes | -- | -- | -- |
|
|
93
|
+
| Repo/subdir gets shared workspace instructions today | yes | -- | yes | yes |
|
|
94
|
+
| Nested `AGENTS.md` discovery | -- | yes | yes | -- |
|
|
95
|
+
| Workspace propagation | yes | -- | yes | yes |
|
|
96
|
+
| Import from | yes | yes | yes | yes |
|
|
97
|
+
| Interactive import (TUI) | yes | yes | yes | yes |
|
|
96
98
|
|
|
97
99
|
Cursor workspace propagation is intentionally disabled to avoid duplicate MCP initialization in multi-root workspaces: https://forum.cursor.com/t/mcp-multi-root-workspace-causes-duplicate-mcp-server-initialization-4x-createclient-actions/144003
|
|
98
100
|
|
|
99
|
-
OpenCode workspace configs include the shared workspace `AGENTS.md` natively via `instructions`, so repos under the workspace get both repo-local and shared workspace instructions. Codex repos receive workspace instructions through a generated `AGENTS.override.md`, which is added to each repo's `.git/info/exclude`.
|
|
101
|
+
OpenCode workspace configs include the shared workspace `AGENTS.md` natively via `instructions`, so repos under the workspace get both repo-local and shared workspace instructions. Codex repos receive workspace instructions through a generated `AGENTS.override.md`, which is added to each repo's `.git/info/exclude`. Claude Code receives workspace instructions through generated `CLAUDE.local.md` files, never by editing committed `CLAUDE.md`.
|
|
100
102
|
|
|
101
103
|
Cursor documents `AGENTS.md` support in project roots and subdirectories. `code-agnostic` still disables Cursor workspace propagation, so it does not copy or link the shared workspace `AGENTS.md` into child repos; Cursor will load repo-local or nested `AGENTS.md` files that already exist in the opened project. Codex documents nested `AGENTS.md` discovery, but not a native config include for an extra workspace file.
|
|
102
104
|
|
|
@@ -172,7 +174,9 @@ code-agnostic plan
|
|
|
172
174
|
code-agnostic apply
|
|
173
175
|
```
|
|
174
176
|
|
|
175
|
-
Global skills live under `~/.config/code-agnostic/skills`. Workspace-local skills live under `~/.config/code-agnostic/workspaces/<name>/skills` and can be inspected with `code-agnostic skills list -w <name>`. Codex generated skill outputs are written to `~/.agents/skills`, while Codex agents and config remain under `~/.codex`.
|
|
177
|
+
Global skills live under `~/.config/code-agnostic/skills`. Workspace-local skills live under `~/.config/code-agnostic/workspaces/<name>/skills` and can be inspected with `code-agnostic skills list -w <name>`. Codex generated skill outputs are written to `~/.agents/skills`, while Codex agents and config remain under `~/.codex`. Claude Code generated skills and agents are written under `~/.claude/skills` and `~/.claude/agents`, with workspace copies under repo-local `.claude/skills` and `.claude/agents`.
|
|
178
|
+
|
|
179
|
+
Project-local skills are not first-class source inputs in `code-agnostic` yet. If a target app discovers repo-local skill folders such as `.agents/skills`, `.opencode/skills`, or user-created `.claude/skills`, treat those as unmanaged app inputs. Workspace sync writes only the exact generated paths recorded in `.sync-state.json`.
|
|
176
180
|
|
|
177
181
|
Planned convenience command:
|
|
178
182
|
|
|
@@ -184,7 +188,7 @@ That command should copy the skill into the source of truth and then run the nor
|
|
|
184
188
|
|
|
185
189
|
### Workspaces
|
|
186
190
|
|
|
187
|
-
Register workspace directories. Workspace rules are compiled into a canonical `AGENTS.md` at the workspace root. Repos keep their own repo-specific `AGENTS.md`; Codex receives the workspace rules through generated, git-excluded `AGENTS.override.md` files, while OpenCode workspace configs reference the shared workspace file through `instructions`. Repo-local app config, skills, and agents are propagated for OpenCode and
|
|
191
|
+
Register workspace directories. Workspace rules are compiled into a canonical `AGENTS.md` at the workspace root. Repos keep their own repo-specific `AGENTS.md`; Codex receives the workspace rules through generated, git-excluded `AGENTS.override.md` files, while OpenCode workspace configs reference the shared workspace file through `instructions`. Claude receives generated `CLAUDE.local.md` files and project MCP entries in `~/.claude.json["projects"][absolute_repo_path]["mcpServers"]`. Repo-local app config, skills, and agents are propagated for OpenCode, Codex, and Claude.
|
|
188
192
|
|
|
189
193
|
`.cursor` workspace propagation is intentionally disabled to avoid duplicate MCP initialization when opening multi-root workspaces in Cursor (related issue: https://forum.cursor.com/t/mcp-multi-root-workspace-causes-duplicate-mcp-server-initialization-4x-createclient-actions/144003).
|
|
190
194
|
|
|
@@ -211,6 +215,7 @@ Migrate existing config from any supported editor into the hub.
|
|
|
211
215
|
```bash
|
|
212
216
|
code-agnostic import plan -a codex
|
|
213
217
|
code-agnostic import apply -a codex
|
|
218
|
+
code-agnostic import plan -a claude
|
|
214
219
|
code-agnostic import apply -a cursor --include mcp --on-conflict overwrite
|
|
215
220
|
code-agnostic import plan -a codex -i # interactive TUI picker
|
|
216
221
|
```
|
|
@@ -243,7 +248,8 @@ The compiler migration is documented in:
|
|
|
243
248
|
- [x] Cross-compilation for skills and agents
|
|
244
249
|
- [x] Per-workspace git-exclude customization
|
|
245
250
|
- [x] Interactive TUI for import selection
|
|
246
|
-
- [
|
|
251
|
+
- [x] Claude Code support
|
|
252
|
+
- [ ] Project-scoped skill installs and sync
|
|
247
253
|
- [ ] `rules add` / `skills add` / `agents add` commands (open `$EDITOR` with template)
|
|
248
254
|
- [ ] Planner integration for cross-compiled skills and agents
|
|
249
255
|
- [ ] Shell auto-complete
|
|
@@ -256,10 +262,37 @@ uv sync --dev
|
|
|
256
262
|
uv run pytest
|
|
257
263
|
```
|
|
258
264
|
|
|
265
|
+
Before pushing release-prep work, run the supported Python matrix locally. Tox
|
|
266
|
+
delegates each environment to `uv run --python`, so `uv` can provide the
|
|
267
|
+
requested interpreter when it is not already installed:
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
uvx tox run -p auto
|
|
271
|
+
uvx tox run -e uv310 -- tests/test_version.py -q
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
For a hermetic Linux matrix that does not depend on locally installed Python
|
|
275
|
+
versions, run the Docker matrix:
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
./scripts/run-docker-matrix.sh
|
|
279
|
+
./scripts/run-docker-matrix.sh tests/test_version.py -q
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Limit the Docker matrix while iterating:
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
PYTHON_VERSIONS="3.10 3.14" ./scripts/run-docker-matrix.sh tests/test_version.py -q
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
The release gate still requires GitHub Actions to pass because the published
|
|
289
|
+
workflow is the source of truth for OS coverage across Ubuntu, macOS, and
|
|
290
|
+
Windows on every supported Python version.
|
|
291
|
+
|
|
259
292
|
Real app-ingestion E2E is gated because it requires installed target CLIs and
|
|
260
293
|
uses each tool's own introspection surface:
|
|
261
294
|
|
|
262
295
|
```bash
|
|
263
296
|
CODE_AGNOSTIC_REAL_APP_E2E=1 uv run pytest tests/e2e/test_real_app_ingestion_e2e.py -q
|
|
264
|
-
CODE_AGNOSTIC_REAL_APP_E2E=1 CODE_AGNOSTIC_REAL_APP_TARGETS=codex,opencode uv run pytest tests/e2e/test_real_app_ingestion_e2e.py -q
|
|
297
|
+
CODE_AGNOSTIC_REAL_APP_E2E=1 CODE_AGNOSTIC_REAL_APP_TARGETS=codex,opencode,claude uv run pytest tests/e2e/test_real_app_ingestion_e2e.py -q
|
|
265
298
|
```
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Claude Code subagent Markdown conversion."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import yaml # type: ignore[import-untyped]
|
|
10
|
+
|
|
11
|
+
from code_agnostic.agents.models import Agent
|
|
12
|
+
|
|
13
|
+
_SAFE_FILE_STEM_RE = re.compile(r"[^A-Za-z0-9_-]+")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def normalize_claude_agent_filename(name: str, fallback: str) -> str:
|
|
17
|
+
candidate = name.strip() or fallback.strip()
|
|
18
|
+
normalized = _SAFE_FILE_STEM_RE.sub("-", candidate).strip("-_")
|
|
19
|
+
return normalized or fallback
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def serialize_claude_agent(agent: Agent) -> str:
|
|
23
|
+
fm: dict[str, Any] = {}
|
|
24
|
+
if agent.metadata.name:
|
|
25
|
+
fm["name"] = agent.metadata.name
|
|
26
|
+
else:
|
|
27
|
+
fm["name"] = agent.name
|
|
28
|
+
|
|
29
|
+
description = agent.metadata.description or agent.metadata.name or agent.name
|
|
30
|
+
if description:
|
|
31
|
+
fm["description"] = description
|
|
32
|
+
|
|
33
|
+
model = agent.metadata.effective_value("claude", "model")
|
|
34
|
+
if model:
|
|
35
|
+
fm["model"] = model
|
|
36
|
+
reasoning_effort = agent.metadata.effective_value("claude", "reasoning_effort")
|
|
37
|
+
if reasoning_effort:
|
|
38
|
+
fm["effort"] = reasoning_effort
|
|
39
|
+
|
|
40
|
+
for key, value in agent.metadata.app_passthrough(
|
|
41
|
+
"claude",
|
|
42
|
+
consumed_keys={
|
|
43
|
+
"model",
|
|
44
|
+
"reasoning_effort",
|
|
45
|
+
"sandbox_mode",
|
|
46
|
+
"nickname_candidates",
|
|
47
|
+
},
|
|
48
|
+
).items():
|
|
49
|
+
if key in fm:
|
|
50
|
+
continue
|
|
51
|
+
fm[key] = value
|
|
52
|
+
|
|
53
|
+
parts = [
|
|
54
|
+
"---",
|
|
55
|
+
yaml.dump(fm, default_flow_style=False, sort_keys=False).rstrip(),
|
|
56
|
+
"---",
|
|
57
|
+
"",
|
|
58
|
+
agent.content,
|
|
59
|
+
]
|
|
60
|
+
return "\n".join(parts)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def claude_agent_target_path(target_dir: Path, agent: Agent) -> Path:
|
|
64
|
+
return (
|
|
65
|
+
target_dir
|
|
66
|
+
/ f"{normalize_claude_agent_filename(agent.metadata.name, agent.name)}.md"
|
|
67
|
+
)
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from abc import ABC, abstractmethod
|
|
6
6
|
|
|
7
|
+
from code_agnostic.agents.claude import serialize_claude_agent
|
|
7
8
|
from code_agnostic.agents.codex import serialize_codex_agent
|
|
8
9
|
from code_agnostic.agents.models import Agent
|
|
9
10
|
from code_agnostic.agents.opencode import serialize_opencode_agent
|
|
@@ -35,3 +36,10 @@ class CodexAgentCompiler(IAgentCompiler):
|
|
|
35
36
|
|
|
36
37
|
def compile(self, agent: Agent) -> str:
|
|
37
38
|
return serialize_codex_agent(agent)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ClaudeAgentCompiler(IAgentCompiler):
|
|
42
|
+
"""Cross-compile for Claude Code subagents."""
|
|
43
|
+
|
|
44
|
+
def compile(self, agent: Agent) -> str:
|
|
45
|
+
return serialize_claude_agent(agent)
|
|
@@ -19,7 +19,7 @@ from code_agnostic.agents.models import (
|
|
|
19
19
|
from code_agnostic.spec.loaders import load_agent_bundle
|
|
20
20
|
|
|
21
21
|
_FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)
|
|
22
|
-
_APP_OVERRIDE_PREFIXES = ("cursor", "codex", "opencode")
|
|
22
|
+
_APP_OVERRIDE_PREFIXES = ("cursor", "codex", "opencode", "claude")
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
def parse_agent(path: Path) -> Agent:
|
|
@@ -2,6 +2,8 @@ from enum import Enum
|
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
|
|
4
4
|
from code_agnostic.constants import (
|
|
5
|
+
CLAUDE_CONFIG_FILENAME,
|
|
6
|
+
CLAUDE_PROJECT_DIRNAME,
|
|
5
7
|
CODEX_CONFIG_FILENAME,
|
|
6
8
|
CODEX_PROJECT_DIRNAME,
|
|
7
9
|
CURSOR_CONFIG_FILENAME,
|
|
@@ -16,6 +18,7 @@ class AppId(str, Enum):
|
|
|
16
18
|
OPENCODE = "opencode"
|
|
17
19
|
CURSOR = "cursor"
|
|
18
20
|
CODEX = "codex"
|
|
21
|
+
CLAUDE = "claude"
|
|
19
22
|
|
|
20
23
|
|
|
21
24
|
@dataclass(frozen=True)
|
|
@@ -75,6 +78,17 @@ APP_CATALOG: dict[AppId, AppMetadata] = {
|
|
|
75
78
|
project_dir_name=CODEX_PROJECT_DIRNAME,
|
|
76
79
|
config_filename=CODEX_CONFIG_FILENAME,
|
|
77
80
|
),
|
|
81
|
+
AppId.CLAUDE: AppMetadata(
|
|
82
|
+
app_id=AppId.CLAUDE,
|
|
83
|
+
label="Claude Code",
|
|
84
|
+
targetable=True,
|
|
85
|
+
toggleable=True,
|
|
86
|
+
importable=True,
|
|
87
|
+
supports_import_agents=True,
|
|
88
|
+
supports_workspace_propagation=True,
|
|
89
|
+
project_dir_name=CLAUDE_PROJECT_DIRNAME,
|
|
90
|
+
config_filename=CLAUDE_CONFIG_FILENAME,
|
|
91
|
+
),
|
|
78
92
|
}
|
|
79
93
|
|
|
80
94
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Claude Code app support."""
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from code_agnostic.apps.common.interfaces.repositories import IAppConfigRepository
|
|
5
|
+
from code_agnostic.constants import (
|
|
6
|
+
AGENTS_DIRNAME,
|
|
7
|
+
CLAUDE_CONFIG_FILENAME,
|
|
8
|
+
CLAUDE_PROJECT_DIRNAME,
|
|
9
|
+
SKILLS_DIRNAME,
|
|
10
|
+
)
|
|
11
|
+
from code_agnostic.errors import InvalidConfigSchemaError, InvalidJsonFormatError
|
|
12
|
+
from code_agnostic.utils import read_json_safe, write_json
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ClaudeConfigRepository(IAppConfigRepository):
|
|
16
|
+
def __init__(
|
|
17
|
+
self, root: Path | None = None, config_path: Path | None = None
|
|
18
|
+
) -> None:
|
|
19
|
+
self._root = root or (Path.home() / CLAUDE_PROJECT_DIRNAME)
|
|
20
|
+
self._config_path = config_path or (Path.home() / CLAUDE_CONFIG_FILENAME)
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def root(self) -> Path:
|
|
24
|
+
return self._root
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def config_path(self) -> Path:
|
|
28
|
+
return self._config_path
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def skills_dir(self) -> Path:
|
|
32
|
+
return self.root / SKILLS_DIRNAME
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def agents_dir(self) -> Path:
|
|
36
|
+
return self.root / AGENTS_DIRNAME
|
|
37
|
+
|
|
38
|
+
def load_config(self) -> dict[str, Any]:
|
|
39
|
+
payload, error = read_json_safe(self.config_path)
|
|
40
|
+
if error is not None:
|
|
41
|
+
raise InvalidJsonFormatError(self.config_path, error)
|
|
42
|
+
if payload is None:
|
|
43
|
+
return {}
|
|
44
|
+
if not isinstance(payload, dict):
|
|
45
|
+
raise InvalidConfigSchemaError(self.config_path, "must be a JSON object")
|
|
46
|
+
return payload
|
|
47
|
+
|
|
48
|
+
def save_config(self, payload: dict[str, Any]) -> None:
|
|
49
|
+
write_json(self.config_path, payload)
|
|
50
|
+
|
|
51
|
+
def load_mcp_payload(self) -> dict[str, Any]:
|
|
52
|
+
payload = self.load_config()
|
|
53
|
+
mcp = payload.get("mcpServers")
|
|
54
|
+
return mcp if isinstance(mcp, dict) else {}
|
|
55
|
+
|
|
56
|
+
def save_mcp_payload(self, payload: dict[str, Any]) -> None:
|
|
57
|
+
config = self.load_config()
|
|
58
|
+
config["mcpServers"] = payload
|
|
59
|
+
self.save_config(config)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from copy import deepcopy
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from code_agnostic.apps.common.interfaces.mapper import IAppMCPMapper
|
|
5
|
+
from code_agnostic.apps.common.models import MCPServerDTO, MCPServerType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _as_list(value: Any) -> list[str]:
|
|
9
|
+
if isinstance(value, list):
|
|
10
|
+
return [str(item) for item in value]
|
|
11
|
+
if isinstance(value, str):
|
|
12
|
+
return [value]
|
|
13
|
+
return []
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _as_str_dict(value: Any) -> dict[str, str]:
|
|
17
|
+
return {str(k): str(v) for k, v in value.items()} if isinstance(value, dict) else {}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _timeout(value: Any) -> int | None:
|
|
21
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
22
|
+
return value
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ClaudeMCPMapper(IAppMCPMapper):
|
|
27
|
+
def to_common(self, payload: dict[str, Any]) -> dict[str, MCPServerDTO]:
|
|
28
|
+
mapped: dict[str, MCPServerDTO] = {}
|
|
29
|
+
for name, server in payload.items():
|
|
30
|
+
if not isinstance(server, dict):
|
|
31
|
+
continue
|
|
32
|
+
|
|
33
|
+
server_type = server.get("type")
|
|
34
|
+
command = server.get("command")
|
|
35
|
+
if server_type == "stdio" or isinstance(command, str):
|
|
36
|
+
if not isinstance(command, str) or not command:
|
|
37
|
+
continue
|
|
38
|
+
mapped[name] = MCPServerDTO(
|
|
39
|
+
name=name,
|
|
40
|
+
type=MCPServerType.STDIO,
|
|
41
|
+
command=command,
|
|
42
|
+
args=_as_list(server.get("args")),
|
|
43
|
+
env=_as_str_dict(server.get("env")),
|
|
44
|
+
headers=_as_str_dict(server.get("headers")),
|
|
45
|
+
timeout_ms=_timeout(server.get("timeout")),
|
|
46
|
+
)
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
url = server.get("url")
|
|
50
|
+
if not isinstance(url, str) or not url:
|
|
51
|
+
continue
|
|
52
|
+
mapped[name] = MCPServerDTO(
|
|
53
|
+
name=name,
|
|
54
|
+
type=MCPServerType.HTTP,
|
|
55
|
+
url=url,
|
|
56
|
+
env=_as_str_dict(server.get("env")),
|
|
57
|
+
headers=_as_str_dict(server.get("headers")),
|
|
58
|
+
timeout_ms=_timeout(server.get("timeout")),
|
|
59
|
+
)
|
|
60
|
+
return mapped
|
|
61
|
+
|
|
62
|
+
def from_common(self, servers: dict[str, MCPServerDTO]) -> dict[str, Any]:
|
|
63
|
+
mapped: dict[str, Any] = {}
|
|
64
|
+
for name, server in servers.items():
|
|
65
|
+
out: dict[str, Any] = {}
|
|
66
|
+
if server.type == MCPServerType.STDIO:
|
|
67
|
+
if not server.command:
|
|
68
|
+
continue
|
|
69
|
+
out["type"] = "stdio"
|
|
70
|
+
out["command"] = server.command
|
|
71
|
+
if server.args:
|
|
72
|
+
out["args"] = deepcopy(server.args)
|
|
73
|
+
else:
|
|
74
|
+
if not server.url:
|
|
75
|
+
continue
|
|
76
|
+
out["type"] = "http"
|
|
77
|
+
out["url"] = server.url
|
|
78
|
+
|
|
79
|
+
if server.env:
|
|
80
|
+
out["env"] = deepcopy(server.env)
|
|
81
|
+
if server.headers:
|
|
82
|
+
out["headers"] = deepcopy(server.headers)
|
|
83
|
+
if server.timeout_ms is not None:
|
|
84
|
+
out["timeout"] = server.timeout_ms
|
|
85
|
+
mapped[name] = out
|
|
86
|
+
return mapped
|