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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ai-agent-rules
3
- Version: 0.11.0
3
+ Version: 0.15.8
4
4
  Summary: Manage user-level AI agent configurations
5
5
  Author-email: Will Pfleger <pfleger.will@gmail.com>
6
6
  License-Expression: MIT
@@ -47,14 +47,14 @@ Manage AI agent configurations through symlinks. Keep all your configs in one gi
47
47
 
48
48
  ## Overview
49
49
 
50
- Consolidates config files for AI coding agents (Claude Code, Goose) into a single source of truth via symlinks:
50
+ Consolidates config files for AI coding agents (Claude Code, Cursor, Goose) into a single source of truth via symlinks:
51
51
 
52
52
  - Git-tracked configs synced across machines
53
53
  - Edit once, apply everywhere
54
54
  - Exclude specific files (e.g., company-managed)
55
55
  - Per-agent customizations
56
56
 
57
- **Supported:** Claude Code (settings, agents, commands), Goose (hints, config), Shared (AGENTS.md)
57
+ **Supported:** Claude Code (settings, agents, commands), Cursor (settings, keybindings), Goose (hints, config), Shared (AGENTS.md)
58
58
 
59
59
  ## Installation
60
60
 
@@ -75,6 +75,16 @@ This will:
75
75
 
76
76
  After setup, you can run `ai-rules` from any directory.
77
77
 
78
+ ### From GitHub (Development)
79
+
80
+ Install from GitHub to get the latest development code:
81
+
82
+ ```bash
83
+ uvx --from ai-agent-rules ai-rules setup --github
84
+ ```
85
+
86
+ This installs from the main branch and auto-detects the GitHub source for future updates.
87
+
78
88
  ### Local Development
79
89
 
80
90
  For contributing or local development:
@@ -82,10 +92,11 @@ For contributing or local development:
82
92
  ```bash
83
93
  git clone https://github.com/wpfleger96/ai-rules.git
84
94
  cd ai-rules
85
- uv tool install -e .
86
- ai-rules install
95
+ uv run ai-rules install
87
96
  ```
88
97
 
98
+ Use `uv run ai-rules <command>` to test local changes. The global `ai-rules` command continues to run your installed version (PyPI/GitHub).
99
+
89
100
  ### Updating
90
101
 
91
102
  Check for and install updates:
@@ -109,6 +120,8 @@ notify_only: false
109
120
 
110
121
  ```bash
111
122
  ai-rules setup # One-time setup: install symlinks + make available system-wide
123
+ ai-rules setup --github # Install from GitHub (pre-release)
124
+ ai-rules setup --profile work # Setup with a specific profile
112
125
  ai-rules upgrade # Upgrade to latest version
113
126
  ai-rules upgrade --check # Check for updates without installing
114
127
 
@@ -118,7 +131,7 @@ ai-rules install --dry-run # Preview changes
118
131
  ai-rules install --force # Skip confirmations
119
132
  ai-rules install --rebuild-cache # Rebuild merged settings cache
120
133
 
121
- ai-rules status # Check symlink status + optional tools (✓✗⚠○)
134
+ ai-rules status # Check symlink status + optional tools + active profile (✓✗⚠○)
122
135
  ai-rules diff # Show config differences
123
136
  ai-rules validate # Verify source files exist
124
137
  ai-rules update # Re-sync after adding files
@@ -145,6 +158,7 @@ ai-rules exclude list # List all exclusions
145
158
 
146
159
  # Manage settings overrides (for machine-specific settings)
147
160
  ai-rules override set claude.model "claude-sonnet-4-5-20250929" # Set simple override
161
+ ai-rules override set cursor.editor.fontSize 14 # Override Cursor font size
148
162
  ai-rules override set claude.hooks.SubagentStop[0].hooks[0].command "script.py" # Array notation
149
163
  ai-rules override unset claude.model # Remove override
150
164
  ai-rules override list # List all overrides
@@ -185,12 +199,17 @@ settings_overrides:
185
199
  claude:
186
200
  model: "claude-sonnet-4-5-20250929" # Override model on personal laptop
187
201
  # Other settings inherited from base config/claude/settings.json
202
+ cursor:
203
+ editor.fontSize: 14 # Override font size on this machine
188
204
  goose:
189
205
  provider: "anthropic"
190
206
  ```
