agentsbridge 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.
- agentsbridge/__init__.py +3 -0
- agentsbridge/cli.py +174 -0
- agentsbridge/core.py +204 -0
- agentsbridge/targets.py +117 -0
- agentsbridge-0.1.0.dist-info/METADATA +194 -0
- agentsbridge-0.1.0.dist-info/RECORD +10 -0
- agentsbridge-0.1.0.dist-info/WHEEL +5 -0
- agentsbridge-0.1.0.dist-info/entry_points.txt +2 -0
- agentsbridge-0.1.0.dist-info/licenses/LICENSE +21 -0
- agentsbridge-0.1.0.dist-info/top_level.txt +1 -0
agentsbridge/__init__.py
ADDED
agentsbridge/cli.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""agentsbridge CLI - bridge AGENTS.md to every AI coding tool's rules file."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from . import __version__, core
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
13
|
+
parser = argparse.ArgumentParser(
|
|
14
|
+
prog="agentsbridge",
|
|
15
|
+
description=(
|
|
16
|
+
"Bridge AGENTS.md to every AI coding tool's rules file. "
|
|
17
|
+
"Maintain one source of truth; generate CLAUDE.md, "
|
|
18
|
+
"copilot-instructions.md, Cursor rules, and more."
|
|
19
|
+
),
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
22
|
+
sub = parser.add_subparsers(dest="command")
|
|
23
|
+
|
|
24
|
+
p_sync = sub.add_parser("sync", help="generate all target rules files from AGENTS.md")
|
|
25
|
+
p_sync.add_argument(
|
|
26
|
+
"-t",
|
|
27
|
+
"--targets",
|
|
28
|
+
default="",
|
|
29
|
+
help="comma-separated subset of targets (default: all)",
|
|
30
|
+
)
|
|
31
|
+
p_sync.add_argument(
|
|
32
|
+
"--force", action="store_true", help="overwrite files not generated by agentsbridge"
|
|
33
|
+
)
|
|
34
|
+
p_sync.add_argument(
|
|
35
|
+
"--dry-run", action="store_true", help="show what would change without writing"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
p_check = sub.add_parser(
|
|
39
|
+
"check", help="exit non-zero if any generated file drifted from AGENTS.md (for CI)"
|
|
40
|
+
)
|
|
41
|
+
p_check.add_argument(
|
|
42
|
+
"-t",
|
|
43
|
+
"--targets",
|
|
44
|
+
default="",
|
|
45
|
+
help="comma-separated subset of targets (default: all)",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
p_import = sub.add_parser(
|
|
49
|
+
"import", help="seed AGENTS.md from an existing rules file (CLAUDE.md, .cursorrules, ...)"
|
|
50
|
+
)
|
|
51
|
+
p_import.add_argument("--out", default="AGENTS.md", help="output path (default: AGENTS.md)")
|
|
52
|
+
|
|
53
|
+
p_init = sub.add_parser("init", help="create a starter AGENTS.md")
|
|
54
|
+
p_init.add_argument("--force", action="store_true", help="overwrite existing AGENTS.md")
|
|
55
|
+
|
|
56
|
+
return parser
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _parse_targets(value: str) -> list[str] | None:
|
|
60
|
+
value = value.strip()
|
|
61
|
+
if not value:
|
|
62
|
+
return None
|
|
63
|
+
return [v.strip() for v in value.split(",") if v.strip()]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _root() -> Path:
|
|
67
|
+
return Path.cwd()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def cmd_sync(args: argparse.Namespace) -> int:
|
|
71
|
+
root = core.walk_up(_root())
|
|
72
|
+
try:
|
|
73
|
+
results = core.sync(
|
|
74
|
+
root, _parse_targets(args.targets), force=args.force, dry_run=args.dry_run
|
|
75
|
+
)
|
|
76
|
+
except FileNotFoundError as e:
|
|
77
|
+
print(f"error: {e}", file=sys.stderr)
|
|
78
|
+
return 1
|
|
79
|
+
except ValueError as e:
|
|
80
|
+
print(f"error: {e}", file=sys.stderr)
|
|
81
|
+
return 2
|
|
82
|
+
|
|
83
|
+
label = "would create/update" if args.dry_run else "wrote"
|
|
84
|
+
for r in results:
|
|
85
|
+
icon = {
|
|
86
|
+
"created": "+",
|
|
87
|
+
"updated": "~",
|
|
88
|
+
"unchanged": "=",
|
|
89
|
+
"skipped": "!",
|
|
90
|
+
"missing": "?",
|
|
91
|
+
}.get(r.action, " ")
|
|
92
|
+
detail = f" ({r.detail})" if r.detail else ""
|
|
93
|
+
print(f" {icon} {r.action:<9} {r.path}{detail}")
|
|
94
|
+
created = sum(1 for r in results if r.action in ("created", "updated"))
|
|
95
|
+
skipped = sum(1 for r in results if r.action == "skipped")
|
|
96
|
+
print(
|
|
97
|
+
f"\n{label} {created} file(s)"
|
|
98
|
+
+ (f", skipped {skipped}" if skipped else "")
|
|
99
|
+
+ (" (dry run)" if args.dry_run else "")
|
|
100
|
+
)
|
|
101
|
+
return 0
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def cmd_check(args: argparse.Namespace) -> int:
|
|
105
|
+
root = core.walk_up(_root())
|
|
106
|
+
try:
|
|
107
|
+
clean, results = core.check(root, _parse_targets(args.targets))
|
|
108
|
+
except FileNotFoundError as e:
|
|
109
|
+
print(f"error: {e}", file=sys.stderr)
|
|
110
|
+
return 1
|
|
111
|
+
except ValueError as e:
|
|
112
|
+
print(f"error: {e}", file=sys.stderr)
|
|
113
|
+
return 2
|
|
114
|
+
|
|
115
|
+
for r in results:
|
|
116
|
+
icon = {"ok": "=", "drifted": "!", "missing": "?", "skipped": "-"}.get(r.action, " ")
|
|
117
|
+
detail = f" ({r.detail})" if r.detail else ""
|
|
118
|
+
print(f" {icon} {r.action:<9} {r.path}{detail}")
|
|
119
|
+
if clean:
|
|
120
|
+
print("\nall target files are up to date")
|
|
121
|
+
return 0
|
|
122
|
+
print("\ndrift detected: run `agentsbridge sync` to fix", file=sys.stderr)
|
|
123
|
+
return 1
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def cmd_import(args: argparse.Namespace) -> int:
|
|
127
|
+
root = _root()
|
|
128
|
+
try:
|
|
129
|
+
body, src = core.import_source(root)
|
|
130
|
+
except FileNotFoundError as e:
|
|
131
|
+
print(f"error: {e}", file=sys.stderr)
|
|
132
|
+
return 1
|
|
133
|
+
|
|
134
|
+
out_path = root / args.out
|
|
135
|
+
if out_path.exists():
|
|
136
|
+
print(f"error: {out_path} already exists. Delete it first or use --out.", file=sys.stderr)
|
|
137
|
+
return 1
|
|
138
|
+
out_path.write_text(body.rstrip() + "\n", encoding="utf-8")
|
|
139
|
+
print(f"created {out_path} from {src.name}")
|
|
140
|
+
print("review it, then run `agentsbridge sync` to propagate.")
|
|
141
|
+
return 0
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def cmd_init(args: argparse.Namespace) -> int:
|
|
145
|
+
root = _root()
|
|
146
|
+
if (root / core.SOURCE_FILENAME).exists() and not args.force:
|
|
147
|
+
print("AGENTS.md already exists (use --force to overwrite)")
|
|
148
|
+
return 0
|
|
149
|
+
r = core.ensure_agents_md(root, dry_run=False)
|
|
150
|
+
if r.action == "created":
|
|
151
|
+
print(f"created {r.path}")
|
|
152
|
+
print("edit it, then run `agentsbridge sync`")
|
|
153
|
+
return 0
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
COMMANDS = {
|
|
157
|
+
"sync": cmd_sync,
|
|
158
|
+
"check": cmd_check,
|
|
159
|
+
"import": cmd_import,
|
|
160
|
+
"init": cmd_init,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def main(argv: list[str] | None = None) -> int:
|
|
165
|
+
parser = build_parser()
|
|
166
|
+
args = parser.parse_args(argv)
|
|
167
|
+
if not args.command:
|
|
168
|
+
parser.print_help()
|
|
169
|
+
return 0
|
|
170
|
+
return COMMANDS[args.command](args)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
if __name__ == "__main__":
|
|
174
|
+
sys.exit(main())
|
agentsbridge/core.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Core logic: parse AGENTS.md, render targets, sync/check/import/init."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .targets import GENERATED_MARKER, TARGETS, Target, render
|
|
10
|
+
|
|
11
|
+
SOURCE_FILENAME = "AGENTS.md"
|
|
12
|
+
|
|
13
|
+
# Files agentsbridge knows how to import from, in priority order.
|
|
14
|
+
IMPORT_SOURCES = [
|
|
15
|
+
"CLAUDE.md",
|
|
16
|
+
".github/copilot-instructions.md",
|
|
17
|
+
".cursorrules",
|
|
18
|
+
".clinerules",
|
|
19
|
+
".windsurfrules",
|
|
20
|
+
"CONVENTIONS.md",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class SyncResult:
|
|
26
|
+
target: str
|
|
27
|
+
path: Path
|
|
28
|
+
action: str # created | updated | skipped | unchanged
|
|
29
|
+
detail: str = ""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def find_source(root: Path) -> Path | None:
|
|
33
|
+
p = root / SOURCE_FILENAME
|
|
34
|
+
return p if p.is_file() else None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def read_source(root: Path) -> str:
|
|
38
|
+
p = find_source(root)
|
|
39
|
+
if p is None:
|
|
40
|
+
raise FileNotFoundError(
|
|
41
|
+
f"No {SOURCE_FILENAME} found in {root}. Run `agentsbridge init` first."
|
|
42
|
+
)
|
|
43
|
+
return strip_generated_marker(p.read_text(encoding="utf-8"))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def strip_generated_marker(text: str) -> str:
|
|
47
|
+
"""Remove our own marker so re-importing a generated file stays clean."""
|
|
48
|
+
lines = text.splitlines()
|
|
49
|
+
out = [ln for ln in lines if GENERATED_MARKER not in ln]
|
|
50
|
+
return "\n".join(out).strip()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def is_generated(path: Path) -> bool:
|
|
54
|
+
try:
|
|
55
|
+
return GENERATED_MARKER in path.read_text(encoding="utf-8", errors="replace")[:500]
|
|
56
|
+
except OSError:
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def sync(
|
|
61
|
+
root: Path,
|
|
62
|
+
selected: list[str] | None = None,
|
|
63
|
+
force: bool = False,
|
|
64
|
+
dry_run: bool = False,
|
|
65
|
+
) -> list[SyncResult]:
|
|
66
|
+
"""Generate target files from AGENTS.md."""
|
|
67
|
+
body = read_source(root)
|
|
68
|
+
targets = resolve_targets(selected)
|
|
69
|
+
results: list[SyncResult] = []
|
|
70
|
+
|
|
71
|
+
for target in targets:
|
|
72
|
+
path = root / target.path
|
|
73
|
+
content = render(target, body)
|
|
74
|
+
|
|
75
|
+
if path.exists():
|
|
76
|
+
if not force and not is_generated(path):
|
|
77
|
+
results.append(
|
|
78
|
+
SyncResult(
|
|
79
|
+
target.name,
|
|
80
|
+
path,
|
|
81
|
+
"skipped",
|
|
82
|
+
"file exists and was not generated by agentsbridge (use --force to overwrite)",
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
continue
|
|
86
|
+
if path.read_text(encoding="utf-8", errors="replace") == content:
|
|
87
|
+
results.append(SyncResult(target.name, path, "unchanged"))
|
|
88
|
+
continue
|
|
89
|
+
action = "updated"
|
|
90
|
+
else:
|
|
91
|
+
action = "created"
|
|
92
|
+
|
|
93
|
+
if not dry_run:
|
|
94
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
path.write_text(content, encoding="utf-8")
|
|
96
|
+
results.append(SyncResult(target.name, path, action))
|
|
97
|
+
|
|
98
|
+
return results
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def check(root: Path, selected: list[str] | None = None) -> tuple[bool, list[SyncResult]]:
|
|
102
|
+
"""Return (clean, results). clean=False means at least one target drifted."""
|
|
103
|
+
body = read_source(root)
|
|
104
|
+
targets = resolve_targets(selected)
|
|
105
|
+
results: list[SyncResult] = []
|
|
106
|
+
clean = True
|
|
107
|
+
|
|
108
|
+
for target in targets:
|
|
109
|
+
path = root / target.path
|
|
110
|
+
content = render(target, body)
|
|
111
|
+
|
|
112
|
+
if not path.exists():
|
|
113
|
+
clean = False
|
|
114
|
+
results.append(SyncResult(target.name, path, "missing"))
|
|
115
|
+
continue
|
|
116
|
+
if not is_generated(path):
|
|
117
|
+
# Hand-written file: we never claim drift on files we don't own.
|
|
118
|
+
results.append(SyncResult(target.name, path, "skipped", "not generated"))
|
|
119
|
+
continue
|
|
120
|
+
if path.read_text(encoding="utf-8", errors="replace") != content:
|
|
121
|
+
clean = False
|
|
122
|
+
results.append(SyncResult(target.name, path, "drifted"))
|
|
123
|
+
else:
|
|
124
|
+
results.append(SyncResult(target.name, path, "ok"))
|
|
125
|
+
|
|
126
|
+
return clean, results
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def resolve_targets(selected: list[str] | None) -> list[Target]:
|
|
130
|
+
if not selected:
|
|
131
|
+
return list(TARGETS.values())
|
|
132
|
+
unknown = [s for s in selected if s not in TARGETS]
|
|
133
|
+
if unknown:
|
|
134
|
+
raise ValueError(
|
|
135
|
+
f"Unknown target(s): {', '.join(unknown)}. Available: {', '.join(sorted(TARGETS))}"
|
|
136
|
+
)
|
|
137
|
+
# preserve user order, dedupe
|
|
138
|
+
seen: set[str] = set()
|
|
139
|
+
out = []
|
|
140
|
+
for s in selected:
|
|
141
|
+
if s not in seen:
|
|
142
|
+
seen.add(s)
|
|
143
|
+
out.append(TARGETS[s])
|
|
144
|
+
return out
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def import_source(root: Path) -> tuple[str, Path]:
|
|
148
|
+
"""Find an existing rules file and return (body, path) to seed AGENTS.md."""
|
|
149
|
+
for name in IMPORT_SOURCES:
|
|
150
|
+
p = root / name
|
|
151
|
+
if p.is_file():
|
|
152
|
+
text = p.read_text(encoding="utf-8", errors="replace")
|
|
153
|
+
return strip_generated_marker(text), p
|
|
154
|
+
raise FileNotFoundError(
|
|
155
|
+
"No importable rules file found. Looked for: " + ", ".join(IMPORT_SOURCES)
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def init_template() -> str:
|
|
160
|
+
return """# Coding Agent Rules
|
|
161
|
+
|
|
162
|
+
<!-- Edit this file. Run `agentsbridge sync` to propagate changes to
|
|
163
|
+
CLAUDE.md, .github/copilot-instructions.md, Cursor, Windsurf, Cline,
|
|
164
|
+
Aider, and more. Tools that read AGENTS.md natively (Codex CLI,
|
|
165
|
+
Gemini CLI, Jules, Amp, Zed) need no bridging. -->
|
|
166
|
+
|
|
167
|
+
## General
|
|
168
|
+
|
|
169
|
+
- Write clear, idiomatic code; prefer simplicity over cleverness.
|
|
170
|
+
- Keep functions small and focused; extract helpers when logic repeats.
|
|
171
|
+
|
|
172
|
+
## Testing
|
|
173
|
+
|
|
174
|
+
- Add or update tests for every behavior change.
|
|
175
|
+
- Run the test suite before committing.
|
|
176
|
+
|
|
177
|
+
## Git
|
|
178
|
+
|
|
179
|
+
- Commit messages: imperative mood, under 72 characters.
|
|
180
|
+
- One logical change per commit.
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def ensure_agents_md(root: Path, template: str | None = None, dry_run: bool = False) -> SyncResult:
|
|
185
|
+
path = root / SOURCE_FILENAME
|
|
186
|
+
if path.exists():
|
|
187
|
+
return SyncResult("agents", path, "unchanged", "AGENTS.md already exists")
|
|
188
|
+
content = template if template is not None else init_template()
|
|
189
|
+
if not dry_run:
|
|
190
|
+
path.write_text(content, encoding="utf-8")
|
|
191
|
+
return SyncResult("agents", path, "created")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def walk_up(start: Path) -> Path:
|
|
195
|
+
"""Walk up from start to find the nearest directory containing AGENTS.md."""
|
|
196
|
+
cur = start.resolve()
|
|
197
|
+
for p in [cur, *cur.parents]:
|
|
198
|
+
if (p / SOURCE_FILENAME).is_file():
|
|
199
|
+
return p
|
|
200
|
+
return start.resolve()
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def is_quiet() -> bool:
|
|
204
|
+
return os.environ.get("AGENTSBRIDGE_QUIET") == "1"
|
agentsbridge/targets.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Target definitions: how AGENTS.md content maps to each tool's rules file.
|
|
2
|
+
|
|
3
|
+
A target is a callable-free dataclass describing:
|
|
4
|
+
- name: identifier used on the CLI (--targets claude,copilot)
|
|
5
|
+
- path: file path relative to the project root
|
|
6
|
+
- style: how the body should be rendered (raw markdown, mdc frontmatter, ...)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class Target:
|
|
16
|
+
name: str
|
|
17
|
+
path: str
|
|
18
|
+
style: str = "markdown" # markdown | mdc
|
|
19
|
+
description: str = ""
|
|
20
|
+
# Some tools nest rules in subdirectories; record parent dirs to create.
|
|
21
|
+
extra_dirs: tuple[str, ...] = field(default_factory=tuple)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
TARGETS: dict[str, Target] = {
|
|
25
|
+
t.name: t
|
|
26
|
+
for t in [
|
|
27
|
+
Target(
|
|
28
|
+
name="claude",
|
|
29
|
+
path="CLAUDE.md",
|
|
30
|
+
style="markdown",
|
|
31
|
+
description="Claude Code (project memory)",
|
|
32
|
+
),
|
|
33
|
+
Target(
|
|
34
|
+
name="copilot",
|
|
35
|
+
path=".github/copilot-instructions.md",
|
|
36
|
+
style="markdown",
|
|
37
|
+
description="GitHub Copilot",
|
|
38
|
+
),
|
|
39
|
+
Target(
|
|
40
|
+
name="cursor",
|
|
41
|
+
path=".cursor/rules/agentsbridge.mdc",
|
|
42
|
+
style="mdc",
|
|
43
|
+
description="Cursor (rules dir)",
|
|
44
|
+
),
|
|
45
|
+
Target(
|
|
46
|
+
name="windsurf",
|
|
47
|
+
path=".windsurfrules",
|
|
48
|
+
style="markdown",
|
|
49
|
+
description="Windsurf",
|
|
50
|
+
),
|
|
51
|
+
Target(
|
|
52
|
+
name="cline",
|
|
53
|
+
path=".clinerules",
|
|
54
|
+
style="markdown",
|
|
55
|
+
description="Cline",
|
|
56
|
+
),
|
|
57
|
+
Target(
|
|
58
|
+
name="aider",
|
|
59
|
+
path="CONVENTIONS.md",
|
|
60
|
+
style="markdown",
|
|
61
|
+
description="Aider",
|
|
62
|
+
),
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# Tools that read AGENTS.md natively - no bridging needed, documented only.
|
|
67
|
+
NATIVE_TOOLS = [
|
|
68
|
+
"Codex CLI",
|
|
69
|
+
"Gemini CLI",
|
|
70
|
+
"Jules",
|
|
71
|
+
"Amp",
|
|
72
|
+
"Zed",
|
|
73
|
+
"Cursor (newer versions)",
|
|
74
|
+
"Factory Droids",
|
|
75
|
+
"opencode",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
GENERATED_MARKER = "<!-- generated by agentsbridge; do not edit -->"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def render(target: Target, body: str) -> str:
|
|
82
|
+
"""Render the AGENTS.md body for a specific target style."""
|
|
83
|
+
if target.style == "mdc":
|
|
84
|
+
return _render_mdc(body)
|
|
85
|
+
return _render_markdown(body)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _render_markdown(body: str) -> str:
|
|
89
|
+
lines = [
|
|
90
|
+
GENERATED_MARKER,
|
|
91
|
+
"",
|
|
92
|
+
"> Generated from `AGENTS.md` by [agentsbridge](https://github.com/DavidStarYu/agentsbridge).",
|
|
93
|
+
"> Run `agentsbridge sync` after editing `AGENTS.md`. Do not edit this file directly.",
|
|
94
|
+
"",
|
|
95
|
+
body.rstrip(),
|
|
96
|
+
"",
|
|
97
|
+
]
|
|
98
|
+
return "\n".join(lines)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _render_mdc(body: str) -> str:
|
|
102
|
+
lines = [
|
|
103
|
+
"---",
|
|
104
|
+
"description: Coding agent rules generated from AGENTS.md by agentsbridge",
|
|
105
|
+
"globs: **/*",
|
|
106
|
+
"alwaysApply: true",
|
|
107
|
+
"---",
|
|
108
|
+
"",
|
|
109
|
+
GENERATED_MARKER,
|
|
110
|
+
"",
|
|
111
|
+
"> Generated from `AGENTS.md` by [agentsbridge](https://github.com/DavidStarYu/agentsbridge).",
|
|
112
|
+
"> Run `agentsbridge sync` after editing `AGENTS.md`. Do not edit this file directly.",
|
|
113
|
+
"",
|
|
114
|
+
body.rstrip(),
|
|
115
|
+
"",
|
|
116
|
+
]
|
|
117
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentsbridge
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Bridge AGENTS.md to every AI coding tool's rules file - one source of truth for Claude Code, Copilot, Cursor, Windsurf, Cline, and Aider.
|
|
5
|
+
Author: agentsbridge contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/DavidStarYu/agentsbridge
|
|
8
|
+
Project-URL: Repository, https://github.com/DavidStarYu/agentsbridge
|
|
9
|
+
Project-URL: Changelog, https://github.com/DavidStarYu/agentsbridge/blob/main/CHANGELOG.md
|
|
10
|
+
Keywords: agents-md,claude,claude-code,cursor,copilot,windsurf,cline,aider,ai,rules,cli,developer-tools
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development
|
|
23
|
+
Classifier: Topic :: Utilities
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
<div align="center">
|
|
30
|
+
|
|
31
|
+
<img src="assets/banner.svg" alt="agentsbridge" width="640">
|
|
32
|
+
|
|
33
|
+
**One `AGENTS.md`. Every AI coding tool. Zero drift.**
|
|
34
|
+
|
|
35
|
+
[](https://github.com/DavidStarYu/agentsbridge/actions/workflows/ci.yml)
|
|
36
|
+
[](https://pypi.org/project/agentsbridge/)
|
|
37
|
+
[](https://pypi.org/project/agentsbridge/)
|
|
38
|
+
[](LICENSE)
|
|
39
|
+
[](https://github.com/astral-sh/ruff)
|
|
40
|
+
|
|
41
|
+
[Bridge](#why) · [Quick start](#quick-start) · [Supported tools](#supported-tools) · [CI drift check](#ci-drift-check) · [FAQ](#faq)
|
|
42
|
+
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Why
|
|
48
|
+
|
|
49
|
+
Your team uses Claude Code. Your teammate uses Cursor. Someone just switched
|
|
50
|
+
to Codex CLI. And suddenly your coding standards live in **five different
|
|
51
|
+
files** that silently drift apart:
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
AGENTS.md ← Codex, Gemini CLI, Jules, Amp, Zed read this
|
|
55
|
+
CLAUDE.md ← Claude Code reads this (not AGENTS.md)
|
|
56
|
+
.github/copilot-instructions.md ← GitHub Copilot reads this (not AGENTS.md)
|
|
57
|
+
.cursor/rules/*.mdc ← Cursor reads this (not AGENTS.md, older versions)
|
|
58
|
+
.windsurfrules / .clinerules ← Windsurf / Cline read these
|
|
59
|
+
CONVENTIONS.md ← Aider reads this
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`agentsbridge` fixes this with one command. You maintain **one** `AGENTS.md`
|
|
63
|
+
(the emerging industry standard) — it generates and keeps every tool's file
|
|
64
|
+
in sync. Edit once, sync everywhere, and let CI catch anything that drifts.
|
|
65
|
+
|
|
66
|
+
## Quick start
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pipx install agentsbridge # or: pip install agentsbridge / uv tool install agentsbridge
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Already have rules somewhere?** Import them:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
agentsbridge import # seeds AGENTS.md from CLAUDE.md, .cursorrules, etc.
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Starting fresh?**
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
agentsbridge init # creates a starter AGENTS.md
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Then — the only command you'll ever need again:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
agentsbridge sync
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
+ created CLAUDE.md
|
|
92
|
+
+ created .github/copilot-instructions.md
|
|
93
|
+
+ created .cursor/rules/agentsbridge.mdc
|
|
94
|
+
+ created .windsurfrules
|
|
95
|
+
+ created .clinerules
|
|
96
|
+
+ created CONVENTIONS.md
|
|
97
|
+
|
|
98
|
+
wrote 6 file(s)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Edit `AGENTS.md`, run `agentsbridge sync`, commit. That's the whole workflow.
|
|
102
|
+
|
|
103
|
+
## Supported tools
|
|
104
|
+
|
|
105
|
+
| Tool | File generated | Notes |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| **Claude Code** | `CLAUDE.md` | |
|
|
108
|
+
| **GitHub Copilot** | `.github/copilot-instructions.md` | |
|
|
109
|
+
| **Cursor** | `.cursor/rules/agentsbridge.mdc` | with `alwaysApply` frontmatter |
|
|
110
|
+
| **Windsurf** | `.windsurfrules` | |
|
|
111
|
+
| **Cline** | `.clinerules` | |
|
|
112
|
+
| **Aider** | `CONVENTIONS.md` | |
|
|
113
|
+
| **Codex CLI, Gemini CLI, Jules, Amp, Zed, opencode** | — | read `AGENTS.md` natively, nothing to generate |
|
|
114
|
+
|
|
115
|
+
Missing a tool? [Open an issue](https://github.com/DavidStarYu/agentsbridge/issues) —
|
|
116
|
+
adding a target is ~10 lines.
|
|
117
|
+
|
|
118
|
+
## Safety
|
|
119
|
+
|
|
120
|
+
Generated files are marked:
|
|
121
|
+
|
|
122
|
+
```markdown
|
|
123
|
+
<!-- generated by agentsbridge; do not edit -->
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
If a target file already exists **without** that marker (i.e. you wrote it by
|
|
127
|
+
hand), `sync` skips it instead of clobbering your work. Use `--force` to
|
|
128
|
+
adopt it deliberately.
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
agentsbridge sync --force # adopt existing files
|
|
132
|
+
agentsbridge sync --dry-run # preview changes
|
|
133
|
+
agentsbridge sync -t claude,copilot # subset of targets
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## CI drift check
|
|
137
|
+
|
|
138
|
+
The whole point: rules that drift are rules nobody follows. Add the check to
|
|
139
|
+
your workflow and CI fails whenever someone edits a generated file — or
|
|
140
|
+
forgets to re-sync after editing `AGENTS.md`:
|
|
141
|
+
|
|
142
|
+
```yaml
|
|
143
|
+
# .github/workflows/ci.yml
|
|
144
|
+
name: rules
|
|
145
|
+
on: [push, pull_request]
|
|
146
|
+
jobs:
|
|
147
|
+
agentsbridge:
|
|
148
|
+
runs-on: ubuntu-latest
|
|
149
|
+
steps:
|
|
150
|
+
- uses: actions/checkout@v4
|
|
151
|
+
- uses: DavidStarYu/agentsbridge/action@main
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Or invoke the CLI directly: `agentsbridge check` (exit code 1 on drift).
|
|
155
|
+
|
|
156
|
+
This repository runs the same check on itself — [dogfooding](.github/workflows/rules.yml).
|
|
157
|
+
|
|
158
|
+
## FAQ
|
|
159
|
+
|
|
160
|
+
**Why AGENTS.md as the source, not my own config?**
|
|
161
|
+
AGENTS.md is the emerging standard (adopted by OpenAI Codex, Gemini CLI, Jules,
|
|
162
|
+
Amp, Zed and others — see [agents.md](https://agents.md)). Tools that don't
|
|
163
|
+
read it yet are exactly the ones this bridge targets. You keep zero
|
|
164
|
+
agentsbridge-specific config: the source file is a standard.
|
|
165
|
+
|
|
166
|
+
**Cursor already reads AGENTS.md, why generate an .mdc?**
|
|
167
|
+
Newer Cursor versions read `AGENTS.md` directly; many teams run versions that
|
|
168
|
+
don't, or want explicit rule scoping. The generated `.mdc` is harmless if
|
|
169
|
+
redundant — and you can exclude it: `agentsbridge sync -t claude,copilot,...`.
|
|
170
|
+
|
|
171
|
+
**Does it send my code anywhere?**
|
|
172
|
+
No. No network calls, no telemetry, no API keys. It's a template engine over
|
|
173
|
+
one markdown file. [Zero dependencies](pyproject.toml).
|
|
174
|
+
|
|
175
|
+
**How is this different from X?**
|
|
176
|
+
rulesync (Node.js) covers more features — MCP servers, subagents, commands —
|
|
177
|
+
at the cost of a bigger footprint and a `.rulesync/` directory convention.
|
|
178
|
+
agentsbridge does one thing: keep rule *files* in sync, in pure Python with
|
|
179
|
+
zero dependencies. If you live in `pipx`/`uv` and want the simple thing, this
|
|
180
|
+
is for you.
|
|
181
|
+
|
|
182
|
+
## Development
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
git clone https://github.com/DavidStarYu/agentsbridge
|
|
186
|
+
cd agentsbridge
|
|
187
|
+
pip install -e . pytest ruff
|
|
188
|
+
pytest # 36 tests
|
|
189
|
+
ruff check .
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
agentsbridge/__init__.py,sha256=Xkjv3FNyx666xFTvwEPOhJHhHlis0Krzlc4h_ggN7oM,99
|
|
2
|
+
agentsbridge/cli.py,sha256=O6UTKHWGFtwFgxmwkyV8eNH3knFv68y8KeLyGxUCFxU,5341
|
|
3
|
+
agentsbridge/core.py,sha256=t_UG2814vP2x6IZOF6ATU_by_X3nMuxdvSkcwwVZJKw,6235
|
|
4
|
+
agentsbridge/targets.py,sha256=PnYwSjz7FNA7iKRp1AGhQ508puJ9Pi12_VwFP0ALB8g,3172
|
|
5
|
+
agentsbridge-0.1.0.dist-info/licenses/LICENSE,sha256=iRxJZoPltXN8yg2qdGpddFfFkyoijlDgaCy_Q3B3pqg,1082
|
|
6
|
+
agentsbridge-0.1.0.dist-info/METADATA,sha256=rTx2k0sPMFQSH6ZTtE-TATqCMQJErC8e5DKFTNSnmTA,6834
|
|
7
|
+
agentsbridge-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
8
|
+
agentsbridge-0.1.0.dist-info/entry_points.txt,sha256=eBXAYoARN5l9hW82X2GpYCu-aamwpgVTWDE7x9IiDu0,55
|
|
9
|
+
agentsbridge-0.1.0.dist-info/top_level.txt,sha256=uHtHVPF3WL52X35o7sWaOYHMsiO-C2t-xPrqLk1p7k8,13
|
|
10
|
+
agentsbridge-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 agentsbridge contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
agentsbridge
|