ai-agent-rules 0.11.0__py3-none-any.whl → 0.15.8__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 → ai_agent_rules-0.15.8.dist-info}/METADATA +91 -6
- {ai_agent_rules-0.11.0.dist-info → ai_agent_rules-0.15.8.dist-info}/RECORD +27 -16
- ai_rules/agents/claude.py +3 -1
- ai_rules/agents/cursor.py +70 -0
- ai_rules/agents/goose.py +4 -1
- ai_rules/bootstrap/__init__.py +8 -4
- ai_rules/bootstrap/installer.py +95 -23
- ai_rules/bootstrap/updater.py +183 -43
- ai_rules/cli.py +360 -42
- ai_rules/config/AGENTS.md +5 -4
- ai_rules/config/claude/CLAUDE.md +1 -0
- ai_rules/config/claude/commands/agents-md.md +422 -0
- ai_rules/config/claude/settings.json +7 -4
- ai_rules/config/cursor/keybindings.json +14 -0
- ai_rules/config/cursor/settings.json +81 -0
- ai_rules/config/goose/.goosehints +1 -0
- ai_rules/config/profiles/default.yaml +6 -0
- ai_rules/config/profiles/work.yaml +11 -0
- ai_rules/config.py +55 -46
- ai_rules/mcp.py +2 -3
- ai_rules/profiles.py +187 -0
- ai_rules/state.py +47 -0
- ai_rules/utils.py +35 -0
- {ai_agent_rules-0.11.0.dist-info → ai_agent_rules-0.15.8.dist-info}/WHEEL +0 -0
- {ai_agent_rules-0.11.0.dist-info → ai_agent_rules-0.15.8.dist-info}/entry_points.txt +0 -0
- {ai_agent_rules-0.11.0.dist-info → ai_agent_rules-0.15.8.dist-info}/licenses/LICENSE +0 -0
- {ai_agent_rules-0.11.0.dist-info → ai_agent_rules-0.15.8.dist-info}/top_level.txt +0 -0
ai_rules/bootstrap/updater.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
|
+
import os
|
|
5
6
|
import re
|
|
6
7
|
import subprocess
|
|
7
8
|
import urllib.request
|
|
@@ -10,17 +11,37 @@ from collections.abc import Callable
|
|
|
10
11
|
from dataclasses import dataclass
|
|
11
12
|
|
|
12
13
|
from .installer import (
|
|
14
|
+
GITHUB_REPO,
|
|
15
|
+
STATUSLINE_GITHUB_REPO,
|
|
13
16
|
UV_NOT_FOUND_ERROR,
|
|
17
|
+
ToolSource,
|
|
14
18
|
_validate_package_name,
|
|
15
19
|
get_tool_source,
|
|
16
20
|
get_tool_version,
|
|
17
21
|
is_command_available,
|
|
22
|
+
make_github_install_url,
|
|
18
23
|
)
|
|
19
24
|
from .version import is_newer
|
|
20
25
|
|
|
21
26
|
logger = logging.getLogger(__name__)
|
|
22
27
|
|
|
23
|
-
|
|
28
|
+
|
|
29
|
+
def get_configured_index_url() -> str | None:
|
|
30
|
+
"""Get package index URL from environment.
|
|
31
|
+
|
|
32
|
+
Checks in order of preference:
|
|
33
|
+
1. UV_DEFAULT_INDEX (modern uv, recommended)
|
|
34
|
+
2. UV_INDEX_URL (deprecated uv, still supported)
|
|
35
|
+
3. PIP_INDEX_URL (pip compatibility)
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Index URL if configured, None otherwise
|
|
39
|
+
"""
|
|
40
|
+
return (
|
|
41
|
+
os.environ.get("UV_DEFAULT_INDEX")
|
|
42
|
+
or os.environ.get("UV_INDEX_URL")
|
|
43
|
+
or os.environ.get("PIP_INDEX_URL")
|
|
44
|
+
)
|
|
24
45
|
|
|
25
46
|
|
|
26
47
|
@dataclass
|
|
@@ -30,65 +51,177 @@ class UpdateInfo:
|
|
|
30
51
|
has_update: bool
|
|
31
52
|
current_version: str
|
|
32
53
|
latest_version: str
|
|
33
|
-
source: str
|
|
54
|
+
source: str
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class ToolSpec:
|
|
59
|
+
"""Specification for an updatable tool."""
|
|
60
|
+
|
|
61
|
+
tool_id: str
|
|
62
|
+
package_name: str
|
|
63
|
+
display_name: str
|
|
64
|
+
get_version: Callable[[], str | None]
|
|
65
|
+
is_installed: Callable[[], bool]
|
|
66
|
+
github_repo: str | None = None
|
|
34
67
|
|
|
68
|
+
@property
|
|
69
|
+
def github_install_url(self) -> str | None:
|
|
70
|
+
"""Get the GitHub install URL for uv tool install."""
|
|
71
|
+
if self.github_repo:
|
|
72
|
+
return make_github_install_url(self.github_repo)
|
|
73
|
+
return None
|
|
35
74
|
|
|
36
|
-
|
|
37
|
-
|
|
75
|
+
|
|
76
|
+
def check_index_updates(
|
|
77
|
+
package_name: str, current_version: str, timeout: int = 30
|
|
38
78
|
) -> UpdateInfo:
|
|
39
|
-
"""Check
|
|
79
|
+
"""Check configured package index for newer version.
|
|
80
|
+
|
|
81
|
+
Uses `uvx pip index versions` to query the user's configured index,
|
|
82
|
+
which respects pip.conf and environment variables.
|
|
40
83
|
|
|
41
84
|
Args:
|
|
42
|
-
package_name: Package name
|
|
85
|
+
package_name: Package name to check
|
|
43
86
|
current_version: Currently installed version
|
|
44
|
-
timeout: Request timeout in seconds (default:
|
|
87
|
+
timeout: Request timeout in seconds (default: 30)
|
|
45
88
|
|
|
46
89
|
Returns:
|
|
47
90
|
UpdateInfo with update status
|
|
48
91
|
"""
|
|
49
|
-
|
|
50
|
-
|
|
92
|
+
if not _validate_package_name(package_name):
|
|
93
|
+
return UpdateInfo(
|
|
94
|
+
has_update=False,
|
|
95
|
+
current_version=current_version,
|
|
96
|
+
latest_version=current_version,
|
|
97
|
+
source="index",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if not is_command_available("uvx"):
|
|
101
|
+
return UpdateInfo(
|
|
102
|
+
has_update=False,
|
|
103
|
+
current_version=current_version,
|
|
104
|
+
latest_version=current_version,
|
|
105
|
+
source="index",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
cmd = ["uvx", "--refresh", "pip", "index", "versions", package_name]
|
|
110
|
+
|
|
111
|
+
# Pass index URL explicitly since pip doesn't understand UV_* env vars
|
|
112
|
+
if index_url := get_configured_index_url():
|
|
113
|
+
cmd.extend(["--index-url", index_url])
|
|
114
|
+
|
|
115
|
+
result = subprocess.run(
|
|
116
|
+
cmd,
|
|
117
|
+
capture_output=True,
|
|
118
|
+
text=True,
|
|
119
|
+
timeout=timeout,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if result.returncode != 0:
|
|
123
|
+
logger.debug(f"uvx pip index versions failed: {result.stderr}")
|
|
124
|
+
return UpdateInfo(
|
|
125
|
+
has_update=False,
|
|
126
|
+
current_version=current_version,
|
|
127
|
+
latest_version=current_version,
|
|
128
|
+
source="index",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
output = result.stdout.strip()
|
|
132
|
+
match = re.search(r"^\S+\s+\(([^)]+)\)", output)
|
|
133
|
+
if match:
|
|
134
|
+
latest_version = match.group(1)
|
|
135
|
+
has_update = is_newer(latest_version, current_version)
|
|
136
|
+
return UpdateInfo(
|
|
137
|
+
has_update=has_update,
|
|
138
|
+
current_version=current_version,
|
|
139
|
+
latest_version=latest_version,
|
|
140
|
+
source="index",
|
|
141
|
+
)
|
|
142
|
+
|
|
51
143
|
return UpdateInfo(
|
|
52
144
|
has_update=False,
|
|
53
145
|
current_version=current_version,
|
|
54
146
|
latest_version=current_version,
|
|
55
|
-
source="
|
|
147
|
+
source="index",
|
|
56
148
|
)
|
|
57
149
|
|
|
150
|
+
except subprocess.TimeoutExpired:
|
|
151
|
+
logger.debug("uvx pip index versions timed out")
|
|
152
|
+
return UpdateInfo(
|
|
153
|
+
has_update=False,
|
|
154
|
+
current_version=current_version,
|
|
155
|
+
latest_version=current_version,
|
|
156
|
+
source="index",
|
|
157
|
+
)
|
|
158
|
+
except Exception as e:
|
|
159
|
+
logger.debug(f"Index check failed: {e}")
|
|
160
|
+
return UpdateInfo(
|
|
161
|
+
has_update=False,
|
|
162
|
+
current_version=current_version,
|
|
163
|
+
latest_version=current_version,
|
|
164
|
+
source="index",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def check_github_updates(
|
|
169
|
+
repo: str, current_version: str, timeout: int = 10
|
|
170
|
+
) -> UpdateInfo:
|
|
171
|
+
"""Check GitHub tags for newer version.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
repo: GitHub repository in format "owner/repo"
|
|
175
|
+
current_version: Currently installed version
|
|
176
|
+
timeout: Request timeout in seconds (default: 10)
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
UpdateInfo with update status
|
|
180
|
+
"""
|
|
58
181
|
try:
|
|
59
|
-
url =
|
|
182
|
+
url = f"https://api.github.com/repos/{repo}/tags"
|
|
60
183
|
|
|
61
184
|
req = urllib.request.Request(url)
|
|
62
|
-
req.add_header("User-Agent", f"
|
|
185
|
+
req.add_header("User-Agent", f"ai-rules/{current_version}")
|
|
63
186
|
|
|
64
187
|
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
65
188
|
data = json.loads(response.read().decode())
|
|
66
189
|
|
|
67
|
-
|
|
190
|
+
if not data or len(data) == 0:
|
|
191
|
+
return UpdateInfo(
|
|
192
|
+
has_update=False,
|
|
193
|
+
current_version=current_version,
|
|
194
|
+
latest_version=current_version,
|
|
195
|
+
source="github",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
latest_tag = data[0]["name"]
|
|
199
|
+
latest_version = latest_tag.lstrip("v")
|
|
200
|
+
|
|
68
201
|
has_update = is_newer(latest_version, current_version)
|
|
69
202
|
|
|
70
203
|
return UpdateInfo(
|
|
71
204
|
has_update=has_update,
|
|
72
205
|
current_version=current_version,
|
|
73
206
|
latest_version=latest_version,
|
|
74
|
-
source="
|
|
207
|
+
source="github",
|
|
75
208
|
)
|
|
76
209
|
|
|
77
|
-
except (urllib.error.URLError, json.JSONDecodeError, KeyError) as e:
|
|
78
|
-
logger.debug(f"
|
|
210
|
+
except (urllib.error.URLError, json.JSONDecodeError, KeyError, IndexError) as e:
|
|
211
|
+
logger.debug(f"GitHub check failed: {e}")
|
|
79
212
|
return UpdateInfo(
|
|
80
213
|
has_update=False,
|
|
81
214
|
current_version=current_version,
|
|
82
215
|
latest_version=current_version,
|
|
83
|
-
source="
|
|
216
|
+
source="github",
|
|
84
217
|
)
|
|
85
218
|
|
|
86
219
|
|
|
87
|
-
def
|
|
88
|
-
"""Upgrade via uv
|
|
220
|
+
def perform_tool_upgrade(tool: ToolSpec) -> tuple[bool, str, bool]:
|
|
221
|
+
"""Upgrade a tool via uv, handling PyPI, GitHub, and local sources.
|
|
89
222
|
|
|
90
223
|
Args:
|
|
91
|
-
|
|
224
|
+
tool: Tool specification to upgrade
|
|
92
225
|
|
|
93
226
|
Returns:
|
|
94
227
|
Tuple of (success, message, was_upgraded)
|
|
@@ -96,18 +229,29 @@ def perform_pypi_update(package_name: str) -> tuple[bool, str, bool]:
|
|
|
96
229
|
- message: Human-readable status message
|
|
97
230
|
- was_upgraded: True if package was actually upgraded (not already up-to-date)
|
|
98
231
|
"""
|
|
99
|
-
if not _validate_package_name(package_name):
|
|
100
|
-
return False, f"Invalid package name: {package_name}", False
|
|
101
|
-
|
|
102
232
|
if not is_command_available("uv"):
|
|
103
233
|
return False, UV_NOT_FOUND_ERROR, False
|
|
104
234
|
|
|
105
|
-
source = get_tool_source(package_name)
|
|
106
|
-
|
|
107
|
-
if source ==
|
|
108
|
-
cmd = [
|
|
235
|
+
source = get_tool_source(tool.package_name)
|
|
236
|
+
|
|
237
|
+
if source == ToolSource.GITHUB and tool.github_install_url:
|
|
238
|
+
cmd = [
|
|
239
|
+
"uv",
|
|
240
|
+
"tool",
|
|
241
|
+
"install",
|
|
242
|
+
"--force",
|
|
243
|
+
"--reinstall",
|
|
244
|
+
tool.github_install_url,
|
|
245
|
+
]
|
|
109
246
|
else:
|
|
110
|
-
|
|
247
|
+
if not _validate_package_name(tool.package_name):
|
|
248
|
+
return False, f"Invalid package name: {tool.package_name}", False
|
|
249
|
+
cmd = ["uv", "tool", "upgrade", tool.package_name, "--no-cache"]
|
|
250
|
+
|
|
251
|
+
# Ensure upgrade uses same index as version check
|
|
252
|
+
# Use --default-index (modern) not --index-url (deprecated)
|
|
253
|
+
if index_url := get_configured_index_url():
|
|
254
|
+
cmd.extend(["--default-index", index_url])
|
|
111
255
|
|
|
112
256
|
try:
|
|
113
257
|
result = subprocess.run(
|
|
@@ -160,17 +304,6 @@ def perform_pypi_update(package_name: str) -> tuple[bool, str, bool]:
|
|
|
160
304
|
return False, f"Unexpected error: {e}", False
|
|
161
305
|
|
|
162
306
|
|
|
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
307
|
UPDATABLE_TOOLS: list[ToolSpec] = [
|
|
175
308
|
ToolSpec(
|
|
176
309
|
tool_id="ai-rules",
|
|
@@ -178,6 +311,7 @@ UPDATABLE_TOOLS: list[ToolSpec] = [
|
|
|
178
311
|
display_name="ai-rules",
|
|
179
312
|
get_version=lambda: get_tool_version("ai-agent-rules"),
|
|
180
313
|
is_installed=lambda: True, # Always installed (it's us)
|
|
314
|
+
github_repo=GITHUB_REPO,
|
|
181
315
|
),
|
|
182
316
|
ToolSpec(
|
|
183
317
|
tool_id="statusline",
|
|
@@ -185,16 +319,17 @@ UPDATABLE_TOOLS: list[ToolSpec] = [
|
|
|
185
319
|
display_name="statusline",
|
|
186
320
|
get_version=lambda: get_tool_version("claude-code-statusline"),
|
|
187
321
|
is_installed=lambda: is_command_available("claude-statusline"),
|
|
322
|
+
github_repo=STATUSLINE_GITHUB_REPO,
|
|
188
323
|
),
|
|
189
324
|
]
|
|
190
325
|
|
|
191
326
|
|
|
192
|
-
def check_tool_updates(tool: ToolSpec, timeout: int =
|
|
193
|
-
"""Check for updates for any tool.
|
|
327
|
+
def check_tool_updates(tool: ToolSpec, timeout: int = 30) -> UpdateInfo | None:
|
|
328
|
+
"""Check for updates for any tool - auto-detect PyPI vs GitHub source.
|
|
194
329
|
|
|
195
330
|
Args:
|
|
196
331
|
tool: Tool specification
|
|
197
|
-
timeout: Request timeout in seconds (default:
|
|
332
|
+
timeout: Request timeout in seconds (default: 30)
|
|
198
333
|
|
|
199
334
|
Returns:
|
|
200
335
|
UpdateInfo if tool is installed and update check succeeds, None otherwise
|
|
@@ -206,7 +341,12 @@ def check_tool_updates(tool: ToolSpec, timeout: int = 10) -> UpdateInfo | None:
|
|
|
206
341
|
if current is None:
|
|
207
342
|
return None
|
|
208
343
|
|
|
209
|
-
|
|
344
|
+
source = get_tool_source(tool.package_name)
|
|
345
|
+
|
|
346
|
+
if source == ToolSource.GITHUB and tool.github_repo:
|
|
347
|
+
return check_github_updates(tool.github_repo, current, timeout)
|
|
348
|
+
else:
|
|
349
|
+
return check_index_updates(tool.package_name, current, timeout)
|
|
210
350
|
|
|
211
351
|
|
|
212
352
|
def get_tool_by_id(tool_id: str) -> ToolSpec | None:
|