191
207
 
192
208
  **Config File Location:**
193
209
  - `~/.ai-rules-config.yaml` - User-specific config (exclusions and overrides)
210
+ - `~/.ai-rules/state.yaml` - Active profile and last install timestamp (auto-managed)
211
+ - `~/.ai-rules/cache/` - Merged settings cache (auto-generated)
212
+ - `~/.ai-rules/update_config.yaml` - Update check configuration
194
213
 
195
214
  ### Settings Overrides - Syncing Configs Across Machines
196
215
 
@@ -241,6 +260,64 @@ ai-rules override set claude.modle "sonnet"
241
260
 
242
261
  Path validation ensures you only set valid overrides that exist in the base settings, preventing typos and configuration errors.
243
262
 
263
+ ### Cursor Settings
264
+
265
+ Cursor settings support the same override mechanism as other agents:
266
+
267
+ ```yaml
268
+ # ~/.ai-rules-config.yaml
269
+ settings_overrides:
270
+ cursor:
271
+ editor.fontSize: 14
272
+ terminal.integrated.defaultLocation: "editor"
273
+ ```
274
+
275
+ > **Note:** `keybindings.json` uses direct symlinks without override merging (array structure).
276
+
277
+ ### Profiles - Machine-Specific Configuration
278
+
279
+ Profiles let you group configuration overrides into named presets. Instead of manually maintaining different `~/.ai-rules-config.yaml` files across machines, define profiles once and select them at install time.
280
+
281
+ ```bash
282
+ # List available profiles
283
+ ai-rules profile list
284
+
285
+ # View profile details
286
+ ai-rules profile show work
287
+ ai-rules profile show work --resolved # Show with inheritance
288
+
289
+ # Check which profile is active
290
+ ai-rules profile current
291
+
292
+ # Switch to a different profile
293
+ ai-rules profile switch work
294
+
295
+ # Install with a specific profile
296
+ ai-rules install --profile work
297
+ ```
298
+
299
+ Profiles are stored in `src/ai_rules/config/profiles/` and support inheritance:
300
+
301
+ ```yaml
302
+ # profiles/work.yaml
303
+ name: work
304
+ description: Work laptop with extended context model
305
+ extends: null
306
+ settings_overrides:
307
+ claude:
308
+ env:
309
+ ANTHROPIC_DEFAULT_SONNET_MODEL: "claude-sonnet-4-5-20250929[1m]"
310
+ model: opusplan
311
+ ```
312
+
313
+ Configuration layers (lowest to highest priority):
314
+ 1. Profile overrides
315
+ 2. Local `~/.ai-rules-config.yaml` overrides
316
+
317
+ Your local config always wins, so you can use a profile as a base and tweak specific settings per-machine. Profiles are git-tracked and can be shared across your team.
318
+
319
+ The active profile is tracked in `~/.ai-rules/state.yaml` and persists across sessions. Use `profile current` to see which profile is active, or `profile switch` to quickly change profiles without re-running the full install.
320
+
244
321
  ## Structure
245
322
 
246
323
  ```
@@ -250,10 +327,18 @@ config/
250
327
  │ ├── settings.json # → ~/.claude/settings.json
251
328
  │ ├── agents/*.md # → ~/.claude/agents/*.md (dynamic)
252
329
  │ └── commands/*.md # → ~/.claude/commands/*.md (dynamic)
330
+ ├── cursor/
331
+ │ ├── settings.json # → ~/Library/Application Support/Cursor/User/ (macOS)
332
+ │ │ # ~/AppData/Roaming/Cursor/User/ (Windows)
333
+ │ │ # ~/.config/Cursor/User/ (Linux)
334
+ │ └── keybindings.json # → (same paths as settings.json)
253
335
  └── goose/
254
336
  └── config.yaml # → ~/.config/goose/config.yaml
255
337
  ```
