code-agnostic 0.3.14__tar.gz → 0.3.15__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 (165) hide show
  1. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/PKG-INFO +8 -3
  2. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/README.md +7 -2
  3. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/__init__.py +1 -1
  4. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/claude/mapper.py +5 -0
  5. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/codex/mapper.py +3 -0
  6. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/codex/schema.json +40 -0
  7. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/common/models.py +2 -0
  8. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/common/utils.py +8 -0
  9. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/cursor/mapper.py +6 -0
  10. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/cursor/schema.json +2 -0
  11. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/opencode/mapper.py +5 -0
  12. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/opencode/schema.json +4 -0
  13. code_agnostic-0.3.15/code_agnostic/cli/commands/apply.py +83 -0
  14. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/cli/commands/import_.py +10 -2
  15. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/cli/commands/mcp.py +3 -0
  16. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/cli/commands/restore.py +10 -5
  17. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/cli/commands/skills.py +12 -5
  18. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/cli/helpers.py +14 -0
  19. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/cli/options.py +9 -0
  20. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/core/repository.py +24 -2
  21. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/executor.py +21 -6
  22. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/imports/service.py +1 -1
  23. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/lossiness.py +47 -0
  24. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/mcp_service.py +5 -0
  25. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/spec/loaders.py +2 -0
  26. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/spec/schemas/mcp.base.schema.json +22 -0
  27. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/spec/schemas/mcp.v1.schema.json +9 -1
  28. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/tui/renderers.py +9 -1
  29. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic.egg-info/PKG-INFO +8 -3
  30. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/pyproject.toml +1 -1
  31. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_cli_apply_cursor.py +1 -0
  32. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_cli_apps.py +32 -0
  33. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_cli_explain_lossiness.py +22 -0
  34. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_cli_import.py +1 -0
  35. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_cli_import_interactive.py +2 -0
  36. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_cli_mcp.py +42 -0
  37. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_cli_projects.py +26 -0
  38. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_cli_restore.py +58 -0
  39. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_cli_skills.py +5 -5
  40. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_cli_workspaces.py +26 -0
  41. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_common_mcp_to_dto.py +12 -1
  42. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_common_repository.py +2 -0
  43. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_dto_to_common_mcp.py +4 -0
  44. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_mcp_service.py +20 -0
  45. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_transactional_executor.py +80 -0
  46. code_agnostic-0.3.14/code_agnostic/cli/commands/apply.py +0 -42
  47. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/LICENSE +0 -0
  48. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/__main__.py +0 -0
  49. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/agents/__init__.py +0 -0
  50. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/agents/claude.py +0 -0
  51. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/agents/codex.py +0 -0
  52. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/agents/compilers.py +0 -0
  53. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/agents/models.py +0 -0
  54. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/agents/opencode.py +0 -0
  55. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/agents/parser.py +0 -0
  56. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/__init__.py +0 -0
  57. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/app_id.py +0 -0
  58. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/apps_service.py +0 -0
  59. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/claude/__init__.py +0 -0
  60. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/claude/config_repository.py +0 -0
  61. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/claude/service.py +0 -0
  62. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/codex/__init__.py +0 -0
  63. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/codex/config_repository.py +0 -0
  64. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/codex/schema_repository.py +0 -0
  65. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/codex/service.py +0 -0
  66. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/common/__init__.py +0 -0
  67. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/common/compiled_planning.py +0 -0
  68. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/common/framework.py +0 -0
  69. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/common/interfaces/__init__.py +0 -0
  70. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/common/interfaces/mapper.py +0 -0
  71. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/common/interfaces/repositories.py +0 -0
  72. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/common/interfaces/service.py +0 -0
  73. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/common/loader.py +0 -0
  74. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/common/schema.py +0 -0
  75. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/common/symlink_planning.py +0 -0
  76. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/cursor/__init__.py +0 -0
  77. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/cursor/config_repository.py +0 -0
  78. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/cursor/schema_repository.py +0 -0
  79. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/cursor/service.py +0 -0
  80. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/opencode/__init__.py +0 -0
  81. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/opencode/config_repository.py +0 -0
  82. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/opencode/schema_repository.py +0 -0
  83. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/apps/opencode/service.py +0 -0
  84. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/cli/__init__.py +0 -0
  85. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/cli/aliases.py +0 -0
  86. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/cli/commands/__init__.py +0 -0
  87. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/cli/commands/agents.py +0 -0
  88. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/cli/commands/apps.py +0 -0
  89. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/cli/commands/explain_lossiness.py +0 -0
  90. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/cli/commands/plan.py +0 -0
  91. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/cli/commands/projects.py +0 -0
  92. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/cli/commands/rules.py +0 -0
  93. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/cli/commands/status.py +0 -0
  94. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/cli/commands/validate.py +0 -0
  95. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/cli/commands/workspaces.py +0 -0
  96. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/constants.py +0 -0
  97. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/core/__init__.py +0 -0
  98. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/core/project_repository.py +0 -0
  99. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/core/workspace_repository.py +0 -0
  100. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/errors.py +0 -0
  101. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/generated_artifacts.py +0 -0
  102. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/git_exclude_service.py +0 -0
  103. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/imports/__init__.py +0 -0
  104. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/imports/adapters.py +0 -0
  105. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/imports/filesystem.py +0 -0
  106. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/imports/models.py +0 -0
  107. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/models.py +0 -0
  108. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/planner.py +0 -0
  109. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/project_artifacts.py +0 -0
  110. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/rules/__init__.py +0 -0
  111. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/rules/compilers.py +0 -0
  112. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/rules/models.py +0 -0
  113. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/rules/parser.py +0 -0
  114. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/rules/repository.py +0 -0
  115. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/skills/__init__.py +0 -0
  116. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/skills/compilers.py +0 -0
  117. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/skills/install_sources.py +0 -0
  118. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/skills/models.py +0 -0
  119. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/skills/parser.py +0 -0
  120. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/spec/__init__.py +0 -0
  121. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/spec/schemas/agent.v1.schema.json +0 -0
  122. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/spec/schemas/rule.v1.schema.json +0 -0
  123. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/spec/schemas/skill.v1.schema.json +0 -0
  124. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/status.py +0 -0
  125. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/tui/__init__.py +0 -0
  126. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/tui/enums.py +0 -0
  127. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/tui/import_selector.py +0 -0
  128. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/tui/sections.py +0 -0
  129. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/tui/tables.py +0 -0
  130. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/utils.py +0 -0
  131. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/validation.py +0 -0
  132. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/workspace_artifacts.py +0 -0
  133. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic/workspaces.py +0 -0
  134. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic.egg-info/SOURCES.txt +0 -0
  135. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic.egg-info/dependency_links.txt +0 -0
  136. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic.egg-info/entry_points.txt +0 -0
  137. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic.egg-info/requires.txt +0 -0
  138. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/code_agnostic.egg-info/top_level.txt +0 -0
  139. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/setup.cfg +0 -0
  140. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_cli_agents.py +0 -0
  141. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_cli_aliases.py +0 -0
  142. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_cli_apply_apps.py +0 -0
  143. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_cli_apply_codex.py +0 -0
  144. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_cli_apply_target.py +0 -0
  145. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_cli_flags.py +0 -0
  146. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_cli_git_exclude.py +0 -0
  147. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_cli_module_organization.py +0 -0
  148. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_cli_plan.py +0 -0
  149. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_cli_rules.py +0 -0
  150. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_cli_status.py +0 -0
  151. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_cli_validate.py +0 -0
  152. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_cli_workspace_resolution.py +0 -0
  153. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_compiled_planning.py +0 -0
  154. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_git_exclude_service.py +0 -0
  155. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_planner_executor.py +0 -0
  156. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_planner_rules.py +0 -0
  157. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_project_config_sync.py +0 -0
  158. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_skill_install_sources.py +0 -0
  159. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_symlink_planning.py +0 -0
  160. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_sync_plan_model.py +0 -0
  161. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_utils.py +0 -0
  162. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_version.py +0 -0
  163. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_workspace_config_sync.py +0 -0
  164. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/tests/test_workspace_repo_status.py +0 -0
  165. {code_agnostic-0.3.14 → code_agnostic-0.3.15}/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.14
