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 ADDED
@@ -0,0 +1,3 @@
1
+ """acp — Python package manager and MCP server for Agent Context Protocol projects."""
2
+
3
+ __version__ = "0.3.0"
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."""