256
338
 
339
+ > **Note:** The Cursor config files contain the maintainer's personal preferences
340
+ > (e.g., macOS-specific terminal settings). Customize for your environment.
341
+
257
342
  ## Optional Tools
258
343
 
259
344
  AI Rules automatically installs optional tools that enhance functionality:
@@ -1,26 +1,32 @@
1
- ai_agent_rules-0.11.0.dist-info/licenses/LICENSE,sha256=eRdOpQ8Kaod-FPwMA-sD9U2817DCp0QNub7f_UpqMe0,1070
1
+ ai_agent_rules-0.15.8.dist-info/licenses/LICENSE,sha256=eRdOpQ8Kaod-FPwMA-sD9U2817DCp0QNub7f_UpqMe0,1070
2
2
  ai_rules/__init__.py,sha256=h0sNb8H1ED7Dy0IXON1Ww3O8cls08Sr_g1osdcQ-4i0,231
3
- ai_rules/cli.py,sha256=oSxAP5cZlv6LXW81zhDslMR17lpwwp0Ss_IICdVwNQg,77887
3
+ ai_rules/cli.py,sha256=yi4iAjTRY6LzNtsoZLoHwmdy-TKCz78Q27pYYKszvn4,88918
4
4
  ai_rules/completions.py,sha256=7Ymgfzd93Owlscfn6sWkopbbAfT_J5PXnY5vl3CN_Xs,5736
5
- ai_rules/config.py,sha256=_updV-Uk2eHN1QK0GiVJv_hlvGefloxPZawisK-K9oA,20943
5
+ ai_rules/config.py,sha256=-AbRaAI6LsGjqOHaoHYUIN_KxkfLAtnmH-5ChEQXI4E,21041
6
6
  ai_rules/display.py,sha256=dltgyoJZSseP1xrj-YBvj8_TOBSTouoiQMjwdC5d2MI,1225
7
- ai_rules/mcp.py,sha256=jbUgRaIyglVfwIFu61Dbtsbbk_o87NCTYwVgz4VclQ8,11958
7
+ ai_rules/mcp.py,sha256=BFBD0MWEjbbGOjZmmLwE8atK0KN-ExylA7VeDAZnv-M,11942
8
+ ai_rules/profiles.py,sha256=YI_p39bvwhtUAcgt6vv27ZymWUBSsRNIY4LjDyFOomg,6223
9
+ ai_rules/state.py,sha256=3VHsdDVERMeNlNNK_s-NwjQKTjx5cH9qwu4Oh2_7W-A,1191
8
10
  ai_rules/symlinks.py,sha256=gXBVMcDU96pMHXR675ePZSnXKY9d0kEEwXnUazd0xhw,6674
11
+ ai_rules/utils.py,sha256=_mjJzcNbi3-zwQLGVdgqeVUv9eVj0SYI0xebZh3262E,1324
9
12
  ai_rules/agents/__init__.py,sha256=VG6BFISMVTETyfs7aAXTa_co6NH_dmvp_5aw09HI35w,32
10
13
  ai_rules/agents/base.py,sha256=vVPEkdczTz8oLUS5XdjHrJbARL2kDfaKDwQOVBX6bYI,1981
11
- ai_rules/agents/claude.py,sha256=5O4e_cOxBdPGYEkL-_pevQquSU7VRb9ohxx6Grn89ko,3756
12
- ai_rules/agents/goose.py,sha256=7zkXWtSZIdnfCEg3k_2-eUOL9bnIQ2HOInBupELRIrc,1077
14
+ ai_rules/agents/claude.py,sha256=XzlkW-E-4oZIWCZTvUihpzptPTiAKH23ADXlczdKHDM,3789
15
+ ai_rules/agents/cursor.py,sha256=PK7sl64UBVJBySRffAsdvo7Azj7lCEE9G-4NeuSNdKk,2105
16
+ ai_rules/agents/goose.py,sha256=omBEPTc0N8A0KenmDCNIkV5oqJsgeCEespJZVJakigw,1136
13
17
  ai_rules/agents/shared.py,sha256=Tj9ll1Z_wZzwM19368r2ku_6aTQYyNCwxzOc_4lXqqg,839