3
+ Version: 0.3.15
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
@@ -171,6 +171,7 @@ If managed outputs need repair after an apply, restore the active synced revisio
171
171
  ```bash
172
172
  code-agnostic restore
173
173
  code-agnostic restore -w myproject
174
+ code-agnostic restore --project myproject
174
175
  ```
175
176
 
176
177
  ### MCP management
@@ -179,11 +180,14 @@ Add, remove, and list MCP servers without editing JSON by hand.
179
180
 
180
181
  ```bash
181
182
  code-agnostic mcp add github --command npx --args @modelcontextprotocol/server-github --env GITHUB_TOKEN
183
+ code-agnostic mcp add local-docs --command uvx --args docs-mcp --cwd ~/code/docs
182
184
  code-agnostic mcp list
183
185
  code-agnostic mcp remove github
184
186
  ```
185
187
 
186
188
  Env vars without a value (`--env GITHUB_TOKEN`) are stored as `${GITHUB_TOKEN}` references.
189
+ Cursor-local MCP `envFile` can be represented in source config and is reported
190
+ as lossy for targets that do not support environment files.
187
191
 
188
192
  ### Rules with metadata
189
193
 
@@ -317,7 +321,8 @@ code-agnostic import plan -a codex -i # interactive TUI picker
317
321
  only the selected sections. Conflicts are skipped by default, so rerun with
