code-agnostic 0.3.2__tar.gz → 0.3.3__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 (155) hide show
  1. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/PKG-INFO +1 -1
  2. code_agnostic-0.3.3/code_agnostic/__main__.py +62 -0
  3. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/app_id.py +22 -4
  4. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/codex/config_repository.py +10 -4
  5. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/codex/schema.json +27 -0
  6. code_agnostic-0.3.3/code_agnostic/apps/codex/service.py +268 -0
  7. code_agnostic-0.3.3/code_agnostic/apps/common/interfaces/service.py +262 -0
  8. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/cursor/config_repository.py +10 -4
  9. code_agnostic-0.3.3/code_agnostic/apps/cursor/service.py +162 -0
  10. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/opencode/config_repository.py +8 -3
  11. code_agnostic-0.3.3/code_agnostic/apps/opencode/service.py +210 -0
  12. code_agnostic-0.3.3/code_agnostic/cli/__init__.py +29 -0
  13. code_agnostic-0.3.3/code_agnostic/cli/aliases.py +20 -0
  14. code_agnostic-0.3.3/code_agnostic/cli/commands/__init__.py +33 -0
  15. code_agnostic-0.3.3/code_agnostic/cli/commands/agents.py +47 -0
  16. code_agnostic-0.3.3/code_agnostic/cli/commands/apply.py +42 -0
  17. code_agnostic-0.3.3/code_agnostic/cli/commands/apps.py +45 -0
  18. code_agnostic-0.3.3/code_agnostic/cli/commands/explain_lossiness.py +45 -0
  19. code_agnostic-0.3.3/code_agnostic/cli/commands/import_.py +189 -0
  20. code_agnostic-0.3.3/code_agnostic/cli/commands/mcp.py +122 -0
  21. code_agnostic-0.3.3/code_agnostic/cli/commands/plan.py +30 -0
  22. code_agnostic-0.3.3/code_agnostic/cli/commands/restore.py +28 -0
  23. code_agnostic-0.3.3/code_agnostic/cli/commands/rules.py +46 -0
  24. code_agnostic-0.3.3/code_agnostic/cli/commands/skills.py +42 -0
  25. code_agnostic-0.3.3/code_agnostic/cli/commands/status.py +52 -0
  26. code_agnostic-0.3.3/code_agnostic/cli/commands/validate.py +30 -0
  27. code_agnostic-0.3.3/code_agnostic/cli/commands/workspaces.py +186 -0
  28. code_agnostic-0.3.3/code_agnostic/cli/helpers.py +87 -0
  29. code_agnostic-0.3.3/code_agnostic/cli/options.py +95 -0
  30. code_agnostic-0.3.3/code_agnostic/constants.py +26 -0
  31. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/core/repository.py +4 -3
  32. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/executor.py +24 -13
  33. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/planner.py +21 -6
  34. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/status.py +15 -19
  35. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic.egg-info/PKG-INFO +1 -1
  36. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic.egg-info/SOURCES.txt +20 -0
  37. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/pyproject.toml +1 -1
  38. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_apply_apps.py +149 -0
  39. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_apply_codex.py +14 -0
  40. code_agnostic-0.3.3/tests/test_cli_module_organization.py +55 -0
  41. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_workspaces.py +1 -1
  42. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_git_exclude_service.py +2 -2
  43. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_planner_rules.py +1 -2
  44. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_workspace_config_sync.py +117 -21
  45. code_agnostic-0.3.3/tests/test_workspace_repo_status.py +240 -0
  46. code_agnostic-0.3.2/code_agnostic/__main__.py +0 -1071
  47. code_agnostic-0.3.2/code_agnostic/apps/codex/service.py +0 -365
  48. code_agnostic-0.3.2/code_agnostic/apps/common/interfaces/service.py +0 -84
  49. code_agnostic-0.3.2/code_agnostic/apps/cursor/service.py +0 -291
  50. code_agnostic-0.3.2/code_agnostic/apps/opencode/service.py +0 -353
  51. code_agnostic-0.3.2/code_agnostic/constants.py +0 -13
  52. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/LICENSE +0 -0
  53. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/README.md +0 -0
  54. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/__init__.py +0 -0
  55. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/agents/__init__.py +0 -0
  56. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/agents/codex.py +0 -0
  57. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/agents/compilers.py +0 -0
  58. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/agents/models.py +0 -0
  59. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/agents/opencode.py +0 -0
  60. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/agents/parser.py +0 -0
  61. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/__init__.py +0 -0
  62. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/apps_service.py +0 -0
  63. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/codex/__init__.py +0 -0
  64. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/codex/mapper.py +0 -0
  65. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/codex/schema_repository.py +0 -0
  66. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/common/__init__.py +0 -0
  67. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/common/compiled_planning.py +0 -0
  68. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/common/framework.py +0 -0
  69. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/common/interfaces/__init__.py +0 -0
  70. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/common/interfaces/mapper.py +0 -0
  71. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/common/interfaces/repositories.py +0 -0
  72. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/common/loader.py +0 -0
  73. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/common/models.py +0 -0
  74. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/common/schema.py +0 -0
  75. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/common/symlink_planning.py +0 -0
  76. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/common/utils.py +0 -0
  77. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/cursor/__init__.py +0 -0
  78. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/cursor/mapper.py +0 -0
  79. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/cursor/schema.json +0 -0
  80. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/cursor/schema_repository.py +0 -0
  81. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/opencode/__init__.py +0 -0
  82. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/opencode/mapper.py +0 -0
  83. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/opencode/schema.json +0 -0
  84. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/opencode/schema_repository.py +0 -0
  85. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/core/__init__.py +0 -0
  86. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/core/workspace_repository.py +0 -0
  87. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/errors.py +0 -0
  88. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/git_exclude_service.py +0 -0
  89. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/imports/__init__.py +0 -0
  90. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/imports/adapters.py +0 -0
  91. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/imports/filesystem.py +0 -0
  92. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/imports/models.py +0 -0
  93. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/imports/service.py +0 -0
  94. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/lossiness.py +0 -0
  95. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/mcp_service.py +0 -0
  96. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/models.py +0 -0
  97. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/rules/__init__.py +0 -0
  98. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/rules/compilers.py +0 -0
  99. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/rules/models.py +0 -0
  100. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/rules/parser.py +0 -0
  101. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/rules/repository.py +0 -0
  102. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/skills/__init__.py +0 -0
  103. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/skills/compilers.py +0 -0
  104. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/skills/models.py +0 -0
  105. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/skills/parser.py +0 -0
  106. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/spec/__init__.py +0 -0
  107. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/spec/loaders.py +0 -0
  108. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/spec/schemas/agent.v1.schema.json +0 -0
  109. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/spec/schemas/mcp.base.schema.json +0 -0
  110. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/spec/schemas/mcp.v1.schema.json +0 -0
  111. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/spec/schemas/rule.v1.schema.json +0 -0
  112. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/spec/schemas/skill.v1.schema.json +0 -0
  113. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/tui/__init__.py +0 -0
  114. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/tui/enums.py +0 -0
  115. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/tui/import_selector.py +0 -0
  116. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/tui/renderers.py +0 -0
  117. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/tui/sections.py +0 -0
  118. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/tui/tables.py +0 -0
  119. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/utils.py +0 -0
  120. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/validation.py +0 -0
  121. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/workspaces.py +0 -0
  122. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic.egg-info/dependency_links.txt +0 -0
  123. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic.egg-info/entry_points.txt +0 -0
  124. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic.egg-info/requires.txt +0 -0
  125. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic.egg-info/top_level.txt +0 -0
  126. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/setup.cfg +0 -0
  127. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_agents.py +0 -0
  128. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_aliases.py +0 -0
  129. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_apply_cursor.py +0 -0
  130. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_apply_target.py +0 -0
  131. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_apps.py +0 -0
  132. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_explain_lossiness.py +0 -0
  133. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_flags.py +0 -0
  134. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_git_exclude.py +0 -0
  135. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_import.py +0 -0
  136. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_import_interactive.py +0 -0
  137. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_mcp.py +0 -0
  138. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_plan.py +0 -0
  139. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_restore.py +0 -0
  140. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_rules.py +0 -0
  141. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_skills.py +0 -0
  142. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_status.py +0 -0
  143. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_validate.py +0 -0
  144. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_workspace_resolution.py +0 -0
  145. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_common_mcp_to_dto.py +0 -0
  146. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_common_repository.py +0 -0
  147. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_compiled_planning.py +0 -0
  148. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_dto_to_common_mcp.py +0 -0
  149. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_mcp_service.py +0 -0
  150. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_planner_executor.py +0 -0
  151. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_symlink_planning.py +0 -0
  152. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_sync_plan_model.py +0 -0
  153. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_transactional_executor.py +0 -0
  154. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_utils.py +0 -0
  155. {code_agnostic-0.3.2 → code_agnostic-0.3.3}/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.2
