agent-context-mcp 0.3.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.
- acp/__init__.py +3 -0
- acp/cli/__init__.py +1 -0
- acp/cli/init.py +222 -0
- acp/cli/main.py +26 -0
- acp/cli/package.py +183 -0
- acp/cli/version.py +190 -0
- acp/core/__init__.py +1 -0
- acp/core/fetch.py +78 -0
- acp/core/package_manager.py +335 -0
- acp/mcp/__init__.py +1 -0
- acp/mcp/server.py +380 -0
- agent_context_mcp-0.3.0.dist-info/METADATA +114 -0
- agent_context_mcp-0.3.0.dist-info/RECORD +15 -0
- agent_context_mcp-0.3.0.dist-info/WHEEL +4 -0
- agent_context_mcp-0.3.0.dist-info/entry_points.txt +5 -0
acp/__init__.py
ADDED
acp/cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI entry points."""
|
acp/cli/init.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""acp init — bootstrap a new ACP project.
|
|
2
|
+
|
|
3
|
+
Fetches templates from agent-context-protocol (GitHub) at runtime via a
|
|
4
|
+
shallow git clone. Templates are not bundled with the wheel; this means
|
|
5
|
+
templates can evolve independently of acp-mcp release cadence.
|
|
6
|
+
|
|
7
|
+
Requires: git on PATH.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
import shutil
|
|
13
|
+
from datetime import date
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import typer
|
|
17
|
+
import yaml
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
from rich.text import Text
|
|
20
|
+
|
|
21
|
+
from acp.core.fetch import fetch_acp_templates
|
|
22
|
+
|
|
23
|
+
console = Console()
|
|
24
|
+
|
|
25
|
+
app = typer.Typer(
|
|
26
|
+
help="Bootstrap a new ACP project.",
|
|
27
|
+
no_args_is_help=False,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Directories inside agent/ to skip entirely when copying from a clone.
|
|
31
|
+
_SKIP_AGENT_SUBDIRS = frozenset({
|
|
32
|
+
"reports",
|
|
33
|
+
"clarifications",
|
|
34
|
+
"feedback",
|
|
35
|
+
"drafts",
|
|
36
|
+
"projects",
|
|
37
|
+
"scripts",
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
# Directories inside agent/ where we copy ONLY *.template.* files
|
|
41
|
+
# (not project-specific instances the source repo might have).
|
|
42
|
+
_TEMPLATE_ONLY_SUBDIRS = frozenset({
|
|
43
|
+
"design",
|
|
44
|
+
"milestones",
|
|
45
|
+
"tasks",
|
|
46
|
+
"patterns",
|
|
47
|
+
"specs",
|
|
48
|
+
"artifacts",
|
|
49
|
+
"sessions",
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _should_copy(rel: Path) -> bool:
|
|
54
|
+
"""Return True if this path from the clone root should be copied to the target.
|
|
55
|
+
|
|
56
|
+
rel is relative to the clone root (e.g. "agent/commands/acp.proceed.md").
|
|
57
|
+
"""
|
|
58
|
+
parts = rel.parts
|
|
59
|
+
|
|
60
|
+
# Skip .git, .github, and other hidden top-level dirs
|
|
61
|
+
if parts[0].startswith("."):
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
# At top level: only AGENT.md and .gitignore are meaningful for new projects
|
|
65
|
+
if len(parts) == 1:
|
|
66
|
+
return parts[0] in ("AGENT.md", ".gitignore")
|
|
67
|
+
|
|
68
|
+
# Inside agent/: apply subdirectory rules
|
|
69
|
+
if parts[0] == "agent":
|
|
70
|
+
if len(parts) < 2:
|
|
71
|
+
return False # bare "agent" dir entry — not a file
|
|
72
|
+
|
|
73
|
+
subdir = parts[1]
|
|
74
|
+
|
|
75
|
+
# Flat files directly under agent/ — templates like progress.template.yaml
|
|
76
|
+
if len(parts) == 2:
|
|
77
|
+
name = parts[1]
|
|
78
|
+
# Only copy *.template.* files; skip live files like progress.yaml
|
|
79
|
+
return ".template." in name
|
|
80
|
+
|
|
81
|
+
# Skip excluded subdirs entirely
|
|
82
|
+
if subdir in _SKIP_AGENT_SUBDIRS:
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
# Template-only subdirs: only *.template.* files
|
|
86
|
+
if subdir in _TEMPLATE_ONLY_SUBDIRS:
|
|
87
|
+
name = parts[-1]
|
|
88
|
+
return ".template." in name
|
|
89
|
+
|
|
90
|
+
# Other subdirs (commands, schemas, index): copy everything
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
# Anything else at top level that isn't AGENT.md — skip
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _copy_templates(src_root: Path, target: Path) -> int:
|
|
98
|
+
"""Copy filtered template files from src_root into target. Returns file count."""
|
|
99
|
+
file_count = 0
|
|
100
|
+
for src_file in sorted(src_root.rglob("*")):
|
|
101
|
+
if not src_file.is_file():
|
|
102
|
+
continue
|
|
103
|
+
try:
|
|
104
|
+
rel = src_file.relative_to(src_root)
|
|
105
|
+
except ValueError:
|
|
106
|
+
continue
|
|
107
|
+
if not _should_copy(rel):
|
|
108
|
+
continue
|
|
109
|
+
dest = target / rel
|
|
110
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
shutil.copy2(src_file, dest)
|
|
112
|
+
file_count += 1
|
|
113
|
+
return file_count
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _generate_progress_yaml(project_name: str) -> str:
|
|
117
|
+
"""Generate a minimal, valid progress.yaml for a fresh project."""
|
|
118
|
+
today = date.today().isoformat()
|
|
119
|
+
data = {
|
|
120
|
+
"project": {
|
|
121
|
+
"name": project_name,
|
|
122
|
+
"version": "0.1.0",
|
|
123
|
+
"started": today,
|
|
124
|
+
"status": "in_progress",
|
|
125
|
+
"current_milestone": None,
|
|
126
|
+
"description": f"ACP project: {project_name}",
|
|
127
|
+
},
|
|
128
|
+
"milestones": [],
|
|
129
|
+
"tasks": {},
|
|
130
|
+
"documentation": {
|
|
131
|
+
"design_documents": 0,
|
|
132
|
+
"milestone_documents": 0,
|
|
133
|
+
"pattern_documents": 0,
|
|
134
|
+
"task_documents": 0,
|
|
135
|
+
"last_updated": today,
|
|
136
|
+
},
|
|
137
|
+
"progress": {
|
|
138
|
+
"planning": 0,
|
|
139
|
+
"implementation": 0,
|
|
140
|
+
"testing": 0,
|
|
141
|
+
"documentation": 0,
|
|
142
|
+
"overall": 0,
|
|
143
|
+
},
|
|
144
|
+
"recent_work": [],
|
|
145
|
+
"next_steps": [
|
|
146
|
+
"Define requirements in agent/design/requirements.md",
|
|
147
|
+
"Create first milestone in agent/milestones/",
|
|
148
|
+
"Run `acp package list` to see installed packages",
|
|
149
|
+
],
|
|
150
|
+
"notes": [],
|
|
151
|
+
"current_blockers": [],
|
|
152
|
+
}
|
|
153
|
+
return yaml.dump(data, default_flow_style=False, allow_unicode=True, sort_keys=False)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def read_acp_version_from_agent_md(agent_md_path: Path) -> str | None:
|
|
157
|
+
"""Extract **Version**: field from AGENT.md. Returns None if not found."""
|
|
158
|
+
if not agent_md_path.exists():
|
|
159
|
+
return None
|
|
160
|
+
text = agent_md_path.read_text(encoding="utf-8", errors="replace")
|
|
161
|
+
match = re.search(r"\*\*Version\*\*\s*:\s*(.+)", text)
|
|
162
|
+
return match.group(1).strip() if match else None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@app.callback(invoke_without_command=True)
|
|
166
|
+
def init(
|
|
167
|
+
path: Path = typer.Argument(Path("."), help="Directory to initialize. Use '.' for current."),
|
|
168
|
+
force: bool = typer.Option(False, "--force", help="Overwrite existing agent/ directory."),
|
|
169
|
+
branch: str = typer.Option("mainline", "--branch", help="agent-context-protocol branch to fetch."),
|
|
170
|
+
) -> None:
|
|
171
|
+
"""Bootstrap a new ACP project at PATH (default: current directory).
|
|
172
|
+
|
|
173
|
+
Fetches templates from agent-context-protocol (GitHub) at runtime.
|
|
174
|
+
Requires git on PATH.
|
|
175
|
+
"""
|
|
176
|
+
target = path.resolve()
|
|
177
|
+
agent_dir = target / "agent"
|
|
178
|
+
|
|
179
|
+
# Guard: already initialized
|
|
180
|
+
if agent_dir.exists() and not force:
|
|
181
|
+
console.print(
|
|
182
|
+
Text(
|
|
183
|
+
f"[error] ACP project already initialized at {target}\n"
|
|
184
|
+
" Use --force to overwrite.",
|
|
185
|
+
style="bold red",
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
raise typer.Exit(code=1)
|
|
189
|
+
|
|
190
|
+
# Create target directory if it doesn't exist
|
|
191
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
192
|
+
|
|
193
|
+
console.print(f"[bold]Bootstrapping ACP project at[/bold] {target}")
|
|
194
|
+
console.print(" Fetching templates from agent-context-protocol…")
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
with fetch_acp_templates(branch=branch) as src_root:
|
|
198
|
+
file_count = _copy_templates(src_root, target)
|
|
199
|
+
except RuntimeError as exc:
|
|
200
|
+
console.print(f"[red]Error:[/red] {exc}")
|
|
201
|
+
raise typer.Exit(code=1)
|
|
202
|
+
|
|
203
|
+
console.print(f" [green]✓[/green] Copied {file_count} template files")
|
|
204
|
+
|
|
205
|
+
# Generate agent/progress.yaml (project-specific; not a template)
|
|
206
|
+
progress_path = agent_dir / "progress.yaml"
|
|
207
|
+
progress_path.parent.mkdir(parents=True, exist_ok=True)
|
|
208
|
+
progress_path.write_text(
|
|
209
|
+
_generate_progress_yaml(project_name=target.name),
|
|
210
|
+
encoding="utf-8",
|
|
211
|
+
)
|
|
212
|
+
console.print(" [green]✓[/green] Created agent/progress.yaml")
|
|
213
|
+
|
|
214
|
+
console.print()
|
|
215
|
+
console.print("[bold green]Done![/bold green] ACP project initialized.")
|
|
216
|
+
console.print()
|
|
217
|
+
console.print("[bold]Next steps:[/bold]")
|
|
218
|
+
if str(path) != ".":
|
|
219
|
+
console.print(f" cd {path}")
|
|
220
|
+
console.print(" Define requirements in [cyan]agent/design/requirements.md[/cyan]")
|
|
221
|
+
console.print(" Run [cyan]acp package list[/cyan] to see installed packages")
|
|
222
|
+
console.print(" Start drafting milestones and tasks in [cyan]agent/milestones/[/cyan]")
|
acp/cli/main.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""acp CLI entry point.
|
|
2
|
+
|
|
3
|
+
Routes top-level subcommands (package, version, init) to their respective
|
|
4
|
+
modules. Workflow commands (proceed, status, etc.) are intentionally NOT
|
|
5
|
+
exposed here — those remain as markdown directives invoked via @acp.* syntax.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from acp.cli import init as init_cmd
|
|
12
|
+
from acp.cli import package as package_cmd
|
|
13
|
+
from acp.cli import version as version_cmd
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(
|
|
16
|
+
help="ACP package manager and MCP integration.",
|
|
17
|
+
no_args_is_help=True,
|
|
18
|
+
add_completion=False,
|
|
19
|
+
)
|
|
20
|
+
app.add_typer(package_cmd.app, name="package")
|
|
21
|
+
app.add_typer(version_cmd.app, name="version")
|
|
22
|
+
app.add_typer(init_cmd.app, name="init")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if __name__ == "__main__":
|
|
26
|
+
app()
|
acp/cli/package.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""acp package — package management subcommands."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from acp.core.package_manager import (
|
|
11
|
+
info_package,
|
|
12
|
+
install_package,
|
|
13
|
+
list_packages,
|
|
14
|
+
remove_package,
|
|
15
|
+
update_package,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
app = typer.Typer(
|
|
21
|
+
help="Package management: install, list, update, remove, info.",
|
|
22
|
+
no_args_is_help=True,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _project_dir() -> Path:
|
|
27
|
+
return Path.cwd()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app.command()
|
|
31
|
+
def install(
|
|
32
|
+
repo: str = typer.Argument(..., help="Git repository URL of the ACP package."),
|
|
33
|
+
global_install: bool = typer.Option(
|
|
34
|
+
False, "--global", help="Install to ~/.acp/agent/ instead of ./agent/."
|
|
35
|
+
),
|
|
36
|
+
experimental: bool = typer.Option(
|
|
37
|
+
False, "--experimental", help="Include experimental package contents."
|
|
38
|
+
),
|
|
39
|
+
force: bool = typer.Option(False, "--force", help="Reinstall even if already installed."),
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Install an ACP package from a git repository."""
|
|
42
|
+
console.print(f"Installing [cyan]{repo}[/cyan]…")
|
|
43
|
+
try:
|
|
44
|
+
result = install_package(repo, _project_dir(), global_install, experimental, force)
|
|
45
|
+
except RuntimeError as exc:
|
|
46
|
+
console.print(f"[red]Error:[/red] {exc}")
|
|
47
|
+
raise typer.Exit(code=1)
|
|
48
|
+
|
|
49
|
+
pkg_name = result["name"]
|
|
50
|
+
pkg_version = result["version"]
|
|
51
|
+
installed = result["installed_files"]
|
|
52
|
+
warnings = result["warnings"]
|
|
53
|
+
|
|
54
|
+
console.print(
|
|
55
|
+
f"[green]Installed[/green] [bold]{pkg_name}[/bold] v{pkg_version}"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Summary by kind
|
|
59
|
+
for kind, files in installed.items():
|
|
60
|
+
if files:
|
|
61
|
+
console.print(f" {kind}: {len(files)} file(s)")
|
|
62
|
+
|
|
63
|
+
for w in warnings:
|
|
64
|
+
console.print(f" [yellow]warn:[/yellow] {w}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@app.command(name="list")
|
|
68
|
+
def list_packages_cmd(
|
|
69
|
+
global_install: bool = typer.Option(
|
|
70
|
+
False, "--global", help="List packages from ~/.acp/agent/manifest.yaml."
|
|
71
|
+
),
|
|
72
|
+
) -> None:
|
|
73
|
+
"""List installed ACP packages."""
|
|
74
|
+
try:
|
|
75
|
+
packages = list_packages(_project_dir(), global_install)
|
|
76
|
+
except Exception as exc:
|
|
77
|
+
console.print(f"[red]Error:[/red] {exc}")
|
|
78
|
+
raise typer.Exit(code=1)
|
|
79
|
+
|
|
80
|
+
if not packages:
|
|
81
|
+
scope = "global (~/.acp/agent/)" if global_install else "project (./agent/)"
|
|
82
|
+
console.print(f"No ACP packages installed in {scope}.")
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
table = Table(show_header=True, header_style="bold")
|
|
86
|
+
table.add_column("Name")
|
|
87
|
+
table.add_column("Version")
|
|
88
|
+
table.add_column("Files")
|
|
89
|
+
table.add_column("Updated")
|
|
90
|
+
|
|
91
|
+
for pkg in packages:
|
|
92
|
+
file_summary = ", ".join(
|
|
93
|
+
f"{count} {kind}" for kind, count in pkg["file_counts"].items()
|
|
94
|
+
)
|
|
95
|
+
updated = (pkg.get("updated_at") or "")[:10] # date portion
|
|
96
|
+
table.add_row(pkg["name"], pkg["version"], file_summary, updated)
|
|
97
|
+
|
|
98
|
+
console.print(table)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@app.command()
|
|
102
|
+
def update(
|
|
103
|
+
name: str = typer.Argument(..., help="Package name to update."),
|
|
104
|
+
global_install: bool = typer.Option(
|
|
105
|
+
False, "--global", help="Update in global context."
|
|
106
|
+
),
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Update an installed ACP package to the latest version from its source."""
|
|
109
|
+
console.print(f"Updating [cyan]{name}[/cyan]…")
|
|
110
|
+
try:
|
|
111
|
+
result = update_package(name, _project_dir(), global_install)
|
|
112
|
+
except RuntimeError as exc:
|
|
113
|
+
console.print(f"[red]Error:[/red] {exc}")
|
|
114
|
+
raise typer.Exit(code=1)
|
|
115
|
+
|
|
116
|
+
console.print(
|
|
117
|
+
f"[green]Updated[/green] [bold]{result['name']}[/bold] to v{result['version']}"
|
|
118
|
+
)
|
|
119
|
+
for w in result.get("warnings", []):
|
|
120
|
+
console.print(f" [yellow]warn:[/yellow] {w}")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@app.command()
|
|
124
|
+
def remove(
|
|
125
|
+
name: str = typer.Argument(..., help="Package name to remove."),
|
|
126
|
+
global_install: bool = typer.Option(
|
|
127
|
+
False, "--global", help="Remove from global context."
|
|
128
|
+
),
|
|
129
|
+
) -> None:
|
|
130
|
+
"""Remove an installed ACP package."""
|
|
131
|
+
try:
|
|
132
|
+
result = remove_package(name, _project_dir(), global_install)
|
|
133
|
+
except RuntimeError as exc:
|
|
134
|
+
console.print(f"[red]Error:[/red] {exc}")
|
|
135
|
+
raise typer.Exit(code=1)
|
|
136
|
+
|
|
137
|
+
removed = result["removed"]
|
|
138
|
+
missing = result["missing"]
|
|
139
|
+
console.print(f"[green]Removed[/green] [bold]{name}[/bold] ({len(removed)} file(s) deleted)")
|
|
140
|
+
for m in missing:
|
|
141
|
+
console.print(f" [yellow]warn:[/yellow] file not found: {m}")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@app.command()
|
|
145
|
+
def info(
|
|
146
|
+
name: str = typer.Argument(..., help="Package name to inspect."),
|
|
147
|
+
global_install: bool = typer.Option(
|
|
148
|
+
False, "--global", help="Look up in global context."
|
|
149
|
+
),
|
|
150
|
+
) -> None:
|
|
151
|
+
"""Show details for an installed ACP package."""
|
|
152
|
+
try:
|
|
153
|
+
pkg = info_package(name, _project_dir(), global_install)
|
|
154
|
+
except Exception as exc:
|
|
155
|
+
console.print(f"[red]Error:[/red] {exc}")
|
|
156
|
+
raise typer.Exit(code=1)
|
|
157
|
+
|
|
158
|
+
if pkg is None:
|
|
159
|
+
console.print(f"[red]Package '{name}' is not installed.[/red]")
|
|
160
|
+
raise typer.Exit(code=1)
|
|
161
|
+
|
|
162
|
+
console.print(f"[bold]{pkg['name']}[/bold] v{pkg['version']}")
|
|
163
|
+
console.print(f" Source: {pkg['source']}")
|
|
164
|
+
console.print(f" Installed: {(pkg.get('installed_at') or '')[:19]}")
|
|
165
|
+
console.print(f" Updated: {(pkg.get('updated_at') or '')[:19]}")
|
|
166
|
+
console.print(" Files:")
|
|
167
|
+
for kind, files in pkg.get("files", {}).items():
|
|
168
|
+
console.print(f" {kind}: {len(files)}")
|
|
169
|
+
for f in files:
|
|
170
|
+
console.print(f" - {f.get('name', '?')}")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@app.command()
|
|
174
|
+
def search(
|
|
175
|
+
query: str = typer.Argument(..., help="Search query (package name or keyword)."),
|
|
176
|
+
) -> None:
|
|
177
|
+
"""Search for ACP packages. (Registry not yet implemented.)"""
|
|
178
|
+
console.print(
|
|
179
|
+
"[yellow]acp package search[/yellow] requires a package registry that is not yet live.\n"
|
|
180
|
+
"To install a package directly, use:\n"
|
|
181
|
+
" [cyan]acp package install <git-url>[/cyan]"
|
|
182
|
+
)
|
|
183
|
+
raise typer.Exit(code=0)
|
acp/cli/version.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""acp version — manage a project's ACP methodology version.
|
|
2
|
+
|
|
3
|
+
Commands:
|
|
4
|
+
show — print the project's installed ACP version (from AGENT.md)
|
|
5
|
+
check — compare project to agent-context-protocol mainline; report update status
|
|
6
|
+
update — fetch latest templates and refresh project's agent/ artifacts
|
|
7
|
+
|
|
8
|
+
Note: to upgrade the acp-mcp tool itself, run `uv tool upgrade acp-mcp` directly.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
|
|
18
|
+
from acp.core.fetch import fetch_acp_templates
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
app = typer.Typer(
|
|
23
|
+
help="Manage the project's ACP methodology version.",
|
|
24
|
+
no_args_is_help=True,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
_VERSION_RE = re.compile(r"\*\*Version\*\*\s*:\s*(.+)")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _find_project_root(start: Path) -> Path | None:
|
|
31
|
+
"""Walk up from start looking for a directory containing agent/AGENT.md or agent/."""
|
|
32
|
+
current = start.resolve()
|
|
33
|
+
for candidate in [current, *current.parents]:
|
|
34
|
+
if (candidate / "AGENT.md").exists():
|
|
35
|
+
return candidate
|
|
36
|
+
if (candidate / "agent").is_dir():
|
|
37
|
+
return candidate
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _read_agent_md_version(agent_md: Path) -> str | None:
|
|
42
|
+
"""Extract **Version**: from AGENT.md. Returns None if not found or file absent."""
|
|
43
|
+
if not agent_md.exists():
|
|
44
|
+
return None
|
|
45
|
+
text = agent_md.read_text(encoding="utf-8", errors="replace")
|
|
46
|
+
match = _VERSION_RE.search(text)
|
|
47
|
+
return match.group(1).strip() if match else None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _copy_templates_to_project(src_root: Path, project_root: Path) -> int:
|
|
51
|
+
"""Copy ACP template files from a clone root into an existing project.
|
|
52
|
+
|
|
53
|
+
Uses the same filter logic as `acp init`: only template files are copied,
|
|
54
|
+
preserving user-authored project-specific content.
|
|
55
|
+
|
|
56
|
+
Returns the count of files written.
|
|
57
|
+
"""
|
|
58
|
+
from acp.cli.init import _copy_templates
|
|
59
|
+
return _copy_templates(src_root, project_root)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.command()
|
|
63
|
+
def show(
|
|
64
|
+
project: Path = typer.Option(
|
|
65
|
+
Path("."),
|
|
66
|
+
"--project",
|
|
67
|
+
help="Project directory (default: current directory).",
|
|
68
|
+
),
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Print the project's installed ACP version (from AGENT.md)."""
|
|
71
|
+
root = _find_project_root(project)
|
|
72
|
+
if root is None:
|
|
73
|
+
console.print("[red]Not an ACP project:[/red] could not find project root.")
|
|
74
|
+
raise typer.Exit(code=1)
|
|
75
|
+
|
|
76
|
+
agent_md = root / "AGENT.md"
|
|
77
|
+
version = _read_agent_md_version(agent_md)
|
|
78
|
+
|
|
79
|
+
if version is None:
|
|
80
|
+
console.print(
|
|
81
|
+
f"[yellow]ACP version unknown.[/yellow] "
|
|
82
|
+
f"AGENT.md not found or missing **Version** field at {root}"
|
|
83
|
+
)
|
|
84
|
+
raise typer.Exit(code=1)
|
|
85
|
+
|
|
86
|
+
console.print(version)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@app.command()
|
|
90
|
+
def check(
|
|
91
|
+
project: Path = typer.Option(
|
|
92
|
+
Path("."),
|
|
93
|
+
"--project",
|
|
94
|
+
help="Project directory (default: current directory).",
|
|
95
|
+
),
|
|
96
|
+
branch: str = typer.Option("mainline", "--branch", help="Upstream branch to compare against."),
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Compare the project's ACP version against agent-context-protocol mainline."""
|
|
99
|
+
root = _find_project_root(project)
|
|
100
|
+
if root is None:
|
|
101
|
+
console.print("[red]Not an ACP project:[/red] could not find project root.")
|
|
102
|
+
raise typer.Exit(code=1)
|
|
103
|
+
|
|
104
|
+
agent_md = root / "AGENT.md"
|
|
105
|
+
local_version = _read_agent_md_version(agent_md)
|
|
106
|
+
|
|
107
|
+
if local_version is None:
|
|
108
|
+
console.print(
|
|
109
|
+
"[yellow]ACP version unknown.[/yellow] "
|
|
110
|
+
f"AGENT.md missing or no **Version** field at {root}."
|
|
111
|
+
)
|
|
112
|
+
raise typer.Exit(code=1)
|
|
113
|
+
|
|
114
|
+
console.print(f"Project ACP version: [bold]{local_version}[/bold]")
|
|
115
|
+
console.print(f"Fetching upstream version from agent-context-protocol ({branch})…")
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
with fetch_acp_templates(branch=branch) as src_root:
|
|
119
|
+
upstream_version = _read_agent_md_version(src_root / "AGENT.md")
|
|
120
|
+
except RuntimeError as exc:
|
|
121
|
+
console.print(f"[red]Could not fetch upstream:[/red] {exc}")
|
|
122
|
+
raise typer.Exit(code=1)
|
|
123
|
+
|
|
124
|
+
if upstream_version is None:
|
|
125
|
+
console.print("[yellow]Upstream AGENT.md missing **Version** field.[/yellow]")
|
|
126
|
+
raise typer.Exit(code=1)
|
|
127
|
+
|
|
128
|
+
console.print(f"Upstream ACP version: [bold]{upstream_version}[/bold]")
|
|
129
|
+
|
|
130
|
+
if local_version == upstream_version:
|
|
131
|
+
console.print("[green]Up to date.[/green]")
|
|
132
|
+
else:
|
|
133
|
+
console.print(
|
|
134
|
+
f"[yellow]Update available:[/yellow] {local_version} → {upstream_version}"
|
|
135
|
+
)
|
|
136
|
+
console.print(" Run [cyan]acp version update[/cyan] to refresh.")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@app.command()
|
|
140
|
+
def update(
|
|
141
|
+
project: Path = typer.Option(
|
|
142
|
+
Path("."),
|
|
143
|
+
"--project",
|
|
144
|
+
help="Project directory (default: current directory).",
|
|
145
|
+
),
|
|
146
|
+
branch: str = typer.Option("mainline", "--branch", help="Upstream branch to fetch from."),
|
|
147
|
+
force: bool = typer.Option(False, "--force", help="Apply updates even if versions match."),
|
|
148
|
+
) -> None:
|
|
149
|
+
"""Refresh the project's ACP artifacts from agent-context-protocol mainline.
|
|
150
|
+
|
|
151
|
+
Only template files are updated. Project-specific content (your design docs,
|
|
152
|
+
task instances, etc.) is not touched.
|
|
153
|
+
|
|
154
|
+
Requires git on PATH.
|
|
155
|
+
"""
|
|
156
|
+
root = _find_project_root(project)
|
|
157
|
+
if root is None:
|
|
158
|
+
console.print("[red]Not an ACP project:[/red] could not find project root.")
|
|
159
|
+
raise typer.Exit(code=1)
|
|
160
|
+
|
|
161
|
+
# Read current local version for user-facing output (best-effort)
|
|
162
|
+
agent_md = root / "AGENT.md"
|
|
163
|
+
local_version = _read_agent_md_version(agent_md)
|
|
164
|
+
if local_version:
|
|
165
|
+
console.print(f"Current ACP version: [bold]{local_version}[/bold]")
|
|
166
|
+
|
|
167
|
+
console.print(f"Fetching latest templates from agent-context-protocol ({branch})…")
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
with fetch_acp_templates(branch=branch) as src_root:
|
|
171
|
+
upstream_version = _read_agent_md_version(src_root / "AGENT.md")
|
|
172
|
+
|
|
173
|
+
if not force and local_version and upstream_version == local_version:
|
|
174
|
+
console.print(f"[green]Already up to date.[/green] ({local_version})")
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
file_count = _copy_templates_to_project(src_root, root)
|
|
178
|
+
except RuntimeError as exc:
|
|
179
|
+
console.print(f"[red]Error:[/red] {exc}")
|
|
180
|
+
raise typer.Exit(code=1)
|
|
181
|
+
|
|
182
|
+
console.print(f" [green]✓[/green] Updated {file_count} template files")
|
|
183
|
+
|
|
184
|
+
if upstream_version:
|
|
185
|
+
console.print(f" ACP version: [bold]{upstream_version}[/bold]")
|
|
186
|
+
|
|
187
|
+
console.print("[bold green]Done![/bold green] ACP templates refreshed.")
|
|
188
|
+
console.print(
|
|
189
|
+
" Note: your project-specific docs (designs, tasks, milestones) are unchanged."
|
|
190
|
+
)
|
acp/core/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core library — pure functions, no CLI/MCP surface."""
|