318
322
  `--on-conflict overwrite` only after reviewing the preview. Use `--include`,
319
323
  `--exclude`, `--source-root`, and `--follow-symlinks` to narrow what gets
320
- imported.
324
+ imported. `--source-root` means the source app config root to read from, not the
325
+ `code-agnostic` hub root.
321
326
 
322
327
  ### CLI conventions
323
328
 
@@ -349,7 +354,7 @@ The compiler migration is documented in:
349
354
  - [x] Per-workspace git-exclude customization
350
355
  - [x] Interactive TUI for import selection
351
356
  - [x] Claude Code support
352
- - [ ] Project-scoped skill installs and sync
357
+ - [x] Project-scoped skill installs and sync
353
358
  - [ ] `rules add` / `skills add` / `agents add` commands (open `$EDITOR` with template)
354
359
  - [ ] Shell auto-complete
355
360
  - [ ] Full TUI mode (command palette + menus)
@@ -146,6 +146,7 @@ If managed outputs need repair after an apply, restore the active synced revisio
146
146
  ```bash
147
147
  code-agnostic restore
148
148
  code-agnostic restore -w myproject
149
+ code-agnostic restore --project myproject
149
150
  ```
150
151
 
151
152
  ### MCP management
@@ -154,11 +155,14 @@ Add, remove, and list MCP servers without editing JSON by hand.
154
155
 
155
156
  ```bash
156
157
  code-agnostic mcp add github --command npx --args @modelcontextprotocol/server-github --env GITHUB_TOKEN
158
+ code-agnostic mcp add local-docs --command uvx --args docs-mcp --cwd ~/code/docs
157
159
  code-agnostic mcp list
158
160
  code-agnostic mcp remove github
159
161
  ```
160
162
 
161
163
  Env vars without a value (`--env GITHUB_TOKEN`) are stored as `${GITHUB_TOKEN}` references.
164
+ Cursor-local MCP `envFile` can be represented in source config and is reported
165
+ as lossy for targets that do not support environment files.
162
166
 
163
167
  ### Rules with metadata
164
168
 
@@ -292,7 +296,8 @@ code-agnostic import plan -a codex -i # interactive TUI picker
292
296
  only the selected sections. Conflicts are skipped by default, so rerun with
293
297
  `--on-conflict overwrite` only after reviewing the preview. Use `--include`,
294
298
  `--exclude`, `--source-root`, and `--follow-symlinks` to narrow what gets
295
- imported.
299
+ imported. `--source-root` means the source app config root to read from, not the
300
+ `code-agnostic` hub root.
296
301
 
297
302
  ### CLI conventions
298
303
 
@@ -324,7 +329,7 @@ The compiler migration is documented in:
324
329
  - [x] Per-workspace git-exclude customization
325
330
  - [x] Interactive TUI for import selection
326
331
  - [x] Claude Code support
327
- - [ ] Project-scoped skill installs and sync
332
+ - [x] Project-scoped skill installs and sync
328
333
  - [ ] `rules add` / `skills add` / `agents add` commands (open `$EDITOR` with template)
329
334
  - [ ] Shell auto-complete
330
335
  - [ ] Full TUI mode (command palette + menus)
@@ -1,3 +1,3 @@
1
1
  __all__ = ["__version__"]
2
2
 
3
- __version__ = "0.3.14"
3
+ __version__ = "0.3.15"
@@ -40,6 +40,9 @@ class ClaudeMCPMapper(IAppMCPMapper):
40
40
  type=MCPServerType.STDIO,
41
41
  command=command,
42
42
  args=_as_list(server.get("args")),
43
+ cwd=server.get("cwd")
44
+ if isinstance(server.get("cwd"), str)
45
+ else None,
43
46
  env=_as_str_dict(server.get("env")),