3
+ Version: 0.3.3
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
@@ -0,0 +1,62 @@
1
+ """CLI entrypoint - thin wrapper that wires command modules."""
2
+
3
+ import click
4
+
5
+ from code_agnostic.cli import AliasedGroup
6
+ from code_agnostic.cli.commands.agents import agents_group
7
+ from code_agnostic.cli.commands.apps import apps
8
+ from code_agnostic.cli.commands.apply import apply
9
+ from code_agnostic.cli.commands.explain_lossiness import explain_lossiness
10
+ from code_agnostic.cli.commands.import_ import import_group
11
+ from code_agnostic.cli.commands.mcp import mcp
12
+ from code_agnostic.cli.commands.plan import plan
13
+ from code_agnostic.cli.commands.restore import restore
14
+ from code_agnostic.cli.commands.rules import rules
15
+ from code_agnostic.cli.commands.skills import skills
16
+ from code_agnostic.cli.commands.status import status
17
+ from code_agnostic.cli.commands.validate import validate
18
+ from code_agnostic.cli.commands.workspaces import workspaces
19
+
20
+
21
+ @click.group(
22
+ cls=AliasedGroup,
23
+ context_settings={"help_option_names": ["-h", "--help"]},
24
+ )
25
+ @click.pass_context
26
+ def cli(ctx: click.Context) -> None:
27
+ """App-based config sync."""
28
+ ctx.obj = {}
29
+
30
+
31
+ # Register individual commands
32
+ cli.add_command(plan)
33
+ cli.add_command(apply)
34
+ cli.add_command(restore)
35
+ cli.add_command(status)
36
+ cli.add_command(validate)
37
+ cli.add_command(explain_lossiness)
38
+
39
+ # Register command groups
40
+ cli.add_command(apps)
41
+ cli.add_command(workspaces)
42
+ cli.add_command(rules)
43
+ cli.add_command(skills)
44
+ cli.add_command(agents_group)
45
+ cli.add_command(mcp)
46
+ cli.add_command(import_group)
47
+
48
+
49
+ def main() -> int:
50
+ try:
51
+ cli(standalone_mode=False)
52
+ except click.exceptions.Exit as exc:
53
+ code = exc.exit_code
54
+ return code if isinstance(code, int) else 1
55
+ except click.ClickException as exc:
56
+ exc.show()
57
+ return 2
58
+ return 0
59
+
60
+
61
+ if __name__ == "__main__":
62
+ raise SystemExit(main())
@@ -1,6 +1,15 @@
1
1
  from enum import Enum
