cc-plugin-to-codex 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,7 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ try:
4
+ __version__ = version("cc-plugin-to-codex")
5
+ except PackageNotFoundError:
6
+ # Package not installed (e.g. running from source without `pip install -e .`).
7
+ __version__ = "0.0.0+unknown"
@@ -0,0 +1,4 @@
1
+ from cc_plugin_to_codex.cli import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
@@ -0,0 +1,95 @@
1
+ """Convert CC agent markdown (YAML frontmatter + body) to Codex agent TOML."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import tomli_w
11
+ import yaml
12
+
13
+ from cc_plugin_to_codex.bridge import build_agent_marker_line
14
+
15
+ FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---\n(.*)$", re.DOTALL)
16
+
17
+ # Codex-supported top-level fields we preserve from frontmatter if present
18
+ _PASSTHROUGH_FIELDS = {"nickname_candidates"}
19
+
20
+ # Fields we explicitly drop (not Codex-compatible)
21
+ _DROPPED_FIELDS = {"model", "tools"}
22
+
23
+
24
+ @dataclass
25
+ class ConversionWarning:
26
+ field: str
27
+ reason: str
28
+
29
+
30
+ @dataclass
31
+ class ConversionResult:
32
+ agent_name: str
33
+ toml: str
34
+ warnings: list[ConversionWarning] = field(default_factory=list)
35
+
36
+
37
+ def snake_case_name(s: str) -> str:
38
+ return s.replace("-", "_")
39
+
40
+
41
+ def convert_agent(
42
+ md_path: Path,
43
+ *,
44
+ bridge_plugin: str,
45
+ source_plugin: str,
46
+ synced_at: str,
47
+ ) -> ConversionResult:
48
+ text = md_path.read_text(encoding="utf-8")
49
+ m = FRONTMATTER_RE.match(text)
50
+ if not m:
51
+ raise ValueError(f"{md_path}: no YAML frontmatter found")
52
+ fm_text, body = m.group(1), m.group(2).lstrip("\n")
53
+ frontmatter = yaml.safe_load(fm_text) or {}
54
+ if not isinstance(frontmatter, dict):
55
+ raise ValueError(f"{md_path}: frontmatter must be a YAML mapping")
56
+
57
+ source_agent = frontmatter.get("name")
58
+ if not source_agent:
59
+ raise ValueError(f"{md_path}: frontmatter missing required 'name'")
60
+
61
+ description = frontmatter.get("description") or f"Bridged from CC plugin {source_plugin}"
62
+
63
+ warnings: list[ConversionWarning] = []
64
+ for key in frontmatter:
65
+ if key in {"name", "description"} or key in _PASSTHROUGH_FIELDS:
66
+ continue
67
+ if key in _DROPPED_FIELDS:
68
+ warnings.append(ConversionWarning(field=key, reason="Not compatible with Codex"))
69
+ else:
70
+ warnings.append(
71
+ ConversionWarning(field=key, reason="Unknown frontmatter field, dropped")
72
+ )
73
+
74
+ agent_name = f"cc_{snake_case_name(source_plugin)}_{snake_case_name(source_agent)}"
75
+
76
+ toml_dict: dict[str, Any] = {
77
+ "name": agent_name,
78
+ "description": description,
79
+ "developer_instructions": body,
80
+ }
81
+ # passthrough fields
82
+ for key in _PASSTHROUGH_FIELDS:
83
+ if key in frontmatter:
84
+ toml_dict[key] = frontmatter[key]
85
+
86
+ toml_body = tomli_w.dumps(toml_dict)
87
+ marker_line = build_agent_marker_line(
88
+ source_plugin=source_plugin,
89
+ source_agent=source_agent,
90
+ bridge_plugin=bridge_plugin,
91
+ synced_at=synced_at,
92
+ )
93
+ full_toml = f"{marker_line}\n{toml_body}"
94
+
95
+ return ConversionResult(agent_name=agent_name, toml=full_toml, warnings=warnings)
@@ -0,0 +1,137 @@
1
+ """x-cc-bridge marker: plugin.json and agent TOML detection/creation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from datetime import UTC, datetime
8
+ from pathlib import Path
9
+ from typing import Any, Literal, TypedDict
10
+
11
+ from cc_plugin_to_codex import __version__
12
+
13
+ SourceKind = Literal["git", "local"]
14
+
15
+ MARKER_KEY = "x-cc-bridge"
16
+ TOOL_ID = f"cc-plugin-to-codex/{__version__}"
17
+
18
+
19
+ class BridgeMarker(TypedDict):
20
+ sourcePlugin: str
21
+ source: str
22
+ sourceKind: SourceKind
23
+ ref: str | None
24
+ commit: str
25
+ marketplace: str
26
+ syncedAt: str
27
+ tool: str
28
+ agents: list[str]
29
+
30
+
31
+ def build_marker(
32
+ *,
33
+ source_plugin: str,
34
+ source: str,
35
+ source_kind: SourceKind,
36
+ ref: str | None,
37
+ commit: str,
38
+ marketplace: str,
39
+ agents: list[str],
40
+ now: datetime | None = None,
41
+ ) -> BridgeMarker:
42
+ ts = (now or datetime.now(UTC)).strftime("%Y-%m-%dT%H:%M:%SZ")
43
+ return BridgeMarker(
44
+ sourcePlugin=source_plugin,
45
+ source=source,
46
+ sourceKind=source_kind,
47
+ ref=ref,
48
+ commit=commit,
49
+ marketplace=marketplace,
50
+ syncedAt=ts,
51
+ tool=TOOL_ID,
52
+ agents=agents,
53
+ )
54
+
55
+
56
+ _REQUIRED_BRIDGE_MARKER_KEYS = {
57
+ "sourcePlugin",
58
+ "source",
59
+ "sourceKind",
60
+ "ref",
61
+ "commit",
62
+ "marketplace",
63
+ "syncedAt",
64
+ "tool",
65
+ "agents",
66
+ }
67
+
68
+
69
+ def is_bridge_manifest(manifest: dict[str, Any]) -> bool:
70
+ return MARKER_KEY in manifest and isinstance(manifest[MARKER_KEY], dict)
71
+
72
+
73
+ def extract_marker(manifest: dict[str, Any]) -> BridgeMarker | None:
74
+ """Return the bridge marker only if every required TypedDict field is present.
75
+
76
+ Silently returns None for a manifest with an incomplete or malformed marker
77
+ (e.g. `{"x-cc-bridge": {}}`), so callers can treat "bridge but unparseable"
78
+ the same as "not a bridge". Matches the stricter validation already done by
79
+ extract_agent_marker on the agent-TOML side.
80
+ """
81
+ if not is_bridge_manifest(manifest):
82
+ return None
83
+ payload = manifest[MARKER_KEY]
84
+ if not _REQUIRED_BRIDGE_MARKER_KEYS.issubset(payload.keys()):
85
+ return None
86
+ return payload
87
+
88
+
89
+ # ---------------------------------------------------------------------------
90
+ # Agent TOML first-line comment marker
91
+ # ---------------------------------------------------------------------------
92
+
93
+ AGENT_MARKER_REGEX = re.compile(r"^# x-cc-bridge: (\{.*\})$")
94
+
95
+
96
+ class AgentMarker(TypedDict):
97
+ sourcePlugin: str
98
+ sourceAgent: str
99
+ bridgePlugin: str
100
+ syncedAt: str
101
+
102
+
103
+ def build_agent_marker_line(
104
+ *,
105
+ source_plugin: str,
106
+ source_agent: str,
107
+ bridge_plugin: str,
108
+ synced_at: str,
109
+ ) -> str:
110
+ payload = {
111
+ "sourcePlugin": source_plugin,
112
+ "sourceAgent": source_agent,
113
+ "bridgePlugin": bridge_plugin,
114
+ "syncedAt": synced_at,
115
+ }
116
+ return f"# x-cc-bridge: {json.dumps(payload, separators=(',', ':'), ensure_ascii=False)}"
117
+
118
+
119
+ def extract_agent_marker(toml_path: Path) -> AgentMarker | None:
120
+ if not toml_path.exists():
121
+ return None
122
+ try:
123
+ with toml_path.open("r", encoding="utf-8") as f:
124
+ first_line = f.readline().rstrip("\n")
125
+ except OSError:
126
+ return None
127
+ m = AGENT_MARKER_REGEX.match(first_line)
128
+ if not m:
129
+ return None
130
+ try:
131
+ payload = json.loads(m.group(1))
132
+ except json.JSONDecodeError:
133
+ return None
134
+ required = {"sourcePlugin", "sourceAgent", "bridgePlugin", "syncedAt"}
135
+ if not required.issubset(payload.keys()):
136
+ return None
137
+ return payload