code-agnostic 0.3.5__tar.gz → 0.3.8__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.
Files changed (159) hide show
  1. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/PKG-INFO +58 -23
  2. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/README.md +55 -22
  3. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/__init__.py +1 -1
  4. code_agnostic-0.3.8/code_agnostic/agents/claude.py +67 -0
  5. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/agents/codex.py +6 -3
  6. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/agents/compilers.py +8 -0
  7. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/agents/parser.py +1 -1
  8. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/app_id.py +15 -1
  9. code_agnostic-0.3.8/code_agnostic/apps/claude/__init__.py +1 -0
  10. code_agnostic-0.3.8/code_agnostic/apps/claude/config_repository.py +59 -0
  11. code_agnostic-0.3.8/code_agnostic/apps/claude/mapper.py +86 -0
  12. code_agnostic-0.3.8/code_agnostic/apps/claude/service.py +184 -0
  13. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/codex/schema.json +12 -0
  14. code_agnostic-0.3.8/code_agnostic/apps/common/compiled_planning.py +115 -0
  15. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/common/interfaces/repositories.py +10 -0
  16. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/common/interfaces/service.py +30 -14
  17. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/common/loader.py +1 -0
  18. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/opencode/service.py +2 -0
  19. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/cli/commands/workspaces.py +6 -1
  20. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/constants.py +3 -0
  21. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/executor.py +46 -2
  22. code_agnostic-0.3.8/code_agnostic/generated_artifacts.py +134 -0
  23. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/git_exclude_service.py +27 -19
  24. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/imports/adapters.py +27 -10
  25. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/imports/service.py +10 -0
  26. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/lossiness.py +65 -7
  27. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/models.py +11 -2
  28. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/planner.py +131 -13
  29. code_agnostic-0.3.8/code_agnostic/skills/compilers.py +107 -0
  30. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/skills/parser.py +1 -1
  31. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/spec/loaders.py +2 -2
  32. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/spec/schemas/agent.v1.schema.json +2 -1
  33. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/spec/schemas/rule.v1.schema.json +2 -1
  34. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/spec/schemas/skill.v1.schema.json +2 -1
  35. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/status.py +28 -49
  36. code_agnostic-0.3.8/code_agnostic/workspace_artifacts.py +238 -0
  37. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic.egg-info/PKG-INFO +58 -23
  38. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic.egg-info/SOURCES.txt +7 -0
  39. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic.egg-info/requires.txt +2 -0
  40. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/pyproject.toml +4 -2
  41. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_cli_apply_apps.py +38 -12
  42. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_cli_explain_lossiness.py +37 -0
  43. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_cli_plan.py +14 -4
  44. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_cli_workspaces.py +58 -19
  45. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_common_mcp_to_dto.py +16 -0
  46. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_git_exclude_service.py +56 -11
  47. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_sync_plan_model.py +23 -0
  48. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_transactional_executor.py +102 -3
  49. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_version.py +5 -1
  50. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_workspace_config_sync.py +396 -17
  51. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_workspace_repo_status.py +57 -11
  52. code_agnostic-0.3.5/code_agnostic/apps/common/compiled_planning.py +0 -140
  53. code_agnostic-0.3.5/code_agnostic/skills/compilers.py +0 -75
  54. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/LICENSE +0 -0
  55. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/__main__.py +0 -0
  56. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/agents/__init__.py +0 -0
  57. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/agents/models.py +0 -0
  58. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/agents/opencode.py +0 -0
  59. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/__init__.py +0 -0
  60. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/apps_service.py +0 -0
  61. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/codex/__init__.py +0 -0
  62. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/codex/config_repository.py +0 -0
  63. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/codex/mapper.py +0 -0
  64. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/codex/schema_repository.py +0 -0
  65. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/codex/service.py +0 -0
  66. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/common/__init__.py +0 -0
  67. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/common/framework.py +0 -0
  68. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/common/interfaces/__init__.py +0 -0
  69. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/common/interfaces/mapper.py +0 -0
  70. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/common/models.py +0 -0
  71. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/common/schema.py +0 -0
  72. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/common/symlink_planning.py +0 -0
  73. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/common/utils.py +0 -0
  74. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/cursor/__init__.py +0 -0
  75. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/cursor/config_repository.py +0 -0
  76. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/cursor/mapper.py +0 -0
  77. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/cursor/schema.json +0 -0
  78. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/cursor/schema_repository.py +0 -0
  79. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/cursor/service.py +0 -0
  80. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/opencode/__init__.py +0 -0
  81. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/opencode/config_repository.py +0 -0
  82. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/opencode/mapper.py +0 -0
  83. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/opencode/schema.json +0 -0
  84. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/apps/opencode/schema_repository.py +0 -0
  85. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/cli/__init__.py +0 -0
  86. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/cli/aliases.py +0 -0
  87. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/cli/commands/__init__.py +0 -0
  88. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/cli/commands/agents.py +0 -0
  89. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/cli/commands/apply.py +0 -0
  90. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/cli/commands/apps.py +0 -0
  91. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/cli/commands/explain_lossiness.py +0 -0
  92. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/cli/commands/import_.py +0 -0
  93. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/cli/commands/mcp.py +0 -0
  94. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/cli/commands/plan.py +0 -0
  95. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/cli/commands/restore.py +0 -0
  96. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/cli/commands/rules.py +0 -0
  97. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/cli/commands/skills.py +0 -0
  98. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/cli/commands/status.py +0 -0
  99. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/cli/commands/validate.py +0 -0
  100. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/cli/helpers.py +0 -0
  101. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/cli/options.py +0 -0
  102. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/core/__init__.py +0 -0
  103. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/core/repository.py +0 -0
  104. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/core/workspace_repository.py +0 -0
  105. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/errors.py +0 -0
  106. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/imports/__init__.py +0 -0
  107. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/imports/filesystem.py +0 -0
  108. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/imports/models.py +0 -0
  109. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/mcp_service.py +0 -0
  110. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/rules/__init__.py +0 -0
  111. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/rules/compilers.py +0 -0
  112. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/rules/models.py +0 -0
  113. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/rules/parser.py +0 -0
  114. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/rules/repository.py +0 -0
  115. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/skills/__init__.py +0 -0
  116. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/skills/models.py +0 -0
  117. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/spec/__init__.py +0 -0
  118. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/spec/schemas/mcp.base.schema.json +0 -0
  119. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/spec/schemas/mcp.v1.schema.json +0 -0
  120. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/tui/__init__.py +0 -0
  121. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/tui/enums.py +0 -0
  122. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/tui/import_selector.py +0 -0
  123. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/tui/renderers.py +0 -0
  124. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/tui/sections.py +0 -0
  125. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/tui/tables.py +0 -0
  126. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/utils.py +0 -0
  127. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/validation.py +0 -0
  128. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic/workspaces.py +0 -0
  129. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic.egg-info/dependency_links.txt +0 -0
  130. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic.egg-info/entry_points.txt +0 -0
  131. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/code_agnostic.egg-info/top_level.txt +0 -0
  132. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/setup.cfg +0 -0
  133. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_cli_agents.py +0 -0
  134. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_cli_aliases.py +0 -0
  135. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_cli_apply_codex.py +0 -0
  136. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_cli_apply_cursor.py +0 -0
  137. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_cli_apply_target.py +0 -0
  138. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_cli_apps.py +0 -0
  139. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_cli_flags.py +0 -0
  140. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_cli_git_exclude.py +0 -0
  141. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_cli_import.py +0 -0
  142. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_cli_import_interactive.py +0 -0
  143. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_cli_mcp.py +0 -0
  144. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_cli_module_organization.py +0 -0
  145. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_cli_restore.py +0 -0
  146. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_cli_rules.py +0 -0
  147. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_cli_skills.py +0 -0
  148. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_cli_status.py +0 -0
  149. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_cli_validate.py +0 -0
  150. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_cli_workspace_resolution.py +0 -0
  151. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_common_repository.py +0 -0
  152. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_compiled_planning.py +0 -0
  153. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_dto_to_common_mcp.py +0 -0
  154. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_mcp_service.py +0 -0
  155. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_planner_executor.py +0 -0
  156. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_planner_rules.py +0 -0
  157. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_symlink_planning.py +0 -0
  158. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/tests/test_utils.py +0 -0
  159. {code_agnostic-0.3.5 → code_agnostic-0.3.8}/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.5