2
2
  from dataclasses import dataclass
3
3
 
4
+ from code_agnostic.constants import (
5
+ CODEX_CONFIG_FILENAME,
6
+ CODEX_PROJECT_DIRNAME,
7
+ CURSOR_CONFIG_FILENAME,
8
+ CURSOR_PROJECT_DIRNAME,
9
+ OPENCODE_CONFIG_FILENAME,
10
+ OPENCODE_PROJECT_DIRNAME,
11
+ )
12
+
4
13
 
5
14
  class AppId(str, Enum):
6
15
  CORE = "core"
@@ -19,6 +28,7 @@ class AppMetadata:
19
28
  supports_import_agents: bool
20
29
  supports_workspace_propagation: bool
21
30
  project_dir_name: str | None = None
31
+ config_filename: str | None = None
22
32
 
23
33
 
24
34
  APP_CATALOG: dict[AppId, AppMetadata] = {
@@ -40,7 +50,8 @@ APP_CATALOG: dict[AppId, AppMetadata] = {
40
50
  importable=True,
41
51
  supports_import_agents=True,
42
52
  supports_workspace_propagation=True,
43
- project_dir_name=".opencode",
53
+ project_dir_name=OPENCODE_PROJECT_DIRNAME,
54
+ config_filename=OPENCODE_CONFIG_FILENAME,
44
55
  ),
45
56
  AppId.CURSOR: AppMetadata(
46
57
  app_id=AppId.CURSOR,
@@ -49,8 +60,9 @@ APP_CATALOG: dict[AppId, AppMetadata] = {
49
60
  toggleable=True,
50
61
  importable=True,
51
62
  supports_import_agents=True,
52
- supports_workspace_propagation=False,
53
- project_dir_name=".cursor",
63
+ supports_workspace_propagation=True,
64
+ project_dir_name=CURSOR_PROJECT_DIRNAME,
65
+ config_filename=CURSOR_CONFIG_FILENAME,
54
66
  ),
55
67
  AppId.CODEX: AppMetadata(
56
68
  app_id=AppId.CODEX,
@@ -60,7 +72,8 @@ APP_CATALOG: dict[AppId, AppMetadata] = {
60
72
  importable=True,
61
73
  supports_import_agents=True,
62
74
  supports_workspace_propagation=True,
63
- project_dir_name=".codex",
75
+ project_dir_name=CODEX_PROJECT_DIRNAME,
76
+ config_filename=CODEX_CONFIG_FILENAME,
64
77
  ),
65
78
  }
66
79
 
@@ -74,6 +87,11 @@ def app_label(app: AppId | str) -> str:
74
87
  return app_metadata(app).label
75
88
 
76
89
 
90
+ def app_scope(app: AppId | str, resource: str) -> str:
91
+ app_id = app if isinstance(app, AppId) else AppId(app)
92
+ return f"app:{app_id.value}:{resource}"
93
+
94
+
77
95
  def app_ids_by_capability(
78
96
  *,
79
97
  targetable: bool | None = None,
@@ -9,12 +9,18 @@ except ModuleNotFoundError: # pragma: no cover
9
9
  import tomlkit
10
10
 
11
11
  from code_agnostic.apps.common.interfaces.repositories import IAppConfigRepository
12
+ from code_agnostic.constants import (
13
+ AGENTS_DIRNAME,
14
+ CODEX_CONFIG_FILENAME,
15
+ CODEX_PROJECT_DIRNAME,
16
+ SKILLS_DIRNAME,
17
+ )
12
18
  from code_agnostic.errors import InvalidConfigSchemaError, InvalidJsonFormatError
13
19
 
14
20
 
15
21
  class CodexConfigRepository(IAppConfigRepository):
16
22
  def __init__(self, root: Path | None = None) -> None:
17
- self._root = root or (Path.home() / ".codex")
23
+ self._root = root or (Path.home() / CODEX_PROJECT_DIRNAME)
18
24
 
19
25
  @property
20
26
  def root(self) -> Path:
@@ -22,15 +28,15 @@ class CodexConfigRepository(IAppConfigRepository):
22
28
 
23
29
  @property
24
30
  def config_path(self) -> Path:
25
- return self.root / "config.toml"
31
+ return self.root / CODEX_CONFIG_FILENAME
26
32
 
27
33
  @property
28
34
  def skills_dir(self) -> Path:
29
- return self.root / "skills"
35
+ return self.root / SKILLS_DIRNAME
30
36
 
31
37
  @property
32
38
  def agents_dir(self) -> Path:
33
- return self.root / "agents"
39
+ return self.root / AGENTS_DIRNAME
34
40
 
35
41
  def load_config(self) -> dict[str, Any]:
36
42
  if not self.config_path.exists() or self.config_path.stat().st_size == 0:
@@ -18,6 +18,10 @@
18
18
  "AbsolutePathBuf": {
19
19
  "type": "string"
20
20
  },
21
+ "AppToolApproval": {
22
+ "enum": ["auto", "prompt", "approve"],
23
+ "type": "string"
24
+ },
21
25
  "AgentRoleToml": {
22
26
  "type": "object",
23
27
  "additionalProperties": false,
@@ -41,6 +45,17 @@
41
45
  "$ref": "#/definitions/AgentRoleToml"
42
46
  }
43
47
  },
48
+ "McpServerToolConfig": {
49
+ "type": "object",
50
+ "additionalProperties": false,
51
+ "properties": {
52
+ "approval_mode": {
53
+ "allOf": [{ "$ref": "#/definitions/AppToolApproval" }],
54
+ "description": "Approval mode for this tool."
55
+ }
56
+ },
57
+ "description": "Per-tool approval settings for a single MCP server tool."
58
+ },
44
59
  "RawMcpServerConfig": {
45
60
  "type": "object",
46
61
  "additionalProperties": false,
@@ -57,12 +72,24 @@
57
72
  "env_http_headers": { "type": "object", "additionalProperties": { "type": "string" } },
58
73
  "env_vars": { "type": "array", "items": { "type": "string" } },
59
74
  "http_headers": { "type": "object", "additionalProperties": { "type": "string" } },
75
+ "name": {
76
+ "type": "string",
77
+ "default": null,
78
+ "description": "Legacy display-name field accepted for backward compatibility."
79
+ },
60
80
  "oauth_resource": { "type": "string", "default": null },
61
81
  "required": { "type": "boolean" },
62
82
  "scopes": { "type": "array", "items": { "type": "string" } },
63
83
  "startup_timeout_ms": { "type": "integer", "minimum": 0 },
64
84
  "startup_timeout_sec": { "type": "number" },
65
85
  "tool_timeout_sec": { "type": "number" },
86
+ "tools": {
87
+ "type": "object",
88
+ "default": null,
89
+ "additionalProperties": {
90
+ "$ref": "#/definitions/McpServerToolConfig"
91
+ }
92
+ },
66
93
  "url": { "type": "string" }
67
94
  }
68
95
  }
@@ -0,0 +1,268 @@
1
+ from copy import deepcopy
2
+ from pathlib import Path
3
+ from typing import Any
4
+
5
+ from jsonschema import Draft7Validator
6
+
7
+ from code_agnostic.agents.codex import normalize_codex_agent_filename
8
+ from code_agnostic.agents.compilers import CodexAgentCompiler
9
+ from code_agnostic.agents.parser import parse_agent
10
+ from code_agnostic.apps.app_id import AppId, app_label
11
+ from code_agnostic.apps.common.framework import (
12
+ RegisteredAppConfigService,
13
+ format_schema_error,
14
+ )
15
+ from code_agnostic.apps.codex.config_repository import CodexConfigRepository
16
+ from code_agnostic.apps.codex.mapper import CodexMCPMapper
17
+ from code_agnostic.apps.codex.schema_repository import CodexSchemaRepository
18
+ from code_agnostic.apps.common.interfaces.mapper import IAppMCPMapper
19
+ from code_agnostic.apps.common.interfaces.repositories import (
20
+ IAppConfigRepository,
21
+ ISchemaRepository,
22
+ )
23
+ from code_agnostic.apps.common.models import MCPServerDTO
24
+ from code_agnostic.errors import (
25
+ InvalidConfigSchemaError,
26
+ InvalidJsonFormatError,
27
+ )
28
+ from code_agnostic.models import Action, ActionKind, ActionStatus
29
+ from code_agnostic.utils import read_json_safe
30
+ from code_agnostic.skills.compilers import CodexSkillCompiler
31
+ from code_agnostic.skills.parser import parse_skill
32
+
33
+
34
+ class CodexConfigService(RegisteredAppConfigService):
35
+ APP_ID = AppId.CODEX
36
+ APP_LABEL = app_label(APP_ID)
37
+
38
+ def __init__(
39
+ self,
40
+ repository: CodexConfigRepository,
41
+ mapper: IAppMCPMapper,
42
+ schema_repository: ISchemaRepository,
43
+ base_config_path: Path | None = None,
44
+ ) -> None:
45
+ self._repository = repository
46
+ self._codex_repo = repository
47
+ self._mapper = mapper
48
+ self._schema_repository = schema_repository
49
+ self._base_config_path = base_config_path
50
+ self._validator = Draft7Validator(self._schema_repository.load_schema())
51
+
52
+ @classmethod
53
+ def create_default(cls, root: Path | None = None) -> "CodexConfigService":
54
+ if root is not None:
55
+ return cls(
56
+ repository=CodexConfigRepository(root=root),
57
+ mapper=CodexMCPMapper(),
58
+ schema_repository=CodexSchemaRepository(),
59
+ base_config_path=None,
60
+ )
61
+ from code_agnostic.core.repository import CoreRepository
62
+
63
+ core = CoreRepository()
64
+ return cls(
65
+ repository=CodexConfigRepository(root=root),
66
+ mapper=CodexMCPMapper(),
67
+ schema_repository=CodexSchemaRepository(),
68
+ base_config_path=core.codex_base_path,
69
+ )
70
+
71
+ @property
72
+ def app_id(self) -> AppId:
73
+ return self.APP_ID
74
+
75
+ @property
76
+ def action_kind(self) -> ActionKind:
77
+ return ActionKind.WRITE_TEXT
78
+
79
+ @property
80
+ def repository(self) -> IAppConfigRepository:
81
+ return self._repository
82
+
83
+ @property
84
+ def mapper(self) -> IAppMCPMapper:
85
+ return self._mapper
86
+
87
+ def validate_config(self, payload: Any) -> None:
88
+ error = next(iter(self._validator.iter_errors(payload)), None)
89
+ if error is not None:
90
+ raise InvalidConfigSchemaError(
91
+ self.repository.config_path, format_schema_error(error)
92
+ )
93
+
94
+ def build_action_payload(self, payload: dict[str, Any]) -> Any:
95
+ return self.repository.serialize_config(payload)
96
+
97
+ def set_mcp_payload(
98
+ self, merged: dict[str, Any], desired_mcp: dict[str, Any]
99
+ ) -> None:
100
+ merged["mcp_servers"] = desired_mcp
101
+
102
+ def derive_status(
103
+ self, existing: dict[str, Any], merged: dict[str, Any]
104
+ ) -> ActionStatus:
105
+ rendered = self.repository.serialize_config(merged)
106
+ existing_text = (
107
+ self.repository.config_path.read_text(encoding="utf-8")
108
+ if self.repository.config_path.exists()
109
+ else ""
110
+ )
111
+ if not self.repository.config_path.exists():
112
+ return ActionStatus.CREATE
113
+ if existing_text == rendered:
114
+ return ActionStatus.NOOP
115
+ return ActionStatus.UPDATE
116
+
117
+ def build_action(
118
+ self,
119
+ common_servers: dict[str, MCPServerDTO],
120
+ agent_sources: list[Path] | None = None,
121
+ ) -> Action:
122
+ existing = self._codex_repo.load_config()
123
+ if existing or self._codex_repo.config_path.exists():
124
+ self.validate_config(existing)
125
+
126
+ desired_mcp = self.mapper.from_common(common_servers)
127
+ merged = dict(existing)
128
+ base = self._load_base_config()
129
+ for key, value in base.items():
130
+ if key == "mcp_servers":
131
+ continue
132
+ if key == "agents" and isinstance(value, dict):
133
+ merged["agents"] = self._merge_agents_payload(
134
+ merged.get("agents"), value
135
+ )
136
+ continue
137
+ merged[key] = deepcopy(value)
138
+ self.set_mcp_payload(merged, desired_mcp)
139
+ if agent_sources:
140
+ merged["agents"] = self._merge_agents_payload(
141
+ merged.get("agents"),
142
+ self._build_agent_registry(agent_sources),
143
+ )
144
+ self.validate_config(merged)
145
+
146
+ return Action(
147
+ kind=self.action_kind,
148
+ path=self.repository.config_path,
149
+ status=self.derive_status(existing, merged),
150
+ detail=f"sync {self.app_id.value} config from common mcp base",
151
+ payload=self.build_action_payload(merged),
152
+ app=self.app_id.value,
153
+ )
154
+
155
+ def _merge_agents_payload(
156
+ self, existing: Any, overlay: dict[str, Any]
157
+ ) -> dict[str, Any]:
158
+ merged = dict(existing) if isinstance(existing, dict) else {}
159
+ for key, value in overlay.items():
160
+ merged[key] = deepcopy(value)
161
+ return merged
162
+
163
+ def _build_agent_registry(self, sources: list[Path]) -> dict[str, dict[str, Any]]:
164
+ registry: dict[str, dict[str, Any]] = {}
165
+ for source in sources:
166
+ try:
167
+ agent = parse_agent(source)
168
+ except InvalidConfigSchemaError:
169
+ raise
170
+ except Exception as exc:
171
+ raise InvalidConfigSchemaError(source, str(exc)) from exc
172
+
173
+ agent_name = agent.metadata.name or agent.name
174
+ target_name = (
175
+ normalize_codex_agent_filename(agent.metadata.name, agent.name)
176
+ + ".toml"
177
+ )
178
+ entry: dict[str, Any] = {
179
+ "description": agent.metadata.description or agent_name,
180
+ "config_file": (Path("agents") / target_name).as_posix(),
181
+ }
182
+ if agent.metadata.nickname_candidates:
183
+ entry["nickname_candidates"] = list(agent.metadata.nickname_candidates)
184
+ registry[agent_name] = entry
185
+ return registry
186
+
187
+ def plan_skill_actions(
188
+ self,
189
+ sources: list[Path],
190
+ target_dir: Path,
191
+ scope: str,
192
+ app: str,
193
+ managed_paths: list[Path],
194
+ removable_links: list[Path],
195
+ ) -> tuple[list[Action], list[Path], list[str]]:
196
+ compiler = CodexSkillCompiler()
197
+ return self._plan_compiled_text_actions(
198
+ sources=sources,
199
+ scope=scope,
200
+ app=app,
201
+ managed_paths=managed_paths,
202
+ removable_links=removable_links,
203
+ compile_source=lambda source: (
204
+ target_dir / source.name / "SKILL.md",
205
+ compiler.compile(
206
+ parse_skill(
207
+ source / "SKILL.md"
208
+ if (source / "SKILL.md").exists()
209
+ else source
210
+ )
211
+ ),
212
+ ),
213
+ create_detail="create compiled codex skill",
214
+ noop_detail="compiled codex skill already up to date",
215
+ update_detail="update compiled codex skill",
216
+ conflict_message="Codex skill sync skipped (conflict): {target}",
217
+ )
218
+
219
+ def plan_agent_actions(
220
+ self,
221
+ sources: list[Path],
222
+ target_dir: Path,
223
+ scope: str,
224
+ app: str,
225
+ managed_paths: list[Path],
226
+ removable_links: list[Path],
227
+ ) -> tuple[list[Action], list[Path], list[str]]:
228
+ compiler = CodexAgentCompiler()
229
+
230
+ def compile_source(source: Path) -> tuple[Path, str]:
231
+ try:
232
+ agent = parse_agent(source)
233
+ payload = compiler.compile(agent)
234
+ except InvalidConfigSchemaError:
235
+ raise
236
+ except Exception as exc:
237
+ raise InvalidConfigSchemaError(source, str(exc)) from exc
238
+
239
+ target_name = (
240
+ normalize_codex_agent_filename(agent.metadata.name, agent.name)
241
+ + ".toml"
242
+ )
243
+ return target_dir / target_name, payload
244
+
245
+ return self._plan_compiled_text_actions(
246
+ sources=sources,
247
+ scope=scope,
248
+ app=app,
249
+ managed_paths=managed_paths,
250
+ removable_links=removable_links,
251
+ compile_source=compile_source,
252
+ create_detail="create compiled codex agent",
253
+ noop_detail="compiled codex agent already up to date",
254
+ update_detail="update compiled codex agent",
255
+ conflict_message="Codex agent sync skipped (conflict): {target}",
256
+ )
257
+
258
+ def _load_base_config(self) -> dict[str, Any]:
259
+ if self._base_config_path is None or not self._base_config_path.exists():
260
+ return {}
261
+ payload, error = read_json_safe(self._base_config_path)
262
+ if error is not None:
263
+ raise InvalidJsonFormatError(self._base_config_path, error)
264
+ if not isinstance(payload, dict):
265
+ raise InvalidConfigSchemaError(
266
+ self._base_config_path, "must be a JSON object"
267
+ )
268
+ return payload