14
- ai_rules/bootstrap/__init__.py,sha256=HTykRNzbh8-kAByfRlvbeS0pefrbR4VlA4AS-5lwcpY,1840
18
+ ai_rules/bootstrap/__init__.py,sha256=zDqyT51J9K_8Ys2vORgXuPLMlS0c4vONpvlFDKoT5bE,1922
15
19
  ai_rules/bootstrap/config.py,sha256=Cp133LLC2xnyM-RRtmKsZ73cSMdm8GVzCmwHjFrcK1I,6970
16
- ai_rules/bootstrap/installer.py,sha256=5t0XJh4yKtaXxA5SnprK62eNalNUdrKBWQ-TFhIFpKg,6757
17
- ai_rules/bootstrap/updater.py,sha256=kW9YUy4DT1F1nNRIvjDHkiuNaLoFP7V4RhbDZO0TiKs,6202
20
+ ai_rules/bootstrap/installer.py,sha256=OrlVO6Bkky86_QE9f_UPD0Mol4k642sdDflf7DcegS0,9274
21
+ ai_rules/bootstrap/updater.py,sha256=vytyMV3FsJqJbR56vSU_lICVz4zc93pXorPJzK4CutE,10581
18
22
  ai_rules/bootstrap/version.py,sha256=WCj4LwzZk-M62ViOf6qg7mQPk-AlGswwHsJWfv9bEko,1236
19
- ai_rules/config/AGENTS.md,sha256=BKch0ZjjTZfzBRJ6n_UWrLfrWKu3WtNh7DR-4mRY2-w,11298
23
+ ai_rules/config/AGENTS.md,sha256=zwQCZ8DiK5t0FuXaLGhfDYl5tUro_32U-CrCL3KAY68,11616
20
24
  ai_rules/config/chat_agent_hints.md,sha256=lW3nCigFn4iuZ6vsCjntwd-Po7MVGFZynjR9X5__vY0,491
25
+ ai_rules/config/claude/CLAUDE.md,sha256=2BtrA5bS05_z_SQYFreWFVUJ-C9ryln1axNLOS44PD4,13
21
26
  ai_rules/config/claude/mcps.json,sha256=yj0WO6sFU4GCciYUBWjzvvfqrBh869doeOC2Pp5EI1Y,3
22
- ai_rules/config/claude/settings.json,sha256=Kf1U5_F6V-c15ipiwAPwmuqiv1esOvl_uEE8PdaiA4E,2926
27
+ ai_rules/config/claude/settings.json,sha256=MtJgKbyMgjx5fc8P0UVeePySXLJfsKKz9fkVcERp5Ok,2989
23
28
  ai_rules/config/claude/agents/code-reviewer.md,sha256=hFyRbuAzliQOmRVTTNAy1unV4Xx5kFb9JlWEwULL7nY,4417
29
+ ai_rules/config/claude/commands/agents-md.md,sha256=9ohpiGf0kC2KkqmkJmx-ZxwrLZwBQYeTZWP3bXily7c,13420
24
30
  ai_rules/config/claude/commands/annotate-changelog.md,sha256=a9AL9Os8VLScL0hryHmesrMdusG2z0TTxH6myeu4aVU,5573
25
31
  ai_rules/config/claude/commands/comment-cleanup.md,sha256=gNLc36PlplP08By-QK4n44gefgPusSQa0lnLQDGujBA,5296
26
32
  ai_rules/config/claude/commands/continue-crash.md,sha256=rRi16p7PyS9IYuanuTUCh6fMmXleSoqbNqZFWzoJR0s,1378
@@ -34,9 +40,14 @@ ai_rules/config/claude/skills/doc-writer/resources/templates.md,sha256=qGkqvY78N
34
40
  ai_rules/config/claude/skills/prompt-engineer/SKILL.md,sha256=TA6c1SUvJKy0pQnTwGGj_IbQEXqe4wUrFZJuaBVpNJ4,9664