3
+ Version: 0.3.8
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
@@ -19,6 +19,8 @@ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
19
19
  Requires-Dist: jsonschema>=4.0; extra == "dev"
20
20
  Requires-Dist: ruff>=0.8.0; extra == "dev"
21
21
  Requires-Dist: mypy>=1.11; extra == "dev"
22
+ Requires-Dist: types-jsonschema>=4.0; extra == "dev"
23
+ Requires-Dist: types-PyYAML>=6.0; extra == "dev"
22
24
  Dynamic: license-file
23
25
 
24
26
  # code-agnostic
@@ -53,9 +55,10 @@ AI coding tools each want config in a different place and format. When you use m
53
55
  ~/.config/opencode/ Compiled & synced for OpenCode
54
56
  ~/.cursor/ Compiled & synced for Cursor
55
57
  ~/.codex/ Compiled & synced for Codex
58
+ ~/.claude.json and ~/.claude/ Compiled & synced for Claude Code
56
59
  ```
57
60
 
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, etc.
61
+ 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
62
 
60
63
  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
64
 
@@ -94,6 +97,7 @@ code-agnostic import apply -a codex
94
97
  # Enable target editors
95
98
  code-agnostic apps enable -a cursor
96
99
  code-agnostic apps enable -a opencode
100
+ code-agnostic apps enable -a claude
97
101
 
98
102
  # Preview and apply
99
103
  code-agnostic validate
@@ -103,25 +107,25 @@ code-agnostic apply
103
107
 
104
108
  ## Editor compatibility
105
109
 
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 `AGENTS.md` today | yes | -- | yes |
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 |
110
+ | Feature | OpenCode | Cursor | Codex | Claude Code |
111
+ |---------|:--------:|:------:|:-----:|:-----------:|
112
+ | MCP sync | yes | yes | yes | yes |
113
+ | Rules sync (cross-compiled) | yes | yes | yes | yes |
114
+ | Skills sync | yes | yes | yes | yes |
115
+ | Agents sync | yes | yes | yes | yes |
116
+ | Workspace root `AGENTS.md` link | yes | yes | yes | yes |
117
+ | Native repo config include for workspace `AGENTS.md` | yes | -- | -- | -- |
118
+ | Repo/subdir gets shared workspace instructions today | yes | -- | yes | yes |
119
+ | Nested `AGENTS.md` discovery | -- | yes | yes | -- |
120
+ | Workspace propagation | yes | yes | yes | yes |
121
+ | Import from | yes | yes | yes | yes |
122
+ | Interactive import (TUI) | yes | yes | yes | yes |
119
123
 
120
- 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
124
+ Cursor workspace propagation writes repo-local MCP, skills, and agents when those resources exist in the workspace source config.
121
125
 
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`.
126
+ 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
127
 
