ai-agent-rules 0.11.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.
Potentially problematic release.
This version of ai-agent-rules might be problematic. Click here for more details.
- ai_agent_rules-0.11.0.dist-info/METADATA +390 -0
- ai_agent_rules-0.11.0.dist-info/RECORD +42 -0
- ai_agent_rules-0.11.0.dist-info/WHEEL +5 -0
- ai_agent_rules-0.11.0.dist-info/entry_points.txt +3 -0
- ai_agent_rules-0.11.0.dist-info/licenses/LICENSE +22 -0
- ai_agent_rules-0.11.0.dist-info/top_level.txt +1 -0
- ai_rules/__init__.py +8 -0
- ai_rules/agents/__init__.py +1 -0
- ai_rules/agents/base.py +68 -0
- ai_rules/agents/claude.py +121 -0
- ai_rules/agents/goose.py +44 -0
- ai_rules/agents/shared.py +35 -0
- ai_rules/bootstrap/__init__.py +75 -0
- ai_rules/bootstrap/config.py +261 -0
- ai_rules/bootstrap/installer.py +249 -0
- ai_rules/bootstrap/updater.py +221 -0
- ai_rules/bootstrap/version.py +52 -0
- ai_rules/cli.py +2292 -0
- ai_rules/completions.py +194 -0
- ai_rules/config/AGENTS.md +249 -0
- ai_rules/config/chat_agent_hints.md +1 -0
- ai_rules/config/claude/agents/code-reviewer.md +121 -0
- ai_rules/config/claude/commands/annotate-changelog.md +191 -0
- ai_rules/config/claude/commands/comment-cleanup.md +161 -0
- ai_rules/config/claude/commands/continue-crash.md +38 -0
- ai_rules/config/claude/commands/dev-docs.md +169 -0
- ai_rules/config/claude/commands/pr-creator.md +247 -0
- ai_rules/config/claude/commands/test-cleanup.md +244 -0
- ai_rules/config/claude/commands/update-docs.md +324 -0
- ai_rules/config/claude/hooks/subagentStop.py +92 -0
- ai_rules/config/claude/mcps.json +1 -0
- ai_rules/config/claude/settings.json +116 -0
- ai_rules/config/claude/skills/doc-writer/SKILL.md +293 -0
- ai_rules/config/claude/skills/doc-writer/resources/templates.md +495 -0
- ai_rules/config/claude/skills/prompt-engineer/SKILL.md +272 -0
- ai_rules/config/claude/skills/prompt-engineer/resources/prompt_engineering_guide_2025.md +855 -0
- ai_rules/config/claude/skills/prompt-engineer/resources/templates.md +232 -0
- ai_rules/config/goose/config.yaml +55 -0
- ai_rules/config.py +635 -0
- ai_rules/display.py +40 -0
- ai_rules/mcp.py +370 -0
- ai_rules/symlinks.py +207 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Update checking and application utilities."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
import urllib.request
|
|
8
|
+
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
from .installer import (
|
|
13
|
+
UV_NOT_FOUND_ERROR,
|
|
14
|
+
_validate_package_name,
|
|
15
|
+
get_tool_source,
|
|
16
|
+
get_tool_version,
|
|
17
|
+
is_command_available,
|
|
18
|
+
)
|
|
19
|
+
from .version import is_newer
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
PYPI_JSON_API_URL = "https://pypi.org/pypi/{package_name}/json"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class UpdateInfo:
|
|
28
|
+
"""Information about available updates."""
|
|
29
|
+
|
|
30
|
+
has_update: bool
|
|
31
|
+
current_version: str
|
|
32
|
+
latest_version: str
|
|
33
|
+
source: str # "pypi"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def check_pypi_updates(
|
|
37
|
+
package_name: str, current_version: str, timeout: int = 10
|
|
38
|
+
) -> UpdateInfo:
|
|
39
|
+
"""Check PyPI for newer version.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
package_name: Package name on PyPI
|
|
43
|
+
current_version: Currently installed version
|
|
44
|
+
timeout: Request timeout in seconds (default: 10)
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
UpdateInfo with update status
|
|
48
|
+
"""
|
|
49
|
+
# Validate package name (PEP 508 compliant)
|
|
50
|
+
if not re.match(r"^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$", package_name):
|
|
51
|
+
return UpdateInfo(
|
|
52
|
+
has_update=False,
|
|
53
|
+
current_version=current_version,
|
|
54
|
+
latest_version=current_version,
|
|
55
|
+
source="pypi",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
url = PYPI_JSON_API_URL.format(package_name=package_name)
|
|
60
|
+
|
|
61
|
+
req = urllib.request.Request(url)
|
|
62
|
+
req.add_header("User-Agent", f"{package_name}/{current_version}")
|
|
63
|
+
|
|
64
|
+
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
65
|
+
data = json.loads(response.read().decode())
|
|
66
|
+
|
|
67
|
+
latest_version = data["info"]["version"]
|
|
68
|
+
has_update = is_newer(latest_version, current_version)
|
|
69
|
+
|
|
70
|
+
return UpdateInfo(
|
|
71
|
+
has_update=has_update,
|
|
72
|
+
current_version=current_version,
|
|
73
|
+
latest_version=latest_version,
|
|
74
|
+
source="pypi",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
except (urllib.error.URLError, json.JSONDecodeError, KeyError) as e:
|
|
78
|
+
logger.debug(f"PyPI check failed: {e}")
|
|
79
|
+
return UpdateInfo(
|
|
80
|
+
has_update=False,
|
|
81
|
+
current_version=current_version,
|
|
82
|
+
latest_version=current_version,
|
|
83
|
+
source="pypi",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def perform_pypi_update(package_name: str) -> tuple[bool, str, bool]:
|
|
88
|
+
"""Upgrade via uv tool upgrade.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
package_name: Name of package to upgrade
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Tuple of (success, message, was_upgraded)
|
|
95
|
+
- success: Whether command succeeded
|
|
96
|
+
- message: Human-readable status message
|
|
97
|
+
- was_upgraded: True if package was actually upgraded (not already up-to-date)
|
|
98
|
+
"""
|
|
99
|
+
if not _validate_package_name(package_name):
|
|
100
|
+
return False, f"Invalid package name: {package_name}", False
|
|
101
|
+
|
|
102
|
+
if not is_command_available("uv"):
|
|
103
|
+
return False, UV_NOT_FOUND_ERROR, False
|
|
104
|
+
|
|
105
|
+
source = get_tool_source(package_name)
|
|
106
|
+
|
|
107
|
+
if source == "local":
|
|
108
|
+
cmd = ["uv", "tool", "install", package_name, "--force", "--no-cache"]
|
|
109
|
+
else:
|
|
110
|
+
cmd = ["uv", "tool", "upgrade", package_name, "--no-cache"]
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
result = subprocess.run(
|
|
114
|
+
cmd,
|
|
115
|
+
capture_output=True,
|
|
116
|
+
text=True,
|
|
117
|
+
timeout=60,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if result.returncode == 0:
|
|
121
|
+
output = result.stdout + result.stderr
|
|
122
|
+
|
|
123
|
+
upgrade_patterns = [
|
|
124
|
+
r"Upgraded .+ from .+ to .+",
|
|
125
|
+
r"Installed .+ \d+\.\d+",
|
|
126
|
+
r"Successfully installed",
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
already_up_to_date_patterns = [
|
|
130
|
+
r"Nothing to upgrade",
|
|
131
|
+
r"already.*installed",
|
|
132
|
+
r"already.*up.*to.*date",
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
was_upgraded = False
|
|
136
|
+
if any(
|
|
137
|
+
re.search(pattern, output, re.IGNORECASE)
|
|
138
|
+
for pattern in upgrade_patterns
|
|
139
|
+
):
|
|
140
|
+
was_upgraded = True
|
|
141
|
+
elif any(
|
|
142
|
+
re.search(pattern, output, re.IGNORECASE)
|
|
143
|
+
for pattern in already_up_to_date_patterns
|
|
144
|
+
):
|
|
145
|
+
was_upgraded = False
|
|
146
|
+
else:
|
|
147
|
+
was_upgraded = True
|
|
148
|
+
|
|
149
|
+
return True, "Upgrade successful", was_upgraded
|
|
150
|
+
|
|
151
|
+
error_msg = result.stderr.strip()
|
|
152
|
+
if not error_msg:
|
|
153
|
+
error_msg = "Upgrade failed with no error message"
|
|
154
|
+
|
|
155
|
+
return False, error_msg, False
|
|
156
|
+
|
|
157
|
+
except subprocess.TimeoutExpired:
|
|
158
|
+
return False, "Upgrade timed out after 60 seconds", False
|
|
159
|
+
except Exception as e:
|
|
160
|
+
return False, f"Unexpected error: {e}", False
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass
|
|
164
|
+
class ToolSpec:
|
|
165
|
+
"""Specification for an updatable tool."""
|
|
166
|
+
|
|
167
|
+
tool_id: str
|
|
168
|
+
package_name: str
|
|
169
|
+
display_name: str
|
|
170
|
+
get_version: Callable[[], str | None]
|
|
171
|
+
is_installed: Callable[[], bool]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
UPDATABLE_TOOLS: list[ToolSpec] = [
|
|
175
|
+
ToolSpec(
|
|
176
|
+
tool_id="ai-rules",
|
|
177
|
+
package_name="ai-agent-rules",
|
|
178
|
+
display_name="ai-rules",
|
|
179
|
+
get_version=lambda: get_tool_version("ai-agent-rules"),
|
|
180
|
+
is_installed=lambda: True, # Always installed (it's us)
|
|
181
|
+
),
|
|
182
|
+
ToolSpec(
|
|
183
|
+
tool_id="statusline",
|
|
184
|
+
package_name="claude-code-statusline",
|
|
185
|
+
display_name="statusline",
|
|
186
|
+
get_version=lambda: get_tool_version("claude-code-statusline"),
|
|
187
|
+
is_installed=lambda: is_command_available("claude-statusline"),
|
|
188
|
+
),
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def check_tool_updates(tool: ToolSpec, timeout: int = 10) -> UpdateInfo | None:
|
|
193
|
+
"""Check for updates for any tool.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
tool: Tool specification
|
|
197
|
+
timeout: Request timeout in seconds (default: 10)
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
UpdateInfo if tool is installed and update check succeeds, None otherwise
|
|
201
|
+
"""
|
|
202
|
+
if not tool.is_installed():
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
current = tool.get_version()
|
|
206
|
+
if current is None:
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
return check_pypi_updates(tool.package_name, current, timeout)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def get_tool_by_id(tool_id: str) -> ToolSpec | None:
|
|
213
|
+
"""Look up tool spec by ID.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
tool_id: Tool identifier (e.g., "ai-rules", "statusline")
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
ToolSpec if found, None otherwise
|
|
220
|
+
"""
|
|
221
|
+
return next((t for t in UPDATABLE_TOOLS if t.tool_id == tool_id), None)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Version utilities for package management."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version as get_version
|
|
4
|
+
|
|
5
|
+
from packaging.version import InvalidVersion, Version
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def parse_version(version_str: str) -> Version:
|
|
9
|
+
"""Parse version string, handling 'v' prefix.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
version_str: Version string (e.g., "1.2.3" or "v1.2.3")
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Parsed Version object
|
|
16
|
+
|
|
17
|
+
Raises:
|
|
18
|
+
InvalidVersion: If version string is malformed
|
|
19
|
+
"""
|
|
20
|
+
clean = version_str.lstrip("v")
|
|
21
|
+
return Version(clean)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def is_newer(latest: str, current: str) -> bool:
|
|
25
|
+
"""Check if latest version is newer than current.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
latest: Latest version string
|
|
29
|
+
current: Current version string
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
True if latest is newer, False otherwise
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
return parse_version(latest) > parse_version(current)
|
|
36
|
+
except InvalidVersion:
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_package_version(package_name: str) -> str:
|
|
41
|
+
"""Get installed version of a package.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
package_name: Name of the package (e.g., "ai-rules")
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Version string (e.g., "0.3.0")
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
PackageNotFoundError: If package is not installed
|
|
51
|
+
"""
|
|
52
|
+
return get_version(package_name)
|