35
41
  ai_rules/config/claude/skills/prompt-engineer/resources/prompt_engineering_guide_2025.md,sha256=h3WlOQTYn1zGymJGSoD6dHCZsOTCe9Tne9a3tuRJFFQ,29592
36
42
  ai_rules/config/claude/skills/prompt-engineer/resources/templates.md,sha256=4gIR1i5k-_GxWrZXStFyYAEfqZJvzh1KngZfq1T2pLY,6856
43
+ ai_rules/config/cursor/keybindings.json,sha256=nfCqj_G_WUjUNj6f3hkchMH6M-SHADhh9-GNS95ypEk,291
44
+ ai_rules/config/cursor/settings.json,sha256=uzPbbyVBXJlNl0EKupT-ACDyoMT3-KNhEKKrXtgLIfk,2488
45
+ ai_rules/config/goose/.goosehints,sha256=2BtrA5bS05_z_SQYFreWFVUJ-C9ryln1axNLOS44PD4,13
37
46
  ai_rules/config/goose/config.yaml,sha256=N_QzIANL_7WnX5xSrMZN_t94wbrGXXHMleQ6gzzIaC8,1396
38
- ai_agent_rules-0.11.0.dist-info/METADATA,sha256=nRRbjUL974E67Oz5UlCx1WYKfzUoochYPPOaqPGQiTI,12546
39
- ai_agent_rules-0.11.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
40
- ai_agent_rules-0.11.0.dist-info/entry_points.txt,sha256=Dnt2pQp6vSWz374RsUNZ5Zh5qmuTy45xR02r5fH_p_E,82
41
- ai_agent_rules-0.11.0.dist-info/top_level.txt,sha256=1jJObrxql_i5okMXb4dxsiQa_cpjtzwwskVvwedEHow,9
42
- ai_agent_rules-0.11.0.dist-info/RECORD,,
47
+ ai_rules/config/profiles/default.yaml,sha256=B1sjVz0V11gE1t3_XYXD5HpiaOWPPXEdzzotwVH8UmI,140
48
+ ai_rules/config/profiles/work.yaml,sha256=e982yg7kY6Mp0h6wE94m48s-mK_kpm7zAQ5OYyBIpBo,314
49
+ ai_agent_rules-0.15.8.dist-info/METADATA,sha256=doQIzH2GCY8A97ZVAhdlsfNFJLMgo7IbYvlqXBRq6GI,15749
50
+ ai_agent_rules-0.15.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
51
+ ai_agent_rules-0.15.8.dist-info/entry_points.txt,sha256=Dnt2pQp6vSWz374RsUNZ5Zh5qmuTy45xR02r5fH_p_E,82
52
+ ai_agent_rules-0.15.8.dist-info/top_level.txt,sha256=1jJObrxql_i5okMXb4dxsiQa_cpjtzwwskVvwedEHow,9
53
+ ai_agent_rules-0.15.8.dist-info/RECORD,,
ai_rules/agents/claude.py CHANGED
@@ -35,7 +35,9 @@ class ClaudeAgent(Agent):
35
35
  """Cached list of all Claude Code symlinks including dynamic agents/commands."""
36
36
  result = []
37
37
 
38
- result.append((Path("~/.claude/CLAUDE.md"), self.config_dir / "AGENTS.md"))
38
+ result.append(
39
+ (Path("~/.claude/CLAUDE.md"), self.config_dir / "claude" / "CLAUDE.md")
40
+ )
39
41
 
40
42
  settings_file = self.config_dir / "claude" / "settings.json"
41
43
  if settings_file.exists():
