code-agnostic 0.3.14__tar.gz → 0.3.16__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 (166) hide show
  1. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/PKG-INFO +17 -9
  2. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/README.md +16 -8
  3. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/__init__.py +1 -1
  4. code_agnostic-0.3.16/code_agnostic/agents/compilers.py +87 -0
  5. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/claude/mapper.py +5 -0
  6. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/codex/mapper.py +31 -2
  7. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/codex/schema.json +112 -0
  8. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/common/models.py +2 -0
  9. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/common/utils.py +8 -0
  10. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/cursor/mapper.py +6 -0
  11. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/cursor/schema.json +2 -0
  12. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/opencode/mapper.py +5 -0
  13. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/opencode/schema.json +4 -0
  14. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/cli/commands/agents.py +6 -1
  15. code_agnostic-0.3.16/code_agnostic/cli/commands/apply.py +86 -0
  16. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/cli/commands/import_.py +10 -2
  17. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/cli/commands/mcp.py +3 -0
  18. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/cli/commands/restore.py +10 -5
  19. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/cli/commands/rules.py +6 -1
  20. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/cli/commands/skills.py +74 -17
  21. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/cli/commands/status.py +1 -1
  22. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/cli/helpers.py +21 -0
  23. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/cli/options.py +9 -0
  24. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/core/repository.py +43 -5
  25. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/executor.py +21 -6
  26. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/imports/service.py +1 -1
  27. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/lossiness.py +59 -2
  28. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/mcp_service.py +5 -0
  29. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/spec/loaders.py +2 -0
  30. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/spec/schemas/mcp.base.schema.json +22 -0
  31. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/spec/schemas/mcp.v1.schema.json +9 -1
  32. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/status.py +2 -1
  33. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/tui/renderers.py +66 -14
  34. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/tui/tables.py +3 -1
  35. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic.egg-info/PKG-INFO +17 -9
  36. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/pyproject.toml +1 -1
  37. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_cli_agents.py +20 -0
  38. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_cli_apply_cursor.py +1 -0
  39. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_cli_apps.py +33 -0
  40. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_cli_explain_lossiness.py +55 -0
  41. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_cli_import.py +25 -0
  42. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_cli_import_interactive.py +2 -0
  43. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_cli_mcp.py +42 -0
  44. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_cli_projects.py +26 -0
  45. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_cli_restore.py +58 -0
  46. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_cli_rules.py +16 -0
  47. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_cli_skills.py +128 -5
  48. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_cli_status.py +26 -0
  49. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_cli_workspaces.py +67 -0
  50. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_common_mcp_to_dto.py +12 -1
  51. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_common_repository.py +5 -4
  52. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_dto_to_common_mcp.py +4 -0
  53. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_mcp_service.py +20 -0
  54. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_project_config_sync.py +31 -0
  55. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_transactional_executor.py +80 -0
  56. code_agnostic-0.3.14/code_agnostic/agents/compilers.py +0 -45
  57. code_agnostic-0.3.14/code_agnostic/cli/commands/apply.py +0 -42
  58. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/LICENSE +0 -0
  59. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/__main__.py +0 -0
  60. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/agents/__init__.py +0 -0
  61. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/agents/claude.py +0 -0
  62. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/agents/codex.py +0 -0
  63. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/agents/models.py +0 -0
  64. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/agents/opencode.py +0 -0
  65. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/agents/parser.py +0 -0
  66. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/__init__.py +0 -0
  67. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/app_id.py +0 -0
  68. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/apps_service.py +0 -0
  69. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/claude/__init__.py +0 -0
  70. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/claude/config_repository.py +0 -0
  71. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/claude/service.py +0 -0
  72. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/codex/__init__.py +0 -0
  73. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/codex/config_repository.py +0 -0
  74. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/codex/schema_repository.py +0 -0
  75. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/codex/service.py +0 -0
  76. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/common/__init__.py +0 -0
  77. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/common/compiled_planning.py +0 -0
  78. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/common/framework.py +0 -0
  79. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/common/interfaces/__init__.py +0 -0
  80. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/common/interfaces/mapper.py +0 -0
  81. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/common/interfaces/repositories.py +0 -0
  82. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/common/interfaces/service.py +0 -0
  83. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/common/loader.py +0 -0
  84. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/common/schema.py +0 -0
  85. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/common/symlink_planning.py +0 -0
  86. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/cursor/__init__.py +0 -0
  87. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/cursor/config_repository.py +0 -0
  88. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/cursor/schema_repository.py +0 -0
  89. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/cursor/service.py +0 -0
  90. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/opencode/__init__.py +0 -0
  91. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/opencode/config_repository.py +0 -0
  92. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/opencode/schema_repository.py +0 -0
  93. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/apps/opencode/service.py +0 -0
  94. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/cli/__init__.py +0 -0
  95. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/cli/aliases.py +0 -0
  96. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/cli/commands/__init__.py +0 -0
  97. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/cli/commands/apps.py +0 -0
  98. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/cli/commands/explain_lossiness.py +0 -0
  99. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/cli/commands/plan.py +0 -0
  100. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/cli/commands/projects.py +0 -0
  101. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/cli/commands/validate.py +0 -0
  102. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/cli/commands/workspaces.py +0 -0
  103. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/constants.py +0 -0
  104. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/core/__init__.py +0 -0
  105. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/core/project_repository.py +0 -0
  106. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/core/workspace_repository.py +0 -0
  107. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/errors.py +0 -0
  108. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/generated_artifacts.py +0 -0
  109. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/git_exclude_service.py +0 -0
  110. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/imports/__init__.py +0 -0
  111. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/imports/adapters.py +0 -0
  112. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/imports/filesystem.py +0 -0
  113. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/imports/models.py +0 -0
  114. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/models.py +0 -0
  115. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/planner.py +0 -0
  116. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/project_artifacts.py +0 -0
  117. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/rules/__init__.py +0 -0
  118. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/rules/compilers.py +0 -0
  119. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/rules/models.py +0 -0
  120. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/rules/parser.py +0 -0
  121. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/rules/repository.py +0 -0
  122. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/skills/__init__.py +0 -0
  123. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/skills/compilers.py +0 -0
  124. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/skills/install_sources.py +0 -0
  125. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/skills/models.py +0 -0
  126. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/skills/parser.py +0 -0
  127. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/spec/__init__.py +0 -0
  128. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/spec/schemas/agent.v1.schema.json +0 -0
  129. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/spec/schemas/rule.v1.schema.json +0 -0
  130. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/spec/schemas/skill.v1.schema.json +0 -0
  131. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/tui/__init__.py +0 -0
  132. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/tui/enums.py +0 -0
  133. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/tui/import_selector.py +0 -0
  134. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/tui/sections.py +0 -0
  135. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/utils.py +0 -0
  136. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/validation.py +0 -0
  137. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/workspace_artifacts.py +0 -0
  138. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic/workspaces.py +0 -0
  139. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic.egg-info/SOURCES.txt +0 -0
  140. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic.egg-info/dependency_links.txt +0 -0
  141. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic.egg-info/entry_points.txt +0 -0
  142. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic.egg-info/requires.txt +0 -0
  143. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/code_agnostic.egg-info/top_level.txt +0 -0
  144. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/setup.cfg +0 -0
  145. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_cli_aliases.py +0 -0
  146. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_cli_apply_apps.py +0 -0
  147. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_cli_apply_codex.py +0 -0
  148. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_cli_apply_target.py +0 -0
  149. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_cli_flags.py +0 -0
  150. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_cli_git_exclude.py +0 -0
  151. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_cli_module_organization.py +0 -0
  152. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_cli_plan.py +0 -0
  153. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_cli_validate.py +0 -0
  154. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_cli_workspace_resolution.py +0 -0
  155. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_compiled_planning.py +0 -0
  156. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_git_exclude_service.py +0 -0
  157. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_planner_executor.py +0 -0
  158. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_planner_rules.py +0 -0
  159. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_skill_install_sources.py +0 -0
  160. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_symlink_planning.py +0 -0
  161. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_sync_plan_model.py +0 -0
  162. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_utils.py +0 -0
  163. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_version.py +0 -0
  164. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_workspace_config_sync.py +0 -0
  165. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/tests/test_workspace_repo_status.py +0 -0
  166. {code_agnostic-0.3.14 → code_agnostic-0.3.16}/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.16
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
 
