code-agnostic 0.6.0__tar.gz → 0.7.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.
Files changed (172) hide show
  1. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/PKG-INFO +1 -1
  2. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/__init__.py +1 -1
  3. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/apps_service.py +24 -0
  4. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/claude/service.py +19 -2
  5. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/planner.py +18 -4
  6. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic.egg-info/PKG-INFO +1 -1
  7. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/pyproject.toml +1 -1
  8. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_disable_cleanup.py +48 -0
  9. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_workspace_config_sync.py +77 -0
  10. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/LICENSE +0 -0
  11. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/README.md +0 -0
  12. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/__main__.py +0 -0
  13. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/agents/__init__.py +0 -0
  14. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/agents/claude.py +0 -0
  15. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/agents/codex.py +0 -0
  16. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/agents/compilers.py +0 -0
  17. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/agents/copilot.py +0 -0
  18. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/agents/models.py +0 -0
  19. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/agents/opencode.py +0 -0
  20. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/agents/parser.py +0 -0
  21. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/__init__.py +0 -0
  22. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/app_id.py +0 -0
  23. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/claude/__init__.py +0 -0
  24. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/claude/config_repository.py +0 -0
  25. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/claude/mapper.py +0 -0
  26. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/codex/__init__.py +0 -0
  27. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/codex/config_repository.py +0 -0
  28. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/codex/mapper.py +0 -0
  29. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/codex/schema.json +0 -0
  30. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/codex/schema_repository.py +0 -0
  31. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/codex/service.py +0 -0
  32. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/common/__init__.py +0 -0
  33. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/common/compiled_planning.py +0 -0
  34. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/common/framework.py +0 -0
  35. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/common/interfaces/__init__.py +0 -0
  36. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/common/interfaces/mapper.py +0 -0
  37. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/common/interfaces/repositories.py +0 -0
  38. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/common/interfaces/service.py +0 -0
  39. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/common/loader.py +0 -0
  40. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/common/models.py +0 -0
  41. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/common/schema.py +0 -0
  42. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/common/symlink_planning.py +0 -0
  43. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/common/utils.py +0 -0
  44. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/copilot/__init__.py +0 -0
  45. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/copilot/config_repository.py +0 -0
  46. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/copilot/mapper.py +0 -0
  47. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/copilot/service.py +0 -0
  48. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/cursor/__init__.py +0 -0
  49. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/cursor/config_repository.py +0 -0
  50. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/cursor/mapper.py +0 -0
  51. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/cursor/schema.json +0 -0
  52. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/cursor/schema_repository.py +0 -0
  53. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/cursor/service.py +0 -0
  54. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/opencode/__init__.py +0 -0
  55. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/opencode/config_repository.py +0 -0
  56. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/opencode/mapper.py +0 -0
  57. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/opencode/schema.json +0 -0
  58. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/opencode/schema_repository.py +0 -0
  59. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/opencode/service.py +0 -0
  60. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/__init__.py +0 -0
  61. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/aliases.py +0 -0
  62. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/commands/__init__.py +0 -0
  63. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/commands/agents.py +0 -0
  64. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/commands/apply.py +0 -0
  65. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/commands/apps.py +0 -0
  66. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/commands/explain_lossiness.py +0 -0
  67. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/commands/import_.py +0 -0
  68. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/commands/mcp.py +0 -0
  69. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/commands/plan.py +0 -0
  70. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/commands/projects.py +0 -0
  71. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/commands/restore.py +0 -0
  72. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/commands/rules.py +0 -0
  73. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/commands/skills.py +0 -0
  74. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/commands/status.py +0 -0
  75. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/commands/validate.py +0 -0
  76. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/commands/workspaces.py +0 -0
  77. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/helpers.py +0 -0
  78. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/options.py +0 -0
  79. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/constants.py +0 -0
  80. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/core/__init__.py +0 -0
  81. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/core/project_repository.py +0 -0
  82. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/core/repository.py +0 -0
  83. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/core/workspace_repository.py +0 -0
  84. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/errors.py +0 -0
  85. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/executor.py +0 -0
  86. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/generated_artifacts.py +0 -0
  87. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/git_exclude_service.py +0 -0
  88. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/imports/__init__.py +0 -0
  89. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/imports/adapters.py +0 -0
  90. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/imports/filesystem.py +0 -0
  91. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/imports/models.py +0 -0
  92. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/imports/service.py +0 -0
  93. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/lossiness.py +0 -0
  94. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/mcp_service.py +0 -0
  95. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/models.py +0 -0
  96. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/project_artifacts.py +0 -0
  97. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/rules/__init__.py +0 -0
  98. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/rules/compilers.py +0 -0
  99. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/rules/models.py +0 -0
  100. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/rules/parser.py +0 -0
  101. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/rules/repository.py +0 -0
  102. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/skills/__init__.py +0 -0
  103. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/skills/compilers.py +0 -0
  104. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/skills/install_sources.py +0 -0
  105. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/skills/models.py +0 -0
  106. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/skills/parser.py +0 -0
  107. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/spec/__init__.py +0 -0
  108. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/spec/loaders.py +0 -0
  109. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/spec/schemas/agent.v1.schema.json +0 -0
  110. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/spec/schemas/mcp.base.schema.json +0 -0
  111. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/spec/schemas/mcp.v1.schema.json +0 -0
  112. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/spec/schemas/rule.v1.schema.json +0 -0
  113. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/spec/schemas/skill.v1.schema.json +0 -0
  114. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/status.py +0 -0
  115. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/tui/__init__.py +0 -0
  116. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/tui/enums.py +0 -0
  117. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/tui/import_selector.py +0 -0
  118. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/tui/renderers.py +0 -0
  119. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/tui/sections.py +0 -0
  120. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/tui/tables.py +0 -0
  121. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/utils.py +0 -0
  122. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/validation.py +0 -0
  123. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/workspace_artifacts.py +0 -0
  124. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/workspaces.py +0 -0
  125. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic.egg-info/SOURCES.txt +0 -0
  126. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic.egg-info/dependency_links.txt +0 -0
  127. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic.egg-info/entry_points.txt +0 -0
  128. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic.egg-info/requires.txt +0 -0
  129. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic.egg-info/top_level.txt +0 -0
  130. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/setup.cfg +0 -0
  131. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_agents.py +0 -0
  132. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_aliases.py +0 -0
  133. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_apply_apps.py +0 -0
  134. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_apply_codex.py +0 -0
  135. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_apply_codex_agent_cleanup.py +0 -0
  136. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_apply_cursor.py +0 -0
  137. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_apply_mcp_cleanup.py +0 -0
  138. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_apply_target.py +0 -0
  139. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_apps.py +0 -0
  140. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_explain_lossiness.py +0 -0
  141. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_flags.py +0 -0
  142. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_git_exclude.py +0 -0
  143. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_import.py +0 -0
  144. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_import_interactive.py +0 -0
  145. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_mcp.py +0 -0
  146. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_module_organization.py +0 -0
  147. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_plan.py +0 -0
  148. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_projects.py +0 -0
  149. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_restore.py +0 -0
  150. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_rules.py +0 -0
  151. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_skills.py +0 -0
  152. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_status.py +0 -0
  153. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_validate.py +0 -0
  154. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_workspace_resolution.py +0 -0
  155. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_workspaces.py +0 -0
  156. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_common_mcp_to_dto.py +0 -0
  157. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_common_repository.py +0 -0
  158. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_compiled_planning.py +0 -0
  159. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_dto_to_common_mcp.py +0 -0
  160. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_git_exclude_service.py +0 -0
  161. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_mcp_service.py +0 -0
  162. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_planner_executor.py +0 -0
  163. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_planner_rules.py +0 -0
  164. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_project_config_sync.py +0 -0
  165. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_skill_install_sources.py +0 -0
  166. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_symlink_planning.py +0 -0
  167. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_sync_plan_model.py +0 -0
  168. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_transactional_executor.py +0 -0
  169. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_utils.py +0 -0
  170. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_version.py +0 -0
  171. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_workspace_repo_status.py +0 -0
  172. {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_workspaces.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-agnostic
3
- Version: 0.6.0
3
+ Version: 0.7.0
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
@@ -1,3 +1,3 @@
1
1
  __all__ = ["__version__"]
2
2
 
3
- __version__ = "0.6.0"
3
+ __version__ = "0.7.0"
@@ -6,6 +6,7 @@ from code_agnostic.apps.common.framework import (
6
6
  create_registered_app_service,
7
7
  list_registered_app_services,
8
8
  )
9
+ from code_agnostic.apps.claude.service import ClaudeConfigService
9
10
  from code_agnostic.apps.common.interfaces.service import IAppConfigService
10
11
  from code_agnostic.apps.common.symlink_planning import (
11
12
  load_state_links,
@@ -113,6 +114,8 @@ class AppsService:
113
114
  self._cleanup_paths(global_paths, scope, app_id.value, skipped)
114
115
  )
115
116
  actions.extend(self._cleanup_global_mcp(app_id, global_mcp))
117
+ if app_id == AppId.CLAUDE:
118
+ actions.extend(self._cleanup_claude_projects(global_mcp))
116
119
 
117
120
  # --- Workspace scopes (ws:<app>:*) ---
118
121
  ws_prefix = f"ws:{app_id.value}:"
@@ -224,6 +227,27 @@ class AppsService:
224
227
  )
