agr 0.4.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.
- agr/__init__.py +3 -0
- agr/cli/__init__.py +5 -0
- agr/cli/add.py +132 -0
- agr/cli/common.py +1085 -0
- agr/cli/init.py +292 -0
- agr/cli/main.py +34 -0
- agr/cli/remove.py +125 -0
- agr/cli/run.py +385 -0
- agr/cli/sync.py +263 -0
- agr/cli/update.py +140 -0
- agr/config.py +187 -0
- agr/exceptions.py +33 -0
- agr/fetcher.py +781 -0
- agr/github.py +95 -0
- agr/scaffold.py +194 -0
- agr-0.4.0.dist-info/METADATA +17 -0
- agr-0.4.0.dist-info/RECORD +19 -0
- agr-0.4.0.dist-info/WHEEL +4 -0
- agr-0.4.0.dist-info/entry_points.txt +3 -0
agr/cli/update.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Update subcommand for agr - re-fetch resources from GitHub."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from agr.cli.common import handle_update_bundle, handle_update_resource
|
|
8
|
+
from agr.fetcher import ResourceType
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(
|
|
11
|
+
help="Update skills, commands, or agents from GitHub.",
|
|
12
|
+
no_args_is_help=True,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.command("skill")
|
|
17
|
+
def update_skill(
|
|
18
|
+
skill_ref: Annotated[
|
|
19
|
+
str,
|
|
20
|
+
typer.Argument(
|
|
21
|
+
help="Skill reference: <username>/<skill-name> or <username>/<repo>/<skill-name>",
|
|
22
|
+
metavar="REFERENCE",
|
|
23
|
+
),
|
|
24
|
+
],
|
|
25
|
+
global_install: Annotated[
|
|
26
|
+
bool,
|
|
27
|
+
typer.Option(
|
|
28
|
+
"--global",
|
|
29
|
+
"-g",
|
|
30
|
+
help="Update in ~/.claude/ instead of ./.claude/",
|
|
31
|
+
),
|
|
32
|
+
] = False,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Update a skill by re-fetching from GitHub.
|
|
35
|
+
|
|
36
|
+
REFERENCE format:
|
|
37
|
+
- username/skill-name: re-fetches from github.com/username/agent-resources
|
|
38
|
+
- username/repo/skill-name: re-fetches from github.com/username/repo
|
|
39
|
+
|
|
40
|
+
Examples:
|
|
41
|
+
agr update skill kasperjunge/hello-world
|
|
42
|
+
agr update skill kasperjunge/my-repo/hello-world --global
|
|
43
|
+
"""
|
|
44
|
+
handle_update_resource(skill_ref, ResourceType.SKILL, "skills", global_install)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@app.command("command")
|
|
48
|
+
def update_command(
|
|
49
|
+
command_ref: Annotated[
|
|
50
|
+
str,
|
|
51
|
+
typer.Argument(
|
|
52
|
+
help="Command reference: <username>/<command-name> or <username>/<repo>/<command-name>",
|
|
53
|
+
metavar="REFERENCE",
|
|
54
|
+
),
|
|
55
|
+
],
|
|
56
|
+
global_install: Annotated[
|
|
57
|
+
bool,
|
|
58
|
+
typer.Option(
|
|
59
|
+
"--global",
|
|
60
|
+
"-g",
|
|
61
|
+
help="Update in ~/.claude/ instead of ./.claude/",
|
|
62
|
+
),
|
|
63
|
+
] = False,
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Update a slash command by re-fetching from GitHub.
|
|
66
|
+
|
|
67
|
+
REFERENCE format:
|
|
68
|
+
- username/command-name: re-fetches from github.com/username/agent-resources
|
|
69
|
+
- username/repo/command-name: re-fetches from github.com/username/repo
|
|
70
|
+
|
|
71
|
+
Examples:
|
|
72
|
+
agr update command kasperjunge/hello
|
|
73
|
+
agr update command kasperjunge/my-repo/hello-world --global
|
|
74
|
+
"""
|
|
75
|
+
handle_update_resource(command_ref, ResourceType.COMMAND, "commands", global_install)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@app.command("agent")
|
|
79
|
+
def update_agent(
|
|
80
|
+
agent_ref: Annotated[
|
|
81
|
+
str,
|
|
82
|
+
typer.Argument(
|
|
83
|
+
help="Agent reference: <username>/<agent-name> or <username>/<repo>/<agent-name>",
|
|
84
|
+
metavar="REFERENCE",
|
|
85
|
+
),
|
|
86
|
+
],
|
|
87
|
+
global_install: Annotated[
|
|
88
|
+
bool,
|
|
89
|
+
typer.Option(
|
|
90
|
+
"--global",
|
|
91
|
+
"-g",
|
|
92
|
+
help="Update in ~/.claude/ instead of ./.claude/",
|
|
93
|
+
),
|
|
94
|
+
] = False,
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Update a sub-agent by re-fetching from GitHub.
|
|
97
|
+
|
|
98
|
+
REFERENCE format:
|
|
99
|
+
- username/agent-name: re-fetches from github.com/username/agent-resources
|
|
100
|
+
- username/repo/agent-name: re-fetches from github.com/username/repo
|
|
101
|
+
|
|
102
|
+
Examples:
|
|
103
|
+
agr update agent kasperjunge/hello-agent
|
|
104
|
+
agr update agent kasperjunge/my-repo/hello-agent --global
|
|
105
|
+
"""
|
|
106
|
+
handle_update_resource(agent_ref, ResourceType.AGENT, "agents", global_install)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@app.command("bundle")
|
|
110
|
+
def update_bundle(
|
|
111
|
+
bundle_ref: Annotated[
|
|
112
|
+
str,
|
|
113
|
+
typer.Argument(
|
|
114
|
+
help="Bundle reference: <username>/<bundle-name> or <username>/<repo>/<bundle-name>",
|
|
115
|
+
metavar="REFERENCE",
|
|
116
|
+
),
|
|
117
|
+
],
|
|
118
|
+
global_install: Annotated[
|
|
119
|
+
bool,
|
|
120
|
+
typer.Option(
|
|
121
|
+
"--global",
|
|
122
|
+
"-g",
|
|
123
|
+
help="Update in ~/.claude/ instead of ./.claude/",
|
|
124
|
+
),
|
|
125
|
+
] = False,
|
|
126
|
+
) -> None:
|
|
127
|
+
"""Update a bundle by re-fetching from GitHub.
|
|
128
|
+
|
|
129
|
+
Re-downloads all resources from the bundle and overwrites local copies.
|
|
130
|
+
Also adds any new resources that were added to the bundle upstream.
|
|
131
|
+
|
|
132
|
+
REFERENCE format:
|
|
133
|
+
- username/bundle-name: re-fetches from github.com/username/agent-resources
|
|
134
|
+
- username/repo/bundle-name: re-fetches from github.com/username/repo
|
|
135
|
+
|
|
136
|
+
Examples:
|
|
137
|
+
agr update bundle kasperjunge/productivity
|
|
138
|
+
agr update bundle kasperjunge/my-repo/productivity --global
|
|
139
|
+
"""
|
|
140
|
+
handle_update_bundle(bundle_ref, global_install)
|
agr/config.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Configuration management for agr.toml."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import tomlkit
|
|
7
|
+
from tomlkit import TOMLDocument
|
|
8
|
+
from tomlkit.exceptions import TOMLKitError
|
|
9
|
+
|
|
10
|
+
from agr.exceptions import ConfigParseError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class DependencySpec:
|
|
15
|
+
"""Specification for a dependency in agr.toml."""
|
|
16
|
+
|
|
17
|
+
type: str | None = None # "skill", "command", "agent"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class AgrConfig:
|
|
22
|
+
"""
|
|
23
|
+
Configuration loaded from agr.toml.
|
|
24
|
+
|
|
25
|
+
The config file tracks dependencies with fully qualified references:
|
|
26
|
+
|
|
27
|
+
[dependencies]
|
|
28
|
+
"kasperjunge/commit" = {}
|
|
29
|
+
"alice/review" = { type = "skill" }
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
dependencies: dict[str, DependencySpec] = field(default_factory=dict)
|
|
33
|
+
_document: TOMLDocument | None = field(default=None, repr=False)
|
|
34
|
+
_path: Path | None = field(default=None, repr=False)
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def load(cls, path: Path) -> "AgrConfig":
|
|
38
|
+
"""
|
|
39
|
+
Load configuration from an agr.toml file.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
path: Path to the agr.toml file
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
AgrConfig instance with loaded dependencies
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
ConfigParseError: If the file contains invalid TOML
|
|
49
|
+
"""
|
|
50
|
+
if not path.exists():
|
|
51
|
+
config = cls()
|
|
52
|
+
config._path = path
|
|
53
|
+
return config
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
content = path.read_text()
|
|
57
|
+
doc = tomlkit.parse(content)
|
|
58
|
+
except TOMLKitError as e:
|
|
59
|
+
raise ConfigParseError(f"Invalid TOML in {path}: {e}")
|
|
60
|
+
|
|
61
|
+
config = cls()
|
|
62
|
+
config._document = doc
|
|
63
|
+
config._path = path
|
|
64
|
+
|
|
65
|
+
# Parse dependencies section
|
|
66
|
+
deps_section = doc.get("dependencies", {})
|
|
67
|
+
for ref, spec in deps_section.items():
|
|
68
|
+
if isinstance(spec, dict):
|
|
69
|
+
config.dependencies[ref] = DependencySpec(
|
|
70
|
+
type=spec.get("type")
|
|
71
|
+
)
|
|
72
|
+
else:
|
|
73
|
+
config.dependencies[ref] = DependencySpec()
|
|
74
|
+
|
|
75
|
+
return config
|
|
76
|
+
|
|
77
|
+
def save(self, path: Path | None = None) -> None:
|
|
78
|
+
"""
|
|
79
|
+
Save configuration to an agr.toml file.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
path: Path to save to (uses original path if not specified)
|
|
83
|
+
"""
|
|
84
|
+
save_path = path or self._path
|
|
85
|
+
if save_path is None:
|
|
86
|
+
raise ValueError("No path specified for saving config")
|
|
87
|
+
|
|
88
|
+
# Use existing document to preserve comments, or create new one
|
|
89
|
+
if self._document is not None:
|
|
90
|
+
doc = self._document
|
|
91
|
+
else:
|
|
92
|
+
doc = tomlkit.document()
|
|
93
|
+
|
|
94
|
+
# Update dependencies section
|
|
95
|
+
if "dependencies" not in doc:
|
|
96
|
+
doc["dependencies"] = tomlkit.table()
|
|
97
|
+
|
|
98
|
+
deps_table = doc["dependencies"]
|
|
99
|
+
|
|
100
|
+
# Clear existing dependencies and rebuild
|
|
101
|
+
# First, collect keys to remove
|
|
102
|
+
existing_keys = list(deps_table.keys())
|
|
103
|
+
for key in existing_keys:
|
|
104
|
+
del deps_table[key]
|
|
105
|
+
|
|
106
|
+
# Add current dependencies
|
|
107
|
+
for ref, spec in self.dependencies.items():
|
|
108
|
+
if spec.type:
|
|
109
|
+
deps_table[ref] = {"type": spec.type}
|
|
110
|
+
else:
|
|
111
|
+
deps_table[ref] = {}
|
|
112
|
+
|
|
113
|
+
save_path.write_text(tomlkit.dumps(doc))
|
|
114
|
+
self._document = doc
|
|
115
|
+
self._path = save_path
|
|
116
|
+
|
|
117
|
+
def add_dependency(self, ref: str, spec: DependencySpec) -> None:
|
|
118
|
+
"""
|
|
119
|
+
Add or update a dependency.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
ref: Dependency reference (e.g., "kasperjunge/commit")
|
|
123
|
+
spec: Dependency specification
|
|
124
|
+
"""
|
|
125
|
+
self.dependencies[ref] = spec
|
|
126
|
+
|
|
127
|
+
def remove_dependency(self, ref: str) -> None:
|
|
128
|
+
"""
|
|
129
|
+
Remove a dependency.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
ref: Dependency reference to remove
|
|
133
|
+
"""
|
|
134
|
+
self.dependencies.pop(ref, None)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def find_config(start_path: Path | None = None) -> Path | None:
|
|
138
|
+
"""
|
|
139
|
+
Find agr.toml by walking up from the start path to the git root.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
start_path: Directory to start searching from (defaults to cwd)
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Path to agr.toml if found, None otherwise
|
|
146
|
+
"""
|
|
147
|
+
current = start_path or Path.cwd()
|
|
148
|
+
|
|
149
|
+
while True:
|
|
150
|
+
config_path = current / "agr.toml"
|
|
151
|
+
if config_path.exists():
|
|
152
|
+
return config_path
|
|
153
|
+
|
|
154
|
+
# Check if we've reached git root
|
|
155
|
+
if (current / ".git").exists():
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
# Move to parent
|
|
159
|
+
parent = current.parent
|
|
160
|
+
if parent == current:
|
|
161
|
+
# Reached filesystem root
|
|
162
|
+
return None
|
|
163
|
+
current = parent
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def get_or_create_config(start_path: Path | None = None) -> tuple[Path, AgrConfig]:
|
|
167
|
+
"""
|
|
168
|
+
Get existing config or create a new one in cwd.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
start_path: Directory to start searching from (defaults to cwd)
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Tuple of (path to config, AgrConfig instance)
|
|
175
|
+
"""
|
|
176
|
+
existing = find_config(start_path)
|
|
177
|
+
if existing:
|
|
178
|
+
return existing, AgrConfig.load(existing)
|
|
179
|
+
|
|
180
|
+
# Create new config in cwd
|
|
181
|
+
cwd = start_path or Path.cwd()
|
|
182
|
+
config_path = cwd / "agr.toml"
|
|
183
|
+
|
|
184
|
+
config = AgrConfig()
|
|
185
|
+
config.save(config_path)
|
|
186
|
+
|
|
187
|
+
return config_path, config
|
agr/exceptions.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Shared exception classes for agr."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AgrError(Exception):
|
|
5
|
+
"""Base exception for agr errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RepoNotFoundError(AgrError):
|
|
9
|
+
"""Raised when the GitHub repo doesn't exist."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ResourceNotFoundError(AgrError):
|
|
13
|
+
"""Raised when the skill/command/agent doesn't exist in the repo."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ResourceExistsError(AgrError):
|
|
17
|
+
"""Raised when the resource already exists locally."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BundleNotFoundError(AgrError):
|
|
21
|
+
"""Raised when no bundle directory exists in any resource type."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MultipleResourcesFoundError(AgrError):
|
|
25
|
+
"""Raised when a resource name exists in multiple resource types."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ConfigNotFoundError(AgrError):
|
|
29
|
+
"""Raised when agr.toml is required but not found."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ConfigParseError(AgrError):
|
|
33
|
+
"""Raised when agr.toml contains invalid TOML syntax."""
|