@@ -260,12 +264,14 @@ Global skills live under `~/.config/code-agnostic/skills`. Workspace-local
260
264
  skills live under `~/.config/code-agnostic/workspaces/<name>/skills` and can be
261
265
  inspected with `code-agnostic skills list -w <name>`. Project-local skills live
262
266
  under `~/.config/code-agnostic/projects/<name>/skills` and are generated into
263
- the registered project directory by `plan` / `apply`. Codex generated skill
264
- outputs are written to `~/.agents/skills`, while Codex agents and config remain
265
- under `CODEX_HOME` when set, defaulting to `~/.codex`. Claude Code generated
266
- skills and agents are written under `~/.claude/skills` and `~/.claude/agents`,
267
- with workspace/project copies under repo-local `.claude/skills` and
268
- `.claude/agents`.
267
+ the registered project directory by `plan` / `apply`. The current first-class
268
+ list/remove commands cover global and workspace scopes; project skill inventory
269
+ and removal are not supported yet, and `code-agnostic projects list` only shows
270
+ a coarse `[skills]` marker today. Codex generated skill outputs are written to
271
+ `~/.agents/skills`, while Codex agents and config remain under `CODEX_HOME` when
272
+ set, defaulting to `~/.codex`. Claude Code generated skills and agents are
273
+ written under `~/.claude/skills` and `~/.claude/agents`, with workspace/project
274
+ copies under repo-local `.claude/skills` and `.claude/agents`.
269
275
 