225
228
  ]
226
229
 
230
+ def _cleanup_claude_projects(self, managed_mcp: dict[str, Any]) -> list[Action]:
231
+ managed = self._names_for_scope(
232
+ managed_mcp, app_scope(AppId.CLAUDE, "projects")
233
+ )
234
+ if not managed:
235
+ return []
236
+ try:
237
+ service = create_registered_app_service(AppId.CLAUDE)
238
+ except (KeyError, ValueError):
239
+ return []
240
+ if not isinstance(service, ClaudeConfigService):
241
+ return []
242
+ if not service.repository.config_path.exists():
243
+ return []
244
+ # Empty desired + our previously-managed project paths → prune only the
245
+ # `mcpServers` sub-keys we wrote, keep the rest of each project entry, and
246
+ # clear the ownership state (managed_entries becomes empty).
247
+ return [
248
+ service.build_project_mcp_action({}, previously_managed_projects=managed)
249
+ ]
250
+
227
251
  @staticmethod
228
252
  def _names_for_scope(group: dict[str, Any], scope: str) -> set[str]:
229
253
  raw = group.get(scope, [])
@@ -5,7 +5,7 @@ from typing import Any
5
5
  from code_agnostic.agents.claude import claude_agent_target_path
6
6
  from code_agnostic.agents.compilers import ClaudeAgentCompiler