44
47
  headers=_as_str_dict(server.get("headers")),
45
48
  timeout_ms=_timeout(server.get("timeout")),
@@ -70,6 +73,8 @@ class ClaudeMCPMapper(IAppMCPMapper):
70
73
  out["command"] = server.command
71
74
  if server.args:
72
75
  out["args"] = deepcopy(server.args)
76
+ if server.cwd is not None:
77
+ out["cwd"] = server.cwd
73
78
  else:
74
79
  if not server.url:
75
80
  continue
@@ -82,6 +82,7 @@ class CodexMCPMapper(IAppMCPMapper):
82
82
  args=[str(item) for item in server.get("args", [])]
83
83
  if isinstance(server.get("args"), list)
84
84
  else [],
85
+ cwd=server.get("cwd") if isinstance(server.get("cwd"), str) else None,
85
86
  url=url if isinstance(url, str) else None,
86
87
  timeout_ms=_coerce_timeout_ms(server.get("tool_timeout_sec")),
87
88
  headers=headers,
@@ -100,6 +101,8 @@ class CodexMCPMapper(IAppMCPMapper):
100
101
  out["command"] = server.command
101
102
  if server.args:
102
103
  out["args"] = deepcopy(server.args)
104
+ if server.cwd is not None:
105
+ out["cwd"] = server.cwd
103
106
  else:
104
107
  if not server.url:
105
108
  continue
@@ -210,6 +210,14 @@
210
210
  "additionalProperties": false,
211
211
  "description": "Default settings that apply to all apps.",
212
212
  "properties": {
213
+ "approvals_reviewer": {
214
+ "allOf": [
215
+ {
216
+ "$ref": "#/definitions/ApprovalsReviewer"
217
+ }
218
+ ],
219
+ "description": "Reviewer for approval prompts unless overridden by per-app settings."
220
+ },
213
221
  "destructive_enabled": {
214
222
  "description": "Whether tools with `destructive_hint = true` are allowed by default.",
215
223
  "type": "boolean"
@@ -576,6 +584,9 @@
576
584
  "request_rule": {
577
585
  "type": "boolean"
578
586
  },
587
+ "resize_all_images": {
588
+ "type": "boolean"
589
+ },
579
590
  "responses_websockets": {
580
591
  "type": "boolean"
581
592
  },
@@ -588,6 +599,9 @@
588
599
  "search_tool": {
589
600
  "type": "boolean"
590
601
  },
602
+ "secret_auth_storage": {
603
+ "type": "boolean"
604
+ },
591
605
  "shell_snapshot": {
592
606
  "type": "boolean"
593
607
  },
@@ -621,6 +635,9 @@
621
635
  "terminal_visualization_instructions": {
622
636
  "type": "boolean"
623
637
  },
638
+ "token_budget": {
639
+ "type": "boolean"
640
+ },
624
641
  "tool_call_mcp_elicitation": {
625
642
  "type": "boolean"
626
643
  },
@@ -2436,6 +2453,13 @@
2436
2453
  },
2437
2454
  "type": "object"
2438
2455
  },