270
276
  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.
271
277
 
@@ -317,7 +323,8 @@ code-agnostic import plan -a codex -i # interactive TUI picker
317
323
  only the selected sections. Conflicts are skipped by default, so rerun with
318
324
  `--on-conflict overwrite` only after reviewing the preview. Use `--include`,
319
325
  `--exclude`, `--source-root`, and `--follow-symlinks` to narrow what gets
320
- imported.
326
+ imported. `--source-root` means the source app config root to read from, not the
327
+ `code-agnostic` hub root.
321
328
 
322
329
  ### CLI conventions
323
330
 
@@ -349,7 +356,8 @@ The compiler migration is documented in:
349
356
  - [x] Per-workspace git-exclude customization
350
357
  - [x] Interactive TUI for import selection
351
358
  - [x] Claude Code support
352
- - [ ] Project-scoped skill installs and sync
359
+ - [x] Project-scoped skill installs and sync
360
+ - [ ] Project-scoped skill inventory and remove commands
353
361
  - [ ] `rules add` / `skills add` / `agents add` commands (open `$EDITOR` with template)
354
362
  - [ ] Shell auto-complete
355
363
  - [ ] 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
 
@@ -235,12 +239,14 @@ Global skills live under `~/.config/code-agnostic/skills`. Workspace-local
235
239
  skills live under `~/.config/code-agnostic/workspaces/<name>/skills` and can be
236
240
  inspected with `code-agnostic skills list -w <name>`. Project-local skills live
237
241
  under `~/.config/code-agnostic/projects/<name>/skills` and are generated into
238
- the registered project directory by `plan` / `apply`. Codex generated skill
239
- outputs are written to `~/.agents/skills`, while Codex agents and config remain
240
- under `CODEX_HOME` when set, defaulting to `~/.codex`. Claude Code generated
241
- skills and agents are written under `~/.claude/skills` and `~/.claude/agents`,
242
- with workspace/project copies under repo-local `.claude/skills` and
243
- `.claude/agents`.
242
+ the registered project directory by `plan` / `apply`. The current first-class
243
+ list/remove commands cover global and workspace scopes; project skill inventory
244
+ and removal are not supported yet, and `code-agnostic projects list` only shows
245
+ a coarse `[skills]` marker today. Codex generated skill outputs are written to
246
+ `~/.agents/skills`, while Codex agents and config remain under `CODEX_HOME` when
247
+ set, defaulting to `~/.codex`. Claude Code generated skills and agents are
248
+ written under `~/.claude/skills` and `~/.claude/agents`, with workspace/project
249
+ copies under repo-local `.claude/skills` and `.claude/agents`.
244
250
 
245
251
  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.