124
- 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.
128
+ Cursor documents `AGENTS.md` support in project roots and subdirectories. `code-agnostic` 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
129
 
126
130
  ## Features
127
131
 
@@ -195,7 +199,9 @@ code-agnostic plan
195
199
  code-agnostic apply
196
200
  ```
197
201
 
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`.
202
+ 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`.
203
+
204
+ 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
205
 
200
206
  Planned convenience command:
201
207
 
@@ -207,9 +213,9 @@ That command should copy the skill into the source of truth and then run the nor
207
213
 
208
214
  ### Workspaces
209
215
 
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 Codex.
216
+ 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, Cursor, Codex, and Claude.
211
217
 
212
- `.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).
218
+ Cursor propagation intentionally stays to repo-local MCP, skills, and agents; it does not copy the shared workspace `AGENTS.md` into child repos.
213
219
 
214
220
  ```bash
215
221
  code-agnostic workspaces add --name myproject --path ~/code/myproject
@@ -234,6 +240,7 @@ Migrate existing config from any supported editor into the hub.
234
240
  ```bash
235
241
  code-agnostic import plan -a codex
236
242
  code-agnostic import apply -a codex
243
+ code-agnostic import plan -a claude
237
244
  code-agnostic import apply -a cursor --include mcp --on-conflict overwrite
238
245
  code-agnostic import plan -a codex -i # interactive TUI picker
239
246
  ```
@@ -266,7 +273,8 @@ The compiler migration is documented in:
266
273
  - [x] Cross-compilation for skills and agents
267
274
  - [x] Per-workspace git-exclude customization
268
275
  - [x] Interactive TUI for import selection
269
- - [ ] Claude Code support
276
+ - [x] Claude Code support
277
+ - [ ] Project-scoped skill installs and sync
270
278
  - [ ] `rules add` / `skills add` / `agents add` commands (open `$EDITOR` with template)
271
279
  - [ ] Planner integration for cross-compiled skills and agents
272
280
  - [ ] Shell auto-complete
@@ -279,10 +287,37 @@ uv sync --dev
279
287
  uv run pytest
280
288
  ```
