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.
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/PKG-INFO +1 -1
- code_agnostic-0.3.3/code_agnostic/__main__.py +62 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/app_id.py +22 -4
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/codex/config_repository.py +10 -4
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/codex/schema.json +27 -0
- code_agnostic-0.3.3/code_agnostic/apps/codex/service.py +268 -0
- code_agnostic-0.3.3/code_agnostic/apps/common/interfaces/service.py +262 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/cursor/config_repository.py +10 -4
- code_agnostic-0.3.3/code_agnostic/apps/cursor/service.py +162 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/opencode/config_repository.py +8 -3
- code_agnostic-0.3.3/code_agnostic/apps/opencode/service.py +210 -0
- code_agnostic-0.3.3/code_agnostic/cli/__init__.py +29 -0
- code_agnostic-0.3.3/code_agnostic/cli/aliases.py +20 -0
- code_agnostic-0.3.3/code_agnostic/cli/commands/__init__.py +33 -0
- code_agnostic-0.3.3/code_agnostic/cli/commands/agents.py +47 -0
- code_agnostic-0.3.3/code_agnostic/cli/commands/apply.py +42 -0
- code_agnostic-0.3.3/code_agnostic/cli/commands/apps.py +45 -0
- code_agnostic-0.3.3/code_agnostic/cli/commands/explain_lossiness.py +45 -0
- code_agnostic-0.3.3/code_agnostic/cli/commands/import_.py +189 -0
- code_agnostic-0.3.3/code_agnostic/cli/commands/mcp.py +122 -0
- code_agnostic-0.3.3/code_agnostic/cli/commands/plan.py +30 -0
- code_agnostic-0.3.3/code_agnostic/cli/commands/restore.py +28 -0
- code_agnostic-0.3.3/code_agnostic/cli/commands/rules.py +46 -0
- code_agnostic-0.3.3/code_agnostic/cli/commands/skills.py +42 -0
- code_agnostic-0.3.3/code_agnostic/cli/commands/status.py +52 -0
- code_agnostic-0.3.3/code_agnostic/cli/commands/validate.py +30 -0
- code_agnostic-0.3.3/code_agnostic/cli/commands/workspaces.py +186 -0
- code_agnostic-0.3.3/code_agnostic/cli/helpers.py +87 -0
- code_agnostic-0.3.3/code_agnostic/cli/options.py +95 -0
- code_agnostic-0.3.3/code_agnostic/constants.py +26 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/core/repository.py +4 -3
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/executor.py +24 -13
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/planner.py +21 -6
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/status.py +15 -19
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic.egg-info/PKG-INFO +1 -1
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic.egg-info/SOURCES.txt +20 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/pyproject.toml +1 -1
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_apply_apps.py +149 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_apply_codex.py +14 -0
- code_agnostic-0.3.3/tests/test_cli_module_organization.py +55 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_workspaces.py +1 -1
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_git_exclude_service.py +2 -2
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_planner_rules.py +1 -2
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_workspace_config_sync.py +117 -21
- code_agnostic-0.3.3/tests/test_workspace_repo_status.py +240 -0
- code_agnostic-0.3.2/code_agnostic/__main__.py +0 -1071
- code_agnostic-0.3.2/code_agnostic/apps/codex/service.py +0 -365
- code_agnostic-0.3.2/code_agnostic/apps/common/interfaces/service.py +0 -84
- code_agnostic-0.3.2/code_agnostic/apps/cursor/service.py +0 -291
- code_agnostic-0.3.2/code_agnostic/apps/opencode/service.py +0 -353
- code_agnostic-0.3.2/code_agnostic/constants.py +0 -13
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/LICENSE +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/README.md +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/__init__.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/agents/__init__.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/agents/codex.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/agents/compilers.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/agents/models.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/agents/opencode.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/agents/parser.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/__init__.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/apps_service.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/codex/__init__.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/codex/mapper.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/codex/schema_repository.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/common/__init__.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/common/compiled_planning.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/common/framework.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/common/interfaces/__init__.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/common/interfaces/mapper.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/common/interfaces/repositories.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/common/loader.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/common/models.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/common/schema.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/common/symlink_planning.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/common/utils.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/cursor/__init__.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/cursor/mapper.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/cursor/schema.json +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/cursor/schema_repository.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/opencode/__init__.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/opencode/mapper.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/opencode/schema.json +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/apps/opencode/schema_repository.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/core/__init__.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/core/workspace_repository.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/errors.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/git_exclude_service.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/imports/__init__.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/imports/adapters.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/imports/filesystem.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/imports/models.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/imports/service.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/lossiness.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/mcp_service.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/models.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/rules/__init__.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/rules/compilers.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/rules/models.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/rules/parser.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/rules/repository.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/skills/__init__.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/skills/compilers.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/skills/models.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/skills/parser.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/spec/__init__.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/spec/loaders.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/spec/schemas/agent.v1.schema.json +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/spec/schemas/mcp.base.schema.json +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/spec/schemas/mcp.v1.schema.json +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/spec/schemas/rule.v1.schema.json +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/spec/schemas/skill.v1.schema.json +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/tui/__init__.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/tui/enums.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/tui/import_selector.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/tui/renderers.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/tui/sections.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/tui/tables.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/utils.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/validation.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic/workspaces.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic.egg-info/dependency_links.txt +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic.egg-info/entry_points.txt +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic.egg-info/requires.txt +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/code_agnostic.egg-info/top_level.txt +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/setup.cfg +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_agents.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_aliases.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_apply_cursor.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_apply_target.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_apps.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_explain_lossiness.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_flags.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_git_exclude.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_import.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_import_interactive.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_mcp.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_plan.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_restore.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_rules.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_skills.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_status.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_validate.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_cli_workspace_resolution.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_common_mcp_to_dto.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_common_repository.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_compiled_planning.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_dto_to_common_mcp.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_mcp_service.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_planner_executor.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_symlink_planning.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_sync_plan_model.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_transactional_executor.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_utils.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.3}/tests/test_workspaces.py +0 -0
|
@@ -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=
|
|
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=
|
|
53
|
-
project_dir_name=
|
|
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=
|
|
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() /
|
|
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 /
|
|
31
|
+
return self.root / CODEX_CONFIG_FILENAME
|
|
26
32
|
|
|
27
33
|
@property
|
|
28
34
|
def skills_dir(self) -> Path:
|
|
29
|
-
return self.root /
|
|
35
|
+
return self.root / SKILLS_DIRNAME
|
|
30
36
|
|
|
31
37
|
@property
|
|
32
38
|
def agents_dir(self) -> Path:
|
|
33
|
-
return self.root /
|
|
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
|