7
7
  from code_agnostic.agents.parser import parse_agent
8
- from code_agnostic.apps.app_id import AppId, app_label
8
+ from code_agnostic.apps.app_id import AppId, app_label, app_scope
9
9
  from code_agnostic.apps.claude.config_repository import ClaudeConfigRepository
10
10
  from code_agnostic.apps.claude.mapper import ClaudeMCPMapper
11
11
  from code_agnostic.apps.common.framework import RegisteredAppConfigService
@@ -72,6 +72,7 @@ class ClaudeConfigService(RegisteredAppConfigService):
72
72
  self,
73
73
  project_servers: dict[Path, dict[str, MCPServerDTO]],
74
74
  base_payload: dict[str, Any] | None = None,
75
+ previously_managed_projects: set[str] | None = None,
75
76
  ) -> Action:
76
77
  existing = self._repository.load_config()
77
78
  if existing or self.repository.config_path.exists():
@@ -86,8 +87,10 @@ class ClaudeConfigService(RegisteredAppConfigService):
86
87
  else:
87
88
  projects = deepcopy(projects)
88
89
 
90
+ desired_keys: set[str] = set()
89
91
  for project_path, servers in project_servers.items():
90
92
  key = str(project_path.resolve())
93
+ desired_keys.add(key)
91
94
  project = projects.get(key)
92
95
  if not isinstance(project, dict):
93
96
  project = {}
@@ -96,10 +99,18 @@ class ClaudeConfigService(RegisteredAppConfigService):
96
99
  project["mcpServers"] = self.mapper.from_common(servers)
97
100
  projects[key] = project
98
101
 
102
+ # Prune the `mcpServers` sub-key from project entries we previously wrote
103
+ # but no longer sync (e.g. a repo removed from a workspace). Leave the rest
104
+ # of the project entry (history etc.) and non-managed projects untouched.
105
+ for stale_key in (previously_managed_projects or set()) - desired_keys:
106
+ stale_project = projects.get(stale_key)
107
+ if isinstance(stale_project, dict):
108
+ stale_project.pop("mcpServers", None)
109
+
99
110
  merged["projects"] = projects
100
111
  self.validate_config(merged)
101
112
 