2456
+ "RealtimeConversationArchitecture": {
2457
+ "enum": [
2458
+ "realtimeapi",
2459
+ "avas"
2460
+ ],
2461
+ "type": "string"
2462
+ },
2439
2463
  "RealtimeConversationVersion": {
2440
2464
  "enum": [
2441
2465
  "v1",
@@ -2446,6 +2470,9 @@
2446
2470
  "RealtimeToml": {
2447
2471
  "additionalProperties": false,
2448
2472
  "properties": {
2473
+ "architecture": {
2474
+ "$ref": "#/definitions/RealtimeConversationArchitecture"
2475
+ },
2449
2476
  "transport": {
2450
2477
  "$ref": "#/definitions/RealtimeTransport"
2451
2478
  },
@@ -4482,6 +4509,10 @@
4482
4509
  "description": "Experimental / do not use. Replaces the built-in realtime start instructions inserted into developer messages when realtime becomes active.",
4483
4510
  "type": "string"
4484
4511
  },
4512
+ "experimental_realtime_webrtc_call_base_url": {
4513
+ "description": "Experimental / do not use. Overrides only the WebRTC realtime call creation base URL. This is separate from `experimental_realtime_ws_base_url` because WebRTC call creation is HTTP, while sideband control is websocket.",
4514
+ "type": "string"
4515
+ },
4485
4516
  "experimental_realtime_ws_backend_prompt": {
4486
4517
  "description": "Experimental / do not use. Overrides only the realtime conversation websocket transport instructions (the `Op::RealtimeConversation` `/ws` session.update instructions) without changing normal prompts.",
4487
4518
  "type": "string"
@@ -4708,6 +4739,9 @@
4708
4739
  "request_rule": {
4709
4740
  "type": "boolean"
4710
4741
  },
4742
+ "resize_all_images": {
4743
+ "type": "boolean"
4744
+ },
4711
4745
  "responses_websockets": {
4712
4746
  "type": "boolean"
4713
4747
  },
@@ -4720,6 +4754,9 @@
4720
4754
  "search_tool": {
4721
4755
  "type": "boolean"
4722
4756
  },
4757
+ "secret_auth_storage": {
4758
+ "type": "boolean"
4759
+ },
4723
4760
  "shell_snapshot": {
4724
4761
  "type": "boolean"
4725
4762
  },
@@ -4753,6 +4790,9 @@
4753
4790
  "terminal_visualization_instructions": {
4754
4791
  "type": "boolean"
4755
4792
  },
4793
+ "token_budget": {
4794
+ "type": "boolean"
4795
+ },
4756
4796
  "tool_call_mcp_elicitation": {
4757
4797
  "type": "boolean"
4758
4798
  },
@@ -22,6 +22,8 @@ class MCPServerDTO:
22
22
  type: MCPServerType
23
23
  command: str | None = None
24
24
  args: list[str] = field(default_factory=list)
25
+ cwd: str | None = None
26
+ env_file: str | None = None
25
27
  url: str | None = None
26
28
  timeout_ms: int | None = None
27
29
  headers: dict[str, str] = field(default_factory=dict)
@@ -67,6 +67,8 @@ def common_mcp_to_dto(mcp_servers: dict[str, Any]) -> dict[str, MCPServerDTO]:
67
67
 
68
68
  command = raw.get("command")
69
69
  args = raw.get("args")
70
+ cwd = raw.get("cwd")
71
+ env_file = raw.get("envFile")
70
72
  url = raw.get("url")
71
73
  timeout_ms = _coerce_timeout_ms(raw.get("timeout"))
72
74
 
@@ -96,6 +98,8 @@ def common_mcp_to_dto(mcp_servers: dict[str, Any]) -> dict[str, MCPServerDTO]:
96
98
  type=MCPServerType.STDIO,
97
99
  command=command,
98
100
  args=[str(item) for item in args] if isinstance(args, list) else [],
101
+ cwd=cwd if isinstance(cwd, str) else None,
102
+ env_file=env_file if isinstance(env_file, str) else None,
99
103
  timeout_ms=timeout_ms,
100
104
  headers={str(k): str(v) for k, v in headers.items()}
101
105
  if isinstance(headers, dict)
@@ -133,6 +137,10 @@ def dto_to_common_mcp(servers: dict[str, MCPServerDTO]) -> dict[str, Any]:
133
137
  if server.command:
134
138
  item["command"] = server.command
135
139
  item["args"] = [str(arg) for arg in server.args]
140
+ if server.cwd is not None:
141
+ item["cwd"] = server.cwd
142
+ if server.env_file is not None:
143
+ item["envFile"] = server.env_file
136
144
  elif server.url:
137
145
  item["url"] = server.url
138
146
  else:
@@ -22,6 +22,9 @@ class CursorMCPMapper(IAppMCPMapper):
22
22
  for item in server.get("args", [])
23
23
  if isinstance(item, (str, int, float, bool))
24
24
  ],
25
+ env_file=server.get("envFile")
26
+ if isinstance(server.get("envFile"), str)
27
+ else None,
25
28
  timeout_ms=(
26
29
  int(server["timeout"])
27
30
  if isinstance(server.get("timeout"), int)
@@ -82,9 +85,12 @@ class CursorMCPMapper(IAppMCPMapper):
82
85
  if server.type == MCPServerType.STDIO:
83
86
  if not server.command:
84
87
  continue
88
+ out["type"] = "stdio"
85
89
  out["command"] = server.command
86
90
  if server.args:
87
91
  out["args"] = deepcopy(server.args)
92
+ if server.env_file is not None:
93
+ out["envFile"] = server.env_file
88
94
  else:
89
95
  if not server.url:
90
96
  continue
@@ -23,12 +23,14 @@
23
23
  "type": "object",
24
24
  "required": ["command"],
25
25
  "properties": {
26
+ "type": { "const": "stdio" },
26
27
  "command": { "type": "string", "minLength": 1 },
27
28
  "args": {
28
29
  "type": "array",
29
30
  "items": { "type": "string" }
30
31
  },
31
32
  "timeout": { "type": "integer", "minimum": 0 },
33
+ "envFile": { "type": "string", "minLength": 1 },
32
34
  "env": { "$ref": "#/$defs/stringMap" },
33
35
  "headers": { "$ref": "#/$defs/stringMap" },
34
36
  "auth": { "$ref": "#/$defs/stringMap" }
@@ -32,6 +32,9 @@ class OpenCodeMCPMapper(IAppMCPMapper):
32
32
  type=MCPServerType.STDIO,
33
33
  command=command,
34
34
  args=args,
35
+ cwd=server.get("cwd")
36
+ if isinstance(server.get("cwd"), str)
37
+ else None,
35
38
  timeout_ms=(
36
39
  int(server["timeout"])
37
40
  if isinstance(server.get("timeout"), int)
@@ -90,6 +93,8 @@ class OpenCodeMCPMapper(IAppMCPMapper):
90
93
  continue
91
94
  out["type"] = "local"
92
95
  out["command"] = [server.command, *server.args]
96
+ if server.cwd is not None:
97
+ out["cwd"] = server.cwd
93
98
  else:
94
99
  if not server.url:
95
100
  continue
@@ -564,6 +564,10 @@
564
564
  },
565
565
  "description": "Command and arguments to run the MCP server"
566
566
  },
567
+ "cwd": {
568
+ "type": "string",
569
+ "description": "Working directory for the MCP server process. Relative paths resolve from the workspace directory."
570
+ },
567
571
  "environment": {
568
572
  "type": "object",
569
573
  "additionalProperties": {
@@ -0,0 +1,83 @@
1
+ """Apply command."""
2
+
3
+ import click
4
+ from rich.console import Console
5
+
6
+ from code_agnostic.apps.apps_service import AppsService
7
+ from code_agnostic.cli.options import app_option, verbose_option
8
+ from code_agnostic.core.repository import CoreRepository
9
+ from code_agnostic.models import SyncPlan
10
+ from code_agnostic.tui import SyncConsoleUI
11
+
12
+
13
+ def _apply_next_steps(plan: SyncPlan, target: str) -> str | None:
14
+ if not plan.actions:
15
+ return None
16
+
17
+ normalized_target = target.lower()
18
+ target_flag = "" if normalized_target == "all" else f" -a {normalized_target}"
19
+ lines = [
20
+ "Managed outputs were written. Check drift after target files change.",
21
+ f"- code-agnostic status{target_flag}",
22
+ ]
23
+
24
+ restore_lines = []
25
+ if any(
26
+ action.workspace is None and action.project is None for action in plan.actions
27
+ ):
28
+ restore_lines.append("- code-agnostic restore")
29
+
30
+ workspace_names = sorted(
31
+ {action.workspace for action in plan.actions if action.workspace}
32
+ )
33
+ for workspace_name in workspace_names[:3]:
34
+ restore_lines.append(f"- code-agnostic restore -w {workspace_name}")
35
+ if len(workspace_names) > 3:
36
+ restore_lines.append("- code-agnostic restore -w <workspace>")
37
+
38
+ if restore_lines:
39
+ lines.append("Repair global/workspace outputs from the active synced revision.")
40
+ lines.extend(restore_lines)
41
+
42
+ if any(action.project is not None for action in plan.actions):
43
+ lines.append(
44
+ "Project outputs are checked by status; project restore is not available yet."
45
+ )
46
+
47
+ return "\n".join(lines)
48
+
49
+
50
+ @click.command(help="Apply planned sync changes.")
51
+ @app_option()
52
+ @verbose_option()
53
+ @click.pass_obj
54
+ def apply(obj: dict[str, str], app: str, verbose: bool) -> None:
55
+ target = app or "all"
56
+ ui = SyncConsoleUI(Console())
57
+ core = CoreRepository()
58
+ apps = AppsService(core)
59
+
60
+ try:
61
+ scoped_plan = apps.plan_for_target(target)
62
+ except Exception as exc:
63
+ raise click.ClickException(f"Fatal: {exc}")
64
+
65
+ ui.render_plan(scoped_plan, mode=f"apply:{target.lower()}", verbose=verbose)
66
+
67
+ if not scoped_plan.actions and not scoped_plan.errors:
68
+ ui.render_apply_result(applied=0, failed=0, failures=[])
69
+ return
70
+
71
+ if scoped_plan.errors:
72
+ raise click.ClickException(
73
+ "Apply aborted due to planning/parsing errors above."
74
+ )
75
+
76
+ applied, failed, failures = apps.execute_plan(scoped_plan)
77
+ next_steps = (
78
+ _apply_next_steps(scoped_plan, target) if applied > 0 and failed == 0 else None
79
+ )
80
+ ui.render_apply_result(applied, failed, failures, next_steps=next_steps)
81
+
82
+ if failed:
83
+ raise click.exceptions.Exit(1)
@@ -45,7 +45,11 @@ def import_group() -> None:
45
45
  default=ConflictPolicy.SKIP.value,
46
46
  show_default=True,
47
47
  )
48
- @click.option("--source-root", type=click.Path(path_type=Path))
48
+ @click.option(
49
+ "--source-root",
50
+ type=click.Path(path_type=Path),
51
+ help="Source app config root to read from.",
52
+ )
49
53
  @click.option("--follow-symlinks", is_flag=True, default=False)
50
54
  @click.option(
51
55
  "-i",
@@ -125,7 +129,11 @@ def import_plan(
125
129
  default=ConflictPolicy.SKIP.value,
126
130
  show_default=True,
127
131
  )
128
- @click.option("--source-root", type=click.Path(path_type=Path))
132
+ @click.option(
133
+ "--source-root",
134
+ type=click.Path(path_type=Path),
135
+ help="Source app config root to read from.",
136
+ )
129
137
  @click.option("--follow-symlinks", is_flag=True, default=False)
130
138
  @click.option(
131
139
  "-i",
@@ -63,6 +63,7 @@ def mcp_list(obj: dict[str, str], workspace: str | None) -> None:
63
63
  @click.option(
64
64
  "--args", "args_str", default=None, help="Comma-separated args for stdio server."
65
65
  )
66
+ @click.option("--cwd", default=None, help="Working directory for stdio server.")
66
67
  @click.option("--url", default=None, help="URL for HTTP/SSE server.")
67
68
  @click.option(
68
69
  "--timeout-ms",
@@ -92,6 +93,7 @@ def mcp_add(
92
93
  name: str,
93
94
  command: str | None,
94
95
  args_str: str | None,
96
+ cwd: str | None,
95
97
  url: str | None,
96
98
  timeout_ms: int | None,
97
99
  env_pairs: tuple[str, ...],
@@ -110,6 +112,7 @@ def mcp_add(
110
112
  name=name,
111
113
  command=command,
112
114
  args=args or None,
115
+ cwd=cwd,
113
116
  url=url,
114
117
  timeout_ms=timeout_ms,
115
118
  env=env or None,
@@ -2,26 +2,31 @@
2
2
 
3
3
  import click
4
4
 
5
- from code_agnostic.cli.helpers import require_workspace_entry
6
- from code_agnostic.cli.options import workspace_option
5
+ from code_agnostic.cli.helpers import require_project_entry, require_workspace_entry
6
+ from code_agnostic.cli.options import project_option, workspace_option
7
7
  from code_agnostic.core.repository import CoreRepository
8
8
  from code_agnostic.executor import SyncExecutor
9
9
 
10
10
 
11
11
  @click.command(
12
- help="Restore the active synced revision for the global root or a workspace."
12
+ help="Restore the active synced revision for the global root, a workspace, or a project."
13
13
  )
14
+ @project_option()
14
15
  @workspace_option()
15
16
  @click.pass_obj
16
- def restore(obj: dict[str, str], workspace: str | None) -> None:
17
+ def restore(obj: dict[str, str], workspace: str | None, project: str | None) -> None:
17
18
  core = CoreRepository()
18
19
 
20
+ if workspace is not None and project is not None:
21
+ raise click.ClickException("Choose only one scope: --workspace or --project.")
19
22
  if workspace is not None:
20
23
  require_workspace_entry(core, workspace)
24
+ if project is not None:
25
+ require_project_entry(core, project)
21
26
 
22
27
  executor = SyncExecutor(core=core)
23
28
  try:
24
- result = executor.restore_active_revision(workspace=workspace)
29
+ result = executor.restore_active_revision(workspace=workspace, project=project)
25
30
  except FileNotFoundError as exc:
26
31
  raise click.ClickException(str(exc))
27
32
 
@@ -79,7 +79,8 @@ def _install_root(
79
79
 
80
80
  raise click.ClickException(
81
81
  "No unique project/workspace scope detected. Use --global, --project, "
82
- "or --workspace."
82
+ "or --workspace. To use this directory as a project, run: "
83
+ "code-agnostic projects add --name <name> --path ."
83
84
  )
84
85
 
85
86
 
@@ -111,15 +112,14 @@ def _validate_skill_source_names(source_dirs: tuple[Path, ...]) -> list[str]:
111
112
 
112
113
  @click.group(
113
114
  help=(
114
- "Manage source skill definitions. Commands use global source by default; "
115
- "pass -w/--workspace for workspace source."
115
+ "Manage source skill definitions across global, workspace, and project scopes."
116
116
  )
117
117
  )
118
118
  def skills() -> None:
119
119
  pass
120
120
 
121
121
 
122
- @skills.command("install", help="Install a skill source into source config.")
122
+ @skills.command("install", help="Install a skill source into managed source config.")
123
123
  @click.argument("source")
124
124
  @click.option(
125
125
  "--skill",
@@ -167,14 +167,20 @@ def skills_install(
167
167
  installs = _preflight_skill_destinations(resolution.skill_dirs, root, scope)
168
168
  for _name, _source_dir, destination in installs:
169
169
  destination.parent.mkdir(parents=True, exist_ok=True)
170
+ installed = False
170
171
  for name, source_dir, destination in installs:
171
172
  shutil.copytree(source_dir, destination)
173
+ installed = True
172
174
  if scope_note is not None:
173
175
  click.echo(scope_note)
174
176
  scope_note = None
175
177
  click.echo(f"Installed {scope} skill: {name}")
176
178
  click.echo(f"Source: {compact_home_path(source_dir)}")
177
179
  click.echo(f"Destination: {compact_home_path(destination)}")
180
+ if installed:
181
+ click.echo("Next:")
182
+ click.echo(" code-agnostic plan")
183
+ click.echo(" code-agnostic apply")
178
184
  except SkillInstallSourceError as exc:
179
185
  raise click.ClickException(str(exc)) from exc
180
186
  finally:
@@ -206,9 +212,10 @@ def skills_list(obj: dict[str, str], workspace: str | None) -> None:
206
212
  if workspace
207
213
  else "No global skills configured"
208
214
  )
215
+ install_scope = f"--workspace {workspace}" if workspace else "--global"
209
216
  empty_message = (
210
217
  f"{scope_message} in {skill_dir}.\n"
211
- f"- Copy a skill into {skill_dir}/<name>\n"
218
+ f"- code-agnostic skills install <source> {install_scope}\n"
212
219
  "- code-agnostic plan\n"
213
220
  "- code-agnostic apply"
214
221
  )
@@ -14,6 +14,10 @@ def _workspace_entries_by_name(core: CoreRepository) -> dict[str, dict[str, str]
14
14
  return {item["name"]: item for item in core.load_workspaces()}
15
15
 
16
16
 
17
+ def _project_entries_by_name(core: CoreRepository) -> dict[str, dict[str, str]]:
18
+ return {item["name"]: item for item in core.load_projects()}
19
+
20
+
17
21
  def require_workspace_entry(core: CoreRepository, workspace: str) -> dict[str, str]:
18
22
  try:
19
23
  entry = _workspace_entries_by_name(core).get(workspace)
@@ -24,6 +28,16 @@ def require_workspace_entry(core: CoreRepository, workspace: str) -> dict[str, s
24
28
  return entry
25
29
 
26
30
 
31
+ def require_project_entry(core: CoreRepository, project: str) -> dict[str, str]:
32
+ try:
33
+ entry = _project_entries_by_name(core).get(project)
34
+ except SyncAppError as exc:
35
+ raise click.ClickException(str(exc)) from exc
36
+ if entry is None:
37
+ raise click.ClickException(f"Project not found: {project}")
38
+ return entry
39
+
40
+
27
41
  def workspace_config_root(core: CoreRepository, workspace: str | None) -> Path:
28
42
  if workspace is None:
29
43
  return core.root
@@ -79,5 +79,14 @@ def workspace_option(required: bool = False) -> Callable:
79
79
  )
80
80
 
81
81
 
82
+ def project_option(required: bool = False) -> Callable:
83
+ return click.option(
84
+ "--project",
85
+ required=required,
86
+ default=None,
87
+ help="Project name.",
88
+ )
89
+
90
+
82
91
  def verbose_option() -> Callable:
83
92
  return click.option("-v", "--verbose", is_flag=True, default=False)