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.
- cc_plugin_to_codex/__init__.py +7 -0
- cc_plugin_to_codex/__main__.py +4 -0
- cc_plugin_to_codex/agent_convert.py +95 -0
- cc_plugin_to_codex/bridge.py +137 -0
- cc_plugin_to_codex/cli.py +554 -0
- cc_plugin_to_codex/interactive.py +146 -0
- cc_plugin_to_codex/log.py +42 -0
- cc_plugin_to_codex/marketplace.py +79 -0
- cc_plugin_to_codex/py.typed +0 -0
- cc_plugin_to_codex/registry.py +49 -0
- cc_plugin_to_codex/scopes.py +46 -0
- cc_plugin_to_codex/sources.py +155 -0
- cc_plugin_to_codex/sync.py +399 -0
- cc_plugin_to_codex-0.1.0.dist-info/METADATA +226 -0
- cc_plugin_to_codex-0.1.0.dist-info/RECORD +18 -0
- cc_plugin_to_codex-0.1.0.dist-info/WHEEL +4 -0
- cc_plugin_to_codex-0.1.0.dist-info/entry_points.txt +2 -0
- cc_plugin_to_codex-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -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
|