code-agnostic 0.3.13__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.13 → code_agnostic-0.3.15}/PKG-INFO +26 -6
  2. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/README.md +25 -5
  3. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/__init__.py +1 -1
  4. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/claude/mapper.py +5 -0
  5. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/codex/mapper.py +3 -0
  6. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/codex/schema.json +40 -0
  7. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/common/models.py +2 -0
  8. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/common/utils.py +8 -0
  9. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/cursor/mapper.py +6 -0
  10. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/cursor/schema.json +2 -0
  11. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/opencode/mapper.py +5 -0
  12. {code_agnostic-0.3.13 → 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.13 → code_agnostic-0.3.15}/code_agnostic/cli/commands/import_.py +10 -2
  15. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/cli/commands/mcp.py +3 -0
  16. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/cli/commands/restore.py +10 -5
  17. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/cli/commands/skills.py +85 -34
  18. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/cli/helpers.py +14 -0
  19. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/cli/options.py +9 -0
  20. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/core/repository.py +24 -2
  21. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/executor.py +21 -6
  22. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/imports/service.py +1 -1
  23. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/lossiness.py +47 -0
  24. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/mcp_service.py +5 -0
  25. code_agnostic-0.3.15/code_agnostic/skills/install_sources.py +369 -0
  26. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/spec/loaders.py +2 -0
  27. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/spec/schemas/mcp.base.schema.json +22 -0
  28. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/spec/schemas/mcp.v1.schema.json +9 -1
  29. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/tui/renderers.py +9 -1
  30. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic.egg-info/PKG-INFO +26 -6
  31. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic.egg-info/SOURCES.txt +2 -0
  32. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/pyproject.toml +1 -1
  33. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_cli_apply_cursor.py +1 -0
  34. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_cli_apps.py +32 -0
  35. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_cli_explain_lossiness.py +22 -0
  36. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_cli_import.py +1 -0
  37. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_cli_import_interactive.py +2 -0
  38. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_cli_mcp.py +42 -0
  39. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_cli_projects.py +26 -0
  40. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_cli_restore.py +58 -0
  41. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_cli_skills.py +93 -6
  42. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_cli_workspaces.py +26 -0
  43. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_common_mcp_to_dto.py +12 -1
  44. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_common_repository.py +2 -0
  45. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_dto_to_common_mcp.py +4 -0
  46. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_mcp_service.py +20 -0
  47. code_agnostic-0.3.15/tests/test_skill_install_sources.py +182 -0
  48. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_transactional_executor.py +80 -0
  49. code_agnostic-0.3.13/code_agnostic/cli/commands/apply.py +0 -42
  50. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/LICENSE +0 -0
  51. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/__main__.py +0 -0
  52. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/agents/__init__.py +0 -0
  53. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/agents/claude.py +0 -0
  54. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/agents/codex.py +0 -0
  55. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/agents/compilers.py +0 -0
  56. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/agents/models.py +0 -0
  57. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/agents/opencode.py +0 -0
  58. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/agents/parser.py +0 -0
  59. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/__init__.py +0 -0
  60. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/app_id.py +0 -0
  61. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/apps_service.py +0 -0
  62. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/claude/__init__.py +0 -0
  63. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/claude/config_repository.py +0 -0
  64. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/claude/service.py +0 -0
  65. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/codex/__init__.py +0 -0
  66. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/codex/config_repository.py +0 -0
  67. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/codex/schema_repository.py +0 -0
  68. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/codex/service.py +0 -0
  69. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/common/__init__.py +0 -0
  70. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/common/compiled_planning.py +0 -0
  71. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/common/framework.py +0 -0
  72. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/common/interfaces/__init__.py +0 -0
  73. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/common/interfaces/mapper.py +0 -0
  74. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/common/interfaces/repositories.py +0 -0
  75. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/common/interfaces/service.py +0 -0
  76. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/common/loader.py +0 -0
  77. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/common/schema.py +0 -0
  78. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/common/symlink_planning.py +0 -0
  79. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/cursor/__init__.py +0 -0
  80. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/cursor/config_repository.py +0 -0
  81. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/cursor/schema_repository.py +0 -0
  82. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/cursor/service.py +0 -0
  83. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/opencode/__init__.py +0 -0
  84. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/opencode/config_repository.py +0 -0
  85. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/opencode/schema_repository.py +0 -0
  86. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/apps/opencode/service.py +0 -0
  87. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/cli/__init__.py +0 -0
  88. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/cli/aliases.py +0 -0
  89. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/cli/commands/__init__.py +0 -0
  90. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/cli/commands/agents.py +0 -0
  91. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/cli/commands/apps.py +0 -0
  92. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/cli/commands/explain_lossiness.py +0 -0
  93. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/cli/commands/plan.py +0 -0
  94. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/cli/commands/projects.py +0 -0
  95. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/cli/commands/rules.py +0 -0
  96. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/cli/commands/status.py +0 -0
  97. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/cli/commands/validate.py +0 -0
  98. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/cli/commands/workspaces.py +0 -0
  99. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/constants.py +0 -0
  100. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/core/__init__.py +0 -0
  101. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/core/project_repository.py +0 -0
  102. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/core/workspace_repository.py +0 -0
  103. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/errors.py +0 -0
  104. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/generated_artifacts.py +0 -0
  105. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/git_exclude_service.py +0 -0
  106. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/imports/__init__.py +0 -0
  107. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/imports/adapters.py +0 -0
  108. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/imports/filesystem.py +0 -0
  109. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/imports/models.py +0 -0
  110. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/models.py +0 -0
  111. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/planner.py +0 -0
  112. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/project_artifacts.py +0 -0
  113. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/rules/__init__.py +0 -0
  114. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/rules/compilers.py +0 -0
  115. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/rules/models.py +0 -0
  116. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/rules/parser.py +0 -0
  117. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/rules/repository.py +0 -0
  118. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/skills/__init__.py +0 -0
  119. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/skills/compilers.py +0 -0
  120. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/skills/models.py +0 -0
  121. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/skills/parser.py +0 -0
  122. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/spec/__init__.py +0 -0
  123. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/spec/schemas/agent.v1.schema.json +0 -0
  124. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/spec/schemas/rule.v1.schema.json +0 -0
  125. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/spec/schemas/skill.v1.schema.json +0 -0
  126. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/status.py +0 -0
  127. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/tui/__init__.py +0 -0
  128. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/tui/enums.py +0 -0
  129. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/tui/import_selector.py +0 -0
  130. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/tui/sections.py +0 -0
  131. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/tui/tables.py +0 -0
  132. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/utils.py +0 -0
  133. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/validation.py +0 -0
  134. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/workspace_artifacts.py +0 -0
  135. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic/workspaces.py +0 -0
  136. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic.egg-info/dependency_links.txt +0 -0
  137. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic.egg-info/entry_points.txt +0 -0
  138. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic.egg-info/requires.txt +0 -0
  139. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/code_agnostic.egg-info/top_level.txt +0 -0
  140. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/setup.cfg +0 -0
  141. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_cli_agents.py +0 -0
  142. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_cli_aliases.py +0 -0
  143. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_cli_apply_apps.py +0 -0
  144. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_cli_apply_codex.py +0 -0
  145. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_cli_apply_target.py +0 -0
  146. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_cli_flags.py +0 -0
  147. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_cli_git_exclude.py +0 -0
  148. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_cli_module_organization.py +0 -0
  149. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_cli_plan.py +0 -0
  150. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_cli_rules.py +0 -0
  151. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_cli_status.py +0 -0
  152. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_cli_validate.py +0 -0
  153. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_cli_workspace_resolution.py +0 -0
  154. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_compiled_planning.py +0 -0
  155. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_git_exclude_service.py +0 -0
  156. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_planner_executor.py +0 -0
  157. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_planner_rules.py +0 -0
  158. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_project_config_sync.py +0 -0
  159. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_symlink_planning.py +0 -0
  160. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_sync_plan_model.py +0 -0
  161. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_utils.py +0 -0
  162. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_version.py +0 -0
  163. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_workspace_config_sync.py +0 -0
  164. {code_agnostic-0.3.13 → code_agnostic-0.3.15}/tests/test_workspace_repo_status.py +0 -0
  165. {code_agnostic-0.3.13 → 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.13
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
 
@@ -230,7 +234,8 @@ code-agnostic skills list
230
234
  code-agnostic agents list
231
235
  ```
232
236
 
233
- Install a local skill directory into managed source:
237
+ Install copies a skill into managed source first for the chosen scope. Run
238
+ `plan` / `apply` afterward to generate target app files.
234
239
 
235
240
  ```bash
236
241
  code-agnostic skills install ./my-skill --global
@@ -241,6 +246,20 @@ code-agnostic plan
241
246
  code-agnostic apply
242
247
  ```
243
248
 
249
+ Remote GitHub-style sources are also supported:
250
+
251
+ ```bash
252
+ code-agnostic skills install owner/repo --global
253
+ code-agnostic skills install https://github.com/owner/repo --workspace myworkspace
254
+ code-agnostic skills install https://github.com/owner/repo/tree/main/path/to/skills --skill reviewer --skill triage --project myproject
255
+ ```
256
+
257
+ Remote sources may be an `owner/repo` shorthand, a GitHub repository URL, or a
258
+ GitHub tree URL. When a remote source contains multiple skill candidates,
259
+ repeat `--skill` to select the intended skills; otherwise install fails instead
260
+ of guessing. Remote installs still copy the selected skill into the
261
+ `code-agnostic` source of truth before any generated target outputs are written.
262
+
244
263
  Global skills live under `~/.config/code-agnostic/skills`. Workspace-local
245
264
  skills live under `~/.config/code-agnostic/workspaces/<name>/skills` and can be
246
265
  inspected with `code-agnostic skills list -w <name>`. Project-local skills live
@@ -252,7 +271,7 @@ skills and agents are written under `~/.claude/skills` and `~/.claude/agents`,
252
271
  with workspace/project copies under repo-local `.claude/skills` and
253
272
  `.claude/agents`.
254
273
 
255
- 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`.
274
+ If a target app discovers user-created repo-local skill folders such as `.agents/skills`, `.opencode/skills`, or `.claude/skills`, treat those as unmanaged app inputs unless they were generated from `code-agnostic` project source. Workspace and project sync write only the exact generated paths recorded in their `.sync-state.json` files.
256
275
 
257
276
  Planned convenience command:
258
277
 
@@ -266,7 +285,7 @@ first implementation slice.
266
285
 
267
286
  ### Workspaces
268
287
 
269
- 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 write project-root `opencode.json` files that 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"]`. Workspace source config, skills, and agents are propagated into repo-local generated paths for OpenCode, Cursor, Codex, and Claude; user-created project-local skill folders remain unmanaged until project-scoped installs are supported.
288
+ 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 write project-root `opencode.json` files that 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"]`. Workspace source config, skills, and agents are propagated into repo-local generated paths for OpenCode, Cursor, Codex, and Claude; user-created target app skill folders remain unmanaged unless they were generated from managed source.
270
289
 
271
290
  Cursor propagation intentionally stays to repo-local MCP, skills, and agents; it does not copy the shared workspace `AGENTS.md` into child repos.
272
291
 
@@ -302,7 +321,8 @@ code-agnostic import plan -a codex -i # interactive TUI picker
302
321
  only the selected sections. Conflicts are skipped by default, so rerun with
303
322
  `--on-conflict overwrite` only after reviewing the preview. Use `--include`,
304
323
  `--exclude`, `--source-root`, and `--follow-symlinks` to narrow what gets
305
- imported.
324
+ imported. `--source-root` means the source app config root to read from, not the
325
+ `code-agnostic` hub root.
306
326
 
307
327
  ### CLI conventions
308
328
 
@@ -334,7 +354,7 @@ The compiler migration is documented in:
334
354
  - [x] Per-workspace git-exclude customization
335
355
  - [x] Interactive TUI for import selection
336
356
  - [x] Claude Code support
337
- - [ ] Project-scoped skill installs and sync
357
+ - [x] Project-scoped skill installs and sync
338
358
  - [ ] `rules add` / `skills add` / `agents add` commands (open `$EDITOR` with template)
339
359
  - [ ] Shell auto-complete
340
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
 
@@ -205,7 +209,8 @@ code-agnostic skills list
205
209
  code-agnostic agents list
206
210
  ```
207
211
 
208
- Install a local skill directory into managed source:
212
+ Install copies a skill into managed source first for the chosen scope. Run
213
+ `plan` / `apply` afterward to generate target app files.
209
214
 
210
215
  ```bash
211
216
  code-agnostic skills install ./my-skill --global
@@ -216,6 +221,20 @@ code-agnostic plan
216
221
  code-agnostic apply
217
222
  ```
218
223
 
224
+ Remote GitHub-style sources are also supported:
225
+
226
+ ```bash
227
+ code-agnostic skills install owner/repo --global
228
+ code-agnostic skills install https://github.com/owner/repo --workspace myworkspace
229
+ code-agnostic skills install https://github.com/owner/repo/tree/main/path/to/skills --skill reviewer --skill triage --project myproject
230
+ ```
231
+
232
+ Remote sources may be an `owner/repo` shorthand, a GitHub repository URL, or a
233
+ GitHub tree URL. When a remote source contains multiple skill candidates,
234
+ repeat `--skill` to select the intended skills; otherwise install fails instead
235
+ of guessing. Remote installs still copy the selected skill into the
236
+ `code-agnostic` source of truth before any generated target outputs are written.
237
+
219
238
  Global skills live under `~/.config/code-agnostic/skills`. Workspace-local
220
239
  skills live under `~/.config/code-agnostic/workspaces/<name>/skills` and can be
221
240
  inspected with `code-agnostic skills list -w <name>`. Project-local skills live
@@ -227,7 +246,7 @@ skills and agents are written under `~/.claude/skills` and `~/.claude/agents`,
227
246
  with workspace/project copies under repo-local `.claude/skills` and
228
247
  `.claude/agents`.
229
248
 
230
- 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`.
249
+ If a target app discovers user-created repo-local skill folders such as `.agents/skills`, `.opencode/skills`, or `.claude/skills`, treat those as unmanaged app inputs unless they were generated from `code-agnostic` project source. Workspace and project sync write only the exact generated paths recorded in their `.sync-state.json` files.
231
250
 
232
251
  Planned convenience command:
233
252
 
@@ -241,7 +260,7 @@ first implementation slice.
241
260
 
242
261
  ### Workspaces
243
262
 
244
- 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 write project-root `opencode.json` files that 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"]`. Workspace source config, skills, and agents are propagated into repo-local generated paths for OpenCode, Cursor, Codex, and Claude; user-created project-local skill folders remain unmanaged until project-scoped installs are supported.
263
+ 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 write project-root `opencode.json` files that 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"]`. Workspace source config, skills, and agents are propagated into repo-local generated paths for OpenCode, Cursor, Codex, and Claude; user-created target app skill folders remain unmanaged unless they were generated from managed source.
245
264
 
246
265
  Cursor propagation intentionally stays to repo-local MCP, skills, and agents; it does not copy the shared workspace `AGENTS.md` into child repos.
247
266
 
@@ -277,7 +296,8 @@ code-agnostic import plan -a codex -i # interactive TUI picker
277
296
  only the selected sections. Conflicts are skipped by default, so rerun with
278
297
  `--on-conflict overwrite` only after reviewing the preview. Use `--include`,
279
298
  `--exclude`, `--source-root`, and `--follow-symlinks` to narrow what gets
280
- imported.
299
+ imported. `--source-root` means the source app config root to read from, not the
300
+ `code-agnostic` hub root.
281
301
 
282
302
  ### CLI conventions
283
303
 
@@ -309,7 +329,7 @@ The compiler migration is documented in:
309
329
  - [x] Per-workspace git-exclude customization
310
330
  - [x] Interactive TUI for import selection
311
331
  - [x] Claude Code support
312
- - [ ] Project-scoped skill installs and sync
332
+ - [x] Project-scoped skill installs and sync
313
333
  - [ ] `rules add` / `skills add` / `agents add` commands (open `$EDITOR` with template)
314
334
  - [ ] Shell auto-complete
315
335
  - [ ] Full TUI mode (command palette + menus)
@@ -1,3 +1,3 @@
1
1
  __all__ = ["__version__"]
2
2
 
3
- __version__ = "0.3.13"
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