246
252
 
@@ -292,7 +298,8 @@ code-agnostic import plan -a codex -i # interactive TUI picker
292
298
  only the selected sections. Conflicts are skipped by default, so rerun with
293
299
  `--on-conflict overwrite` only after reviewing the preview. Use `--include`,
294
300
  `--exclude`, `--source-root`, and `--follow-symlinks` to narrow what gets
295
- imported.
301
+ imported. `--source-root` means the source app config root to read from, not the
302
+ `code-agnostic` hub root.
296
303
 
297
304
  ### CLI conventions
298
305
 
@@ -324,7 +331,8 @@ The compiler migration is documented in:
324
331
  - [x] Per-workspace git-exclude customization
325
332
  - [x] Interactive TUI for import selection
326
333
  - [x] Claude Code support
327
- - [ ] Project-scoped skill installs and sync
334
+ - [x] Project-scoped skill installs and sync
335
+ - [ ] Project-scoped skill inventory and remove commands
328
336
  - [ ] `rules add` / `skills add` / `agents add` commands (open `$EDITOR` with template)
329
337
  - [ ] Shell auto-complete
330
338
  - [ ] 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.16"
@@ -0,0 +1,87 @@
1
+ """Per-editor agent compilers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import Any
7
+
8
+ import yaml
9
+
10
+ from code_agnostic.agents.claude import serialize_claude_agent
11
+ from code_agnostic.agents.codex import serialize_codex_agent
12
+ from code_agnostic.agents.models import Agent
13
+ from code_agnostic.agents.opencode import serialize_opencode_agent
14
+ from code_agnostic.errors import InvalidConfigSchemaError
15
+
16
+ _CURSOR_AGENT_FRONTMATTER_KEYS = frozenset(
17
+ {"name", "description", "model", "readonly", "is_background"}
18
+ )
19
+
20
+
21
+ class IAgentCompiler(ABC):
22
+ @abstractmethod
23
+ def compile(self, agent: Agent) -> str:
24
+ """Return compiled agent content for target editor."""
25
+
26
+
27
+ class OpenCodeAgentCompiler(IAgentCompiler):
28
+ """Cross-compile for OpenCode agents."""
29
+
30
+ def compile(self, agent: Agent) -> str:
31
+ return serialize_opencode_agent(agent)
32
+
33
+
34
+ class CursorAgentCompiler(IAgentCompiler):
35
+ """Cross-compile for Cursor subagents."""
36
+
37
+ def compile(self, agent: Agent) -> str:
38
+ return _serialize_cursor_agent(agent)
39
+
40
+
41
+ def _serialize_cursor_agent(agent: Agent) -> str:
42
+ overrides = dict(agent.metadata.app_overrides.get("cursor", {}))
43
+ fm: dict[str, Any] = {}
44
+ if agent.metadata.name:
45
+ fm["name"] = agent.metadata.name
46
+ if agent.metadata.description:
47
+ fm["description"] = agent.metadata.description
48
+
49
+ model = overrides.pop("model", agent.metadata.model)
50
+ if model:
51
+ fm["model"] = model
52
+
53
+ if agent.metadata.tools.write is False:
54
+ fm["readonly"] = True
55
+
56
+ for key in sorted(overrides):
57
+ if key not in _CURSOR_AGENT_FRONTMATTER_KEYS:
58
+ allowed = ", ".join(sorted(_CURSOR_AGENT_FRONTMATTER_KEYS))
59
+ raise InvalidConfigSchemaError(
60
+ agent.source_path,
61
+ f"x-cursor.{key} is not supported in cursor agent "
62
+ f"frontmatter; allowed keys: {allowed}",
63
+ )
64
+ fm[key] = overrides[key]
65
+
66
+ parts: list[str] = []
67
+ if fm:
68
+ parts.append("---")
69
+ parts.append(yaml.dump(fm, default_flow_style=False, sort_keys=False).rstrip())
70
+ parts.append("---")
71
+ parts.append("")
72
+ parts.append(agent.content)
73
+ return "\n".join(parts)
74
+
75
+
76
+ class CodexAgentCompiler(IAgentCompiler):
77
+ """Cross-compile for Codex subagents."""
78
+
79
+ def compile(self, agent: Agent) -> str:
80
+ return serialize_codex_agent(agent)
81
+
82
+
83
+ class ClaudeAgentCompiler(IAgentCompiler):
84
+ """Cross-compile for Claude Code subagents."""
85
+
86
+ def compile(self, agent: Agent) -> str:
87
+ return serialize_claude_agent(agent)
@@ -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
@@ -1,9 +1,11 @@
1
1
  import re
2
2
  from copy import deepcopy
3
+ from pathlib import Path
3
4
  from typing import Any
4
5
 
5
6
  from code_agnostic.apps.common.interfaces.mapper import IAppMCPMapper
6
7
  from code_agnostic.apps.common.models import MCPServerDTO, MCPServerType
8
+ from code_agnostic.errors import InvalidConfigSchemaError
7
9
 
8
10
  _ENV_PATTERN = re.compile(r"^\$\{(?:env:)?([A-Z_][A-Z0-9_]*)\}$")
9
11
  _BEARER_PATTERN = re.compile(r"^Bearer\s+\$\{(?:env:)?([A-Z_][A-Z0-9_]*)\}$")
@@ -34,6 +36,29 @@ def _encode_tool_timeout(timeout_ms: int) -> int | float:
34
36
  return int(seconds) if timeout_ms % 1000 == 0 else seconds
35
37
 
36
38
 
39
+ def _codex_env_var_name(server_name: str, entry: Any) -> str | None:
40
+ if isinstance(entry, str):
41
+ return entry
42
+ if not isinstance(entry, dict):
43
+ return None
44
+
45
+ name = entry.get("name")
46
+ if not isinstance(name, str):
47
+ return None
48
+
49
+ source = entry.get("source", "local")
50
+ if source in (None, "local"):
51
+ return name
52
+
53
+ detail = (
54
+ "Codex env_vars entries with source = 'remote' cannot be imported "
55
+ "into common MCP config without losing remote-executor semantics"
56
+ if source == "remote"
57
+ else f"Unsupported Codex env_vars source: {source!r}"
58
+ )
59
+ raise InvalidConfigSchemaError(Path(f"mcp_servers.{server_name}.env_vars"), detail)
60
+
61
+
37
62
  class CodexMCPMapper(IAppMCPMapper):
38
63
  def to_common(self, payload: dict[str, Any]) -> dict[str, MCPServerDTO]:
39
64
  mapped: dict[str, MCPServerDTO] = {}
@@ -49,8 +74,9 @@ class CodexMCPMapper(IAppMCPMapper):
49
74
  env: dict[str, str] = {}
50
75
  env_vars = server.get("env_vars")
51
76
  if isinstance(env_vars, list):
52
- for key in env_vars:
53
- if isinstance(key, str):
77
+ for entry in env_vars:
78
+ key = _codex_env_var_name(name, entry)
79
+ if key is not None:
54
80
  env[key] = f"${{{key}}}"
55
81
  env_table = server.get("env")
56
82
  if isinstance(env_table, dict):
@@ -82,6 +108,7 @@ class CodexMCPMapper(IAppMCPMapper):
82
108
  args=[str(item) for item in server.get("args", [])]
83
109
  if isinstance(server.get("args"), list)
84
110
  else [],
111
+ cwd=server.get("cwd") if isinstance(server.get("cwd"), str) else None,
85
112
  url=url if isinstance(url, str) else None,
86
113
  timeout_ms=_coerce_timeout_ms(server.get("tool_timeout_sec")),
87
114
  headers=headers,
@@ -100,6 +127,8 @@ class CodexMCPMapper(IAppMCPMapper):
100
127
  out["command"] = server.command
101
128
  if server.args:
102
129
  out["args"] = deepcopy(server.args)
130
+ if server.cwd is not None:
131
+ out["cwd"] = server.cwd
103
132
  else:
104
133
  if not server.url:
105
134
  continue
@@ -210,6 +210,22 @@
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
+ },
221
+ "default_tools_approval_mode": {
222
+ "allOf": [
223
+ {
224
+ "$ref": "#/definitions/AppToolApproval"
225
+ }
226
+ ],
227
+ "description": "Approval mode for tools unless overridden by per-app or per-tool settings."
228
+ },
213
229
  "destructive_enabled": {
214
230
  "description": "Whether tools with `destructive_hint = true` are allowed by default.",
215
231
  "type": "boolean"
@@ -346,6 +362,13 @@
346
362
  "CodeModeConfigToml": {
347
363
  "additionalProperties": false,
348
364
  "properties": {
365
+ "direct_only_tool_namespaces": {
366
+ "description": "Exact tool namespaces to expose only as direct model tools. These tools bypass deferral, remain top-level in code-mode-only sessions, and are omitted from the nested code-mode tool surface.",
367
+ "items": {
368
+ "type": "string"
369
+ },
370
+ "type": "array"
371
+ },
349
372
  "enabled": {
350
373
  "type": "boolean"
351
374
  },
@@ -576,18 +599,30 @@
576
599
  "request_rule": {
577
600
  "type": "boolean"
578
601
  },
602
+ "resize_all_images": {
603
+ "type": "boolean"
604
+ },
579
605
  "responses_websockets": {
580
606
  "type": "boolean"
581
607
  },
582
608
  "responses_websockets_v2": {
583
609
  "type": "boolean"
584
610
  },
611
+ "rollout_budget": {
612
+ "$ref": "#/definitions/FeatureToml_for_RolloutBudgetConfigToml"
613
+ },
614
+ "respect_system_proxy": {
615
+ "type": "boolean"
616
+ },
585
617
  "runtime_metrics": {
586
618
  "type": "boolean"
587
619
  },
588
620
  "search_tool": {
589
621
  "type": "boolean"
590
622
  },
623
+ "secret_auth_storage": {
624
+ "type": "boolean"
625
+ },
591
626
  "shell_snapshot": {
592
627
  "type": "boolean"
593
628
  },
@@ -603,6 +638,9 @@
603
638
  "skill_mcp_dependency_install": {
604
639
  "type": "boolean"
605
640
  },
641
+ "sleep_tool": {
642
+ "type": "boolean"
643
+ },
606
644
  "sqlite": {
607
645
  "type": "boolean"
608
646
  },
@@ -621,6 +659,9 @@
621
659
  "terminal_visualization_instructions": {
622
660
  "type": "boolean"
623
661
  },
662
+ "token_budget": {
663
+ "type": "boolean"
664
+ },
624
665
  "tool_call_mcp_elicitation": {
625
666
  "type": "boolean"
626
667
  },
@@ -870,6 +911,16 @@
870
911
  }
871
912
  ]
872
913
  },
914
+ "FeatureToml_for_RolloutBudgetConfigToml": {
915
+ "anyOf": [
916
+ {
917
+ "type": "boolean"
918
+ },
919
+ {
920
+ "$ref": "#/definitions/RolloutBudgetConfigToml"
921
+ }
922
+ ]
923
+ },
873
924
  "FeedbackConfigToml": {
874
925
  "additionalProperties": false,
875
926
  "properties": {
@@ -2436,6 +2487,13 @@
2436
2487
  },
2437
2488
  "type": "object"
2438
2489
  },
2490
+ "RealtimeConversationArchitecture": {
2491
+ "enum": [
2492
+ "realtimeapi",
2493
+ "avas"
2494
+ ],
2495
+ "type": "string"
2496
+ },
2439
2497
  "RealtimeConversationVersion": {
2440
2498
  "enum": [
2441
2499
  "v1",
@@ -2446,6 +2504,9 @@
2446
2504
  "RealtimeToml": {
2447
2505
  "additionalProperties": false,
2448
2506
  "properties": {
2507
+ "architecture": {
2508
+ "$ref": "#/definitions/RealtimeConversationArchitecture"
2509
+ },
2449
2510
  "transport": {
2450
2511
  "$ref": "#/definitions/RealtimeTransport"
2451
2512
  },
@@ -2524,6 +2585,35 @@
2524
2585
  }
2525
2586
  ]
2526
2587
  },
2588
+ "RolloutBudgetConfigToml": {
2589
+ "additionalProperties": false,
2590
+ "properties": {
2591
+ "enabled": {
2592
+ "type": "boolean"
2593
+ },
2594
+ "limit_tokens": {
2595
+ "format": "int64",
2596
+ "minimum": 1.0,
2597
+ "type": "integer"
2598
+ },
2599
+ "prefill_token_weight": {
2600
+ "format": "double",
2601
+ "minimum": 0.0,
2602
+ "type": "number"
2603
+ },
2604
+ "reminder_interval_tokens": {
2605
+ "format": "int64",
2606
+ "minimum": 1.0,
2607
+ "type": "integer"
2608
+ },
2609
+ "sampling_token_weight": {
2610
+ "format": "double",
2611
+ "minimum": 0.0,
2612
+ "type": "number"
2613
+ }
2614
+ },
2615
+ "type": "object"
2616
+ },
2527
2617
  "SandboxMode": {
2528
2618
  "enum": [
2529
2619
  "read-only",
@@ -4482,6 +4572,10 @@
4482
4572
  "description": "Experimental / do not use. Replaces the built-in realtime start instructions inserted into developer messages when realtime becomes active.",
4483
4573
  "type": "string"
4484
4574
  },
4575
+ "experimental_realtime_webrtc_call_base_url": {
4576
+ "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.",
4577
+ "type": "string"
4578
+ },
4485
4579
  "experimental_realtime_ws_backend_prompt": {
4486
4580
  "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
4581
  "type": "string"
@@ -4708,18 +4802,30 @@
4708
4802
  "request_rule": {
4709
4803
  "type": "boolean"
4710
4804
  },
4805
+ "resize_all_images": {
4806
+ "type": "boolean"
4807
+ },
4711
4808
  "responses_websockets": {
4712
4809
  "type": "boolean"
4713
4810
  },
4714
4811
  "responses_websockets_v2": {
4715
4812
  "type": "boolean"
4716
4813
  },
4814
+ "rollout_budget": {
4815
+ "$ref": "#/definitions/FeatureToml_for_RolloutBudgetConfigToml"
4816
+ },
4817
+ "respect_system_proxy": {
4818
+ "type": "boolean"
4819
+ },
4717
4820
  "runtime_metrics": {
4718
4821
  "type": "boolean"
4719
4822
  },
4720
4823
  "search_tool": {
4721
4824
  "type": "boolean"
4722
4825
  },
4826
+ "secret_auth_storage": {
4827
+ "type": "boolean"
4828
+ },
4723
4829
  "shell_snapshot": {
4724
4830
  "type": "boolean"
4725
4831
  },
@@ -4735,6 +4841,9 @@
4735
4841
  "skill_mcp_dependency_install": {
4736
4842
  "type": "boolean"
4737
4843
  },
4844
+ "sleep_tool": {
4845
+ "type": "boolean"
4846
+ },
4738
4847
  "sqlite": {
4739
4848
  "type": "boolean"
4740
4849
  },
@@ -4753,6 +4862,9 @@
4753
4862
  "terminal_visualization_instructions": {
4754
4863
  "type": "boolean"
4755
4864
  },
4865
+ "token_budget": {
4866
+ "type": "boolean"
4867
+ },
4756
4868
  "tool_call_mcp_elicitation": {
4757
4869
  "type": "boolean"
4758
4870
  },
@@ -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": {
@@ -5,7 +5,11 @@ import shutil
5
5
  import click
6
6
  from rich.console import Console
7
7
 
8
- from code_agnostic.cli.helpers import validate_resource_name, workspace_config_root
8
+ from code_agnostic.cli.helpers import (
9
+ reject_symlinked_source_dir,
10
+ validate_resource_name,
11
+ workspace_config_root,
12
+ )
9
13
  from code_agnostic.cli.options import workspace_option
10
14
  from code_agnostic.core.repository import CoreRepository
11
15
  from code_agnostic.tui import SyncConsoleUI
@@ -36,6 +40,7 @@ def agents_remove(obj: dict[str, str], name: str, workspace: str | None) -> None
36
40
  validate_resource_name(name, "agent")
37
41
  core = CoreRepository()
38
42
  root = workspace_config_root(core, workspace)
43
+ reject_symlinked_source_dir(root / "agents", "agents")
39
44
  agent_dir = root / "agents" / name
40
45
  if agent_dir.is_dir():
41
46
  shutil.rmtree(agent_dir)