102
- return Action(
113
+ action = Action(
103
114
  kind=self.action_kind,
104
115
  path=self.repository.config_path,
105
116
  status=self.derive_status(existing, merged),
@@ -107,6 +118,12 @@ class ClaudeConfigService(RegisteredAppConfigService):
107
118
  payload=merged,
108
119
  app=self.app_id.value,
109
120
  )
121
+ # Track which project paths we own so a later source removal prunes only
122
+ # our `mcpServers` sub-keys without touching the user's project entries.
123
+ projects_scope = app_scope(self.app_id, "projects")
124
+ action.scope = projects_scope
125
+ action.managed_entries = {projects_scope: sorted(desired_keys)}
126
+ return action
110
127
 
111
128
  def plan_skill_actions(
112
129
  self,
@@ -1,6 +1,6 @@
1
1
  from pathlib import Path
2
2
 
3
- from code_agnostic.apps.app_id import AppId, app_metadata
3
+ from code_agnostic.apps.app_id import AppId, app_metadata, app_scope
4
4
  from code_agnostic.apps.common.compiled_planning import (
5
5
  plan_owned_compiled_text_action,
6
6
  )
@@ -283,15 +283,29 @@ class SyncPlanner:
283
283
  if existing_action is not None and isinstance(existing_action.payload, dict)
284
284
  else None
285
285
  )
286
+ state = self.core.load_state()
287
+ managed_mcp = state.get("managed_mcp")
288
+ if not isinstance(managed_mcp, dict):
289
+ managed_mcp = {}
290
+ prev_projects_raw = managed_mcp.get(app_scope(AppId.CLAUDE, "projects"), [])
291
+ previously_managed_projects = (
292
+ {path for path in prev_projects_raw if isinstance(path, str)}
293
+ if isinstance(prev_projects_raw, list)
294
+ else set()
295
+ )
286
296
  project_action = claude_service.build_project_mcp_action(
287
297
  self._claude_project_mcp,
288
298
  base_payload=base_payload,
299
+ previously_managed_projects=previously_managed_projects,
289
300
  )
290
301
  if existing_action is not None:
291
302
  # Carry global MCP ownership tracking onto the merged Claude action so
292
- # top-level `mcpServers` orphans still get pruned on later applies.
293
- project_action.scope = existing_action.scope
294
- project_action.managed_entries = existing_action.managed_entries
303
+ # top-level `mcpServers` orphans still get pruned on later applies. The
304
+ # project-path ownership set by build_project_mcp_action is preserved.
305
+ project_action.scope = existing_action.scope or project_action.scope
306
+ merged_entries = dict(existing_action.managed_entries or {})
307
+ merged_entries.update(project_action.managed_entries or {})
308
+ project_action.managed_entries = merged_entries
295
309
  actions = [
296
310
  action
297
311
  for action in plan.actions
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-agnostic
3
- Version: 0.6.0
3
+ Version: 0.7.0
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "code-agnostic"
7
- version = "0.6.0"
7
+ version = "0.7.0"
8
8
  description = "Centralized hub for LLM coding config: MCP, skills, rules, and agents."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -6,6 +6,7 @@ import json
6
6
  from pathlib import Path
7
7
 
8
8
  from code_agnostic.__main__ import cli
9
+ from code_agnostic.core.repository import CoreRepository
9
10
 
10
11
 
11
12
  def _write_skill(core_root: Path, name: str) -> None:
@@ -67,3 +68,50 @@ def test_disable_cursor_removes_skill_and_prunes_owned_mcp(
67
68
  # Flag is off.
68
69
  apps_cfg = json.loads((core_root / "config" / "apps.json").read_text())
69
70
  assert apps_cfg["cursor"] is False
71
+
72
+
73
+ def test_disable_claude_prunes_project_mcp_keeps_user_project(
74
+ minimal_shared_config: Path,
75
+ core_root: Path,
76
+ tmp_path: Path,
77
+ cli_runner,
78
+ enable_app,
79
+ ) -> None:
80
+ enable_app("claude")
81
+ workspace_root = tmp_path / "workspace"
82
+ (workspace_root / "repo-a" / ".git").mkdir(parents=True)
83
+
84
+ core = CoreRepository(core_root)
85
+ core.add_workspace("myws", workspace_root)
86
+ (core.workspace_config_dir("myws") / "mcp.base.json").write_text(
87
+ json.dumps({"mcpServers": {"ws-server": {"url": "https://example.com/mcp"}}}),
88
+ encoding="utf-8",
89
+ )
90
+
91
+ # User's own project entry — must survive disable untouched.
92
+ config_path = tmp_path / ".claude.json"
93
+ user_project = str((tmp_path / "user-repo").resolve())
94
+ config_path.write_text(
95
+ json.dumps(
96
+ {"projects": {user_project: {"mcpServers": {"personal": {"type": "http"}}}}}
97
+ ),
98
+ encoding="utf-8",
99
+ )
100
+
101
+ assert cli_runner.invoke(cli, ["apply", "-a", "claude"]).exit_code == 0
102
+
103
+ projects = json.loads(config_path.read_text())["projects"]
104
+ repo_key = str((workspace_root / "repo-a").resolve())
105
+ assert "mcpServers" in projects[repo_key]
106
+ state = json.loads((core_root / ".sync-state.json").read_text())
107
+ assert repo_key in state["managed_mcp"]["app:claude:projects"]
108
+
109
+ # Disable → our project mcpServers pruned, user's project kept.
110
+ assert cli_runner.invoke(cli, ["apps", "disable", "-a", "claude"]).exit_code == 0
111
+
112
+ projects = json.loads(config_path.read_text())["projects"]
113
+ assert "mcpServers" not in projects.get(repo_key, {})
114
+ assert projects[user_project] == {"mcpServers": {"personal": {"type": "http"}}}
115
+
116
+ state = json.loads((core_root / ".sync-state.json").read_text())
117
+ assert "app:claude:projects" not in state.get("managed_mcp", {})
@@ -1,5 +1,7 @@
1
1
  """Tests for workspace-level config sync (MCP, skills, agents, rules)."""
2
2
 
3
+ import json
4
+ import shutil
3
5
  from pathlib import Path
4
6
 
5
7
  try:
@@ -747,6 +749,81 @@ def test_workspace_claude_mcp_merges_project_entries_into_home_config(
747
749
  }
748
750
 
749
751
 
752
+ def test_workspace_claude_removed_repo_prunes_project_mcp_keeps_others(
753
+ minimal_shared_config: Path,
754
+ core_root: Path,
755
+ tmp_path: Path,
756
+ write_json,
757
+ ) -> None:
758
+ """P1: a repo removed from a workspace has its `projects.<path>.mcpServers`
759
+ pruned from ~/.claude.json on the next apply, while the other repo, the
760
+ workspace root, and a user-added project entry are all preserved."""
761
+ workspace_root = tmp_path / "workspace"
762
+ workspace_root.mkdir()
763
+ repo_a = workspace_root / "repo-a"
764
+ repo_b = workspace_root / "repo-b"
765
+ (repo_a / ".git").mkdir(parents=True)
766
+ (repo_b / ".git").mkdir(parents=True)
767
+
768
+ core = CoreRepository(core_root)
769
+ core.add_workspace("myws", workspace_root)
770
+
771
+ ws_config = core.workspace_config_dir("myws")
772
+ write_json(
773
+ ws_config / "mcp.base.json",
774
+ {"mcpServers": {"ws-server": {"url": "https://example.com/mcp"}}},
775
+ )
776
+
777
+ # User has their own project entry in ~/.claude.json that we must never touch.
778
+ user_project = str((tmp_path / "user-repo").resolve())
779
+ config_path = tmp_path / ".claude.json"
780
+ write_json(
781
+ config_path,
782
+ {"projects": {user_project: {"mcpServers": {"personal": {"type": "http"}}}}},
783
+ )
784
+
785
+ def _apply() -> dict:
786
+ plan = SyncPlanner(
787
+ core=core,
788
+ app_services=[_claude_service(tmp_path / ".claude")],
789
+ ).build()
790
+ _applied, failed, failures = SyncExecutor(core=core).execute(plan)
791
+ assert failed == 0, failures
792
+ return json.loads(config_path.read_text(encoding="utf-8"))
793
+
794
+ payload = _apply()
795
+ projects = payload["projects"]
796
+ assert str(workspace_root.resolve()) in projects
797
+ assert "mcpServers" in projects[str(repo_a.resolve())]
798
+ assert "mcpServers" in projects[str(repo_b.resolve())]
799
+ assert user_project in projects
800
+
801
+ state = json.loads((core_root / ".sync-state.json").read_text(encoding="utf-8"))
802
+ assert set(state["managed_mcp"]["app:claude:projects"]) == {
803
+ str(workspace_root.resolve()),
804
+ str(repo_a.resolve()),
805
+ str(repo_b.resolve()),
806
+ }
807
+
808
+ # Remove repo-a from the workspace and re-apply.
809
+ shutil.rmtree(repo_a)
810
+ payload = _apply()
811
+ projects = payload["projects"]
812
+
813
+ assert "mcpServers" not in projects.get(
814
+ str(repo_a.resolve()), {}
815
+ ), "removed repo's mcpServers must be pruned"
816
+ assert "mcpServers" in projects[str(repo_b.resolve())]
817
+ assert "mcpServers" in projects[str(workspace_root.resolve())]
818
+ assert projects[user_project] == {"mcpServers": {"personal": {"type": "http"}}}
819
+
820
+ state = json.loads((core_root / ".sync-state.json").read_text(encoding="utf-8"))
821
+ assert set(state["managed_mcp"]["app:claude:projects"]) == {
822
+ str(workspace_root.resolve()),
823
+ str(repo_b.resolve()),
824
+ }
825
+
826
+
750
827
  def test_workspace_targeted_plan_does_not_cleanup_other_app_scopes(
751
828
  minimal_shared_config: Path,
752
829
  core_root: Path,
File without changes
File without changes
File without changes