@@ -0,0 +1,70 @@
1
+ """Cursor editor agent implementation."""
2
+
3
+ import sys
4
+
5
+ from functools import cached_property
6
+ from pathlib import Path
7
+
8
+ from ai_rules.agents.base import Agent
9
+
10
+
11
+ def _get_cursor_target_prefix() -> str:
12
+ """Get platform-specific Cursor config path with ~ prefix.
13
+
14
+ Returns:
15
+ Path string with ~ prefix for the current platform:
16
+ - macOS: ~/Library/Application Support/Cursor/User
17
+ - Windows: ~/AppData/Roaming/Cursor/User
18
+ - Linux/WSL: ~/.config/Cursor/User
19
+ """
20
+ if sys.platform == "darwin":
21
+ return "~/Library/Application Support/Cursor/User"
22
+ elif sys.platform == "win32":
23
+ return "~/AppData/Roaming/Cursor/User"
24
+ else: # Linux/WSL
25
+ return "~/.config/Cursor/User"
26
+
27
+
28
+ class CursorAgent(Agent):
29
+ """Agent for Cursor editor configuration."""
30
+
31
+ @property
32
+ def name(self) -> str:
33
+ return "Cursor"
34
+
35
+ @property
36
+ def agent_id(self) -> str:
37
+ return "cursor"
38
+
39
+ @property
40
+ def config_file_name(self) -> str:
41
+ return "settings.json"
42
+
43
+ @property
44
+ def config_file_format(self) -> str:
45
+ return "json"
46
+
47
+ @cached_property
48
+ def symlinks(self) -> list[tuple[Path, Path]]:
49
+ """Cached list of all Cursor symlinks.
50
+
51
+ Settings file uses cache-based approach with override merging.
52
+ Keybindings file uses direct symlink (array structure, no merging).
53
+ """
54
+ result = []
55
+ prefix = _get_cursor_target_prefix()
56
+
57
+ # Settings file - use cache if overrides exist
58
+ settings_file = self.config_dir / "cursor" / "settings.json"
59
+ if settings_file.exists():
60
+ target_file = self.config.get_settings_file_for_symlink(
61
+ "cursor", settings_file
62
+ )
63
+ result.append((Path(f"{prefix}/settings.json"), target_file))
64
+
65
+ # Keybindings file - direct symlink (no override merging for arrays)
66
+ keybindings_file = self.config_dir / "cursor" / "keybindings.json"
67
+ if keybindings_file.exists():
68
+ result.append((Path(f"{prefix}/keybindings.json"), keybindings_file))
69
+
70
+ return result
ai_rules/agents/goose.py CHANGED
@@ -31,7 +31,10 @@ class GooseAgent(Agent):
31
31
  result = []
32
32
 
33
33
  result.append(
34
- (Path("~/.config/goose/.goosehints"), self.config_dir / "AGENTS.md")
34
+ (
35
+ Path("~/.config/goose/.goosehints"),
36
+ self.config_dir / "goose" / ".goosehints",
37
+ )
35
38
  )
36
39
 
37
40
  config_file = self.config_dir / "goose" / "config.yaml"
@@ -24,8 +24,10 @@ from .config import (
24
24
  )