281
289
 
290
+ Before pushing release-prep work, run the supported Python matrix locally. Tox
291
+ delegates each environment to `uv run --python`, so `uv` can provide the
292
+ requested interpreter when it is not already installed:
293
+
294
+ ```bash
295
+ uvx tox run -p auto
296
+ uvx tox run -e uv310 -- tests/test_version.py -q
297
+ ```
298
+
299
+ For a hermetic Linux matrix that does not depend on locally installed Python
300
+ versions, run the Docker matrix:
301
+
302
+ ```bash
303
+ ./scripts/run-docker-matrix.sh
304
+ ./scripts/run-docker-matrix.sh tests/test_version.py -q
305
+ ```
306
+
307
+ Limit the Docker matrix while iterating:
308
+
309
+ ```bash
310
+ PYTHON_VERSIONS="3.10 3.14" ./scripts/run-docker-matrix.sh tests/test_version.py -q
311
+ ```
312
+
313
+ The release gate still requires GitHub Actions to pass because the published
314
+ workflow is the source of truth for OS coverage across Ubuntu, macOS, and
315
+ Windows on every supported Python version.
316
+
282
317
  Real app-ingestion E2E is gated because it requires installed target CLIs and
283
318
  uses each tool's own introspection surface:
284
319
 
285
320
  ```bash
286
321
  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
322
+ 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
323
  ```
@@ -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, etc.
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,25 +82,25 @@ 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 `AGENTS.md` today | yes | -- | yes |
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 | yes |
96
+ | Import from | yes | yes | yes | yes |
97
+ | Interactive import (TUI) | yes | yes | yes | yes |
96
98
 
97
- 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
99
+ Cursor workspace propagation writes repo-local MCP, skills, and agents when those resources exist in the workspace source config.
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
- 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.
103
+ Cursor documents `AGENTS.md` support in project roots and subdirectories. `code-agnostic` 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
 
103
105
  ## Features
104
106
 
@@ -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,9 +188,9 @@ 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 Codex.
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, Cursor, Codex, and Claude.
188
192
 
189
- `.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).
193
+ Cursor propagation intentionally stays to repo-local MCP, skills, and agents; it does not copy the shared workspace `AGENTS.md` into child repos.
190
194
 
191
195
  ```bash
192
196
  code-agnostic workspaces add --name myproject --path ~/code/myproject
@@ -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
- - [ ] Claude Code support
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
  ```
@@ -1,3 +1,3 @@
1
1
  __all__ = ["__version__"]
2
2
 
3
- __version__ = "0.3.5"
3
+ __version__ = "0.3.8"
@@ -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
+ )
@@ -69,10 +69,13 @@ def serialize_codex_agent(agent: Agent) -> str:
69
69
  description = agent.metadata.description or agent.metadata.name or agent.name
70
70
 
71
71
  doc = tomlkit.document()
72
- doc.add("name", agent.metadata.name or agent.name)
73
- doc.add("description", description)
72
+ doc.add("name", tomlkit.item(agent.metadata.name or agent.name))
73
+ doc.add("description", tomlkit.item(description))
74
74
  if agent.metadata.nickname_candidates:
75
- doc.add("nickname_candidates", list(agent.metadata.nickname_candidates))
75
+ doc.add(
76
+ "nickname_candidates",
77
+ tomlkit.item(list(agent.metadata.nickname_candidates)),
78
+ )
76
79
  model = agent.metadata.effective_value("codex", "model")
77
80
  if model:
78
81
  doc.add("model", model)
@@ -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)
@@ -60,7 +63,7 @@ APP_CATALOG: dict[AppId, AppMetadata] = {
60
63
  toggleable=True,
61
64
  importable=True,
62
65
  supports_import_agents=True,
63
- supports_workspace_propagation=False,
66
+ supports_workspace_propagation=True,
64
67
  project_dir_name=CURSOR_PROJECT_DIRNAME,
65
68
  config_filename=CURSOR_CONFIG_FILENAME,
66
69
  ),
@@ -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