25
25
  from .installer import (
26
26
  UV_NOT_FOUND_ERROR,
27
+ ToolSource,
27
28
  ensure_statusline_installed,
28
29
  get_tool_config_dir,
30
+ get_tool_source,
29
31
  get_tool_version,
30
32
  install_tool,
31
33
  is_command_available,
@@ -35,10 +37,10 @@ from .updater import (
35
37
  UPDATABLE_TOOLS,
36
38
  ToolSpec,
37
39
  UpdateInfo,
38
- check_pypi_updates,
40
+ check_index_updates,
39
41
  check_tool_updates,
40
42
  get_tool_by_id,
41
- perform_pypi_update,
43
+ perform_tool_upgrade,
42
44
  )
43
45
  from .version import get_package_version, is_newer, parse_version
44
46
 
@@ -47,8 +49,10 @@ __all__ = [
47
49
  "is_newer",
48
50
  "parse_version",
49
51
  "UV_NOT_FOUND_ERROR",
52
+ "ToolSource",
50
53
  "ensure_statusline_installed",
51
54
  "get_tool_config_dir",
55
+ "get_tool_source",
52
56
  "get_tool_version",
53
57
  "install_tool",
54
58
  "is_command_available",
@@ -56,10 +60,10 @@ __all__ = [
56
60
  "UPDATABLE_TOOLS",
57
61
  "ToolSpec",
58
62
  "UpdateInfo",
59
- "check_pypi_updates",
63
+ "check_index_updates",
60
64
  "check_tool_updates",
61
65
  "get_tool_by_id",
62
- "perform_pypi_update",
66
+ "perform_tool_upgrade",
63
67
  "AutoUpdateConfig",
64
68
  "clear_all_pending_updates",
65
69
  "clear_pending_update",
@@ -6,6 +6,7 @@ import shutil
6
6
  import subprocess
7
7
  import sys
8
8
 
9
+ from enum import Enum, auto
9
10
  from pathlib import Path
10
11
 
11
12
  if sys.version_info >= (3, 11):
@@ -13,7 +14,30 @@ if sys.version_info >= (3, 11):
13
14
  else:
14
15
  import tomli as tomllib
15
16
 
17
+
18
+ class ToolSource(Enum):
19
+ """Source from which a tool was installed."""
20
+
21
+ PYPI = auto()
22
+ GITHUB = auto()
23
+
24
+
25
+ def make_github_install_url(repo: str) -> str:
26
+ """Construct GitHub install URL for uv tool install.
27
+
28
+ Args:
29
+ repo: GitHub repository in format "owner/repo"
30
+
31
+ Returns:
32
+ Full git+ssh URL for uv tool install
33
+ """
34
+ return f"git+ssh://git@github.com/{repo}.git"
35
+
36
+
16
37
  UV_NOT_FOUND_ERROR = "uv not found in PATH. Install from https://docs.astral.sh/uv/"
38
+ PACKAGE_NAME = "ai-agent-rules"
39
+ GITHUB_REPO = "wpfleger96/ai-rules"
40
+ STATUSLINE_GITHUB_REPO = "wpfleger96/claude-code-status-line"
17
41
 
18
42
 
19
43
  def _validate_package_name(package_name: str) -> bool:
@@ -50,15 +74,15 @@ def get_tool_config_dir(package_name: str = "ai-agent-rules") -> Path:
50
74
  )
51
75
 
52
76
 
53
- def get_tool_source(package_name: str) -> str | None:
54
- """Detect how a uv tool was installed (PyPI vs local file).
77
+ def get_tool_source(package_name: str) -> ToolSource | None:
78
+ """Detect how a uv tool was installed.
55
79
 
56
80
  Args:
57
81
  package_name: Name of the uv tool package
58
82
 
59
83
  Returns:
60
- "pypi" if installed from PyPI (no path key in requirements)
61
- "local" if installed from local file (has path key)
84
+ ToolSource.PYPI if installed from PyPI
85
+ ToolSource.GITHUB if installed from GitHub
62
86
  None if tool not installed or receipt file not found
63
87
  """
64
88
  data_home = os.environ.get("XDG_DATA_HOME", str(Path.home() / ".local" / "share"))
@@ -75,12 +99,12 @@ def get_tool_source(package_name: str) -> str | None:
75
99
  if not requirements:
76
100
  return None
77
101
 
78
- # Check first requirement for path key (indicates local install)
79
102
  first_req = requirements[0]
80
- if isinstance(first_req, dict) and "path" in first_req:
81
- return "local"
103
+ if isinstance(first_req, dict):
104
+ if "git" in first_req and "github.com" in first_req["git"]:
105
+ return ToolSource.GITHUB
82
106
 
83
- return "pypi"
107
+ return ToolSource.PYPI
84
108
 
85
109
  except (OSError, tomllib.TOMLDecodeError, KeyError, IndexError):
86
110
  return None
@@ -100,28 +124,39 @@ def is_command_available(command: str) -> bool:
100
124
 
101
125
  def install_tool(
102
126
  package_name: str = "ai-agent-rules",
127
+ from_github: bool = False,
128
+ github_url: str | None = None,
103
129
  force: bool = False,
104
130
  dry_run: bool = False,
105
131
  ) -> tuple[bool, str]:
106
- """Install package as a uv tool from PyPI.
132
+ """Install package as a uv tool.
107
133
 
108
134
  Args:
109
- package_name: Name of package to install
135
+ package_name: Name of package to install (ignored if from_github=True)
136
+ from_github: Install from GitHub instead of PyPI
137
+ github_url: GitHub URL to install from (only used if from_github=True)
110
138
  force: Force reinstall if already installed
111
139
  dry_run: Show what would be done without executing
112
140
 
113
141
  Returns:
114
142
  Tuple of (success, message)
115
143
  """
116
- if not _validate_package_name(package_name):
144
+ if not from_github and not _validate_package_name(package_name):
117
145
  return False, f"Invalid package name: {package_name}"
118
146
 
119
147
  if not is_command_available("uv"):
120
148
  return False, UV_NOT_FOUND_ERROR
121
149
 
122
- cmd = ["uv", "tool", "install", package_name]
150
+ if from_github:
151
+ source = github_url if github_url else make_github_install_url(GITHUB_REPO)
152
+ else:
153
+ source = package_name
154
+ cmd = ["uv", "tool", "install", source]
155
+
123
156
  if force:
124
157
  cmd.insert(3, "--force")
158
+ if from_github:
159
+ cmd.insert(4, "--reinstall")
125
160
 
126
161
  if dry_run:
127
162
  return True, f"Would run: {' '.join(cmd)}"
@@ -227,23 +262,60 @@ def get_tool_version(tool_name: str) -> str | None:
227
262
  return None
228
263
 
229
264
 
230
- def ensure_statusline_installed(dry_run: bool = False) -> str:
231
- """Install claude-code-statusline if not already present. Fails open.
265
+ def ensure_statusline_installed(
266
+ dry_run: bool = False, from_github: bool = False
267
+ ) -> tuple[str, str | None]:
268
+ """Install or upgrade claude-code-statusline if needed. Fails open.
232
269
 
233
270
  Args:
234
- dry_run: If True, skip installation
271
+ dry_run: If True, show what would be done without executing
272
+ from_github: Install from GitHub instead of PyPI
235
273
 
236
274
  Returns:
237
- Status: "already_installed", "installed", "failed", or "skipped"
275
+ Tuple of (status, message) where status is:
276
+ "already_installed", "installed", "upgraded", "upgrade_available", "failed", or "skipped"
277
+ Message is only provided in dry_run mode or when upgraded
238
278
  """
239
279
  if is_command_available("claude-statusline"):
240
- return "already_installed"
241
-
242
- if dry_run:
243
- return "skipped"
280
+ try:
281
+ from ai_rules.bootstrap.updater import (
282
+ check_tool_updates,
283
+ get_tool_by_id,
284
+ perform_tool_upgrade,
285
+ )
286
+
287
+ statusline_tool = get_tool_by_id("statusline")
288
+ if statusline_tool:
289
+ update_info = check_tool_updates(statusline_tool, timeout=10)
290
+ if update_info and update_info.has_update:
291
+ if dry_run:
292
+ return (
293
+ "upgrade_available",
294
+ f"Would upgrade statusline {update_info.current_version} → {update_info.latest_version}",
295
+ )
296
+ success, msg, _ = perform_tool_upgrade(statusline_tool)
297
+ if success:
298
+ return (
299
+ "upgraded",
300
+ f"{update_info.current_version} → {update_info.latest_version}",
301
+ )
302
+ except Exception:
303
+ pass
304
+ return "already_installed", None
244
305
 
245
306
  try:
246
- success, _ = install_tool("claude-code-statusline", force=False, dry_run=False)
247
- return "installed" if success else "failed"
307
+ success, message = install_tool(
308
+ "claude-code-statusline",
309
+ from_github=from_github,
310
+ github_url=make_github_install_url(STATUSLINE_GITHUB_REPO)
311
+ if from_github
312
+ else None,
313
+ force=False,
314
+ dry_run=dry_run,
315
+ )
316
+ if success:
317
+ return "installed", message if dry_run else None
318
+ else:
319
+ return "failed", None
248
320
  except Exception:
249
- return "failed"
321
+ return "failed", None